@tdacorp/hyle-sdk 0.2.0 → 0.3.0

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/README.md CHANGED
@@ -127,12 +127,25 @@ export default async function sitemap() {
127
127
 
128
128
  ## Host requirements
129
129
 
130
+ Bundler only: `@tdacorp/hyle-sdk/next` and `/next/use-cache` import `next/navigation` and `next/cache` the way a Next app does, so plain Node ESM cannot import them. Real `next build` runs of the packed package passed with Next 16.2.7, 16.3.4 and 16.4.2 (Turbopack, the default).
131
+
130
132
  No layout or page above the mount may export `dynamicParams = false`. hyle content (an article published after your last build) is by definition not known at build time, and Next inherits a parent's `false` for every child route: the route is compiled with `"fallback": false`, so each path not generated at build time is a hard 404 that falls through to any `[locale]/[...rest]` catch-all (observed: `x-matched-path: /[locale]/[...rest]`, content looks missing). Setting `dynamicParams = true` on the page does not override a parent's `false`.
131
133
 
132
134
  Verify after `next build`: `.next/prerender-manifest.json` must not show `"fallback": false` for the route that mounts `hylePage`, and a request for a published article that did not exist at build time must return 200.
133
135
 
134
136
  Locale-prefixed hosts: you may keep `generateStaticParams` for the locales, but validate the locale in the layout (call `notFound()` for an unknown one) and set `dynamicParams = true` there, so unknown locales are still rejected.
135
137
 
138
+ ## Mermaid diagrams
139
+
140
+ Zero setup. A Mermaid fence arrives from hyle as a sandboxed `<iframe srcdoc>` that loads a script from cdnjs, and a `srcdoc` frame inherits the host page's CSP, so a host with `script-src 'self'` would show a blank box. The SDK therefore reads the diagram text out of that document (only after it matched hyle's exact template) and renders it in your page with the Mermaid library it bundles as a dependency (`mermaid` `~11.9.0`, MIT). No cdnjs, no extra package, no config.
141
+
142
+ - What loads when: the server renders a no-JavaScript fallback, a `<figure class="hyle-mermaid">` holding the source as a `language-mermaid` code block. On a page with a diagram, the browser then imports `mermaid` (a dynamic import, so your bundler emits it as lazy chunks served from your own origin) and swaps in the SVG. Pages without a diagram never load it: the client component is not even in their JavaScript. In the e2e app (Next 16.2.7, Turbopack) the lazy chunks total about 2.8 MB raw, 0.8 MB gzipped across every diagram type; a page loads only the ones its diagram type needs. `mermaid` itself is 64 MB on disk in `node_modules` (it is not shipped to browsers).
143
+ - Rendering: a diagram renders when it is within 200 px of the viewport, one at a time, each limited to 10 s (a slower one falls back to the code block and the note). Per-diagram configuration (`%%{init}%%` directives and frontmatter) is stripped, see Security model. A re-render (theme change) keeps the previous SVG on screen until the new one is ready, and keeps it if the new one fails.
144
+ - Themes: dark or default is chosen from the figure's computed background (falling back to `prefers-color-scheme`), and the diagram re-renders when `data-act`, `data-theme` or `class` change on `<html>` or the OS scheme changes.
145
+ - Fallback: if the source cannot be extracted (over 20,000 characters, or not the renderer's template) the old sandboxed frame is used, which needs cdnjs in your CSP. If Mermaid cannot render the source (a syntax error), the code block stays and a short "diagram could not be rendered" note appears.
146
+ - Host CSP: `script-src 'self'` is enough (plus `'unsafe-inline'` or nonces for Next's own inline scripts, as for any Next app); `style-src 'unsafe-inline'` is required because Mermaid's SVG carries inline styles; `img-src` decides whether images that a diagram label references load. Checked under `default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:` in headless Chromium on Next 16.2.7, 16.3.4 and 16.4.2 (`node scripts/e2e-next-build.mjs --browser`).
147
+ - Override: `components.Mermaid` still replaces all of this. It receives `{ srcDoc, source, title }`; `source` is the diagram text, or `undefined` when it could not be extracted. `MermaidDiagram` (`{ source, title, errorText }`) is exported from `./react` if you want to wrap it. It is the SDK's only client component and takes nothing but those strings: no token or client ever crosses the boundary.
148
+
136
149
  ## Styling
137
150
 
138
151
  Import `@tdacorp/hyle-sdk/styles.css`. It is the design kit's stylesheet: every value comes from a public `--hyle-*` custom property with a SIGNAL fallback (`--sig-*` from `@tdacorp/signal-tokens`, so dark and light acts flip with no extra code). Set variables on `:root`, `body` or any wrapper above the mount; the SDK never declares them:
@@ -147,7 +160,7 @@ The 52 variables are listed in the token block at the top of `styles.css`. Layou
147
160
 
148
161
  `components` replaces how a part is drawn, `slots` fills named places. Both are props of the pages and options of `hylePage`.
149
162
 
150
- - Component slots: `Link` (every internal link, breadcrumb and in-page link: pass your router's Link), `Image`, `CodeBlock` (receives `code`, `language`, and the default `children`; the stock block has no copy button because that needs client JavaScript), `Mermaid` (receives hyle's sandboxed `srcDoc`), `Callout` (GFM alerts such as `> [!NOTE]`). The raw element overrides `a`, `img`, `code`, `pre`, `table`, `blockquote` and `h1` to `h6` also work.
163
+ - Component slots: `Link` (every internal link, breadcrumb and in-page link: pass your router's Link), `Image`, `CodeBlock` (receives `code`, `language`, and the default `children`; the stock block has no copy button because that needs client JavaScript), `Mermaid` (receives hyle's sandboxed `srcDoc` and the extracted `source`; the default renders the diagram in the page, see "Mermaid diagrams"), `Callout` (GFM alerts such as `> [!NOTE]`). The raw element overrides `a`, `img`, `code`, `pre`, `table`, `blockquote` and `h1` to `h6` also work.
151
164
  - Visual slots: `article.before`, `article.meta`, `article.aside`, `article.after`, `list.header`, `list.item` (a component that receives the item), `list.empty`, `docs.sidebar-header`, `docs.version`, `docs.sidebar-footer`, `docs.after`, `notfound.body`, `notfound.actions`, `redirecting.body`. An unfilled slot renders nothing.
152
165
  - Layout: `layout: "docs"` renders articles as `DocsPage` (tree navigation from `ancestors`, `children` and `breadcrumbs`).
153
166
 
@@ -157,7 +170,8 @@ Chrome strings exist for en, fr and es (fr and es are unreviewed drafts). The pa
157
170
 
158
171
  - The token is sent only as an `Authorization` header from the server. It is never placed in a URL, logged or put in an error message. Keep it out of client components. The client requires an https `baseUrl` (http only for localhost, or with `allowInsecureHttp`), refuses redirects, aborts after `timeoutMs` (default 10 s) and refuses responses over `maxResponseBytes` (default 5 MiB).
159
172
  - The path is sent as a query parameter (encoded once); catch-all segments are decoded and refused if they contain `/`, `\`, control characters, `.` or `..`.
160
- - The `hast` tree is rendered through an allowlist (`sanitizeHast`) even though hyle sanitizes server-side: only known tags and properties with accepted value shapes survive; no `on*` attributes, `style` only inside KaTeX output and only for the properties KaTeX emits with small values (no `position:absolute`, `z-index`, `left` or large sizes), ids and in-page fragments scoped with the server's `user-content-` prefix (so heading ids, footnote links and TOC links still resolve), `href` and `src` limited to site-relative paths and `http(s)` (and `mailto` for links), unknown tags are unwrapped, `script` and `style` are dropped, and an `iframe` is kept only with `sandbox="allow-scripts"` and a `srcdoc` that is exactly the document hyle's renderer builds for a Mermaid fence (pinned script URL, at most 64 KiB); the SDK adds `referrerpolicy="no-referrer"` and a `csp` that allows only that script (checked in headless Chromium: the diagram still renders). A tree over 20,000 nodes or 512 KiB of text fails closed with `HastLimitError` and a safe notice. `dangerouslySetInnerHTML` is used in exactly one place, `TrustedServerHtml`, the escape hatch for `content.format === "html"`: it renders HTML as is, so pass it only HTML that came straight from hyle's resolve endpoint, never user input.
173
+ - The `hast` tree is rendered through an allowlist (`sanitizeHast`) even though hyle sanitizes server-side: only known tags and properties with accepted value shapes survive; no `on*` attributes, `style` only inside KaTeX output and only for the properties KaTeX emits with small values (no `position:absolute`, `z-index`, `left` or large sizes), ids and in-page fragments scoped with the server's `user-content-` prefix (so heading ids, footnote links and TOC links still resolve), `href` and `src` limited to site-relative paths and `http(s)` (and `mailto` for links), unknown tags are unwrapped, `script` and `style` are dropped, and an `iframe` is kept only with `sandbox="allow-scripts"` and a `srcdoc` that is exactly the document hyle's renderer builds for a Mermaid fence (pinned script URL, at most 64 KiB); the SDK adds `referrerpolicy="no-referrer"` and a `csp` that allows only that script (checked in headless Chromium: the diagram still renders). A tree over 20,000 nodes or 512 KiB of text fails closed with `HastLimitError` and a safe notice. `dangerouslySetInnerHTML` is used in exactly two places: the Mermaid SVG sink (below) and `TrustedServerHtml`, the escape hatch for `content.format === "html"`: it renders HTML as is, so pass it only HTML that came straight from hyle's resolve endpoint, never user input.
174
+ - Mermaid renders into your page, so: the source comes only from hyle content, passes the exact-template check and a 20,000 character cap, and is rendered with `securityLevel: "strict"` (Mermaid's own DOMPurify pass, no raw HTML labels) and `maxTextSize` set to the cap. The SDK strips every `%%{ ... }%%` directive (multi-line, unterminated and `init`, `initialize`, `config`, `wrap` forms) and a leading YAML frontmatter block from the source before rendering, and keeps plain `%% comment` lines. Reason: Mermaid's `themeCSS` (set by a directive or by frontmatter `config:`) is injected into the page as CSS and its sanitizer only checks brace balance, so a diagram could make the page fetch a URL or overlay the host UI; verified against Mermaid 11.9.0 with a loopback canary server, which received requests when the stripping was disabled and none with it. Consequence: authors cannot configure a diagram per diagram; a diagram that carries an init directive renders with the SDK's defaults. As a second guard `.hyle-mermaid-svg` uses `contain: paint` and the SVG is `position: static !important`, so diagram CSS cannot place anything over the host page. Flowchart `style` and `classDef` lines cannot carry `url(...)` (they fail to parse). What remains: Mermaid keeps an `<img>` inside a label (event handlers are stripped), so an author can make the page request an image, and your CSP `img-src` governs that exactly as for Markdown images (the e2e proves a strict `img-src 'self' data:` stops it); a `click A "https://..."` line makes a clickable link, a phishing-link capability inside author-controlled content, so treat hyle authors as trusted to link. Nothing is executed: the SDK ignores Mermaid's `bindFunctions`, and the hostile `click ... call`, `<img onerror>` and `securityLevel: "loose"` payloads did nothing in the browser check. The host CSP needs `style-src 'unsafe-inline'`.
161
175
  - Webhooks are verified with HMAC-SHA256 over the raw body (read with a running 64 KiB cap), a 300 s timestamp tolerance and constant-time comparison, with dual-secret rotation (empty secrets are never candidates), a payload shape check (400, not 500), tag validation, and delivery-id dedupe that records an id only after every purge succeeded, so a failed delivery can be retried.
162
176
 
163
177
  ## Not included
@@ -39,6 +39,12 @@ function Icon({ name, className }) {
39
39
  return /* @__PURE__ */ jsx("svg", { className: className ? `hyle-ic ${className}` : "hyle-ic", viewBox: "0 0 16 16", "aria-hidden": "true", children: SHAPES[name] });
40
40
  }
41
41
 
42
+ // src/react/content.tsx
43
+ import { MermaidDiagram } from "./mermaid-diagram.js";
44
+
45
+ // src/react/mermaid-config.ts
46
+ var MAX_MERMAID_SOURCE = 2e4;
47
+
42
48
  // src/react/sanitize.ts
43
49
  var HastLimitError = class extends Error {
44
50
  constructor(message) {
@@ -97,9 +103,14 @@ var isHref = (v) => isSafeUrl(v, "href") && (typeof v !== "string" || !v.trim().
97
103
  var isSrc = (v) => isSafeUrl(v, "src");
98
104
  var MERMAID_SCRIPT_URL = "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.9.0/mermaid.min.js";
99
105
  var MERMAID_SRCDOC = new RegExp(
100
- `^<!doctype html><html><head><meta charset="utf-8"></head><body><div class="mermaid">[^<]*</div><script src="${MERMAID_SCRIPT_URL.replace(/[.]/g, "\\.")}"></script><script>mermaid\\.initialize\\(\\{ startOnLoad: true, securityLevel: "strict" \\}\\);</script></body></html>$`
106
+ `^<!doctype html><html><head><meta charset="utf-8"></head><body><div class="mermaid">([^<]*)</div><script src="${MERMAID_SCRIPT_URL.replace(/[.]/g, "\\.")}"></script><script>mermaid\\.initialize\\(\\{ startOnLoad: true, securityLevel: "strict" \\}\\);</script></body></html>$`
101
107
  );
102
108
  var isMermaidSrcdoc = (v) => typeof v === "string" && v.length <= MAX_SRCDOC && MERMAID_SRCDOC.test(v);
109
+ var ENTITIES = { "&#x3c;": "<", "&#x26;": "&", "&lt;": "<", "&gt;": ">", "&amp;": "&", "&quot;": '"', "&#x27;": "'" };
110
+ function mermaidSource(srcDoc) {
111
+ const text = MERMAID_SRCDOC.exec(srcDoc)?.[1]?.replace(/&(?:#x3c|#x26|#x27|lt|gt|amp|quot);/gi, (e) => ENTITIES[e.toLowerCase()]);
112
+ return text === void 0 || text.length > MAX_MERMAID_SOURCE ? void 0 : text;
113
+ }
103
114
  var MERMAID_FRAME_CSP = `default-src 'none'; script-src ${MERMAID_SCRIPT_URL} 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:`;
104
115
  var heading = { id: isIdValue, className: isClass };
105
116
  var ATTRS = new Map(
@@ -225,6 +236,7 @@ var en = {
225
236
  tip: "Tip",
226
237
  caution: "Caution",
227
238
  diagram: "Diagram",
239
+ diagramError: "This diagram could not be rendered.",
228
240
  contentError: "This content cannot be displayed.",
229
241
  fallbackTitle: "Not translated into {language} yet.",
230
242
  fallbackBody: "You are reading the original.",
@@ -264,6 +276,7 @@ var fr = {
264
276
  tip: "Astuce",
265
277
  caution: "Danger",
266
278
  diagram: "Sch\xE9ma",
279
+ diagramError: "Ce sch\xE9ma n'a pas pu \xEAtre affich\xE9.",
267
280
  contentError: "Ce contenu ne peut pas \xEAtre affich\xE9.",
268
281
  fallbackTitle: "Pas encore traduit en {language}.",
269
282
  fallbackBody: "Vous lisez l'original.",
@@ -303,6 +316,7 @@ var es = {
303
316
  tip: "Consejo",
304
317
  caution: "Peligro",
305
318
  diagram: "Diagrama",
319
+ diagramError: "No se pudo mostrar este diagrama.",
306
320
  contentError: "No se puede mostrar este contenido.",
307
321
  fallbackTitle: "A\xFAn no est\xE1 traducida al {language}.",
308
322
  fallbackBody: "Est\xE1s leyendo el original.",
@@ -390,7 +404,9 @@ function buildComponents(c, s) {
390
404
  };
391
405
  const Frame = ({ srcDoc, sandbox }) => {
392
406
  if (typeof srcDoc !== "string") return null;
393
- if (c.Mermaid) return /* @__PURE__ */ jsx3(c.Mermaid, { srcDoc, title: s.diagram });
407
+ const source = mermaidSource(srcDoc);
408
+ if (c.Mermaid) return /* @__PURE__ */ jsx3(c.Mermaid, { srcDoc, source, title: s.diagram });
409
+ if (source !== void 0) return /* @__PURE__ */ jsx3(MermaidDiagram, { source, title: s.diagram, errorText: s.diagramError });
394
410
  return /* @__PURE__ */ jsx3("figure", { className: "hyle-mermaid", "data-slot-component": "Mermaid", children: /* @__PURE__ */ jsx3("iframe", { srcDoc, sandbox: String(sandbox), title: s.diagram, loading: "lazy", referrerPolicy: "no-referrer", ...{ csp: MERMAID_FRAME_CSP } }) });
395
411
  };
396
412
  const Heading = (level) => function HyleHeading({ id, children, ...rest }) {
@@ -430,6 +446,9 @@ function TrustedServerHtml({ html }) {
430
446
  return /* @__PURE__ */ jsx3("div", { dangerouslySetInnerHTML: { __html: html } });
431
447
  }
432
448
 
449
+ // src/react/index.ts
450
+ import { MermaidDiagram as MermaidDiagram2 } from "./mermaid-diagram.js";
451
+
433
452
  // src/react/pages.tsx
434
453
  import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
435
454
  function Slot({ name, slots, tag: Tag = "div", className }) {
@@ -732,5 +751,6 @@ export {
732
751
  DocsPage,
733
752
  NotFound,
734
753
  Unavailable,
735
- Redirecting
754
+ Redirecting,
755
+ MermaidDiagram2 as MermaidDiagram
736
756
  };
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ "use client";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/react/mermaid-diagram.tsx
32
+ var mermaid_diagram_exports = {};
33
+ __export(mermaid_diagram_exports, {
34
+ MermaidDiagram: () => MermaidDiagram
35
+ });
36
+ module.exports = __toCommonJS(mermaid_diagram_exports);
37
+ var import_react = require("react");
38
+
39
+ // src/react/mermaid-config.ts
40
+ var MAX_MERMAID_SOURCE = 2e4;
41
+ function luminance(css) {
42
+ const m = /^(rgba?)\(([^)]*)\)$|^color\(srgb ([^)]*)\)$/.exec(css.trim());
43
+ if (!m) return void 0;
44
+ const parts = (m[2] ?? m[3] ?? "").split(/[\s,/]+/).filter(Boolean).map(Number);
45
+ const [r, g, b] = parts.map((v) => m[3] ? v : v / 255);
46
+ const alpha = parts[3] ?? 1;
47
+ if (r === void 0 || g === void 0 || b === void 0 || [r, g, b, alpha].some(Number.isNaN) || alpha < 0.5) return void 0;
48
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
49
+ }
50
+ function pickMermaidTheme(background, prefersDark) {
51
+ const l = luminance(background);
52
+ return (l === void 0 ? prefersDark : l < 0.5) ? "dark" : "default";
53
+ }
54
+ var RENDER_TIMEOUT_MS = 1e4;
55
+ var NEAR_VIEWPORT_MARGIN = "200px";
56
+ var FRONTMATTER = /^\s*-{3}[^\S\n]*\n[\s\S]*?\n-{3}[^\S\n]*(?:\n|$)/;
57
+ var DIRECTIVE = /%%\{(?:(?!\}%%)[\s\S])*(?:\}%%)?/g;
58
+ function stripMermaidConfig(source) {
59
+ let text = source.replace(/\r\n?/g, "\n");
60
+ for (let previous = ""; previous !== text; ) {
61
+ previous = text;
62
+ text = text.replace(FRONTMATTER, "").replace(DIRECTIVE, "");
63
+ }
64
+ return text;
65
+ }
66
+ var counter = 0;
67
+ var nextMermaidId = () => `hyle-mermaid-${++counter}`;
68
+ function withTimeout(promise, ms) {
69
+ let timer;
70
+ const timeout = new Promise((_, reject) => {
71
+ timer = setTimeout(() => reject(new Error("timeout")), ms);
72
+ });
73
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
74
+ }
75
+ function whenNearViewport(el, onNear, Observer = globalThis.IntersectionObserver) {
76
+ if (!Observer) {
77
+ onNear();
78
+ return () => {
79
+ };
80
+ }
81
+ const observer = new Observer((entries) => {
82
+ if (entries.some((e) => e.isIntersecting)) {
83
+ observer.disconnect();
84
+ onNear();
85
+ }
86
+ }, { rootMargin: NEAR_VIEWPORT_MARGIN });
87
+ observer.observe(el);
88
+ return () => observer.disconnect();
89
+ }
90
+
91
+ // src/react/mermaid-diagram.tsx
92
+ var import_jsx_runtime = require("react/jsx-runtime");
93
+ var queue = Promise.resolve();
94
+ async function renderSvg(id, source, theme) {
95
+ const { default: mermaid } = await import("mermaid");
96
+ mermaid.initialize({ startOnLoad: false, securityLevel: "strict", theme, maxTextSize: MAX_MERMAID_SOURCE, suppressErrorRendering: true });
97
+ try {
98
+ return (await mermaid.render(id, stripMermaidConfig(source))).svg;
99
+ } finally {
100
+ document.getElementById(`d${id}`)?.remove();
101
+ }
102
+ }
103
+ var THEME_ATTRS = ["data-act", "data-theme", "class"];
104
+ function MermaidDiagram({ source, title, errorText }) {
105
+ const figure = (0, import_react.useRef)(null);
106
+ const [theme, setTheme] = (0, import_react.useState)(null);
107
+ const [near, setNear] = (0, import_react.useState)(false);
108
+ const [svg, setSvg] = (0, import_react.useState)(null);
109
+ const [failed, setFailed] = (0, import_react.useState)(false);
110
+ (0, import_react.useEffect)(() => {
111
+ const scheme = window.matchMedia("(prefers-color-scheme: dark)");
112
+ const update = () => figure.current && setTheme(pickMermaidTheme(getComputedStyle(figure.current).backgroundColor, scheme.matches));
113
+ update();
114
+ const observer = new MutationObserver(update);
115
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: THEME_ATTRS });
116
+ scheme.addEventListener("change", update);
117
+ return () => {
118
+ observer.disconnect();
119
+ scheme.removeEventListener("change", update);
120
+ };
121
+ }, []);
122
+ (0, import_react.useEffect)(() => figure.current ? whenNearViewport(figure.current, () => setNear(true)) : void 0, []);
123
+ (0, import_react.useEffect)(() => {
124
+ if (theme === null || !near) return;
125
+ let current = true;
126
+ const id = nextMermaidId();
127
+ queue = queue.then(() => current ? withTimeout(renderSvg(id, source, theme), RENDER_TIMEOUT_MS) : void 0).then(
128
+ (out) => current && out !== void 0 && (setSvg(out), setFailed(false)),
129
+ () => current && setFailed(true)
130
+ );
131
+ return () => {
132
+ current = false;
133
+ };
134
+ }, [source, theme, near]);
135
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("figure", { ref: figure, className: "hyle-mermaid", "data-slot-component": "Mermaid", children: [
136
+ svg === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { tabIndex: 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "language-mermaid", children: source }) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "hyle-mermaid-svg", role: "img", "aria-label": title, dangerouslySetInnerHTML: { __html: svg } }),
137
+ failed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "hyle-mermaid-note", role: "status", children: errorText }) : null,
138
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("figcaption", { className: "sr-only", children: title })
139
+ ] });
140
+ }
141
+ // Annotate the CommonJS export names for ESM import in node:
142
+ 0 && (module.exports = {
143
+ MermaidDiagram
144
+ });
@@ -0,0 +1,17 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * The SDK's only client component. It receives the diagram text and presentational strings and nothing else: never a token,
5
+ * base URL or client (a test pins this). Mermaid is a dynamic import, so the chunk loads only on pages that have a diagram,
6
+ * from the host's own origin.
7
+ */
8
+ interface MermaidDiagramProps {
9
+ source: string;
10
+ /** Accessible name of the diagram. */
11
+ title: string;
12
+ /** Shown when Mermaid cannot render the source. */
13
+ errorText: string;
14
+ }
15
+ declare function MermaidDiagram({ source, title, errorText }: MermaidDiagramProps): react.JSX.Element;
16
+
17
+ export { MermaidDiagram, type MermaidDiagramProps };
@@ -0,0 +1,17 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * The SDK's only client component. It receives the diagram text and presentational strings and nothing else: never a token,
5
+ * base URL or client (a test pins this). Mermaid is a dynamic import, so the chunk loads only on pages that have a diagram,
6
+ * from the host's own origin.
7
+ */
8
+ interface MermaidDiagramProps {
9
+ source: string;
10
+ /** Accessible name of the diagram. */
11
+ title: string;
12
+ /** Shown when Mermaid cannot render the source. */
13
+ errorText: string;
14
+ }
15
+ declare function MermaidDiagram({ source, title, errorText }: MermaidDiagramProps): react.JSX.Element;
16
+
17
+ export { MermaidDiagram, type MermaidDiagramProps };
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ // src/react/mermaid-diagram.tsx
4
+ import { useEffect, useRef, useState } from "react";
5
+
6
+ // src/react/mermaid-config.ts
7
+ var MAX_MERMAID_SOURCE = 2e4;
8
+ function luminance(css) {
9
+ const m = /^(rgba?)\(([^)]*)\)$|^color\(srgb ([^)]*)\)$/.exec(css.trim());
10
+ if (!m) return void 0;
11
+ const parts = (m[2] ?? m[3] ?? "").split(/[\s,/]+/).filter(Boolean).map(Number);
12
+ const [r, g, b] = parts.map((v) => m[3] ? v : v / 255);
13
+ const alpha = parts[3] ?? 1;
14
+ if (r === void 0 || g === void 0 || b === void 0 || [r, g, b, alpha].some(Number.isNaN) || alpha < 0.5) return void 0;
15
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
16
+ }
17
+ function pickMermaidTheme(background, prefersDark) {
18
+ const l = luminance(background);
19
+ return (l === void 0 ? prefersDark : l < 0.5) ? "dark" : "default";
20
+ }
21
+ var RENDER_TIMEOUT_MS = 1e4;
22
+ var NEAR_VIEWPORT_MARGIN = "200px";
23
+ var FRONTMATTER = /^\s*-{3}[^\S\n]*\n[\s\S]*?\n-{3}[^\S\n]*(?:\n|$)/;
24
+ var DIRECTIVE = /%%\{(?:(?!\}%%)[\s\S])*(?:\}%%)?/g;
25
+ function stripMermaidConfig(source) {
26
+ let text = source.replace(/\r\n?/g, "\n");
27
+ for (let previous = ""; previous !== text; ) {
28
+ previous = text;
29
+ text = text.replace(FRONTMATTER, "").replace(DIRECTIVE, "");
30
+ }
31
+ return text;
32
+ }
33
+ var counter = 0;
34
+ var nextMermaidId = () => `hyle-mermaid-${++counter}`;
35
+ function withTimeout(promise, ms) {
36
+ let timer;
37
+ const timeout = new Promise((_, reject) => {
38
+ timer = setTimeout(() => reject(new Error("timeout")), ms);
39
+ });
40
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
41
+ }
42
+ function whenNearViewport(el, onNear, Observer = globalThis.IntersectionObserver) {
43
+ if (!Observer) {
44
+ onNear();
45
+ return () => {
46
+ };
47
+ }
48
+ const observer = new Observer((entries) => {
49
+ if (entries.some((e) => e.isIntersecting)) {
50
+ observer.disconnect();
51
+ onNear();
52
+ }
53
+ }, { rootMargin: NEAR_VIEWPORT_MARGIN });
54
+ observer.observe(el);
55
+ return () => observer.disconnect();
56
+ }
57
+
58
+ // src/react/mermaid-diagram.tsx
59
+ import { jsx, jsxs } from "react/jsx-runtime";
60
+ var queue = Promise.resolve();
61
+ async function renderSvg(id, source, theme) {
62
+ const { default: mermaid } = await import("mermaid");
63
+ mermaid.initialize({ startOnLoad: false, securityLevel: "strict", theme, maxTextSize: MAX_MERMAID_SOURCE, suppressErrorRendering: true });
64
+ try {
65
+ return (await mermaid.render(id, stripMermaidConfig(source))).svg;
66
+ } finally {
67
+ document.getElementById(`d${id}`)?.remove();
68
+ }
69
+ }
70
+ var THEME_ATTRS = ["data-act", "data-theme", "class"];
71
+ function MermaidDiagram({ source, title, errorText }) {
72
+ const figure = useRef(null);
73
+ const [theme, setTheme] = useState(null);
74
+ const [near, setNear] = useState(false);
75
+ const [svg, setSvg] = useState(null);
76
+ const [failed, setFailed] = useState(false);
77
+ useEffect(() => {
78
+ const scheme = window.matchMedia("(prefers-color-scheme: dark)");
79
+ const update = () => figure.current && setTheme(pickMermaidTheme(getComputedStyle(figure.current).backgroundColor, scheme.matches));
80
+ update();
81
+ const observer = new MutationObserver(update);
82
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: THEME_ATTRS });
83
+ scheme.addEventListener("change", update);
84
+ return () => {
85
+ observer.disconnect();
86
+ scheme.removeEventListener("change", update);
87
+ };
88
+ }, []);
89
+ useEffect(() => figure.current ? whenNearViewport(figure.current, () => setNear(true)) : void 0, []);
90
+ useEffect(() => {
91
+ if (theme === null || !near) return;
92
+ let current = true;
93
+ const id = nextMermaidId();
94
+ queue = queue.then(() => current ? withTimeout(renderSvg(id, source, theme), RENDER_TIMEOUT_MS) : void 0).then(
95
+ (out) => current && out !== void 0 && (setSvg(out), setFailed(false)),
96
+ () => current && setFailed(true)
97
+ );
98
+ return () => {
99
+ current = false;
100
+ };
101
+ }, [source, theme, near]);
102
+ return /* @__PURE__ */ jsxs("figure", { ref: figure, className: "hyle-mermaid", "data-slot-component": "Mermaid", children: [
103
+ svg === null ? /* @__PURE__ */ jsx("pre", { tabIndex: 0, children: /* @__PURE__ */ jsx("code", { className: "language-mermaid", children: source }) }) : /* @__PURE__ */ jsx("div", { className: "hyle-mermaid-svg", role: "img", "aria-label": title, dangerouslySetInnerHTML: { __html: svg } }),
104
+ failed ? /* @__PURE__ */ jsx("p", { className: "hyle-mermaid-note", role: "status", children: errorText }) : null,
105
+ /* @__PURE__ */ jsx("figcaption", { className: "sr-only", children: title })
106
+ ] });
107
+ }
108
+ export {
109
+ MermaidDiagram
110
+ };
package/dist/next.cjs CHANGED
@@ -352,6 +352,12 @@ function Icon({ name, className }) {
352
352
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: className ? `hyle-ic ${className}` : "hyle-ic", viewBox: "0 0 16 16", "aria-hidden": "true", children: SHAPES[name] });
353
353
  }
354
354
 
355
+ // src/react/content.tsx
356
+ var import_mermaid_diagram = require("./mermaid-diagram.cjs");
357
+
358
+ // src/react/mermaid-config.ts
359
+ var MAX_MERMAID_SOURCE = 2e4;
360
+
355
361
  // src/react/sanitize.ts
356
362
  var HastLimitError = class extends Error {
357
363
  constructor(message) {
@@ -410,9 +416,14 @@ var isHref = (v) => isSafeUrl(v, "href") && (typeof v !== "string" || !v.trim().
410
416
  var isSrc = (v) => isSafeUrl(v, "src");
411
417
  var MERMAID_SCRIPT_URL = "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.9.0/mermaid.min.js";
412
418
  var MERMAID_SRCDOC = new RegExp(
413
- `^<!doctype html><html><head><meta charset="utf-8"></head><body><div class="mermaid">[^<]*</div><script src="${MERMAID_SCRIPT_URL.replace(/[.]/g, "\\.")}"></script><script>mermaid\\.initialize\\(\\{ startOnLoad: true, securityLevel: "strict" \\}\\);</script></body></html>$`
419
+ `^<!doctype html><html><head><meta charset="utf-8"></head><body><div class="mermaid">([^<]*)</div><script src="${MERMAID_SCRIPT_URL.replace(/[.]/g, "\\.")}"></script><script>mermaid\\.initialize\\(\\{ startOnLoad: true, securityLevel: "strict" \\}\\);</script></body></html>$`
414
420
  );
415
421
  var isMermaidSrcdoc = (v) => typeof v === "string" && v.length <= MAX_SRCDOC && MERMAID_SRCDOC.test(v);
422
+ var ENTITIES = { "&#x3c;": "<", "&#x26;": "&", "&lt;": "<", "&gt;": ">", "&amp;": "&", "&quot;": '"', "&#x27;": "'" };
423
+ function mermaidSource(srcDoc) {
424
+ const text = MERMAID_SRCDOC.exec(srcDoc)?.[1]?.replace(/&(?:#x3c|#x26|#x27|lt|gt|amp|quot);/gi, (e) => ENTITIES[e.toLowerCase()]);
425
+ return text === void 0 || text.length > MAX_MERMAID_SOURCE ? void 0 : text;
426
+ }
416
427
  var MERMAID_FRAME_CSP = `default-src 'none'; script-src ${MERMAID_SCRIPT_URL} 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:`;
417
428
  var heading = { id: isIdValue, className: isClass };
418
429
  var ATTRS = new Map(
@@ -538,6 +549,7 @@ var en = {
538
549
  tip: "Tip",
539
550
  caution: "Caution",
540
551
  diagram: "Diagram",
552
+ diagramError: "This diagram could not be rendered.",
541
553
  contentError: "This content cannot be displayed.",
542
554
  fallbackTitle: "Not translated into {language} yet.",
543
555
  fallbackBody: "You are reading the original.",
@@ -577,6 +589,7 @@ var fr = {
577
589
  tip: "Astuce",
578
590
  caution: "Danger",
579
591
  diagram: "Sch\xE9ma",
592
+ diagramError: "Ce sch\xE9ma n'a pas pu \xEAtre affich\xE9.",
580
593
  contentError: "Ce contenu ne peut pas \xEAtre affich\xE9.",
581
594
  fallbackTitle: "Pas encore traduit en {language}.",
582
595
  fallbackBody: "Vous lisez l'original.",
@@ -616,6 +629,7 @@ var es = {
616
629
  tip: "Consejo",
617
630
  caution: "Peligro",
618
631
  diagram: "Diagrama",
632
+ diagramError: "No se pudo mostrar este diagrama.",
619
633
  contentError: "No se puede mostrar este contenido.",
620
634
  fallbackTitle: "A\xFAn no est\xE1 traducida al {language}.",
621
635
  fallbackBody: "Est\xE1s leyendo el original.",
@@ -703,7 +717,9 @@ function buildComponents(c, s) {
703
717
  };
704
718
  const Frame = ({ srcDoc, sandbox }) => {
705
719
  if (typeof srcDoc !== "string") return null;
706
- if (c.Mermaid) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(c.Mermaid, { srcDoc, title: s.diagram });
720
+ const source = mermaidSource(srcDoc);
721
+ if (c.Mermaid) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(c.Mermaid, { srcDoc, source, title: s.diagram });
722
+ if (source !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_mermaid_diagram.MermaidDiagram, { source, title: s.diagram, errorText: s.diagramError });
707
723
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("figure", { className: "hyle-mermaid", "data-slot-component": "Mermaid", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("iframe", { srcDoc, sandbox: String(sandbox), title: s.diagram, loading: "lazy", referrerPolicy: "no-referrer", ...{ csp: MERMAID_FRAME_CSP } }) });
708
724
  };
709
725
  const Heading = (level) => function HyleHeading({ id, children, ...rest }) {
@@ -743,6 +759,9 @@ function TrustedServerHtml({ html }) {
743
759
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { dangerouslySetInnerHTML: { __html: html } });
744
760
  }
745
761
 
762
+ // src/react/index.ts
763
+ var import_mermaid_diagram2 = require("./mermaid-diagram.cjs");
764
+
746
765
  // src/react/pages.tsx
747
766
  var import_jsx_runtime4 = require("react/jsx-runtime");
748
767
  function Slot({ name, slots, tag: Tag = "div", className }) {
package/dist/next.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactElement } from 'react';
2
- import { H as HyleComponents, a as HyleSlots } from './types-DLWvqanV.cjs';
2
+ import { H as HyleComponents, a as HyleSlots } from './types-D0qoLh4E.cjs';
3
3
  import { F as FetchResolverOptions, H as HyleResolver } from './resolve-B_p0p4Zn.cjs';
4
4
  export { f as fetchResolver, w as workspaceTags } from './resolve-B_p0p4Zn.cjs';
5
5
  import { MetadataRoute, Metadata } from 'next';
package/dist/next.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactElement } from 'react';
2
- import { H as HyleComponents, a as HyleSlots } from './types-BNaGecO7.js';
2
+ import { H as HyleComponents, a as HyleSlots } from './types-CBvLhUPr.js';
3
3
  import { F as FetchResolverOptions, H as HyleResolver } from './resolve-fMx2W8RR.js';
4
4
  export { f as fetchResolver, w as workspaceTags } from './resolve-fMx2W8RR.js';
5
5
  import { MetadataRoute, Metadata } from 'next';
package/dist/next.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  ArticlePage,
3
3
  DocsPage,
4
4
  ListPage
5
- } from "./chunk-MHVWHSKC.js";
5
+ } from "./chunk-EARQBSMF.js";
6
6
  import {
7
7
  fetchResolver,
8
8
  workspaceTags
@@ -14,10 +14,10 @@ import {
14
14
  } from "./chunk-HFUJRNFP.js";
15
15
 
16
16
  // src/next/page.tsx
17
- import { notFound, permanentRedirect, redirect } from "next/navigation.js";
17
+ import { notFound, permanentRedirect, redirect } from "next/navigation";
18
18
 
19
19
  // src/next/options.ts
20
- import { unstable_rethrow } from "next/navigation.js";
20
+ import { unstable_rethrow } from "next/navigation";
21
21
 
22
22
  // src/next/paths.ts
23
23
  function normalizeBasePath(basePath) {
@@ -185,7 +185,7 @@ function hyleMetadata(options) {
185
185
  }
186
186
 
187
187
  // src/next/revalidate.ts
188
- import { revalidateTag } from "next/cache.js";
188
+ import { revalidateTag } from "next/cache";
189
189
  var isSwitchedOff = (value) => ["0", "false"].includes(value?.trim().toLowerCase() ?? "");
190
190
  function createRevalidateRoute(options) {
191
191
  let cached;
package/dist/react.cjs CHANGED
@@ -25,6 +25,7 @@ __export(react_exports, {
25
25
  HastContent: () => HastContent,
26
26
  HastLimitError: () => HastLimitError,
27
27
  ListPage: () => ListPage,
28
+ MermaidDiagram: () => import_mermaid_diagram2.MermaidDiagram,
28
29
  NotFound: () => NotFound,
29
30
  Redirecting: () => Redirecting,
30
31
  TrustedServerHtml: () => TrustedServerHtml,
@@ -77,6 +78,12 @@ function Icon({ name, className }) {
77
78
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: className ? `hyle-ic ${className}` : "hyle-ic", viewBox: "0 0 16 16", "aria-hidden": "true", children: SHAPES[name] });
78
79
  }
79
80
 
81
+ // src/react/content.tsx
82
+ var import_mermaid_diagram = require("./mermaid-diagram.cjs");
83
+
84
+ // src/react/mermaid-config.ts
85
+ var MAX_MERMAID_SOURCE = 2e4;
86
+
80
87
  // src/react/sanitize.ts
81
88
  var HastLimitError = class extends Error {
82
89
  constructor(message) {
@@ -135,9 +142,14 @@ var isHref = (v) => isSafeUrl(v, "href") && (typeof v !== "string" || !v.trim().
135
142
  var isSrc = (v) => isSafeUrl(v, "src");
136
143
  var MERMAID_SCRIPT_URL = "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.9.0/mermaid.min.js";
137
144
  var MERMAID_SRCDOC = new RegExp(
138
- `^<!doctype html><html><head><meta charset="utf-8"></head><body><div class="mermaid">[^<]*</div><script src="${MERMAID_SCRIPT_URL.replace(/[.]/g, "\\.")}"></script><script>mermaid\\.initialize\\(\\{ startOnLoad: true, securityLevel: "strict" \\}\\);</script></body></html>$`
145
+ `^<!doctype html><html><head><meta charset="utf-8"></head><body><div class="mermaid">([^<]*)</div><script src="${MERMAID_SCRIPT_URL.replace(/[.]/g, "\\.")}"></script><script>mermaid\\.initialize\\(\\{ startOnLoad: true, securityLevel: "strict" \\}\\);</script></body></html>$`
139
146
  );
140
147
  var isMermaidSrcdoc = (v) => typeof v === "string" && v.length <= MAX_SRCDOC && MERMAID_SRCDOC.test(v);
148
+ var ENTITIES = { "&#x3c;": "<", "&#x26;": "&", "&lt;": "<", "&gt;": ">", "&amp;": "&", "&quot;": '"', "&#x27;": "'" };
149
+ function mermaidSource(srcDoc) {
150
+ const text = MERMAID_SRCDOC.exec(srcDoc)?.[1]?.replace(/&(?:#x3c|#x26|#x27|lt|gt|amp|quot);/gi, (e) => ENTITIES[e.toLowerCase()]);
151
+ return text === void 0 || text.length > MAX_MERMAID_SOURCE ? void 0 : text;
152
+ }
141
153
  var MERMAID_FRAME_CSP = `default-src 'none'; script-src ${MERMAID_SCRIPT_URL} 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:`;
142
154
  var heading = { id: isIdValue, className: isClass };
143
155
  var ATTRS = new Map(
@@ -263,6 +275,7 @@ var en = {
263
275
  tip: "Tip",
264
276
  caution: "Caution",
265
277
  diagram: "Diagram",
278
+ diagramError: "This diagram could not be rendered.",
266
279
  contentError: "This content cannot be displayed.",
267
280
  fallbackTitle: "Not translated into {language} yet.",
268
281
  fallbackBody: "You are reading the original.",
@@ -302,6 +315,7 @@ var fr = {
302
315
  tip: "Astuce",
303
316
  caution: "Danger",
304
317
  diagram: "Sch\xE9ma",
318
+ diagramError: "Ce sch\xE9ma n'a pas pu \xEAtre affich\xE9.",
305
319
  contentError: "Ce contenu ne peut pas \xEAtre affich\xE9.",
306
320
  fallbackTitle: "Pas encore traduit en {language}.",
307
321
  fallbackBody: "Vous lisez l'original.",
@@ -341,6 +355,7 @@ var es = {
341
355
  tip: "Consejo",
342
356
  caution: "Peligro",
343
357
  diagram: "Diagrama",
358
+ diagramError: "No se pudo mostrar este diagrama.",
344
359
  contentError: "No se puede mostrar este contenido.",
345
360
  fallbackTitle: "A\xFAn no est\xE1 traducida al {language}.",
346
361
  fallbackBody: "Est\xE1s leyendo el original.",
@@ -428,7 +443,9 @@ function buildComponents(c, s) {
428
443
  };
429
444
  const Frame = ({ srcDoc, sandbox }) => {
430
445
  if (typeof srcDoc !== "string") return null;
431
- if (c.Mermaid) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(c.Mermaid, { srcDoc, title: s.diagram });
446
+ const source = mermaidSource(srcDoc);
447
+ if (c.Mermaid) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(c.Mermaid, { srcDoc, source, title: s.diagram });
448
+ if (source !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_mermaid_diagram.MermaidDiagram, { source, title: s.diagram, errorText: s.diagramError });
432
449
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("figure", { className: "hyle-mermaid", "data-slot-component": "Mermaid", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("iframe", { srcDoc, sandbox: String(sandbox), title: s.diagram, loading: "lazy", referrerPolicy: "no-referrer", ...{ csp: MERMAID_FRAME_CSP } }) });
433
450
  };
434
451
  const Heading = (level) => function HyleHeading({ id, children, ...rest }) {
@@ -468,6 +485,9 @@ function TrustedServerHtml({ html }) {
468
485
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { dangerouslySetInnerHTML: { __html: html } });
469
486
  }
470
487
 
488
+ // src/react/index.ts
489
+ var import_mermaid_diagram2 = require("./mermaid-diagram.cjs");
490
+
471
491
  // src/react/pages.tsx
472
492
  var import_jsx_runtime4 = require("react/jsx-runtime");
473
493
  function Slot({ name, slots, tag: Tag = "div", className }) {
@@ -763,6 +783,7 @@ function Redirecting({ from, to, ...base }) {
763
783
  HastContent,
764
784
  HastLimitError,
765
785
  ListPage,
786
+ MermaidDiagram,
766
787
  NotFound,
767
788
  Redirecting,
768
789
  TrustedServerHtml,
package/dist/react.d.cts CHANGED
@@ -1,6 +1,7 @@
1
+ import * as react from 'react';
1
2
  import { ReactElement } from 'react';
2
- import { H as HyleComponents, P as PageBaseProps } from './types-DLWvqanV.cjs';
3
- export { C as CalloutKind, b as CalloutProps, c as CodeBlockProps, a as HyleSlots, I as ImageProps, L as LinkProps, d as ListItemProps, M as MermaidProps } from './types-DLWvqanV.cjs';
3
+ import { H as HyleComponents, P as PageBaseProps } from './types-D0qoLh4E.cjs';
4
+ export { C as CalloutKind, b as CalloutProps, c as CodeBlockProps, a as HyleSlots, I as ImageProps, L as LinkProps, d as ListItemProps, M as MermaidProps } from './types-D0qoLh4E.cjs';
4
5
  import { Root } from 'hast';
5
6
  import { b as ResolvedArticle, d as ResolvedList } from './types-DjXf6iPo.cjs';
6
7
 
@@ -23,6 +24,20 @@ declare function TrustedServerHtml({ html }: {
23
24
  html: string;
24
25
  }): ReactElement;
25
26
 
27
+ /**
28
+ * The SDK's only client component. It receives the diagram text and presentational strings and nothing else: never a token,
29
+ * base URL or client (a test pins this). Mermaid is a dynamic import, so the chunk loads only on pages that have a diagram,
30
+ * from the host's own origin.
31
+ */
32
+ interface MermaidDiagramProps {
33
+ source: string;
34
+ /** Accessible name of the diagram. */
35
+ title: string;
36
+ /** Shown when Mermaid cannot render the source. */
37
+ errorText: string;
38
+ }
39
+ declare function MermaidDiagram({ source, title, errorText }: MermaidDiagramProps): react.JSX.Element;
40
+
26
41
  /**
27
42
  * Defense in depth. hyle sanitizes server-side (packages/renderer/src/render.ts); this guard repeats the same allowlist
28
43
  * so a compromised or buggy response still cannot put script, event handlers or `javascript:` URLs on the page. It
@@ -91,6 +106,7 @@ declare const en: {
91
106
  tip: string;
92
107
  caution: string;
93
108
  diagram: string;
109
+ diagramError: string;
94
110
  contentError: string;
95
111
  fallbackTitle: string;
96
112
  fallbackBody: string;
@@ -118,4 +134,4 @@ type Strings = typeof en;
118
134
  /** Strings for a BCP 47 tag; the primary subtag decides, anything unknown falls back to English. */
119
135
  declare function stringsFor(locale: string | undefined): Strings;
120
136
 
121
- export { ArticlePage, type ArticlePageProps, DocsPage, HastContent, type HastContentProps, HastLimitError, HyleComponents, ListPage, type ListPageProps, NotFound, type NotFoundProps, PageBaseProps, Redirecting, type RedirectingProps, type Strings, TrustedServerHtml, Unavailable, isSafeUrl, sanitizeHast, scopeId, stringsFor };
137
+ export { ArticlePage, type ArticlePageProps, DocsPage, HastContent, type HastContentProps, HastLimitError, HyleComponents, ListPage, type ListPageProps, MermaidDiagram, type MermaidDiagramProps, NotFound, type NotFoundProps, PageBaseProps, Redirecting, type RedirectingProps, type Strings, TrustedServerHtml, Unavailable, isSafeUrl, sanitizeHast, scopeId, stringsFor };
package/dist/react.d.ts CHANGED
@@ -1,6 +1,7 @@
1
+ import * as react from 'react';
1
2
  import { ReactElement } from 'react';
2
- import { H as HyleComponents, P as PageBaseProps } from './types-BNaGecO7.js';
3
- export { C as CalloutKind, b as CalloutProps, c as CodeBlockProps, a as HyleSlots, I as ImageProps, L as LinkProps, d as ListItemProps, M as MermaidProps } from './types-BNaGecO7.js';
3
+ import { H as HyleComponents, P as PageBaseProps } from './types-CBvLhUPr.js';
4
+ export { C as CalloutKind, b as CalloutProps, c as CodeBlockProps, a as HyleSlots, I as ImageProps, L as LinkProps, d as ListItemProps, M as MermaidProps } from './types-CBvLhUPr.js';
4
5
  import { Root } from 'hast';
5
6
  import { b as ResolvedArticle, d as ResolvedList } from './types-DjXf6iPo.js';
6
7
 
@@ -23,6 +24,20 @@ declare function TrustedServerHtml({ html }: {
23
24
  html: string;
24
25
  }): ReactElement;
25
26
 
27
+ /**
28
+ * The SDK's only client component. It receives the diagram text and presentational strings and nothing else: never a token,
29
+ * base URL or client (a test pins this). Mermaid is a dynamic import, so the chunk loads only on pages that have a diagram,
30
+ * from the host's own origin.
31
+ */
32
+ interface MermaidDiagramProps {
33
+ source: string;
34
+ /** Accessible name of the diagram. */
35
+ title: string;
36
+ /** Shown when Mermaid cannot render the source. */
37
+ errorText: string;
38
+ }
39
+ declare function MermaidDiagram({ source, title, errorText }: MermaidDiagramProps): react.JSX.Element;
40
+
26
41
  /**
27
42
  * Defense in depth. hyle sanitizes server-side (packages/renderer/src/render.ts); this guard repeats the same allowlist
28
43
  * so a compromised or buggy response still cannot put script, event handlers or `javascript:` URLs on the page. It
@@ -91,6 +106,7 @@ declare const en: {
91
106
  tip: string;
92
107
  caution: string;
93
108
  diagram: string;
109
+ diagramError: string;
94
110
  contentError: string;
95
111
  fallbackTitle: string;
96
112
  fallbackBody: string;
@@ -118,4 +134,4 @@ type Strings = typeof en;
118
134
  /** Strings for a BCP 47 tag; the primary subtag decides, anything unknown falls back to English. */
119
135
  declare function stringsFor(locale: string | undefined): Strings;
120
136
 
121
- export { ArticlePage, type ArticlePageProps, DocsPage, HastContent, type HastContentProps, HastLimitError, HyleComponents, ListPage, type ListPageProps, NotFound, type NotFoundProps, PageBaseProps, Redirecting, type RedirectingProps, type Strings, TrustedServerHtml, Unavailable, isSafeUrl, sanitizeHast, scopeId, stringsFor };
137
+ export { ArticlePage, type ArticlePageProps, DocsPage, HastContent, type HastContentProps, HastLimitError, HyleComponents, ListPage, type ListPageProps, MermaidDiagram, type MermaidDiagramProps, NotFound, type NotFoundProps, PageBaseProps, Redirecting, type RedirectingProps, type Strings, TrustedServerHtml, Unavailable, isSafeUrl, sanitizeHast, scopeId, stringsFor };
package/dist/react.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  HastContent,
5
5
  HastLimitError,
6
6
  ListPage,
7
+ MermaidDiagram,
7
8
  NotFound,
8
9
  Redirecting,
9
10
  TrustedServerHtml,
@@ -12,13 +13,14 @@ import {
12
13
  sanitizeHast,
13
14
  scopeId,
14
15
  stringsFor
15
- } from "./chunk-MHVWHSKC.js";
16
+ } from "./chunk-EARQBSMF.js";
16
17
  export {
17
18
  ArticlePage,
18
19
  DocsPage,
19
20
  HastContent,
20
21
  HastLimitError,
21
22
  ListPage,
23
+ MermaidDiagram,
22
24
  NotFound,
23
25
  Redirecting,
24
26
  TrustedServerHtml,
package/dist/styles.css CHANGED
@@ -355,7 +355,7 @@
355
355
  .hyle-callout .bd{font-size:.9375em;line-height:1.6}
356
356
 
357
357
  /* mermaid placeholder frame */
358
- .hyle-mermaid{margin-inline:0;border:var(--h-border-width) dashed var(--h-border);border-radius:var(--h-radius);background:var(--h-surface);overflow:hidden}
358
+ .hyle-mermaid{margin-inline:0;border:var(--h-border-width) dashed var(--h-border);border-radius:var(--h-radius);background:var(--h-surface);overflow-x:auto}
359
359
  .hyle-mermaid .hyle-stage{display:flex;align-items:center;justify-content:center;gap:0;padding:32px 16px;flex-wrap:wrap;row-gap:12px}
360
360
  .hyle-mermaid .node{display:grid;place-items:center;min-width:104px;height:40px;padding:0 12px;border:var(--h-border-width) solid var(--h-border);border-radius:min(var(--h-radius),8px);background:var(--h-surface-2);font:500 13px/1 var(--h-font-mono);color:var(--h-text-muted)}
361
361
  .hyle-mermaid .edge{width:32px;height:1px;background:var(--h-border);position:relative}
@@ -401,3 +401,8 @@
401
401
  /* ---- additions not in the design kit (the kit mocks these; the real hast needs them) ---- */
402
402
  .hyle .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
403
403
  .hyle-mermaid iframe{display:block;width:100%;min-height:240px;border:0;background:transparent}
404
+ .hyle-mermaid>pre{padding:16px}
405
+ /* contain:paint clips the SVG and makes it the containing block of any absolute or fixed descendant, so diagram CSS cannot overlay the host page */
406
+ .hyle-mermaid-svg{display:flex;justify-content:center;padding:16px;contain:paint}
407
+ .hyle-mermaid-svg svg{position:static!important;max-width:100%;height:auto}
408
+ .hyle-mermaid-note{margin:0;padding:8px 16px;border-top:1px dashed var(--h-border);font-size:13px;color:var(--h-warn)}
@@ -23,6 +23,8 @@ interface CodeBlockProps {
23
23
  interface MermaidProps {
24
24
  /** The sandboxed document hyle produced for the diagram. Render it only inside an iframe with `sandbox="allow-scripts"`. */
25
25
  srcDoc: string;
26
+ /** The diagram text, extracted from `srcDoc` (at most 20,000 characters); undefined when it could not be extracted. */
27
+ source: string | undefined;
26
28
  title: string;
27
29
  }
28
30
  interface CalloutProps {
@@ -23,6 +23,8 @@ interface CodeBlockProps {
23
23
  interface MermaidProps {
24
24
  /** The sandboxed document hyle produced for the diagram. Render it only inside an iframe with `sandbox="allow-scripts"`. */
25
25
  srcDoc: string;
26
+ /** The diagram text, extracted from `srcDoc` (at most 20,000 characters); undefined when it could not be extracted. */
27
+ source: string | undefined;
26
28
  title: string;
27
29
  }
28
30
  interface CalloutProps {
package/dist/use-cache.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-HFUJRNFP.js";
8
8
 
9
9
  // src/next/use-cache.ts
10
- import { cacheLife, cacheTag } from "next/cache.js";
10
+ import { cacheLife, cacheTag } from "next/cache";
11
11
  var MAX_TAGS = 128;
12
12
  var DISABLED_REVALIDATE = 30;
13
13
  var tokens = /* @__PURE__ */ new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tdacorp/hyle-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "hyle SDK: a typed client for hyle's resolve endpoint, a hast renderer for React, and a Next.js catch-all page with tag-based revalidation.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -71,7 +71,8 @@
71
71
  "LICENSE"
72
72
  ],
73
73
  "dependencies": {
74
- "hast-util-to-jsx-runtime": "^2.3.6"
74
+ "hast-util-to-jsx-runtime": "^2.3.6",
75
+ "mermaid": "~11.9.0"
75
76
  },
76
77
  "peerDependencies": {
77
78
  "next": ">=16",
@@ -106,7 +107,7 @@
106
107
  "tdacorp"
107
108
  ],
108
109
  "scripts": {
109
- "build": "tsup && cp src/styles.css dist/styles.css",
110
+ "build": "rimraf dist && tsup && cp src/styles.css dist/styles.css",
110
111
  "clean": "rimraf dist",
111
112
  "test": "vitest run",
112
113
  "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",