birdclaw 0.8.3 → 0.8.5

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 (284) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +10 -8
  3. package/bin/birdclaw.mjs +4 -66
  4. package/dist/cli/birdclaw.js +28164 -0
  5. package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
  6. package/dist/client/assets/ProfilePreview-DQX6_xc3.js +1 -0
  7. package/dist/client/assets/SavedTimelineView-BhSiVyR1.js +1 -0
  8. package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
  9. package/dist/client/assets/TimelineRouteFrame-9NDLnXzH.js +1 -0
  10. package/dist/client/assets/blocks-CFp_njvG.js +1 -0
  11. package/dist/client/assets/bookmarks-LbYOsoKf.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-sR4xMQxY.js +5 -0
  15. package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
  16. package/dist/client/assets/data-sources-euTr6T_L.js +1 -0
  17. package/dist/client/assets/discuss-BWKu_p7B.js +1 -0
  18. package/dist/client/assets/dms-SLXMtC4v.js +1 -0
  19. package/dist/client/assets/inbox-BybSI5Vf.js +1 -0
  20. package/dist/client/assets/index-DZVisyna.js +31 -0
  21. package/dist/client/assets/index-bpFrQ5yG.css +1 -0
  22. package/dist/client/assets/likes-dyh9p8y_.js +1 -0
  23. package/dist/client/assets/links-CIwqK_dX.js +1 -0
  24. package/dist/client/assets/mapbox-B4zqX7r0.js +2 -0
  25. package/dist/client/assets/mapbox-gl-DYNcLcll.js +3215 -0
  26. package/dist/client/assets/mentions-BoVVAIT7.js +1 -0
  27. package/dist/client/assets/network-map-zRysIY_b.js +2 -0
  28. package/dist/client/assets/profile-analyze-C_yDeX6-.js +1 -0
  29. package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
  30. package/dist/client/assets/profiles._handle-DdIVPqU-.js +1 -0
  31. package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
  32. package/dist/client/assets/rate-limits-BWPmPOSh.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-CiLuYpZr.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-BoO64YoY.js +1 -0
  42. package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
  43. package/dist/client/assets/useTimelineRouteData-BaO3Rna1.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-L7bs6IHH.js +198 -0
  48. package/dist/server/assets/ProfilePreview-Cg-AOHBE.js +751 -0
  49. package/dist/server/assets/SavedTimelineView-BtCq1dHk.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-CjMIh63G.js +85 -0
  53. package/dist/server/assets/TweetRichText-DDywESRF.js +74 -0
  54. package/dist/server/assets/_tanstack-start-manifest_v-aLWUdbAc.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-D_YkmCK3.js +14 -0
  59. package/dist/server/assets/client-stream-contracts-Bq6kMmhh.js +769 -0
  60. package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
  61. package/dist/server/assets/discuss-C1QMBBSk.js +338 -0
  62. package/dist/server/assets/discuss-DAq6ehL9.js +15 -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-BMm2JWYI.js +14 -0
  69. package/dist/server/assets/links-Da4tjWtI.js +468 -0
  70. package/dist/server/assets/links-dqXb35YM.js +240 -0
  71. package/dist/server/assets/mentions-CEyEcOFw.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-CGCj4VFd.js +90 -0
  75. package/dist/server/assets/profile-analyze-DH1TSBkT.js +9 -0
  76. package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
  77. package/dist/server/assets/profiles._handle-B5V-huoo.js +12 -0
  78. package/dist/server/assets/profiles._handle-CJ6rgzt8.js +184 -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-C4cz9ypd.js +17026 -0
  83. package/dist/server/assets/routes-B9aikOdB.js +26 -0
  84. package/dist/server/assets/start-FaDOTmto.js +4 -0
  85. package/dist/server/assets/today-B_TA16QG.js +303 -0
  86. package/dist/server/assets/today-JkxZXdBt.js +17 -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-BLXvMGb2.js +1101 -0
  90. package/dist/server/server.js +5520 -0
  91. package/package.json +26 -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/command-context.ts +0 -17
  100. package/src/cli/register-analysis.ts +0 -500
  101. package/src/cli/register-compose.ts +0 -40
  102. package/src/cli/register-graph.ts +0 -132
  103. package/src/cli/register-inbox.ts +0 -41
  104. package/src/cli/register-storage.ts +0 -106
  105. package/src/cli-moderation.ts +0 -210
  106. package/src/cli.ts +0 -1616
  107. package/src/components/AccountSwitcher.tsx +0 -178
  108. package/src/components/AppNav.tsx +0 -118
  109. package/src/components/AvatarChip.tsx +0 -58
  110. package/src/components/AvatarPreload.ts +0 -149
  111. package/src/components/BrandMark.tsx +0 -67
  112. package/src/components/ConversationThread.tsx +0 -131
  113. package/src/components/DmWorkspace.tsx +0 -286
  114. package/src/components/EmbeddedTweetCard.tsx +0 -55
  115. package/src/components/FeedState.tsx +0 -147
  116. package/src/components/FloatingPreview.tsx +0 -382
  117. package/src/components/InboxCard.tsx +0 -152
  118. package/src/components/LinkPreviewCard.tsx +0 -292
  119. package/src/components/MarkdownCitations.tsx +0 -680
  120. package/src/components/MarkdownViewer.tsx +0 -111
  121. package/src/components/ProfileAnalysisClient.ts +0 -191
  122. package/src/components/ProfileAnalysisStream.tsx +0 -259
  123. package/src/components/ProfilePreview.tsx +0 -131
  124. package/src/components/SavedTimelineView.tsx +0 -155
  125. package/src/components/SmartTimestamp.tsx +0 -72
  126. package/src/components/SyncNowButton.tsx +0 -140
  127. package/src/components/ThemeSlider.tsx +0 -80
  128. package/src/components/TimelineCard.tsx +0 -533
  129. package/src/components/TimelineRouteFrame.tsx +0 -186
  130. package/src/components/TweetArticleCard.tsx +0 -66
  131. package/src/components/TweetMediaGrid.tsx +0 -185
  132. package/src/components/TweetRichText.tsx +0 -159
  133. package/src/components/account-selection.ts +0 -64
  134. package/src/components/links-controller.ts +0 -162
  135. package/src/components/links-model.ts +0 -198
  136. package/src/components/network-map-controller.ts +0 -84
  137. package/src/components/network-map-model.ts +0 -255
  138. package/src/components/useTimelineRouteData.ts +0 -193
  139. package/src/lib/account-sync-job.ts +0 -666
  140. package/src/lib/actions-transport.ts +0 -300
  141. package/src/lib/analysis-runtime.ts +0 -238
  142. package/src/lib/api-client.ts +0 -220
  143. package/src/lib/api-contracts.ts +0 -328
  144. package/src/lib/archive-finder.ts +0 -185
  145. package/src/lib/archive-import-plan.ts +0 -102
  146. package/src/lib/archive-import.ts +0 -2695
  147. package/src/lib/authored-live.ts +0 -1027
  148. package/src/lib/avatar-cache.ts +0 -349
  149. package/src/lib/backup.ts +0 -2652
  150. package/src/lib/bird-actions.ts +0 -294
  151. package/src/lib/bird-command.ts +0 -130
  152. package/src/lib/bird.ts +0 -1140
  153. package/src/lib/blocklist.ts +0 -136
  154. package/src/lib/blocks-write.ts +0 -171
  155. package/src/lib/blocks.ts +0 -387
  156. package/src/lib/bookmark-sync-job.ts +0 -536
  157. package/src/lib/config.ts +0 -224
  158. package/src/lib/conversation-surface.ts +0 -205
  159. package/src/lib/data-sources.ts +0 -219
  160. package/src/lib/database-metrics.ts +0 -88
  161. package/src/lib/database-migrations.ts +0 -34
  162. package/src/lib/database-schema.ts +0 -312
  163. package/src/lib/database-writer.ts +0 -69
  164. package/src/lib/db.ts +0 -889
  165. package/src/lib/dm-read-model.ts +0 -533
  166. package/src/lib/dms-live.ts +0 -870
  167. package/src/lib/effect-runtime.ts +0 -45
  168. package/src/lib/follow-graph.ts +0 -1087
  169. package/src/lib/geocoding.ts +0 -296
  170. package/src/lib/http-effect.ts +0 -222
  171. package/src/lib/identity-search-index.ts +0 -369
  172. package/src/lib/import-repository.ts +0 -138
  173. package/src/lib/inbox.ts +0 -242
  174. package/src/lib/link-index.ts +0 -641
  175. package/src/lib/link-insights.ts +0 -860
  176. package/src/lib/link-preview-metadata.ts +0 -754
  177. package/src/lib/live-sync-engine.ts +0 -209
  178. package/src/lib/live-transport-gateway.ts +0 -128
  179. package/src/lib/location.ts +0 -137
  180. package/src/lib/media-fetch.ts +0 -860
  181. package/src/lib/media-includes.ts +0 -165
  182. package/src/lib/mention-threads-live.ts +0 -830
  183. package/src/lib/mentions-export.ts +0 -147
  184. package/src/lib/mentions-live.ts +0 -1114
  185. package/src/lib/moderation-target.ts +0 -227
  186. package/src/lib/moderation-write.ts +0 -131
  187. package/src/lib/mutes-write.ts +0 -171
  188. package/src/lib/mutes.ts +0 -84
  189. package/src/lib/ndjson-stream.ts +0 -106
  190. package/src/lib/network-map.ts +0 -382
  191. package/src/lib/openai-response-runtime.ts +0 -251
  192. package/src/lib/openai.ts +0 -117
  193. package/src/lib/paginated-sync.ts +0 -93
  194. package/src/lib/period-digest.ts +0 -1364
  195. package/src/lib/present.ts +0 -118
  196. package/src/lib/profile-affiliation-hydration.ts +0 -228
  197. package/src/lib/profile-affiliations.ts +0 -262
  198. package/src/lib/profile-analysis.ts +0 -1198
  199. package/src/lib/profile-bio-entities.ts +0 -318
  200. package/src/lib/profile-history.ts +0 -164
  201. package/src/lib/profile-hydration.ts +0 -180
  202. package/src/lib/profile-replies.ts +0 -77
  203. package/src/lib/profile-resolver.ts +0 -650
  204. package/src/lib/queries.ts +0 -6
  205. package/src/lib/query-actions.ts +0 -437
  206. package/src/lib/query-client.tsx +0 -47
  207. package/src/lib/query-read-model-shared.ts +0 -52
  208. package/src/lib/query-read-models.ts +0 -5
  209. package/src/lib/query-resource.ts +0 -41
  210. package/src/lib/query-status.ts +0 -164
  211. package/src/lib/research.ts +0 -652
  212. package/src/lib/runtime-services.ts +0 -20
  213. package/src/lib/search-discussion.ts +0 -812
  214. package/src/lib/seed.ts +0 -613
  215. package/src/lib/server-runtime-services.ts +0 -30
  216. package/src/lib/sqlite.ts +0 -183
  217. package/src/lib/streaming-ingestion.ts +0 -240
  218. package/src/lib/sync-cache.ts +0 -70
  219. package/src/lib/sync-plan.ts +0 -175
  220. package/src/lib/theme-transition.ts +0 -113
  221. package/src/lib/theme.tsx +0 -165
  222. package/src/lib/timeline-collections-live.ts +0 -407
  223. package/src/lib/timeline-live.ts +0 -302
  224. package/src/lib/timeline-read-model.ts +0 -1191
  225. package/src/lib/tweet-account-edges.ts +0 -48
  226. package/src/lib/tweet-lookup.ts +0 -46
  227. package/src/lib/tweet-render.ts +0 -430
  228. package/src/lib/tweet-repository.ts +0 -156
  229. package/src/lib/tweet-search-live.ts +0 -462
  230. package/src/lib/types.ts +0 -723
  231. package/src/lib/ui.ts +0 -529
  232. package/src/lib/url-expansion-store.ts +0 -110
  233. package/src/lib/url-expansion.ts +0 -343
  234. package/src/lib/url-safety.ts +0 -220
  235. package/src/lib/web-sync.ts +0 -533
  236. package/src/lib/whois.ts +0 -1080
  237. package/src/lib/x-profile.ts +0 -365
  238. package/src/lib/x-web.ts +0 -199
  239. package/src/lib/xurl-rate-limits.ts +0 -267
  240. package/src/lib/xurl.ts +0 -1677
  241. package/src/routeTree.gen.ts +0 -723
  242. package/src/router.tsx +0 -20
  243. package/src/routes/__root.tsx +0 -82
  244. package/src/routes/api/action.tsx +0 -147
  245. package/src/routes/api/avatar.tsx +0 -50
  246. package/src/routes/api/blocks.tsx +0 -35
  247. package/src/routes/api/conversation.tsx +0 -45
  248. package/src/routes/api/data-sources.tsx +0 -24
  249. package/src/routes/api/inbox.tsx +0 -43
  250. package/src/routes/api/link-insights.tsx +0 -85
  251. package/src/routes/api/link-preview.tsx +0 -47
  252. package/src/routes/api/network-map.tsx +0 -55
  253. package/src/routes/api/period-digest.tsx +0 -101
  254. package/src/routes/api/profile-analysis.tsx +0 -97
  255. package/src/routes/api/profile-hydrate.tsx +0 -54
  256. package/src/routes/api/query.tsx +0 -121
  257. package/src/routes/api/search-discussion.tsx +0 -113
  258. package/src/routes/api/status.tsx +0 -30
  259. package/src/routes/api/sync.tsx +0 -108
  260. package/src/routes/api/xurl-rate-limits.tsx +0 -24
  261. package/src/routes/blocks.tsx +0 -343
  262. package/src/routes/bookmarks.tsx +0 -18
  263. package/src/routes/data-sources.tsx +0 -249
  264. package/src/routes/discuss.tsx +0 -419
  265. package/src/routes/dms.tsx +0 -415
  266. package/src/routes/inbox.tsx +0 -232
  267. package/src/routes/index.tsx +0 -32
  268. package/src/routes/likes.tsx +0 -18
  269. package/src/routes/links.tsx +0 -631
  270. package/src/routes/mentions.tsx +0 -32
  271. package/src/routes/network-map.tsx +0 -732
  272. package/src/routes/profile-analyze.tsx +0 -112
  273. package/src/routes/profiles.$handle.tsx +0 -228
  274. package/src/routes/rate-limits.tsx +0 -305
  275. package/src/routes/today.tsx +0 -511
  276. package/src/styles.css +0 -257
  277. package/tsconfig.json +0 -29
  278. package/vite.config.ts +0 -29
  279. /package/{public → dist/client}/birdclaw-mark.png +0 -0
  280. /package/{public → dist/client}/favicon.ico +0 -0
  281. /package/{public → dist/client}/logo192.png +0 -0
  282. /package/{public → dist/client}/logo512.png +0 -0
  283. /package/{public → dist/client}/manifest.json +0 -0
  284. /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
- }