UNPKG

@diplodoc/transform

Version:

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

216 lines (174 loc) 6.33 kB
import type {Dictionary} from 'lodash'; import type {ArgvSettings} from './services/argv'; import cloneDeepWith from 'lodash/cloneDeepWith'; import {composeFrontMatter, extractFrontMatter} from '../frontmatter'; import applySubstitutions from './substitutions'; import {prepareSourceMap} from './sourceMap'; import applyCycles from './cycles'; import applyConditions from './conditions'; import ArgvService from './services/argv'; const fence = '```'; const find = (open: string, close: string, string: string, index: number) => { const start = string.indexOf(open, index); const end = start > -1 ? string.indexOf(close, start + open.length) : -1; return [start, end]; }; const replace = ( open: string, close: string, value: (string: string) => string, string: string, ) => { let result = ''; let carriage = 0; let [start, end] = find(open, close, string, carriage); while (start > -1 && end > -1) { const fragment = string.slice(start + open.length, end); result += string.slice(carriage, start) + open + value(fragment) + close; carriage = end + close.length; [start, end] = find(open, close, string, carriage); } result += string.slice(carriage); return result; }; function saveCode( str: string, vars: Record<string, unknown>, codes: string[], path?: string, substitutions?: boolean, ) { return replace( fence, fence, (code) => { const codeWithVars = substitutions ? applySubstitutions(code, vars, path) : code; const index = codes.push(codeWithVars) - 1; /* Keep the same count of lines to avoid transformation of the source map */ const codeLines = codeWithVars.split('\n'); const emptyLines = codeLines.length > 1 ? '\n'.repeat(codeLines.length) : ''; return `${index}${emptyLines}`; }, str, ); } function repairCode(str: string, codes: string[]) { return replace(fence, fence, (code) => codes[Number(code)], str); } function liquidSnippet< B extends boolean = false, C = B extends false ? string : {output: string; sourceMap: Dictionary<string>}, >( originInput: string, vars: Record<string, unknown>, path?: string, settings?: ArgvSettings & {withSourceMap?: B}, ): C { const { cycles = true, conditions = true, substitutions = true, conditionsInCode = false, useLegacyConditions = false, keepNotVar = false, withSourceMap, } = settings || {}; ArgvService.init({ cycles, conditions, substitutions, conditionsInCode, useLegacyConditions, keepNotVar, withSourceMap, }); const codes: string[] = []; let output = conditionsInCode ? originInput : saveCode(originInput, vars, codes, path, substitutions); let sourceMap: Record<number, number> = {}; if (withSourceMap) { const lines = output.split('\n'); sourceMap = lines.reduce((acc: Record<number, number>, _cur, index) => { acc[index + 1] = index + 1; return acc; }, {}); } if (cycles) { output = applyCycles(output, vars, path, {sourceMap}); } if (conditions) { const strict = conditions === 'strict'; output = applyConditions(output, vars, path, {sourceMap, strict, useLegacyConditions}); } if (substitutions) { output = applySubstitutions(output, vars, path); } if (!conditionsInCode && typeof output === 'string') { output = repairCode(output, codes); } codes.length = 0; if (withSourceMap) { return { output, sourceMap: prepareSourceMap(sourceMap), } as unknown as C; } return output as unknown as C; } function linesCount(content: string) { let count = 1, index = -1; while ((index = content.indexOf('\n', index + 1)) > -1) { count++; } return count; } function liquidDocument< B extends boolean = false, C = B extends false ? string : {output: string; sourceMap: Dictionary<string>}, >( input: string, vars: Record<string, unknown>, path?: string, settings?: ArgvSettings & {withSourceMap?: B}, ): C { const [frontMatter, strippedContent] = extractFrontMatter(input, path); const liquidedFrontMatter = cloneDeepWith(frontMatter, (value: unknown) => typeof value === 'string' ? liquidSnippet(value, vars, path, {...settings, withSourceMap: false}) : undefined, ); const liquidedResult = liquidSnippet(strippedContent, vars, path, settings); const liquidedContent = typeof liquidedResult === 'object' ? liquidedResult.output : liquidedResult; const output = composeFrontMatter(liquidedFrontMatter, liquidedContent as string); if (typeof liquidedResult === 'object') { const inputLinesCount = linesCount(input); const outputLinesCount = linesCount(output); const contentLinesCount = linesCount(strippedContent); const contentLinesDiff = linesCount(liquidedContent as string) - contentLinesCount; const fullLinesDiff = outputLinesCount - inputLinesCount; // Always >= 0 const sourceOffset = inputLinesCount - contentLinesCount; // Content lines diff already counted in source map const resultOffset = fullLinesDiff - contentLinesDiff; liquidedResult.sourceMap = Object.fromEntries( Object.entries(liquidedResult.sourceMap).map(([lineInResult, lineInSource]) => [ (Number(lineInResult) + resultOffset).toString(), (Number(lineInSource) + sourceOffset).toString(), ]), ); } // typeof check for better inference; the catch is that return of liquidSnippet can be an // object even with source maps off, see `substitutions.test.ts` return (settings?.withSourceMap && typeof liquidedResult === 'object' ? { output, sourceMap: liquidedResult.sourceMap, } : output) as unknown as C; } // both default and named exports for convenience export {liquidDocument, liquidSnippet}; export default liquidDocument;