@diplodoc/transform
Version:
A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML
244 lines (204 loc) • 6.45 kB
text/typescript
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;
};