@dogsbay/render-html 0.2.0-beta.129 → 0.2.0-beta.131

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/css/code-rich.css CHANGED
@@ -86,3 +86,36 @@
86
86
  padding: 1px 4px;
87
87
  margin: 0 -1px;
88
88
  }
89
+
90
+ /* ── `collapse` fence option: line runs folded behind a toggle ──
91
+ No script: a checkbox inside the label. Lines hide only where :has() works,
92
+ so a browser without it shows every line rather than losing them. */
93
+ .code-rich-body .code-collapse-label {
94
+ display: inline-block;
95
+ width: 100%;
96
+ cursor: pointer;
97
+ user-select: none;
98
+ /* The code's own colour: a muted label fell under 4.5:1 on the code background */
99
+ border-block: 1px dashed color-mix(in oklab, currentColor 35%, transparent);
100
+ font-style: italic;
101
+ }
102
+ .code-rich-body .code-collapse-toggle {
103
+ position: absolute;
104
+ width: 1px;
105
+ height: 1px;
106
+ opacity: 0;
107
+ }
108
+ .code-rich-body .code-collapse-label:has(.code-collapse-toggle:focus-visible) {
109
+ outline: 2px solid var(--ring);
110
+ outline-offset: -2px;
111
+ }
112
+ /* The label stays when open, so focus stays on the checkbox and it folds again */
113
+ .code-rich-body .code-collapse-label::before {
114
+ content: "▸ ";
115
+ }
116
+ .code-rich-body .code-collapse-label:has(.code-collapse-toggle:checked)::before {
117
+ content: "▾ ";
118
+ }
119
+ .code-rich-body .code-collapse:has(.code-collapse-toggle:not(:checked)) > .code-collapse-lines {
120
+ display: none;
121
+ }
package/css/docs.css CHANGED
@@ -65,7 +65,10 @@
65
65
  & li > * + *:not(.dba-api *) { margin-top: 0.75rem; }
66
66
 
67
67
  & ul:not(.dba-api *) { list-style: disc; padding-left: 1.5rem; margin: 0.75rem 0; }
68
- & ol:not(.dba-api *) { list-style: decimal; padding-left: 1.5rem; margin: 0.75rem 0; }
68
+ /* Not a step list, nor a list inside one: Steps.astro draws their numbers as circles, and this
69
+ rule (one class and an element) outranked its .steps-list { list-style: none }, so each step
70
+ showed its "2." beside its circle */
71
+ & ol:not(.dba-api *):not(.steps-list, .sub-steps-list, .steps-list ol) { list-style: decimal; padding-left: 1.5rem; margin: 0.75rem 0; }
69
72
  & li:not(.dba-api *) { margin: 0.25rem 0; }
70
73
  & li > ul:not(.dba-api *), & li > ol:not(.dba-api *) { margin: 0.25rem 0; }
71
74
  /* A file tree draws its own folder and file icons (files/markup.ts): the list rules above gave it bullets too */
package/css/steps.css CHANGED
@@ -98,6 +98,11 @@
98
98
  opacity: 0.6;
99
99
  }
100
100
 
101
+ /* A step's title on its own line, above its detail (format-astro and render-html mark it) */
102
+ .steps-list > li > .step-title {
103
+ display: block;
104
+ }
105
+
101
106
 
102
107
  .sub-steps-list {
103
108
  position: relative;
@@ -147,3 +152,8 @@
147
152
  background: var(--color-border);
148
153
  opacity: 0.6;
149
154
  }
