plugin infrastruktur
This commit is contained in:
1 parent
1545b90f39
commit
3f4ba3c0f4
35 files changed
+1303
-195
No files matched your search
@@ -11,6 +11,32 @@ marked.use({ breaks: true, gfm: true, renderer: {
|
||||
}
|
||||
}});
|
||||
|
||||
// ── Built-in fenced block handlers ──
|
||||
// Registered through the same plugin API so plugins and core use one path.
|
||||
if (typeof window !== 'undefined' && window.hyprcrd?.markdown) {
|
||||
const md = window.hyprcrd.markdown;
|
||||
|
||||
// ```gallery — image grid
|
||||
md.registerFencedBlock('gallery', (content, { esc: e }) => {
|
||||
const imgs = [];
|
||||
const imgRe = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
||||
let m;
|
||||
while ((m = imgRe.exec(content)) !== null) imgs.push({ alt: m[1], src: m[2].split(/\s/)[0] });
|
||||
if (!imgs.length) return '';
|
||||
return '<div class="md-gallery">' + imgs.map(i =>
|
||||
`<div class="md-gallery-item"><img src="${i.src}" alt="${e(i.alt)}" loading="lazy"></div>`
|
||||
).join('') + '</div>';
|
||||
});
|
||||
|
||||
// ```tree / ```bullets — custom list rendering (handled via post-processor)
|
||||
const listHandler = (kind) => (content, { normalizeListBlock: norm }) => {
|
||||
const normalized = norm(content);
|
||||
return `\n<!-- list:${kind} -->\n${normalized}\n`;
|
||||
};
|
||||
md.registerFencedBlock('tree', listHandler('tree'));
|
||||
md.registerFencedBlock('bullets', listHandler('bullets'));
|
||||
}
|
||||
|
||||
const purifyConfig = {
|
||||
ALLOWED_TAGS: ['h1','h2','h3','h4','h5','h6','p','br','hr','ul','ol','li','a','strong','em','del','code','pre','blockquote','table','thead','tbody','tr','th','td','img','input','span','div','audio','source'],
|
||||
ALLOWED_ATTR: ['href','src','alt','width','height','type','checked','disabled','class','style','data-card-id','title','controls','preload','target','rel','align'],
|
||||
@@ -79,27 +105,32 @@ export function renderMd(src) {
|
||||
'</tbody></table>\n';
|
||||
});
|
||||
|
||||
// ── Fenced block registry ─────────────────────────────────────
|
||||
// Apply all registered ```<lang> handlers (built-in + plugins).
|
||||
// Plugins register via window.hyprcrd.markdown.registerFencedBlock().
|
||||
const fencedBlocks = (typeof window !== 'undefined' && window.hyprcrd?.markdown?.getFencedBlocks)
|
||||
? window.hyprcrd.markdown.getFencedBlocks()
|
||||
: [];
|
||||
for (const { lang, handler } of fencedBlocks) {
|
||||
// Escape regex metachars in lang (allow simple identifiers)
|
||||
const safeLang = String(lang).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const re = new RegExp('```' + safeLang + '\\s*\\n([\\s\\S]*?)(?:```|$)', 'g');
|
||||
processed = processed.replace(re, (_, content) => {
|
||||
try { return handler(content, { esc, normalizeListBlock }); }
|
||||
catch (err) { console.error(`[markdown fenced ${lang}]`, err); return ''; }
|
||||
});
|
||||
}
|
||||
|
||||
// ── Plugin pre-processors ─────────────────────────────────────
|
||||
const preProcessors = (typeof window !== 'undefined' && window.hyprcrd?.markdown?.getPreProcessors)
|
||||
? window.hyprcrd.markdown.getPreProcessors()
|
||||
: [];
|
||||
for (const fn of preProcessors) {
|
||||
try { processed = fn(processed) ?? processed; }
|
||||
catch (err) { console.error('[markdown pre]', err); }
|
||||
}
|
||||
|
||||
processed = processed
|
||||
// ASCII canvas block: ```ascii
|
||||
.replace(/```ascii\s*\n([\s\S]*?)(?:```|$)/g, (_, content) => {
|
||||
return `<pre class="ascii-art">${esc(content.replace(/\s+$/, ''))}</pre>`;
|
||||
})
|
||||
// Gallery fenced block: ```gallery
|
||||
.replace(/```gallery\s*\n([\s\S]*?)(?:```|$)/g, (_, content) => {
|
||||
const imgs = [];
|
||||
const imgRe = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
||||
let m;
|
||||
while ((m = imgRe.exec(content)) !== null) imgs.push({ alt: m[1], src: m[2].split(/\s/)[0] });
|
||||
if (!imgs.length) return '';
|
||||
return '<div class="md-gallery">' + imgs.map(i =>
|
||||
`<div class="md-gallery-item"><img src="${i.src}" alt="${esc(i.alt)}" loading="lazy"></div>`
|
||||
).join('') + '</div>';
|
||||
})
|
||||
// Custom fenced list blocks: ```tree / ```bullets with or without closing ```
|
||||
.replace(/```(tree|bullets)\s*\n([\s\S]*?)(?:```|$)/g, (_, kind, content) => {
|
||||
const normalized = normalizeListBlock(content);
|
||||
return `\n<!-- list:${kind} -->\n${normalized}\n`;
|
||||
})
|
||||
// Help parser: ensure a list can start after text without requiring manual blank line
|
||||
.replace(/([^\n])\n(?=(?: {0,3}[-*+]\s| {0,3}\d+\.\s))/g, '$1\n\n')
|
||||
// Preserve extra blank lines as <br> spacers (3+ newlines = 1+ extra blank lines)
|
||||
@@ -124,6 +155,15 @@ export function renderMd(src) {
|
||||
html = html.replace(/<!--\s*list:tree\s*-->\s*<ul>/g, '<ul class="tree-list">');
|
||||
html = html.replace(/<!--\s*list:bullets\s*-->\s*<ul>/g, '<ul class="bullet-list">');
|
||||
|
||||
// ── Plugin post-processors (run on HTML, before sanitize) ──
|
||||
const postProcessors = (typeof window !== 'undefined' && window.hyprcrd?.markdown?.getPostProcessors)
|
||||
? window.hyprcrd.markdown.getPostProcessors()
|
||||
: [];
|
||||
for (const fn of postProcessors) {
|
||||
try { html = fn(html) ?? html; }
|
||||
catch (err) { console.error('[markdown post]', err); }
|
||||
}
|
||||
|
||||
html = typeof DOMPurify !== 'undefined' ? DOMPurify.sanitize(html, purifyConfig) : html;
|
||||
return html;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user