Skip to content

Commit

Permalink
feat: added abitility to register block directive using config object
Browse files Browse the repository at this point in the history
  • Loading branch information
d3m1d0v committed Nov 7, 2024
1 parent 46b33f3 commit 3d06c73
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 47 deletions.
203 changes: 156 additions & 47 deletions src/helpers/registrars.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,192 @@
/* 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 [email protected]
params.attrs = args.attrs;
}
if (args.dests !== undefined) {
params.dests = buildDests(
// @ts-expect-error types fixed in [email protected]
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 [email protected]
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;
token.block = true;
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);
token.block = true;
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);
token.block = true;
}

if (content) {
token = state.push(content.token + '_open', content.tag, 1);
token.map = [params.startLine + 1, params.endLine - 1];
token.block = true;
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.block = true;
}

token = state.push(container.token + '_close', container.tag, -1);
token.block = true;

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 [email protected]
params.attrs = args.attrs;
}
if (args.dests !== undefined) {
params.dests = buildDests(
// @ts-expect-error types fixed in [email protected]
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 [email protected]
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 {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type {
DirectiveAttrs,
DirectiveDests,
BlockDirectiveParams,
BlockDirectiveConfig,
BlockDirectiveHandler,
InlineDirectiveParams,
InlineDirectiveHandler,
Expand Down
24 changes: 24 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
7 changes: 7 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
}

0 comments on commit 3d06c73

Please sign in to comment.