@dogsbay/ui 0.2.0-beta.162 → 0.2.0-beta.163

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/ui",
3
- "version": "0.2.0-beta.162",
3
+ "version": "0.2.0-beta.163",
4
4
  "description": "Accessible UI components for Astro, inspired by Base UI and shadcn",
5
5
  "type": "module",
6
6
  "exports": {
@@ -22,9 +22,9 @@
22
22
  "katex": "^0.16.47",
23
23
  "maplibre-gl": "^5.24.0",
24
24
  "shiki": "^4.3.1",
25
- "@dogsbay/elements": "0.2.0-beta.162",
26
- "@dogsbay/icons": "0.2.0-beta.162",
27
- "@dogsbay/primitives": "0.2.0-beta.162"
25
+ "@dogsbay/elements": "0.2.0-beta.163",
26
+ "@dogsbay/primitives": "0.2.0-beta.163",
27
+ "@dogsbay/icons": "0.2.0-beta.163"
28
28
  },
29
29
  "files": [
30
30
  "src",
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import { Code } from "astro/components";
3
+ import { ansiColoursOnly, stripAnsi } from "./ansi";
3
4
  import { tv } from "tailwind-variants";
4
5
 
5
6
  const codeBlockWrapper = tv({
@@ -55,6 +56,16 @@ const LANG_ALIASES: Record<string, string> = {
55
56
  };
56
57
  const shikiLang = LANG_ALIASES[lang.toLowerCase()] ?? lang.toLowerCase();
57
58
 
59
+ // Tags that mean "no language": no label, as render-html (its PLAIN set, @dogsbay/render-html/lines).
60
+ // Only `plaintext` was hidden, so `txt` and `output` blocks showed "txt" and "output".
61
+ const PLAIN = new Set(["", "text", "txt", "plain", "plaintext", "none", "output", "log", "code", "codeblock"]);
62
+ const plain = PLAIN.has(lang.toLowerCase().trim());
63
+ // An `ansi` block shows its colours, not its escape codes: copy the text as shown
64
+ const isAnsi = shikiLang === "ansi";
65
+ const copyText = isAnsi ? stripAnsi(code) : code;
66
+ // Shiki draws colours only; any other sequence would garble the text (./ansi.ts)
67
+ const shownCode = isAnsi ? ansiColoursOnly(code) : code;
68
+
58
69
  // Show header bar based on showTitle prop:
59
70
  // - true: show when there's a title or a declared language (default, original behavior)
60
71
  // - false: never show
@@ -66,7 +77,7 @@ const showHeader =
66
77
  hasCaption ? true
67
78
  : showTitle === false ? false
68
79
  : showTitle === "auto" ? !!title
69
- : !!title || lang !== "plaintext";
80
+ : !!title || !plain;
70
81
  // When no header, copy button floats inside the code body
71
82
  const floatingCopy = copy && !showHeader;
72
83
  ---
@@ -90,12 +101,12 @@ const floatingCopy = copy && !showHeader;
90
101
  <span class="select-none">{lang}</span>
91
102
  )}
92
103
  <span class="flex shrink-0 items-center gap-2">
93
- {(hasCaption || title) && lang !== "plaintext" && <span class="select-none" data-part="language">{lang}</span>}
104
+ {(hasCaption || title) && !plain && <span class="select-none" data-part="language">{lang}</span>}
94
105
  {copy && (
95
106
  <button
96
107
  class={codeBlockCopy()}
97
108
  data-part="copy"
98
- data-copy-text={code}
109
+ data-copy-text={copyText}
99
110
  type="button"
100
111
  aria-label="Copy code"
101
112
  >
@@ -137,12 +148,12 @@ const floatingCopy = copy && !showHeader;
137
148
  </Header>
138
149
  )}
139
150
  <div class="relative overflow-x-auto [&_pre]:!m-0 [&_pre]:!rounded-none [&_pre]:!border-0 [&_pre]:px-4 [&_pre]:py-3">
140
- <Code code={code} lang={shikiLang} themes={{ light: "github-light-default", dark: theme }} defaultColor={false} />
151
+ <Code code={shownCode} lang={shikiLang} themes={{ light: "github-light-default", dark: theme }} defaultColor={false} />
141
152
  {floatingCopy && (
142
153
  <button
143
154
  class:list={[codeBlockCopy(), "absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity"]}
144
155
  data-part="copy"
145
- data-copy-text={code}
156
+ data-copy-text={copyText}
146
157
  type="button"
147
158
  aria-label="Copy code"
148
159
  >
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A terminal's escape sequences in an `ansi` code block (dogsbay-ssg, 2026-11-04). Shared with
3
+ * @dogsbay/render-html (copied by its sync-sources as `code-ansi.ts`), so both pipelines show and
4
+ * copy the same text.
5
+ *
6
+ * One pattern, linear (each alternative is a run of one character class with a fixed end):
7
+ * - CSI: `ESC [` or the 8-bit `\x9b`, parameters, intermediates, a final byte — or cut off at the
8
+ * end of the text;
9
+ * - OSC (titles, `ESC ] 8` hyperlinks): to BEL or `ESC \`, or the end of the text;
10
+ * - `ESC` + intermediates + a final byte (`ESC ( B`, which `tput sgr0` writes), and any other
11
+ * two-byte escape.
12
+ */
13
+ const SEQUENCE = /(?:\x1b\[|\x9b)[0-?]*[ -/]*(?:[@-~]|$)|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)|\x1b[ -/]*[0-~]?/g;
14
+
15
+ /** Is this sequence a colour or style (SGR, `ESC [ … m`), the only kind a highlighter draws? */
16
+ const SGR = /^(?:\x1b\[|\x9b)[0-9;:]*m$/;
17
+
18
+ /** The text as a terminal shows it, every escape sequence left out (plain rendering, copy). */
19
+ export function stripAnsi(s: string): string {
20
+ return s.replace(SEQUENCE, "");
21
+ }
22
+
23
+ /**
24
+ * Only the colour and style sequences kept, for Shiki's `ansi`: it reads `ESC [` as running to the
25
+ * next `m`, so `ESC [2K` (erase line) ate the next letters (`Compiling` → `piling`), and it shows
26
+ * an OSC hyperlink's bytes as text. What is left is what it draws correctly. 8-bit CSI is written
27
+ * in its 7-bit form.
28
+ */
29
+ export function ansiColoursOnly(s: string): string {
30
+ return s.replace(SEQUENCE, (m) => (SGR.test(m) ? m.replace(/^\x9b/, "\x1b[") : ""));
31
+ }
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import "./code-rich.css";
3
3
  import { stripMarkersForCopy, stripUnprocessableMarkers } from "./markers";
4
+ import { ansiColoursOnly, stripAnsi } from "../code-block/ansi";
4
5
  import { codeToHtml, bundledLanguages, type BundledLanguage, type BundledTheme } from "shiki";
5
6
  import {
6
7
  transformerNotationDiff,
@@ -246,19 +247,22 @@ if (wordHighlights) metaStr += `/${wordHighlights}/ `;
246
247
  // Copy text: explicit override (diff blocks pass the post-change code)
247
248
  // or the block's own code — either way every annotation form is
248
249
  // stripped (see ./markers.ts).
249
- const copyText = stripMarkersForCopy(copyTextOverride ?? code);
250
+ const copyText = stripMarkersForCopy(copyTextOverride ?? (lang.toLowerCase() === "ansi" ? stripAnsi(code) : code));
250
251
 
251
252
  // Unknown languages (package-install, mdx-ish doc tokens) fall back
252
253
  // to plaintext instead of throwing — same behavior as CodeBlock's
253
254
  // <Code>. The header still shows the declared language label.
254
255
  const requestedLang = lang.toLowerCase();
256
+ // `ansi` is no grammar: Shiki colours its escape codes itself (as CodeBlock's <Code>), and it has
257
+ // no comments for [!code] markers, so they are stripped as in plain text.
258
+ const isAnsi = requestedLang === "ansi";
255
259
  const isPlainGrammar =
256
- !(requestedLang in bundledLanguages) || ["plaintext", "text", "txt", "ansi"].includes(requestedLang);
257
- const shikiLang = isPlainGrammar ? "plaintext" : requestedLang;
260
+ !isAnsi && (!(requestedLang in bundledLanguages) || ["plaintext", "text", "txt"].includes(requestedLang));
261
+ const shikiLang = isAnsi ? "ansi" : isPlainGrammar ? "plaintext" : requestedLang;
258
262
 
259
263
  // Markers Shiki cannot process would render as literal text — strip
260
264
  // exactly those, leave the rest for the notation transformers.
261
- const highlightCode = stripUnprocessableMarkers(code, isPlainGrammar);
265
+ const highlightCode = stripUnprocessableMarkers(isAnsi ? ansiColoursOnly(code) : code, isPlainGrammar || isAnsi);
262
266
 
263
267
  const html = await codeToHtml(highlightCode, {
264
268
  lang: shikiLang as BundledLanguage,
@@ -275,7 +279,10 @@ const html = await codeToHtml(highlightCode, {
275
279
  const hasCaption = Astro.slots.has("caption");
276
280
  const Wrapper = hasCaption ? "figure" : "div";
277
281
  const Header = hasCaption ? "figcaption" : "div";
278
- const hasLabel = hasCaption || !!title || (lang !== "plaintext");
282
+ // Tags that mean "no language" show no label, as CodeBlock and render-html
283
+ const PLAIN = new Set(["", "text", "txt", "plain", "plaintext", "none", "output", "log", "code", "codeblock"]);
284
+ const plain = PLAIN.has(requestedLang.trim());
285
+ const hasLabel = hasCaption || !!title || !plain;
279
286
  const showHeader = hasLabel;
280
287
  // When no header, copy button floats inside the code body
281
288
  const floatingCopy = copy && !showHeader;
@@ -301,7 +308,7 @@ const floatingCopy = copy && !showHeader;
301
308
  <span class="select-none">{lang}</span>
302
309
  )}
303
310
  <span class="flex shrink-0 items-center gap-2">
304
- {(hasCaption || title) && lang !== "plaintext" && <span class="select-none" data-part="language">{lang}</span>}
311
+ {(hasCaption || title) && !plain && <span class="select-none" data-part="language">{lang}</span>}
305
312
  {copy && (
306
313
  <button
307
314
  class={codeRichCopy()}