Add support for wysiwyg markdown preview

This commit is contained in:
gsb
2026-04-29 05:01:51 +00:00
parent 86d59877f1
commit 4237a3f6a2
10 changed files with 430 additions and 72 deletions
+30 -18
View File
@@ -68,7 +68,7 @@ export class HopDown {
this.blockTags = allTags.filter(tag =>
defaultBlockNames.has(tag.name) || tag.name === 'macro' ||
(!defaultInlineNames.has(tag.name) && !(tag as any).pattern)
(!defaultInlineNames.has(tag.name) && !tag.pattern)
);
// Ensure macro block tag runs after fencedCode but before everything else
@@ -83,7 +83,7 @@ export class HopDown {
});
this.inlineTags = allTags.filter(tag =>
defaultInlineNames.has(tag.name) || (tag as any).pattern
defaultInlineNames.has(tag.name) || tag.pattern
);
this.tags = new Map();
@@ -113,11 +113,11 @@ export class HopDown {
*/
private validateInlineTags(): void {
const withDelimiters = this.inlineTags
.filter(tag => (tag as any).delimiter)
.filter(tag => tag.delimiter)
.map(tag => ({
name: tag.name,
delimiter: (tag as any).delimiter as string,
precedence: (tag as any).precedence as number ?? 50,
delimiter: tag.delimiter as string,
precedence: tag.precedence as number ?? 50,
}));
for (let i = 0; i < withDelimiters.length; i++) {
@@ -159,6 +159,20 @@ export class HopDown {
return this.nodeToMd(container).replace(/\n{3,}/g, '\n\n').trim();
}
/**
* Return the block tags for external iteration (e.g. speculative rendering).
*/
getBlockTags(): Tag[] {
return this.blockTags;
}
/**
* Return the inline tags for external iteration (e.g. speculative rendering).
*/
getInlineTags(): Tag[] {
return this.inlineTags;
}
private processBlocks(md: string): string {
const lines = md.replace(/\r\n/g, '\n').split('\n');
const output: string[] = [];
@@ -186,6 +200,7 @@ export class HopDown {
output.push(result.html);
index = result.end;
} else {
output.push(tag.toHTML(token, this.makeConverter()));
index += token.consumed;
}
@@ -216,13 +231,11 @@ export class HopDown {
// Pass 1: extract links and non-recursive tags into placeholders before escaping
for (const tag of sorted) {
const recursive = (tag as any).recursive ?? true;
const recursive = tag.recursive ?? true;
if (tag.name === 'link') {
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText: string, href: string) => {
// Process link text: restore earlier placeholders, then run inline on any remaining markdown
let inner = linkText;
// Check if link text contains placeholders (already-processed content)
const hasPlaceholders = /\x00P\d+\x00/.test(inner);
if (hasPlaceholders) {
inner = inner.replace(/\x00P(\d+)\x00/g, (__, idx: string) => placeholders[parseInt(idx)]);
@@ -232,9 +245,10 @@ export class HopDown {
placeholders.push('<a href="' + escapeHtml(href) + '">' + inner + '</a>');
return '\x00P' + (placeholders.length - 1) + '\x00';
});
} else if (!recursive && (tag as any).pattern) {
const globalPattern = (tag as any).pattern as RegExp;
} else if (!recursive && tag.pattern) {
const globalPattern = tag.pattern as RegExp;
globalPattern.lastIndex = 0;
text = text.replace(globalPattern, (_, content: string) => {
placeholders.push(tag.toHTML(
{ content, raw: '', consumed: 0 },
@@ -247,21 +261,20 @@ export class HopDown {
text = escapeHtml(text);
// Pass 2: apply recursive tags in precedence order (longest delimiter first).
// Content matched here is already HTML-escaped and has had earlier
// passes applied, so we wrap directly without re-processing.
// Pass 2: apply recursive tags in precedence order.
// Content is already HTML-escaped from pass 1, so we wrap directly
// without re-processing through convert.inline().
for (const tag of sorted) {
const recursive = (tag as any).recursive ?? true;
const recursive = tag.recursive ?? true;
if (tag.name === 'link' || !recursive) {
continue;
}
const globalPattern = (tag as any).pattern as RegExp | undefined;
const globalPattern = tag.pattern as RegExp | undefined;
if (globalPattern) {
globalPattern.lastIndex = 0;
text = text.replace(globalPattern, (_, content: string) => {
// Restore any placeholders in the captured content
const restored = content.replace(/\x00P(\d+)\x00/g, (__, idx: string) => placeholders[parseInt(idx)]);
const htmlTag = (tag as any).name === 'boldItalic'
const htmlTag = tag.name === 'boldItalic'
? null
: ((tag.selector as string) || '').split(',')[0].toLowerCase();
if (tag.name === 'boldItalic') {
@@ -272,7 +285,6 @@ export class HopDown {
}
}
// Restore placeholders
text = text.replace(/\x00P(\d+)\x00/g, (_, index: string) => placeholders[parseInt(index)]);
return text;
}