From e22fc330c85414c0881613894f1eb4194d37c67e Mon Sep 17 00:00:00 2001 From: purocean Date: Sat, 13 Nov 2021 10:31:52 +0800 Subject: [PATCH] feat(macro): add `$include` to include other document fragments --- src/renderer/plugins/markdown-macro.ts | 99 +++++++++++++++++++------- 1 file changed, 74 insertions(+), 25 deletions(-) diff --git a/src/renderer/plugins/markdown-macro.ts b/src/renderer/plugins/markdown-macro.ts index 9678cbe3..cb490f23 100644 --- a/src/renderer/plugins/markdown-macro.ts +++ b/src/renderer/plugins/markdown-macro.ts @@ -1,40 +1,44 @@ +import frontMatter from 'front-matter' import type { Plugin } from '@fe/context' import { render } from '@fe/services/view' +import { readFile } from '@fe/support/api' +import type { Doc } from '@fe/types' import { getLogger, md5 } from '@fe/utils' +import { dirname, resolve } from '@fe/utils/path' -const logger = getLogger('macro') +type Result = { __macroResult: true, vars?: Record, toString: () => string } +type Expression = { id: string, match: string, exp: string, vars: Record } + +const logger = getLogger('plugin-macro') const AsyncFunction = Object.getPrototypeOf(async () => 0).constructor -let macroCache: Record> = {} +let macroCache: Record> = {} function checkMacroResult (result: any) { - if (typeof result === 'object' || + if ( + result === null || typeof result === 'function' || typeof result === 'symbol' || - typeof result === 'undefined' + typeof result === 'undefined' || + (typeof result === 'object' && (!result.__macroResult)) ) { throw new Error('Macro result type error.') } } -function macro (match: string, vars: Record, cache: Record) { - const expression = match - .substring(2, match.length - 2) - .trim() - .replace(/(?:=\\>|<\\=)/g, x => x.replace('\\', '')) +function macro (expression: Expression, cache: Record): Result { + logger.debug('macro', expression) - const id = md5(expression) - - logger.debug(id, expression, vars) + const { id, match, exp, vars } = expression // async expression result cache if (id in cache) { return cache[id] } - const FunctionConstructor = expression.startsWith('await') ? AsyncFunction : Function + const FunctionConstructor = exp.startsWith('await') ? AsyncFunction : Function - const fun = new FunctionConstructor('vars', `with (vars) { return ${expression}; }`) + const fun = new FunctionConstructor('vars', `with (vars) { return ${exp}; }`) const result = fun(vars) @@ -43,20 +47,20 @@ function macro (match: string, vars: Record, cache: Record { - logger.error(id, e) + logger.error('macro', id, e) return match }).then(res => { cache[id] = res - logger.debug('async', id, 'rerender') + logger.debug('macro', 'async', id, 'rerender') render() }) - return 'macro is running……' + return { __macroResult: true, toString: () => 'macro is running……' } } checkMacroResult(result) - return '' + result + return result } function lineCount (str: string) { @@ -72,9 +76,36 @@ function lineCount (str: string) { return s } +async function include (belongDoc: Doc | undefined | null, path: string): Promise { + if (!belongDoc) { + throw new Error('Current document is null') + } + + try { + const absolutePath = resolve(dirname(belongDoc.path), path) + const { content } = await readFile({ repo: belongDoc.repo, path: absolutePath }) + const fm = frontMatter(content) + + // merge front-matter attributes to current document vars. + const vars = {} + if (fm.attributes && typeof fm.attributes === 'object') { + Object.assign(vars, fm.attributes) + } + + return { __macroResult: true, vars, toString: () => fm.body } + } catch (error: any) { + return error.message + } +} + export default { name: 'markdown-macro', register: ctx => { + // clear cache after view refresh + ctx.registerHook('VIEW_BEFORE_REFRESH', () => { + macroCache = {} + }) + ctx.markdown.registerPlugin(md => { md.core.ruler.after('normalize', 'macro', (state) => { const env = state.env || {} @@ -84,13 +115,14 @@ export default { } const file = env.file || {} - const vars = { + const vars: Record = { + $include: include.bind(null, file), $ctx: ctx, $doc: { basename: file.name ? file.name.substring(0, file.name.lastIndexOf('.')) : '', ...ctx.lib.lodash.pick(env.file, 'name', 'repo', 'path', 'content', 'status') }, - ...env.attributes + ...env.attributes, } if (!env.macroLines) { @@ -106,14 +138,31 @@ export default { let posOffset = 0 state.src = state.src.replace(reg, (match, matchPos) => { try { - const result = macro(match, vars, macroCache[cacheKey]) + console.log('xxx', match, { ...vars }) + + const exp = match + .substring(2, match.length - 2) + .trim() + .replace(/(?:=\\>|<\\=)/g, x => x.replace('\\', '')) + + const id = md5(exp) + + const expression = { id, match, exp, vars } + const result = macro(expression, macroCache[cacheKey]) + + if (result && result.__macroResult && result.vars) { + Object.assign(vars, result.vars) + } + + // result maybe string or other type. + const resultStr = '' + result const matchLine = lineCount(match) - const resultLine = lineCount(result) + const resultLine = lineCount(resultStr) if (resultLine !== matchLine) { const currentLineOffset = (matchLine - resultLine) - const currentPosOffset = (match.length - result.length) + const currentPosOffset = (match.length - resultStr.length) lineOffset += currentLineOffset posOffset += currentPosOffset env.macroLines.push({ @@ -125,11 +174,11 @@ export default { currentPosOffset, currentLineOffset, matchLength: match.length, - resultLength: result.length, + resultLength: resultStr.length, }) } - return result + return resultStr } catch (error) {} return match