diff --git a/assets/css/base.css b/assets/css/base.css index 772f46f..11e49eb 100644 --- a/assets/css/base.css +++ b/assets/css/base.css @@ -5163,6 +5163,16 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas .lt-markdown a:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 2px; } .lt-markdown strong { color: var(--text-primary); } .lt-markdown img, .md-image { max-width: 100%; height: auto; border: 1px solid var(--border-dim); border-radius: 2px; display: block; margin: 0.5rem 0; } +.lt-markdown mark { background: var(--accent-yellow-dim, #2a2500); color: var(--accent-yellow, #e6c619); padding: 0 3px; border-radius: 2px; } +.lt-markdown del { color: var(--text-muted); text-decoration: line-through; } +.lt-markdown sub, .lt-markdown sup { font-size: 0.7em; line-height: 0; } +.lt-markdown .task-item { list-style: none; margin-left: -1.2em; } +.lt-markdown .task-cb { margin-right: 0.35em; font-size: 1em; } +.lt-markdown .task-done { color: var(--text-muted); text-decoration: line-through; } +.lt-markdown .task-todo { color: var(--text-secondary); } +.lt-markdown ol { padding-left: 1.5em; margin: 0.5rem 0; } +.lt-markdown ol li { color: var(--text-secondary); } +.lt-markdown ol li::marker { color: var(--accent-orange); } .lt-markdown table { width: 100%; border-collapse: collapse; font-size: 0.78rem; margin: 0.75rem 0; } .lt-markdown th { background: var(--bg-secondary); color: var(--accent-cyan); padding: 0.4rem 0.6rem; border: 1px solid var(--border-dim); text-align: left; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; } .lt-markdown td { padding: 0.35rem 0.6rem; border: 1px solid var(--border-dim); color: var(--text-secondary); } diff --git a/assets/js/markdown.js b/assets/js/markdown.js index 44e0a4d..bc1fdf1 100644 --- a/assets/js/markdown.js +++ b/assets/js/markdown.js @@ -33,6 +33,9 @@ function parseMarkdown(markdown) { // Tables (must be processed before other block elements) html = parseMarkdownTables(html); + // Emoji :name: — common set + html = replaceEmoji(html); + // Bold (**text** or __text__) html = html.replace(/\*\*(.+?)\*\*/g, '$1'); html = html.replace(/__(.+?)__/g, '$1'); @@ -41,9 +44,18 @@ function parseMarkdown(markdown) { html = html.replace(/\*(.+?)\*/g, '$1'); html = html.replace(/_(.+?)_/g, '$1'); - // Strikethrough (~~text~~) + // Strikethrough (~~text~~) — must run before subscript (~) html = html.replace(/~~(.+?)~~/g, '$1'); + // Highlight (==text==) + html = html.replace(/==(.+?)==/g, '$1'); + + // Subscript H~2~O — single tilde (not preceded/followed by another tilde) + html = html.replace(/(?$1'); + + // Superscript X^2^ — caret pair + html = html.replace(/\^([^\^\n]+?)\^/g, '$1'); + // Images ![alt](url) - must come before link handler html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function(match, alt, url) { if (/^https?:/i.test(url)) { @@ -62,21 +74,29 @@ function parseMarkdown(markdown) { return text; }); - // Auto-link bare URLs (http, https, ftp) + // Auto-link bare URLs (http, https) html = html.replace(/(?])(\bhttps?:\/\/[^\s<>\[\]()]+)/g, '$1'); - // Headers (# H1, ## H2, etc.) - html = html.replace(/^### (.+)$/gm, '

$1

'); - html = html.replace(/^## (.+)$/gm, '

$1

'); - html = html.replace(/^# (.+)$/gm, '

$1

'); + // Headings with optional {#id} anchor — ### My Heading {#my-id} + html = html.replace(/^(#{1,6})\s+(.+?)\s*(?:\{#([a-z0-9_-]+)\})?$/gm, function(match, hashes, text, id) { + const level = hashes.length; + const idAttr = id ? ' id="' + id + '"' : ''; + return '' + text + ''; + }); - // Lists - // Unordered lists (- item or * item) + // Task lists — must run before general list processing + html = html.replace(/^\s*-\s+\[x\]\s+(.+)$/gim, '
  • $1
  • '); + html = html.replace(/^\s*-\s+\[ \]\s+(.+)$/gm, '
  • $1
  • '); + + // Unordered lists (- item or * item) — wrap consecutive
  • in