@petrarca/sonnet-ui 0.8.0 → 0.9.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 +2 -0
- package/dist/chunk-JDD3LLTR.js +239 -0
- package/dist/chunk-JDD3LLTR.js.map +1 -0
- package/dist/chunk-ZQAHSGOS.js +177 -0
- package/dist/chunk-ZQAHSGOS.js.map +1 -0
- package/dist/code-editor/index.d.ts +76 -0
- package/dist/code-editor/index.js +13 -0
- package/dist/code-editor/index.js.map +1 -0
- package/dist/index.d.ts +488 -67
- package/dist/index.js +2139 -1054
- package/dist/index.js.map +1 -1
- package/dist/json-editor/index.d.ts +12 -14
- package/dist/json-editor/index.js +35 -56
- package/dist/json-editor/index.js.map +1 -1
- package/dist/markdown/index.d.ts +67 -0
- package/dist/markdown/index.js +237 -0
- package/dist/markdown/index.js.map +1 -0
- package/package.json +40 -23
- package/styles.css +46 -0
- package/tailwind-content.cjs +30 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as _codemirror_state from '@codemirror/state';
|
|
1
|
+
import React__default from 'react';
|
|
3
2
|
import { Extension } from '@codemirror/state';
|
|
4
3
|
|
|
5
4
|
interface JsonEditorProps {
|
|
@@ -39,23 +38,22 @@ interface JsonEditorProps {
|
|
|
39
38
|
* the document the user can modify. Everything else is visible but
|
|
40
39
|
* locked. Changes outside editable regions are silently rejected.
|
|
41
40
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* override styling for consumers that do not use
|
|
41
|
+
* A CodeEditor with the JSON language and the locking extensions. The default
|
|
42
|
+
* theme reads CSS variables from @petrarca/sonnet-ui/styles.css; pass a custom
|
|
43
|
+
* `theme` extension to override styling for consumers that do not use
|
|
44
|
+
* sonnet-ui tokens.
|
|
45
45
|
*/
|
|
46
|
-
declare function JsonEditor({ value, editablePaths, onChange, height, className, theme, fillContainer, }: JsonEditorProps):
|
|
46
|
+
declare function JsonEditor({ value, editablePaths, onChange, height, className, theme, fillContainer, }: JsonEditorProps): React__default.JSX.Element;
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Default
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* and --state-selected. Each holds a bare HSL triple, so every reference has
|
|
53
|
-
* to be wrapped in hsl() -- `var(--border)` on its own is not a colour and the
|
|
54
|
-
* declaration is dropped.
|
|
49
|
+
* Default theme for JsonEditor: the CodeEditor look (design tokens from
|
|
50
|
+
* @petrarca/sonnet-ui/styles.css), plus the JSON editor's own marks -- the
|
|
51
|
+
* locked regions, and no active-line band (it would read as a lock).
|
|
55
52
|
*
|
|
56
53
|
* Consumers not using sonnet-ui tokens can pass their own Extension
|
|
57
54
|
* via the JsonEditor `theme` prop to override this entirely.
|
|
58
55
|
*/
|
|
59
|
-
declare const defaultTheme:
|
|
56
|
+
declare const defaultTheme: Extension;
|
|
60
57
|
|
|
61
|
-
export { JsonEditor,
|
|
58
|
+
export { JsonEditor, defaultTheme };
|
|
59
|
+
export type { JsonEditorProps };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CodeEditor,
|
|
3
|
+
codeTheme
|
|
4
|
+
} from "../chunk-JDD3LLTR.js";
|
|
5
|
+
|
|
1
6
|
// src/JsonEditor/JsonEditor.tsx
|
|
2
|
-
import {
|
|
3
|
-
import CodeMirror from "@uiw/react-codemirror";
|
|
7
|
+
import { useMemo } from "react";
|
|
4
8
|
import { json } from "@codemirror/lang-json";
|
|
5
9
|
import { EditorState, StateField } from "@codemirror/state";
|
|
6
10
|
import { EditorView as EditorView2, Decoration } from "@codemirror/view";
|
|
@@ -37,33 +41,24 @@ function computeReadonlyRanges(editableRanges, docLength) {
|
|
|
37
41
|
|
|
38
42
|
// src/JsonEditor/defaultTheme.ts
|
|
39
43
|
import { EditorView } from "@codemirror/view";
|
|
40
|
-
var defaultTheme =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
border: "none"
|
|
56
|
-
},
|
|
57
|
-
".cm-activeLine": {
|
|
58
|
-
backgroundColor: "transparent"
|
|
59
|
-
},
|
|
60
|
-
".cm-activeLineGutter": {
|
|
61
|
-
backgroundColor: "transparent"
|
|
62
|
-
}
|
|
63
|
-
});
|
|
44
|
+
var defaultTheme = [
|
|
45
|
+
codeTheme,
|
|
46
|
+
EditorView.theme({
|
|
47
|
+
".cm-readonly-region": {
|
|
48
|
+
backgroundColor: "hsl(var(--state-selected) / 0.5)",
|
|
49
|
+
opacity: "0.7"
|
|
50
|
+
},
|
|
51
|
+
".cm-activeLine": {
|
|
52
|
+
backgroundColor: "transparent"
|
|
53
|
+
},
|
|
54
|
+
".cm-activeLineGutter": {
|
|
55
|
+
backgroundColor: "transparent"
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
];
|
|
64
59
|
|
|
65
60
|
// src/JsonEditor/JsonEditor.tsx
|
|
66
|
-
import { jsx
|
|
61
|
+
import { jsx } from "react/jsx-runtime";
|
|
67
62
|
var readonlyMark = Decoration.mark({ class: "cm-readonly-region" });
|
|
68
63
|
function buildEditableExtensions(editablePaths) {
|
|
69
64
|
const changeFilter = EditorState.changeFilter.of((tr) => {
|
|
@@ -113,38 +108,22 @@ function JsonEditor({
|
|
|
113
108
|
}) {
|
|
114
109
|
const joinedPaths = editablePaths.join(",");
|
|
115
110
|
const extensions = useMemo(
|
|
116
|
-
() => [json(),
|
|
117
|
-
[joinedPaths
|
|
111
|
+
() => [json(), ...buildEditableExtensions(editablePaths)],
|
|
112
|
+
[joinedPaths]
|
|
118
113
|
// eslint-disable-line react-hooks/exhaustive-deps
|
|
119
114
|
);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
return /* @__PURE__ */ jsx(
|
|
116
|
+
CodeEditor,
|
|
117
|
+
{
|
|
118
|
+
value,
|
|
119
|
+
onChange,
|
|
120
|
+
height,
|
|
121
|
+
fillContainer,
|
|
122
|
+
theme,
|
|
123
|
+
extensions,
|
|
124
|
+
className
|
|
125
|
+
}
|
|
125
126
|
);
|
|
126
|
-
const effectiveHeight = fillContainer ? "100%" : height;
|
|
127
|
-
const wrapperStyle = fillContainer ? { height: "100%" } : void 0;
|
|
128
|
-
const wrapperClass = fillContainer ? `json-editor--fill${className ? ` ${className}` : ""}` : className;
|
|
129
|
-
return /* @__PURE__ */ jsxs("div", { className: wrapperClass, style: wrapperStyle, children: [
|
|
130
|
-
fillContainer && /* @__PURE__ */ jsx("style", { children: ".json-editor--fill > .cm-theme-light { height: 100%; }" }),
|
|
131
|
-
/* @__PURE__ */ jsx(
|
|
132
|
-
CodeMirror,
|
|
133
|
-
{
|
|
134
|
-
value,
|
|
135
|
-
height: effectiveHeight,
|
|
136
|
-
extensions,
|
|
137
|
-
onChange: handleChange,
|
|
138
|
-
basicSetup: {
|
|
139
|
-
lineNumbers: true,
|
|
140
|
-
foldGutter: true,
|
|
141
|
-
bracketMatching: true,
|
|
142
|
-
closeBrackets: true,
|
|
143
|
-
indentOnInput: true
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
)
|
|
147
|
-
] });
|
|
148
127
|
}
|
|
149
128
|
export {
|
|
150
129
|
JsonEditor,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/JsonEditor/JsonEditor.tsx","../../src/JsonEditor/readonlyRanges.ts","../../src/JsonEditor/defaultTheme.ts"],"sourcesContent":["import React, { useCallback, useMemo } from \"react\";\nimport CodeMirror from \"@uiw/react-codemirror\";\nimport { json } from \"@codemirror/lang-json\";\nimport { EditorState, StateField, type Extension } from \"@codemirror/state\";\nimport { EditorView, Decoration, type DecorationSet } from \"@codemirror/view\";\nimport { resolvePointerRanges } from \"@petrarca/sonnet-core\";\nimport { computeReadonlyRanges } from \"./readonlyRanges\";\nimport { defaultTheme } from \"./defaultTheme\";\n\nexport interface JsonEditorProps {\n /** The JSON string to display and edit. */\n value: string;\n /**\n * JSON Pointer (RFC 6901) paths whose values are editable.\n * Everything outside these paths is readonly.\n * Example: [\"/metadata\", \"/definitions/Patient/description\"]\n */\n editablePaths: string[];\n /** Called when the document changes (only editable regions can change). */\n onChange?: (value: string) => void;\n /** Editor height. Defaults to \"400px\". */\n height?: string;\n /** Additional CSS class on the wrapper. */\n className?: string;\n /**\n * Make the editor fill its container's height. Sets height=\"100%\" and\n * propagates the fill through CodeMirror's internal wrappers so the editor\n * scrolls internally. Use when the container has an explicit height (e.g.\n * `absolute inset-0` or `flex-1`). Prefer this over external `<style>` rules\n * targeting CodeMirror's internal class names.\n */\n fillContainer?: boolean;\n /**\n * CodeMirror theme extension. Defaults to a built-in theme that reads\n * CSS variables (--border, --font-mono, --muted) from the host app.\n * Pass your own Extension to override styling entirely.\n */\n theme?: Extension;\n}\n\n// Decoration mark for readonly regions (subtle blue background)\nconst readonlyMark = Decoration.mark({ class: \"cm-readonly-region\" });\n\n/**\n * Build CodeMirror extensions that enforce editable regions:\n * - changeFilter: suppresses changes outside editable ranges\n * - StateField: decorates readonly regions with a visual style\n */\nfunction buildEditableExtensions(editablePaths: string[]) {\n // changeFilter: recompute editable ranges from the live document on every\n // transaction so offsets remain accurate as the user types. Using a snapshot\n // from mount time would cause ranges to drift and incorrectly block edits.\n const changeFilter = EditorState.changeFilter.of((tr) => {\n const currentDoc = tr.startState.doc.toString();\n const editableRanges = resolvePointerRanges(currentDoc, editablePaths);\n const readonlyRanges = computeReadonlyRanges(\n editableRanges,\n tr.startState.doc.length,\n );\n // Empty readonly list means the whole document is editable -- return true\n // to allow all changes. An empty array would block everything.\n if (readonlyRanges.length === 0) return true;\n return readonlyRanges.flatMap((r) => [r.from, r.to]);\n });\n\n // Decorations: mark readonly regions with a subtle background.\n // Recomputed from the live document on every change so highlights stay\n // accurate as content shifts.\n const readonlyDecorations = StateField.define<DecorationSet>({\n create(state) {\n const editableRanges = resolvePointerRanges(\n state.doc.toString(),\n editablePaths,\n );\n const ranges = computeReadonlyRanges(editableRanges, state.doc.length);\n return Decoration.set(\n ranges.map((r) => readonlyMark.range(r.from, r.to)),\n );\n },\n update(deco, tr) {\n if (!tr.docChanged) return deco;\n const editableRanges = resolvePointerRanges(\n tr.state.doc.toString(),\n editablePaths,\n );\n const ranges = computeReadonlyRanges(editableRanges, tr.state.doc.length);\n return Decoration.set(\n ranges.map((r) => readonlyMark.range(r.from, r.to)),\n );\n },\n provide: (field) => EditorView.decorations.from(field),\n });\n\n return [changeFilter, readonlyDecorations];\n}\n\n/**\n * JsonEditor - A JSON text editor with selectively editable regions.\n *\n * Pass JSON Pointer paths to `editablePaths` to mark which parts of\n * the document the user can modify. Everything else is visible but\n * locked. Changes outside editable regions are silently rejected.\n *\n * The default theme reads CSS variables from @petrarca/sonnet-ui/styles.css\n * (--border, --font-mono, --muted). Pass a custom `theme` extension to\n * override styling for consumers that do not use sonnet-ui tokens.\n */\nexport default function JsonEditor({\n value,\n editablePaths,\n onChange,\n height = \"400px\",\n className,\n theme = defaultTheme,\n fillContainer = false,\n}: JsonEditorProps) {\n // Ranges are structural: recompute only when the set of editable paths\n // or the theme changes, not on every keystroke. When paths change, use the\n // current value so ranges are computed against what is actually in the editor.\n const joinedPaths = editablePaths.join(\",\");\n\n const extensions = useMemo(\n () => [json(), theme, ...buildEditableExtensions(editablePaths)],\n [joinedPaths, theme], // eslint-disable-line react-hooks/exhaustive-deps\n );\n\n const handleChange = useCallback(\n (val: string) => {\n onChange?.(val);\n },\n [onChange],\n );\n\n const effectiveHeight = fillContainer ? \"100%\" : height;\n // When fillContainer=true the wrapper div needs an explicit height so the\n // \"100%\" value on CodeMirror resolves against the container. However\n // @uiw/react-codemirror does not propagate a percentage height to its\n // internal .cm-theme-light wrapper, so we also set that explicitly via a\n // scoped <style> that only targets children of the .json-editor--fill class.\n const wrapperStyle = fillContainer\n ? ({ height: \"100%\" } as React.CSSProperties)\n : undefined;\n const wrapperClass = fillContainer\n ? `json-editor--fill${className ? ` ${className}` : \"\"}`\n : className;\n\n return (\n <div className={wrapperClass} style={wrapperStyle}>\n {fillContainer && (\n <style>\n {\".json-editor--fill > .cm-theme-light { height: 100%; }\"}\n </style>\n )}\n <CodeMirror\n value={value}\n height={effectiveHeight}\n extensions={extensions}\n onChange={handleChange}\n basicSetup={{\n lineNumbers: true,\n foldGutter: true,\n bracketMatching: true,\n closeBrackets: true,\n indentOnInput: true,\n }}\n />\n </div>\n );\n}\n","/**\n * Editable-region maths for JsonEditor.\n *\n * Pure functions, kept out of the component file so it exports only components\n * (Fast Refresh) and so the inversion is directly testable. Not re-exported\n * from the package root -- these are internals, like the table's column\n * resolver.\n */\n\nimport type { OffsetRange } from \"@petrarca/sonnet-core\";\n\n/**\n * Normalize ranges into a sorted, non-overlapping set.\n *\n * Inversion walks a single cursor forward, so it is only correct for input that\n * is already sorted and disjoint. Neither holds in general: a caller may pass a\n * pointer together with one nested inside it -- `/metadata` and\n * `/metadata/version` -- which is a reasonable way to say \"this object is\n * editable, and so is this field within it\". Merging first makes the inversion\n * correct for any input rather than for input that happens to satisfy an\n * unstated precondition.\n */\nfunction mergeRanges(ranges: OffsetRange[]): { from: number; to: number }[] {\n const sorted = [...ranges].sort((a, b) => a.from - b.from);\n const merged: { from: number; to: number }[] = [];\n for (const range of sorted) {\n const last = merged[merged.length - 1];\n // `<=` also folds ranges that merely touch, so the inversion emits no\n // zero-width readonly slivers between them.\n if (last && range.from <= last.to) {\n last.to = Math.max(last.to, range.to);\n } else {\n merged.push({ from: range.from, to: range.to });\n }\n }\n return merged;\n}\n\n/**\n * Compute readonly ranges from editable ranges (inversion against doc length).\n * Used by both the changeFilter and the decoration builder.\n */\nexport function computeReadonlyRanges(\n editableRanges: OffsetRange[],\n docLength: number,\n): { from: number; to: number }[] {\n const ranges: { from: number; to: number }[] = [];\n let cursor = 0;\n for (const range of mergeRanges(editableRanges)) {\n if (cursor < range.from) {\n ranges.push({ from: cursor, to: range.from });\n }\n cursor = range.to;\n }\n if (cursor < docLength) {\n ranges.push({ from: cursor, to: docLength });\n }\n return ranges;\n}\n","import { EditorView } from \"@codemirror/view\";\n\n/**\n * Default CodeMirror theme for JsonEditor.\n *\n * Reads design tokens from @petrarca/sonnet-ui/styles.css: --border, --muted\n * and --state-selected. Each holds a bare HSL triple, so every reference has\n * to be wrapped in hsl() -- `var(--border)` on its own is not a colour and the\n * declaration is dropped.\n *\n * Consumers not using sonnet-ui tokens can pass their own Extension\n * via the JsonEditor `theme` prop to override this entirely.\n */\nexport const defaultTheme = EditorView.theme({\n \"&\": {\n fontSize: \"13px\",\n border: \"1px solid hsl(var(--border))\",\n borderRadius: \"6px\",\n },\n \".cm-content\": {\n fontFamily: \"'JetBrains Mono', ui-monospace, SFMono-Regular, monospace\",\n },\n \".cm-readonly-region\": {\n backgroundColor: \"hsl(var(--state-selected) / 0.5)\",\n opacity: \"0.7\",\n },\n \".cm-gutters\": {\n backgroundColor: \"hsl(var(--muted) / 0.3)\",\n border: \"none\",\n },\n \".cm-activeLine\": {\n backgroundColor: \"transparent\",\n },\n \".cm-activeLineGutter\": {\n backgroundColor: \"transparent\",\n },\n});\n"],"mappings":"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","names":["EditorView","EditorView"]}
|
|
1
|
+
{"version":3,"sources":["../../src/JsonEditor/JsonEditor.tsx","../../src/JsonEditor/readonlyRanges.ts","../../src/JsonEditor/defaultTheme.ts"],"sourcesContent":["import React, { useMemo } from \"react\";\nimport { json } from \"@codemirror/lang-json\";\nimport { EditorState, StateField, type Extension } from \"@codemirror/state\";\nimport { EditorView, Decoration, type DecorationSet } from \"@codemirror/view\";\nimport { resolvePointerRanges } from \"@petrarca/sonnet-core\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport { computeReadonlyRanges } from \"./readonlyRanges\";\nimport { defaultTheme } from \"./defaultTheme\";\n\nexport interface JsonEditorProps {\n /** The JSON string to display and edit. */\n value: string;\n /**\n * JSON Pointer (RFC 6901) paths whose values are editable.\n * Everything outside these paths is readonly.\n * Example: [\"/metadata\", \"/definitions/Patient/description\"]\n */\n editablePaths: string[];\n /** Called when the document changes (only editable regions can change). */\n onChange?: (value: string) => void;\n /** Editor height. Defaults to \"400px\". */\n height?: string;\n /** Additional CSS class on the wrapper. */\n className?: string;\n /**\n * Make the editor fill its container's height. Sets height=\"100%\" and\n * propagates the fill through CodeMirror's internal wrappers so the editor\n * scrolls internally. Use when the container has an explicit height (e.g.\n * `absolute inset-0` or `flex-1`). Prefer this over external `<style>` rules\n * targeting CodeMirror's internal class names.\n */\n fillContainer?: boolean;\n /**\n * CodeMirror theme extension. Defaults to a built-in theme that reads\n * CSS variables (--border, --font-mono, --muted) from the host app.\n * Pass your own Extension to override styling entirely.\n */\n theme?: Extension;\n}\n\n// Decoration mark for readonly regions (subtle blue background)\nconst readonlyMark = Decoration.mark({ class: \"cm-readonly-region\" });\n\n/**\n * Build CodeMirror extensions that enforce editable regions:\n * - changeFilter: suppresses changes outside editable ranges\n * - StateField: decorates readonly regions with a visual style\n */\nfunction buildEditableExtensions(editablePaths: string[]) {\n // changeFilter: recompute editable ranges from the live document on every\n // transaction so offsets remain accurate as the user types. Using a snapshot\n // from mount time would cause ranges to drift and incorrectly block edits.\n const changeFilter = EditorState.changeFilter.of((tr) => {\n const currentDoc = tr.startState.doc.toString();\n const editableRanges = resolvePointerRanges(currentDoc, editablePaths);\n const readonlyRanges = computeReadonlyRanges(\n editableRanges,\n tr.startState.doc.length,\n );\n // Empty readonly list means the whole document is editable -- return true\n // to allow all changes. An empty array would block everything.\n if (readonlyRanges.length === 0) return true;\n return readonlyRanges.flatMap((r) => [r.from, r.to]);\n });\n\n // Decorations: mark readonly regions with a subtle background.\n // Recomputed from the live document on every change so highlights stay\n // accurate as content shifts.\n const readonlyDecorations = StateField.define<DecorationSet>({\n create(state) {\n const editableRanges = resolvePointerRanges(\n state.doc.toString(),\n editablePaths,\n );\n const ranges = computeReadonlyRanges(editableRanges, state.doc.length);\n return Decoration.set(\n ranges.map((r) => readonlyMark.range(r.from, r.to)),\n );\n },\n update(deco, tr) {\n if (!tr.docChanged) return deco;\n const editableRanges = resolvePointerRanges(\n tr.state.doc.toString(),\n editablePaths,\n );\n const ranges = computeReadonlyRanges(editableRanges, tr.state.doc.length);\n return Decoration.set(\n ranges.map((r) => readonlyMark.range(r.from, r.to)),\n );\n },\n provide: (field) => EditorView.decorations.from(field),\n });\n\n return [changeFilter, readonlyDecorations];\n}\n\n/**\n * JsonEditor - A JSON text editor with selectively editable regions.\n *\n * Pass JSON Pointer paths to `editablePaths` to mark which parts of\n * the document the user can modify. Everything else is visible but\n * locked. Changes outside editable regions are silently rejected.\n *\n * A CodeEditor with the JSON language and the locking extensions. The default\n * theme reads CSS variables from @petrarca/sonnet-ui/styles.css; pass a custom\n * `theme` extension to override styling for consumers that do not use\n * sonnet-ui tokens.\n */\nexport default function JsonEditor({\n value,\n editablePaths,\n onChange,\n height = \"400px\",\n className,\n theme = defaultTheme,\n fillContainer = false,\n}: JsonEditorProps) {\n // Ranges are structural: recompute only when the set of editable paths\n // changes, not on every keystroke.\n const joinedPaths = editablePaths.join(\",\");\n\n const extensions = useMemo(\n () => [json(), ...buildEditableExtensions(editablePaths)],\n [joinedPaths], // eslint-disable-line react-hooks/exhaustive-deps\n );\n\n return (\n <CodeEditor\n value={value}\n onChange={onChange}\n height={height}\n fillContainer={fillContainer}\n theme={theme}\n extensions={extensions}\n className={className}\n />\n );\n}\n","/**\n * Editable-region maths for JsonEditor.\n *\n * Pure functions, kept out of the component file so it exports only components\n * (Fast Refresh) and so the inversion is directly testable. Not re-exported\n * from the package root -- these are internals, like the table's column\n * resolver.\n */\n\nimport type { OffsetRange } from \"@petrarca/sonnet-core\";\n\n/**\n * Normalize ranges into a sorted, non-overlapping set.\n *\n * Inversion walks a single cursor forward, so it is only correct for input that\n * is already sorted and disjoint. Neither holds in general: a caller may pass a\n * pointer together with one nested inside it -- `/metadata` and\n * `/metadata/version` -- which is a reasonable way to say \"this object is\n * editable, and so is this field within it\". Merging first makes the inversion\n * correct for any input rather than for input that happens to satisfy an\n * unstated precondition.\n */\nfunction mergeRanges(ranges: OffsetRange[]): { from: number; to: number }[] {\n const sorted = [...ranges].sort((a, b) => a.from - b.from);\n const merged: { from: number; to: number }[] = [];\n for (const range of sorted) {\n const last = merged[merged.length - 1];\n // `<=` also folds ranges that merely touch, so the inversion emits no\n // zero-width readonly slivers between them.\n if (last && range.from <= last.to) {\n last.to = Math.max(last.to, range.to);\n } else {\n merged.push({ from: range.from, to: range.to });\n }\n }\n return merged;\n}\n\n/**\n * Compute readonly ranges from editable ranges (inversion against doc length).\n * Used by both the changeFilter and the decoration builder.\n */\nexport function computeReadonlyRanges(\n editableRanges: OffsetRange[],\n docLength: number,\n): { from: number; to: number }[] {\n const ranges: { from: number; to: number }[] = [];\n let cursor = 0;\n for (const range of mergeRanges(editableRanges)) {\n if (cursor < range.from) {\n ranges.push({ from: cursor, to: range.from });\n }\n cursor = range.to;\n }\n if (cursor < docLength) {\n ranges.push({ from: cursor, to: docLength });\n }\n return ranges;\n}\n","import { EditorView } from \"@codemirror/view\";\nimport type { Extension } from \"@codemirror/state\";\nimport { codeTheme } from \"../CodeEditor/theme\";\n\n/**\n * Default theme for JsonEditor: the CodeEditor look (design tokens from\n * @petrarca/sonnet-ui/styles.css), plus the JSON editor's own marks -- the\n * locked regions, and no active-line band (it would read as a lock).\n *\n * Consumers not using sonnet-ui tokens can pass their own Extension\n * via the JsonEditor `theme` prop to override this entirely.\n */\nexport const defaultTheme: Extension = [\n codeTheme,\n EditorView.theme({\n \".cm-readonly-region\": {\n backgroundColor: \"hsl(var(--state-selected) / 0.5)\",\n opacity: \"0.7\",\n },\n \".cm-activeLine\": {\n backgroundColor: \"transparent\",\n },\n \".cm-activeLineGutter\": {\n backgroundColor: \"transparent\",\n },\n }),\n];\n"],"mappings":"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","names":["EditorView","EditorView"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React__default from 'react';
|
|
2
|
+
|
|
3
|
+
/** A link's target within the document's set: a path and an optional fragment. */
|
|
4
|
+
interface DocumentTarget {
|
|
5
|
+
/** The path, without a leading "/" ("server/docs/ontology.md"). */
|
|
6
|
+
path: string;
|
|
7
|
+
/** The part after "#", if any. */
|
|
8
|
+
fragment?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface MarkdownViewerProps {
|
|
12
|
+
/** The Markdown text. */
|
|
13
|
+
content: string;
|
|
14
|
+
/** Draw ```mermaid blocks as diagrams. Default true. */
|
|
15
|
+
diagrams?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* A leading YAML front-matter block (between --- lines): shown as YAML above
|
|
18
|
+
* the document ("show", default), or left out ("hide"). Never rendered as
|
|
19
|
+
* Markdown, where it would read as a rule and a heading.
|
|
20
|
+
*/
|
|
21
|
+
frontmatter?: "show" | "hide";
|
|
22
|
+
/**
|
|
23
|
+
* Where the document lives, as a path in its set of files
|
|
24
|
+
* ("server/docs/guide.md"): its relative links resolve against it.
|
|
25
|
+
*/
|
|
26
|
+
documentPath?: string;
|
|
27
|
+
/**
|
|
28
|
+
* A link to another document of the set, given its resolved path (and
|
|
29
|
+
* #fragment): the element to render, e.g. a router link opening that file.
|
|
30
|
+
* With documentPath; without it, such links stay as written.
|
|
31
|
+
*/
|
|
32
|
+
renderDocumentLink?: (target: DocumentTarget, children: React__default.ReactNode) => React__default.ReactNode;
|
|
33
|
+
/**
|
|
34
|
+
* A section to show: the document scrolls to the heading with this id (as
|
|
35
|
+
* GitHub makes them: "## Getting started" is "getting-started"), e.g. the
|
|
36
|
+
* #fragment of the link that opened it.
|
|
37
|
+
*/
|
|
38
|
+
fragment?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The text is still arriving (a chat answer): incomplete Markdown is closed
|
|
41
|
+
* as it streams and new text fades in. Default false (a whole document).
|
|
42
|
+
*/
|
|
43
|
+
streaming?: boolean;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* MarkdownViewer -- Markdown rendered for reading: GitHub-flavoured (tables,
|
|
48
|
+
* task lists, strikethrough), code blocks in the sonnet CodeEditor, Mermaid
|
|
49
|
+
* diagrams (loaded only when a document has one), and a leading front-matter
|
|
50
|
+
* block as YAML.
|
|
51
|
+
*
|
|
52
|
+
* Content is treated as untrusted: raw HTML is sanitized (no script, no event
|
|
53
|
+
* handlers or styles), unsafe links and images are blocked, links ask before
|
|
54
|
+
* leaving, and diagrams render at Mermaid's strict security level. None of
|
|
55
|
+
* this can be loosened through the props.
|
|
56
|
+
*
|
|
57
|
+
* Built on Streamdown, which stays an implementation detail: the props name no
|
|
58
|
+
* Streamdown concept, so it can be replaced here without touching an app.
|
|
59
|
+
*
|
|
60
|
+
* Usage:
|
|
61
|
+
* <MarkdownViewer content={readme} />
|
|
62
|
+
* <MarkdownViewer content={answer} streaming={isStreaming} />
|
|
63
|
+
*/
|
|
64
|
+
declare function MarkdownViewer({ content, diagrams, frontmatter, documentPath, renderDocumentLink, fragment, streaming, className, }: MarkdownViewerProps): React__default.ReactElement;
|
|
65
|
+
|
|
66
|
+
export { MarkdownViewer };
|
|
67
|
+
export type { DocumentTarget, MarkdownViewerProps };
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CopyButton
|
|
3
|
+
} from "../chunk-ZQAHSGOS.js";
|
|
4
|
+
import {
|
|
5
|
+
CodeEditor
|
|
6
|
+
} from "../chunk-JDD3LLTR.js";
|
|
7
|
+
|
|
8
|
+
// src/MarkdownViewer/MarkdownViewer.tsx
|
|
9
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
10
|
+
import { Streamdown } from "streamdown";
|
|
11
|
+
import { cn } from "@petrarca/sonnet-core";
|
|
12
|
+
import { useTheme } from "@petrarca/sonnet-core/hooks";
|
|
13
|
+
|
|
14
|
+
// src/MarkdownViewer/mermaid.ts
|
|
15
|
+
var DEFAULTS = {
|
|
16
|
+
startOnLoad: false,
|
|
17
|
+
theme: "default",
|
|
18
|
+
securityLevel: "strict",
|
|
19
|
+
fontFamily: "monospace",
|
|
20
|
+
suppressErrorRendering: true
|
|
21
|
+
};
|
|
22
|
+
function asMermaidConfig(config) {
|
|
23
|
+
return { ...config, securityLevel: "strict" };
|
|
24
|
+
}
|
|
25
|
+
var mermaidModule;
|
|
26
|
+
var loadMermaid = () => mermaidModule ??= import("mermaid");
|
|
27
|
+
var lazyMermaid = {
|
|
28
|
+
name: "mermaid",
|
|
29
|
+
type: "diagram",
|
|
30
|
+
language: "mermaid",
|
|
31
|
+
getMermaid(config) {
|
|
32
|
+
const settings = { ...DEFAULTS, ...asMermaidConfig(config) };
|
|
33
|
+
let initialized = false;
|
|
34
|
+
return {
|
|
35
|
+
initialize(next) {
|
|
36
|
+
Object.assign(settings, asMermaidConfig(next));
|
|
37
|
+
initialized = false;
|
|
38
|
+
},
|
|
39
|
+
async render(id, source) {
|
|
40
|
+
const mermaid = (await loadMermaid()).default;
|
|
41
|
+
if (!initialized) {
|
|
42
|
+
mermaid.initialize(settings);
|
|
43
|
+
initialized = true;
|
|
44
|
+
}
|
|
45
|
+
return mermaid.render(id, source);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// src/MarkdownViewer/MarkdownCode.tsx
|
|
52
|
+
import { createElement } from "react";
|
|
53
|
+
import { defaultComponents } from "streamdown";
|
|
54
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
55
|
+
function languageOf(className) {
|
|
56
|
+
return className?.match(/language-([\w+#-]+)/)?.[1];
|
|
57
|
+
}
|
|
58
|
+
function MarkdownCode(props) {
|
|
59
|
+
const language = languageOf(props.className);
|
|
60
|
+
if (!props["data-block"] || language === "mermaid") {
|
|
61
|
+
return createElement(
|
|
62
|
+
defaultComponents.code,
|
|
63
|
+
props
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const text = String(props.children ?? "").replace(/\n$/, "");
|
|
67
|
+
return /* @__PURE__ */ jsxs("div", { className: "group/code relative my-4", "data-language": language, children: [
|
|
68
|
+
/* @__PURE__ */ jsx(
|
|
69
|
+
CodeEditor,
|
|
70
|
+
{
|
|
71
|
+
value: text,
|
|
72
|
+
language,
|
|
73
|
+
readOnly: true,
|
|
74
|
+
variant: "compact",
|
|
75
|
+
maxHeight: "24rem",
|
|
76
|
+
"aria-label": language ? `${language} code` : "Code"
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
/* @__PURE__ */ jsx(
|
|
80
|
+
CopyButton,
|
|
81
|
+
{
|
|
82
|
+
value: text,
|
|
83
|
+
label: "Copy code",
|
|
84
|
+
className: "absolute right-1 top-1 opacity-0 transition-opacity group-hover/code:opacity-100 focus-visible:opacity-100"
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
] });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/MarkdownViewer/frontmatter.ts
|
|
91
|
+
var FRONTMATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/;
|
|
92
|
+
function splitFrontmatter(text) {
|
|
93
|
+
const match = FRONTMATTER.exec(text);
|
|
94
|
+
return match ? { yaml: match[1], body: text.slice(match[0].length) } : { body: text };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/MarkdownViewer/documentLinks.tsx
|
|
98
|
+
import { createElement as createElement2 } from "react";
|
|
99
|
+
import {
|
|
100
|
+
defaultComponents as defaultComponents2,
|
|
101
|
+
defaultRehypePlugins
|
|
102
|
+
} from "streamdown";
|
|
103
|
+
import rehypeSlug from "rehype-slug";
|
|
104
|
+
import { jsxs as jsxs2 } from "react/jsx-runtime";
|
|
105
|
+
var DOCUMENT_ORIGIN = "https://document.invalid";
|
|
106
|
+
function rehypePluginsFor(documentPath) {
|
|
107
|
+
const defaults = Object.entries(defaultRehypePlugins).map(
|
|
108
|
+
([name, plugin]) => {
|
|
109
|
+
if (!documentPath || name !== "harden" || !Array.isArray(plugin)) {
|
|
110
|
+
return plugin;
|
|
111
|
+
}
|
|
112
|
+
const [harden, options] = plugin;
|
|
113
|
+
return [
|
|
114
|
+
harden,
|
|
115
|
+
{
|
|
116
|
+
...options,
|
|
117
|
+
defaultOrigin: `${DOCUMENT_ORIGIN}/${documentPath.replace(/^\/+/, "")}`
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
return [...defaults, rehypeSlug];
|
|
123
|
+
}
|
|
124
|
+
function targetOf(href, documentPath) {
|
|
125
|
+
if (!href) return void 0;
|
|
126
|
+
if (href.startsWith("#")) {
|
|
127
|
+
return href.length > 1 ? { path: documentPath, fragment: decodeURIComponent(href.slice(1)) } : void 0;
|
|
128
|
+
}
|
|
129
|
+
const local = href.startsWith(DOCUMENT_ORIGIN) ? href.slice(DOCUMENT_ORIGIN.length) : href.startsWith("/") && !href.startsWith("//") ? href : void 0;
|
|
130
|
+
if (local === void 0) return void 0;
|
|
131
|
+
const [path, fragment] = local.replace(/^\/+/, "").split("#", 2);
|
|
132
|
+
return {
|
|
133
|
+
path: decodeURIComponent(path),
|
|
134
|
+
...fragment ? { fragment: decodeURIComponent(fragment) } : {}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function documentComponents(documentPath, renderLink) {
|
|
138
|
+
return {
|
|
139
|
+
a: (props) => {
|
|
140
|
+
const target = targetOf(props.href, documentPath);
|
|
141
|
+
return target ? renderLink(target, props.children) : createElement2(
|
|
142
|
+
defaultComponents2.a,
|
|
143
|
+
props
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
// An image in the document's set has no address the app can load from.
|
|
147
|
+
img: (props) => targetOf(props.src, documentPath) ? /* @__PURE__ */ jsxs2("span", { className: "text-xs text-muted-foreground", children: [
|
|
148
|
+
"[Image:",
|
|
149
|
+
" ",
|
|
150
|
+
props.alt || targetOf(props.src, documentPath)?.path,
|
|
151
|
+
"]"
|
|
152
|
+
] }) : createElement2(
|
|
153
|
+
defaultComponents2.img,
|
|
154
|
+
props
|
|
155
|
+
)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/MarkdownViewer/MarkdownViewer.tsx
|
|
160
|
+
import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
161
|
+
var PLUGINS_WITH_DIAGRAMS = { mermaid: lazyMermaid };
|
|
162
|
+
var MERMAID_THEMES = {
|
|
163
|
+
light: { config: { theme: "default" } },
|
|
164
|
+
dark: { config: { theme: "dark" } }
|
|
165
|
+
};
|
|
166
|
+
var COMPONENTS = { code: MarkdownCode };
|
|
167
|
+
function useDocumentSetup(documentPath, renderDocumentLink) {
|
|
168
|
+
return useMemo(
|
|
169
|
+
() => documentPath && renderDocumentLink ? {
|
|
170
|
+
components: {
|
|
171
|
+
...COMPONENTS,
|
|
172
|
+
...documentComponents(documentPath, renderDocumentLink)
|
|
173
|
+
},
|
|
174
|
+
rehypePlugins: rehypePluginsFor(documentPath)
|
|
175
|
+
} : { components: COMPONENTS, rehypePlugins: rehypePluginsFor() },
|
|
176
|
+
[documentPath, renderDocumentLink]
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function useScrollToFragment(fragment, body) {
|
|
180
|
+
const ref = useRef(null);
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!fragment || !ref.current) return;
|
|
183
|
+
const heading = Array.from(ref.current.querySelectorAll("[id]")).find(
|
|
184
|
+
(el) => el.id === fragment
|
|
185
|
+
);
|
|
186
|
+
heading?.scrollIntoView({ block: "start" });
|
|
187
|
+
}, [fragment, body]);
|
|
188
|
+
return ref;
|
|
189
|
+
}
|
|
190
|
+
function MarkdownViewer({
|
|
191
|
+
content,
|
|
192
|
+
diagrams = true,
|
|
193
|
+
frontmatter = "show",
|
|
194
|
+
documentPath,
|
|
195
|
+
renderDocumentLink,
|
|
196
|
+
fragment,
|
|
197
|
+
streaming = false,
|
|
198
|
+
className
|
|
199
|
+
}) {
|
|
200
|
+
const { yaml, body } = splitFrontmatter(content);
|
|
201
|
+
const { components, rehypePlugins } = useDocumentSetup(
|
|
202
|
+
documentPath,
|
|
203
|
+
renderDocumentLink
|
|
204
|
+
);
|
|
205
|
+
const ref = useScrollToFragment(fragment, body);
|
|
206
|
+
const { theme } = useTheme();
|
|
207
|
+
return /* @__PURE__ */ jsxs3("div", { ref, className: cn("sonnet-markdown text-sm", className), children: [
|
|
208
|
+
yaml && frontmatter === "show" && /* @__PURE__ */ jsx2(
|
|
209
|
+
CodeEditor,
|
|
210
|
+
{
|
|
211
|
+
value: yaml,
|
|
212
|
+
language: "yaml",
|
|
213
|
+
readOnly: true,
|
|
214
|
+
variant: "compact",
|
|
215
|
+
className: "mb-4",
|
|
216
|
+
"aria-label": "Front matter"
|
|
217
|
+
}
|
|
218
|
+
),
|
|
219
|
+
/* @__PURE__ */ jsx2(
|
|
220
|
+
Streamdown,
|
|
221
|
+
{
|
|
222
|
+
mode: streaming ? "streaming" : "static",
|
|
223
|
+
isAnimating: streaming,
|
|
224
|
+
plugins: diagrams ? PLUGINS_WITH_DIAGRAMS : void 0,
|
|
225
|
+
mermaid: MERMAID_THEMES[theme],
|
|
226
|
+
components,
|
|
227
|
+
rehypePlugins,
|
|
228
|
+
children: body
|
|
229
|
+
},
|
|
230
|
+
theme
|
|
231
|
+
)
|
|
232
|
+
] });
|
|
233
|
+
}
|
|
234
|
+
export {
|
|
235
|
+
MarkdownViewer
|
|
236
|
+
};
|
|
237
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/MarkdownViewer/MarkdownViewer.tsx","../../src/MarkdownViewer/mermaid.ts","../../src/MarkdownViewer/MarkdownCode.tsx","../../src/MarkdownViewer/frontmatter.ts","../../src/MarkdownViewer/documentLinks.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useRef } from \"react\";\nimport { Streamdown } from \"streamdown\";\nimport { cn } from \"@petrarca/sonnet-core\";\nimport { useTheme } from \"@petrarca/sonnet-core/hooks\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport { lazyMermaid } from \"./mermaid\";\nimport { MarkdownCode } from \"./MarkdownCode\";\nimport { splitFrontmatter } from \"./frontmatter\";\nimport {\n documentComponents,\n rehypePluginsFor,\n type DocumentTarget,\n} from \"./documentLinks\";\n\nexport interface MarkdownViewerProps {\n /** The Markdown text. */\n content: string;\n /** Draw ```mermaid blocks as diagrams. Default true. */\n diagrams?: boolean;\n /**\n * A leading YAML front-matter block (between --- lines): shown as YAML above\n * the document (\"show\", default), or left out (\"hide\"). Never rendered as\n * Markdown, where it would read as a rule and a heading.\n */\n frontmatter?: \"show\" | \"hide\";\n /**\n * Where the document lives, as a path in its set of files\n * (\"server/docs/guide.md\"): its relative links resolve against it.\n */\n documentPath?: string;\n /**\n * A link to another document of the set, given its resolved path (and\n * #fragment): the element to render, e.g. a router link opening that file.\n * With documentPath; without it, such links stay as written.\n */\n renderDocumentLink?: (\n target: DocumentTarget,\n children: React.ReactNode,\n ) => React.ReactNode;\n /**\n * A section to show: the document scrolls to the heading with this id (as\n * GitHub makes them: \"## Getting started\" is \"getting-started\"), e.g. the\n * #fragment of the link that opened it.\n */\n fragment?: string;\n /**\n * The text is still arriving (a chat answer): incomplete Markdown is closed\n * as it streams and new text fades in. Default false (a whole document).\n */\n streaming?: boolean;\n className?: string;\n}\n\nconst PLUGINS_WITH_DIAGRAMS = { mermaid: lazyMermaid };\n// Diagrams in Mermaid's theme for the page's: its dark one on the dark tokens.\nconst MERMAID_THEMES = {\n light: { config: { theme: \"default\" as const } },\n dark: { config: { theme: \"dark\" as const } },\n};\nconst COMPONENTS = { code: MarkdownCode };\n\n// The elements and plugins for a document: its own links, when it has a place.\nfunction useDocumentSetup(\n documentPath: string | undefined,\n renderDocumentLink: MarkdownViewerProps[\"renderDocumentLink\"],\n) {\n return useMemo(\n () =>\n documentPath && renderDocumentLink\n ? {\n components: {\n ...COMPONENTS,\n ...documentComponents(documentPath, renderDocumentLink),\n },\n rehypePlugins: rehypePluginsFor(documentPath),\n }\n : { components: COMPONENTS, rehypePlugins: rehypePluginsFor() },\n [documentPath, renderDocumentLink],\n );\n}\n\n// Scroll to the heading `fragment` names, once the document shows it.\nfunction useScrollToFragment(\n fragment: string | undefined,\n body: string,\n): React.RefObject<HTMLDivElement | null> {\n const ref = useRef<HTMLDivElement>(null);\n useEffect(() => {\n if (!fragment || !ref.current) return;\n const heading = Array.from(ref.current.querySelectorAll(\"[id]\")).find(\n (el) => el.id === fragment,\n );\n heading?.scrollIntoView({ block: \"start\" });\n }, [fragment, body]);\n return ref;\n}\n\n/**\n * MarkdownViewer -- Markdown rendered for reading: GitHub-flavoured (tables,\n * task lists, strikethrough), code blocks in the sonnet CodeEditor, Mermaid\n * diagrams (loaded only when a document has one), and a leading front-matter\n * block as YAML.\n *\n * Content is treated as untrusted: raw HTML is sanitized (no script, no event\n * handlers or styles), unsafe links and images are blocked, links ask before\n * leaving, and diagrams render at Mermaid's strict security level. None of\n * this can be loosened through the props.\n *\n * Built on Streamdown, which stays an implementation detail: the props name no\n * Streamdown concept, so it can be replaced here without touching an app.\n *\n * Usage:\n * <MarkdownViewer content={readme} />\n * <MarkdownViewer content={answer} streaming={isStreaming} />\n */\nexport function MarkdownViewer({\n content,\n diagrams = true,\n frontmatter = \"show\",\n documentPath,\n renderDocumentLink,\n fragment,\n streaming = false,\n className,\n}: MarkdownViewerProps): React.ReactElement {\n const { yaml, body } = splitFrontmatter(content);\n const { components, rehypePlugins } = useDocumentSetup(\n documentPath,\n renderDocumentLink,\n );\n const ref = useScrollToFragment(fragment, body);\n const { theme } = useTheme();\n return (\n <div ref={ref} className={cn(\"sonnet-markdown text-sm\", className)}>\n {yaml && frontmatter === \"show\" && (\n <CodeEditor\n value={yaml}\n language=\"yaml\"\n readOnly\n variant=\"compact\"\n className=\"mb-4\"\n aria-label=\"Front matter\"\n />\n )}\n {/* Keyed by the theme: Streamdown draws a diagram once, so a switch\n redraws the document with the diagrams in the new theme. */}\n <Streamdown\n key={theme}\n mode={streaming ? \"streaming\" : \"static\"}\n isAnimating={streaming}\n plugins={diagrams ? PLUGINS_WITH_DIAGRAMS : undefined}\n mermaid={MERMAID_THEMES[theme]}\n components={components}\n rehypePlugins={rehypePlugins}\n >\n {body}\n </Streamdown>\n </div>\n );\n}\n","import type { MermaidConfig } from \"mermaid\";\nimport type { DiagramPlugin } from \"streamdown\";\n\n/**\n * Mermaid for Streamdown, loaded only when a diagram is first drawn: Mermaid\n * is large, and most documents have no diagram. Same defaults as Streamdown's\n * own plugin -- notably securityLevel \"strict\", which keeps a diagram's text\n * from running script or reaching outside it.\n */\nconst DEFAULTS: MermaidConfig = {\n startOnLoad: false,\n theme: \"default\",\n securityLevel: \"strict\",\n fontFamily: \"monospace\",\n suppressErrorRendering: true,\n};\n\n// Streamdown's view of a Mermaid config: Mermaid's own, with looser types.\ntype StreamdownConfig = Parameters<DiagramPlugin[\"getMermaid\"]>[0];\n\n// A config from Streamdown, as Mermaid's own -- strict security always wins.\nfunction asMermaidConfig(config: StreamdownConfig): MermaidConfig {\n return { ...(config as MermaidConfig), securityLevel: \"strict\" };\n}\n\n// One Mermaid instance per app; initialized before each first use of a config.\nlet mermaidModule: Promise<typeof import(\"mermaid\")> | undefined;\nconst loadMermaid = () => (mermaidModule ??= import(\"mermaid\"));\n\nexport const lazyMermaid: DiagramPlugin = {\n name: \"mermaid\",\n type: \"diagram\",\n language: \"mermaid\",\n getMermaid(config?: StreamdownConfig) {\n const settings: MermaidConfig = { ...DEFAULTS, ...asMermaidConfig(config) };\n let initialized = false;\n return {\n initialize(next) {\n Object.assign(settings, asMermaidConfig(next));\n initialized = false;\n },\n async render(id, source) {\n const mermaid = (await loadMermaid()).default;\n if (!initialized) {\n mermaid.initialize(settings);\n initialized = true;\n }\n return mermaid.render(id, source);\n },\n };\n },\n};\n","import React, { createElement } from \"react\";\nimport { defaultComponents } from \"streamdown\";\nimport { CodeEditor } from \"../CodeEditor/CodeEditor\";\nimport { CopyButton } from \"../copy-button\";\n\ntype CodeProps = React.ComponentProps<\"code\"> & { \"data-block\"?: unknown };\n\n// The language a fenced block names (\"language-python\" -> \"python\").\nfunction languageOf(className: string | undefined): string | undefined {\n return className?.match(/language-([\\w+#-]+)/)?.[1];\n}\n\n/**\n * The `code` element of a Markdown document: a fenced block shows in the\n * sonnet CodeEditor (compact, read-only, the same colours as any code view)\n * with a copy button; inline code and diagram blocks (mermaid) stay\n * Streamdown's.\n */\nexport function MarkdownCode(props: CodeProps): React.ReactElement {\n const language = languageOf(props.className);\n if (!props[\"data-block\"] || language === \"mermaid\") {\n return createElement(\n defaultComponents.code as React.ComponentType<CodeProps>,\n props,\n );\n }\n const text = String(props.children ?? \"\").replace(/\\n$/, \"\");\n return (\n <div className=\"group/code relative my-4\" data-language={language}>\n <CodeEditor\n value={text}\n language={language}\n readOnly\n variant=\"compact\"\n maxHeight=\"24rem\"\n aria-label={language ? `${language} code` : \"Code\"}\n />\n <CopyButton\n value={text}\n label=\"Copy code\"\n className=\"absolute right-1 top-1 opacity-0 transition-opacity group-hover/code:opacity-100 focus-visible:opacity-100\"\n />\n </div>\n );\n}\n","/** A document's leading YAML front matter (between --- lines), and the rest. */\nexport interface SplitDocument {\n /** The front matter's YAML, without the --- lines; undefined when none. */\n yaml?: string;\n /** The Markdown after it (the whole text when there is none). */\n body: string;\n}\n\n// A block opening the document: \"---\", lines, \"---\" (or \"...\"), each alone on\n// its line. An unclosed one (a document still streaming in) is not a block.\nconst FRONTMATTER =\n /^---[ \\t]*\\r?\\n([\\s\\S]*?)\\r?\\n(?:---|\\.\\.\\.)[ \\t]*(?:\\r?\\n|$)/;\n\nexport function splitFrontmatter(text: string): SplitDocument {\n const match = FRONTMATTER.exec(text);\n return match\n ? { yaml: match[1], body: text.slice(match[0].length) }\n : { body: text };\n}\n","import React, { createElement } from \"react\";\nimport {\n defaultComponents,\n defaultRehypePlugins,\n type StreamdownProps,\n} from \"streamdown\";\nimport rehypeSlug from \"rehype-slug\";\n\ntype RehypePlugins = NonNullable<StreamdownProps[\"rehypePlugins\"]>;\n\n/**\n * A document's relative links, resolved against where it lives.\n *\n * The sanitizer (rehype-harden) resolves every link and image against an\n * origin; given the document's address on a stand-in origin, a relative link\n * resolves to the path it names in the document's own set of files. The\n * stand-in uses the reserved .invalid domain, so it never reaches a network.\n */\nconst DOCUMENT_ORIGIN = \"https://document.invalid\";\n\n/** A link's target within the document's set: a path and an optional fragment. */\nexport interface DocumentTarget {\n /** The path, without a leading \"/\" (\"server/docs/ontology.md\"). */\n path: string;\n /** The part after \"#\", if any. */\n fragment?: string;\n}\n\n/**\n * The rehype plugins: the defaults, with links and images resolved against\n * `documentPath` when the document has one; then ids on the headings, as\n * GitHub makes them (\"## Getting started\" -> \"getting-started\"), so a link to\n * a section finds it. The ids are added after sanitizing, from the heading text.\n */\nexport function rehypePluginsFor(documentPath?: string): RehypePlugins {\n const defaults = Object.entries(defaultRehypePlugins).map(\n ([name, plugin]) => {\n if (!documentPath || name !== \"harden\" || !Array.isArray(plugin)) {\n return plugin;\n }\n const [harden, options] = plugin;\n return [\n harden,\n {\n ...(options as object),\n defaultOrigin: `${DOCUMENT_ORIGIN}/${documentPath.replace(/^\\/+/, \"\")}`,\n },\n ] as RehypePlugins[number];\n },\n );\n return [...defaults, rehypeSlug];\n}\n\n// The target a resolved href names within the document's set, if it does: on\n// the stand-in origin, or a bare path (how the sanitizer returns some); a\n// \"#section\" link names a section of the document itself.\nfunction targetOf(\n href: string | undefined,\n documentPath: string,\n): DocumentTarget | undefined {\n if (!href) return undefined;\n if (href.startsWith(\"#\")) {\n return href.length > 1\n ? { path: documentPath, fragment: decodeURIComponent(href.slice(1)) }\n : undefined;\n }\n const local = href.startsWith(DOCUMENT_ORIGIN)\n ? href.slice(DOCUMENT_ORIGIN.length)\n : href.startsWith(\"/\") && !href.startsWith(\"//\")\n ? href\n : undefined;\n if (local === undefined) return undefined;\n const [path, fragment] = local.replace(/^\\/+/, \"\").split(\"#\", 2);\n return {\n path: decodeURIComponent(path),\n ...(fragment ? { fragment: decodeURIComponent(fragment) } : {}),\n };\n}\n\ntype AnchorProps = React.ComponentProps<\"a\">;\ntype ImageProps = React.ComponentProps<\"img\">;\n\n/** The link and image elements for a document whose links stay in the app. */\nexport function documentComponents(\n documentPath: string,\n renderLink: (\n target: DocumentTarget,\n children: React.ReactNode,\n ) => React.ReactNode,\n) {\n return {\n a: (props: AnchorProps) => {\n const target = targetOf(props.href, documentPath);\n return target\n ? renderLink(target, props.children)\n : createElement(\n defaultComponents.a as React.ComponentType<AnchorProps>,\n props,\n );\n },\n // An image in the document's set has no address the app can load from.\n img: (props: ImageProps) =>\n targetOf(props.src as string | undefined, documentPath) ? (\n <span className=\"text-xs text-muted-foreground\">\n [Image:{\" \"}\n {props.alt || targetOf(props.src as string, documentPath)?.path}]\n </span>\n ) : (\n createElement(\n defaultComponents.img as React.ComponentType<ImageProps>,\n props,\n )\n ),\n };\n}\n"],"mappings":"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","names":["createElement","defaultComponents","jsxs","jsx","jsxs"]}
|