2020-12-03 20:11:48 +03:00
|
|
|
/**
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2020-12-29 04:38:00 +03:00
|
|
|
// @ts-check
|
|
|
|
|
|
|
|
/** @typedef {{
|
|
|
|
* type: 'text' | 'li' | 'code' | 'gen' | 'h0' | 'h1' | 'h2' | 'h3' | 'h4',
|
|
|
|
* text?: string,
|
|
|
|
* codeLang?: string,
|
|
|
|
* lines?: string[],
|
|
|
|
* liType?: 'default' | 'bullet' | 'ordinal',
|
|
|
|
* children?: MarkdownNode[]
|
|
|
|
* }} MarkdownNode */
|
|
|
|
|
2020-12-03 20:11:48 +03:00
|
|
|
function normalizeLines(content) {
|
2020-12-27 04:05:44 +03:00
|
|
|
const inLines = content.replace(/\r\n/g, '\n').split('\n');
|
2020-12-03 20:11:48 +03:00
|
|
|
let inCodeBlock = false;
|
|
|
|
const outLines = [];
|
|
|
|
let outLineTokens = [];
|
|
|
|
for (const line of inLines) {
|
|
|
|
let singleLineExpression = line.startsWith('#');
|
|
|
|
let flushParagraph = !line.trim()
|
|
|
|
|| line.trim().startsWith('1.')
|
|
|
|
|| line.trim().startsWith('<')
|
|
|
|
|| line.trim().startsWith('>')
|
|
|
|
|| line.trim().startsWith('-')
|
2020-12-04 05:05:36 +03:00
|
|
|
|| line.trim().startsWith('*')
|
2020-12-03 20:11:48 +03:00
|
|
|
|| singleLineExpression;
|
|
|
|
if (line.startsWith('```')) {
|
|
|
|
inCodeBlock = !inCodeBlock;
|
|
|
|
flushParagraph = true;
|
|
|
|
}
|
|
|
|
if (flushParagraph && outLineTokens.length) {
|
|
|
|
outLines.push(outLineTokens.join(' '));
|
|
|
|
outLineTokens = [];
|
|
|
|
}
|
2020-12-04 03:02:34 +03:00
|
|
|
const trimmedLine = line.trim();
|
2020-12-03 20:11:48 +03:00
|
|
|
if (inCodeBlock || singleLineExpression)
|
|
|
|
outLines.push(line);
|
2020-12-04 03:02:34 +03:00
|
|
|
else if (trimmedLine)
|
|
|
|
outLineTokens.push(trimmedLine.startsWith('-') ? line : trimmedLine);
|
2020-12-03 20:11:48 +03:00
|
|
|
}
|
|
|
|
if (outLineTokens.length)
|
|
|
|
outLines.push(outLineTokens.join(' '));
|
|
|
|
return outLines;
|
|
|
|
}
|
|
|
|
|
2020-12-29 04:38:00 +03:00
|
|
|
/**
|
|
|
|
* @param {string[]} lines
|
|
|
|
*/
|
2020-12-03 20:11:48 +03:00
|
|
|
function buildTree(lines) {
|
2020-12-29 04:38:00 +03:00
|
|
|
/** @type {MarkdownNode} */
|
2020-12-03 20:11:48 +03:00
|
|
|
const root = {
|
2020-12-24 06:35:43 +03:00
|
|
|
type: 'h0',
|
2020-12-29 04:38:00 +03:00
|
|
|
text: '<root>',
|
2020-12-05 05:05:35 +03:00
|
|
|
children: []
|
2020-12-03 20:11:48 +03:00
|
|
|
};
|
2020-12-29 04:38:00 +03:00
|
|
|
/** @type {MarkdownNode[]} */
|
2020-12-03 20:11:48 +03:00
|
|
|
const stack = [root];
|
2020-12-29 04:38:00 +03:00
|
|
|
/** @type {MarkdownNode[]} */
|
2020-12-05 05:05:35 +03:00
|
|
|
let liStack = null;
|
|
|
|
|
2020-12-03 20:11:48 +03:00
|
|
|
for (let i = 0; i < lines.length; ++i) {
|
|
|
|
let line = lines[i];
|
|
|
|
|
|
|
|
if (line.startsWith('```')) {
|
2020-12-29 04:38:00 +03:00
|
|
|
/** @type {MarkdownNode} */
|
2020-12-03 20:11:48 +03:00
|
|
|
const node = {
|
2020-12-24 06:35:43 +03:00
|
|
|
type: 'code',
|
|
|
|
lines: [],
|
2020-12-04 05:05:36 +03:00
|
|
|
codeLang: line.substring(3)
|
2020-12-03 20:11:48 +03:00
|
|
|
};
|
2020-12-05 05:05:35 +03:00
|
|
|
stack[0].children.push(node);
|
2020-12-03 20:11:48 +03:00
|
|
|
line = lines[++i];
|
|
|
|
while (!line.startsWith('```')) {
|
2020-12-24 06:35:43 +03:00
|
|
|
node.lines.push(line);
|
2020-12-03 20:11:48 +03:00
|
|
|
line = lines[++i];
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (line.startsWith('<!-- GEN')) {
|
2020-12-29 04:38:00 +03:00
|
|
|
/** @type {MarkdownNode} */
|
2020-12-03 20:11:48 +03:00
|
|
|
const node = {
|
2020-12-24 06:35:43 +03:00
|
|
|
type: 'gen',
|
|
|
|
lines: [line]
|
2020-12-03 20:11:48 +03:00
|
|
|
};
|
2020-12-05 05:05:35 +03:00
|
|
|
stack[0].children.push(node);
|
2020-12-03 20:11:48 +03:00
|
|
|
line = lines[++i];
|
|
|
|
while (!line.startsWith('<!-- GEN')) {
|
2020-12-24 06:35:43 +03:00
|
|
|
node.lines.push(line);
|
2020-12-03 20:11:48 +03:00
|
|
|
line = lines[++i];
|
|
|
|
}
|
2020-12-24 06:35:43 +03:00
|
|
|
node.lines.push(line);
|
2020-12-03 20:11:48 +03:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const header = line.match(/^(#+)/);
|
|
|
|
if (header) {
|
2020-12-05 05:05:35 +03:00
|
|
|
const h = header[1].length;
|
2020-12-29 04:38:00 +03:00
|
|
|
const node = /** @type {MarkdownNode} */({ type: 'h' + h, text: line.substring(h + 1), children: [] });
|
2020-12-05 05:05:35 +03:00
|
|
|
|
|
|
|
while (true) {
|
2020-12-24 06:35:43 +03:00
|
|
|
const lastH = +stack[0].type.substring(1);
|
2020-12-05 05:05:35 +03:00
|
|
|
if (h <= lastH)
|
|
|
|
stack.shift();
|
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
stack[0].children.push(node);
|
|
|
|
stack.unshift(node);
|
|
|
|
liStack = [node];
|
2020-12-03 20:11:48 +03:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-12-04 05:05:36 +03:00
|
|
|
const list = line.match(/^(\s*)(-|1.|\*) /);
|
2020-12-03 20:11:48 +03:00
|
|
|
const depth = list ? (list[1].length / 2) : 0;
|
2020-12-29 04:38:00 +03:00
|
|
|
const node = /** @type {MarkdownNode} */({ type: 'text', text: line });
|
2020-12-03 20:11:48 +03:00
|
|
|
if (list) {
|
2020-12-24 06:35:43 +03:00
|
|
|
node.type = 'li';
|
|
|
|
node.text = line.substring(list[0].length);
|
2020-12-04 05:05:36 +03:00
|
|
|
if (line.trim().startsWith('1.'))
|
|
|
|
node.liType = 'ordinal';
|
|
|
|
else if (line.trim().startsWith('*'))
|
|
|
|
node.liType = 'bullet';
|
|
|
|
else
|
|
|
|
node.liType = 'default';
|
2020-12-03 20:11:48 +03:00
|
|
|
}
|
2020-12-05 05:05:35 +03:00
|
|
|
if (!liStack[depth].children)
|
|
|
|
liStack[depth].children = [];
|
|
|
|
liStack[depth].children.push(node);
|
|
|
|
liStack[depth + 1] = node;
|
2020-12-03 20:11:48 +03:00
|
|
|
}
|
2020-12-05 05:05:35 +03:00
|
|
|
return root.children;
|
2020-12-03 20:11:48 +03:00
|
|
|
}
|
|
|
|
|
2020-12-29 04:38:00 +03:00
|
|
|
/**
|
|
|
|
* @param {string} content
|
|
|
|
*/
|
|
|
|
function parse(content) {
|
2020-12-03 20:11:48 +03:00
|
|
|
return buildTree(normalizeLines(content));
|
|
|
|
}
|
|
|
|
|
2020-12-29 04:38:00 +03:00
|
|
|
/**
|
|
|
|
* @param {MarkdownNode[]} nodes
|
2020-12-29 23:12:46 +03:00
|
|
|
* @param {number=} maxColumns
|
2020-12-29 04:38:00 +03:00
|
|
|
*/
|
2020-12-29 23:12:46 +03:00
|
|
|
function render(nodes, maxColumns) {
|
2020-12-03 20:11:48 +03:00
|
|
|
const result = [];
|
2020-12-04 05:05:36 +03:00
|
|
|
let lastNode;
|
|
|
|
for (let node of nodes) {
|
2020-12-29 23:12:46 +03:00
|
|
|
innerRenderMdNode(node, lastNode, result, maxColumns);
|
2020-12-04 05:05:36 +03:00
|
|
|
lastNode = node;
|
|
|
|
}
|
|
|
|
return result.join('\n');
|
|
|
|
}
|
|
|
|
|
2020-12-29 04:38:00 +03:00
|
|
|
/**
|
|
|
|
* @param {MarkdownNode} node
|
|
|
|
* @param {MarkdownNode} lastNode
|
2020-12-29 23:12:46 +03:00
|
|
|
* @param {number=} maxColumns
|
2020-12-29 04:38:00 +03:00
|
|
|
* @param {string[]} result
|
|
|
|
*/
|
2020-12-29 23:12:46 +03:00
|
|
|
function innerRenderMdNode(node, lastNode, result, maxColumns) {
|
2020-12-04 05:05:36 +03:00
|
|
|
const newLine = () => {
|
|
|
|
if (result[result.length - 1] !== '')
|
|
|
|
result.push('');
|
|
|
|
};
|
|
|
|
|
2020-12-24 06:35:43 +03:00
|
|
|
if (node.type.startsWith('h')) {
|
|
|
|
newLine();
|
2020-12-29 04:38:00 +03:00
|
|
|
const depth = +node.type.substring(1);
|
2020-12-24 06:35:43 +03:00
|
|
|
result.push(`${'#'.repeat(depth)} ${node.text}`);
|
|
|
|
let lastNode = node;
|
2020-12-28 18:03:09 +03:00
|
|
|
for (const child of node.children || []) {
|
2020-12-29 23:12:46 +03:00
|
|
|
innerRenderMdNode(child, lastNode, result, maxColumns);
|
2020-12-24 06:35:43 +03:00
|
|
|
lastNode = child;
|
2020-12-05 05:05:35 +03:00
|
|
|
}
|
2020-12-04 05:05:36 +03:00
|
|
|
}
|
2020-12-05 05:05:35 +03:00
|
|
|
|
2020-12-24 06:35:43 +03:00
|
|
|
if (node.type === 'text') {
|
|
|
|
const bothComments = node.text.startsWith('>') && lastNode && lastNode.type === 'text' && lastNode.text.startsWith('>');
|
|
|
|
if (!bothComments && lastNode && lastNode.text)
|
2020-12-04 05:05:36 +03:00
|
|
|
newLine();
|
2020-12-29 23:12:46 +03:00
|
|
|
result.push(wrapText(node.text, maxColumns));
|
2020-12-04 05:05:36 +03:00
|
|
|
}
|
2020-12-05 05:05:35 +03:00
|
|
|
|
2020-12-24 06:35:43 +03:00
|
|
|
if (node.type === 'code') {
|
2020-12-04 05:05:36 +03:00
|
|
|
newLine();
|
|
|
|
result.push('```' + node.codeLang);
|
2020-12-24 06:35:43 +03:00
|
|
|
for (const line of node.lines)
|
2020-12-04 05:05:36 +03:00
|
|
|
result.push(line);
|
|
|
|
result.push('```');
|
|
|
|
newLine();
|
|
|
|
}
|
2020-12-05 05:05:35 +03:00
|
|
|
|
2020-12-24 06:35:43 +03:00
|
|
|
if (node.type === 'gen') {
|
2020-12-04 05:05:36 +03:00
|
|
|
newLine();
|
2020-12-24 06:35:43 +03:00
|
|
|
for (const line of node.lines)
|
2020-12-04 05:05:36 +03:00
|
|
|
result.push(line);
|
|
|
|
newLine();
|
|
|
|
}
|
2020-12-05 05:05:35 +03:00
|
|
|
|
2020-12-24 06:35:43 +03:00
|
|
|
if (node.type === 'li') {
|
2020-12-04 05:05:36 +03:00
|
|
|
const visit = (node, indent) => {
|
|
|
|
let char;
|
|
|
|
switch (node.liType) {
|
|
|
|
case 'bullet': char = '*'; break;
|
|
|
|
case 'default': char = '-'; break;
|
|
|
|
case 'ordinal': char = '1.'; break;
|
|
|
|
}
|
2020-12-29 23:12:46 +03:00
|
|
|
result.push(`${indent}${char} ${wrapText(node.text, maxColumns, indent + ' '.repeat(char.length + 1))}`);
|
2020-12-05 05:05:35 +03:00
|
|
|
for (const child of node.children || [])
|
2020-12-04 05:05:36 +03:00
|
|
|
visit(child, indent + ' ');
|
|
|
|
};
|
|
|
|
visit(node, '');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-29 23:12:46 +03:00
|
|
|
/**
|
|
|
|
* @param {string} text
|
|
|
|
*/
|
|
|
|
function tokenizeText(text) {
|
|
|
|
const links = [];
|
|
|
|
// Don't wrap simple links with spaces.
|
|
|
|
text = text.replace(/\[[^\]]+\]/g, match => {
|
|
|
|
links.push(match);
|
|
|
|
return `[${links.length - 1}]`;
|
|
|
|
});
|
|
|
|
return text.split(' ').map(c => c.replace(/\[(\d+)\]/g, (_, p1) => links[+p1]));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} text
|
|
|
|
* @param {number=} maxColumns
|
|
|
|
* @param {string=} indent
|
|
|
|
*/
|
|
|
|
function wrapText(text, maxColumns = 0, indent = '') {
|
|
|
|
if (!maxColumns)
|
|
|
|
return text;
|
|
|
|
const lines = [];
|
|
|
|
maxColumns -= indent.length;
|
|
|
|
const words = tokenizeText(text);
|
|
|
|
let line = '';
|
|
|
|
for (const word of words) {
|
|
|
|
if (line.length && line.length + word.length < maxColumns) {
|
|
|
|
line += ' ' + word;
|
|
|
|
} else {
|
|
|
|
if (line)
|
|
|
|
lines.push(line);
|
|
|
|
line = (lines.length ? indent : '') + word;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (line)
|
|
|
|
lines.push(line);
|
|
|
|
return lines.join('\n');
|
|
|
|
}
|
|
|
|
|
2020-12-29 04:38:00 +03:00
|
|
|
/**
|
|
|
|
* @param {MarkdownNode} node
|
|
|
|
*/
|
2020-12-24 06:35:43 +03:00
|
|
|
function clone(node) {
|
|
|
|
const copy = { ...node };
|
|
|
|
copy.children = copy.children ? copy.children.map(c => clone(c)) : undefined;
|
|
|
|
return copy;
|
|
|
|
}
|
|
|
|
|
2020-12-29 04:38:00 +03:00
|
|
|
/**
|
|
|
|
* @param {MarkdownNode[]} nodes
|
|
|
|
* @param {function(MarkdownNode): void} visitor
|
|
|
|
*/
|
|
|
|
function visitAll(nodes, visitor) {
|
|
|
|
for (const node of nodes)
|
|
|
|
visit(node, visitor);
|
2020-12-24 06:35:43 +03:00
|
|
|
}
|
|
|
|
|
2020-12-27 01:31:41 +03:00
|
|
|
/**
|
2020-12-29 04:38:00 +03:00
|
|
|
* @param {MarkdownNode} node
|
|
|
|
* @param {function(MarkdownNode): void} visitor
|
2020-12-27 01:31:41 +03:00
|
|
|
*/
|
2020-12-29 04:38:00 +03:00
|
|
|
function visit(node, visitor) {
|
|
|
|
visitor(node);
|
|
|
|
for (const n of node.children || [])
|
|
|
|
visit(n, visitor);
|
2020-12-04 03:02:34 +03:00
|
|
|
}
|
|
|
|
|
2020-12-29 04:38:00 +03:00
|
|
|
module.exports = { parse, render, clone, visitAll, visit };
|