Files
memos-chrome-extension/marked.min.js
2026-03-14 21:21:53 +01:00

147 lines
4.4 KiB
JavaScript

/*!
* marked-mini - minimal Markdown renderer for Memos Clipper
* Supports: headings, bold, italic, strike, code, pre, blockquote,
* links, images, ul, ol, hr, tables, line breaks
*/
(function(global){
"use strict";
function escape(s){return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");}
function parseInline(s){
// code spans
s = s.replace(/`([^`]+)`/g,(_,c)=>`<code>${escape(c)}</code>`);
// bold+italic
s = s.replace(/\*\*\*(.+?)\*\*\*/g,"<strong><em>$1</em></strong>");
// bold
s = s.replace(/\*\*(.+?)\*\*/g,"<strong>$1</strong>");
s = s.replace(/__(.+?)__/g,"<strong>$1</strong>");
// italic
s = s.replace(/\*(.+?)\*/g,"<em>$1</em>");
s = s.replace(/_([^_]+)_/g,"<em>$1</em>");
// strikethrough
s = s.replace(/~~(.+?)~~/g,"<del>$1</del>");
// images
s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g,(_,alt,src)=>`<img src="${escape(src)}" alt="${escape(alt)}">`);
// links
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(_,txt,href)=>`<a href="${escape(href)}" target="_blank">${txt}</a>`);
// auto-link
s = s.replace(/(?<!["\(])(https?:\/\/[^\s<>)]+)/g,url=>`<a href="${escape(url)}" target="_blank">${escape(url)}</a>`);
// hard break
s = s.replace(/ \n/g,"<br>");
return s;
}
function parse(src){
const lines = src.split("\n");
const out = [];
let i = 0;
while(i < lines.length){
const line = lines[i];
// Fenced code block
const fenceM = line.match(/^```(\w*)/);
if(fenceM){
const lang = fenceM[1]||"";
const code = [];
i++;
while(i<lines.length && !lines[i].startsWith("```")){
code.push(lines[i]);
i++;
}
i++; // skip closing ```
out.push(`<pre><code class="language-${escape(lang)}">${escape(code.join("\n"))}</code></pre>`);
continue;
}
// Headings
const hM = line.match(/^(#{1,6})\s+(.*)/);
if(hM){
const lvl = hM[1].length;
out.push(`<h${lvl}>${parseInline(hM[2])}</h${lvl}>`);
i++; continue;
}
// HR
if(/^[-*_]{3,}\s*$/.test(line)){
out.push("<hr>");
i++; continue;
}
// Blockquote
if(line.startsWith("> ")){
const bq = [];
while(i<lines.length && lines[i].startsWith("> ")){
bq.push(lines[i].slice(2));
i++;
}
out.push(`<blockquote>${parse(bq.join("\n"))}</blockquote>`);
continue;
}
// Unordered list
if(/^[-*+]\s/.test(line)){
const items = [];
while(i<lines.length && /^[-*+]\s/.test(lines[i])){
items.push(`<li>${parseInline(lines[i].replace(/^[-*+]\s/,""))}</li>`);
i++;
}
out.push(`<ul>${items.join("")}</ul>`);
continue;
}
// Ordered list
if(/^\d+\.\s/.test(line)){
const items = [];
while(i<lines.length && /^\d+\.\s/.test(lines[i])){
items.push(`<li>${parseInline(lines[i].replace(/^\d+\.\s/,""))}</li>`);
i++;
}
out.push(`<ol>${items.join("")}</ol>`);
continue;
}
// Table (| col | col |)
if(line.startsWith("|") && i+1<lines.length && /^\|[-| :]+\|/.test(lines[i+1])){
const headers = line.split("|").filter((_,idx,a)=>idx>0&&idx<a.length-1).map(h=>`<th>${parseInline(h.trim())}</th>`);
i+=2; // skip header + separator
const rows = [];
while(i<lines.length && lines[i].startsWith("|")){
const cells = lines[i].split("|").filter((_,idx,a)=>idx>0&&idx<a.length-1).map(c=>`<td>${parseInline(c.trim())}</td>`);
rows.push(`<tr>${cells.join("")}</tr>`);
i++;
}
out.push(`<table><thead><tr>${headers.join("")}</tr></thead><tbody>${rows.join("")}</tbody></table>`);
continue;
}
// Empty line
if(line.trim()===""){
i++; continue;
}
// Paragraph — collect until blank line
const para = [];
while(i<lines.length && lines[i].trim()!==""){
// stop on block elements
if(/^(#{1,6}\s|```|>|[-*+]\s|\d+\.\s|[-*_]{3,}\s*$)/.test(lines[i])) break;
para.push(lines[i]);
i++;
}
if(para.length) out.push(`<p>${parseInline(para.join(" "))}</p>`);
}
return out.join("\n");
}
global.marked = {
parse: function(src, opts) {
const breaks = opts && opts.breaks;
// With breaks:true, single newlines in paragraphs become <br>
if (breaks) {
// Pre-process: single \n inside paragraph text → two spaces + \n (markdown hard break)
src = src.replace(/([^\n])\n([^\n])/g, "$1 \n$2");
}
return parse(src);
}
};})(window);