Feat v0.6.5 renderer pipeline (#40)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m48s
CI/CD / test-sqlite (push) Successful in 2m52s
CI/CD / build-and-deploy (push) Successful in 1m20s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #40.
This commit is contained in:
2026-03-31 16:37:33 +00:00
committed by xcaliber
parent 36d6158940
commit 81c28a50bf
33 changed files with 2089 additions and 1974 deletions

View File

@@ -69,100 +69,13 @@
}
// ═══════════════════════════════════════════
// Markdown renderer (inline, no deps)
// Markdown rendering via sw.markdown
// ═══════════════════════════════════════════
// Preload marked + DOMPurify so renderSync works in useMemo
sw.markdown.preload();
function renderMarkdown(src) {
if (!src) return '';
var lines = src.split('\n');
var out = [];
var inCode = false;
var inList = false;
var listTag = '';
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
// fenced code blocks
if (line.trim().startsWith('```')) {
if (inCode) {
out.push('</code></pre>');
inCode = false;
} else {
if (inList) { out.push('</' + listTag + '>'); inList = false; }
out.push('<pre><code>');
inCode = true;
}
continue;
}
if (inCode) {
out.push(escHtml(line) + '\n');
continue;
}
// blank line
if (!line.trim()) {
if (inList) { out.push('</' + listTag + '>'); inList = false; }
continue;
}
// headings
if (line.startsWith('### ')) { closeList(); out.push('<h3>' + inline(line.slice(4)) + '</h3>'); continue; }
if (line.startsWith('## ')) { closeList(); out.push('<h2>' + inline(line.slice(3)) + '</h2>'); continue; }
if (line.startsWith('# ')) { closeList(); out.push('<h1>' + inline(line.slice(2)) + '</h1>'); continue; }
// hr
if (/^[-*_]{3,}\s*$/.test(line)) { closeList(); out.push('<hr>'); continue; }
// blockquote
if (line.startsWith('> ')) { closeList(); out.push('<blockquote><p>' + inline(line.slice(2)) + '</p></blockquote>'); continue; }
// unordered list
if (/^[\-*+]\s/.test(line)) {
if (!inList || listTag !== 'ul') { closeList(); out.push('<ul>'); inList = true; listTag = 'ul'; }
out.push('<li>' + inline(line.replace(/^[\-*+]\s/, '')) + '</li>');
continue;
}
// ordered list
if (/^\d+\.\s/.test(line)) {
if (!inList || listTag !== 'ol') { closeList(); out.push('<ol>'); inList = true; listTag = 'ol'; }
out.push('<li>' + inline(line.replace(/^\d+\.\s/, '')) + '</li>');
continue;
}
// paragraph
closeList();
out.push('<p>' + inline(line) + '</p>');
}
if (inCode) out.push('</code></pre>');
if (inList) out.push('</' + listTag + '>');
return out.join('\n');
function closeList() { if (inList) { out.push('</' + listTag + '>'); inList = false; } }
}
function escHtml(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function inline(s) {
s = escHtml(s);
// inline code
s = s.replace(/`([^`]+)`/g, '<code>$1</code>');
// bold
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
s = s.replace(/__(.+?)__/g, '<strong>$1</strong>');
// italic
s = s.replace(/\*(.+?)\*/g, '<em>$1</em>');
s = s.replace(/_(.+?)_/g, '<em>$1</em>');
// links
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
// wikilinks: [[Note Title]] → clickable internal link
s = s.replace(/\[\[([^\]]+)\]\]/g, function(match, linkText) {
return '<a class="wikilink" data-wikilink="' + linkText.trim() + '" href="#">' + linkText.trim() + '</a>';
});
return s;
function _escAttr(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
@@ -854,13 +767,13 @@
}, [note ? note.id : null, saveCount]);
var previewHtml = useMemo(function() {
var h = renderMarkdown(body);
var h = sw.markdown.renderSync(body, { sanitize: true });
// mark unresolved wikilinks
for (var i = 0; i < outgoingLinks.length; i++) {
var link = outgoingLinks[i];
if (!link.target_id) {
var search = 'class="wikilink" data-wikilink="' + escHtml(link.link_text) + '"';
var replace = 'class="wikilink wikilink--unresolved" data-wikilink="' + escHtml(link.link_text) + '"';
var search = 'class="wikilink" data-wikilink="' + _escAttr(link.link_text) + '"';
var replace = 'class="wikilink wikilink--unresolved" data-wikilink="' + _escAttr(link.link_text) + '"';
h = h.split(search).join(replace);
}
}
@@ -884,6 +797,13 @@
return function() { el.removeEventListener('click', handleClick); };
}, [viewMode, onNavigateToTitle]);
// ── Run post-renderers (mermaid, katex, etc.) on preview ──
useEffect(function() {
var el = previewRef.current;
if (!el || viewMode === 'edit') return;
sw.renderers.runPostRenderers(el);
}, [previewHtml, viewMode]);
// ── Synced scroll in split mode ──
useEffect(function() {
if (viewMode !== 'split' || !cmEditorRef.current) return;