a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 2.9 kB view raw
1import { createHash } from "node:crypto"; 2 3/** 4 * Represents a section extracted from markdown 5 */ 6export type Section = { heading: string; content: string; hash: string }; 7 8/** 9 * Result of diffing two sets of sections 10 */ 11export type SectionDiff = { added: number; removed: number; edited: number }; 12 13/** 14 * Extract all ## and ### headings from markdown content 15 */ 16export function extractSections(markdown: string): Section[] { 17 const lines = markdown.split("\n"); 18 const sections: Section[] = []; 19 let currentSection: { heading: string; lines: string[] } | undefined = undefined; 20 21 for (const line of lines) { 22 const trimmed = line.trim(); 23 24 if (trimmed.startsWith("## ") || trimmed.startsWith("### ")) { 25 if (currentSection) { 26 const content = currentSection.lines.join("\n").trim(); 27 sections.push({ heading: currentSection.heading, content, hash: hashContent(content) }); 28 } 29 30 currentSection = { heading: trimmed, lines: [] }; 31 } else if (currentSection) { 32 currentSection.lines.push(line); 33 } 34 } 35 36 if (currentSection) { 37 const content = currentSection.lines.join("\n").trim(); 38 sections.push({ heading: currentSection.heading, content, hash: hashContent(content) }); 39 } 40 41 return sections; 42} 43 44/** 45 * Compare two sets of sections (matched by heading text) and calculate the diff 46 */ 47export function diffSections(oldSections: Section[], newSections: Section[]): SectionDiff { 48 const oldMap = new Map(oldSections.map((s) => [s.heading, s])); 49 const newMap = new Map(newSections.map((s) => [s.heading, s])); 50 51 let added = 0; 52 let removed = 0; 53 let edited = 0; 54 55 for (const [heading, newSection] of newMap) { 56 const oldSection = oldMap.get(heading); 57 58 if (!oldSection) { 59 added++; 60 } else if (oldSection.hash !== newSection.hash) { 61 edited++; 62 } 63 } 64 65 // Find removed sections 66 for (const heading of oldMap.keys()) { 67 if (!newMap.has(heading)) { 68 removed++; 69 } 70 } 71 72 return { added, removed, edited }; 73} 74 75/** 76 * Hash content using SHA-256 77 */ 78function hashContent(content: string): string { 79 return createHash("sha256").update(content).digest("hex"); 80} 81 82/** 83 * Extract section headings as a simple string array 84 */ 85export function extractHeadings(markdown: string): string[] { 86 return extractSections(markdown).map((s) => s.heading); 87} 88 89/** 90 * Hash entire markdown content (without frontmatter) 91 */ 92export function hashMarkdown(markdown: string): string { 93 const withoutFrontmatter = stripFrontmatter(markdown); 94 return hashContent(withoutFrontmatter); 95} 96 97/** 98 * Remove YAML frontmatter from markdown 99 */ 100function stripFrontmatter(markdown: string): string { 101 const lines = markdown.split("\n"); 102 103 if (lines[0]?.trim() !== "---") { 104 return markdown; 105 } 106 107 for (let i = 1; i < lines.length; i++) { 108 if (lines[i].trim() === "---") { 109 return lines.slice(i + 1).join("\n"); 110 } 111 } 112 113 return markdown; 114}