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