birdclaw 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +10 -8
  3. package/bin/birdclaw.mjs +4 -66
  4. package/dist/cli/birdclaw.js +28165 -0
  5. package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
  6. package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
  7. package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
  8. package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
  9. package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
  10. package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
  11. package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
  12. package/dist/client/assets/chunk-C0phanGV.js +1 -0
  13. package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
  14. package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
  15. package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
  16. package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
  17. package/dist/client/assets/discuss-DTjaOugw.js +1 -0
  18. package/dist/client/assets/dms-CGd6Eogq.js +1 -0
  19. package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
  20. package/dist/client/assets/index-CEIG7S6s.js +31 -0
  21. package/dist/client/assets/index-bpFrQ5yG.css +1 -0
  22. package/dist/client/assets/likes-cLrcAQem.js +1 -0
  23. package/dist/client/assets/links-CM9QO1LC.js +1 -0
  24. package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
  25. package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
  26. package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
  27. package/dist/client/assets/network-map-B5a3whqj.js +2 -0
  28. package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
  29. package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
  30. package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
  31. package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
  32. package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
  33. package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
  34. package/dist/client/assets/react-nBylq1U5.js +1 -0
  35. package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
  36. package/dist/client/assets/routes-q7ZaW4En.js +1 -0
  37. package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
  38. package/dist/client/assets/search-Dq5kYsxA.js +1 -0
  39. package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
  40. package/dist/client/assets/styles-B8s3tt3r.css +2 -0
  41. package/dist/client/assets/today-CeG1O7Py.js +1 -0
  42. package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
  43. package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
  44. package/dist/client/assets/users-CHbKlqUr.js +1 -0
  45. package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
  46. package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
  47. package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
  48. package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
  49. package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
  50. package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
  51. package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
  52. package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
  53. package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
  54. package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
  55. package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
  56. package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
  57. package/dist/server/assets/blocks-DSgQghqP.js +17 -0
  58. package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
  59. package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
  60. package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
  61. package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
  62. package/dist/server/assets/discuss-QTo08NCN.js +338 -0
  63. package/dist/server/assets/dms-BuLj4bp8.js +368 -0
  64. package/dist/server/assets/dms-DAyQrVM2.js +240 -0
  65. package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
  66. package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
  67. package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
  68. package/dist/server/assets/likes-DBOAtsJU.js +14 -0
  69. package/dist/server/assets/links-DAhSjynu.js +240 -0
  70. package/dist/server/assets/links-DmH5lxHO.js +468 -0
  71. package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
  72. package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
  73. package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
  74. package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
  75. package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
  76. package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
  77. package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
  78. package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
  79. package/dist/server/assets/query-client-BdauHBgd.js +923 -0
  80. package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
  81. package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
  82. package/dist/server/assets/router-DqkwSPiM.js +17026 -0
  83. package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
  84. package/dist/server/assets/start-FaDOTmto.js +4 -0
  85. package/dist/server/assets/today-B5hVM57Z.js +17 -0
  86. package/dist/server/assets/today-fvGiZ7ud.js +303 -0
  87. package/dist/server/assets/ui-qbmcMSSX.js +137 -0
  88. package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
  89. package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
  90. package/dist/server/server.js +5520 -0
  91. package/package.json +27 -34
  92. package/playwright.config.ts +0 -35
  93. package/scripts/browser-perf.mjs +0 -427
  94. package/scripts/build-docs-site.mjs +0 -967
  95. package/scripts/docs-site-assets.mjs +0 -311
  96. package/scripts/run-vitest.mjs +0 -21
  97. package/scripts/sanitize-node-options.mjs +0 -23
  98. package/scripts/start-test-server.mjs +0 -42
  99. package/src/cli-moderation.ts +0 -210
  100. package/src/cli.ts +0 -2336
  101. package/src/components/AccountSwitcher.tsx +0 -186
  102. package/src/components/AppNav.tsx +0 -118
  103. package/src/components/AvatarChip.tsx +0 -58
  104. package/src/components/BrandMark.tsx +0 -67
  105. package/src/components/ConversationThread.tsx +0 -131
  106. package/src/components/DmWorkspace.tsx +0 -286
  107. package/src/components/EmbeddedTweetCard.tsx +0 -55
  108. package/src/components/FeedState.tsx +0 -147
  109. package/src/components/FloatingPreview.tsx +0 -382
  110. package/src/components/InboxCard.tsx +0 -152
  111. package/src/components/LinkPreviewCard.tsx +0 -292
  112. package/src/components/MarkdownViewer.tsx +0 -777
  113. package/src/components/ProfileAnalysisStream.tsx +0 -428
  114. package/src/components/ProfilePreview.tsx +0 -129
  115. package/src/components/SavedTimelineView.tsx +0 -155
  116. package/src/components/SmartTimestamp.tsx +0 -72
  117. package/src/components/SyncNowButton.tsx +0 -140
  118. package/src/components/ThemeSlider.tsx +0 -80
  119. package/src/components/TimelineCard.tsx +0 -533
  120. package/src/components/TimelineRouteFrame.tsx +0 -186
  121. package/src/components/TweetArticleCard.tsx +0 -66
  122. package/src/components/TweetMediaGrid.tsx +0 -185
  123. package/src/components/TweetRichText.tsx +0 -159
  124. package/src/components/account-selection.ts +0 -64
  125. package/src/components/useTimelineRouteData.ts +0 -323
  126. package/src/lib/account-sync-job.ts +0 -666
  127. package/src/lib/actions-transport.ts +0 -300
  128. package/src/lib/api-client.ts +0 -419
  129. package/src/lib/archive-finder.ts +0 -185
  130. package/src/lib/archive-import.ts +0 -2764
  131. package/src/lib/authored-live.ts +0 -1072
  132. package/src/lib/avatar-cache.ts +0 -349
  133. package/src/lib/backup.ts +0 -2741
  134. package/src/lib/bird-actions.ts +0 -294
  135. package/src/lib/bird-command.ts +0 -130
  136. package/src/lib/bird.ts +0 -1140
  137. package/src/lib/blocklist.ts +0 -136
  138. package/src/lib/blocks-write.ts +0 -167
  139. package/src/lib/blocks.ts +0 -389
  140. package/src/lib/bookmark-sync-job.ts +0 -536
  141. package/src/lib/client-cache.ts +0 -109
  142. package/src/lib/config.ts +0 -224
  143. package/src/lib/conversation-surface.ts +0 -205
  144. package/src/lib/data-sources.ts +0 -219
  145. package/src/lib/db.ts +0 -1135
  146. package/src/lib/dms-live.ts +0 -933
  147. package/src/lib/effect-runtime.ts +0 -45
  148. package/src/lib/follow-graph.ts +0 -1097
  149. package/src/lib/geocoding.ts +0 -296
  150. package/src/lib/http-effect.ts +0 -222
  151. package/src/lib/identity-search-index.ts +0 -369
  152. package/src/lib/inbox.ts +0 -241
  153. package/src/lib/link-index.ts +0 -641
  154. package/src/lib/link-insights.ts +0 -860
  155. package/src/lib/link-preview-metadata.ts +0 -754
  156. package/src/lib/location.ts +0 -137
  157. package/src/lib/media-fetch.ts +0 -860
  158. package/src/lib/media-includes.ts +0 -165
  159. package/src/lib/mention-threads-live.ts +0 -917
  160. package/src/lib/mentions-export.ts +0 -147
  161. package/src/lib/mentions-live.ts +0 -1238
  162. package/src/lib/moderation-target.ts +0 -216
  163. package/src/lib/moderation-write.ts +0 -131
  164. package/src/lib/mutes-write.ts +0 -167
  165. package/src/lib/mutes.ts +0 -84
  166. package/src/lib/ndjson-stream.ts +0 -106
  167. package/src/lib/network-map.ts +0 -382
  168. package/src/lib/openai.ts +0 -117
  169. package/src/lib/period-digest.ts +0 -1552
  170. package/src/lib/present.ts +0 -118
  171. package/src/lib/profile-affiliation-hydration.ts +0 -228
  172. package/src/lib/profile-affiliations.ts +0 -262
  173. package/src/lib/profile-analysis.ts +0 -1272
  174. package/src/lib/profile-bio-entities.ts +0 -318
  175. package/src/lib/profile-history.ts +0 -164
  176. package/src/lib/profile-hydration.ts +0 -180
  177. package/src/lib/profile-replies.ts +0 -77
  178. package/src/lib/profile-resolver.ts +0 -650
  179. package/src/lib/queries.ts +0 -2381
  180. package/src/lib/research.ts +0 -652
  181. package/src/lib/search-discussion.ts +0 -1016
  182. package/src/lib/seed.ts +0 -613
  183. package/src/lib/sqlite.ts +0 -147
  184. package/src/lib/sync-cache.ts +0 -65
  185. package/src/lib/theme-transition.ts +0 -113
  186. package/src/lib/theme.tsx +0 -165
  187. package/src/lib/timeline-collections-live.ts +0 -581
  188. package/src/lib/timeline-live.ts +0 -452
  189. package/src/lib/tweet-account-edges.ts +0 -48
  190. package/src/lib/tweet-lookup.ts +0 -46
  191. package/src/lib/tweet-render.ts +0 -430
  192. package/src/lib/tweet-search-live.ts +0 -566
  193. package/src/lib/types.ts +0 -723
  194. package/src/lib/ui.ts +0 -529
  195. package/src/lib/url-expansion-store.ts +0 -110
  196. package/src/lib/url-expansion.ts +0 -343
  197. package/src/lib/url-safety.ts +0 -220
  198. package/src/lib/web-sync.ts +0 -516
  199. package/src/lib/whois.ts +0 -1079
  200. package/src/lib/x-profile.ts +0 -365
  201. package/src/lib/x-web.ts +0 -199
  202. package/src/lib/xurl-rate-limits.ts +0 -267
  203. package/src/lib/xurl.ts +0 -1677
  204. package/src/routeTree.gen.ts +0 -723
  205. package/src/router.tsx +0 -20
  206. package/src/routes/__root.tsx +0 -79
  207. package/src/routes/api/action.tsx +0 -147
  208. package/src/routes/api/avatar.tsx +0 -50
  209. package/src/routes/api/blocks.tsx +0 -35
  210. package/src/routes/api/conversation.tsx +0 -45
  211. package/src/routes/api/data-sources.tsx +0 -24
  212. package/src/routes/api/inbox.tsx +0 -43
  213. package/src/routes/api/link-insights.tsx +0 -85
  214. package/src/routes/api/link-preview.tsx +0 -47
  215. package/src/routes/api/network-map.tsx +0 -55
  216. package/src/routes/api/period-digest.tsx +0 -101
  217. package/src/routes/api/profile-analysis.tsx +0 -97
  218. package/src/routes/api/profile-hydrate.tsx +0 -54
  219. package/src/routes/api/query.tsx +0 -115
  220. package/src/routes/api/search-discussion.tsx +0 -113
  221. package/src/routes/api/status.tsx +0 -28
  222. package/src/routes/api/sync.tsx +0 -105
  223. package/src/routes/api/xurl-rate-limits.tsx +0 -24
  224. package/src/routes/blocks.tsx +0 -377
  225. package/src/routes/bookmarks.tsx +0 -18
  226. package/src/routes/data-sources.tsx +0 -257
  227. package/src/routes/discuss.tsx +0 -419
  228. package/src/routes/dms.tsx +0 -432
  229. package/src/routes/inbox.tsx +0 -226
  230. package/src/routes/index.tsx +0 -32
  231. package/src/routes/likes.tsx +0 -18
  232. package/src/routes/links.tsx +0 -994
  233. package/src/routes/mentions.tsx +0 -32
  234. package/src/routes/network-map.tsx +0 -1035
  235. package/src/routes/profile-analyze.tsx +0 -112
  236. package/src/routes/profiles.$handle.tsx +0 -228
  237. package/src/routes/rate-limits.tsx +0 -309
  238. package/src/routes/today.tsx +0 -511
  239. package/src/styles.css +0 -257
  240. package/tsconfig.json +0 -29
  241. package/vite.config.ts +0 -29
  242. /package/{public → dist/client}/birdclaw-mark.png +0 -0
  243. /package/{public → dist/client}/favicon.ico +0 -0
  244. /package/{public → dist/client}/logo192.png +0 -0
  245. /package/{public → dist/client}/logo512.png +0 -0
  246. /package/{public → dist/client}/manifest.json +0 -0
  247. /package/{public → dist/client}/robots.txt +0 -0