155
+
156
+ /* A step's title on its own line, above its detail (format-astro and render-html mark it) */
157
+ .sub-steps-list > li > .step-title {
158
+ display: block;
159
+ }
@@ -10,7 +10,7 @@ export const RENDER_HTML_CAPABILITIES = [
10
10
  {
11
11
  feature: "code",
12
12
  support: "full",
13
- rendersAs: "`<db-code-block>` (CodeBlock / CodeRich markup); highlighted only when a Highlighter is passed, plain text otherwise",
13
+ rendersAs: "`<db-code-block>` (CodeBlock / CodeRich markup, `collapse` runs behind a toggle); highlighted only when a Highlighter is passed, plain text otherwise",
14
14
  },
15
15
  {
16
16
  feature: "math-block",
@@ -51,13 +51,13 @@ export const RENDER_HTML_CAPABILITIES = [
51
51
  support: "full",
52
52
  rendersAs: "`<db-tabs>` with Tabs.astro markup, synced groups by label; a child that is not a tab stays in reading order around the group",
53
53
  },
54
- { feature: "steps", support: "full", rendersAs: "`<ol class=\"steps-list\">`; a non-step child is wrapped in its own `<li>`" },
54
+ { feature: "steps", support: "full", rendersAs: "`<ol class=\"steps-list\">`; a non-step child is wrapped in its own `<li>`; a step's `title` prop, or its leading bold run (`stepLead`), is its title, a block above the detail" },
55
55
  {
56
56
  feature: "accordion",
57
57
  support: "full",
58
58
  rendersAs: "`<db-accordion>` with Accordion.astro markup; a child that is not an item stays in reading order around the group",
59
59
  },
60
- { feature: "cards", support: "full", rendersAs: "a responsive card grid `<div>`" },
60
+ { feature: "cards", support: "full", rendersAs: "a responsive card grid `<div>`; `cols` 1–6, one column on a phone" },
61
61
  { feature: "card", support: "full", rendersAs: "Card.astro markup; icon, title, description, body; linked when `href` is set" },
62
62
  { feature: "link-card", support: "full", rendersAs: "LinkCard.astro markup; a body of several blocks renders as a linked Card" },
63
63
  { feature: "files", support: "full", rendersAs: "FileTree's markup: folder and file icons, nested" },
package/dist/context.js CHANGED
@@ -2,6 +2,9 @@
2
2
  export const UI_STRINGS = {
3
3
  backToContent: "Back to content",
4
4
  close: "Close",
5
+ /** A `collapse` fence's toggle; `{n}` is the number of lines it hides (`collapsedLine` for one). */
6
+ collapsedLine: "1 collapsed line",
7
+ collapsedLines: "{n} collapsed lines",
5
8
  copyCode: "Copy code",
6
9
  details: "Details",
7
10
  related: "Related links",
package/dist/lines.d.ts CHANGED
@@ -44,6 +44,15 @@ export interface Highlighter {
44
44
  export declare const PLAIN: Set<string>;
45
45
  /** Parse "1,3-5" into [1,3,4,5]. */
46
46
  export declare function parseLineRanges(spec: unknown): number[];
47
+ /** Sorted line numbers as contiguous runs: [2,3,4,7] → [[2,4],[7,7]]. */
48
+ export declare function lineRuns(lines: number[]): Array<[number, number]>;
49
+ /**
50
+ * The fence's `collapse` option (Expressive Code's): each run of lines folded behind a toggle that
51
+ * names how many lines it hides, as CodeRich.astro draws it. No script: a checkbox in the label, and
52
+ * code-rich.css shows the lines when it is checked (or always, in a browser without `:has()`).
53
+ * Works on any highlighter's `<pre><code>`, whose lines are `\n`-separated `<span class="line">`s.
54
+ */
55
+ export declare function collapseLines(pre: string, lines: number[], label: (count: number) => string): string;
47
56
  /** CodeRich's gutter: the line number and a diff marker (+, −, or a space) in one column. */
48
57
  export declare function gutterHtml(n: number, kind: string, lineNumbers: boolean, diffs: boolean): string;
49
58
  /** Code as escaped text, with the annotations and gutter a highlighted block has. `src` has had its unprocessable markers removed. */
package/dist/lines.js CHANGED
@@ -26,6 +26,41 @@ export function parseLineRanges(spec) {
26
26
  }
27
27
  return out;
28
28
  }
29
+ /** Sorted line numbers as contiguous runs: [2,3,4,7] → [[2,4],[7,7]]. */
30
+ export function lineRuns(lines) {
31
+ const sorted = [...new Set(lines)].filter((n) => n > 0).sort((a, b) => a - b);
32
+ const runs = [];
33
+ for (const n of sorted) {
34
+ const last = runs[runs.length - 1];
35
+ if (last && n === last[1] + 1)
36
+ last[1] = n;
37
+ else
38
+ runs.push([n, n]);
39
+ }
40
+ return runs;
41
+ }
42
+ /**
43
+ * The fence's `collapse` option (Expressive Code's): each run of lines folded behind a toggle that
44
+ * names how many lines it hides, as CodeRich.astro draws it. No script: a checkbox in the label, and
45
+ * code-rich.css shows the lines when it is checked (or always, in a browser without `:has()`).
46
+ * Works on any highlighter's `<pre><code>`, whose lines are `\n`-separated `<span class="line">`s.
47
+ */
48
+ export function collapseLines(pre, lines, label) {
49
+ const open = pre.indexOf("<code>");
50
+ const close = pre.lastIndexOf("</code>");
51
+ if (open < 0 || close < open || !lines.length)
52
+ return pre;
53
+ const parts = pre.slice(open + 6, close).split("\n");
54
+ for (const [a, b] of lineRuns(lines).reverse()) {
55
+ if (a > parts.length)
56
+ continue;
57
+ const end = Math.min(b, parts.length);
58
+ const group = `<span class="code-collapse"><label class="code-collapse-label" data-pagefind-ignore><input type="checkbox" class="code-collapse-toggle">${label(end - a + 1)}</label>` +
59
+ `<span class="code-collapse-lines">${parts.slice(a - 1, end).join("\n")}</span></span>`;
60
+ parts.splice(a - 1, end - a + 1, group);
61
+ }
62
+ return pre.slice(0, open + 6) + parts.join("\n") + pre.slice(close);
63
+ }
29
64
  /** CodeRich's gutter: the line number and a diff marker (+, −, or a space) in one column. */
30
65
  export function gutterHtml(n, kind, lineNumbers, diffs) {
31
66
  if (!lineNumbers && !diffs)
package/dist/render.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * contract comes from `@dogsbay/elements`. Everything is a string, so
5
5
  * rendering is synchronous and runs anywhere.
6
6
  */
7
- import { isDetailsSheet, renderAroundGroup } from "@dogsbay/serialize-core";
7
+ import { isDetailsSheet, renderAroundGroup, stepLead, stepTitle } from "@dogsbay/serialize-core";
8
8
  import { parseInline } from "@dogsbay/format-dogsbay-md/browser";
9
9
  import { attrs, esc, FOREIGN_END, FOREIGN_START, passthrough, slugify, unesc } from "./html.js";
10
10
  import { CALLOUTS, hasIcon, icon } from "./icons.js";
@@ -14,7 +14,7 @@ import { CARD_TONE_CLASSES, TAB_VARIANTS, THEME_SCOPES, TONE_CLASSES, lookup } f
14
14
  import { fileTreeHtml } from "./file-tree.js";
15
15
  import { stripMarkersForCopy } from "./code-markers.js";
16
16
  import { icons as ALERT_ICONS, chevronSvg as ALERT_CHEVRON } from "./alert-icons.js";
17
- import { parseLineRanges, plainHighlighter } from "./lines.js";
17
+ import { collapseLines, parseLineRanges, plainHighlighter } from "./lines.js";
18
18
  import { decorateDiffHtml, diffMarksOf, hasDiffMarks, inlineSegmentsToHtml, TAB_GLYPH, tabDiffSignal } from "./diff.js";
19
19
  import { scriptUrl } from "./url.js";
20
20
  import { API_NODE_TYPES, apiCodeInputs, renderApiNode } from "./api.js";
@@ -425,9 +425,18 @@ function block(n, ctx) {
425
425
  return callout(n, ctx);
426
426
  case "steps":
427
427
  return steps(n, ctx);
428
- case "step":
429
- // Tight, like the Astro Step: the lead text is not wrapped in <p>.
430
- return `<li>${renderInline(n.inline, ctx)}${renderBlocks(n.children, ctx)}</li>`;
428
+ case "step": {
429
+ // Tight, like the Astro Step: the lead text is not wrapped in <p>. A `title` prop (Mintlify
430
+ // `<Step title>`) leads it in bold, as format-astro draws it.
431
+ // The title on its own line (steps.css: `.step-title` is a block): from the prop, or the
432
+ // step's leading bold run (stepLead), which Markdown otherwise runs into the detail
433
+ const title = stepTitle(n);
434
+ const split = title ? undefined : stepLead(n.inline);
435
+ const lead = title ? `<strong class="step-title">${esc(title)}</strong>` : split ? `<strong class="step-title">${renderInline(split.title, ctx)}</strong>` : "";
436
+ const text = renderInline(split ? split.rest : n.inline, ctx);
437
+ // A space after the title: invisible beside a block, but what a text reader (export) needs
438
+ return `<li>${lead}${lead && text ? " " : ""}${text}${renderBlocks(n.children, ctx)}</li>`;
439
+ }
431
440
  case "tabs":
432
441
  return renderAroundGroup(n, "tab", (g) => tabs(g, ctx), (c) => renderBlocks([c], ctx), "");
433
442
  case "tab":
@@ -456,7 +465,10 @@ function block(n, ctx) {
456
465
  // A group tone applies to every card that has none of its own
457
466
  const tone = str(p.tone);
458
467
  const kids = tone ? (n.children ?? []).map((c) => (c.type === "card" && !c.props?.tone ? { ...c, props: { ...c.props, tone } } : c)) : n.children;
459
- return `<div${attrs(passthrough(p, "not-prose my-4 grid grid-cols-1 gap-4 sm:grid-cols-2"))}>${renderBlocks(kids, ctx)}</div>\n`;
468
+ // `cols` as format-astro's cardCols: one column on a phone, up to `cols` (1–6) wider; default 2
469
+ // A list (`1,3`, `[1,2,3]`: format-astro's own breakpoints) takes its widest
470
+ const cols = Math.min(6, Math.max(1, Math.round(Number(String(p.cols ?? "").replace(/[[\]\s]/g, "").split(",").pop()) || 2)));
471
+ return `<div${attrs(passthrough(p, `not-prose my-4 grid grid-cols-1 gap-4 ${GRID_COLS[cols]}`))}>${renderBlocks(kids, ctx)}</div>\n`;
460
472
  }
461
473
  // A scoped theme: `.dark` re-declares the tokens for its subtree (THEME_SCOPES)
462
474
  case "theme": {
@@ -690,10 +702,13 @@ function codeBlock(n, ctx) {
690
702
  const title = str(p.title) ?? str(p.filename);
691
703
  const { highlights, lineNumbers, diffAdd, diffRemove } = opts;
692
704
  const hl = ctx.highlighter ?? plainHighlighter;
693
- const pre = hl.code(code, lang, opts);
705
+ // `collapse`: line runs folded behind a toggle (CodeRich.astro's); the label is not code, so copy the source
706
+ const collapse = parseLineRanges(p.collapse);
707
+ const collapsedLabel = (count) => esc((count === 1 ? ctx.t("collapsedLine") : ctx.t("collapsedLines")).replace("{n}", String(count)));
708
+ const pre = collapseLines(hl.code(code, lang, opts), collapse, collapsedLabel);
694
709
  // A block with annotations takes CodeRich's styles, as the Astro pipeline routes it to CodeRich.
695
710
  const diffs = diffAdd.length > 0 || diffRemove.length > 0;
696
- const rich = lineNumbers || highlights.length > 0 || diffs || code.includes("[!code");
711
+ const rich = lineNumbers || highlights.length > 0 || diffs || code.includes("[!code") || collapse.length > 0;
697
712
  // Plain by its TAG, as CodeBlock.astro: a language the highlighter has no grammar for (mermaid) keeps its label
698
713
  const plain = !lang || plainHighlighter.resolve(lang) === "text";
699
714
  const copyLabel = esc(ctx.t("copyCode"));
@@ -716,7 +731,7 @@ function codeBlock(n, ctx) {
716
731
  "data-lang": lang,
717
732
  "data-line-numbers": lineNumbers || undefined,
718
733
  // With a gutter or [!code …] markers the text on the page is not the code: copy the source, markers removed.
719
- "data-copy-text": str(p.copyText) ?? (lineNumbers || diffs || code.includes("[!code") ? stripMarkersForCopy(code.replace(/\n$/, "")) : undefined),
734
+ "data-copy-text": str(p.copyText) ?? (lineNumbers || diffs || collapse.length > 0 || code.includes("[!code") ? stripMarkersForCopy(code.replace(/\n$/, "")) : undefined),
720
735
  })}>` +
721
736
  header +
722
737
  floating +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/render-html",
3
- "version": "0.2.0-beta.129",
3
+ "version": "0.2.0-beta.131",
4
4
  "description": "Dogsbay MD TreeNode → HTML with the Astro components' markup and classes. No highlighter, math or CSS compiler unless you pass one; Worker- and browser-safe",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,9 +30,9 @@
30
30
  "dependencies": {
31
31
  "htmlparser2": "^10.1.0",
32
32
  "markdown-it": "^14.3.0",
33
- "@dogsbay/format-dogsbay-md": "0.2.0-beta.129",
34
- "@dogsbay/serialize-core": "0.2.0-beta.129",
35
- "@dogsbay/types": "0.2.0-beta.129"
33
+ "@dogsbay/format-dogsbay-md": "0.2.0-beta.131",
34
+ "@dogsbay/serialize-core": "0.2.0-beta.131",
35
+ "@dogsbay/types": "0.2.0-beta.131"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "katex": "^0.16.0"