@coywolf/emdash 0.0.0-stage → 0.35.14

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 (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -2
  3. package/docs/README.md +45 -0
  4. package/docs/ai.md +39 -0
  5. package/docs/backups.md +88 -0
  6. package/docs/breadcrumbs.md +59 -0
  7. package/docs/code-blocks.md +21 -0
  8. package/docs/content-urls.md +41 -0
  9. package/docs/custom-blocks.md +27 -0
  10. package/docs/discovery.md +32 -0
  11. package/docs/files.md +44 -0
  12. package/docs/form-uploads.md +59 -0
  13. package/docs/headings.md +37 -0
  14. package/docs/images.md +141 -0
  15. package/docs/internals.md +13 -0
  16. package/docs/links.md +34 -0
  17. package/docs/performance.md +72 -0
  18. package/docs/redirects.md +84 -0
  19. package/docs/releasing.md +21 -0
  20. package/docs/reviews.md +89 -0
  21. package/docs/robots.md +81 -0
  22. package/docs/schema.md +79 -0
  23. package/docs/search.md +114 -0
  24. package/docs/videos.md +48 -0
  25. package/docs/wordpress-import.md +198 -0
  26. package/package.json +79 -5
  27. package/src/admin/admin-css.ts +57 -0
  28. package/src/admin/ai.tsx +915 -0
  29. package/src/admin/backups.tsx +613 -0
  30. package/src/admin/breadcrumbs.tsx +542 -0
  31. package/src/admin/codeBlocks.tsx +204 -0
  32. package/src/admin/customBlocks.tsx +321 -0
  33. package/src/admin/dirty.ts +20 -0
  34. package/src/admin/discovery.tsx +463 -0
  35. package/src/admin/download.ts +28 -0
  36. package/src/admin/feature-switches.tsx +124 -0
  37. package/src/admin/features.tsx +142 -0
  38. package/src/admin/files.tsx +791 -0
  39. package/src/admin/formUploads.tsx +445 -0
  40. package/src/admin/guides.tsx +18 -0
  41. package/src/admin/headings.tsx +262 -0
  42. package/src/admin/images.tsx +825 -0
  43. package/src/admin/index.tsx +62 -0
  44. package/src/admin/links.tsx +980 -0
  45. package/src/admin/performance.tsx +330 -0
  46. package/src/admin/preview.tsx +63 -0
  47. package/src/admin/redirects-removed.tsx +266 -0
  48. package/src/admin/redirects.tsx +470 -0
  49. package/src/admin/reviews.tsx +232 -0
  50. package/src/admin/robots-bots.tsx +647 -0
  51. package/src/admin/robots-shared.tsx +109 -0
  52. package/src/admin/robots-wizard.tsx +748 -0
  53. package/src/admin/robots.tsx +911 -0
  54. package/src/admin/save-bar.tsx +134 -0
  55. package/src/admin/schema.tsx +1187 -0
  56. package/src/admin/search.tsx +355 -0
  57. package/src/admin/settings-ui.tsx +87 -0
  58. package/src/admin/trash-prompt-core.ts +135 -0
  59. package/src/admin/trash-prompt.tsx +156 -0
  60. package/src/admin/usage.tsx +301 -0
  61. package/src/admin/videos-display.tsx +248 -0
  62. package/src/admin/videos.tsx +1202 -0
  63. package/src/admin/wpImport.tsx +1516 -0
  64. package/src/ai/entities.ts +59 -0
  65. package/src/ai/export.ts +61 -0
  66. package/src/ai/logic.ts +606 -0
  67. package/src/ai/pack.ts +121 -0
  68. package/src/ai/prompts.ts +87 -0
  69. package/src/ai/providers.ts +276 -0
  70. package/src/ai/routes.ts +359 -0
  71. package/src/ai/store.ts +301 -0
  72. package/src/ai/wikidata.ts +87 -0
  73. package/src/ai/worker.ts +530 -0
  74. package/src/astro/astro-modules.d.ts +6 -0
  75. package/src/astro/breadcrumbs/Breadcrumbs.astro +129 -0
  76. package/src/astro/breadcrumbs/BreadcrumbsBlock.astro +12 -0
  77. package/src/astro/codeBlocks/CodeBlock.astro +62 -0
  78. package/src/astro/codeBlocks/EmDashCode.astro +61 -0
  79. package/src/astro/customBlocks/CustomBlock.astro +64 -0
  80. package/src/astro/customBlocks/Details.astro +8 -0
  81. package/src/astro/customBlocks/Disclosure.astro +8 -0
  82. package/src/astro/customBlocks/Note.astro +8 -0
  83. package/src/astro/customBlocks/Podcast.astro +8 -0
  84. package/src/astro/customBlocks/Quote.astro +8 -0
  85. package/src/astro/customBlocks/Testimonial.astro +8 -0
  86. package/src/astro/files/FileCard.astro +103 -0
  87. package/src/astro/headings/HeadingBlock.astro +129 -0
  88. package/src/astro/headings/TableOfContents.astro +45 -0
  89. package/src/astro/headings/request.ts +19 -0
  90. package/src/astro/images/ResponsiveImage.astro +60 -0
  91. package/src/astro/index.ts +84 -0
  92. package/src/astro/reviews/CoywolfReview.astro +27 -0
  93. package/src/astro/reviews/Review.astro +49 -0
  94. package/src/astro/search/SearchBox.astro +61 -0
  95. package/src/astro/search/SearchBoxForm.astro +509 -0
  96. package/src/astro/tables/CaptionedTable.astro +72 -0
  97. package/src/astro/videos/CoywolfVideo.astro +448 -0
  98. package/src/backups/download.ts +112 -0
  99. package/src/backups/dump.ts +288 -0
  100. package/src/backups/module.ts +276 -0
  101. package/src/backups/pack.ts +33 -0
  102. package/src/backups/restore.ts +209 -0
  103. package/src/backups/store.ts +409 -0
  104. package/src/breadcrumbs/pack.ts +113 -0
  105. package/src/breadcrumbs/settings.ts +102 -0
  106. package/src/breadcrumbs/stamp.ts +40 -0
  107. package/src/breadcrumbs/trail.ts +123 -0
  108. package/src/codeBlocks/contrast.ts +179 -0
  109. package/src/codeBlocks/hljs.d.ts +5 -0
  110. package/src/codeBlocks/pack.ts +110 -0
  111. package/src/codeBlocks/render.ts +304 -0
  112. package/src/codeBlocks/settings.ts +18 -0
  113. package/src/codeBlocks/themes.generated.ts +304 -0
  114. package/src/codeBlocks/themes.ts +114 -0
  115. package/src/core/compose.ts +244 -0
  116. package/src/core/content-url.ts +888 -0
  117. package/src/core/csv.ts +119 -0
  118. package/src/core/d1-batch.ts +65 -0
  119. package/src/core/entry-docs.ts +175 -0
  120. package/src/core/features-module.ts +48 -0
  121. package/src/core/features.ts +392 -0
  122. package/src/core/generation.ts +88 -0
  123. package/src/core/guide-content.ts +166 -0
  124. package/src/core/module.ts +104 -0
  125. package/src/core/read-failures.ts +57 -0
  126. package/src/core/request-memo.ts +71 -0
  127. package/src/core/secrets.ts +83 -0
  128. package/src/core/settings-enhance.ts +74 -0
  129. package/src/core/site.ts +36 -0
  130. package/src/core/storage.ts +27 -0
  131. package/src/customBlocks/pack.ts +260 -0
  132. package/src/customBlocks/render.ts +424 -0
  133. package/src/customBlocks/rich.ts +313 -0
  134. package/src/customBlocks/settings.ts +46 -0
  135. package/src/discovery/build.ts +252 -0
  136. package/src/discovery/feed-build.ts +217 -0
  137. package/src/discovery/feed.ts +172 -0
  138. package/src/discovery/indexnow.ts +114 -0
  139. package/src/discovery/llms.ts +79 -0
  140. package/src/discovery/markdown.ts +294 -0
  141. package/src/discovery/memory-cache.ts +97 -0
  142. package/src/discovery/middleware.ts +186 -0
  143. package/src/discovery/module.ts +353 -0
  144. package/src/discovery/news.ts +99 -0
  145. package/src/discovery/pack.ts +58 -0
  146. package/src/discovery/settings.ts +91 -0
  147. package/src/discovery/urls.ts +35 -0
  148. package/src/env.d.ts +10 -0
  149. package/src/files/card.ts +231 -0
  150. package/src/files/counts.ts +60 -0
  151. package/src/files/export.ts +44 -0
  152. package/src/files/format.ts +163 -0
  153. package/src/files/module.ts +596 -0
  154. package/src/files/pack.ts +92 -0
  155. package/src/files/r2-rules.ts +41 -0
  156. package/src/files/range.ts +50 -0
  157. package/src/files/s3.ts +114 -0
  158. package/src/files/serve.ts +141 -0
  159. package/src/files/sigv4.ts +160 -0
  160. package/src/files/site.ts +137 -0
  161. package/src/files/walker.ts +58 -0
  162. package/src/formUploads/lib.ts +266 -0
  163. package/src/formUploads/pack.ts +55 -0
  164. package/src/formUploads/wrap.ts +592 -0
  165. package/src/forms.ts +16 -0
  166. package/src/headings/pack.ts +154 -0
  167. package/src/headings/render.ts +139 -0
  168. package/src/headings/settings.ts +117 -0
  169. package/src/headings/slug.ts +68 -0
  170. package/src/headings/stamp.ts +142 -0
  171. package/src/headings/test-hooks.mjs +14 -0
  172. package/src/headings/toc.ts +48 -0
  173. package/src/images/cloudflare.ts +329 -0
  174. package/src/images/lib.ts +221 -0
  175. package/src/images/module.ts +372 -0
  176. package/src/images/pack.ts +804 -0
  177. package/src/images/pending.ts +37 -0
  178. package/src/images/settings.ts +48 -0
  179. package/src/images/setup.ts +192 -0
  180. package/src/images/variants-estimate.ts +162 -0
  181. package/src/images/variants-store.ts +475 -0
  182. package/src/images/variants.ts +311 -0
  183. package/src/index.ts +143 -0
  184. package/src/links/check.ts +101 -0
  185. package/src/links/classify.ts +270 -0
  186. package/src/links/export.ts +59 -0
  187. package/src/links/module.ts +652 -0
  188. package/src/links/pack.ts +43 -0
  189. package/src/links/pt.ts +325 -0
  190. package/src/links/store.ts +704 -0
  191. package/src/middleware.ts +287 -0
  192. package/src/modules.ts +53 -0
  193. package/src/options.ts +66 -0
  194. package/src/pageCache/lib.ts +274 -0
  195. package/src/pageCache/pack.ts +240 -0
  196. package/src/pageCache/usage-core.ts +152 -0
  197. package/src/pageCache/usage.ts +262 -0
  198. package/src/pageCache/warm.ts +722 -0
  199. package/src/redirects/middleware.ts +169 -0
  200. package/src/redirects/module.ts +124 -0
  201. package/src/redirects/pack.ts +89 -0
  202. package/src/redirects/removed-core.ts +151 -0
  203. package/src/redirects/removed.ts +293 -0
  204. package/src/redirects/rules.ts +355 -0
  205. package/src/redirects/slug-changes.ts +207 -0
  206. package/src/redirects/slug-core.ts +49 -0
  207. package/src/redirects/transfer.ts +92 -0
  208. package/src/reviews/lib.ts +624 -0
  209. package/src/reviews/pack.ts +135 -0
  210. package/src/reviews/schema.ts +128 -0
  211. package/src/reviews/settings.ts +26 -0
  212. package/src/robots/bots.ts +321 -0
  213. package/src/robots/data/bots.json +696 -0
  214. package/src/robots/data/verified.json +582 -0
  215. package/src/robots/directory.ts +26 -0
  216. package/src/robots/explain.ts +191 -0
  217. package/src/robots/guided.ts +300 -0
  218. package/src/robots/importer.ts +263 -0
  219. package/src/robots/middleware.ts +151 -0
  220. package/src/robots/module.ts +446 -0
  221. package/src/robots/pack.ts +69 -0
  222. package/src/robots/radar.ts +129 -0
  223. package/src/robots/rep.ts +362 -0
  224. package/src/robots/rules.ts +883 -0
  225. package/src/robots/transfer.ts +45 -0
  226. package/src/robots/validate.ts +524 -0
  227. package/src/schema/catalog.ts +146 -0
  228. package/src/schema/graph.ts +791 -0
  229. package/src/schema/module.ts +1138 -0
  230. package/src/schema/og-image.ts +100 -0
  231. package/src/schema/pack.ts +58 -0
  232. package/src/search/engine.ts +578 -0
  233. package/src/search/fallback.ts +80 -0
  234. package/src/search/live-cache.ts +116 -0
  235. package/src/search/live-client.ts +770 -0
  236. package/src/search/live-serve.ts +282 -0
  237. package/src/search/module.ts +261 -0
  238. package/src/search/pack.ts +57 -0
  239. package/src/search/query.ts +132 -0
  240. package/src/search/ratelimit-core.ts +118 -0
  241. package/src/search/ratelimit.ts +79 -0
  242. package/src/search/snippet.ts +135 -0
  243. package/src/search/title-index.ts +139 -0
  244. package/src/search/title-match.ts +192 -0
  245. package/src/shared.ts +71 -0
  246. package/src/version.ts +2 -0
  247. package/src/videos/date.ts +40 -0
  248. package/src/videos/engagement.ts +75 -0
  249. package/src/videos/lib.ts +679 -0
  250. package/src/videos/module.ts +1160 -0
  251. package/src/videos/pack.ts +187 -0
  252. package/src/videos/pause.ts +42 -0
  253. package/src/videos/poster.ts +324 -0
  254. package/src/videos/render.ts +336 -0
  255. package/src/videos/store.ts +551 -0
  256. package/src/videos/stream.ts +134 -0
  257. package/src/wpImport/convert.ts +702 -0
  258. package/src/wpImport/guests.ts +329 -0
  259. package/src/wpImport/gutenberg.ts +157 -0
  260. package/src/wpImport/markers.ts +66 -0
  261. package/src/wpImport/pack.ts +451 -0
  262. package/src/wpImport/parents.ts +202 -0
  263. package/src/wpImport/prepare.ts +592 -0
  264. package/src/wpImport/redirects.ts +358 -0
  265. package/src/wpImport/sources.ts +67 -0
  266. package/src/wpImport/stream.ts +160 -0
  267. package/src/wpImport/urls.ts +354 -0
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Server-side code block rendering: lowlight (highlight.js grammars, no
3
+ * Shiki) produces a hast tree, which is turned into HTML here by hand so the
4
+ * output is limited to escaped text and <span class="hljs-…"> elements.
5
+ *
6
+ * Kept free of relative imports so `node --test` can load it directly.
7
+ */
8
+ import type { Element, Root, RootContent } from "hast";
9
+ import dockerfile from "highlight.js/lib/languages/dockerfile";
10
+ import { common, createLowlight } from "lowlight";
11
+
12
+ // ── Languages ────────────────────────────────────────────────────
13
+
14
+ /** EmDash's editor language list (ids + labels), plus the grammar each one maps to when its id isn't one. */
15
+ const LANGUAGES: Record<string, { label: string; grammar?: string | null; aliases?: string[] }> = {
16
+ plaintext: { label: "Plain text", aliases: ["text", "plain", "txt"] },
17
+ astro: { label: "Astro", grammar: "xml" },
18
+ bash: { label: "Bash", aliases: ["sh", "shell", "zsh"] },
19
+ c: { label: "C" },
20
+ cpp: { label: "C++", aliases: ["c++"] },
21
+ csharp: { label: "C#", aliases: ["cs", "c#"] },
22
+ css: { label: "CSS" },
23
+ diff: { label: "Diff", aliases: ["patch"] },
24
+ dockerfile: { label: "Dockerfile", aliases: ["docker"] },
25
+ go: { label: "Go", aliases: ["golang"] },
26
+ graphql: { label: "GraphQL", aliases: ["gql"] },
27
+ // "markup" is Prism's name for HTML (Code Block Enhancer for WordPress stored it).
28
+ html: { label: "HTML", grammar: "xml", aliases: ["markup"] },
29
+ java: { label: "Java" },
30
+ javascript: { label: "JavaScript", aliases: ["js"] },
31
+ json: { label: "JSON" },
32
+ jsx: { label: "JSX", grammar: "javascript" },
33
+ kotlin: { label: "Kotlin", aliases: ["kt"] },
34
+ lua: { label: "Lua" },
35
+ markdown: { label: "Markdown", aliases: ["md"] },
36
+ mdx: { label: "MDX", grammar: "markdown" },
37
+ php: { label: "PHP" },
38
+ python: { label: "Python", aliases: ["py"] },
39
+ ruby: { label: "Ruby", aliases: ["rb"] },
40
+ rust: { label: "Rust", aliases: ["rs"] },
41
+ scss: { label: "SCSS", aliases: ["sass"] },
42
+ sql: { label: "SQL" },
43
+ svelte: { label: "Svelte", grammar: "xml" },
44
+ swift: { label: "Swift" },
45
+ toml: { label: "TOML", grammar: "ini" },
46
+ tsx: { label: "TSX", grammar: "typescript" },
47
+ typescript: { label: "TypeScript", aliases: ["ts"] },
48
+ vue: { label: "Vue", grammar: "xml" },
49
+ xml: { label: "XML", aliases: ["svg", "mathml"] },
50
+ yaml: { label: "YAML", aliases: ["yml"] },
51
+ zig: { label: "Zig", grammar: null },
52
+ // Common highlight.js grammars the editor doesn't list but content may use.
53
+ ini: { label: "INI" },
54
+ less: { label: "Less" },
55
+ makefile: { label: "Makefile" },
56
+ objectivec: { label: "Objective-C" },
57
+ perl: { label: "Perl" },
58
+ r: { label: "R" },
59
+ shell: { label: "Shell session" },
60
+ wasm: { label: "WebAssembly" },
61
+ };
62
+
63
+ const ALIASES = new Map<string, string>();
64
+ for (const [id, lang] of Object.entries(LANGUAGES)) {
65
+ ALIASES.set(id, id);
66
+ for (const alias of lang.aliases ?? []) ALIASES.set(alias, id);
67
+ }
68
+
69
+ let lowlight: ReturnType<typeof createLowlight> | null = null;
70
+ function engine() {
71
+ if (!lowlight) {
72
+ lowlight = createLowlight(common);
73
+ lowlight.register({ dockerfile });
74
+ }
75
+ return lowlight;
76
+ }
77
+
78
+ export interface ResolvedLanguage {
79
+ /** Sanitized id for the `language-…` class (EmDash's convention), or null. */
80
+ id: string | null;
81
+ /** Display label, or null for none / plain text. */
82
+ label: string | null;
83
+ /** lowlight grammar to use, or null for plain text. */
84
+ grammar: string | null;
85
+ }
86
+
87
+ export function resolveLanguage(raw: string | null | undefined): ResolvedLanguage {
88
+ const needle = typeof raw === "string" ? raw.trim().toLowerCase() : "";
89
+ if (!needle) return { id: null, label: null, grammar: null };
90
+ const known = ALIASES.get(needle);
91
+ if (known) {
92
+ const lang = LANGUAGES[known]!;
93
+ const grammar = known === "plaintext" ? null : lang.grammar === undefined ? known : lang.grammar;
94
+ return {
95
+ id: known,
96
+ label: known === "plaintext" ? null : lang.label,
97
+ grammar: grammar && engine().registered(grammar) ? grammar : null,
98
+ };
99
+ }
100
+ const id = needle.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40).replace(/-+$/, "") || null;
101
+ // Unknown to the editor but maybe a highlight.js alias (e.g. "ps1"); otherwise plain text.
102
+ const grammar = id && engine().registered(id) ? id : null;
103
+ return { id, label: id ? (raw as string).trim().slice(0, 40) : null, grammar };
104
+ }
105
+
106
+ // ── hast → HTML ──────────────────────────────────────────────────
107
+
108
+ /**
109
+ * Skip highlighting for large blocks and long lines: highlight.js grammars
110
+ * (JS/TS/CSS especially) are superlinear on long lines, which would burn
111
+ * Workers CPU time (a 40k-character line takes seconds).
112
+ */
113
+ export const MAX_HIGHLIGHT_CHARS = 30_000;
114
+ export const MAX_HIGHLIGHT_LINE = 2_000;
115
+
116
+ function tooCostly(code: string): boolean {
117
+ if (code.length > MAX_HIGHLIGHT_CHARS) return true;
118
+ let start = 0;
119
+ while (start <= code.length) {
120
+ const end = code.indexOf("\n", start);
121
+ const stop = end === -1 ? code.length : end;
122
+ if (stop - start > MAX_HIGHLIGHT_LINE) return true;
123
+ if (end === -1) break;
124
+ start = end + 1;
125
+ }
126
+ return false;
127
+ }
128
+
129
+ const CLASS_OK = /^(?:hljs-[a-z0-9_-]+|[a-z][a-z0-9]*_+)$/i;
130
+
131
+ export function escapeHtml(text: string): string {
132
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
133
+ }
134
+
135
+ interface Segment {
136
+ /** One entry per open span, each a space-joined class list. */
137
+ stack: string[];
138
+ text: string;
139
+ }
140
+
141
+ function classesOf(node: Element): string {
142
+ const raw: unknown = node.properties?.className;
143
+ const list: unknown[] = Array.isArray(raw) ? raw : typeof raw === "string" ? raw.split(/\s+/) : [];
144
+ return list.map(String).filter((c) => CLASS_OK.test(c)).join(" ");
145
+ }
146
+
147
+ function flatten(nodes: RootContent[], stack: string[], out: Segment[]): void {
148
+ for (const node of nodes) {
149
+ if (node.type === "text") {
150
+ if (node.value) out.push({ stack, text: node.value });
151
+ } else if (node.type === "element") {
152
+ const cls = classesOf(node);
153
+ flatten(node.children as RootContent[], cls ? [...stack, cls] : stack, out);
154
+ }
155
+ // Comments, doctypes, raw: never emitted.
156
+ }
157
+ }
158
+
159
+ /** Split segments into lines; a span crossing a newline is closed and reopened on the next line. */
160
+ function toLines(segments: Segment[]): Segment[][] {
161
+ const lines: Segment[][] = [[]];
162
+ for (const seg of segments) {
163
+ const parts = seg.text.split("\n");
164
+ parts.forEach((part, i) => {
165
+ if (i > 0) lines.push([]);
166
+ if (part) lines[lines.length - 1]!.push({ stack: seg.stack, text: part });
167
+ });
168
+ }
169
+ return lines;
170
+ }
171
+
172
+ function renderLine(line: Segment[]): string {
173
+ let html = "";
174
+ let open: string[] = [];
175
+ for (const seg of line) {
176
+ let common = 0;
177
+ while (common < open.length && common < seg.stack.length && open[common] === seg.stack[common]) common++;
178
+ html += "</span>".repeat(open.length - common);
179
+ for (const cls of seg.stack.slice(common)) html += `<span class="${escapeHtml(cls)}">`;
180
+ open = seg.stack;
181
+ html += escapeHtml(seg.text);
182
+ }
183
+ return html + "</span>".repeat(open.length);
184
+ }
185
+
186
+ /** Render a lowlight tree to HTML lines (no trailing empty line). */
187
+ export function hastToLines(tree: Root): string[] {
188
+ const segments: Segment[] = [];
189
+ flatten(tree.children, [], segments);
190
+ const lines = toLines(segments).map(renderLine);
191
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
192
+ return lines;
193
+ }
194
+
195
+ export function highlightLines(code: string, grammar: string | null): string[] {
196
+ const tree: Root =
197
+ grammar && !tooCostly(code)
198
+ ? engine().highlight(grammar, code)
199
+ : { type: "root", children: [{ type: "text", value: code }] };
200
+ return hastToLines(tree);
201
+ }
202
+
203
+ // ── Block markup ─────────────────────────────────────────────────
204
+
205
+ export interface BlockOptions {
206
+ label: boolean;
207
+ copy: boolean;
208
+ lineNumbers: boolean;
209
+ }
210
+
211
+ export interface CodeNode {
212
+ code: string;
213
+ language?: string;
214
+ filename?: string;
215
+ }
216
+
217
+ const COPY_ICON =
218
+ '<svg class="cw-code-i-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
219
+ const CHECK_ICON =
220
+ '<svg class="cw-code-i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polyline points="20 6 9 17 4 12"/></svg>';
221
+
222
+ /** Rendered blocks, per isolate, so page views don't re-highlight. */
223
+ const CACHE_MAX = 200;
224
+ const cache = new Map<string, string>();
225
+
226
+ /** cyrb53: fast 53-bit string hash. */
227
+ function hash(text: string): string {
228
+ let h1 = 0xdeadbeef;
229
+ let h2 = 0x41c6ce57;
230
+ for (let i = 0; i < text.length; i++) {
231
+ const ch = text.charCodeAt(i);
232
+ h1 = Math.imul(h1 ^ ch, 2654435761);
233
+ h2 = Math.imul(h2 ^ ch, 1597334677);
234
+ }
235
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
236
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
237
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36);
238
+ }
239
+
240
+ export function renderBlock(node: CodeNode, options: BlockOptions): string {
241
+ const code = String(node.code ?? "");
242
+ const key = [
243
+ Number(options.label),
244
+ Number(options.copy),
245
+ Number(options.lineNumbers),
246
+ typeof node.language === "string" ? node.language : "",
247
+ code.length,
248
+ hash(code),
249
+ ].join("\u0000");
250
+ const hit = cache.get(key);
251
+ if (hit !== undefined) {
252
+ cache.delete(key);
253
+ cache.set(key, hit);
254
+ return hit;
255
+ }
256
+ const html = renderUncached({ ...node, code }, options);
257
+ cache.set(key, html);
258
+ if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value as string);
259
+ return html;
260
+ }
261
+
262
+ function renderUncached(node: CodeNode, options: BlockOptions): string {
263
+ const lang = resolveLanguage(node.language);
264
+ const lines = highlightLines(node.code, lang.grammar);
265
+ const body = options.lineNumbers ? lines.map((l) => `<span class="cw-line">${l}</span>`).join("\n") : lines.join("\n");
266
+ const langClass = lang.id ? `language-${lang.id}` : "";
267
+ const label = options.label && lang.label ? `<span class="cw-code-label">${escapeHtml(lang.label)}</span>` : "";
268
+ // The header shows the language (no file name), left-aligned, with the copy button on the right.
269
+ const head = label ? `<div class="cw-code-head">${label}</div>` : "";
270
+ const classes = ["cw-code", head ? "" : "cw-code--bare", options.copy ? "cw-code--copy" : "", options.lineNumbers ? "cw-code--lines" : ""]
271
+ .filter(Boolean)
272
+ .join(" ");
273
+ const copy = options.copy
274
+ ? `<button type="button" class="cw-code-copy" aria-label="Copy code to clipboard">${COPY_ICON}${CHECK_ICON}</button><span class="cw-code-status" role="status" aria-live="polite"></span>`
275
+ : "";
276
+ const pre = `<pre${langClass ? ` class="${langClass}"` : ""} tabindex="0"><code class="${langClass ? `${langClass} ` : ""}hljs">${body}</code></pre>`;
277
+ return `<div class="${classes}"${lang.id ? ` data-language="${escapeHtml(lang.id)}"` : ""}>${head}${pre}${copy}</div>`;
278
+ }
279
+
280
+ // ── Chrome CSS + copy script (sent once per page) ────────────────
281
+
282
+ export const CHROME_CSS = [
283
+ ".cw-code{position:relative;margin:1.5rem 0;border-radius:.5rem;overflow:hidden;font-size:.875rem;line-height:1.6;box-shadow:inset 0 0 0 1px color-mix(in srgb,currentColor 12%,transparent)}",
284
+ ".cw-code-head{display:flex;align-items:center;gap:.75rem;min-height:2.75rem;padding:.25rem 1rem;font:.75rem/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:.02em;border-bottom:1px solid color-mix(in srgb,currentColor 15%,transparent)}",
285
+ ".cw-code--copy .cw-code-head{padding-right:3.25rem}",
286
+ ".cw-code-label{font-weight:600;color:var(--cw-label,currentColor)}",
287
+ ".cw-code pre{margin:0;padding:1rem;overflow-x:auto;background:transparent;color:inherit;border:0;border-radius:0;tab-size:4}",
288
+ ".cw-code--bare.cw-code--copy pre{padding-right:3.25rem}",
289
+ ".cw-code pre:focus-visible{outline:2px solid currentColor;outline-offset:-2px}",
290
+ ".cw-code code{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace;font-size:inherit;background:none;padding:0;color:inherit;white-space:pre}",
291
+ ".cw-code--lines code{counter-reset:cw-line}",
292
+ ".cw-code--lines .cw-line::before{counter-increment:cw-line;content:counter(cw-line);display:inline-block;width:2.5ch;margin-right:1.25ch;text-align:right;color:var(--cw-line,currentColor);user-select:none}",
293
+ ".cw-code-copy{position:absolute;top:.375rem;right:.375rem;display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;padding:0;color:inherit;background:transparent;border:1px solid color-mix(in srgb,currentColor 25%,transparent);border-radius:.5rem;cursor:pointer;opacity:.75;transition:opacity .15s ease,background-color .15s ease}",
294
+ ".cw-code-copy:hover{opacity:1;background:color-mix(in srgb,currentColor 10%,transparent)}",
295
+ ".cw-code-copy:focus-visible{opacity:1;outline:2px solid currentColor;outline-offset:2px}",
296
+ ".cw-code-copy svg{width:1rem;height:1rem}",
297
+ ".cw-code-i-check,.cw-code-copy.is-done .cw-code-i-copy{display:none}.cw-code-copy.is-done .cw-code-i-check{display:block}",
298
+ ".cw-code-status{position:absolute;top:.625rem;right:2.875rem;padding:.2rem .5rem;font-size:.75rem;line-height:1.4;white-space:nowrap;color:#fff;background:#111;border-radius:.375rem;opacity:0;pointer-events:none;transition:opacity .15s ease}",
299
+ ".cw-code-status.is-visible{opacity:1}",
300
+ "@media (prefers-reduced-motion:reduce){.cw-code-copy,.cw-code-status{transition:none}}",
301
+ ].join("");
302
+
303
+ /** Event-delegated copy handler; installs once per page. Mirrors Code Block Enhancer's pattern. */
304
+ export const COPY_SCRIPT = `(()=>{if(window.__cwCodeCopy)return;window.__cwCodeCopy=1;function w(t){if(navigator.clipboard&&window.isSecureContext)return navigator.clipboard.writeText(t);return new Promise(function(y,n){var a=document.createElement("textarea");a.value=t;a.setAttribute("readonly","");a.style.cssText="position:fixed;top:0;opacity:0";document.body.appendChild(a);a.select();try{document.execCommand("copy")?y():n()}catch(e){n(e)}finally{a.remove()}})}document.addEventListener("click",function(e){var b=e.target instanceof Element&&e.target.closest(".cw-code-copy");if(!b)return;var r=b.closest(".cw-code"),c=r&&r.querySelector("code"),s=r&&r.querySelector(".cw-code-status");if(!c||!s)return;function f(ok){b.classList.toggle("is-done",ok);s.textContent=ok?"Copied to clipboard":"Copy failed";s.classList.add("is-visible");clearTimeout(b._cwT);b._cwT=setTimeout(function(){b.classList.remove("is-done");s.classList.remove("is-visible");s.textContent=""},2000)}w(c.textContent||"").then(function(){f(true)},function(){f(false)})})})();`;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The active theme, read outside the plugin context (Astro block renderer)
3
+ * from D1 in the feature switches' query, sharing their per-isolate cache.
4
+ */
5
+ import { invalidateFeatures, readSiteSetting, registerSiteSetting } from "../core/features.js";
6
+ import { DEFAULT_THEME, THEME_SETTING, isTheme } from "./themes.js";
7
+
8
+ registerSiteSetting(THEME_SETTING);
9
+
10
+ export function invalidateTheme(): void {
11
+ invalidateFeatures();
12
+ }
13
+
14
+ export async function siteTheme(database = "DB"): Promise<string> {
15
+ const read = await readSiteSetting(THEME_SETTING, database);
16
+ if (!read) console.error("coywolf-pack: could not read code block theme");
17
+ return read && isTheme(read.value) ? read.value : DEFAULT_THEME;
18
+ }