@tdacorp/hyle-sdk 0.2.1 → 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 +14 -2
- package/dist/{chunk-MHVWHSKC.js → chunk-EARQBSMF.js} +23 -3
- package/dist/mermaid-diagram.cjs +144 -0
- package/dist/mermaid-diagram.d.cts +17 -0
- package/dist/mermaid-diagram.d.ts +17 -0
- package/dist/mermaid-diagram.js +110 -0
- package/dist/next.cjs +21 -2
- package/dist/next.d.cts +1 -1
- package/dist/next.d.ts +1 -1
- package/dist/next.js +1 -1
- package/dist/react.cjs +23 -2
- package/dist/react.d.cts +19 -3
- package/dist/react.d.ts +19 -3
- package/dist/react.js +3 -1
- package/dist/styles.css +6 -1
- package/dist/{types-BNaGecO7.d.ts → types-CBvLhUPr.d.ts} +2 -0
- package/dist/{types-DLWvqanV.d.cts → types-D0qoLh4E.d.cts} +2 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -135,6 +135,17 @@ Verify after `next build`: `.next/prerender-manifest.json` must not show `"fallb
|
|
|
135
135
|
|
|
136
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.
|
|
137
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
|
+
|
|
138
149
|
## Styling
|
|
139
150
|
|
|
140
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:
|
|
@@ -149,7 +160,7 @@ The 52 variables are listed in the token block at the top of `styles.css`. Layou
|
|
|
149
160
|
|
|
150
161
|
`components` replaces how a part is drawn, `slots` fills named places. Both are props of the pages and options of `hylePage`.
|
|
151
162
|
|
|
152
|
-
- 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.
|
|
153
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.
|
|
154
165
|
- Layout: `layout: "docs"` renders articles as `DocsPage` (tree navigation from `ancestors`, `children` and `breadcrumbs`).
|
|
155
166
|
|
|
@@ -159,7 +170,8 @@ Chrome strings exist for en, fr and es (fr and es are unreviewed drafts). The pa
|
|
|
159
170
|
|
|
160
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).
|
|
161
172
|
- The path is sent as a query parameter (encoded once); catch-all segments are decoded and refused if they contain `/`, `\`, control characters, `.` or `..`.
|
|
162
|
-
- 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
|
|
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'`.
|
|
163
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.
|
|
164
176
|
|
|
165
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">[^<]
|
|
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 = { "<": "<", "&": "&", "<": "<", ">": ">", "&": "&", """: '"', "'": "'" };
|
|
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
|
-
|
|
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">[^<]
|
|
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 = { "<": "<", "&": "&", "<": "<", ">": ">", "&": "&", """: '"', "'": "'" };
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
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">[^<]
|
|
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 = { "<": "<", "&": "&", "<": "<", ">": ">", "&": "&", """: '"', "'": "'" };
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tdacorp/hyle-sdk",
|
|
3
|
-
"version": "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",
|