Files
mnote/rust/crates/mnote-web/browser/sidebar-page-ai-markdown-runtime.js
T

136 lines
4.9 KiB
JavaScript

export function createSidebarPageAiMarkdownRuntime(context) {
const { escapeHtml } = context;
function textFromUnknown(value) {
if (value == null) return '';
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
if (Array.isArray(value)) return value.map(textFromUnknown).filter(Boolean).join(' ');
if (typeof value !== 'object') return '';
var parts = [];
['text', 'title', 'content', 'children', 'blocks', 'body'].forEach(function(key) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
var text = textFromUnknown(value[key]);
if (text) parts.push(text);
}
});
return parts.join(' ');
}
function renderPageAiMarkdownInline(text) {
var html = escapeHtml(String(text || ''));
var codeSpans = [];
html = html.replace(/`([^`\n]+)`/g, function(_, code) {
var key = '\u0000CODE' + codeSpans.length + '\u0000';
codeSpans.push('<code>' + code + '</code>');
return key;
});
html = html.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, function(match, label, href) {
var normalizedHref = normalizePageAiMarkdownHref(href);
if (!normalizedHref) return match;
var citationAttr = isMnoteCitationHref(normalizedHref) ? ' data-page-ai-citation-link="true"' : '';
return '<a href="' + escapeHtml(normalizedHref) + '" target="_blank" rel="noopener noreferrer"' + citationAttr + '>' + label + '</a>';
});
html = html.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
html = html.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>');
codeSpans.forEach(function(value, index) {
html = html.replace('\u0000CODE' + index + '\u0000', value);
});
return html;
}
function normalizePageAiMarkdownHref(value) {
var href = String(value || '').trim()
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
if (!href || /[\u0000-\u001f<>"']/.test(href)) return '';
var lower = href.toLowerCase();
if (lower.startsWith('javascript:') || lower.startsWith('data:') || lower.startsWith('vbscript:')) return '';
if (href.startsWith('/') || href.startsWith('#')) return href;
if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href)) return href;
return '';
}
function isMnoteCitationHref(href) {
try {
var url = new URL(href, window.location.origin);
return url.origin === window.location.origin && url.pathname.startsWith('/documents/');
} catch (_error) {
return false;
}
}
function renderPageAiMarkdown(content) {
var lines = String(content || '').replace(/\r\n?/g, '\n').split('\n');
var blocks = [];
var index = 0;
function isBlockBoundary(line) {
return !line.trim() ||
/^```/.test(line.trim()) ||
/^#{1,6}\s+/.test(line) ||
/^\s*[-*]\s+/.test(line) ||
/^\s*\d+[.)]\s+/.test(line);
}
while (index < lines.length) {
var line = lines[index];
if (!line.trim()) {
index += 1;
continue;
}
if (/^```/.test(line.trim())) {
index += 1;
var codeLines = [];
while (index < lines.length && !/^```/.test(lines[index].trim())) {
codeLines.push(lines[index]);
index += 1;
}
if (index < lines.length) index += 1;
blocks.push('<pre><code>' + escapeHtml(codeLines.join('\n')) + '</code></pre>');
continue;
}
var heading = line.match(/^(#{1,6})\s+(.+)$/);
if (heading) {
var level = Math.min(6, heading[1].length);
blocks.push('<h' + level + '>' + renderPageAiMarkdownInline(heading[2]) + '</h' + level + '>');
index += 1;
continue;
}
if (/^\s*[-*]\s+/.test(line)) {
var unordered = [];
while (index < lines.length && /^\s*[-*]\s+/.test(lines[index])) {
unordered.push('<li>' + renderPageAiMarkdownInline(lines[index].replace(/^\s*[-*]\s+/, '')) + '</li>');
index += 1;
}
blocks.push('<ul>' + unordered.join('') + '</ul>');
continue;
}
if (/^\s*\d+[.)]\s+/.test(line)) {
var ordered = [];
while (index < lines.length && /^\s*\d+[.)]\s+/.test(lines[index])) {
ordered.push('<li>' + renderPageAiMarkdownInline(lines[index].replace(/^\s*\d+[.)]\s+/, '')) + '</li>');
index += 1;
}
blocks.push('<ol>' + ordered.join('') + '</ol>');
continue;
}
var paragraph = [];
while (index < lines.length && !isBlockBoundary(lines[index])) {
paragraph.push(renderPageAiMarkdownInline(lines[index]));
index += 1;
}
if (paragraph.length) {
blocks.push('<p>' + paragraph.join('<br />') + '</p>');
} else {
index += 1;
}
}
return blocks.join('') || escapeHtml(String(content || ''));
}
return {
textFromUnknown,
renderPageAiMarkdown,
renderPageAiMarkdownInline
};
}