Markdown -> Semble importer
at canon 268 lines 7.3 kB view raw
1import type {SembleCard, SembleCollection, SembleCollectionLink} from "../semble/types"; 2import type {SembleUrlMetadata} from "../metadata/citoid"; 3import { 4 CARD_COLLECTION, 5 COLLECTION_COLLECTION, 6 COLLECTION_LINK_COLLECTION 7} from "./collections"; 8 9const DEFAULT_DID = "did:example:semble"; 10 11export interface MockStrongRef { 12 uri: string; 13 cid: string; 14} 15 16export interface AtprotoRecordEnvelope { 17 collection: string; 18 rkey: string; 19 record: Record<string, unknown>; 20 strongRef: MockStrongRef; 21 recordType: "card" | "collection" | "collectionLink" | "note"; 22 recordId: string; 23 cardId?: string; 24 collectionName?: string; 25} 26 27export interface AtprotoDraftRecords { 28 cards: AtprotoRecordEnvelope[]; 29 collections: AtprotoRecordEnvelope[]; 30 collectionLinks: AtprotoRecordEnvelope[]; 31 notes: AtprotoRecordEnvelope[]; 32} 33 34export function buildMockAtprotoRecords(input: { 35 cards: SembleCard[]; 36 collections: SembleCollection[]; 37 collectionLinks: SembleCollectionLink[]; 38 now?: Date; 39 did?: string; 40}): AtprotoDraftRecords { 41 const now = input.now ?? new Date(); 42 const did = input.did ?? DEFAULT_DID; 43 44 const cardRecords = input.cards.map(card => buildCardRecord(card, now, did)); 45 const collectionRecords = input.collections.map(collection => 46 buildCollectionRecord(collection, now, did) 47 ); 48 49 const cardRefMap = new Map( 50 cardRecords.map(record => [record.recordId, record.strongRef]) 51 ); 52 const collectionRefMap = new Map( 53 collectionRecords.map(record => [record.recordId, record.strongRef]) 54 ); 55 56 const collectionLinkRecords = input.collectionLinks.map(link => { 57 const collectionRef = collectionRefMap.get(link.collectionName); 58 const cardRef = cardRefMap.get(link.cardId); 59 60 if (!collectionRef || !cardRef) { 61 return buildCollectionLinkRecord(link, now, did, undefined, undefined); 62 } 63 64 return buildCollectionLinkRecord(link, now, did, collectionRef, cardRef); 65 }); 66 67 const noteRecords = input.cards 68 .filter(card => Boolean(card.note)) 69 .map(card => { 70 const parentRef = cardRefMap.get(card.id); 71 return buildNoteRecord(card, now, did, parentRef); 72 }); 73 74 return { 75 cards: cardRecords.map(record => record.envelope), 76 collections: collectionRecords.map(record => record.envelope), 77 collectionLinks: collectionLinkRecords.map(record => record.envelope), 78 notes: noteRecords.map(record => record.envelope) 79 }; 80} 81 82function buildCardRecord(card: SembleCard, now: Date, did: string): { 83 recordId: string; 84 envelope: AtprotoRecordEnvelope; 85 strongRef: MockStrongRef; 86} { 87 const rkey = stableRkey(`card:${card.id}`); 88 const uri = `at://${did}/${CARD_COLLECTION}/${rkey}`; 89 const strongRef = {uri, cid: `mock:${stableHash(uri)}`}; 90 91 const record: Record<string, unknown> = { 92 $type: CARD_COLLECTION, 93 type: "URL", 94 content: buildUrlContent(card), 95 createdAt: now.toISOString() 96 }; 97 98 const envelope: AtprotoRecordEnvelope = { 99 collection: CARD_COLLECTION, 100 rkey, 101 record, 102 strongRef, 103 recordType: "card", 104 recordId: card.id 105 }; 106 107 return {recordId: card.id, envelope, strongRef}; 108} 109 110function buildUrlContent(card: SembleCard): Record<string, unknown> { 111 const content: Record<string, unknown> = { 112 $type: `${CARD_COLLECTION}#urlContent`, 113 url: card.url 114 }; 115 116 const metadata = buildUrlMetadata(card.metadata); 117 if (metadata) { 118 content.metadata = metadata; 119 } 120 121 return content; 122} 123 124function buildUrlMetadata(metadata?: SembleUrlMetadata): Record<string, unknown> | undefined { 125 if (!metadata) return undefined; 126 127 const record: Record<string, unknown> = { 128 $type: `${CARD_COLLECTION}#urlMetadata` 129 }; 130 131 if (metadata.title) record.title = metadata.title; 132 if (metadata.description) record.description = metadata.description; 133 if (metadata.author) record.author = metadata.author; 134 if (metadata.publishedDate) { 135 record.publishedDate = metadata.publishedDate.toISOString(); 136 } 137 if (metadata.siteName) record.siteName = metadata.siteName; 138 if (metadata.type) record.type = metadata.type; 139 if (metadata.doi) record.doi = metadata.doi; 140 if (metadata.isbn) record.isbn = metadata.isbn; 141 142 return record; 143} 144 145function buildCollectionRecord(collection: SembleCollection, now: Date, did: string): { 146 recordId: string; 147 envelope: AtprotoRecordEnvelope; 148 strongRef: MockStrongRef; 149} { 150 const rkey = stableRkey(`collection:${collection.name}`); 151 const uri = `at://${did}/${COLLECTION_COLLECTION}/${rkey}`; 152 const strongRef = {uri, cid: `mock:${stableHash(uri)}`}; 153 154 const record: Record<string, unknown> = { 155 $type: COLLECTION_COLLECTION, 156 name: collection.name, 157 accessType: "OPEN", 158 createdAt: now.toISOString() 159 }; 160 161 if (collection.description) { 162 record.description = collection.description; 163 } 164 165 const envelope: AtprotoRecordEnvelope = { 166 collection: COLLECTION_COLLECTION, 167 rkey, 168 record, 169 strongRef, 170 recordType: "collection", 171 recordId: collection.name 172 }; 173 174 return {recordId: collection.name, envelope, strongRef}; 175} 176 177function buildNoteRecord( 178 card: SembleCard, 179 now: Date, 180 did: string, 181 parentRef?: MockStrongRef 182): { 183 recordId: string; 184 envelope: AtprotoRecordEnvelope; 185} { 186 const rkey = stableRkey(`note:${card.id}`); 187 const uri = `at://${did}/${CARD_COLLECTION}/${rkey}`; 188 const strongRef = {uri, cid: `mock:${stableHash(uri)}`}; 189 190 const record: Record<string, unknown> = { 191 $type: CARD_COLLECTION, 192 type: "NOTE", 193 content: { 194 $type: `${CARD_COLLECTION}#noteContent`, 195 text: card.note 196 }, 197 createdAt: now.toISOString() 198 }; 199 200 if (card.url) { 201 record.url = card.url; 202 } 203 204 if (parentRef) { 205 record.parentCard = parentRef; 206 } 207 208 const envelope: AtprotoRecordEnvelope = { 209 collection: CARD_COLLECTION, 210 rkey, 211 record, 212 strongRef, 213 recordType: "note", 214 recordId: `${card.id}:note`, 215 cardId: card.id 216 }; 217 218 return {recordId: `${card.id}:note`, envelope}; 219} 220 221function buildCollectionLinkRecord( 222 link: SembleCollectionLink, 223 now: Date, 224 did: string, 225 collectionRef?: MockStrongRef, 226 cardRef?: MockStrongRef 227): { 228 recordId: string; 229 envelope: AtprotoRecordEnvelope; 230} { 231 const rkey = stableRkey(`link:${link.collectionName}:${link.cardId}`); 232 const uri = `at://${did}/${COLLECTION_LINK_COLLECTION}/${rkey}`; 233 const strongRef = {uri, cid: `mock:${stableHash(uri)}`}; 234 235 const record: Record<string, unknown> = { 236 $type: COLLECTION_LINK_COLLECTION, 237 collection: collectionRef ?? {uri: "at://unknown/collection", cid: "mock:unknown"}, 238 card: cardRef ?? {uri: "at://unknown/card", cid: "mock:unknown"}, 239 addedBy: did, 240 addedAt: now.toISOString(), 241 createdAt: now.toISOString() 242 }; 243 244 const envelope: AtprotoRecordEnvelope = { 245 collection: COLLECTION_LINK_COLLECTION, 246 rkey, 247 record, 248 strongRef, 249 recordType: "collectionLink", 250 recordId: `${link.collectionName}:${link.cardId}`, 251 cardId: link.cardId, 252 collectionName: link.collectionName 253 }; 254 255 return {recordId: `${link.collectionName}:${link.cardId}`, envelope}; 256} 257 258export function stableRkey(seed: string): string { 259 return `semble-${stableHash(seed)}`; 260} 261 262function stableHash(input: string): string { 263 let hash = 0; 264 for (let i = 0; i < input.length; i += 1) { 265 hash = (hash * 31 + input.charCodeAt(i)) | 0; 266 } 267 return Math.abs(hash).toString(36); 268}