Various AT Protocol integrations with obsidian
20
fork

Configure Feed

Select the types of activity you want to include in your feed.

pckt: Properly encode facets (#14)

Producing a Pckt.blog artifact now includes the correct facets for
encoding headers, links, and styling.

authored by

Isaac Corbrey and committed by
GitHub
778ab113 2d4b808a

+134 -15
+134 -15
src/lib/markdown/pckt.ts
··· 11 11 BlogPcktBlockHorizontalRule, 12 12 BlogPcktBlockBlockquote, 13 13 BlogPcktContent, 14 + BlogPcktRichtextFacet, 14 15 } from "@atcute/pckt"; 15 - import { parseMarkdown, extractText, cleanPlaintext } from "../markdown"; 16 + import { parseMarkdown, cleanPlaintext } from "../markdown"; 16 17 17 18 type PcktBlock = 18 19 | BlogPcktBlockText.Main ··· 22 23 | BlogPcktBlockOrderedList.Main 23 24 | BlogPcktBlockHorizontalRule.Main 24 25 | BlogPcktBlockBlockquote.Main; 26 + 27 + type PcktTextBlock = BlogPcktBlockText.Main & { $type: "blog.pckt.block.text" }; 25 28 26 29 export function markdownToPcktContent(markdown: string): BlogPcktContent.Main { 27 30 const tree = parseMarkdown(markdown); ··· 40 43 } as BlogPcktContent.Main; 41 44 } 42 45 46 + const textEncoder = new TextEncoder(); 47 + 48 + function byteLength(text: string): number { 49 + return textEncoder.encode(text).length; 50 + } 51 + 52 + function createFacet(byteStart: number, byteEnd: number, features: BlogPcktRichtextFacet.Main["features"]): BlogPcktRichtextFacet.Main { 53 + return { 54 + $type: "blog.pckt.richtext.facet", 55 + index: { 56 + $type: "blog.pckt.richtext.facet#byteSlice", 57 + byteStart, 58 + byteEnd, 59 + }, 60 + features, 61 + }; 62 + } 63 + 64 + function buildTextFromNodes(nodes: RootContent[]): { text: string; facets: BlogPcktRichtextFacet.Main[] } { 65 + let text = ""; 66 + let byteOffset = 0; 67 + const facets: BlogPcktRichtextFacet.Main[] = []; 68 + 69 + const appendText = (value: string) => { 70 + if (!value) { 71 + return; 72 + } 73 + text += value; 74 + byteOffset += byteLength(value); 75 + }; 76 + 77 + const walk = (node: RootContent) => { 78 + switch (node.type) { 79 + case "text": 80 + appendText(node.value); 81 + return; 82 + case "inlineCode": { 83 + const start = byteOffset; 84 + appendText(node.value); 85 + const end = byteOffset; 86 + if (start < end) { 87 + facets.push(createFacet(start, end, [{ $type: "blog.pckt.richtext.facet#code" }])); 88 + } 89 + return; 90 + } 91 + case "strong": { 92 + const start = byteOffset; 93 + for (const child of node.children) { 94 + walk(child); 95 + } 96 + const end = byteOffset; 97 + if (start < end) { 98 + facets.push(createFacet(start, end, [{ $type: "blog.pckt.richtext.facet#bold" }])); 99 + } 100 + return; 101 + } 102 + case "emphasis": { 103 + const start = byteOffset; 104 + for (const child of node.children) { 105 + walk(child); 106 + } 107 + const end = byteOffset; 108 + if (start < end) { 109 + facets.push(createFacet(start, end, [{ $type: "blog.pckt.richtext.facet#italic" }])); 110 + } 111 + return; 112 + } 113 + case "delete": { 114 + const start = byteOffset; 115 + for (const child of node.children) { 116 + walk(child); 117 + } 118 + const end = byteOffset; 119 + if (start < end) { 120 + facets.push(createFacet(start, end, [{ $type: "blog.pckt.richtext.facet#strikethrough" }])); 121 + } 122 + return; 123 + } 124 + case "link": { 125 + const start = byteOffset; 126 + for (const child of node.children) { 127 + walk(child); 128 + } 129 + const end = byteOffset; 130 + if (start < end && node.url) { 131 + facets.push(createFacet(start, end, [{ $type: "blog.pckt.richtext.facet#link", uri: node.url }])); 132 + } 133 + return; 134 + } 135 + case "break": 136 + appendText("\n"); 137 + return; 138 + default: { 139 + if ("children" in node && Array.isArray(node.children)) { 140 + for (const child of node.children) { 141 + walk(child); 142 + } 143 + return; 144 + } 145 + if ("value" in node && typeof node.value === "string") { 146 + appendText(node.value); 147 + } 148 + return; 149 + } 150 + } 151 + }; 152 + 153 + for (const node of nodes) { 154 + walk(node); 155 + } 156 + 157 + return { text, facets }; 158 + } 159 + 160 + function buildTextBlockFromNode(node: { children?: RootContent[] }): PcktTextBlock { 161 + const { text, facets } = buildTextFromNodes(node.children ?? []); 162 + return { 163 + $type: "blog.pckt.block.text", 164 + plaintext: text, 165 + facets: facets.length > 0 ? facets : undefined, 166 + } as PcktTextBlock; 167 + } 168 + 43 169 function convertNodeToBlock(node: RootContent): PcktBlock | null { 44 170 switch (node.type) { 45 171 case "heading": { 172 + const { text, facets } = buildTextFromNodes(node.children); 46 173 const block: BlogPcktBlockHeading.Main = { 47 174 $type: "blog.pckt.block.heading", 48 175 level: node.depth, 49 - plaintext: extractText(node), 176 + plaintext: text, 177 + facets: facets.length > 0 ? facets : undefined, 50 178 }; 51 179 return block; 52 180 } 53 181 54 182 case "paragraph": { 55 - const block: BlogPcktBlockText.Main = { 56 - $type: "blog.pckt.block.text", 57 - plaintext: extractText(node), 58 - }; 59 - return block; 183 + return buildTextBlockFromNode(node); 60 184 } 61 185 62 186 case "list": { 63 187 const listItems: BlogPcktBlockListItem.Main[] = node.children.map((item) => ({ 64 188 $type: "blog.pckt.block.listItem", 65 - content: [{ 66 - $type: "blog.pckt.block.text", 67 - plaintext: extractText(item), 68 - }], 189 + content: [buildTextBlockFromNode(item)], 69 190 })); 70 191 71 192 if (node.ordered) { ··· 100 221 } 101 222 102 223 case "blockquote": { 224 + const textBlock = buildTextBlockFromNode(node); 103 225 const block: BlogPcktBlockBlockquote.Main = { 104 226 $type: "blog.pckt.block.blockquote", 105 - content: [{ 106 - $type: "blog.pckt.block.text", 107 - plaintext: extractText(node), 108 - }], 227 + content: [textBlock], 109 228 }; 110 229 return block; 111 230 }