From cd83464c5d00e85aafdde2af7a097745076ff8db Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Tue, 7 Apr 2026 00:13:38 -0400 Subject: [PATCH] Add extended markdown: task lists, highlight, sub/superscript, heading IDs, emoji - Task lists: - [x] / - [ ] with checkbox glyphs, done items struck through - Highlight: ==text== -> - Subscript: ~text~ -> (runs after ~~ strikethrough to avoid conflict) - Superscript: ^text^ -> - Heading IDs: ### Title {#my-id} adds id attribute for anchor links - Ordered lists: now properly wrapped in
    - Emoji: :name: shortcodes (~100 common emojis) - CSS for all new elements Co-Authored-By: Claude Sonnet 4.6 --- assets/css/base.css | 10 +++++ assets/js/markdown.js | 88 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 87 insertions(+), 11 deletions(-) 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. $1
  2. '); + html = html.replace(/^\s*-\s+\[ \]\s+(.+)$/gm, '
  3. $1
  4. '); + + // Unordered lists (- item or * item) — wrap consecutive
  5. in
      html = html.replace(/^\s*[-*]\s+(.+)$/gm, '
    • $1
    • '); - html = html.replace(/(
    • .*<\/li>)/s, '
        $1
      '); + html = html.replace(/(
    • (?:(?!
    • |<\/ul>)[\s\S])*<\/li>(?:\n
    • (?:(?!
    • |<\/ul>)[\s\S])*<\/li>)*)/g, '
        $1
      '); - // Ordered lists (1. item) - html = html.replace(/^\s*\d+\.\s+(.+)$/gm, '
    • $1
    • '); + // Ordered lists (1. item) — wrap in
        + html = html.replace(/(?:^\s*\d+\.\s+.+$\n?)+/gm, function(block) { + const items = block.trim().replace(/^\s*\d+\.\s+(.+)$/gm, '
      1. $1
      2. '); + return '
          ' + items + '
        '; + }); // Blockquotes (> text) html = html.replace(/^>\s+(.+)$/gm, '
        $1
        '); @@ -104,6 +124,52 @@ function parseMarkdown(markdown) { return html; } +/** + * Replace :emoji: shortcodes with Unicode characters + */ +const _emojiMap = { + // Faces & emotions + smile: '😊', grin: '😁', joy: '😂', rofl: '🤣', smiley: '😃', sweat_smile: '😅', + wink: '😉', blush: '😊', heart_eyes: '😍', kissing: '😗', thinking: '🤔', + raised_eyebrow: '🤨', neutral_face: '😐', expressionless: '😑', unamused: '😒', + roll_eyes: '🙄', pensive: '😔', confused: '😕', worried: '😟', cry: '😢', + sob: '😭', scream: '😱', angry: '😠', rage: '😡', skull: '💀', sunglasses: '😎', + nerd: '🤓', monocle: '🧐', clown: '🤡', ghost: '👻', robot: '🤖', alien: '👽', + // Hands & people + thumbsup: '👍', '+1': '👍', thumbsdown: '👎', '-1': '👎', clap: '👏', + wave: '👋', raised_hands: '🙌', pray: '🙏', point_up: '☝️', point_right: '👉', + point_left: '👈', point_down: '👇', fist: '✊', punch: '👊', v: '✌️', ok_hand: '👌', + muscle: '💪', eyes: '👀', eye: '👁️', ear: '👂', brain: '🧠', man: '👨', woman: '👩', + // Hearts & symbols + heart: '❤️', orange_heart: '🧡', yellow_heart: '💛', green_heart: '💚', + blue_heart: '💙', purple_heart: '💜', black_heart: '🖤', broken_heart: '💔', + star: '⭐', star2: '🌟', sparkles: '✨', fire: '🔥', boom: '💥', zap: '⚡', + check: '✅', white_check_mark: '✅', x: '❌', heavy_check_mark: '✔️', + warning: '⚠️', no_entry: '⛔', stop_sign: '🛑', prohibited: '🚫', + question: '❓', exclamation: '❗', grey_question: '❔', grey_exclamation: '❕', + 100: '💯', tada: '🎉', confetti_ball: '🎊', trophy: '🏆', medal: '🥇', + // Tech & work + bug: '🐛', rocket: '🚀', computer: '💻', keyboard: '⌨️', mouse: '🖱️', + printer: '🖨️', phone: '📱', email: '📧', inbox_tray: '📥', outbox_tray: '📤', + memo: '📝', pencil: '✏️', pen: '🖊️', paperclip: '📎', link: '🔗', + hammer: '🔨', wrench: '🔧', gear: '⚙️', lock: '🔒', unlock: '🔓', + key: '🔑', mag: '🔍', bar_chart: '📊', chart_increasing: '📈', chart_decreasing: '📉', + clipboard: '📋', calendar: '📅', clock: '🕐', hourglass: '⏳', bell: '🔔', + mute: '🔇', loud_sound: '🔊', bulb: '💡', battery: '🔋', electric_plug: '🔌', + recycle: '♻️', package: '📦', label: '🏷️', bookmark: '🔖', flag: '🚩', + // Nature & misc + sun: '☀️', moon: '🌙', cloud: '☁️', snowflake: '❄️', umbrella: '☂️', + dog: '🐶', cat: '🐱', pizza: '🍕', coffee: '☕', beer: '🍺', + white_flag: '🏳️', checkered_flag: '🏁', construction: '🚧', sos: '🆘', + info: 'ℹ️', new: '🆕', free: '🆓', cool: '🆒', up: '🆙', soon: '🔜', +}; + +function replaceEmoji(text) { + return text.replace(/:([a-z0-9_+\-]+):/g, function(match, name) { + return _emojiMap[name] || match; + }); +} + /** * Parse markdown tables * Supports: | Header | Header |