2026-06-01 09:29:12 +08:00
|
|
|
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 = [];
|
2026-06-08 20:35:49 +08:00
|
|
|
var htmlSpans = [];
|
|
|
|
|
function stashHtml(value) {
|
|
|
|
|
var key = '\u0000HTML' + htmlSpans.length + '\u0000';
|
|
|
|
|
htmlSpans.push(value);
|
|
|
|
|
return key;
|
|
|
|
|
}
|
2026-06-01 09:29:12 +08:00
|
|
|
html = html.replace(/`([^`\n]+)`/g, function(_, code) {
|
|
|
|
|
var key = '\u0000CODE' + codeSpans.length + '\u0000';
|
|
|
|
|
codeSpans.push('<code>' + code + '</code>');
|
|
|
|
|
return key;
|
|
|
|
|
});
|
2026-06-08 20:35:49 +08:00
|
|
|
html = html.replace(/\[((?:\\.|[^\]\n])+)\]\(([^)\n]+)\)/g, function(match, label, href) {
|
2026-06-05 23:00:53 +08:00
|
|
|
var normalizedHref = normalizePageAiMarkdownHref(href);
|
|
|
|
|
if (!normalizedHref) return match;
|
|
|
|
|
var citationAttr = isMnoteCitationHref(normalizedHref) ? ' data-page-ai-citation-link="true"' : '';
|
2026-06-08 20:35:49 +08:00
|
|
|
return stashHtml('<a href="' + escapeHtml(normalizedHref) + '" target="_blank" rel="noopener noreferrer"' + citationAttr + '>' + unescapePageAiMarkdownLabel(label) + '</a>');
|
|
|
|
|
});
|
|
|
|
|
html = html.replace(/(^|[\s(])((?:https?:\/\/|\/documents\/|mnote:\/\/open(?:Resource)?)[^\s<>()]+[^\s<>().,;:!?])/g, function(_, prefix, href) {
|
|
|
|
|
var normalizedHref = normalizePageAiMarkdownHref(href);
|
|
|
|
|
if (!normalizedHref) return prefix + href;
|
|
|
|
|
var citationAttr = isMnoteCitationHref(normalizedHref) ? ' data-page-ai-citation-link="true"' : '';
|
|
|
|
|
return prefix + stashHtml('<a href="' + escapeHtml(normalizedHref) + '" target="_blank" rel="noopener noreferrer"' + citationAttr + '>' + escapeHtml(normalizedHref) + '</a>');
|
2026-06-05 23:00:53 +08:00
|
|
|
});
|
2026-06-01 09:29:12 +08:00
|
|
|
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);
|
|
|
|
|
});
|
2026-06-08 20:35:49 +08:00
|
|
|
htmlSpans.forEach(function(value, index) {
|
|
|
|
|
html = html.replace('\u0000HTML' + index + '\u0000', value);
|
|
|
|
|
});
|
2026-06-01 09:29:12 +08:00
|
|
|
return html;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 23:00:53 +08:00
|
|
|
function normalizePageAiMarkdownHref(value) {
|
|
|
|
|
var href = String(value || '').trim()
|
2026-06-08 20:35:49 +08:00
|
|
|
.replace(/</g, '<')
|
|
|
|
|
.replace(/>/g, '>')
|
2026-06-05 23:00:53 +08:00
|
|
|
.replace(/"/g, '"')
|
|
|
|
|
.replace(/'/g, "'");
|
2026-06-08 20:35:49 +08:00
|
|
|
while (href.includes('&')) href = href.replace(/&/g, '&');
|
|
|
|
|
if (href.startsWith('<') && href.endsWith('>')) href = href.slice(1, -1).trim();
|
2026-06-05 23:00:53 +08:00
|
|
|
if (!href || /[\u0000-\u001f<>"']/.test(href)) return '';
|
|
|
|
|
var lower = href.toLowerCase();
|
|
|
|
|
if (lower.startsWith('javascript:') || lower.startsWith('data:') || lower.startsWith('vbscript:')) return '';
|
2026-06-08 20:35:49 +08:00
|
|
|
if (lower.startsWith('mnote://open')) href = normalizePageAiMnoteOpenHref(href);
|
|
|
|
|
href = normalizePageAiLegacyCitationHref(href);
|
2026-06-05 23:00:53 +08:00
|
|
|
if (href.startsWith('/') || href.startsWith('#')) return href;
|
|
|
|
|
if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href)) return href;
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 20:35:49 +08:00
|
|
|
function unescapePageAiMarkdownLabel(value) {
|
|
|
|
|
return String(value || '').replace(/\\([\\\[\]()*_`])/g, '$1');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePageAiMnoteOpenHref(href) {
|
|
|
|
|
try {
|
|
|
|
|
var url = new URL(href);
|
|
|
|
|
if (url.protocol !== 'mnote:' || (url.hostname !== 'open' && url.hostname !== 'openResource')) return href;
|
|
|
|
|
var path = String(url.searchParams.get('path') || url.searchParams.get('resourcePath') || '').trim();
|
|
|
|
|
if (!path) return '';
|
|
|
|
|
var params = new URLSearchParams();
|
|
|
|
|
var rootUri = String(url.searchParams.get('rootUri') || '').trim();
|
|
|
|
|
if (!rootUri) {
|
|
|
|
|
try {
|
|
|
|
|
rootUri = new URL(window.location.href).searchParams.get('rootUri') || '';
|
|
|
|
|
} catch (_locationError) {}
|
|
|
|
|
}
|
|
|
|
|
if (rootUri) params.set('rootUri', rootUri);
|
|
|
|
|
params.set('path', path);
|
|
|
|
|
['page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
|
|
|
|
|
var value = String(url.searchParams.get(key) || '').trim();
|
|
|
|
|
if (value) params.set(key, value);
|
|
|
|
|
});
|
|
|
|
|
return '/api/local-folder/files/open?' + params.toString();
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
return href;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePageAiLegacyCitationHref(href) {
|
2026-06-05 23:00:53 +08:00
|
|
|
try {
|
|
|
|
|
var url = new URL(href, window.location.origin);
|
2026-06-08 20:35:49 +08:00
|
|
|
var unwrappedHref = normalizePageAiSearchWrappedCitationHref(url);
|
|
|
|
|
if (unwrappedHref) return normalizePageAiLegacyCitationHref(unwrappedHref);
|
|
|
|
|
if (!isPageAiSameMnoteOrigin(url) && !isPageAiPortableMnoteCitationUrl(url)) return href;
|
|
|
|
|
if (url.pathname === '/api/local-folder/files/open') return url.pathname + url.search;
|
|
|
|
|
if (!url.pathname.startsWith('/documents/')) return href;
|
|
|
|
|
if (isPageAiUnsafeCitationDocumentId(url)) {
|
|
|
|
|
var fileOpenHref = normalizePageAiCitationFileOpenHref(url);
|
|
|
|
|
if (fileOpenHref) return fileOpenHref;
|
|
|
|
|
}
|
|
|
|
|
var decodedHash = '';
|
|
|
|
|
try {
|
|
|
|
|
decodedHash = decodeURIComponent(url.hash || '');
|
|
|
|
|
} catch (_decodeError) {
|
|
|
|
|
decodedHash = url.hash || '';
|
|
|
|
|
}
|
|
|
|
|
var marker = '#resource-tab-';
|
|
|
|
|
var markerIndex = decodedHash.indexOf(marker);
|
|
|
|
|
if (markerIndex < 0 || url.searchParams.get('resourceTab')) {
|
|
|
|
|
return url.pathname + url.search + url.hash;
|
|
|
|
|
}
|
|
|
|
|
var identity = decodedHash.slice(markerIndex + marker.length).replace(/^#+/, '').trim();
|
|
|
|
|
if (!identity.startsWith('resource:file:')) return url.pathname + url.search + url.hash;
|
|
|
|
|
url.searchParams.set('resourceTab', identity);
|
|
|
|
|
if (!url.searchParams.get('sourceKind')) url.searchParams.set('sourceKind', 'local_folder');
|
|
|
|
|
var rootUri = String(url.searchParams.get('rootUri') || '').trim();
|
|
|
|
|
var prefix = 'resource:file:' + rootUri + ':';
|
|
|
|
|
if (rootUri && identity.startsWith(prefix) && !url.searchParams.get('resourcePath')) {
|
|
|
|
|
url.searchParams.set('resourcePath', identity.slice(prefix.length).replace(/^\/+/, ''));
|
|
|
|
|
}
|
|
|
|
|
url.hash = '';
|
|
|
|
|
return url.pathname + url.search;
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
return href;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePageAiSearchWrappedCitationHref(url) {
|
|
|
|
|
var raw = '';
|
|
|
|
|
['wd', 'q', 'query'].some(function(key) {
|
|
|
|
|
raw = String(url.searchParams.get(key) || '').trim();
|
|
|
|
|
return Boolean(raw);
|
|
|
|
|
});
|
|
|
|
|
if (!raw) return '';
|
|
|
|
|
if (/^documents\//i.test(raw)) raw = '/' + raw;
|
|
|
|
|
if (!/^\/documents\//i.test(raw) && !/^https?:\/\/[^/]+\/documents\//i.test(raw) && !/^mnote:\/\/open/i.test(raw)) return '';
|
|
|
|
|
if (/^mnote:\/\/open/i.test(raw)) return normalizePageAiMnoteOpenHref(raw);
|
|
|
|
|
var nested = new URL(raw, window.location.origin);
|
|
|
|
|
if (!nested.pathname.startsWith('/documents/')) return '';
|
|
|
|
|
['sourceKind', 'rootUri', 'workspaceId', 'resourceTab', 'resourcePath', 'page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
|
|
|
|
|
if (nested.searchParams.get(key)) return;
|
|
|
|
|
var value = String(url.searchParams.get(key) || '').trim();
|
|
|
|
|
if (value) nested.searchParams.set(key, value);
|
|
|
|
|
});
|
|
|
|
|
return nested.pathname + nested.search;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizePageAiCitationFileOpenHref(url) {
|
|
|
|
|
var rootUri = String(url.searchParams.get('rootUri') || '').trim();
|
|
|
|
|
var path = String(url.searchParams.get('resourcePath') || '').trim();
|
|
|
|
|
if (!rootUri || !path) return '';
|
|
|
|
|
var params = new URLSearchParams();
|
|
|
|
|
params.set('rootUri', rootUri);
|
|
|
|
|
params.set('path', path);
|
|
|
|
|
['page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
|
|
|
|
|
var value = String(url.searchParams.get(key) || '').trim();
|
|
|
|
|
if (value) params.set(key, value);
|
|
|
|
|
});
|
|
|
|
|
return '/api/local-folder/files/open?' + params.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPageAiUnsafeCitationDocumentId(url) {
|
|
|
|
|
try {
|
|
|
|
|
var raw = String(url.pathname || '').replace(/^\/documents\//, '').split('/')[0] || '';
|
|
|
|
|
if (!raw) return false;
|
|
|
|
|
var decoded = decodeURIComponent(raw);
|
|
|
|
|
return decoded.indexOf('%') >= 0 || decoded.indexOf('\uFFFD') >= 0;
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPageAiPortableMnoteCitationUrl(url) {
|
|
|
|
|
try {
|
|
|
|
|
if (url.pathname === '/api/local-folder/files/open') {
|
|
|
|
|
return Boolean(String(url.searchParams.get('rootUri') || '').trim()) &&
|
|
|
|
|
Boolean(String(url.searchParams.get('path') || '').trim());
|
|
|
|
|
}
|
|
|
|
|
if (!url.pathname.startsWith('/documents/')) return false;
|
|
|
|
|
var resourceTab = String(url.searchParams.get('resourceTab') || '').trim();
|
|
|
|
|
return String(url.searchParams.get('sourceKind') || '').trim() === 'local_folder' ||
|
|
|
|
|
Boolean(String(url.searchParams.get('rootUri') || '').trim()) ||
|
|
|
|
|
Boolean(String(url.searchParams.get('resourcePath') || '').trim()) ||
|
|
|
|
|
resourceTab.startsWith('resource:file:');
|
2026-06-05 23:00:53 +08:00
|
|
|
} catch (_error) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 20:35:49 +08:00
|
|
|
function isPageAiMarkdownTableDivider(line) {
|
|
|
|
|
var cells = splitPageAiMarkdownTableRow(line);
|
|
|
|
|
if (cells.length < 2) return false;
|
|
|
|
|
return cells.every(function(cell) {
|
|
|
|
|
return /^:?-{3,}:?$/.test(cell.trim());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function splitPageAiMarkdownTableRow(line) {
|
|
|
|
|
var text = String(line || '').trim();
|
|
|
|
|
if (!text.includes('|')) return [];
|
|
|
|
|
if (text.startsWith('|')) text = text.slice(1);
|
|
|
|
|
if (text.endsWith('|')) text = text.slice(0, -1);
|
|
|
|
|
return text.split('|').map(function(cell) { return cell.trim(); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderPageAiMarkdownTable(lines, startIndex) {
|
|
|
|
|
if (startIndex + 1 >= lines.length || !isPageAiMarkdownTableDivider(lines[startIndex + 1])) return null;
|
|
|
|
|
var header = splitPageAiMarkdownTableRow(lines[startIndex]);
|
|
|
|
|
var divider = splitPageAiMarkdownTableRow(lines[startIndex + 1]);
|
|
|
|
|
if (!header.length || header.length !== divider.length) return null;
|
|
|
|
|
var rows = [];
|
|
|
|
|
var index = startIndex + 2;
|
|
|
|
|
while (index < lines.length && lines[index].trim() && lines[index].includes('|')) {
|
|
|
|
|
var cells = splitPageAiMarkdownTableRow(lines[index]);
|
|
|
|
|
if (!cells.length) break;
|
|
|
|
|
rows.push(cells);
|
|
|
|
|
index += 1;
|
|
|
|
|
}
|
|
|
|
|
function cellHtml(tag, value) {
|
|
|
|
|
return '<' + tag + '>' + renderPageAiMarkdownInline(value) + '</' + tag + '>';
|
|
|
|
|
}
|
|
|
|
|
var head = '<thead><tr>' + header.map(function(cell) { return cellHtml('th', cell); }).join('') + '</tr></thead>';
|
|
|
|
|
var body = rows.length
|
|
|
|
|
? '<tbody>' + rows.map(function(row) {
|
|
|
|
|
return '<tr>' + header.map(function(_, cellIndex) { return cellHtml('td', row[cellIndex] || ''); }).join('') + '</tr>';
|
|
|
|
|
}).join('') + '</tbody>'
|
|
|
|
|
: '';
|
|
|
|
|
return {
|
|
|
|
|
html: '<div class="wolai-page-ai-markdown-table-wrap"><table>' + head + body + '</table></div>',
|
|
|
|
|
nextIndex: index
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isMnoteCitationHref(href) {
|
|
|
|
|
try {
|
|
|
|
|
var url = new URL(href, window.location.origin);
|
|
|
|
|
if (!isPageAiSameMnoteOrigin(url)) return false;
|
|
|
|
|
return url.pathname.startsWith('/documents/') || url.pathname === '/api/local-folder/files/open';
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPageAiSameMnoteOrigin(url) {
|
|
|
|
|
try {
|
|
|
|
|
var current = new URL(window.location.origin);
|
|
|
|
|
if (url.origin === current.origin) return true;
|
|
|
|
|
if (url.hostname === 'mnote.local') return true;
|
|
|
|
|
var localNames = ['localhost', '127.0.0.1', '::1', 'mnote.local'];
|
|
|
|
|
return localNames.indexOf(url.hostname) >= 0 &&
|
|
|
|
|
localNames.indexOf(current.hostname) >= 0 &&
|
|
|
|
|
String(url.port || defaultPortForProtocol(url.protocol)) === String(current.port || defaultPortForProtocol(current.protocol));
|
|
|
|
|
} catch (_error) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function defaultPortForProtocol(protocol) {
|
|
|
|
|
return protocol === 'https:' ? '443' : protocol === 'http:' ? '80' : '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 09:29:12 +08:00
|
|
|
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;
|
|
|
|
|
}
|
2026-06-08 20:35:49 +08:00
|
|
|
if (/^\s*-{3,}\s*$/.test(line)) {
|
|
|
|
|
blocks.push('<hr />');
|
|
|
|
|
index += 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
var table = renderPageAiMarkdownTable(lines, index);
|
|
|
|
|
if (table) {
|
|
|
|
|
blocks.push(table.html);
|
|
|
|
|
index = table.nextIndex;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-06-01 09:29:12 +08:00
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|