playwright/utils/markdown.js

301 lines
7.6 KiB
JavaScript
Raw Normal View History

/**
* 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 */
function normalizeLines(content) {
2020-12-27 04:05:44 +03:00
const inLines = content.replace(/\r\n/g, '\n').split('\n');
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('*')
|| singleLineExpression;
if (line.startsWith('```')) {
inCodeBlock = !inCodeBlock;
flushParagraph = true;
}
if (flushParagraph && outLineTokens.length) {
outLines.push(outLineTokens.join(' '));
outLineTokens = [];
}
const trimmedLine = line.trim();
if (inCodeBlock || singleLineExpression)
outLines.push(line);
else if (trimmedLine)
outLineTokens.push(trimmedLine.startsWith('-') ? line : trimmedLine);
}
if (outLineTokens.length)
outLines.push(outLineTokens.join(' '));
return outLines;
}
2020-12-29 04:38:00 +03:00
/**
* @param {string[]} lines
*/
function buildTree(lines) {
2020-12-29 04:38:00 +03:00
/** @type {MarkdownNode} */
const root = {
2020-12-24 06:35:43 +03:00
type: 'h0',
2020-12-29 04:38:00 +03:00
text: '<root>',
children: []
};
2020-12-29 04:38:00 +03:00
/** @type {MarkdownNode[]} */
const stack = [root];
2020-12-29 04:38:00 +03:00
/** @type {MarkdownNode[]} */
let liStack = null;
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} */
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)
};
stack[0].children.push(node);
line = lines[++i];
while (!line.startsWith('```')) {
2020-12-24 06:35:43 +03:00
node.lines.push(line);
line = lines[++i];
}
continue;
}
if (line.startsWith('<!-- GEN')) {
2020-12-29 04:38:00 +03:00
/** @type {MarkdownNode} */
const node = {
2020-12-24 06:35:43 +03:00
type: 'gen',
lines: [line]
};
stack[0].children.push(node);
line = lines[++i];
while (!line.startsWith('<!-- GEN')) {
2020-12-24 06:35:43 +03:00
node.lines.push(line);
line = lines[++i];
}
2020-12-24 06:35:43 +03:00
node.lines.push(line);
continue;
}
const header = line.match(/^(#+)/);
if (header) {
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: [] });
while (true) {
2020-12-24 06:35:43 +03:00
const lastH = +stack[0].type.substring(1);
if (h <= lastH)
stack.shift();
else
break;
}
stack[0].children.push(node);
stack.unshift(node);
liStack = [node];
continue;
}
2020-12-04 05:05:36 +03:00
const list = line.match(/^(\s*)(-|1.|\*) /);
const depth = list ? (list[1].length / 2) : 0;
2020-12-29 04:38:00 +03:00
const node = /** @type {MarkdownNode} */({ type: 'text', text: line });
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';
}
if (!liStack[depth].children)
liStack[depth].children = [];
liStack[depth].children.push(node);
liStack[depth + 1] = node;
}
return root.children;
}
2020-12-29 04:38:00 +03:00
/**
* @param {string} content
*/
function parse(content) {
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) {
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;
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-04 05:05:36 +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-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-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-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))}`);
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-29 04:38:00 +03:00
* @param {MarkdownNode} node
* @param {function(MarkdownNode): void} visitor
*/
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-29 04:38:00 +03:00
module.exports = { parse, render, clone, visitAll, visit };