Markdown -> Semble importer
at canon 118 lines 3.1 kB view raw
1import type {SembleCard, SembleCollection} from "../semble/types"; 2 3export interface ParsedSource { 4 cards: SembleCard[]; 5 collections: SembleCollection[]; 6} 7 8export function parseMarkdownSource(sourceId: string, text: string): ParsedSource { 9 const lines = text.split(/\r?\n/); 10 const cards: SembleCard[] = []; 11 const collections: SembleCollection[] = []; 12 let collection: string | undefined; 13 14 for (let index = 0; index < lines.length; index += 1) { 15 const line = lines[index]; 16 if (!line) continue; 17 18 const headerMatch = line.match(/^#\s+(.+?)\s*$/); 19 if (headerMatch) { 20 collection = headerMatch[1]?.trim(); 21 if (collection) { 22 const {description, nextIndex} = parseCollectionDescription(lines, index + 1); 23 collections.push({ 24 name: collection, 25 description, 26 sourceId, 27 line: index + 1 28 }); 29 if (nextIndex > index + 1) { 30 index = nextIndex - 1; 31 } 32 } 33 continue; 34 } 35 36 const listMatch = line.match(/^\s*[-*]\s+(.+)$/); 37 const candidate = listMatch ? listMatch[1] ?? "" : line.trim(); 38 const parsed = parseCardLine(candidate); 39 if (!parsed) continue; 40 41 const cardId = `${sourceId}#L${index + 1}:${parsed.url}`; 42 cards.push({ 43 ...parsed, 44 collection, 45 sourceId, 46 line: index + 1, 47 id: cardId 48 }); 49 } 50 51 return {cards, collections}; 52} 53 54function parseCollectionDescription(lines: string[], startIndex: number): { 55 description?: string; 56 nextIndex: number; 57} { 58 const descriptionParts: string[] = []; 59 let index = startIndex; 60 61 for (; index < lines.length; index += 1) { 62 const line = lines[index] ?? ""; 63 const trimmed = line.trim(); 64 if (!trimmed) { 65 if (descriptionParts.length > 0) { 66 index += 1; 67 } 68 break; 69 } 70 if (/^#\s+/.test(trimmed) || /^\s*[-*]\s+/.test(trimmed)) { 71 break; 72 } 73 descriptionParts.push(trimmed); 74 } 75 76 const description = descriptionParts.length > 0 ? descriptionParts.join(" ") : undefined; 77 return {description, nextIndex: index}; 78} 79 80function parseCardLine(text: string): Omit<SembleCard, "collection" | "sourceId" | "line" | "id"> | null { 81 const trimmed = text.trim(); 82 if (!trimmed) return null; 83 84 const splitIndex = trimmed.indexOf(" : "); 85 const linkPart = splitIndex >= 0 ? trimmed.slice(0, splitIndex).trim() : trimmed; 86 const notePart = splitIndex >= 0 ? trimmed.slice(splitIndex + 3).trim() : undefined; 87 88 const markdownLink = linkPart.match(/^\[(.+?)\]\((.+?)\)$/); 89 if (markdownLink) { 90 const title = markdownLink[1]?.trim(); 91 const url = markdownLink[2]?.trim(); 92 if (!url) return null; 93 return { 94 url, 95 title: title || undefined, 96 note: notePart || undefined 97 }; 98 } 99 100 const angleLink = linkPart.match(/^<([^>]+)>$/); 101 if (angleLink) { 102 const url = angleLink[1]?.trim(); 103 if (!url) return null; 104 return { 105 url, 106 note: notePart || undefined 107 }; 108 } 109 110 if (!/^(https?:\/\/|at:\/\/)\S+$/i.test(linkPart)) { 111 return null; 112 } 113 114 return { 115 url: linkPart, 116 note: notePart || undefined 117 }; 118}