@diplodoc/transform
Version:
A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML
176 lines • 6.53 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.termDefinitions = termDefinitions;
const constants_1 = require("./constants");
const INCLUDE_LINE_RE = /^{%\s*include\s/;
const NEW_LINE_RE = /^(\r\n|\r|\n)/;
const TERM_DEF_RE = /^\[\*([^[\]]+)\]:/;
/**
* @param state - markdown-it block state
* @param line - line number to read
* @returns raw source text of the given line
*/
function getNextLineContent(state, line) {
const start = state.bMarks[line];
const end = state.eMarks[line];
return start === end ? state.src[start] : state.src.slice(start, end);
}
/**
* Checks whether the first non-blank line after {@link fromLine} is an
* `{% include %}` directive. Used to allow blank-line gaps between
* consecutive includes inside a single term definition.
*
* @param state - markdown-it block state
* @param fromLine - line number from which to start searching
* @param endLine - last line number to search within
* @returns true if the first non-blank line is an include directive
*/
function hasIncludeAfterBlanks(state, fromLine, endLine) {
for (let line = fromLine + 1; line <= endLine; line++) {
const start = state.bMarks[line];
const end = state.eMarks[line];
if (start === end) {
continue;
}
const content = state.src.slice(start, end);
return INCLUDE_LINE_RE.test(content.trimStart());
}
return false;
}
/**
* Scans forward from {@link startLine} to find where the current term
* definition ends.
*
* When `multilineTermDefinitions` is enabled, the definition continues
* past blank lines and stops only at the next `[*key]:` or end of block.
* Otherwise blank lines terminate the definition unless followed by an
* `{% include %}` directive (legacy behaviour).
*
* @param state - markdown-it block state
* @param startLine - line where the current definition starts
* @param endLine - last line of the block
* @param multiline - whether multiline mode is enabled
* @returns line number where the definition ends
*/
function findDefinitionEnd(state, startLine, endLine, multiline) {
let currentLine = startLine;
for (; currentLine < endLine; currentLine++) {
const nextLine = getNextLineContent(state, currentLine + 1);
if (TERM_DEF_RE.test(nextLine)) {
break;
}
if (!multiline && NEW_LINE_RE.test(nextLine)) {
if (!hasIncludeAfterBlanks(state, currentLine + 1, endLine)) {
break;
}
}
state.line = currentLine + 1;
}
return currentLine;
}
function termDefinitions(md, options) {
const multiline = options.multilineTermDefinitions === true;
return (state, startLine, endLine, silent) => {
let ch;
let labelEnd;
let pos = state.bMarks[startLine] + state.tShift[startLine];
let max = state.eMarks[startLine];
if (pos + 2 >= max) {
return false;
}
if (state.src.charCodeAt(pos++) !== 0x5b /* [ */) {
return false;
}
if (state.src.charCodeAt(pos++) !== 0x2a /* * */) {
return false;
}
const labelStart = pos;
for (; pos < max; pos++) {
ch = state.src.charCodeAt(pos);
if (ch === 0x5b /* [ */) {
return false;
}
else if (ch === 0x5d /* ] */) {
labelEnd = pos;
break;
}
else if (ch === 0x5c /* \ */) {
pos++;
}
}
const currentLine = findDefinitionEnd(state, startLine, endLine, multiline);
max = state.eMarks[currentLine];
if (!labelEnd || labelEnd < 0 || state.src.charCodeAt(labelEnd + 1) !== 0x3a /* : */) {
return false;
}
if (silent) {
return true;
}
const label = state.src.slice(labelStart, labelEnd).replace(/\\(.)/g, '$1');
const title = state.src.slice(labelEnd + 2, max).trim();
if (label.length === 0 || title.length === 0) {
return false;
}
return processTermDefinition(md, options, state, currentLine, startLine, endLine, label, title);
};
}
function processTermDefinition(md, options, state, currentLine, startLine, endLine, label, title) {
var _a;
let token;
if (!state.env.terms) {
state.env.terms = {};
}
const basicTermDefinitionRegexp = new RegExp(constants_1.BASIC_TERM_REGEXP, 'gm');
// If term inside definition
const { isLintRun } = options;
if (basicTermDefinitionRegexp.test(title) && isLintRun) {
token = new state.Token('__yfm_lint', '', 0);
token.hidden = true;
token.map = [currentLine, endLine];
token.attrSet('YFM008', 'true');
state.tokens.push(token);
}
// If term definition duplicated
if (state.env.terms[':' + label] && isLintRun) {
token = new state.Token('__yfm_lint', '', 0);
token.hidden = true;
token.map = [currentLine, endLine];
token.attrSet('YFM006', 'true');
state.tokens.push(token);
state.line = currentLine + 1;
return true;
}
if (typeof state.env.terms[':' + label] === 'undefined') {
state.env.terms[':' + label] = title;
}
const fromInclude = Array.isArray(state.env.includes) && state.env.includes.length > 0;
token = new state.Token('dfn_open', 'dfn', 1);
token.attrSet('class', 'yfm yfm-term_dfn');
token.attrSet('id', ':' + label + '_element');
token.attrSet('role', 'dialog');
token.attrSet('aria-live', 'polite');
token.attrSet('aria-modal', 'true');
if (fromInclude) {
token.attrSet('from-include', 'true');
}
state.tokens.push(token);
const titleTokens = md.parse(title, state.env);
for (const titleToken of titleTokens) {
if ((_a = titleToken.children) === null || _a === void 0 ? void 0 : _a.length) {
titleToken.content = '';
}
if (!titleToken.map) {
state.tokens.push(titleToken);
continue;
}
const [start, end] = titleToken.map;
titleToken.map = [start + startLine, end + startLine];
state.tokens.push(titleToken);
}
token = new state.Token('dfn_close', 'dfn', -1);
state.tokens.push(token);
/** current line links to end of term definition */
state.line = currentLine + 1;
return true;
}
//# sourceMappingURL=termDefinitions.js.map