From 8e42ad42b862b7fce2c3ad027f42b7086df071a4 Mon Sep 17 00:00:00 2001 From: d3m1d0v Date: Thu, 7 Nov 2024 18:53:25 +0300 Subject: [PATCH] feat: added abitility to register block directive using config object --- src/helpers/registrars.ts | 197 +++++++++++++++++++++++++++++--------- src/index.ts | 1 + src/types.ts | 24 +++++ src/utils.ts | 7 ++ 4 files changed, 182 insertions(+), 47 deletions(-) create mode 100644 src/utils.ts diff --git a/src/helpers/registrars.ts b/src/helpers/registrars.ts index ed7f0a9..ec4fd3c 100644 --- a/src/helpers/registrars.ts +++ b/src/helpers/registrars.ts @@ -1,83 +1,186 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import type MarkdownIt from 'markdown-it'; -import type {MarkdownItWithDirectives} from 'markdown-it-directive'; import type { + DirectiveBlockHandlerArgs, + DirectiveInlineHandlerArgs, + MarkdownItWithDirectives, +} from 'markdown-it-directive'; +import type { + BlockDirectiveConfig, BlockDirectiveHandler, BlockDirectiveParams, + DirectiveAttrs, DirectiveDests, DirectiveDestsOrig, InlineDirectiveHandler, InlineDirectiveParams, } from '../types'; +import {isFunction, isString} from '../utils'; + +import {createBlockInlineToken, tokenizeBlockContent} from './tokenizers'; + export function registerInlineDirective( md: MarkdownIt, name: string, handler: InlineDirectiveHandler, ): void { (md as MarkdownItWithDirectives).inlineDirectives[name] = (args) => { - const params: InlineDirectiveParams = { - startPos: args.directiveStart, - endPos: args.directiveEnd, - }; - if (args.attrs !== undefined) { - // @ts-expect-error types fixed in markdown-it-directive@2.0.3 - params.attrs = args.attrs; - } - if (args.dests !== undefined) { - params.dests = buildDests( - // @ts-expect-error types fixed in markdown-it-directive@2.0.3 - args.dests, - ); - } - if (args.content !== undefined) { - params.content = { - raw: args.content, - startPos: args.contentStart!, - endPos: args.contentEnd!, - }; - } + const params: InlineDirectiveParams = buildInlineParams(args); return handler(args.state, params); }; } +export function registerBlockDirective(md: MarkdownIt, config: BlockDirectiveConfig): void; export function registerBlockDirective( md: MarkdownIt, name: string, handler: BlockDirectiveHandler, +): void; +export function registerBlockDirective( + md: MarkdownIt, + nameOrConfig: string | BlockDirectiveConfig, + maybeHandler?: BlockDirectiveHandler, ): void { + const [name, handler]: [string, BlockDirectiveHandler] = isString(nameOrConfig) + ? [nameOrConfig, maybeHandler!] + : [nameOrConfig.name, buildContainerHandler(nameOrConfig)]; + (md as MarkdownItWithDirectives).blockDirectives[name] = (args) => { - const params: BlockDirectiveParams = { - startLine: args.directiveStartLine, - endLine: args.directiveEndLine, - }; - if (args.attrs !== undefined) { - // @ts-expect-error types fixed in markdown-it-directive@2.0.3 - params.attrs = args.attrs; + const params: BlockDirectiveParams = buildBlockParams(args); + return handler(args.state, params); + }; +} + +function buildContainerHandler(config: BlockDirectiveConfig): BlockDirectiveHandler { + if (config.type !== 'container') { + throw new Error(`Unknown type in ${config.name} directive config: ${config.type}`); + } + + return (state, params) => { + if (!params.content) { + return false; } - if (args.dests !== undefined) { - params.dests = buildDests( - // @ts-expect-error fix in https://github.com/hilookas/markdown-it-directive/pull/8 - args.dests, - ); + if ( + config.inlineContent && + !params.inlineContent && + config.inlineContent.required !== false + ) { + return false; } - if (args.inlineContent !== undefined) { - params.inlineContent = { - raw: args.inlineContent, - startPos: args.inlineContentStart!, - endPos: args.inlineContentEnd!, - }; + if (!config.match(params, state)) { + return false; } - if (args.content !== undefined) { - params.content = { - raw: args.content, - startLine: args.contentStartLine!, - endLine: args.contentEndLine!, - }; + + const {container, inlineContent, content, contentTokenizer} = config; + + let token = state.push(container.token + '_open', container.tag, 1); + token.map = [params.startLine, params.endLine]; + token.markup = ':::' + config.name; + if (container.attrs) { + const attrs: DirectiveAttrs = isFunction(container.attrs) + ? container.attrs(params) + : container.attrs; + token.attrs = Object.entries(attrs); } - return handler(args.state, params); + + if (inlineContent) { + token = state.push(inlineContent.token + '_open', inlineContent.tag, 1); + if (inlineContent.attrs) { + const attrs: DirectiveAttrs = isFunction(inlineContent.attrs) + ? inlineContent.attrs(params) + : inlineContent.attrs; + token.attrs = Object.entries(attrs); + } + + token = createBlockInlineToken(state, params); + + token = state.push(inlineContent.token + '_close', inlineContent.tag, -1); + } + + if (content) { + token = state.push(content.token + '_open', content.tag, 1); + token.map = [params.startLine + 1, params.endLine - 1]; + if (content.attrs) { + const attrs: DirectiveAttrs = isFunction(content.attrs) + ? content.attrs(params) + : content.attrs; + token.attrs = Object.entries(attrs); + } + } + + if (contentTokenizer) { + contentTokenizer(state, params.content, params); + } else { + tokenizeBlockContent(state, params.content, config.name + '-directive'); + } + + if (content) { + token = state.push(content.token + '_close', content.tag, -1); + } + + token = state.push(container.token + '_close', container.tag, -1); + + return true; + }; +} + +function buildInlineParams(args: DirectiveInlineHandlerArgs): InlineDirectiveParams { + const params: InlineDirectiveParams = { + startPos: args.directiveStart, + endPos: args.directiveEnd, }; + if (args.attrs !== undefined) { + // @ts-expect-error types fixed in markdown-it-directive@2.0.3 + params.attrs = args.attrs; + } + if (args.dests !== undefined) { + params.dests = buildDests( + // @ts-expect-error types fixed in markdown-it-directive@2.0.3 + args.dests, + ); + } + if (args.content !== undefined) { + params.content = { + raw: args.content, + startPos: args.contentStart!, + endPos: args.contentEnd!, + }; + } + return params; +} + +function buildBlockParams(args: DirectiveBlockHandlerArgs): BlockDirectiveParams { + const params: BlockDirectiveParams = { + startLine: args.directiveStartLine, + endLine: args.directiveEndLine, + }; + if (args.attrs !== undefined) { + // @ts-expect-error types fixed in markdown-it-directive@2.0.3 + params.attrs = args.attrs; + } + if (args.dests !== undefined) { + params.dests = buildDests( + // @ts-expect-error fix in https://github.com/hilookas/markdown-it-directive/pull/8 + args.dests, + ); + } + if (args.inlineContent !== undefined) { + params.inlineContent = { + raw: args.inlineContent, + startPos: args.inlineContentStart!, + endPos: args.inlineContentEnd!, + }; + } + if (args.content !== undefined) { + params.content = { + raw: args.content, + startLine: args.contentStartLine!, + endLine: args.contentEndLine!, + }; + } + return params; } function buildDests(orig: DirectiveDestsOrig): DirectiveDests { diff --git a/src/index.ts b/src/index.ts index 4584e2f..5832b50 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ export type { DirectiveAttrs, DirectiveDests, BlockDirectiveParams, + BlockDirectiveConfig, BlockDirectiveHandler, InlineDirectiveParams, InlineDirectiveHandler, diff --git a/src/types.ts b/src/types.ts index b189175..a2b30b5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -44,3 +44,27 @@ export type InlineDirectiveParams = { export type BlockDirectiveHandler = (state: StateBlock, params: BlockDirectiveParams) => boolean; export type InlineDirectiveHandler = (state: StateInline, params: InlineDirectiveParams) => boolean; + +type TokensDesc = { + tag: string; + token: string; + attrs?: DirectiveAttrs | ((params: BlockDirectiveParams) => DirectiveAttrs); +}; + +export type BlockDirectiveConfig = { + name: string; + type: 'container'; + match: (params: BlockDirectiveParams, state: StateBlock) => boolean; + container: TokensDesc; + inlineContent?: TokensDesc & { + /** @default true */ + required?: boolean; + }; + content?: TokensDesc; + /** If not passed – default tokenizer will be used */ + contentTokenizer?: ( + state: StateBlock, + content: BlockContent, + params: BlockDirectiveParams, + ) => void; +}; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..0ebd2cd --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,7 @@ +export function isFunction(arg: unknown): arg is Function { + return typeof arg === 'function'; +} + +export function isString(arg: unknown): arg is string { + return typeof arg === 'string'; +}