/* * Minimal, self-hosted Markdown renderer for task descriptions. * Safe by construction: every piece of source text is HTML-escaped BEFORE any * markup is applied, and only a fixed whitelist of tags is emitted * (h4–h6, p, br, ul/ol/li, strong, em, a[href to http/https/mailto]). * Equipment [[markers]] and auto-linked terms are intentionally LEFT as text * here — the app layers those on afterward via a safe text-node pass. * Exposes a global: window.MD.render(text) -> html string. */ (function () { 'use strict'; function escapeHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Inline formatting on already-escaped text. function inline(escaped) { var s = escaped; // links: [label](http(s)://…, mailto:…, or a site-relative /doc-files/… upload) // — label already escaped. /doc-files is the ONLY relative prefix allowed, so // the href whitelist stays as tight as before. s = s.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+|mailto:[^\s)]+|\/doc-files\/[^\s)]+)\)/g, function (m, label, url) { var cls = url.indexOf('/doc-files/') === 0 ? ' class="doc-link"' : ''; return '' + label + ''; }); // bold+italic ***text*** (must run before the ** and * rules) s = s.replace(/\*\*\*([^*\n]+)\*\*\*/g, '$1'); // bold **text** s = s.replace(/\*\*([^*\n]+)\*\*/g, '$1'); // italic *text* / _text_ (require a boundary so it doesn't eat stray asterisks) s = s.replace(/(^|[\s(])\*([^*\n]+)\*(?=$|[\s).,!?])/g, '$1$2'); s = s.replace(/(^|[\s(])_([^_\n]+)_(?=$|[\s).,!?])/g, '$1$2'); return s; } // CANONICAL task-list line regex. MUST stay byte-identical to server.js TASK_LINE_RE and // app.js parseTaskList — the count/index/label must agree across all three or per-location // checkmarks misalign. Group 1 = mark char ( / x / X), group 2 = label (may be empty). var TASK_RE = /^\s*[-*]\s+\[([ xX])\]\s*(.*)$/; function render(src) { var lines = String(src == null ? '' : src).replace(/\r\n?/g, '\n').split('\n'); var out = []; var para = []; var taskIndex = 0; // 0-based position among task-list lines, in document order (per render) function flushPara() { if (para.length) { out.push('
' + para.map(inline).join('
') + '