350 lines
9.7 KiB
TypeScript
350 lines
9.7 KiB
TypeScript
// @ts-nocheck
|
||
|
||
|
||
let _maskingPidMap = null;
|
||
|
||
export function setMaskingPidMap(pidMap) {
|
||
_maskingPidMap = (pidMap && typeof pidMap === 'object') ? pidMap : null;
|
||
}
|
||
|
||
export function clearMaskingPidMap() {
|
||
_maskingPidMap = null;
|
||
}
|
||
|
||
function _unmaskText(s) {
|
||
if (!_maskingPidMap) return s;
|
||
let r = String(s);
|
||
|
||
r = r.replace(/\[(?:NAME|STATION|CAR|TRAIN|TEAM):(emp_[a-z0-9]+)\]/gi, (m, pid) => {
|
||
const e = _maskingPidMap[pid];
|
||
return e?.name ? e.name : m;
|
||
});
|
||
|
||
r = r.replace(/\[ID:(emp_[a-z0-9]+)\]/gi, (m, pid) => {
|
||
const e = _maskingPidMap[pid];
|
||
return e?.id != null ? String(e.id) : m;
|
||
});
|
||
|
||
r = r.replace(/\bemp_[a-z0-9]+\b/gi, (m) => {
|
||
const e = _maskingPidMap[m];
|
||
return e?.id != null ? String(e.id) : m;
|
||
});
|
||
return r;
|
||
}
|
||
|
||
export function render(text) {
|
||
if (!text) return '';
|
||
|
||
|
||
const unmasked = _maskingPidMap ? _unmaskText(text) : text;
|
||
let s = escapeHtml(normalizeTableMarkup(unmasked));
|
||
|
||
|
||
const codeBlocks = [];
|
||
s = s.replace(/```(\w*)\n?([\s\S]*?)```/g, (m, lang, code) => {
|
||
const idx = codeBlocks.length;
|
||
codeBlocks.push(`<pre><code>${code.trim()}</code></pre>`);
|
||
return `\x00CB${idx}\x00`;
|
||
});
|
||
|
||
const inlineCodes = [];
|
||
s = s.replace(/`([^`\n]+)`/g, (m, code) => {
|
||
const idx = inlineCodes.length;
|
||
inlineCodes.push(`<code>${code}</code>`);
|
||
return `\x00IC${idx}\x00`;
|
||
});
|
||
|
||
|
||
s = s.replace(/^### (.+)$/gm, '<h3>$1</h3>');
|
||
s = s.replace(/^## (.+)$/gm, '<h2>$1</h2>');
|
||
s = s.replace(/^# (.+)$/gm, '<h1>$1</h1>');
|
||
|
||
|
||
s = s.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
|
||
|
||
|
||
s = s.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<em>$2</em>');
|
||
|
||
|
||
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||
|
||
s = renderBlocks(s);
|
||
|
||
|
||
s = s.replace(/\x00CB(\d+)\x00/g, (m, idx) => codeBlocks[idx]);
|
||
s = s.replace(/\x00IC(\d+)\x00/g, (m, idx) => inlineCodes[idx]);
|
||
|
||
return s;
|
||
}
|
||
|
||
function renderBlocks(s) {
|
||
const lines = s.split('\n');
|
||
const out = [];
|
||
let i = 0;
|
||
|
||
while (i < lines.length) {
|
||
const line = lines[i];
|
||
|
||
if (!line.trim()) {
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
if (/^\x00CB\d+\x00$/.test(line.trim())) {
|
||
out.push(line.trim());
|
||
i++;
|
||
continue;
|
||
}
|
||
|
||
if (isListItemLine(line)) {
|
||
const collected = [];
|
||
while (i < lines.length) {
|
||
const L = lines[i];
|
||
if (!L.trim()) {
|
||
|
||
let j = i + 1;
|
||
while (j < lines.length && !lines[j].trim()) j++;
|
||
if (j < lines.length && isListItemLine(lines[j])) {
|
||
i = j;
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
if (isListItemLine(L) || isListContinuation(L)) {
|
||
collected.push(L);
|
||
i++;
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
out.push(renderList(collected));
|
||
continue;
|
||
}
|
||
|
||
if (/^> /.test(line)) {
|
||
const quoteLines = [];
|
||
while (i < lines.length && /^> /.test(lines[i])) {
|
||
quoteLines.push(lines[i].replace(/^> /, ''));
|
||
i++;
|
||
}
|
||
out.push(`<blockquote>${quoteLines.join('<br/>')}</blockquote>`);
|
||
continue;
|
||
}
|
||
|
||
|
||
if (isTableRow(line) && i + 1 < lines.length) {
|
||
const next = lines[i + 1];
|
||
if (isTableSeparator(next) || isImplicitTableBody(line, next)) {
|
||
const headerLine = lines[i];
|
||
let separatorLine = next;
|
||
i += 1;
|
||
if (isTableSeparator(next)) {
|
||
i += 1;
|
||
} else {
|
||
|
||
const cols = parseTableRow(headerLine).length;
|
||
separatorLine = '|' + Array(cols).fill(' --- ').join('|') + '|';
|
||
}
|
||
const bodyLines = [];
|
||
while (i < lines.length && isTableRow(lines[i])) {
|
||
bodyLines.push(lines[i]);
|
||
i++;
|
||
}
|
||
out.push(renderTable(headerLine, separatorLine, bodyLines));
|
||
continue;
|
||
}
|
||
}
|
||
|
||
|
||
const para = [];
|
||
while (i < lines.length) {
|
||
const L = lines[i];
|
||
if (!L.trim()) break;
|
||
if (isListItemLine(L)) break;
|
||
if (/^> /.test(L)) break;
|
||
if (/^\x00CB\d+\x00$/.test(L.trim())) break;
|
||
|
||
if (isTableRow(L) && i + 1 < lines.length
|
||
&& (isTableSeparator(lines[i + 1]) || isImplicitTableBody(L, lines[i + 1]))) break;
|
||
if (/^<h[1-3]>/.test(L)) {
|
||
if (para.length) break;
|
||
out.push(L);
|
||
i++;
|
||
|
||
continue;
|
||
}
|
||
para.push(L);
|
||
i++;
|
||
}
|
||
if (para.length) {
|
||
|
||
if (para.every(l => /^<h[1-3]>/.test(l))) {
|
||
out.push(para.join('\n'));
|
||
} else {
|
||
out.push(`<p>${para.join('<br/>')}</p>`);
|
||
}
|
||
}
|
||
}
|
||
|
||
return out.join('\n');
|
||
}
|
||
|
||
function isListItemLine(line) {
|
||
return /^(\s*)([-*]|\d+\.) /.test(line);
|
||
}
|
||
|
||
function normalizeTableMarkup(text) {
|
||
return String(text || '')
|
||
.split('\n')
|
||
.map((line) => {
|
||
if (!/[||]/.test(line)) return line;
|
||
return line
|
||
.replace(/\uFF5C/g, '|')
|
||
.replace(/[-—–]/g, '-');
|
||
})
|
||
.join('\n');
|
||
}
|
||
|
||
function isTableRow(line) {
|
||
if (!line) return false;
|
||
const trimmed = line.trim();
|
||
if (!trimmed.includes('|')) return false;
|
||
|
||
const cells = parseTableRow(trimmed);
|
||
return cells.length >= 2;
|
||
}
|
||
|
||
function isImplicitTableBody(headerLine, nextLine) {
|
||
if (!isTableRow(headerLine) || !isTableRow(nextLine) || isTableSeparator(nextLine)) return false;
|
||
const a = parseTableRow(headerLine);
|
||
const b = parseTableRow(nextLine);
|
||
return a.length >= 2 && a.length === b.length;
|
||
}
|
||
|
||
function isTableSeparator(line) {
|
||
if (!line) return false;
|
||
const trimmed = line.trim();
|
||
if (!trimmed.includes('|') || !trimmed.includes('-')) return false;
|
||
|
||
if (!/^[\s|:\-]+$/.test(trimmed)) return false;
|
||
|
||
const cells = parseTableRow(trimmed);
|
||
return cells.length >= 2 && cells.every(c => /^:?-+:?$/.test(c.trim()));
|
||
}
|
||
|
||
function parseTableRow(line) {
|
||
return line.trim()
|
||
.replace(/^\|/, '')
|
||
.replace(/\|$/, '')
|
||
.split('|')
|
||
.map(c => c.trim());
|
||
}
|
||
|
||
function renderTable(headerLine, separatorLine, bodyLines) {
|
||
const headers = parseTableRow(headerLine);
|
||
const separators = parseTableRow(separatorLine);
|
||
const aligns = separators.map(sep => {
|
||
const s = sep.trim();
|
||
if (/^:-+$/.test(s)) return 'left';
|
||
if (/^-+:$/.test(s)) return 'right';
|
||
if (/^:-+:$/.test(s)) return 'center';
|
||
return null;
|
||
});
|
||
|
||
const cellTag = (isHeader, idx) => {
|
||
const align = aligns[idx];
|
||
const style = align ? ` style="text-align:${align}"` : '';
|
||
const tag = isHeader ? 'th' : 'td';
|
||
return `<${tag}${style}>`;
|
||
};
|
||
const closeTag = (isHeader) => isHeader ? '</th>' : '</td>';
|
||
|
||
let html = '<table><thead><tr>';
|
||
headers.forEach((h, idx) => {
|
||
html += `${cellTag(true, idx)}${h}${closeTag(true)}`;
|
||
});
|
||
html += '</tr></thead><tbody>';
|
||
bodyLines.forEach(line => {
|
||
const cells = parseTableRow(line);
|
||
html += '<tr>';
|
||
const max = Math.max(headers.length, cells.length);
|
||
for (let idx = 0; idx < max; idx++) {
|
||
html += `${cellTag(false, idx)}${cells[idx] || ''}${closeTag(false)}`;
|
||
}
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
return html;
|
||
}
|
||
|
||
function isListContinuation(line) {
|
||
|
||
return /^\s{2,}\S/.test(line) && !isListItemLine(line);
|
||
}
|
||
|
||
function parseListMarker(line) {
|
||
const m = line.match(/^(\s*)([-*]|\d+\.) (.*)$/);
|
||
if (!m) return null;
|
||
return {
|
||
indent: m[1].length,
|
||
ordered: /^\d+\.$/.test(m[2]),
|
||
content: m[3],
|
||
};
|
||
}
|
||
|
||
function renderList(lines) {
|
||
|
||
const items = [];
|
||
for (const line of lines) {
|
||
const parsed = parseListMarker(line);
|
||
if (parsed) {
|
||
items.push({ ...parsed, children: [] });
|
||
} else if (items.length && isListContinuation(line)) {
|
||
items[items.length - 1].content += '<br/>' + line.trim();
|
||
}
|
||
}
|
||
return renderListTree(items, 0);
|
||
}
|
||
|
||
function renderListTree(items, minIndent) {
|
||
if (!items.length) return '';
|
||
|
||
const levelIndent = items[0].indent;
|
||
const ordered = items[0].ordered;
|
||
const tag = ordered ? 'ol' : 'ul';
|
||
const parts = [];
|
||
let i = 0;
|
||
|
||
while (i < items.length) {
|
||
const item = items[i];
|
||
if (item.indent < minIndent) break;
|
||
if (item.indent > levelIndent) {
|
||
|
||
break;
|
||
}
|
||
if (item.indent < levelIndent) break;
|
||
|
||
i++;
|
||
|
||
const childStart = i;
|
||
while (i < items.length && items[i].indent > levelIndent) i++;
|
||
const children = items.slice(childStart, i);
|
||
let html = `<li>${item.content}`;
|
||
if (children.length) {
|
||
html += renderListTree(children, levelIndent + 1);
|
||
}
|
||
html += '</li>';
|
||
parts.push(html);
|
||
}
|
||
|
||
return `<${tag}>${parts.join('')}</${tag}>`;
|
||
}
|
||
|
||
function escapeHtml(s) {
|
||
return String(s)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|