@curvenote/schema
Version:
Schema and markdown parser for @curvenote/editor
328 lines • 12.4 kB
JavaScript
import { tableNodes } from 'prosemirror-tables';
import { Fragment } from 'prosemirror-model';
import { nodeNames } from '../types';
import { NodeGroups } from './types';
import { writeDirectiveOptions } from '../serialize/markdown/utils';
import { indent } from '../serialize/indent';
import { writeMdastSnippet, getColumnWidths, hasFancyTable, renderPColumn } from './utils';
export const nodes = tableNodes({
tableGroup: NodeGroups.top,
cellContent: NodeGroups.blockOrEquation,
cellAttributes: {
background: {
default: null,
getFromDOM(dom) {
return dom.style.backgroundColor || null;
},
setDOMAttr(value, attrs) {
// eslint-disable-next-line prefer-template
if (value)
attrs.style = (attrs.style || '') + `background-color: ${value};`;
},
},
},
});
nodes.table.attrsFromMyst = () => ({});
nodes.table.toMyst = (props) => {
var _a;
if (((_a = props.children) === null || _a === void 0 ? void 0 : _a.length) === 1 && props.children[0].type === 'table') {
return props.children[0];
}
return { type: 'table', align: undefined, children: (props.children || []) };
};
nodes.table_row.attrsFromMyst = () => ({});
nodes.table_row.toMyst = (props) => ({
type: 'tableRow',
children: (props.children || []),
});
function ifGreaterThanOne(num) {
if (!num)
return undefined;
return num === 1 ? undefined : num;
}
nodes.table_header.attrsFromMyst = () => ({});
nodes.table_header.toMyst = (props) => ({
type: 'tableCell',
header: true,
align: props.align || undefined,
colspan: ifGreaterThanOne(props.colspan),
rowspan: ifGreaterThanOne(props.rowspan),
children: (props.children || []),
});
nodes.table_cell.attrsFromMyst = () => ({});
nodes.table_cell.toMyst = (props) => ({
type: 'tableCell',
header: undefined,
align: props.align || undefined,
colspan: ifGreaterThanOne(props.colspan),
rowspan: ifGreaterThanOne(props.rowspan),
children: (props.children || []),
});
/**
* Create a "row" using a list-table
* ```text
* * - Col1
* - Col2
* ```
*/
const renderListTableRow = (state, row) => {
state.write('* ');
const dedent = indent(state);
row.content.forEach((cell) => {
// TODO: make the lists tight!
state.wrapBlock(' ', '- ', cell, () => {
// Delete empty paragraph nodes and trim leading newlines to single newline.
const children = [];
cell.forEach((child) => {
var _a, _b;
if (child.type.name === nodeNames.paragraph) {
if (child.childCount === 0) {
return;
}
if (((_a = child.firstChild) === null || _a === void 0 ? void 0 : _a.type.name) === nodeNames.text) {
child.firstChild.text = (_b = child.firstChild.text) === null || _b === void 0 ? void 0 : _b.replace(/^\n+/, '\n');
}
}
children.push(child);
});
cell = cell.copy(Fragment.from(children));
return state.renderContent(cell);
});
// const atBlank = /(\n\n)$/.test(state.out as string);
// if (state.options.tightLists && atBlank && state.out) {
// // Remove the trailing new line on `state.out` to make it tight
// state.out = state.out.replace(/\n\n$/, '');
// }
});
dedent();
};
export const toListTable = (state, node, figure, index) => {
// Use ~~~ for fence, as tables often have captions with roles/citations
state.write('~~~{list-table}');
if (state.nextTableCaption) {
state.write(' ');
state.renderInline(state.nextTableCaption);
}
state.ensureNewLine();
const opts = { 'header-rows': 1, name: state.nextCaptionId };
writeDirectiveOptions(state, opts);
node.content.forEach((row) => {
renderListTableRow(state, row, figure, index);
});
state.write('~~~');
state.closeBlock(node);
};
export const toGFMMarkdownTable = (state, node) => {
let rowIndex = 0;
node.content.forEach((child) => {
if (child.type.name === nodeNames.table_row) {
let isHeader = false;
let columnIndex = 0;
state.write('| ');
// Create a fake header and switch `|---|` code off below
child.content.forEach((cell) => {
if (columnIndex === 0 && rowIndex === 0) {
if (cell.type.name === nodeNames.table_header) {
// mark this row as header row to append header string after this row before the second row rendering
isHeader = true;
}
else {
// Creates placeholder header with header seperator
// | Column 1 | Column 2 |
// |---|---|
let headerStr = '|';
let counter = 0;
child.content.forEach(() => {
counter += 1;
headerStr += `Column ${counter} |`;
});
headerStr += '\n|';
child.content.forEach(() => {
headerStr += '---|';
});
headerStr += '\n';
state.write(headerStr);
}
}
if (cell.type.name === nodeNames.table_cell || cell.type.name === nodeNames.table_header) {
const columnCount = Number(cell.attrs.colspan);
if (columnCount > 1) {
// Duplicate the content across columns
for (let i = 0; i < columnCount; i += 1) {
cell.content.forEach((content) => {
state.renderInline(content);
state.write(' ');
});
state.write(' |');
}
}
else {
state.write(' ');
cell.content.forEach((content) => {
state.renderInline(content);
});
state.write(' |');
}
}
columnIndex += 1;
});
if (isHeader) {
isHeader = false;
state.ensureNewLine();
state.write('|');
child.content.forEach((cell) => {
if (cell.type.name === nodeNames.table_header) {
state.write('---|');
}
});
}
state.ensureNewLine();
}
rowIndex += 1;
});
state.closeBlock(node);
};
function renderTableCell(state, cell, i, spanIdx, widths, childCount) {
let renderedSpan = 1;
const { attrs: { colspan }, } = cell;
if (colspan > 1) {
let width = 0;
for (let j = 0; j < colspan; j += 1) {
width += widths[spanIdx + j];
}
state.write(`\\multicolumn{${colspan}}{${renderPColumn(width)}}{`);
renderedSpan = colspan;
}
if (cell.content.childCount === 1 && cell.content.child(0).type.name === nodeNames.paragraph) {
// Render simple things inline, otherwise render a block
state.renderInline(cell.content.child(0));
}
else {
cell.content.forEach((content, index) => {
state.render(content, cell, index);
});
}
if (colspan > 1)
state.write('}');
if (i < childCount - 1) {
state.write(' & ');
}
return renderedSpan;
}
/**
* convert prosemirror table node into latex table
*/
export function renderNodeToLatex(state, node) {
const { widths, columnSpec, numColumns } = getColumnWidths(node);
if (!numColumns) {
throw new Error('invalid table format, no columns');
}
state.isInTable = true; // this is cleared at the end of this function
state.ensureNewLine();
// if not in a longtable environment already (these replace the figure environment)
let dedent;
// handle initial headers first
let numHeaderRowsFound = 0;
if (state.longFigure) {
state.ensureNewLine();
state.write('\\hline');
state.ensureNewLine();
let endHeader = false;
// write the first header section
node.content.forEach(({ content: rowContent }) => {
var _a, _b;
if (endHeader)
return;
if (((_a = rowContent.firstChild) === null || _a === void 0 ? void 0 : _a.type.name) === nodeNames.table_header) {
numHeaderRowsFound += 1;
let spanIdx = 0;
rowContent.forEach((cell, _, i) => {
spanIdx += renderTableCell(state, cell, i, spanIdx, widths, rowContent.childCount);
});
state.write(' \\\\');
state.ensureNewLine();
}
if (((_b = rowContent.firstChild) === null || _b === void 0 ? void 0 : _b.type.name) !== nodeNames.table_header) {
endHeader = true;
}
});
if (numHeaderRowsFound > 0) {
state.ensureNewLine();
state.write('\\hline');
state.ensureNewLine();
state.write('\\endfirsthead');
state.ensureNewLine();
state.write('\\hline');
state.ensureNewLine();
// write the continuation header section
state.write(`\\multicolumn{${numColumns}}{c}{\\tablename\\ \\thetable\\ -- \\textit{Continued from previous page}}\\\\`);
state.ensureNewLine();
node.content.forEach(({ content: rowContent }, offset, index) => {
if (index >= numHeaderRowsFound)
return;
let spanIdx = 0;
rowContent.forEach((cell, _, i) => {
spanIdx += renderTableCell(state, cell, i, spanIdx, widths, rowContent.childCount);
});
state.write(' \\\\');
state.ensureNewLine();
});
state.ensureNewLine();
state.write('\\hline');
state.ensureNewLine();
state.write('\\endhead');
state.ensureNewLine();
}
}
else {
state.write(`\\begin{tabular}{${columnSpec}}`);
state.ensureNewLine();
dedent = indent(state);
state.write(`\\toprule`);
state.ensureNewLine();
}
// todo: can we use offset and index to better handle row and column spans?
node.content.forEach(({ content: rowContent }, offset, index) => {
var _a;
if (index < numHeaderRowsFound)
return; // skip the header rows
let spanIdx = 0;
rowContent.forEach((cell, _, i) => {
spanIdx += renderTableCell(state, cell, i, spanIdx, widths, rowContent.childCount);
});
state.write(' \\\\');
state.ensureNewLine();
// If the first cell in this row is a table header, make a line
if (((_a = rowContent.firstChild) === null || _a === void 0 ? void 0 : _a.type.name) === nodeNames.table_header) {
state.write('\\hline');
state.ensureNewLine();
}
});
if (state.longFigure) {
state.write('\\hline');
}
else {
state.write('\\bottomrule');
state.ensureNewLine();
dedent === null || dedent === void 0 ? void 0 : dedent();
state.write('\\end{tabular}');
}
state.closeBlock(node);
state.isInTable = false;
}
export const toMarkdown = (state, node, figure, index) => {
if (node && hasFancyTable(node)) {
writeMdastSnippet(state, node);
return;
}
toListTable(state, node, figure, index);
};
export const toTex = (state, node) => {
try {
renderNodeToLatex(state, node);
}
catch (e) {
state.write(`{\\bf Error converting \`${node.type.name}' to \\LaTeX}`);
}
};
//# sourceMappingURL=table.js.map