145 lines
5.7 KiB
JavaScript
145 lines
5.7 KiB
JavaScript
(function (root, factory) {
|
|
const renderMarkdown = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = renderMarkdown;
|
|
root.renderMarkdown = renderMarkdown;
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
function escapeHtml(value) {
|
|
return String(value || '').replace(/[&<>"']/g, (character) => ({
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
})[character]);
|
|
}
|
|
|
|
function renderInline(value) {
|
|
const code = [];
|
|
let rendered = escapeHtml(value).replace(/`([^`\n]+)`/g, (_match, body) => {
|
|
const marker = '\u0000CODE' + code.length + '\u0000';
|
|
code.push('<code>' + body + '</code>');
|
|
return marker;
|
|
});
|
|
rendered = rendered
|
|
.replace(/\[([^\]\n]+)\]\(([^\s)]+)(?:\s+"[^&]*?")?\)/g, (_match, label, escapedUrl) => {
|
|
const url = escapedUrl.replace(/&/g, '&');
|
|
const safe = /^(https?:|mailto:)/i.test(url) || /^(\/|#|\.\/|\.\.\/)/.test(url);
|
|
if (!safe) return label;
|
|
return '<a href="' + escapedUrl + '" target="_blank" rel="noopener noreferrer">' + label + '</a>';
|
|
})
|
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>');
|
|
return rendered.replace(/\u0000CODE(\d+)\u0000/g, (_match, index) => code[Number(index)]);
|
|
}
|
|
|
|
function renderList(lines, options, firstTaskIndex) {
|
|
const taskList = lines.every(line => /^[-*+]\s+\[[ xX]\]\s+/.test(line));
|
|
const items = lines.map((line, offset) => {
|
|
let body = line.replace(/^[-*+]\s+/, '');
|
|
if (!taskList) return '<li>' + renderInline(body) + '</li>';
|
|
const checked = /^\[[xX]\]\s+/.test(body);
|
|
body = body.replace(/^\[[ xX]\]\s+/, '');
|
|
const control = options.interactiveTasks ?
|
|
' class="task-list-toggle" data-task-index="' + (firstTaskIndex + offset) +
|
|
'" aria-label="Mark ' + escapeHtml(body) + (checked ? ' incomplete"' : ' complete"') :
|
|
' disabled';
|
|
const manage = options.interactiveTasks && options.manageTasks ?
|
|
'<button type="button" class="task-list-manage" data-task-index="' + (firstTaskIndex + offset) +
|
|
'" data-task-label="' + escapeHtml(body) + '" aria-label="Manage step: ' + escapeHtml(body) + '"' +
|
|
(offset === 0 ? ' data-task-first="true"' : '') +
|
|
(offset === lines.length - 1 ? ' data-task-last="true"' : '') + '>Manage</button>' : '';
|
|
return '<li class="task-list-item"><input type="checkbox"' + control +
|
|
(checked ? ' checked' : '') + '> ' + renderInline(body) + manage + '</li>';
|
|
}).join('');
|
|
return '<ul' + (taskList ? ' class="task-list"' : '') + '>' + items + '</ul>';
|
|
}
|
|
|
|
function renderMarkdown(raw, options = {}) {
|
|
const lines = String(raw || '').replace(/\r\n?/g, '\n').split('\n');
|
|
const output = [];
|
|
let index = 0;
|
|
let taskIndex = 0;
|
|
while (index < lines.length) {
|
|
const line = lines[index];
|
|
if (!line.trim()) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
const fence = line.match(/^```([A-Za-z0-9_-]*)\s*$/);
|
|
if (fence) {
|
|
const body = [];
|
|
index += 1;
|
|
while (index < lines.length && !/^```\s*$/.test(lines[index])) {
|
|
body.push(lines[index]);
|
|
index += 1;
|
|
}
|
|
if (index < lines.length) index += 1;
|
|
const language = fence[1] ? ' class="language-' + fence[1] + '"' : '';
|
|
output.push('<pre><code' + language + '>' + escapeHtml(body.join('\n')) + '</code></pre>');
|
|
continue;
|
|
}
|
|
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
if (heading) {
|
|
const level = heading[1].length;
|
|
output.push('<h' + level + '>' + renderInline(heading[2]) + '</h' + level + '>');
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (/^>\s?/.test(line)) {
|
|
const quote = [];
|
|
while (index < lines.length && /^>\s?/.test(lines[index])) {
|
|
quote.push(lines[index].replace(/^>\s?/, ''));
|
|
index += 1;
|
|
}
|
|
output.push('<blockquote><p>' + renderInline(quote.join('\n')).replace(/\n/g, '<br>') + '</p></blockquote>');
|
|
continue;
|
|
}
|
|
if (/^[-*+]\s+/.test(line)) {
|
|
const list = [];
|
|
while (index < lines.length && /^[-*+]\s+/.test(lines[index])) {
|
|
list.push(lines[index]);
|
|
index += 1;
|
|
}
|
|
output.push(renderList(list, options, taskIndex));
|
|
if (list.every(candidate => /^[-*+]\s+\[[ xX]\]\s+/.test(candidate))) {
|
|
taskIndex += list.length;
|
|
}
|
|
continue;
|
|
}
|
|
const paragraph = [];
|
|
while (index < lines.length && lines[index].trim() &&
|
|
!/^```/.test(lines[index]) && !/^(#{1,6})\s+/.test(lines[index]) &&
|
|
!/^>\s?/.test(lines[index]) && !/^[-*+]\s+/.test(lines[index])) {
|
|
paragraph.push(lines[index]);
|
|
index += 1;
|
|
}
|
|
output.push('<p>' + renderInline(paragraph.join('\n')).replace(/\n/g, '<br>') + '</p>');
|
|
}
|
|
return output.join('');
|
|
}
|
|
|
|
renderMarkdown.toggleTask = function toggleTask(raw, targetIndex, checked) {
|
|
const parts = String(raw || '').split(/(\r\n|\n|\r)/);
|
|
let fenced = false;
|
|
let taskIndex = 0;
|
|
for (let index = 0; index < parts.length; index += 2) {
|
|
const line = parts[index];
|
|
if (/^\s*```/.test(line)) {
|
|
fenced = !fenced;
|
|
continue;
|
|
}
|
|
if (fenced) continue;
|
|
const task = line.match(/^([-*+]\s+\[)([ xX])(\])/);
|
|
if (!task) continue;
|
|
if (taskIndex === Number(targetIndex)) {
|
|
parts[index] = task[1] + (checked ? 'x' : ' ') + task[3] + line.slice(task[0].length);
|
|
return parts.join('');
|
|
}
|
|
taskIndex += 1;
|
|
}
|
|
return parts.join('');
|
|
};
|
|
|
|
return renderMarkdown;
|
|
});
|