Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/markdown-parser/src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BlockState, InlineState } from './state';
import { linkify } from '@fuyeor/linkify';
import type {
ASTNode,
ASTTransform,
BlockRule,
InlineRule,
Linkifier,
Expand All @@ -23,6 +24,7 @@ function linkifyWithCandidateCheck(text: string): ReturnType<typeof linkify> {
export class MarkdownParser {
#blockRuleMap = new Map<string, BlockRule[]>();
#inlineRuleMap = new Map<string, InlineRule[]>();
#astTransforms: ASTTransform[] = [];
#idSequence = 0;
#isPreflight = false;
readonly #maxNestingDepth: number;
Expand Down Expand Up @@ -82,11 +84,21 @@ export class MarkdownParser {
return this;
}

// Register a post-parse AST transform for extensions such as Twemoji.
addAstTransform(transform: ASTTransform): this {
this.#astTransforms.push(transform);
return this;
}

build(): (content: string) => ASTNode[] {
return (content: string) => {
this.#idSequence = 0;
const state = new BlockState(content);
return this.#parseBlocks(state);
const nodes = this.#parseBlocks(state);
return this.#astTransforms.reduce(
(currentNodes, transform) => transform(currentNodes),
nodes,
);
};
}

Expand Down Expand Up @@ -165,7 +177,9 @@ export class MarkdownParser {
// ` for code block and inline code
firstChar === 96 ||
// ~ equals `
firstChar === 126;
firstChar === 126 ||
// $ for block math
firstChar === 36;

if (mayInterrupt) {
const rules = this.#blockRuleMap.get(
Expand All @@ -182,6 +196,7 @@ export class MarkdownParser {
'blockquote',
'list',
'code_block',
'math_block',
'ffm_blocks',
].includes(rule.name)
) {
Expand Down
70 changes: 66 additions & 4 deletions packages/markdown-parser/src/core/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,29 @@ const getTableAlignment = (align: unknown) =>
? ` align="${align}"`
: '';

const displayLanguageNames: Record<string, string> = {
javascript: 'JavaScript',
typescript: 'TypeScript',
cpp: 'C++',
csharp: 'C#',
xml: 'XML',
json: 'JSON',
yaml: 'YAML',
ffm: 'Fuyeor Flavored Markdown',
fon: 'FON',
fer: 'Fer',
};

// Render a stable fenced-code contract for asynchronous highlight.js processing.
const getCodeLanguage = (lang: unknown): string => {
if (typeof lang !== 'string') return '';
return lang.trim().split(/\s+/u)[0] ?? '';
};

const getDisplayLanguageName = (language: string): string =>
displayLanguageNames[language] ??
(language ? language.charAt(0).toUpperCase() + language.slice(1) : 'Text');

export function render(nodes?: ASTNode[]): string {
let html = '';

Expand All @@ -52,6 +75,19 @@ export function render(nodes?: ASTNode[]): string {
case 'text':
html += node.content ? escapeHtml(node.content) : '';
break;
case 'emoji': {
const emoji = String(node.content ?? '');
const alt = String(node.alt ?? emoji);
const src = String(node.src ?? '');
html += `<img class="emoji" draggable="false" alt="${escapeHtml(alt)}" src="${escapeHtml(src)}"/>`;
break;
}
case 'math_inline':
html += `<span class="math-inline">${escapeHtml(String(node.content ?? ''))}</span>`;
break;
case 'math_block':
html += `<div class="math-block">${escapeHtml(String(node.content ?? ''))}</div>\n`;
break;
case 'bold':
html += `<strong>${render(node.children)}</strong>`;
break;
Expand Down Expand Up @@ -83,13 +119,19 @@ export function render(nodes?: ASTNode[]): string {
: render(node.children);
break;
}
case 'code_block':
case 'code_block': {
const language = getCodeLanguage(node.lang);
const displayLanguage = getDisplayLanguageName(language);
const preAttributes = language
? `class="hljs language-${escapeHtml(language)}" data-language="${escapeHtml(language)}"`
: 'class="hljs language-plaintext"';
html += `<div class="code-block-wrapper">`;
if (node.lang) {
html += `<div class="code-lang">${escapeHtml(node.lang)}</div>`;
if (language) {
html += `<div class="code-lang">${escapeHtml(displayLanguage)}</div>`;
}
html += `<pre><code${node.lang ? ` class="language-${escapeHtml(node.lang)}"` : ''}>${node.content ? escapeHtml(node.content) : ''}\n</code></pre></div>\n`;
html += `<pre ${preAttributes}><code>${escapeHtml(String(node.content ?? ''))}\n</code></pre></div>\n`;
break;
}
case 'list': {
const tag = node.ordered ? 'ol' : 'ul';
const start =
Expand All @@ -102,6 +144,26 @@ export function render(nodes?: ASTNode[]): string {
case 'list_item':
html += `<li>${render(node.children)}</li>\n`;
break;
case 'mermaid':
html += `<div class="language-mermaid">${escapeHtml(String(node.content ?? ''))}</div>\n`;
break;
case 'abc':
html += `<div class="language-abc">${escapeHtml(String(node.content ?? ''))}</div>\n`;
break;
case 'smiles_inline':
html += `<span class="language-smiles smiles-inline" data-smiles="${escapeHtml(String(node.content ?? ''))}"></span>`;
break;
case 'smiles_block': {
const blocks = String(node.content ?? '')
.split('\n')
.filter((line) => line.trim().length > 0)
.map(
(line) =>
`<div class="language-smiles smiles-block" data-smiles="${escapeHtml(line.trim())}"></div>`,
);
html += blocks.length > 0 ? `${blocks.join('\n')}\n` : '';
break;
}
case 'table': {
html += '<table>\n';
if (node.headers) {
Expand Down
4 changes: 4 additions & 0 deletions packages/markdown-parser/src/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
strikeRule,
} from './rules/inlines';
import { ffmBlockRule } from './rules/ffm';
import { latexPlugin } from './plugins/latex';
import { extensionsPlugin } from './plugins/extensions';

export function createMarkdownParser(options: MarkdownParserOptions = {}) {
return (
Expand Down Expand Up @@ -57,6 +59,8 @@ export function createFuyeorMarkdownParser(
.addBlockRule(tableRule)
.addBlockRule(hrRule)
.addBlockRule(blockquoteRule)
.use(latexPlugin)
.use(extensionsPlugin)

// inline order: Inline code -> Links -> Bold/Underline/Strikethrough
.addInlineRule(hardBreakRule)
Expand Down
179 changes: 0 additions & 179 deletions packages/markdown-parser/src/index.spec.ts

This file was deleted.

26 changes: 26 additions & 0 deletions packages/markdown-parser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,37 @@ export {
underlineRule,
strikeRule,
} from './rules/inlines';
export { latexPlugin, latexInlineRule, latexBlockRule } from './plugins/latex';
export {
twemojiPlugin,
nativeEmojiPlugin,
twemojiTransform,
transformTwemojiNode,
getTwemojiUrl,
} from './plugins/twemoji';
export { highlightPlugin, highlightTransform } from './plugins/highlight';
export {
abcPlugin,
abcTransform,
mermaidPlugin,
mermaidTransform,
smilesBlockPlugin,
smilesBlockTransform,
smilesInlinePlugin,
smilesPlugin,
smilesInlineRule,
SPECIAL_BLOCK_LANGUAGES,
specialBlockPlugin,
specialBlockTransform,
transformSpecialBlockNode,
} from './plugins/async-blocks';
export { extensionsPlugin, extensionsTransform } from './plugins/extensions';
export { createMarkdownParser, createFuyeorMarkdownParser } from './default';

export type {
NodeType,
ASTNode,
ASTTransform,
ParserContext,
BlockRule,
InlineRule,
Expand Down
Loading
Loading