1/* 2 * table-pandoc.ts 3 * 4 * Copyright (C) 2021 by RStudio, PBC 5 * 6 * Unless you have received this program directly from RStudio pursuant 7 * to the terms of a commercial license agreement with RStudio, then 8 * this program is licensed to you under the terms of version 3 of the 9 * GNU Affero General Public License. This program is distributed WITHOUT 10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, 11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the 12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. 13 * 14 */ 15 16import { Schema, NodeType, Node as ProsemirrorNode, Fragment } from 'prosemirror-model'; 17 18import { ProsemirrorWriter, PandocToken, PandocTokenType, PandocOutput } from '../../api/pandoc'; 19 20import { CssAlignment } from './table-commands'; 21import { tableColumnAlignments, tableColumnWidths } from './table-columns'; 22 23// attributes 24const kTableAttr = 0; 25 26// caption 27const kTableCaption = 1; 28const kTableCaptionShort = 0; // [Inline] 29const kTableCaptionFull = 1; // [Block] 30 31// columdefs 32const kTableColSpec = 2; 33const kTableColSpecAlign = 0; 34const kTableColSpecWidth = 1; 35 36// table head 37const kTableHead = 3; 38const kTableHeadAttr = 0; 39const kTableHeadRows = 1; // [Row] 40 41// table body 42const kTableBody = 4; 43const kTableBodyAttr = 0; 44const kTableBodyRowHeadNumColumns = 1; 45const kTableBodyRowHead = 2; 46const kTableBodyRows = 3; // [Row] 47 48// table foot 49const kTableFoot = 5; 50const kTableFootAttr = 0; 51const kTableFootRows = 1; // [Row] 52 53// table row 54const kTableRowAttr = 0; 55const kTableRowCells = 1; // [Cell] 56 57// table cell 58const kTableCellAttr = 0; 59const kTableCellAlignments = 1; 60const kTableCellRowSpan = 2; 61const kTableCellColSpan = 3; 62const KTableCellContents = 4; // [Block] 63 64 65export function readPandocTable(schema: Schema) { 66 return (writer: ProsemirrorWriter, tok: PandocToken) => { 67 // get alignments and columns widths 68 const alignments = columnCssAlignments(tok); 69 const colpercents = columnPercents(tok); 70 71 // helper function to parse a table row 72 const parseRow = (row: any[], cellType: NodeType) => { 73 const cells: any[] = row[kTableRowCells]; 74 if (cells.length) { 75 writer.openNode(schema.nodes.table_row, {}); 76 cells.forEach((cell: any[], i) => { 77 writer.openNode(cellType, { align: alignments[i] }); 78 writer.writeTokens(cell[KTableCellContents]); 79 writer.closeNode(); 80 }); 81 writer.closeNode(); 82 } 83 }; 84 85 // open table container node 86 writer.openNode(schema.nodes.table_container, {}); 87 88 // open table node 89 writer.openNode(schema.nodes.table, { colpercents }); 90 91 // parse column headers 92 const head = tok.c[kTableHead] as any[]; 93 const firstRow = head[kTableHeadRows][0]; 94 if (firstRow && firstRow[kTableRowCells].some((cell: any[]) => cell[KTableCellContents].length > 0)) { 95 parseRow(firstRow, schema.nodes.table_header); 96 } 97 98 // parse table rows 99 const body = tok.c[kTableBody][0] as any[]; 100 body[kTableBodyRows].forEach((row: any[]) => { 101 parseRow(row, schema.nodes.table_cell); 102 }); 103 104 // close table node 105 writer.closeNode(); 106 107 // read caption 108 const caption = tok.c[kTableCaption][kTableCaptionFull]; 109 const captionBlock: PandocToken[] = caption.length ? caption[0].c : []; 110 writer.openNode(schema.nodes.table_caption, { inactive: captionBlock.length === 0 }); 111 writer.writeTokens(captionBlock); 112 writer.closeNode(); 113 114 // close table container node 115 writer.closeNode(); 116 }; 117} 118 119export function writePandocTableContainer(output: PandocOutput, node: ProsemirrorNode) { 120 const caption = node.lastChild!; 121 const table = node.firstChild!; 122 123 output.writeToken(PandocTokenType.Table, () => { 124 125 // write empty attributes 126 output.writeAttr(); 127 128 // write caption 129 output.writeNode(caption); 130 131 // write table 132 output.writeNode(table); 133 }); 134} 135 136export function writePandocTable(output: PandocOutput, node: ProsemirrorNode) { 137 const firstRow = node.firstChild!; 138 139 // get alignments and column widths 140 const alignments = tableColumnAlignments(node); 141 const widths = tableColumnWidths(node); 142 143 // write colspcs 144 // TODO: Columns are coming out ColWidthDefault 145 output.writeArray(() => { 146 alignments.forEach((align, i) => { 147 output.writeArray(() => { 148 output.writeToken(align); 149 if (widths[i] === 0) { 150 output.writeToken(PandocTokenType.ColWidthDefault); 151 } else { 152 output.writeToken(PandocTokenType.ColWidth, widths[i]); 153 } 154 }); 155 }); 156 }); 157 158 159 // write header row if necessary 160 const headerCut = firstRow.firstChild!.type === node.type.schema.nodes.table_header ? 1 : 0; 161 output.writeArray(() => { 162 output.writeAttr(); 163 output.writeArray(() => { 164 writePandocTableRow(output, firstRow, headerCut === 0); 165 }); 166 }); 167 168 // write table body 169 output.writeArray(() => { 170 output.writeArray(() => { 171 output.writeAttr(); 172 output.write(0); 173 output.writeArray(() => { /* */ }); 174 // write rows 175 output.writeArray(() => { 176 for (let i = headerCut; i < node.childCount; i++) { 177 writePandocTableRow(output, node.content.child(i)); 178 } 179 }); 180 }); 181 }); 182 183 // write table footer 184 output.writeArray(() => { 185 output.writeAttr(); 186 output.writeArray(() => { /* */ }); 187 }); 188 189 190} 191 192export function writePandocTableCaption(output: PandocOutput, node: ProsemirrorNode) { 193 output.writeArray(() => { 194 output.write(null); 195 output.writeArray(() => { 196 if (!node.attrs.inactive && node.childCount > 0) { 197 output.writeToken(PandocTokenType.Plain, () => { 198 output.writeInlines(node.content); 199 }); 200 } 201 }); 202 }); 203} 204 205export function writePandocTableNodes(output: PandocOutput, node: ProsemirrorNode) { 206 output.writeArray(() => { 207 output.writeNodes(node); 208 }); 209} 210 211export function writePandocTableHeaderNodes(output: PandocOutput, node: ProsemirrorNode) { 212 output.writeArray(() => { 213 if (node.textContent.length > 0) { 214 output.writeNodes(node); 215 } else { 216 // write a paragraph containing a space (this is an attempt to fix an issue where 217 // empty headers don't get correct round-tripping) 218 output.writeToken(PandocTokenType.Para, () => { 219 output.writeRawMarkdown(' '); 220 }); 221 } 222 }); 223} 224 225function writePandocTableRow(output: PandocOutput, node: ProsemirrorNode, empty = false) { 226 output.writeArray(() => { 227 output.writeAttr(); 228 output.writeArray(() => { 229 node.forEach((cellNode) => { 230 output.writeArray(() => { 231 output.writeAttr(); 232 output.writeToken(PandocTokenType.AlignDefault); 233 output.write(1); 234 output.write(1); 235 if (!empty) { 236 output.writeNode(cellNode); 237 } else { 238 output.writeArray(() => { 239 output.writeInlines(Fragment.empty); 240 }); 241 } 242 }); 243 }); 244 }); 245 }); 246} 247 248 249function columnCssAlignments(tableToken: PandocToken) { 250 return tableToken.c[kTableColSpec].map((spec: any) => { 251 const alignment = spec[kTableColSpecAlign]; 252 switch (alignment.t) { 253 case PandocTokenType.AlignLeft: 254 return CssAlignment.Left; 255 case PandocTokenType.AlignRight: 256 return CssAlignment.Right; 257 case PandocTokenType.AlignCenter: 258 return CssAlignment.Center; 259 case PandocTokenType.AlignDefault: 260 default: 261 return null; 262 } 263 }); 264} 265 266function columnPercents(tableToken: PandocToken): number[] { 267 return tableToken.c[kTableColSpec].map((spec: any) => { 268 const width = spec[kTableColSpecWidth]; 269 return width.t === PandocTokenType.ColWidth ? width.c : 0; 270 }); 271} 272