Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
4.6 kB · 107 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108// src/plugins/quick-access/preview.ts//// Pure preview extraction for Quick Access rows.//// Strips frontmatter, code fences, HTML, embeds/images/links syntax,// heading/quote/list/emphasis markers, then collapses whitespace and// hard-truncates at 160 chars. CSS line-clamp does the visual work; the// 160-char cap is a deterministic upper bound that:// - prevents huge strings from sitting in memory,// - makes the function testable without DOM measurement,// - caps reconciler cost for large vaults.//// Tradeoff: regex-only (no markdown AST). Cheap and deterministic for "two// lines of skim-able text". See plan D4 for full rationale and edge cases.
const PREVIEW_CAP = 160
function replaceDelimited(text: string, open: string, close: string, replacement: (value: string) => string): string { let result = '' let cursor = 0 while (cursor < text.length) { const start = text.indexOf(open, cursor) if (start < 0) return result + text.slice(cursor) const end = text.indexOf(close, start + open.length) if (end < 0) return result + text.slice(cursor) result += text.slice(cursor, start) + replacement(text.slice(start + open.length, end)) cursor = end + close.length } return result}
export function extractPreview(content: string): string { // 0. Strip a leading UTF-8 BOM (\uFEFF) if present. Some editors/exporters // prepend one, which would otherwise prevent the `^---` frontmatter // regex from matching and leak the entire YAML block into the preview. let body = content.replace(/^\uFEFF/, '')
// 1. Strip code fences FIRST. This must happen before frontmatter so a // code block containing a `---` line is not mis-recognized as the // frontmatter terminator. body = body.replace(/```(?:[^`]|`{1,2}(?!`))*```/g, ' ')
// 2. Strip leading frontmatter block (--- ... --- at file start only). if (body.startsWith('---\n') || body.startsWith('---\r\n')) { const firstLineEnd = body.indexOf('\n') + 1 const closing = body.indexOf('\n---', firstLineEnd) if (closing >= 0) { const closingLineEnd = body.indexOf('\n', closing + 1) body = body.slice(closingLineEnd >= 0 ? closingLineEnd + 1 : body.length) } }
// 3. Strip HTML tags (open and close). body = replaceDelimited(body, '<', '>', () => ' ')
// 4. Strip embedded wikilinks (![[X]]) — drop entirely (they show as broken // in a one-line preview). body = body.replace(/!\[\[[^\]]*\]\]/g, ' ')
// 5. Strip image markdown () — drop entirely. body = body.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
// 6. Wikilinks: [[target|display]] → display; [[target]] → target. body = replaceDelimited(body, '[[', ']]', (target) => { const segments = target.split('|') return (segments[segments.length - 1] ?? '').trim() })
// 7. Markdown links [text](url) → text. body = replaceDelimited(body, '[', ')', (link) => { const separator = link.indexOf('](') return separator >= 0 ? link.slice(0, separator) : `[${link})` })
// 8. Strip leading ATX heading markers (# ## ### etc.) at line start. body = body.replace(/^[ \t]{0,3}#{1,6}[ \t]+/gm, '')
// 9. Strip blockquote markers (> ) at line start. body = body.replace(/^[ \t]{0,3}>[ \t]?/gm, '')
// 10. Strip list markers (-, *, +, 1.) at line start. body = body.replace(/^[ \t]{0,3}(?:[-*+]|\d+\.)[ \t]+/gm, '')
// 11. Strip emphasis markers: **bold**, *italic*, _emph_, ~~strike~~, `code`. // Conservative: only replace matched pairs. Documented edge per plan D4: // paired single underscores in identifiers like snake_case_word are // stripped — acceptable for a skim-preview. body = body.replace(/(\*\*|__)([^\r\n]+?)\1/g, '$2') body = body.replace(/([*_])([^\r\n]+?)\1/g, '$2') body = body.replace(/~~(.+?)~~/g, '$1') body = body.replace(/`([^`]+)`/g, '$1')
// 12. Collapse all whitespace runs (including newlines) to single spaces, trim. body = body.replace(/\s+/g, ' ').trim()
// 13. Hard truncate at PREVIEW_CAP code points with ellipsis if longer. // `Array.from(body)` iterates by Unicode code point, keeping astral // characters (e.g. emoji built from a UTF-16 surrogate pair) intact; // a naive `body.slice` could otherwise split a surrogate pair and // emit a lone surrogate followed by '…'. PREVIEW_CAP is therefore // interpreted as a code-point cap, which is what users perceive. const codepoints = Array.from(body) if (codepoints.length > PREVIEW_CAP) { body = codepoints.slice(0, PREVIEW_CAP - 1).join('').trimEnd() + '…' } return body}