// Markdown -> HTML subset (Qt rich text) renderer. Pure ES5, no deps. // IMPORTANT (QML): functions are TOP-LEVEL so a QML namespace import exposes // them directly: import "../components/Markdown.js" as Markdown // ... Markdown.render(src) ... // (an IIFE + var would hide them under Markdown.Markdown and QML would throw // "Property 'render' of object [object Object] is not a function"). // The module.exports guard at the bottom is for node tests only. // // render(src, baseUrl, opts): // baseUrl - resolves relative links/images against (the note's WebDAV folder) // opts.imageMap - { url: localCachePath } -> inline instead of placeholder // opts.imageLabel - localized word for the image placeholder ("image") function escapeHtml(s) { return s.replace(/&/g, "&").replace(//g, ">"); } function resolveUrl(u, baseUrl) { if (baseUrl && !/^[a-z]+:/i.test(u) && u.charAt(0) !== "#" && u.charAt(0) !== "/") { return String(baseUrl).replace(/\/+$/, "") + "/" + u; } return u; } function inline(s, baseUrl, opts) { s = escapeHtml(s); s = s.replace(/`([^`]+)`/g, "$1"); s = s.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, function (m, alt, u) { var key = resolveUrl(u, baseUrl); var p = opts && opts.imageMap ? opts.imageMap[key] : ""; if (p && p !== "error") { return '' + alt + ''; } var label = opts && opts.imageLabel ? opts.imageLabel : "image"; return "[" + label + ": " + alt + "]"; }); s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, function (m, t, u) { return '' + t + ""; }); // Wiki links [[Nota]] -> quill://; the app intercepts // the scheme and opens the note in the editor. s = s.replace(/\[\[([^\]]+)\]\]/g, function (m, name) { var full = name; var lower = name.toLowerCase(); if (lower.indexOf(".md") !== lower.length - 3) full = name + ".md"; var label = full; if (label.toLowerCase().indexOf(".md") === label.length - 3) { label = label.substring(0, label.length - 3); } return '' + label + ""; }); s = s.replace(/\*\*(.+?)\*\*/g, "$1"); s = s.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1$2"); s = s.replace(/~~([^~]+)~~/g, "$1"); return s; } // Split a table row on unescaped pipes; leading/trailing pipe is optional. function splitRow(s) { var t = s.trim(); if (t.charAt(0) === "|") t = t.substring(1); if (t.charAt(t.length - 1) === "|") t = t.substring(0, t.length - 1); var parts = t.split("|"); var cells = []; for (var i = 0; i < parts.length; i++) cells.push(parts[i].trim()); return cells; } // GFM alignment from a delimiter cell: ":---" left, "---:" right, ":---:" center. function alignmentFromSep(sep, n) { var parts = splitRow(sep); var aligns = []; for (var i = 0; i < n; i++) { var p = i < parts.length ? parts[i] : ""; if (p.charAt(0) === ":" && p.charAt(p.length - 1) === ":") aligns.push("center"); else if (p.charAt(0) === ":") aligns.push("left"); else if (p.charAt(p.length - 1) === ":") aligns.push("right"); else aligns.push(""); } return aligns; } function alignAttr(a) { return a ? ' align="' + a + '"' : ""; } function render(src, baseUrl, opts) { if (!src) return ""; src = src.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); // Strip UTF-8 BOM: a leading \uFEFF breaks the first heading/block // regex (line no longer starts with #) and the symbol stays visible. src = src.replace(/^\uFEFF/, ""); var lines = src.split("\n"); var out = []; var i = 0; var n = lines.length; var headingRe = /^\s*(#{1,6})\s+(.*)$/; var hrRe = /^\s*(-{3,}|\*{3,}|_{3,})\s*$/; var quoteRe = /^>\s?(.*)$/; var listRe = /^\s*([-*+]|\d+[.)])\s+(.*)$/; var fenceRe = /^```/; var tableHeaderRe = /^\s*\|.*\|/; var tableSepRe = /^\s*\|?[\s:|-]+\|?\s*$/; while (i < n) { var line = lines[i]; var m; if (fenceRe.test(line)) { var buf = []; i++; while (i < n && !fenceRe.test(lines[i])) { buf.push(lines[i]); i++; } i++; // closing fence out.push('
' + escapeHtml(buf.join("\n")) + "
"); continue; } // GFM table: header row + delimiter row (must contain a pipe and a dash). if (tableHeaderRe.test(line) && i + 1 < n) { var sepLine = lines[i + 1]; if (tableSepRe.test(sepLine) && sepLine.indexOf("|") >= 0 && sepLine.indexOf("-") >= 0) { var headerCells = splitRow(line); var aligns = alignmentFromSep(sepLine, headerCells.length); var tbody = []; i += 2; while (i < n) { var dl = lines[i]; if (dl.trim() === "" || dl.indexOf("|") < 0) break; tbody.push(splitRow(dl)); i++; } var t = ''; t += ""; for (var h = 0; h < headerCells.length; h++) { t += "" + inline(headerCells[h], baseUrl, opts) + ""; } t += ""; for (var r = 0; r < tbody.length; r++) { t += ""; var row = tbody[r]; for (var c = 0; c < headerCells.length; c++) { var cell = c < row.length ? row[c] : ""; t += "" + inline(cell, baseUrl, opts) + ""; } t += ""; } t += "
"; out.push(t); continue; } } m = headingRe.exec(line); if (m) { var level = m[1].length; out.push("" + inline(m[2], baseUrl, opts) + ""); i++; continue; } if (hrRe.test(line)) { out.push("
"); i++; continue; } m = quoteRe.exec(line); if (m) { var q = []; while (i < n) { var qm = quoteRe.exec(lines[i]); if (!qm) break; q.push(inline(qm[1], baseUrl, opts)); i++; } out.push('

' + q.join("
") + "

"); continue; } m = listRe.exec(line); if (m) { var ordered = /^\s*\d/.test(line); var items = []; while (i < n) { var lm = listRe.exec(lines[i]); if (!lm) break; items.push("
  • " + inline(lm[2], baseUrl, opts) + "
  • "); i++; } out.push(ordered ? "
      " + items.join("") + "
    " : ""); continue; } if (line.trim() === "") { i++; continue; } var para = []; while (i < n) { var pl = lines[i]; if (pl.trim() === "" || headingRe.test(pl) || hrRe.test(pl) || quoteRe.test(pl) || listRe.test(pl) || fenceRe.test(pl)) { break; } para.push(inline(pl, baseUrl, opts)); i++; } out.push("

    " + para.join("
    ") + "

    "); } return out.join("\n"); } if (typeof module !== "undefined" && module.exports) { module.exports = { render: render }; }