You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

280 lines
8.3 KiB
TypeScript

import type MarkdownIt from 'markdown-it'
import type { RenderRule } from 'markdown-it/lib/renderer'
import type StateBlock from 'markdown-it/lib/rules_block/state_block'
import { isSpace } from 'markdown-it/lib/common/utils'
import container from 'markdown-it-container'
import kbd from 'markdown-it-kbd'
export const addPlugins = (md: MarkdownIt) => {
md.use(...createContainer('info', 'Информация', md))
.use(...createContainer('tip', 'Подсказка', md))
.use(...createContainer('warning', 'Внимание', md))
.use(...createContainer('danger', 'Осторожно', md))
.use(...createContainer('details', 'Подробнее', md))
.use(kbd)
md.block.ruler.at('table', table)
}
type ContainerArgs = [typeof container, string, { render: RenderRule }]
function createContainer(
klass: string,
defaultTitle: string,
md: MarkdownIt
): ContainerArgs {
return [
container,
klass,
{
render(tokens, idx, _options, env) {
const token = tokens[idx]
const info = token.info.trim().slice(klass.length).trim()
const attrs = md.renderer.renderAttrs(token)
if (token.nesting === 1) {
const title = md.renderInline(info || defaultTitle, {
references: env.references
})
if (klass === 'details')
return `<details class="${klass} custom-block"${attrs}><summary>${title}</summary>\n`
return `<div class="${klass} custom-block"${attrs}><p class="custom-block-title">${title}</p>\n`
} else return klass === 'details' ? `</details>\n` : `</div>\n`
}
}
]
}
// from https://github.com/markdown-it/markdown-it/blob/2b6cac25823af011ff3bc7628bc9b06e483c5a08/lib/rules_block/table.js
// GFM table, non-standard
function table(
state: StateBlock,
startLine: number,
endLine: number,
silent: any
) {
var ch, lineText, pos, i, l, nextLine, headers, columns, columnCount, token,
aligns, t, tableLines, tbodyLines, oldParentType, terminate,
terminatorRules, firstCh, secondCh;
// should have at least two lines
if (startLine + 2 > endLine) { return false; }
nextLine = startLine + 1;
if (state.sCount[nextLine] < state.blkIndent) { return false; }
// if it's indented more than 3 spaces, it should be a code block
if (state.sCount[nextLine] - state.blkIndent >= 4) { return false; }
// first character of the second line should be '|', '-', ':',
// and no other characters are allowed but spaces;
// basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp
pos = state.bMarks[nextLine] + state.tShift[nextLine];
if (pos >= state.eMarks[nextLine]) { return false; }
firstCh = state.src.charCodeAt(pos++);
if (firstCh !== 0x7C/* | */ && firstCh !== 0x2D/* - */ && firstCh !== 0x3A/* : */) { return false; }
if (pos >= state.eMarks[nextLine]) { return false; }
secondCh = state.src.charCodeAt(pos++);
if (secondCh !== 0x7C/* | */ && secondCh !== 0x2D/* - */ && secondCh !== 0x3A/* : */ && !isSpace(secondCh)) {
return false;
}
// if first character is '-', then second character must not be a space
// (due to parsing ambiguity with list)
if (firstCh === 0x2D/* - */ && isSpace(secondCh)) { return false; }
while (pos < state.eMarks[nextLine]) {
ch = state.src.charCodeAt(pos);
if (ch !== 0x7C/* | */ && ch !== 0x2D/* - */ && ch !== 0x3A/* : */ && !isSpace(ch)) { return false; }
pos++;
}
lineText = getLine(state, startLine + 1);
columns = lineText.split('|');
aligns = [];
for (i = 0; i < columns.length; i++) {
t = columns[i].trim();
if (!t) {
// allow empty columns before and after table, but not in between columns;
// e.g. allow ` |---| `, disallow ` ---||--- `
if (i === 0 || i === columns.length - 1) {
continue;
} else {
return false;
}
}
if (!/^:?-+:?$/.test(t)) { return false; }
if (t.charCodeAt(t.length - 1) === 0x3A/* : */) {
aligns.push(t.charCodeAt(0) === 0x3A/* : */ ? 'center' : 'right');
} else if (t.charCodeAt(0) === 0x3A/* : */) {
aligns.push('left');
} else {
aligns.push('');
}
}
lineText = getLine(state, startLine).trim();
if (lineText.indexOf('|') === -1) { return false; }
if (state.sCount[startLine] - state.blkIndent >= 4) { return false; }
columns = escapedSplit(lineText);
if (columns.length && columns[0] === '') columns.shift();
if (columns.length && columns[columns.length - 1] === '') columns.pop();
// header row will define an amount of columns in the entire table,
// and align row should be exactly the same (the rest of the rows can differ)
columnCount = columns.length;
headers = [...columns];
if (columnCount === 0 || columnCount !== aligns.length) { return false; }
if (silent) { return true; }
oldParentType = state.parentType;
// @ts-expect-error
state.parentType = 'table';
// use 'blockquote' lists for termination because it's
// the most similar to tables
terminatorRules = state.md.block.ruler.getRules('blockquote');
token = state.push('table_open', 'table', 1);
token.map = tableLines = [ startLine, 0 ];
token = state.push('thead_open', 'thead', 1);
token.map = [ startLine, startLine + 1 ];
token = state.push('tr_open', 'tr', 1);
token.map = [ startLine, startLine + 1 ];
for (i = 0; i < columns.length; i++) {
token = state.push('th_open', 'th', 1);
if (aligns[i]) {
token.attrs = [ [ 'style', 'text-align:' + aligns[i] ] ];
}
token = state.push('inline', '', 0);
token.content = columns[i].trim();
token.children = [];
token = state.push('th_close', 'th', -1);
}
token = state.push('tr_close', 'tr', -1);
token = state.push('thead_close', 'thead', -1);
for (nextLine = startLine + 2; nextLine < endLine; nextLine++) {
if (state.sCount[nextLine] < state.blkIndent) { break; }
terminate = false;
for (i = 0, l = terminatorRules.length; i < l; i++) {
if (terminatorRules[i](state, nextLine, endLine, true)) {
terminate = true;
break;
}
}
if (terminate) { break; }
lineText = getLine(state, nextLine).trim();
if (!lineText) { break; }
if (state.sCount[nextLine] - state.blkIndent >= 4) { break; }
columns = escapedSplit(lineText);
if (columns.length && columns[0] === '') columns.shift();
if (columns.length && columns[columns.length - 1] === '') columns.pop();
if (nextLine === startLine + 2) {
token = state.push('tbody_open', 'tbody', 1);
token.map = tbodyLines = [ startLine + 2, 0 ];
}
token = state.push('tr_open', 'tr', 1);
token.map = [ nextLine, nextLine + 1 ];
for (i = 0; i < columnCount; i++) {
token = state.push('td_open', 'td', 1);
const attrs = [];
if (aligns[i]) {
token.attrs = attrs.push([ 'style', 'text-align:' + aligns[i] ]);
}
attrs.push(['data-label', headers[i].trim()]);
if (attrs.length) {
token.attrs = attrs;
}
token = state.push('inline', '', 0);
token.content = columns[i] ? columns[i].trim() : '';
token.children = [];
token = state.push('td_close', 'td', -1);
}
token = state.push('tr_close', 'tr', -1);
}
if (tbodyLines) {
token = state.push('tbody_close', 'tbody', -1);
tbodyLines[1] = nextLine;
}
token = state.push('table_close', 'table', -1);
tableLines[1] = nextLine;
state.parentType = oldParentType;
state.line = nextLine;
return true;
}
function getLine(
state: StateBlock,
line: number
): string {
var pos = state.bMarks[line] + state.tShift[line],
max = state.eMarks[line];
return state.src.slice(pos, max)
}
function escapedSplit(str: string): string[] {
var result = [],
pos = 0,
max = str.length,
ch,
isEscaped = false,
lastPos = 0,
current = '';
ch = str.charCodeAt(pos);
while (pos < max) {
if (ch === 0x7c/* | */) {
if (!isEscaped) {
// pipe separating cells, '|'
result.push(current + str.substring(lastPos, pos));
current = '';
lastPos = pos + 1;
} else {
// escaped pipe, '\|'
current += str.substring(lastPos, pos - 1);
lastPos = pos;
}
}
isEscaped = (ch === 0x5c/* \ */);
pos++;
ch = str.charCodeAt(pos);
}
result.push(current + str.substring(lastPos));
return result;
}