@@ -1,967 +0,0 @@
1
- #!/usr/bin/env node
2
- import fs from "node:fs";
3
- import path from "node:path";
4
-
5
- import {
6
- css,
7
- js,
8
- preThemeScript,
9
- themeToggleHtml,
10
- } from "./docs-site-assets.mjs";
11
-
12
- const root = process.cwd();
13
- const docsDir = path.join(root, "docs");
14
- const outDir = path.join(root, "dist", "docs-site");
15
- const repoBase = "https://github.com/steipete/birdclaw";
16
- const repoEditBase = `${repoBase}/edit/main/docs`;
17
- const cname = readCname();
18
- const siteBase = cname ? `https://${cname}` : "";
19
-
20
- const productName = "birdclaw";
21
- const productTagline = "Local Twitter memory in SQLite";
22
- const productDescription =
23
- "birdclaw is a local-first Twitter workspace: archive import, cached live reads, focused triage, and reply flows in one local web app + CLI.";
24
- const brewInstall = "brew install steipete/tap/birdclaw";
25
-
26
- const sections = [
27
- [
28
- "Start",
29
- ["index.md", "install.md", "auth.md", "quickstart.md", "configuration.md"],
30
- ],
31
- [
32
- "Archive & Sync",
33
- ["archive.md", "sync.md", "media.md", "backup.md", "jobs.md"],
34
- ],
35
- [
36
- "Reading & Triage",
37
- ["search.md", "mentions.md", "dms.md", "inbox.md", "research.md"],
38
- ],
39
- ["Acting", ["compose.md", "moderation.md"]],
40
- ["Reference", ["cli.md", "data-architecture.md", "spec.md"]],
41
- ];
42
-
43
- const buildExcludes = [];
44
-
45
- fs.rmSync(outDir, { recursive: true, force: true });
46
- fs.mkdirSync(outDir, { recursive: true });
47
-
48
- const allPages = allMarkdown(docsDir).map((file) => {
49
- const rel = path.relative(docsDir, file).replaceAll(path.sep, "/");
50
- const raw = fs.readFileSync(file, "utf8");
51
- const { frontmatter, body } = parseFrontmatter(raw);
52
- const cleaned = stripStrayDirectives(body);
53
- const title =
54
- frontmatter.title ||
55
- firstHeading(cleaned) ||
56
- titleize(path.basename(rel, ".md"));
57
- return {
58
- file,
59
- rel,
60
- title,
61
- outRel: outPath(rel, frontmatter),
62
- markdown: cleaned,
63
- frontmatter,
64
- };
65
- });
66
-
67
- const pages = allPages.filter(
68
- (page) => !buildExcludes.some((re) => re.test(page.rel)),
69
- );
70
- const pageMap = new Map(pages.map((page) => [page.rel, page]));
71
- const permalinkMap = new Map();
72
- for (const page of pages) {
73
- if (page.frontmatter.permalink) {
74
- permalinkMap.set(normalizePermalink(page.frontmatter.permalink), page);
75
- }
76
- }
77
-
78
- const nav = sections
79
- .map(([name, rels]) => ({
80
- name,
81
- pages: rels.map((rel) => pageMap.get(rel)).filter(Boolean),
82
- }))
83
- .filter((section) => section.pages.length);
84
-
85
- const sectionByRel = new Map();
86
- for (const section of nav)
87
- for (const page of section.pages) sectionByRel.set(page.rel, section.name);
88
- const orderedPages = nav.flatMap((s) => s.pages);
89
-
90
- for (const page of pages) {
91
- const html = markdownToHtml(page.markdown, page.rel);
92
- const toc = tocFromHtml(html);
93
- const idx = orderedPages.findIndex((p) => p.rel === page.rel);
94
- const prev = idx > 0 ? orderedPages[idx - 1] : null;
95
- const next =
96
- idx >= 0 && idx < orderedPages.length - 1 ? orderedPages[idx + 1] : null;
97
- const sectionName = sectionByRel.get(page.rel) || "Reference";
98
- const pageOut = path.join(outDir, page.outRel);
99
- fs.mkdirSync(path.dirname(pageOut), { recursive: true });
100
- fs.writeFileSync(
101
- pageOut,
102
- layout({ page, html, toc, prev, next, sectionName }),
103
- "utf8",
104
- );
105
- }
106
-
107
- copyStaticAsset("birdclaw-mark.png");
108
- copyStaticAsset("favicon.ico");
109
- copyStaticAsset("social-card.svg");
110
- copyStaticAsset("social-card.png");
111
- fs.writeFileSync(path.join(outDir, ".nojekyll"), "", "utf8");
112
- if (cname) fs.writeFileSync(path.join(outDir, "CNAME"), cname, "utf8");
113
- validateLinks(outDir);
114
- fs.writeFileSync(path.join(outDir, "llms.txt"), llmsTxt(), "utf8");
115
- console.log(`built docs site: ${path.relative(root, outDir)}`);
116
-
117
- function llmsTxt() {
118
- const origin = docsOrigin();
119
- const source = docsSourceUrl();
120
- const name =
121
- typeof productName !== "undefined" ? productName : path.basename(root);
122
- const description =
123
- typeof productDescription !== "undefined"
124
- ? productDescription
125
- : `${name} documentation index.`;
126
- const install = docsInstallHint();
127
- const docPages = docsLlmsPages().map(
128
- (page) => `- ${page.title}: ${pageUrl(origin, page.outRel)}`,
129
- );
130
- const lines = [
131
- `# ${name}`,
132
- "",
133
- description,
134
- "",
135
- "Canonical documentation:",
136
- ...docPages,
137
- ];
138
- if (install) {
139
- lines.push("", "Install:", `- ${install}`);
140
- }
141
- if (source) {
142
- lines.push("", `Source: ${source}`);
143
- }
144
- lines.push(
145
- "",
146
- "Guidance for agents:",
147
- "- Prefer the canonical documentation URLs above over README excerpts or package metadata.",
148
- "- Fetch only the pages needed for the current task; this is an index, not a full-site corpus.",
149
- );
150
- return `${lines.join("\n")}\n`;
151
- }
152
-
153
- function docsLlmsPages() {
154
- const seen = new Set();
155
- const ordered = typeof orderedPages !== "undefined" ? orderedPages : [];
156
- return [...ordered, ...pages].filter(
157
- (page) => page.outRel && !seen.has(page.outRel) && seen.add(page.outRel),
158
- );
159
- }
160
-
161
- function docsOrigin() {
162
- const value =
163
- (typeof siteBase !== "undefined" && siteBase) ||
164
- (typeof siteUrl !== "undefined" && siteUrl) ||
165
- (typeof customDomain !== "undefined" && customDomain
166
- ? `https://${customDomain}`
167
- : "");
168
- return value.replace(/\/$/, "");
169
- }
170
-
171
- function docsSourceUrl() {
172
- if (typeof repoBase !== "undefined") return repoBase;
173
- if (typeof repoUrl !== "undefined") return repoUrl;
174
- if (typeof repoEditBase !== "undefined")
175
- return repoEditBase.replace(/\/edit\/main\/docs\/?$/, "");
176
- return "";
177
- }
178
-
179
- function docsInstallHint() {
180
- if (typeof installCommand !== "undefined") return installCommand;
181
- if (typeof installLine !== "undefined") return installLine;
182
- if (typeof installCmd !== "undefined") return installCmd;
183
- if (typeof installSnippet !== "undefined") return installSnippet;
184
- if (typeof brewInstall !== "undefined") return brewInstall;
185
- return "";
186
- }
187
-
188
- function pageUrl(origin, outRel) {
189
- const normalized =
190
- outRel === "index.html"
191
- ? ""
192
- : outRel.replace(/(?:^|\/)index\.html$/, (match) =>
193
- match === "index.html" ? "" : "/",
194
- );
195
- if (!origin) return normalized || "index.html";
196
- return normalized ? `${origin}/${normalized}` : `${origin}/`;
197
- }
198
-
199
- function copyStaticAsset(name) {
200
- const source = path.join(docsDir, name);
201
- if (fs.existsSync(source)) fs.copyFileSync(source, path.join(outDir, name));
202
- }
203
-
204
- function readCname() {
205
- for (const candidate of [
206
- path.join(docsDir, "CNAME"),
207
- path.join(root, "CNAME"),
208
- ]) {
209
- if (fs.existsSync(candidate))
210
- return fs.readFileSync(candidate, "utf8").trim();
211
- }
212
- return "";
213
- }
214
-
215
- function parseFrontmatter(raw) {
216
- const match = raw.match(/^---\n([\s\S]*?)\n---\n?/);
217
- if (!match) return { frontmatter: {}, body: raw };
218
- const fm = {};
219
- for (const line of match[1].split("\n")) {
220
- const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/);
221
- if (!m) continue;
222
- let value = m[2];
223
- if (
224
- (value.startsWith('"') && value.endsWith('"')) ||
225
- (value.startsWith("'") && value.endsWith("'"))
226
- ) {
227
- value = value.slice(1, -1);
228
- }
229
- fm[m[1]] = value;
230
- }
231
- return { frontmatter: fm, body: raw.slice(match[0].length) };
232
- }
233
-
234
- function stripStrayDirectives(body) {
235
- return body
236
- .replace(/\r\n/g, "\n")
237
- .split("\n")
238
- .filter((line) => !/^\s*\{:\s*[^}]*\}\s*$/.test(line))
239
- .map((line) => line.replace(/\s*\{:\s*[^}]*\}\s*$/, ""))
240
- .join("\n");
241
- }
242
-
243
- function normalizePermalink(value) {
244
- let v = value.trim();
245
- if (!v) return "/";
246
- if (!v.startsWith("/")) v = `/${v}`;
247
- if (v.length > 1 && v.endsWith("/")) v = v.slice(0, -1);
248
- return v;
249
- }
250
-
251
- function allMarkdown(dir) {
252
- return fs
253
- .readdirSync(dir, { withFileTypes: true })
254
- .flatMap((entry) => {
255
- const full = path.join(dir, entry.name);
256
- if (entry.isDirectory()) return allMarkdown(full);
257
- return entry.name.endsWith(".md") ? [full] : [];
258
- })
259
- .sort();
260
- }
261
-
262
- function outPath(rel, frontmatter = {}) {
263
- if (frontmatter.permalink) {
264
- const permalink = normalizePermalink(frontmatter.permalink);
265
- if (permalink === "/") return "index.html";
266
- return `${permalink.slice(1)}/index.html`;
267
- }
268
- if (rel === "index.md") return "index.html";
269
- if (rel === "README.md") return "index.html";
270
- if (rel.endsWith("/README.md"))
271
- return rel.replace(/README\.md$/, "index.html");
272
- return rel.replace(/\.md$/, ".html");
273
- }
274
-
275
- function firstHeading(markdown) {
276
- return markdown.match(/^#\s+(.+)$/m)?.[1]?.trim();
277
- }
278
-
279
- function titleize(input) {
280
- return input.replaceAll("-", " ").replace(/\b\w/g, (m) => m.toUpperCase());
281
- }
282
-
283
- function markdownToHtml(markdown, currentRel) {
284
- const lines = markdown.replace(/\r\n/g, "\n").split("\n");
285
- const html = [];
286
- let paragraph = [];
287
- let list = null;
288
- let fence = null;
289
- let blockquote = [];
290
-
291
- const flushParagraph = () => {
292
- if (!paragraph.length) return;
293
- html.push(`<p>${inline(paragraph.join(" "), currentRel)}</p>`);
294
- paragraph = [];
295
- };
296
- const closeList = () => {
297
- if (!list) return;
298
- html.push(`</${list}>`);
299
- list = null;
300
- };
301
- const flushBlockquote = () => {
302
- if (!blockquote.length) return;
303
- const inner = markdownToHtml(blockquote.join("\n"), currentRel);
304
- html.push(`<blockquote>${inner}</blockquote>`);
305
- blockquote = [];
306
- };
307
- const splitRow = (line) => {
308
- let trimmed = line.trim();
309
- if (trimmed.startsWith("|")) trimmed = trimmed.slice(1);
310
- if (trimmed.endsWith("|") && !trimmed.endsWith("\\|"))
311
- trimmed = trimmed.slice(0, -1);
312
- const cells = [];
313
- let current = "";
314
- for (let idx = 0; idx < trimmed.length; idx++) {
315
- const char = trimmed[idx];
316
- if (char === "\\" && trimmed[idx + 1] === "|") {
317
- current += "\\|";
318
- idx += 1;
319
- continue;
320
- }
321
- if (char === "|") {
322
- cells.push(current.trim().replace(/\\\|/g, "|"));
323
- current = "";
324
- continue;
325
- }
326
- current += char;
327
- }
328
- cells.push(current.trim().replace(/\\\|/g, "|"));
329
- return cells;
330
- };
331
- const isDivider = (line) =>
332
- /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(line);
333
-
334
- for (let i = 0; i < lines.length; i++) {
335
- const line = lines[i];
336
- const fenceMatch = line.match(/^```([\w+-]+)?\s*$/);
337
- if (fenceMatch) {
338
- flushParagraph();
339
- closeList();
340
- flushBlockquote();
341
- if (fence) {
342
- const lang = fence.lang;
343
- const body = highlightCode(fence.lines.join("\n"), lang);
344
- html.push(
345
- `<pre><code class="language-${escapeAttr(lang)}">${body}</code></pre>`,
346
- );
347
- fence = null;
348
- } else {
349
- fence = { lang: fenceMatch[1] || "text", lines: [] };
350
- }
351
- continue;
352
- }
353
- if (fence) {
354
- fence.lines.push(line);
355
- continue;
356
- }
357
- if (/^>\s?/.test(line)) {
358
- flushParagraph();
359
- closeList();
360
- blockquote.push(line.replace(/^>\s?/, ""));
361
- continue;
362
- }
363
- flushBlockquote();
364
- if (!line.trim()) {
365
- flushParagraph();
366
- closeList();
367
- continue;
368
- }
369
- if (/^\s*---+\s*$/.test(line)) {
370
- flushParagraph();
371
- closeList();
372
- html.push("<hr>");
373
- continue;
374
- }
375
- const heading = line.match(/^(#{1,4})\s+(.+)$/);
376
- if (heading) {
377
- flushParagraph();
378
- closeList();
379
- const level = heading[1].length;
380
- const text = heading[2].trim();
381
- const id = slug(text);
382
- const inner = inline(text, currentRel);
383
- if (level === 1) {
384
- html.push(`<h1 id="${id}">${inner}</h1>`);
385
- } else {
386
- html.push(
387
- `<h${level} id="${id}"><a class="anchor" href="#${id}" aria-label="Anchor link">#</a>${inner}</h${level}>`,
388
- );
389
- }
390
- continue;
391
- }
392
- if (
393
- line.trimStart().startsWith("|") &&
394
- line.includes("|", line.indexOf("|") + 1) &&
395
- isDivider(lines[i + 1] || "")
396
- ) {
397
- flushParagraph();
398
- closeList();
399
- const header = splitRow(line);
400
- const aligns = splitRow(lines[i + 1]).map((cell) => {
401
- const left = cell.startsWith(":");
402
- const right = cell.endsWith(":");
403
- return right && left ? "center" : right ? "right" : left ? "left" : "";
404
- });
405
- i += 1;
406
- const rows = [];
407
- while (i + 1 < lines.length && lines[i + 1].trimStart().startsWith("|")) {
408
- i += 1;
409
- rows.push(splitRow(lines[i]));
410
- }
411
- const th = header
412
- .map(
413
- (c, idx) =>
414
- `<th${aligns[idx] ? ` style="text-align:${aligns[idx]}"` : ""}>${inline(c, currentRel)}</th>`,
415
- )
416
- .join("");
417
- const tb = rows
418
- .map(
419
- (r) =>
420
- `<tr>${r.map((c, idx) => `<td${aligns[idx] ? ` style="text-align:${aligns[idx]}"` : ""}>${inline(c, currentRel)}</td>`).join("")}</tr>`,
421
- )
422
- .join("");
423
- html.push(
424
- `<table><thead><tr>${th}</tr></thead><tbody>${tb}</tbody></table>`,
425
- );
426
- continue;
427
- }
428
- const bullet = line.match(/^\s*-\s+(.+)$/);
429
- const numbered = line.match(/^\s*\d+\.\s+(.+)$/);
430
- if (bullet || numbered) {
431
- flushParagraph();
432
- const tag = bullet ? "ul" : "ol";
433
- if (list && list !== tag) closeList();
434
- if (!list) {
435
- list = tag;
436
- html.push(`<${tag}>`);
437
- }
438
- html.push(`<li>${inline((bullet || numbered)[1], currentRel)}</li>`);
439
- continue;
440
- }
441
- paragraph.push(line.trim());
442
- }
443
- flushParagraph();
444
- closeList();
445
- flushBlockquote();
446
- return html.join("\n");
447
- }
448
-
449
- function inline(text, currentRel) {
450
- const stash = [];
451
- let out = text.replace(/`([^`]+)`/g, (_, code) => {
452
- stash.push(`<code>${escapeHtml(code)}</code>`);
453
- return `\u0000${stash.length - 1}\u0000`;
454
- });
455
- const stashLink = (label, href) => {
456
- stash.push(
457
- `<a href="${escapeAttr(rewriteHref(href, currentRel))}">${renderInlineText(label, stash)}</a>`,
458
- );
459
- return `\u0000${stash.length - 1}\u0000`;
460
- };
461
- out = out.replace(/\[([^\]]+)\]\(<([^<>]+)>\)/g, (_, label, href) =>
462
- stashLink(label, href),
463
- );
464
- out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) =>
465
- stashLink(label, href),
466
- );
467
- out = out.replace(/<(https?:\/\/[^\s<>]+)>/g, (_, url) => {
468
- stash.push(`<a href="${escapeAttr(url)}">${escapeHtml(url)}</a>`);
469
- return `\u0000${stash.length - 1}\u0000`;
470
- });
471
- return renderInlineText(out, stash);
472
- }
473
-
474
- function renderInlineText(text, stash) {
475
- let out = formatInlineText(text);
476
- out = out.replace(/\\\|/g, "|");
477
- out = out.replace(/&lt;br&gt;/g, "<br>");
478
- return restoreInlineStash(out, stash);
479
- }
480
-
481
- function formatInlineText(text) {
482
- return escapeHtml(text)
483
- .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
484
- .replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, "$1<em>$2</em>")
485
- .replace(/(^|[^_])_([^_\s][^_]*?)_(?!_)/g, "$1<em>$2</em>");
486
- }
487
-
488
- function restoreInlineStash(text, stash) {
489
- return text.replace(/\u0000(\d+)\u0000/g, (_, i) => stash[Number(i)]);
490
- }
491
-
492
- function rewriteHref(href, currentRel) {
493
- if (/^(https?:|mailto:|tel:|#)/.test(href)) return href;
494
- const [raw, hash = ""] = href.split("#");
495
- if (!raw) return hash ? `#${hash}` : "";
496
- if (raw.startsWith("/")) {
497
- const target = permalinkMap.get(normalizePermalink(raw));
498
- if (target) {
499
- const currentOut = pageMap.get(currentRel)?.outRel || outPath(currentRel);
500
- const out = hrefToOutRel(target.outRel, currentOut);
501
- return hash ? `${out}#${hash}` : out;
502
- }
503
- return href;
504
- }
505
- if (!raw.endsWith(".md")) return href;
506
- const from = path.posix.dirname(currentRel);
507
- const target = path.posix.normalize(path.posix.join(from, raw));
508
- let rewritten = pageMap.get(target)?.outRel || outPath(target);
509
- const currentOut = pageMap.get(currentRel)?.outRel || outPath(currentRel);
510
- rewritten = hrefToOutRel(rewritten, currentOut);
511
- return `${rewritten}${hash ? `#${hash}` : ""}`;
512
- }
513
-
514
- function tocFromHtml(html) {
515
- const items = [];
516
- const re = /<h([23]) id="([^"]+)">([\s\S]*?)<\/h[23]>/g;
517
- let m;
518
- while ((m = re.exec(html))) {
519
- const text = m[3]
520
- .replace(/<a class="anchor"[^>]*>.*?<\/a>/, "")
521
- .replace(/<[^>]+>/g, "")
522
- .trim();
523
- items.push({ level: Number(m[1]), id: m[2], text });
524
- }
525
- if (items.length < 2) return "";
526
- return `<nav class="toc" aria-label="On this page"><h2>On this page</h2>${items
527
- .map(
528
- (i) =>
529
- `<a class="toc-l${i.level}" href="#${i.id}">${escapeHtml(i.text)}</a>`,
530
- )
531
- .join("")}</nav>`;
532
- }
533
-
534
- function isHomePage(page) {
535
- if (
536
- page.frontmatter.permalink &&
537
- normalizePermalink(page.frontmatter.permalink) === "/"
538
- )
539
- return true;
540
- return page.rel === "index.md" || page.rel === "README.md";
541
- }
542
-
543
- function homeHero(page) {
544
- const description = page.frontmatter.description || productDescription;
545
- const installRel = pageMap.get("install.md")?.outRel
546
- ? hrefToOutRel(pageMap.get("install.md").outRel, page.outRel)
547
- : "install.html";
548
- const quickstartRel = pageMap.get("quickstart.md")?.outRel
549
- ? hrefToOutRel(pageMap.get("quickstart.md").outRel, page.outRel)
550
- : "quickstart.html";
551
- const surfaces = [
552
- "Tweets",
553
- "DMs",
554
- "Likes",
555
- "Bookmarks",
556
- "Mentions",
557
- "Inbox",
558
- "Blocks",
559
- "Mutes",
560
- "Graph",
561
- ];
562
- return `<header class="home-hero">
563
- <img class="home-mark" src="birdclaw-mark.png" alt="" aria-hidden="true">
564
- <p class="eyebrow">Local-first · CLI + Web · SQLite</p>
565
- <h1>Your Twitter, <span class="accent">your bird</span>.</h1>
566
- <p class="lede">${escapeHtml(description)}</p>
567
- <div class="home-cta">
568
- <a class="btn btn-primary" href="${quickstartRel}">Quickstart</a>
569
- <a class="btn btn-ghost" href="${repoBase}" rel="noopener">GitHub</a>
570
- <div class="home-install" aria-label="Install with Homebrew">
571
- <span class="prompt" aria-hidden="true">$</span>
572
- <code>${escapeHtml(brewInstall)}</code>
573
- </div>
574
- </div>
575
- <div class="home-services" aria-label="Things birdclaw stores locally">
576
- ${surfaces.map((s) => `<span>${escapeHtml(s)}</span>`).join("")}
577
- </div>
578
- <p><a href="${installRel}">Other install options →</a></p>
579
- </header>`;
580
- }
581
-
582
- function standardHero(page, sectionName, editUrl) {
583
- return `<header class="hero">
584
- <div class="hero-text">
585
- <p class="eyebrow">${escapeHtml(sectionName)}</p>
586
- <h1>${escapeHtml(page.title)}</h1>
587
- </div>
588
- <div class="hero-meta">
589
- <a class="repo" href="${repoBase}" rel="noopener">GitHub</a>
590
- <a class="edit" href="${escapeAttr(editUrl)}" rel="noopener">Edit page</a>
591
- </div>
592
- </header>`;
593
- }
594
-
595
- function layout({ page, html, toc, prev, next, sectionName }) {
596
- const depth = page.outRel.split("/").length - 1;
597
- const rootPrefix = depth ? "../".repeat(depth) : "";
598
- const editUrl = `${repoEditBase}/${page.rel}`;
599
- const home = isHomePage(page);
600
- const prevNext =
601
- !home && (prev || next) ? pageNavHtml(prev, next, page.outRel) : "";
602
- const heroBlock = home
603
- ? homeHero(page)
604
- : standardHero(page, sectionName, editUrl);
605
- const articleClass = home ? "doc doc-home" : "doc";
606
- const tocBlock = home ? "" : toc;
607
- const titleSuffix = home
608
- ? `${productName} — ${productTagline}`
609
- : `${page.title} — ${productName}`;
610
- const description =
611
- page.frontmatter.description ||
612
- (home
613
- ? productDescription
614
- : `${page.title} — ${productName} CLI documentation.`);
615
- const canonicalUrl = pageCanonicalUrl(page);
616
- const socialImage = siteBase
617
- ? `${siteBase}/social-card.png`
618
- : `${rootPrefix}social-card.png`;
619
- const socialMeta = [
620
- ["link", "rel", "canonical", "href", canonicalUrl],
621
- ["meta", "property", "og:type", "content", "website"],
622
- ["meta", "property", "og:site_name", "content", productName],
623
- ["meta", "property", "og:title", "content", titleSuffix],
624
- ["meta", "property", "og:description", "content", description],
625
- ["meta", "property", "og:url", "content", canonicalUrl],
626
- ["meta", "property", "og:image", "content", socialImage],
627
- ["meta", "property", "og:image:width", "content", "1200"],
628
- ["meta", "property", "og:image:height", "content", "630"],
629
- ["meta", "name", "twitter:card", "content", "summary_large_image"],
630
- ["meta", "name", "twitter:title", "content", titleSuffix],
631
- ["meta", "name", "twitter:description", "content", description],
632
- ["meta", "name", "twitter:image", "content", socialImage],
633
- ]
634
- .map(tagHtml)
635
- .join("\n ");
636
- return `<!doctype html>
637
- <html lang="en">
638
- <head>
639
- <meta charset="utf-8">
640
- <meta name="viewport" content="width=device-width, initial-scale=1">
641
- <title>${escapeHtml(titleSuffix)}</title>
642
- <meta name="description" content="${escapeAttr(description)}">
643
- ${socialMeta}
644
- <link rel="icon" href="${rootPrefix}favicon.ico" sizes="any">
645
- <link rel="preconnect" href="https://fonts.googleapis.com">
646
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
647
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
648
- <script>${preThemeScript()}</script>
649
- <style>${css()}</style>
650
- </head>
651
- <body${home ? ' class="home"' : ""}>
652
- <button class="nav-toggle" type="button" aria-label="Toggle navigation" aria-expanded="false">
653
- <span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
654
- </button>
655
- <div class="shell">
656
- <aside class="sidebar">
657
- <div class="sidebar-head">
658
- <a class="brand" href="${hrefToOutRel("index.html", page.outRel)}" aria-label="${productName} docs home">
659
- <span class="mark" aria-hidden="true"><img src="${rootPrefix}birdclaw-mark.png" alt=""></span>
660
- <span><strong>${escapeHtml(productName)}</strong><small>Local Twitter memory</small></span>
661
- </a>
662
- ${themeToggleHtml()}
663
- </div>
664
- <label class="search"><span>Search</span><input id="doc-search" type="search" placeholder="archive, sync, dms"></label>
665
- <nav>${navHtml(page)}</nav>
666
- </aside>
667
- <main>
668
- ${heroBlock}
669
- <div class="doc-grid${home ? " doc-grid-home" : ""}">
670
- <article class="${articleClass}">${html}${prevNext}</article>
671
- ${tocBlock}
672
- </div>
673
- </main>
674
- </div>
675
- <script>${js()}</script>
676
- </body>
677
- </html>`;
678
- }
679
-
680
- function pageCanonicalUrl(page) {
681
- if (!siteBase) return page.outRel;
682
- if (page.outRel === "index.html") return `${siteBase}/`;
683
- const rel = page.outRel.endsWith("/index.html")
684
- ? page.outRel.slice(0, -"index.html".length)
685
- : page.outRel;
686
- return `${siteBase}/${rel}`;
687
- }
688
-
689
- function tagHtml([tag, k1, v1, k2, v2]) {
690
- return tag === "link"
691
- ? `<link ${k1}="${v1}" ${k2}="${escapeAttr(v2)}">`
692
- : `<meta ${k1}="${v1}" ${k2}="${escapeAttr(v2)}">`;
693
- }
694
-
695
- function pageNavHtml(prev, next, currentOutRel) {
696
- const cell = (page, dir) => {
697
- if (!page) return "";
698
- return `<a class="page-nav-${dir}" href="${hrefToOutRel(page.outRel, currentOutRel)}"><small>${dir === "prev" ? "Previous" : "Next"}</small><span>${escapeHtml(page.title)}</span></a>`;
699
- };
700
- return `<nav class="page-nav" aria-label="Pager">${cell(prev, "prev")}${cell(next, "next")}</nav>`;
701
- }
702
-
703
- function navHtml(currentPage) {
704
- return nav
705
- .map(
706
- (section) =>
707
- `<section><h2>${escapeHtml(section.name)}</h2>${section.pages
708
- .map((page) => {
709
- const href = hrefToOutRel(page.outRel, currentPage.outRel);
710
- const active = page.rel === currentPage.rel ? " active" : "";
711
- return `<a class="nav-link${active}" href="${href}">${escapeHtml(navTitle(page))}</a>`;
712
- })
713
- .join("")}</section>`,
714
- )
715
- .join("");
716
- }
717
-
718
- function navTitle(page) {
719
- if (page.rel === "index.md") return "Overview";
720
- return page.title.replace(/^`birdclaw\s*/, "").replace(/`$/, "");
721
- }
722
-
723
- function hrefToOutRel(targetOutRel, currentOutRel) {
724
- const currentDir = path.posix.dirname(currentOutRel);
725
- if (targetOutRel.endsWith("/index.html")) {
726
- const targetDir = targetOutRel.slice(0, -"index.html".length);
727
- const rel = path.posix.relative(currentDir, targetDir || ".") || ".";
728
- return rel.endsWith("/") ? rel : `${rel}/`;
729
- }
730
- if (targetOutRel === "index.html") {
731
- const rel = path.posix.relative(currentDir, ".") || ".";
732
- return rel.endsWith("/") ? rel : `${rel}/`;
733
- }
734
- return (
735
- path.posix.relative(currentDir, targetOutRel) ||
736
- path.posix.basename(targetOutRel)
737
- );
738
- }
739
-
740
- function slug(text) {
741
- return text
742
- .toLowerCase()
743
- .replace(/`/g, "")
744
- .replace(/[^a-z0-9]+/g, "-")
745
- .replace(/^-|-$/g, "");
746
- }
747
-
748
- function escapeHtml(value) {
749
- return String(value ?? "").replace(
750
- /[&<>"']/g,
751
- (char) =>
752
- ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
753
- char
754
- ],
755
- );
756
- }
757
-
758
- function escapeAttr(value) {
759
- return escapeHtml(value);
760
- }
761
-
762
- function highlightCode(code, lang) {
763
- const language = (lang || "text").toLowerCase();
764
- if (
765
- language === "bash" ||
766
- language === "sh" ||
767
- language === "shell" ||
768
- language === "zsh" ||
769
- language === "console"
770
- ) {
771
- return highlightShell(code);
772
- }
773
- if (language === "json" || language === "json5") return highlightJson(code);
774
- if (
775
- language === "ts" ||
776
- language === "typescript" ||
777
- language === "js" ||
778
- language === "javascript" ||
779
- language === "tsx" ||
780
- language === "jsx"
781
- ) {
782
- return highlightJs(code);
783
- }
784
- if (language === "yaml" || language === "yml") return highlightYaml(code);
785
- return escapeHtml(code);
786
- }
787
-
788
- function withStash(code, patterns) {
789
- const stash = [];
790
- let working = code;
791
- for (const [re, cls] of patterns) {
792
- working = working.replace(re, (match) => {
793
- const idx = stash.length;
794
- stash.push(`<span class="${cls}">${escapeHtml(match)}</span>`);
795
- return stashToken(idx);
796
- });
797
- }
798
- let escaped = escapeHtml(working);
799
- escaped = restoreStashTokens(escaped, stash);
800
- return escaped;
801
- }
802
-
803
- function highlightShell(code) {
804
- return code
805
- .split("\n")
806
- .map((line) => {
807
- const promptMatch = line.match(/^(\s*)([$#>])(\s+)(.*)$/);
808
- if (promptMatch) {
809
- const [, lead, sym, gap, rest] = promptMatch;
810
- return `${escapeHtml(lead)}<span class="hl-p">${escapeHtml(sym)}</span>${escapeHtml(gap)}${highlightShellLine(rest)}`;
811
- }
812
- if (/^\s*#/.test(line))
813
- return `<span class="hl-c">${escapeHtml(line)}</span>`;
814
- return highlightShellLine(line);
815
- })
816
- .join("\n");
817
- }
818
-
819
- function highlightShellLine(line) {
820
- const stash = [];
821
- let working = line;
822
- const stashAdd = (match, cls) => {
823
- const idx = stash.length;
824
- stash.push(`<span class="${cls}">${escapeHtml(match)}</span>`);
825
- return stashToken(idx);
826
- };
827
- working = working.replace(/(?:'[^']*'|"[^"]*")/g, (m) => stashAdd(m, "hl-s"));
828
- working = working.replace(/\s#.*$/g, (m) => stashAdd(m, "hl-c"));
829
- working = working.replace(
830
- /(^|\s)(--?[A-Za-z][A-Za-z0-9-]*)/g,
831
- (_, lead, flag) => `${escapeHtml(lead)}${stashAdd(flag, "hl-f")}`,
832
- );
833
- working = working.replace(
834
- /\b(birdclaw|pnpm|npm|brew|bird|xurl|git|node|tsx|sudo|launchctl|tail|jq|cd|export|cat|curl|cargo|ls|mv|cp|rm|mkdir|cron)\b/g,
835
- (m) => stashAdd(m, "hl-cmd"),
836
- );
837
- working = working.replace(/\b(\d+(?:\.\d+)?)\b/g, (m) => stashAdd(m, "hl-n"));
838
- let escaped = escapeHtml(working);
839
- return restoreStashTokens(escaped, stash);
840
- }
841
-
842
- function stashToken(idx) {
843
- return String.fromCharCode(0xe000 + idx);
844
- }
845
-
846
- function restoreStashTokens(value, stash) {
847
- return value.replace(/[\ue000-\uf8ff]/g, (token) => {
848
- const idx = token.charCodeAt(0) - 0xe000;
849
- return stash[idx] ?? "";
850
- });
851
- }
852
-
853
- function highlightJson(code) {
854
- return withStash(code, [
855
- [/"(?:\\.|[^"\\])*"\s*:/g, "hl-k"],
856
- [/"(?:\\.|[^"\\])*"/g, "hl-s"],
857
- [/\b(true|false|null)\b/g, "hl-m"],
858
- [/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/gi, "hl-n"],
859
- ]);
860
- }
861
-
862
- function highlightJs(code) {
863
- return withStash(code, [
864
- [/\/\/[^\n]*/g, "hl-c"],
865
- [/\/\*[\s\S]*?\*\//g, "hl-c"],
866
- [/`(?:\\.|[^`\\])*`/g, "hl-s"],
867
- [/"(?:\\.|[^"\\])*"/g, "hl-s"],
868
- [/'(?:\\.|[^'\\])*'/g, "hl-s"],
869
- [
870
- /\b(const|let|var|function|return|if|else|for|while|switch|case|break|continue|class|extends|new|import|from|export|default|async|await|try|catch|finally|throw|typeof|instanceof|interface|type|enum|as|of|in|null|undefined|true|false|this)\b/g,
871
- "hl-k",
872
- ],
873
- [/\b(\d+(?:\.\d+)?)\b/g, "hl-n"],
874
- ]);
875
- }
876
-
877
- function highlightYaml(code) {
878
- return code
879
- .split("\n")
880
- .map((line) => {
881
- if (/^\s*#/.test(line))
882
- return `<span class="hl-c">${escapeHtml(line)}</span>`;
883
- const m = line.match(/^(\s*-?\s*)([A-Za-z0-9_.-]+)(\s*:)(.*)$/);
884
- if (m) {
885
- const [, lead, key, colon, rest] = m;
886
- return `${escapeHtml(lead)}<span class="hl-k">${escapeHtml(key)}</span>${escapeHtml(colon)}${highlightYamlValue(rest)}`;
887
- }
888
- return escapeHtml(line);
889
- })
890
- .join("\n");
891
- }
892
-
893
- function highlightYamlValue(rest) {
894
- if (!rest.trim()) return escapeHtml(rest);
895
- const trimmed = rest.trim();
896
- if (/^["'].*["']$/.test(trimmed))
897
- return (
898
- escapeHtml(rest.replace(trimmed, "")) +
899
- `<span class="hl-s">${escapeHtml(trimmed)}</span>`
900
- );
901
- if (/^(true|false|null|~)$/i.test(trimmed))
902
- return (
903
- escapeHtml(rest.replace(trimmed, "")) +
904
- `<span class="hl-m">${escapeHtml(trimmed)}</span>`
905
- );
906
- if (/^-?\d+(\.\d+)?$/.test(trimmed))
907
- return (
908
- escapeHtml(rest.replace(trimmed, "")) +
909
- `<span class="hl-n">${escapeHtml(trimmed)}</span>`
910
- );
911
- return escapeHtml(rest);
912
- }
913
-
914
- export const __test__ = { inline };
915
-
916
- function validateLinks(outputDir) {
917
- const failures = [];
918
- const placeholderHrefs =
919
- /^(url|path|file|dir|name|host|port|kind|tweet-id|conversation-id|handle-or-id|account-id|n|seconds|date|cursor-or-id|query|text)$/i;
920
- for (const file of allHtml(outputDir)) {
921
- const html = fs.readFileSync(file, "utf8");
922
- for (const match of html.matchAll(/href="([^"]+)"/g)) {
923
- const href = match[1];
924
- if (/^(#|https?:|mailto:|tel:|javascript:)/.test(href)) continue;
925
- if (placeholderHrefs.test(href)) continue;
926
- const [rawPath, anchor = ""] = href.split("#");
927
- const targetPath = rawPath
928
- ? path.resolve(path.dirname(file), rawPath)
929
- : file;
930
- const target =
931
- fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()
932
- ? path.join(targetPath, "index.html")
933
- : targetPath;
934
- if (!fs.existsSync(target)) {
935
- failures.push(
936
- `${path.relative(outputDir, file)}: ${href} -> missing ${path.relative(outputDir, target)}`,
937
- );
938
- continue;
939
- }
940
- if (anchor) {
941
- const targetHtml = fs.readFileSync(target, "utf8");
942
- if (
943
- !targetHtml.includes(`id="${anchor}"`) &&
944
- !targetHtml.includes(`name="${anchor}"`)
945
- ) {
946
- failures.push(
947
- `${path.relative(outputDir, file)}: ${href} -> missing anchor`,
948
- );
949
- }
950
- }
951
- }
952
- }
953
- if (failures.length) {
954
- throw new Error(`broken docs links:\n${failures.join("\n")}`);
955
- }
956
- }
957
-
958
- function allHtml(dir) {
959
- return fs
960
- .readdirSync(dir, { withFileTypes: true })
961
- .flatMap((entry) => {
962
- const full = path.join(dir, entry.name);
963
- if (entry.isDirectory()) return allHtml(full);
964
- return entry.name.endsWith(".html") ? [full] : [];
965
- })
966
- .sort();
967
- }