UNPKG

@diplodoc/transform

Version:

A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML

406 lines 16.6 kB
"use strict"; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; const attrs_1 = require("./attrs"); const pluginName = 'yfm_table'; const pipeChar = 0x7c; // | const apostropheChar = 0x60; // ` const hashChar = 0x23; // # const backSlashChar = 0x5c; // \ const curlyBraceOpen = 123; const curlyBraceClose = 125; const checkCharsOrder = (order, src, pos) => { const currentOrder = [...order]; const currentSrc = src.slice(pos); for (let i = 0; i < currentOrder.length; i++) { const rowSymbol = currentSrc.charCodeAt(i); const orderSymbol = currentOrder[i]; if (rowSymbol !== orderSymbol) { return false; } } return true; }; const liquidVariableStartOrder = [curlyBraceOpen, curlyBraceOpen]; const isLiquidVariableStart = (src, pos) => checkCharsOrder(liquidVariableStartOrder, src, pos); const liquidVariableEndOrder = [curlyBraceClose, curlyBraceClose]; const isLiquidVariableEnd = (src, pos) => checkCharsOrder(liquidVariableEndOrder, src, pos); const codeBlockOrder = [apostropheChar, apostropheChar, apostropheChar]; const isCodeBlockOrder = (src, pos) => checkCharsOrder(codeBlockOrder, src, pos); const openTableOrder = [hashChar, pipeChar]; const isOpenTableOrder = (src, pos) => checkCharsOrder(openTableOrder, src, pos); const notEscaped = (src, pos) => src.charCodeAt(pos - 1) !== backSlashChar; const rowStartOrder = [pipeChar, pipeChar]; const isRowOrder = (src, pos) => checkCharsOrder(rowStartOrder, src, pos) && notEscaped(src, pos); const cellStartOrder = [pipeChar]; const isCellOrder = (src, pos) => checkCharsOrder(cellStartOrder, src, pos) && notEscaped(src, pos) && !isRowOrder(src, pos); const closeTableOrder = [pipeChar, hashChar]; const isCloseTableOrder = (src, pos) => checkCharsOrder(closeTableOrder, src, pos); class StateIterator { constructor(state, pos, line) { this.state = state; this.line = line; this.pos = pos; this.lineEnds = this.state.eMarks[this.line]; } stats() { return { line: this.line, pos: this.pos, }; } get symbol() { return this.state.src[this.pos]; } next(steps = 1) { for (let i = 0; i < steps; i++) { this.pos++; if (this.pos > this.lineEnds) { this.line++; this.pos = this.state.bMarks[this.line] + this.state.tShift[this.line]; this.lineEnds = this.state.eMarks[this.line]; } } } } function getTableRowPositions(state, startPosition, endPosition, startLine) { let endOfTable = null; let tableLevel = 0; let currentRow = []; let colStart = null; let rowStart = null; const iter = new StateIterator(state, startPosition + openTableOrder.length, startLine); const rows = []; let isInsideCode = false; let isInsideTable = false; let isInsideLiquidVariable = false; const rowMap = new Map(); const addRow = () => { if (colStart) { currentRow.push([colStart, iter.stats()]); } if (currentRow.length && rowStart) { rows.push([rowStart, iter.line, currentRow]); } currentRow = []; colStart = null; rowStart = null; }; while (iter.pos <= endPosition) { if (iter.symbol === undefined) { break; } if (isCodeBlockOrder(state.src, iter.pos)) { isInsideCode = !isInsideCode; iter.next(codeBlockOrder.length); } if (isInsideCode) { iter.next(); continue; } if (!isInsideLiquidVariable && isLiquidVariableStart(state.src, iter.pos)) { isInsideLiquidVariable = true; iter.next(liquidVariableStartOrder.length); } if (isInsideLiquidVariable && isLiquidVariableEnd(state.src, iter.pos)) { isInsideLiquidVariable = false; iter.next(liquidVariableEndOrder.length); } if (isInsideLiquidVariable) { iter.next(); continue; } if (isOpenTableOrder(state.src, iter.pos)) { isInsideTable = true; tableLevel++; iter.next(openTableOrder.length); continue; } if (isCloseTableOrder(state.src, iter.pos)) { if (tableLevel === 0) { addRow(); iter.next(closeTableOrder.length); endOfTable = iter.line + 2; break; } else { isInsideTable = false; tableLevel--; iter.next(closeTableOrder.length); continue; } } if (isInsideTable) { iter.next(); continue; } if (isRowOrder(state.src, iter.pos)) { const insideRow = rowMap.get(tableLevel); if (insideRow) { addRow(); iter.next(rowStartOrder.length); } else { iter.next(rowStartOrder.length); rowStart = iter.line; colStart = iter.stats(); } rowMap.set(tableLevel, !insideRow); continue; } if (isCellOrder(state.src, iter.pos)) { if (colStart) { currentRow.push([colStart, iter.stats()]); } iter.next(cellStartOrder.length); colStart = iter.stats(); continue; } iter.next(); } const { pos } = iter; return { rows, endOfTable, pos }; } function extractAttributes(state, pos) { const attrsStringStart = state.skipSpaces(pos); const attrsString = state.src.slice(attrsStringStart); const attrsParser = new attrs_1.AttrsParser(); return attrsParser.parse(attrsString); } /** * Extracts the class attribute from the given content token and applies it to the tdOpenToken. * Preserves other attributes. * * @param {Token} contentToken - Search the content of this token for the class. * @param {Token} tdOpenToken - Parent td_open token. Extracted class is applied to this token. * @returns {void} */ function extractAndApplyClassFromToken(contentToken, tdOpenToken) { var _a; // Regex to find class attribute in any position within brackets const blockRegex = /\s*\{[^}]*}$/; const allAttrs = contentToken.content.match(blockRegex); if (!allAttrs) { return; } const attrs = new attrs_1.AttrsParser().parse(allAttrs[0].trim()); const attrsClass = (_a = attrs === null || attrs === void 0 ? void 0 : attrs.class) === null || _a === void 0 ? void 0 : _a.join(' '); if (attrsClass) { tdOpenToken.attrSet('class', attrsClass); // remove the class from the token so that it's not propagated to tr or table level let replacedContent = allAttrs[0].replace(`.${attrsClass}`, ''); if (replacedContent.trim() === '{}') { replacedContent = ''; } contentToken.content = contentToken.content.replace(allAttrs[0], replacedContent); } } const COLSPAN_SYMBOL = '>'; const ROWSPAN_SYMBOL = '^'; /** * Traverses through the content map, applying row/colspan attributes and marking the special cells for deletion. * Upon encountering a symbol denoting a row span or a column span, proceed backwards in row or column * until text cell is found. Upon finding the text cell, store the colspan or rowspan value. * During the backward traversal, if the same symbol is encountered, increment the value of rowspan/colspan. * Colspan symbol is ignored for the first column. Rowspan symbol is ignored for the first row * * @param contentMap string[][] * @param tokenMap Token[][] * @return {void} */ const applySpans = (contentMap, tokenMap) => { for (let i = 0; i < contentMap.length; i++) { for (let j = 0; j < contentMap[0].length; j++) { if (contentMap[i][j] === COLSPAN_SYMBOL) { // skip the first column if (j === 0) { continue; } tokenMap[i][j].meta = { markForDeletion: true }; let colspanFactor = 2; // traverse columns backwards for (let col = j - 1; col >= 0; col--) { if (contentMap[i][col] === COLSPAN_SYMBOL) { colspanFactor++; tokenMap[i][col].meta = { markForDeletion: true }; } else if (contentMap[i][col] === ROWSPAN_SYMBOL) { // Do nothing, this should be applied on the row that's being extended break; } else { tokenMap[i][col].attrSet('colspan', colspanFactor.toString()); break; } } } if (contentMap[i][j] === ROWSPAN_SYMBOL) { // skip the first row if (i === 0) { continue; } tokenMap[i][j].meta = { markForDeletion: true }; let rowSpanFactor = 2; // traverse rows upward for (let row = i - 1; row >= 0; row--) { if (contentMap[row][j] === ROWSPAN_SYMBOL) { rowSpanFactor++; tokenMap[row][j].meta = { markForDeletion: true }; } else if (contentMap[row][j] === COLSPAN_SYMBOL) { break; } else { tokenMap[row][j].attrSet('rowspan', rowSpanFactor.toString()); break; } } } } } }; /** * Removes td_open and matching td_close tokens and the content within them * * @param {number} tableStart - The index of the start of the table in the state tokens array. * @param {Token[]} tokens - The array of tokens from state. * @returns {void} */ const clearTokens = (tableStart, tokens) => { var _a; // use splices array to avoid modifying the tokens array during iteration const splices = []; for (let i = tableStart; i < tokens.length; i++) { if ((_a = tokens[i].meta) === null || _a === void 0 ? void 0 : _a.markForDeletion) { // Use unshift instead of push so that the splices indexes are in reverse order. // Reverse order guarantees that we don't mess up the indexes while removing the items. splices.unshift([i]); const level = tokens[i].level; // find matching td_close with the same level for (let j = i + 1; j < tokens.length; j++) { if (tokens[j].type === 'yfm_td_close' && tokens[j].level === level) { splices[0].push(j); break; } } } } splices.forEach(([start, end]) => { // check that we have both start and end defined // it's possible we didn't find td_close index if (start && end) { tokens.splice(start, end - start + 1); } }); }; const yfmTable = (md) => { md.block.ruler.before('code', pluginName, (state, startLine, endLine, silent) => { let token; const startPosition = state.bMarks[startLine] + state.tShift[startLine]; const endPosition = state.eMarks[endLine]; // #| minimum 2 symbols if (endPosition - startPosition < 2) { return false; } if (!isOpenTableOrder(state.src, startPosition)) { return false; } if (silent) { return true; } const { rows, endOfTable, pos } = getTableRowPositions(state, startPosition, endPosition, startLine); const attrs = extractAttributes(state, pos); if (!endOfTable) { token = state.push('__yfm_lint', '', 0); token.hidden = true; token.map = [startLine, endLine]; token.attrSet('YFM004', 'true'); return false; } const oldParentLineMax = state.lineMax; state.lineMax = endOfTable; state.line = startLine; const tableStart = state.tokens.length; token = state.push('yfm_table_open', 'table', 1); const { attr: singleKeyAttrs = [] } = attrs, fullAttrs = __rest(attrs, ["attr"]); for (const [property, values] of Object.entries(fullAttrs)) { token.attrJoin(property, values.join(' ')); } for (const attr of singleKeyAttrs) { token.attrJoin(attr, 'true'); } token.map = [startLine, endOfTable]; token = state.push('yfm_tbody_open', 'tbody', 1); token.map = [startLine + 1, endOfTable - 1]; const maxRowLength = Math.max(...rows.map(([, , cols]) => cols.length)); // cellsMaps is a 2-D map of all td_open tokens in the table. // cellsMap is used to access the table cells by [row][column] coordinates const cellsMap = []; // contentMap is a 2-D map of the text content within cells in the table. // To apply spans, traverse the contentMap and modify the cells from cellsMap const contentMap = []; for (let i = 0; i < rows.length; i++) { const [rowLineStarts, rowLineEnds, cols] = rows[i]; cellsMap.push([]); contentMap.push([]); const rowLength = cols.length; token = state.push('yfm_tr_open', 'tr', 1); token.map = [rowLineStarts, rowLineEnds]; for (let j = 0; j < cols.length; j++) { const [begin, end] = cols[j]; token = state.push('yfm_td_open', 'td', 1); cellsMap[i].push(token); token.map = [begin.line, end.line]; const oldTshift = state.tShift[begin.line]; const oldEMark = state.eMarks[end.line]; const oldBMark = state.bMarks[begin.line]; const oldLineMax = state.lineMax; state.tShift[begin.line] = 0; state.bMarks[begin.line] = begin.pos; state.eMarks[end.line] = end.pos; state.lineMax = end.line + 1; state.md.block.tokenize(state, begin.line, end.line + 1); const contentToken = state.tokens[state.tokens.length - 2]; // In case of ">" within a cell without whitespace it gets consumed as a blockquote. // To handle that, check markup as well const content = contentToken.content.trim() || contentToken.markup.trim(); contentMap[i].push(content); token = state.push('yfm_td_close', 'td', -1); state.tokens[state.tokens.length - 1].map = [end.line, end.line + 1]; state.lineMax = oldLineMax; state.tShift[begin.line] = oldTshift; state.bMarks[begin.line] = oldBMark; state.eMarks[end.line] = oldEMark; const rowTokens = cellsMap[cellsMap.length - 1]; extractAndApplyClassFromToken(contentToken, rowTokens[rowTokens.length - 1]); } if (rowLength < maxRowLength) { const emptyCellsCount = maxRowLength - rowLength; for (let k = 0; k < emptyCellsCount; k++) { token = state.push('yfm_td_open', 'td', 1); token = state.push('yfm_td_close', 'td', -1); } } token = state.push('yfm_tr_close', 'tr', -1); } applySpans(contentMap, cellsMap); clearTokens(tableStart, state.tokens); token = state.push('yfm_tbody_close', 'tbody', -1); token = state.push('yfm_table_close', 'table', -1); state.tokens[state.tokens.length - 1].map = [endOfTable, endOfTable + 1]; state.lineMax = oldParentLineMax; state.line = endOfTable; return true; }); }; module.exports = yfmTable; //# sourceMappingURL=index.js.map