UNPKG

@diplodoc/transform

Version:

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

244 lines (204 loc) 6.45 kB
import {bold} from 'chalk'; import {log} from '../log'; import {evalExp} from './evaluation'; import {tagLine, variable} from './lexical'; import {getPreparedLeftContent} from './utils'; import {createSourceMapApi, getLineNumber} from './sourceMap'; import applyLiquid from './index'; type Options = { firstLineNumber: number; lastLineNumber: number; resFirstLineNumber: number; resLastLineNumber: number; contentLinesTotal: number; linesTotal: number; sourceMap: Record<number, number>; }; function changeSourceMap({ firstLineNumber, lastLineNumber, resFirstLineNumber, resLastLineNumber, contentLinesTotal, linesTotal, sourceMap, }: Options) { if (!sourceMap) { return; } const isInlineTag = firstLineNumber === lastLineNumber; const {moveLines, removeLine} = createSourceMapApi(sourceMap); if (isInlineTag || !resFirstLineNumber) { return; } const offsetRestLines = contentLinesTotal - (lastLineNumber - firstLineNumber + 1); // Move condition's content to the top const offsetContentLines = firstLineNumber - resFirstLineNumber; moveLines({ start: resFirstLineNumber, end: resLastLineNumber - 1, offset: offsetContentLines, withReplace: true, }); // Remove tags removeLine(firstLineNumber); removeLine(lastLineNumber); // Offset the rest lines moveLines({start: lastLineNumber + 1, end: linesTotal, offset: offsetRestLines}); } type Args2 = { forTag: Tag; vars: Record<string, unknown>; content: string; match: RegExpExecArray; path?: string; lastIndex: number; sourceMap: Record<number, number>; linesTotal: number; }; function inlineConditions({ forTag, vars, content, match, path, lastIndex, sourceMap, linesTotal, }: Args2) { let res = ''; const firstLineNumber = getLineNumber(content, forTag.startPos); const lastLineNumber = getLineNumber(content, lastIndex); const forRawLastIndex = forTag.startPos + forTag.forRaw.length; const contentLastIndex = match.index; const forTemplate = content.substring(forRawLastIndex, contentLastIndex); const resFirstLineNumber = getLineNumber(content, forRawLastIndex + 1); const resLastLineNumber = getLineNumber(content, contentLastIndex + 1); let collection = evalExp(forTag.collectionName, vars); if (!collection || !Array.isArray(collection)) { collection = []; log.error(`${bold(forTag.collectionName)} is undefined or not iterable`); } collection.forEach((item) => { const newVars = {...vars, [forTag.variableName]: item}; res += applyLiquid(forTemplate, newVars, path).trimRight(); }); const contentLinesTotal = res.split('\n').length - 1; changeSourceMap({ firstLineNumber, lastLineNumber, resFirstLineNumber, resLastLineNumber, linesTotal, sourceMap, contentLinesTotal, }); const preparedLeftContent = getPreparedLeftContent({ content, tagStartPos: forTag.startPos, tagContent: res, }); let shift = 0; if ( res === '' && preparedLeftContent[preparedLeftContent.length - 1] === '\n' && content[lastIndex] === '\n' ) { shift = 1; } if (res !== '') { if (res[0] === ' ' || res[0] === '\n') { res = res.substring(1); } } const leftPart = preparedLeftContent + res; return { result: leftPart + content.substring(lastIndex + shift), idx: leftPart.length, }; } type Tag = { item: string; variableName: string; collectionName: string; startPos: number; forRaw: string; }; export = function cycles( originInput: string, vars: Record<string, unknown>, path?: string, settings: {sourceMap?: Record<number, number>} = {}, ) { const {sourceMap} = settings; const R_LIQUID = /({%-?([\s\S]*?)-?%})/g; const FOR_SYNTAX = new RegExp(`(\\w+)\\s+in\\s+(${variable.source})`); let match; const tagStack: Tag[] = []; let input = originInput; let countSkippedInnerTags = 0; let linesTotal = originInput.split('\n').length; 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 'for': { if (tagStack.length) { countSkippedInnerTags += 1; break; } const matches = args.match(FOR_SYNTAX); if (!matches) { log.error(`Incorrect syntax in if condition${path ? ` in ${bold(path)}` : ''}`); break; } const [variableName, collectionName] = matches.slice(1); tagStack.push({ item: args, variableName, collectionName, startPos: match.index, forRaw: match[1], }); break; } case 'endfor': { if (countSkippedInnerTags > 0) { countSkippedInnerTags -= 1; break; } const forTag = tagStack.pop(); if (!forTag) { log.error( `For block must be opened before close${path ? ` in ${bold(path)}` : ''}`, ); break; } const {idx, result} = inlineConditions({ forTag, vars, content: input, match, path, lastIndex: R_LIQUID.lastIndex, sourceMap: sourceMap || {}, linesTotal, }); R_LIQUID.lastIndex = idx; input = result; linesTotal = result.split('\n').length; break; } } } if (tagStack.length !== 0) { log.error(`For block must be closed${path ? ` in ${bold(path)}` : ''}`); } return input; };