diff --git a/src/transform/index.ts b/src/transform/index.ts index 9d7d45df..f5986817 100644 --- a/src/transform/index.ts +++ b/src/transform/index.ts @@ -1,169 +1,43 @@ +import type {OptionsType, OutputType, EnvType} from './typings'; import {bold} from 'chalk'; -import attrs from 'markdown-it-attrs'; -import Token from 'markdown-it/lib/token'; - -import {log, LogLevels} from './log'; -import makeHighlight from './highlight'; -import extractTitle from './title'; -import getHeadings from './headings'; +import {log} from './log'; import liquid from './liquid'; -import sanitizeHtml, {SanitizeOptions} from './sanitize'; - -import notes from './plugins/notes'; -import anchors from './plugins/anchors'; -import code from './plugins/code'; -import cut from './plugins/cut'; -import deflist from './plugins/deflist'; -import term from './plugins/term'; -import file from './plugins/file'; -import imsize from './plugins/imsize'; -import meta from './plugins/meta'; -import sup from './plugins/sup'; -import tabs from './plugins/tabs'; -import video from './plugins/video'; -import monospace from './plugins/monospace'; -import yfmTable from './plugins/table'; -import {initMd} from './md'; -import {MarkdownItPluginCb} from './plugins/typings'; -import type {HighlightLangMap, Heading} from './typings'; - -interface OutputType { - result: { - html: string; - title?: string; - headings: Heading[]; - assets?: unknown[]; - meta?: object; - }; - logs: Record; -} -interface OptionsType { - vars?: Record; - path?: string; - extractTitle?: boolean; - needTitle?: boolean; - allowHTML?: boolean; - linkify?: boolean; - linkifyTlds?: string | string[]; - breaks?: boolean; - conditionsInCode?: boolean; - disableLiquid?: boolean; - leftDelimiter?: string; - rightDelimiter?: string; - isLiquided?: boolean; - needToSanitizeHtml?: boolean; - sanitizeOptions?: SanitizeOptions; - needFlatListHeadings?: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - plugins?: MarkdownItPluginCb[]; - highlightLangs?: HighlightLangMap; - root?: string; - [x: string]: unknown; -} +import initMarkdownit from './md'; -function transform(originInput: string, opts: OptionsType = {}): OutputType { +function applyLiquid(input: string, options: OptionsType) { const { vars = {}, path, - extractTitle: extractTitleOption, - needTitle, - allowHTML = false, - linkify = false, - linkifyTlds, - breaks = true, conditionsInCode = false, - needToSanitizeHtml = false, - sanitizeOptions, - needFlatListHeadings = false, disableLiquid = false, - leftDelimiter = '{', - rightDelimiter = '}', isLiquided = false, - plugins = [ - meta, - deflist, - cut, - notes, - anchors, - tabs, - code, - sup, - video, - monospace, - yfmTable, - file, - imsize, - term, - ], - highlightLangs = {}, - ...customOptions - } = opts; + } = options; - const pluginOptions = { - ...customOptions, - conditionsInCode, - vars, - path, - extractTitle: extractTitleOption, - disableLiquid, - log, - }; + return disableLiquid || isLiquided ? input : liquid(input, vars, path, {conditionsInCode}); +} - const input = - disableLiquid || isLiquided - ? originInput - : liquid(originInput, vars, path, {conditionsInCode}); +function handleError(error: unknown, path?: string): never { + log.error(`Error occurred${path ? ` in ${bold(path)}` : ''}`); - const highlight = makeHighlight(highlightLangs); - const md = initMd({html: allowHTML, linkify, highlight, breaks}); - // Need for ids of headers - md.use(attrs, {leftDelimiter, rightDelimiter}); + throw error; +} - plugins.forEach((plugin) => md.use(plugin, pluginOptions)); +function emitResult(html: string, env: EnvType): OutputType { + return { + result: {...env, html}, + logs: log.get(), + }; +} - if (linkify && linkifyTlds) { - md.linkify.tlds(linkifyTlds, true); - } +// eslint-disable-next-line consistent-return +function transform(originInput: string, options: OptionsType = {}) { + const input = applyLiquid(originInput, options); + const {parse, compile, env} = initMarkdownit(options); try { - let title; - let tokens; - let titleTokens; - const env = {} as {[key: string]: Token[] | unknown}; - - tokens = md.parse(input, env); - - if (extractTitleOption) { - ({title, tokens, titleTokens} = extractTitle(tokens)); - - // title tokens include other tokens that need to be transformed - if (titleTokens.length > 1) { - title = md.renderer.render(titleTokens, md.options, env); - } - } - if (needTitle) { - ({title} = extractTitle(tokens)); - } - - const headings = getHeadings(tokens, needFlatListHeadings); - - // add all term template tokens to the end of the html - const termTokens = (env.termTokens as Token[]) || []; - let html = md.renderer.render([...tokens, ...termTokens], md.options, env); - if (needToSanitizeHtml) { - html = sanitizeHtml(html, sanitizeOptions); - } - - const assets = md.assets; - const meta = md.meta; - - return { - result: {html, title, headings, assets, meta}, - logs: log.get(), - }; - } catch (err) { - log.error(`Error occurred${path ? ` in ${bold(path)}` : ''}`); - throw err; + return emitResult(compile(parse(input)), env); + } catch (error) { + handleError(error, options.path); } } diff --git a/src/transform/md.ts b/src/transform/md.ts index 98470728..d3c4ddee 100644 --- a/src/transform/md.ts +++ b/src/transform/md.ts @@ -1,6 +1,117 @@ -import DefaultMarkdownIt, {Options} from 'markdown-it'; -import {MarkdownIt} from './typings'; +import type {MarkdownIt, OptionsType, EnvType} from './typings'; +import type Token from 'markdown-it/lib/token'; -export const initMd = ({html, linkify, highlight, breaks}: Partial) => { - return new DefaultMarkdownIt({html, linkify, highlight, breaks}) as MarkdownIt; -}; +import DefaultMarkdownIt from 'markdown-it'; +import DefaultPlugins from './plugins'; +import {log} from './log'; +import makeHighlight from './highlight'; +import attrs from 'markdown-it-attrs'; +import extractTitle from './title'; +import getHeadings from './headings'; +import sanitizeHtml from './sanitize'; + +function initMarkdownit(options: OptionsType) { + const {allowHTML = false, linkify = false, breaks = true, highlightLangs = {}} = options; + + const highlight = makeHighlight(highlightLangs); + const md = new DefaultMarkdownIt({html: allowHTML, linkify, highlight, breaks}) as MarkdownIt; + const env = { + // TODO: move md.meta directly to env + get meta() { + return md.meta; + }, + // TODO: move md.assets directly to env + get assets() { + return md.assets; + }, + headings: [], + title: '', + } as EnvType; + + initPlugins(md, options); + + const parse = initParser(md, options, env); + const compile = initCompiler(md, options, env); + + return {parse, compile, env}; +} + +function initPlugins(md: MarkdownIt, options: OptionsType) { + const { + vars = {}, + path, + extractTitle, + conditionsInCode = false, + disableLiquid = false, + linkify = false, + linkifyTlds, + leftDelimiter = '{', + rightDelimiter = '}', + plugins = DefaultPlugins, + ...customOptions + } = options; + + const pluginOptions = { + ...customOptions, + conditionsInCode, + vars, + path, + extractTitle, + disableLiquid, + log, + }; + + // Need for ids of headers + md.use(attrs, {leftDelimiter, rightDelimiter}); + + plugins.forEach((plugin) => md.use(plugin, pluginOptions)); + + if (linkify && linkifyTlds) { + md.linkify.tlds(linkifyTlds, true); + } +} + +function initParser(md: MarkdownIt, options: OptionsType, env: EnvType) { + return (input: string) => { + const {extractTitle: extractTitleOption, needTitle, needFlatListHeadings = false} = options; + + let tokens = md.parse(input, env); + + if (extractTitleOption) { + const {title, tokens: slicedTokens, titleTokens} = extractTitle(tokens); + + tokens = slicedTokens; + + // title tokens include other tokens that need to be transformed + if (titleTokens.length > 1) { + env.title = md.renderer.render(titleTokens, md.options, env); + } else { + env.title = title; + } + } + + if (needTitle) { + env.title = extractTitle(tokens).title; + } + + env.headings = getHeadings(tokens, needFlatListHeadings); + + return tokens; + }; +} + +function initCompiler(md: MarkdownIt, options: OptionsType, env: EnvType<{termTokens?: Token[]}>) { + const {needToSanitizeHtml = false, sanitizeOptions} = options; + + return (tokens: Token[]) => { + // TODO: define postprocess step on term plugin + const {termTokens = []} = env; + delete env.termTokens; + + const html = md.renderer.render([...tokens, ...termTokens], md.options, env); + + return needToSanitizeHtml ? sanitizeHtml(html, sanitizeOptions) : html; + }; +} + +export = initMarkdownit; diff --git a/src/transform/plugins.ts b/src/transform/plugins.ts new file mode 100644 index 00000000..116bbab7 --- /dev/null +++ b/src/transform/plugins.ts @@ -0,0 +1,35 @@ +import type {MarkdownItPluginCb} from './plugins/typings'; + +import meta from './plugins/meta'; +import deflist from './plugins/deflist'; +import cut from './plugins/cut'; +import notes from './plugins/notes'; +import anchors from './plugins/anchors'; +import tabs from './plugins/tabs'; +import code from './plugins/code'; +import sup from './plugins/sup'; +import video from './plugins/video'; +import monospace from './plugins/monospace'; +import yfmTable from './plugins/table'; +import file from './plugins/file'; +import imsize from './plugins/imsize'; +import term from './plugins/term'; + +const defaultPlugins = [ + meta, + deflist, + cut, + notes, + anchors, + tabs, + code, + sup, + video, + monospace, + yfmTable, + file, + imsize, + term, +] as MarkdownItPluginCb[]; + +export = defaultPlugins; diff --git a/src/transform/plugins/typings.ts b/src/transform/plugins/typings.ts index a0c0546c..3a22af81 100644 --- a/src/transform/plugins/typings.ts +++ b/src/transform/plugins/typings.ts @@ -1,5 +1,5 @@ -import {Logger} from '../log'; -import {MarkdownIt} from '../typings'; +import type {Logger} from '../log'; +import type {MarkdownIt} from '../typings'; export interface MarkdownItPluginOpts { path: string; @@ -9,7 +9,6 @@ export interface MarkdownItPluginOpts { isLintRun: boolean; } -export type MarkdownItPluginCb = ( - md: MarkdownIt, - opts: T & MarkdownItPluginOpts, -) => void; +export type MarkdownItPluginCb = { + (md: MarkdownIt, opts: T & MarkdownItPluginOpts): void; +}; diff --git a/src/transform/typings.ts b/src/transform/typings.ts index 3633b022..431657d7 100644 --- a/src/transform/typings.ts +++ b/src/transform/typings.ts @@ -1,6 +1,9 @@ import {LanguageFn} from 'highlight.js'; import DefaultMarkdownIt from 'markdown-it'; import DefaultStateCore from 'markdown-it/lib/rules_core/state_core'; +import {SanitizeOptions} from './sanitize'; +import {MarkdownItPluginCb} from './plugins/typings'; +import {LogLevels} from './log'; export interface MarkdownIt extends DefaultMarkdownIt { assets?: string[]; @@ -18,3 +21,41 @@ export type Heading = { level: number; items?: Heading[]; }; + +export interface OptionsType { + vars?: Record; + path?: string; + extractTitle?: boolean; + needTitle?: boolean; + allowHTML?: boolean; + linkify?: boolean; + linkifyTlds?: string | string[]; + breaks?: boolean; + conditionsInCode?: boolean; + disableLiquid?: boolean; + leftDelimiter?: string; + rightDelimiter?: string; + isLiquided?: boolean; + needToSanitizeHtml?: boolean; + sanitizeOptions?: SanitizeOptions; + needFlatListHeadings?: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + plugins?: MarkdownItPluginCb[]; + highlightLangs?: HighlightLangMap; + root?: string; + [x: string]: unknown; +} + +export interface OutputType { + result: { + html: string; + } & EnvType; + logs: Record; +} + +export type EnvType = { + title?: string; + headings: Heading[]; + assets?: unknown[]; + meta?: object; +} & Extras;