UNPKG

@diplodoc/transform

Version:

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

332 lines (259 loc) 8.86 kB
import {bold} from 'chalk'; import {log} from '../log'; import {NoValue, evalExp} from './evaluation'; import {tagLine} from './lexical'; import {SourceMapApi, createSourceMapApi, getLineNumber} from './sourceMap'; import legacyConditions from './legacyConditions'; interface SourceMap { start: number; end: number; rawStart: string; rawEnd: string; } function resourcemap(source: string, ifTag: SourceMap, ifCon: SourceMap | null, api: SourceMapApi) { const [sourseStartLine, sourceEndLine] = [ getLineNumber(source, ifTag.start + 1), getLineNumber(source, ifTag.end - 1), ]; if (sourseStartLine === sourceEndLine || ifTag === ifCon) { return; } const linesTotal = source.split('\n').length; const {getSourceMapValue, moveLines, removeLines} = api; let offsetRestLines; if (ifCon) { const [resultStartLine, resultEndLine] = [ getLineNumber(source, ifCon.start), getLineNumber(source, ifCon.end), ]; // Move condition's content to the top const offsetContentLines = sourseStartLine - resultStartLine; moveLines({ start: resultStartLine, end: resultEndLine, offset: offsetContentLines, withReplace: true, }); // Remove the rest lines of the condition block removeLines({start: sourseStartLine, end: resultStartLine - 1}); removeLines({start: resultEndLine + 1, end: sourceEndLine}); // Calculate an offset of the rest lines offsetRestLines = getSourceMapValue(resultEndLine) - sourceEndLine; } else { // Remove the whole condition block removeLines({start: sourseStartLine, end: sourceEndLine}); // Calculate offset of the rest lines offsetRestLines = sourseStartLine - sourceEndLine - 1; } // Offset the rest lines moveLines({start: sourceEndLine + 1, end: linesTotal, offset: offsetRestLines}); } type IfCondition = SourceMap & { expr: string; }; function headLinebreak(raw: string) { const match = raw.match(/^([^{]+){.*/); return match ? match[1] : ''; } function tailLinebreak(raw: string) { const match = raw.match(/.*}(\s*\n)$/); return match ? match[1] : ''; } function trimResult(content: string, ifTag: IfTag, ifCon: IfCondition | null) { if (!ifCon) { const head = headLinebreak(ifTag.rawStart); const tail = tailLinebreak(ifTag.rawEnd); let rest = head + tail; if (rest !== head && rest !== tail) { // We have extra line break, if condition was placed on individual line rest = rest.replace('\n', ''); } return ifTag.isBlock ? '\n' : rest; } content = content.substring(ifCon.start, ifCon.end); if (ifTag.isBlock) { return trimBlockResult(content, ifCon); } else { return trimInlineResult(content, ifTag); } } function trimBlockResult(content: string, ifCon: IfCondition) { const head = headLinebreak(ifCon.rawStart); if (head) { content = '\n' + content; } const tail = tailLinebreak(ifCon.rawEnd); if (tail) { content = content + '\n'; } return content; } function trimInlineResult(content: string, ifTag: IfTag) { const head = headLinebreak(ifTag.rawStart); if (head) { content = head + content; } const tail = tailLinebreak(ifTag.rawEnd); if (tail) { content = content + tail; } return content; } class IfTag implements SourceMap { private conditions: IfCondition[] = []; get start() { if (!this.conditions.length) { return -1; } const first = this.conditions[0]; return first.start - first.rawStart.length; } get end() { if (!this.conditions.length) { return -1; } const last = this.conditions[this.conditions.length - 1]; return last.end + last.rawEnd.length; } get rawStart() { if (!this.conditions.length) { return ''; } const first = this.conditions[0]; return first.rawStart; } get rawEnd() { if (!this.conditions.length) { return ''; } const last = this.conditions[this.conditions.length - 1]; return last.rawEnd; } get isBlock() { const first = this.conditions[0]; const last = this.conditions[this.conditions.length - 1]; return tailLinebreak(first.rawStart) && headLinebreak(last.rawEnd); } *[Symbol.iterator](): Generator<IfCondition> { for (const condition of this.conditions) { yield condition; } } openCondition(raw: string, expr: string, start: number) { this.closeCondition(raw, start); this.conditions.push({ rawStart: raw, start: start + raw.length, expr, } as IfCondition); return start + raw.length - tailLinebreak(raw).length; } closeCondition(raw: string, end: number) { const condition = this.conditions[this.conditions.length - 1]; if (condition) { condition.rawEnd = raw; condition.end = end; } } } function inlineConditions( content: string, ifTag: IfTag, vars: Record<string, unknown>, strict: boolean, ) { let ifCon = null; for (const condition of ifTag) { const value = evalExp(condition.expr, vars, strict); if (condition.expr && value === NoValue) { return { result: content, // Fix offset for next matches. // There can be some significant linebreak and spaces. lastIndex: ifTag.end - tailLinebreak(ifTag.rawEnd).length, ifCon: ifTag, }; } if (!condition.expr || value) { ifCon = condition; break; } } const start = content.slice(0, ifTag.start); const end = content.slice(ifTag.end); const result = trimResult(content, ifTag, ifCon); return { result: start + result + end, lastIndex: start.length + result.length - tailLinebreak(ifTag.rawEnd).length, ifCon, }; } export = function conditions( input: string, vars: Record<string, unknown>, path?: string, settings?: { sourceMap: Record<number, number>; strict?: boolean; useLegacyConditions?: boolean; }, ) { if (settings?.useLegacyConditions) { return legacyConditions(input, vars, path, settings); } const sourceMap = settings?.sourceMap || {}; const strict = settings?.strict || false; const tagStack: IfTag[] = []; // Consumes all between curly braces // and all closest upon to first linebreak before and after braces. const R_LIQUID = /((?:\n[\t ]*)?{%-?([\s\S]*?)-?%}(?:[\t ]*\n)?)/g; let match; while ((match = R_LIQUID.exec(input)) !== null) { if (!match[1]) { continue; } const tagMatch = match[2].trim().match(tagLine); if (!tagMatch) { continue; } const [type, args] = tagMatch.slice(1); switch (type) { case 'if': { const tag = new IfTag(); R_LIQUID.lastIndex = tag.openCondition(match[1], args, match.index); tagStack.push(tag); break; } case 'elsif': case 'else': { const tag = tagStack[tagStack.length - 1] as IfTag; R_LIQUID.lastIndex = tag.openCondition(match[1], args, match.index); break; } case 'endif': { const ifTag = tagStack.pop(); if (!ifTag) { // TODO(3y3): make lint rule log.error( `If block must be opened before close${path ? ` in ${bold(path)}` : ''}`, ); break; } ifTag.closeCondition(match[1], match.index); const {result, lastIndex, ifCon} = inlineConditions(input, ifTag, vars, strict); resourcemap(input, ifTag, ifCon, createSourceMapApi(sourceMap)); R_LIQUID.lastIndex = lastIndex; input = result; break; } default: // This is not condition. // Step back last linebreaks to match them on next condition R_LIQUID.lastIndex -= tailLinebreak(match[1]).length; } } if (tagStack.length !== 0) { log.error(`Condition block must be closed${path ? ` in ${bold(path)}` : ''}`); } return input; };