@felisdiligens/md-table-tools
Version:
MultiMarkdown table tools
380 lines (312 loc) • 13.5 kB
text/typescript
import {
Table,
TableCaption,
TableCaptionPosition,
TableCell,
TableCellMerge,
TableRow,
TextAlignment,
} from "./table.js";
import { TableParser } from "./tableParser.js";
import { TableRenderer } from "./tableRenderer.js";
import stringWidth from "string-width";
/*
Syntax: https://www.dokuwiki.org/wiki:syntax#tables
*/
function dokuWikiToMarkdown(markup: string): string {
// Image:
markup = markup.replace(/{{([^\|]*?)\|?([^\|]*?)}}/g, "");
// Links:
markup = markup.replace(/\[\[([^\|]*?)\|?([^\|]*?)\]\]/g, "[$2]($1)");
// Block code:
markup = markup.replace(/<code\s+?([a-zA-Z0-9]*?)>\n(.*?)<\/code>/gs, "```$1\n$2```");
// Inline code / monospaced text:
markup = markup.replace(/''(.*?)''/g, "`$1`");
// Oblique:
markup = markup.replace(/\/\/\*\*(.*?)\*\*\/\//g, "***$1***");
markup = markup.replace(/\*\*\/\/(.*?)\/\/\*\*/g, "***$1***");
// Italic:
markup = markup.replace(/\/\/(.*?)\/\//g, "*$1*");
// Underlined:
markup = markup.replace(/__(.*?)__/g, "<u>$1</u>");
// Newlines:
markup = markup.replace(/\\\\ /g, "\n");
return markup;
}
function markdownToDokuWiki(markup: string): string {
// Image:
markup = markup.replace(/!\[([^\[\]]+)\]\(([^\(\)]+)\)/g, "{{$2|$1}}");
// Links:
markup = markup.replace(/\[([^\[\]]+)\]\(([^\(\)]+)\)/g, "[[$2|$1]]");
// Block code:
markup = markup.replace(/```([a-zA-Z0-9]*?)\n(.*?)\n```/gs, "<code $1>\n$2\n</code>");
// Inline code / monospaced text:
markup = markup.replace(/`(.*?)`/g, "''$1''");
// Strikethrough:
markup = markup.replace(/~~(.*?)~~/g, "<del>$1</del>");
// Oblique:
markup = markup.replace(/___(.*?)___/g, "//**$1**//");
markup = markup.replace(/\*\*\*(.*?)\*\*\*/g, "//**$1**//");
// Bold:
markup = markup.replace(/__(.*?)__/g, "**$1**");
// Italic:
markup = markup.replace(/_(.*?)_/g, "//$1//");
markup = markup.replace(/\*(.*?)\*/g, "//$1//");
// Underlined:
markup = markup.replace(/<ins>(.*?)<\/ins>/g, "__$1__");
markup = markup.replace(/<u>(.*?)<\/u>/g, "__$1__");
// Superscript:
markup = markup.replace(/\^(.*?)\^/g, "<sup>$1</sup>");
// Subscript:
markup = markup.replace(/~(.*?)~/g, "<sub>$1</sub>");
// Escaped characters:
markup = markup.replace(/\\([#\.\|\*_\s`\[\]\-])/g, "$1");
// Newlines:
markup = markup.replace(/<br\s?\/?>/g, "\\\\ ");
markup = markup.replace(/\r?\n/g, "\\\\ ");
return markup;
}
const rowRegex = /^[\|\^](.+)[\|\^]$/;
enum ParsingState {
BeforeTable,
Row,
AfterTable,
}
export class DokuWikiTableParser implements TableParser {
public constructor(
/** If true, converts DokuWiki syntax to Markdown syntax */
public convertMarkup: boolean = true
) {}
parse(table: string): Table {
let parsedTable = new Table();
let state = ParsingState.BeforeTable;
let beforeTable = [];
let afterTable = [];
// Parse line by line:
for (let line of table.trim().split("\n")) {
// Check if we are in the table:
if (state == ParsingState.BeforeTable && line.match(rowRegex)) {
state = ParsingState.Row;
}
// Check if we are no longer in the table:
if (state != ParsingState.BeforeTable && state != ParsingState.AfterTable && !line.match(rowRegex)) {
state = ParsingState.AfterTable;
}
// If not inside table:
if (state == ParsingState.BeforeTable) {
beforeTable.push(line);
continue; // Skip the rest
} else if (state == ParsingState.AfterTable) {
afterTable.push(line);
continue; // Skip the rest
}
// Parse row:
if (state == ParsingState.Row) {
let tableRow = new TableRow();
parsedTable.addRow(-1, tableRow);
// Parse each character:
let cellContent = "";
let colIndex = 0;
let spaces = 0;
let leftHasSpaces = false;
let rightHasSpaces = false;
let foundCellContent = false;
let cellIsHeader = line.trim()[0] == "^";
let rowIsHeader = line.trim()[0] == "^";
for (let char of line.trim().substring(1, line.length)) {
if (char == "|" || char == "^") {
let tableColumn = parsedTable.getColumn(colIndex);
if (!tableColumn) tableColumn = parsedTable.addColumn();
let cell = new TableCell(parsedTable, tableRow, tableColumn);
parsedTable.addCell(cell);
if (foundCellContent && spaces >= 2) {
rightHasSpaces = true;
}
cell.isHeader = cellIsHeader;
if (leftHasSpaces && rightHasSpaces) {
if (tableColumn.textAlign == TextAlignment.default) {
tableColumn.textAlign = TextAlignment.center;
}
cell.textAlign = TextAlignment.center;
} else if (leftHasSpaces) {
if (tableColumn.textAlign == TextAlignment.default) {
tableColumn.textAlign = TextAlignment.right;
}
cell.textAlign = TextAlignment.right;
} else if (rightHasSpaces) {
if (tableColumn.textAlign == TextAlignment.default) {
tableColumn.textAlign = TextAlignment.left;
}
cell.textAlign = TextAlignment.left;
}
if (cellContent.trim() == ":::") {
cell.merged = TableCellMerge.above;
} else if (cellContent === "") {
cell.merged = TableCellMerge.left;
} else {
cellContent = this.convertMarkup
? dokuWikiToMarkdown(cellContent.trim())
: cellContent.trim();
cell.setText(cellContent);
}
cellContent = "";
colIndex++;
if (char == "|") {
cellIsHeader = false;
rowIsHeader = false;
} else if (char == "^") {
cellIsHeader = true;
}
spaces = 0;
leftHasSpaces = false;
rightHasSpaces = false;
foundCellContent = false;
} else if (char == " ") {
spaces++;
cellContent += char;
} else {
if (!foundCellContent && spaces >= 2) {
leftHasSpaces = true;
}
cellContent += char;
spaces = 0;
foundCellContent = true;
}
}
// Insert empty cells if missing:
for (; colIndex < parsedTable.columnCount(); colIndex++) {
let cell = new TableCell(parsedTable, tableRow, parsedTable.getColumn(colIndex));
parsedTable.addCell(cell);
}
tableRow.isHeader = rowIsHeader;
}
}
parsedTable.beforeTable = beforeTable.join("\n");
parsedTable.afterTable = afterTable.join("\n");
return parsedTable.update();
}
}
export class DokuWikiTableRenderer implements TableRenderer {
public constructor(
/** If true, converts Markdown syntax to DokuWiki syntax */
public convertMarkup: boolean = true
) {}
render(table: Table): string {
const rows = table.getRows();
const columnWidths = this.determineColumnWidths(table);
let result: string[] = [];
if (table.beforeTable.trim() !== "") {
result.push(table.beforeTable);
}
// DokuWiki has no table caption -- insert "caption" (if position is top):
if (table.caption && table.caption.position == TableCaptionPosition.top) {
result.push(this.renderCaption(table.caption));
}
// Rows:
for (const row of rows) {
result.push(this.renderRow(table, row, columnWidths));
}
// DokuWiki has no table caption -- insert "caption" (if position is bottom):
if (table.caption && table.caption.position == TableCaptionPosition.bottom) {
result.push(this.renderCaption(table.caption));
}
if (table.afterTable.trim() !== "") {
result.push(table.afterTable);
}
return result.join("\n");
}
private renderCaption(caption: TableCaption): string {
let result: string = "";
if (caption.position == TableCaptionPosition.bottom) {
result += "\n";
}
result += "> " + caption.text;
if (caption.position == TableCaptionPosition.top) {
result += "\n";
}
return result;
}
private renderRow(table: Table, row: TableRow, columnWidths: number[]): string {
let result: string[] = [];
let lastCellIsHeader = false;
table.getCellsInRow(row).forEach((cell, i) => {
let colspan = cell.getColspan();
let cellWidth = columnWidths[i];
if (colspan > 1) {
for (let col = i + 1; col < i + colspan; col++) {
cellWidth += columnWidths[col];
}
cellWidth += colspan * 2 - 2;
}
lastCellIsHeader = cell.isHeaderCell();
result.push(this.renderCell(cell, colspan, cellWidth));
});
if (lastCellIsHeader) {
return `${result.join("")}^`;
} else {
return `${result.join("")}|`;
}
}
private renderCell(cell: TableCell, colspan: number = 1, cellWidth: number = -1): string {
let isHeader = cell.isHeaderCell();
let separator = isHeader ? "^" : "|";
if (cell.merged == TableCellMerge.left) {
return separator;
}
// `:::` => merge cell above
// `\\ ` => insert a newline
let text = cell.merged == TableCellMerge.above ? ":::" : this.renderText(cell.text);
const textLength = stringWidth(text);
switch (cell.getTextAlignment()) {
case TextAlignment.center:
return `${separator}${" ".repeat(
Math.max(0, Math.floor((cellWidth - textLength + colspan - 1) / 2))
)} ${text} ${" ".repeat(Math.max(0, Math.ceil((cellWidth - textLength - colspan + 1) / 2)))}`;
case TextAlignment.right:
return `${separator}${" ".repeat(Math.max(0, cellWidth - textLength))} ${text} `;
case TextAlignment.left:
case TextAlignment.default:
default:
return `${separator} ${text} ${" ".repeat(Math.max(0, cellWidth - textLength))}`;
}
}
private renderText(text: string) {
return this.convertMarkup ? markdownToDokuWiki(text) : text;
}
private determineColumnWidths(table: Table): number[] {
let columnWidths: number[] = Array.from({ length: table.columnCount() }, () => 0);
for (let colIndex = table.columnCount() - 1; colIndex >= 0; colIndex--) {
const column = table.getColumn(colIndex);
let colWidth = 0;
for (const cell of table.getCellsInColumn(column)) {
let colspan = cell.getColspan();
let cellWidth = cell.merged == TableCellMerge.above ? 3 : stringWidth(this.renderText(cell.text));
// Is alignment spacing needed?
switch (cell.getTextAlignment()) {
case TextAlignment.center:
// "Add two spaces at least at both ends for centered text"
cellWidth += 2;
break;
case TextAlignment.right:
case TextAlignment.left:
// "Add two spaces on the left to align right, two spaces on the right to align left"
cellWidth += 1;
break;
case TextAlignment.default:
default:
break;
}
if (colspan == 1) {
colWidth = Math.max(cellWidth, colWidth);
} else {
let leftoverWidth = columnWidths
.slice(colIndex + 1, colIndex + colspan)
.reduce((pv, cv) => pv + cv);
colWidth = Math.max(cellWidth - leftoverWidth, colWidth);
}
}
columnWidths.splice(colIndex, 1, colWidth);
}
return columnWidths;
}
}