duckfn-docs-kit 0.3.0 → 0.4.1
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/AGENTS.md +326 -234
- package/README.md +10 -6
- package/dist/IconButton.d.ts +22 -0
- package/dist/codemirror.d.ts +29 -0
- package/dist/download.d.ts +51 -0
- package/dist/index.d.ts +28 -7
- package/dist/index.js +2 -2
- package/dist/mermaid/DfkMermaid.d.ts +25 -0
- package/dist/mermaid/config.d.ts +48 -0
- package/dist/mermaid/remark.d.ts +40 -0
- package/dist/mermaid/remark.js +32 -0
- package/dist/mermaid/render.d.ts +92 -0
- package/dist/mermaid/styles.d.ts +6 -0
- package/dist/panzoom-view.d.ts +17 -0
- package/dist/{register-CALCwFBv.js → register-wdwf0LC4.js} +1365 -738
- package/dist/remark.d.ts +1 -1
- package/dist/source-dialog.d.ts +35 -0
- package/dist/sql/PreviewTabs.d.ts +39 -23
- package/dist/sql/SvgViewer.d.ts +53 -0
- package/dist/sql/client.js +1 -1
- package/dist/sql/harness.js +1 -1
- package/dist/sql/remark.d.ts +6 -1
- package/dist/sql/renderers.d.ts +9 -0
- package/package.json +6 -2
- package/src/IconButton.ts +50 -0
- package/src/codemirror.ts +97 -0
- package/src/download.ts +169 -0
- package/src/index.ts +31 -7
- package/src/mermaid/DfkMermaid.css +326 -0
- package/src/mermaid/DfkMermaid.ts +411 -0
- package/src/mermaid/config.ts +74 -0
- package/src/mermaid/remark.ts +98 -0
- package/src/mermaid/render.ts +172 -0
- package/src/mermaid/styles.ts +24 -0
- package/src/panzoom-view.ts +235 -0
- package/src/register.ts +3 -0
- package/src/remark.ts +1 -1
- package/src/source-dialog.ts +126 -0
- package/src/sql/DfkSql.css +13 -10
- package/src/sql/DfkSql.ts +60 -51
- package/src/sql/PreviewTabs.ts +98 -52
- package/src/sql/SvgViewer.ts +155 -0
- package/src/sql/remark.ts +6 -1
- package/src/sql/renderers.ts +433 -159
- package/src/sql/sql.css +189 -19
- package/dist/sql/editor.d.ts +0 -16
- package/src/sql/editor.ts +0 -75
package/dist/remark.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { Plugin } from 'unified';
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const DEFAULT_VERSION_PLACEHOLDER = "{{DUCKFN_VERSION}}";
|
|
15
15
|
export interface VersionPlaceholderOptions {
|
|
16
|
-
/** The real version string to substitute in, e.g. `0.0.
|
|
16
|
+
/** The real version string to substitute in, e.g. `0.0.17`. */
|
|
17
17
|
version: string;
|
|
18
18
|
/** Override the token if a site uses a different one. */
|
|
19
19
|
placeholder?: string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "edit the source" dialog shared by `<dfk-mermaid>` and the SQL result `svg`
|
|
3
|
+
* viewer: a modal holding a plain-text CodeMirror editor and an Apply / Cancel
|
|
4
|
+
* pair.
|
|
5
|
+
*
|
|
6
|
+
* The editor writes back through the callback {@link open} is given rather than
|
|
7
|
+
* through an event, so the owner keeps its own source as the single truth — the
|
|
8
|
+
* dialog never renders anything and never decides what a change means.
|
|
9
|
+
*
|
|
10
|
+
* The `<dialog>` is built once and reused; the editor mounts on first open and is
|
|
11
|
+
* kept, because a reader who edits one diagram is likely to edit the next. The
|
|
12
|
+
* dialog is shown *before* the editor mounts: CodeMirror measures its container as
|
|
13
|
+
* it is constructed, and a `display: none` dialog measures to zero.
|
|
14
|
+
*
|
|
15
|
+
* This is browser-only code.
|
|
16
|
+
*/
|
|
17
|
+
export interface SourceDialogLabels {
|
|
18
|
+
title: string;
|
|
19
|
+
apply: string;
|
|
20
|
+
cancel: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class SourceDialog {
|
|
23
|
+
#private;
|
|
24
|
+
readonly root: HTMLDialogElement;
|
|
25
|
+
constructor(labels: SourceDialogLabels);
|
|
26
|
+
setLabels(labels: SourceDialogLabels): void;
|
|
27
|
+
/**
|
|
28
|
+
* Shows the dialog with `source` in the editor. `onApply` receives the edited
|
|
29
|
+
* text — only when it differs from what was opened, so an unedited Apply is a
|
|
30
|
+
* plain close.
|
|
31
|
+
*/
|
|
32
|
+
open(source: string, onApply: (value: string) => void): void;
|
|
33
|
+
/** Closes the dialog and releases the editor. The dialog node stays in place. */
|
|
34
|
+
destroy(): void;
|
|
35
|
+
}
|
|
@@ -1,37 +1,53 @@
|
|
|
1
|
+
import { type DownloadPayload } from '../download';
|
|
1
2
|
/**
|
|
2
3
|
* Every renderer's result shell: a tab strip plus the panels behind it.
|
|
3
4
|
*
|
|
4
|
-
* The strip
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The strip holds one tab per item — a preview row, a `Text` view, or the
|
|
6
|
+
* trailing `Table` view — and, at its right end, the chrome that acts on the
|
|
7
|
+
* result as a whole: the active tab's own {@link PreviewTabItem.actions}, a
|
|
8
|
+
* **download** button, and whatever the caller parks last (the fullscreen toggle
|
|
9
|
+
* `<dfk-sql>` owns). Every result therefore has the same chrome, and the panel
|
|
10
|
+
* behind a tab is built the first time that tab is shown.
|
|
9
11
|
*
|
|
10
|
-
* Retained mode: every button and panel is built in the
|
|
11
|
-
* a field. Activating a tab mutates the nodes it owns
|
|
12
|
-
* `aria-selected`, `tabIndex`) — there is no rebuild, and
|
|
13
|
-
* first time it is shown rather than up front.
|
|
14
|
-
*
|
|
15
|
-
* The table panel is mounted on first activation on purpose: VTable measures
|
|
16
|
-
* its container when it is constructed, and a `hidden` panel measures to zero.
|
|
12
|
+
* Retained mode: every button, action container and panel is built in the
|
|
13
|
+
* constructor and held in a field. Activating a tab mutates the nodes it owns
|
|
14
|
+
* (`hidden`, `classList`, `aria-selected`, `tabIndex`) — there is no rebuild, and
|
|
15
|
+
* a panel is filled the first time it is shown rather than up front.
|
|
17
16
|
*/
|
|
18
|
-
/** One
|
|
17
|
+
/** One tab: its label, how to fill its panel, and what it offers the strip. */
|
|
19
18
|
export interface PreviewTabItem {
|
|
20
19
|
label: string;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Fills the panel. Called once, the first time the tab is shown; an async mount
|
|
22
|
+
* may resolve with a disposer, which the strip runs when it is disposed (that is
|
|
23
|
+
* how the table's handle gets released, including when the countdown lands after
|
|
24
|
+
* {@link PreviewTabs.dispose}).
|
|
25
|
+
*/
|
|
26
|
+
mount(panel: HTMLElement): void | (() => void) | Promise<void | (() => void)>;
|
|
27
|
+
/**
|
|
28
|
+
* Controls for the strip's right end, shown only while this tab is active — the
|
|
29
|
+
* place for actions that belong to *this* result (a table's search and view
|
|
30
|
+
* switches, a figure's zoom reset) rather than to every result.
|
|
31
|
+
*/
|
|
32
|
+
actions?: HTMLElement;
|
|
33
|
+
/** This tab's file, or `null` while there is nothing to save yet. */
|
|
34
|
+
download?: () => DownloadPayload | null;
|
|
35
|
+
/** Told the result area's fullscreen state, for figures that zoom inside it. */
|
|
36
|
+
setFullscreen?: (value: boolean) => void;
|
|
26
37
|
}
|
|
27
38
|
export declare class PreviewTabs {
|
|
28
39
|
#private;
|
|
29
40
|
/**
|
|
30
|
-
* @param corner Node parked at the
|
|
31
|
-
*
|
|
32
|
-
*
|
|
41
|
+
* @param corner Node parked at the very end of the strip, after the download
|
|
42
|
+
* button. `<dfk-sql>` passes its fullscreen toggle: it owns that button's state,
|
|
43
|
+
* so it owns the node and only lends it here.
|
|
33
44
|
*/
|
|
34
|
-
constructor(host: HTMLElement, items: readonly PreviewTabItem[],
|
|
35
|
-
/** Releases
|
|
45
|
+
constructor(host: HTMLElement, items: readonly PreviewTabItem[], downloadLabel: string, corner?: HTMLElement);
|
|
46
|
+
/** Releases every mounted item and empties the panels. */
|
|
36
47
|
dispose(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Records the result area's fullscreen state and passes it on to the tab on
|
|
50
|
+
* screen: a figure zooms only where the result is expanded (see `PanZoomView`).
|
|
51
|
+
*/
|
|
52
|
+
setFullscreen(value: boolean): void;
|
|
37
53
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type DownloadPayload } from '../download';
|
|
2
|
+
/**
|
|
3
|
+
* The `svg` result view: an inline SVG a reader can zoom (in the result area's
|
|
4
|
+
* fullscreen), edit, and download — the same interactions a diagram gets from
|
|
5
|
+
* `<dfk-mermaid>`, which is why both are built on `PanZoomView` and
|
|
6
|
+
* `SourceDialog`.
|
|
7
|
+
*
|
|
8
|
+
* A plain class rather than a custom element: nothing hands it content through
|
|
9
|
+
* markup, and the SQL renderer already has the parsed node, so there is no
|
|
10
|
+
* upgrade to wait for.
|
|
11
|
+
*
|
|
12
|
+
* Like an embedded `<dfk-mermaid>`, the element contributes no frame — the result
|
|
13
|
+
* area's panel is the frame — and no floating cluster: its two controls travel out
|
|
14
|
+
* through {@link actions} for the result's tab strip, and its file travels out
|
|
15
|
+
* through {@link downloadPayload} for the strip's download button. Zoom is off
|
|
16
|
+
* until {@link setFullscreen} says the result area is expanded.
|
|
17
|
+
*
|
|
18
|
+
* This is browser-only code.
|
|
19
|
+
*/
|
|
20
|
+
/** The strings a figure's controls need. */
|
|
21
|
+
export interface FigureLabels {
|
|
22
|
+
reset: string;
|
|
23
|
+
edit: string;
|
|
24
|
+
editTitle: string;
|
|
25
|
+
apply: string;
|
|
26
|
+
cancel: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class SvgViewer {
|
|
29
|
+
#private;
|
|
30
|
+
/** The box in the tab panel: the panzoom viewport, and the frame's clipping box. */
|
|
31
|
+
readonly root: HTMLDivElement;
|
|
32
|
+
/** The controls the result area's tab strip shows while this tab is active. */
|
|
33
|
+
readonly actions: HTMLDivElement;
|
|
34
|
+
constructor(svg: SVGElement | null, markup: string, labels: FigureLabels, fallback: string);
|
|
35
|
+
/** Turns zoom and pan on or off; the result area calls this with its fullscreen. */
|
|
36
|
+
setFullscreen(value: boolean): void;
|
|
37
|
+
/**
|
|
38
|
+
* The file this view would be saved as: the SVG itself, or — when the markup
|
|
39
|
+
* does not parse and the view is showing it as text — that text.
|
|
40
|
+
*/
|
|
41
|
+
downloadPayload(): DownloadPayload;
|
|
42
|
+
dispose(): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parses SVG markup from a result cell into a node this document can host.
|
|
46
|
+
*
|
|
47
|
+
* `image/svg+xml` is strict XML: malformed markup (unclosed tags, a bare `&`,
|
|
48
|
+
* an HTML `<br>`) comes back as a `<parsererror>` element rather than throwing,
|
|
49
|
+
* so the caller can degrade to text. Script-bearing and event-handler content
|
|
50
|
+
* is stripped — inline SVG is *not* isolated (use the `iframe` renderer for
|
|
51
|
+
* untrusted markup).
|
|
52
|
+
*/
|
|
53
|
+
export declare function parseSvgMarkup(document: Document, markup: string): SVGElement | null;
|
package/dist/sql/client.js
CHANGED
package/dist/sql/harness.js
CHANGED
|
@@ -8000,7 +8000,7 @@ var Eu, Du, Ou, ku, Au, ju, Mu, Nu, Pu, Fu, Iu, Lu, Ru, zu, Bu, Vu, Hu, Uu, Wu,
|
|
|
8000
8000
|
11
|
|
8001
8001
|
])), od = {
|
|
8002
8002
|
name: "@duckdb/duckdb-wasm",
|
|
8003
|
-
version: "1.33.1-
|
|
8003
|
+
version: "1.33.1-dev65.0",
|
|
8004
8004
|
description: "DuckDB powered by WebAssembly",
|
|
8005
8005
|
license: "MIT",
|
|
8006
8006
|
repository: {
|
package/dist/sql/remark.d.ts
CHANGED
|
@@ -34,8 +34,13 @@ export interface RunnableSqlConfig {
|
|
|
34
34
|
*
|
|
35
35
|
* `html` and `iframe` are the same renderer: both sandbox the markup in an
|
|
36
36
|
* iframe, so scripts run with an opaque origin.
|
|
37
|
+
*
|
|
38
|
+
* `mermaid` renders the column's mermaid source as a diagram through
|
|
39
|
+
* `<dfk-mermaid>` (the same element a ```mermaid fence produces), so the result
|
|
40
|
+
* gets the element's zoom, fullscreen, source editing and SVG download for
|
|
41
|
+
* free.
|
|
37
42
|
*/
|
|
38
|
-
show?: 'table' | 'html' | 'iframe' | 'svg' | 'text';
|
|
43
|
+
show?: 'table' | 'html' | 'iframe' | 'svg' | 'text' | 'mermaid';
|
|
39
44
|
/**
|
|
40
45
|
* What this block is expected to do when the docs' own SQL test suite runs it
|
|
41
46
|
* (`duckfn-docs-kit/sql/verify`). Defaults to `'ok'`; `'error'` marks a block
|
package/dist/sql/renderers.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { RunnableSqlConfig } from './remark';
|
|
|
5
5
|
*
|
|
6
6
|
* The registry is the seam later phases plug into. It ships `table` (VisActor
|
|
7
7
|
* VTable), a `text` fallback, the markup previews `iframe` / `html` / `svg`,
|
|
8
|
+
* `mermaid` (which hands the cell to the kit's own `<dfk-mermaid>` element, so a
|
|
9
|
+
* query can produce a diagram the reader can zoom, expand, edit and download),
|
|
8
10
|
* plus the `error` view every renderer shares.
|
|
9
11
|
*
|
|
10
12
|
* Heavy dependencies (`@visactor/vtable`) load through dynamic `import()`
|
|
@@ -23,6 +25,13 @@ export interface RenderContext {
|
|
|
23
25
|
* renderer only borrows it so every result has the same chrome.
|
|
24
26
|
*/
|
|
25
27
|
fullscreenButton: HTMLElement;
|
|
28
|
+
/**
|
|
29
|
+
* Subscribes to the result area's fullscreen state, calling back with the
|
|
30
|
+
* current value straight away and returning the unsubscribe. A renderer whose
|
|
31
|
+
* figure zooms only in fullscreen (`svg`, `mermaid`) forwards the value to its
|
|
32
|
+
* viewer; the subscription must be released by the renderer's disposer.
|
|
33
|
+
*/
|
|
34
|
+
onFullscreenChange(listener: (value: boolean) => void): () => void;
|
|
26
35
|
}
|
|
27
36
|
/**
|
|
28
37
|
* Renders `result` into `context.host`. Resolves with a disposer releasing any
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "duckfn-docs-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Shared Docusaurus building blocks (TOC toggle, home-page web components, brand tokens, remark version placeholder) for duckfn-family extension docs sites.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -58,10 +58,14 @@
|
|
|
58
58
|
"@codemirror/lang-sql": "^6.10.0",
|
|
59
59
|
"@codemirror/state": "^6.7.6",
|
|
60
60
|
"@codemirror/view": "^6.43.13",
|
|
61
|
-
"@duckdb/duckdb-wasm": "1.33.1-
|
|
61
|
+
"@duckdb/duckdb-wasm": "1.33.1-dev65.0",
|
|
62
|
+
"@panzoom/panzoom": "^4.6.2",
|
|
62
63
|
"@visactor/vtable": "^1.26.8",
|
|
64
|
+
"@visactor/vtable-search": "^1.26.8",
|
|
63
65
|
"codemirror": "^6.0.2",
|
|
66
|
+
"filenamify": "^7.0.3",
|
|
64
67
|
"iconify-icon": "^3.0.3",
|
|
68
|
+
"mermaid": "^12.0.0",
|
|
65
69
|
"playwright-core": "^1.63.0",
|
|
66
70
|
"sql-formatter": "^15.9.0"
|
|
67
71
|
},
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type {IconifyIconHTMLElement} from 'iconify-icon';
|
|
2
|
+
import {el} from './dom';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A compact icon-only button with a hover/focus tooltip, built once and then
|
|
6
|
+
* mutated. Shared by `<dfk-sql>`'s code-block cluster and `<dfk-mermaid>`'s
|
|
7
|
+
* diagram cluster: both want the same "floating icon row over content" idiom,
|
|
8
|
+
* and neither wants a second implementation of it.
|
|
9
|
+
*
|
|
10
|
+
* The tooltip is also the accessible name — an icon-only control has no text to
|
|
11
|
+
* fall back on. The two class names are written by this module and styled in
|
|
12
|
+
* *each* consumer's shadow-root CSS; that duplication is unavoidable (a shadow
|
|
13
|
+
* boundary stops one sheet from reaching the other tree), so the rules carry the
|
|
14
|
+
* same names and are kept in sync by hand.
|
|
15
|
+
*/
|
|
16
|
+
export class IconButton {
|
|
17
|
+
readonly root = el('button', {class: 'dfk-icon-button', type: 'button'});
|
|
18
|
+
readonly #icon: IconifyIconHTMLElement = el('iconify-icon', {
|
|
19
|
+
class: 'dfk-icon',
|
|
20
|
+
attrs: {'aria-hidden': 'true'},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
constructor(icon: string, onClick: () => void) {
|
|
24
|
+
this.root.appendChild(this.#icon);
|
|
25
|
+
this.root.addEventListener('click', onClick);
|
|
26
|
+
this.setIcon(icon);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setIcon(icon: string): void {
|
|
30
|
+
this.#icon.setAttribute('icon', icon);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setLabel(text: string): void {
|
|
34
|
+
this.root.setAttribute('data-tip', text);
|
|
35
|
+
this.root.setAttribute('aria-label', text);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Marks a toggle as currently on (e.g. the SQL block's wrap toggle). */
|
|
39
|
+
setOn(on: boolean): void {
|
|
40
|
+
this.root.classList.toggle('dfk-icon-on', on);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setDisabled(disabled: boolean): void {
|
|
44
|
+
if (disabled) {
|
|
45
|
+
this.root.setAttribute('disabled', '');
|
|
46
|
+
} else {
|
|
47
|
+
this.root.removeAttribute('disabled');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CodeMirror 6 editor behind the kit's editable content: the code view of a
|
|
3
|
+
* runnable SQL block (`<dfk-sql>`) and the diagram-source dialog of a
|
|
4
|
+
* `<dfk-mermaid>`. One implementation, two languages — the SQL block asks for
|
|
5
|
+
* highlighting, the diagram editor takes the plain-text default.
|
|
6
|
+
*
|
|
7
|
+
* Every CodeMirror module arrives through a dynamic `import()` inside
|
|
8
|
+
* {@link mountCodeEditor}: a page full of blocks pays nothing for the editor on
|
|
9
|
+
* its critical path, and Docusaurus' Node prerender never evaluates any of it.
|
|
10
|
+
*
|
|
11
|
+
* This is browser-only code: `document` is touched only through the container
|
|
12
|
+
* the caller hands over, and everything else is behind the `import()`s.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface CodeEditor {
|
|
16
|
+
getValue(): string;
|
|
17
|
+
setValue(value: string): void;
|
|
18
|
+
/** Soft-wraps long lines, or stops wrapping them (the block's wrap toggle). */
|
|
19
|
+
setWrap(wrapped: boolean): void;
|
|
20
|
+
destroy(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CodeEditorOptions {
|
|
24
|
+
/**
|
|
25
|
+
* Highlight the document as SQL. Omitted, the editor is plain text — which is
|
|
26
|
+
* what a mermaid diagram gets: there is no first-party CodeMirror language for
|
|
27
|
+
* it, and a docs reader is editing prose-shaped source, not writing SQL.
|
|
28
|
+
*/
|
|
29
|
+
language?: 'sql';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function mountCodeEditor(
|
|
33
|
+
container: HTMLElement,
|
|
34
|
+
value: string,
|
|
35
|
+
onChange: (value: string) => void,
|
|
36
|
+
options: CodeEditorOptions = {},
|
|
37
|
+
): Promise<CodeEditor> {
|
|
38
|
+
const [{basicSetup}, {EditorView, keymap}, {defaultKeymap, historyKeymap}, {Compartment}] =
|
|
39
|
+
await Promise.all([
|
|
40
|
+
import('codemirror'),
|
|
41
|
+
import('@codemirror/view'),
|
|
42
|
+
import('@codemirror/commands'),
|
|
43
|
+
import('@codemirror/state'),
|
|
44
|
+
]);
|
|
45
|
+
// Loaded only for the blocks that ask for it, so a diagram editor does not pull
|
|
46
|
+
// the SQL grammar (and its lezer parsers) into the page.
|
|
47
|
+
const language =
|
|
48
|
+
options.language === 'sql' ? [(await import('@codemirror/lang-sql')).sql()] : [];
|
|
49
|
+
|
|
50
|
+
// Wrapping is toggled from the outside, and reconfiguring it must not disturb
|
|
51
|
+
// the document or the undo history — that is exactly what a compartment is
|
|
52
|
+
// for, so the extension is swapped in place rather than rebuilt.
|
|
53
|
+
const wrap = new Compartment();
|
|
54
|
+
|
|
55
|
+
const view = new EditorView({
|
|
56
|
+
doc: value,
|
|
57
|
+
extensions: [
|
|
58
|
+
basicSetup,
|
|
59
|
+
...language,
|
|
60
|
+
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
61
|
+
wrap.of([]),
|
|
62
|
+
EditorView.updateListener.of((update) => {
|
|
63
|
+
if (update.docChanged) {
|
|
64
|
+
onChange(update.state.doc.toString());
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
parent: container,
|
|
69
|
+
// The tree the container really belongs to — `getRootNode()`, not CodeMirror's
|
|
70
|
+
// own `getRoot()`. `getRoot()` follows `assignedSlot` before `parentNode`, so a
|
|
71
|
+
// container in a *slotted* subtree is reported as living in the shadow root it
|
|
72
|
+
// is slotted **into**. The SQL result area is exactly that (light DOM, slotted
|
|
73
|
+
// into `<dfk-sql>` through `slot="dfk-result"`), so style-mod mounted the base
|
|
74
|
+
// theme in a shadow root, where it cannot reach slotted nodes at all: the
|
|
75
|
+
// editor laid out as plain block boxes — `.cm-scroller` stacked instead of
|
|
76
|
+
// flex, gutter above the code.
|
|
77
|
+
//
|
|
78
|
+
// `getRootNode()` stops at the tree that owns the container: `document` for
|
|
79
|
+
// that slotted light DOM, the shadow root for a genuine shadow child (the
|
|
80
|
+
// code view, a diagram's dialog) — which is where the `.cm-*` rules are
|
|
81
|
+
// needed. Pinning it to `document` would break the shadow cases instead.
|
|
82
|
+
root: container.getRootNode() as Document | ShadowRoot,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
getValue: () => view.state.doc.toString(),
|
|
87
|
+
setValue: (next: string) =>
|
|
88
|
+
view.dispatch({
|
|
89
|
+
changes: {from: 0, to: view.state.doc.length, insert: next},
|
|
90
|
+
}),
|
|
91
|
+
setWrap: (wrapped: boolean) =>
|
|
92
|
+
view.dispatch({
|
|
93
|
+
effects: wrap.reconfigure(wrapped ? EditorView.lineWrapping : []),
|
|
94
|
+
}),
|
|
95
|
+
destroy: () => view.destroy(),
|
|
96
|
+
};
|
|
97
|
+
}
|
package/src/download.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import filenamify from 'filenamify';
|
|
2
|
+
import {el} from './dom';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Saving something the kit rendered to a file: the payload shape, the click that
|
|
6
|
+
* performs the save, and how the file is named.
|
|
7
|
+
*
|
|
8
|
+
* Shared by `<dfk-mermaid>` (a downloaded diagram) and the SQL result chrome (a
|
|
9
|
+
* downloaded result of any kind), so a docs page has one definition of "what a
|
|
10
|
+
* download is" rather than one per component.
|
|
11
|
+
*
|
|
12
|
+
* This is browser-only code.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** One file's worth of text, ready to save. */
|
|
16
|
+
export interface DownloadPayload {
|
|
17
|
+
/** The file name, extension included. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** The blob's MIME type. */
|
|
20
|
+
mime: string;
|
|
21
|
+
/** The file's contents — text for every format the kit produces. */
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Saves a payload through a temporary object URL.
|
|
27
|
+
*
|
|
28
|
+
* The anchor is in the document for the click — some browsers ignore a detached
|
|
29
|
+
* one — and the URL is revoked on the next tick, once the download has been
|
|
30
|
+
* handed to the browser.
|
|
31
|
+
*/
|
|
32
|
+
export function saveDownload(payload: DownloadPayload): void {
|
|
33
|
+
const blob = new Blob([payload.text], {type: payload.mime});
|
|
34
|
+
const url = URL.createObjectURL(blob);
|
|
35
|
+
const link = el('a', {href: url, download: payload.name});
|
|
36
|
+
document.body.appendChild(link);
|
|
37
|
+
link.click();
|
|
38
|
+
link.remove();
|
|
39
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Used when nothing on the page says anything about what this is. */
|
|
43
|
+
const DEFAULT_BASE = 'diagram';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The length cap. `filenamify` truncates by grapheme, so a CJK heading is cut at
|
|
47
|
+
* 80 *characters*, not bytes, and an emoji survives whole.
|
|
48
|
+
*/
|
|
49
|
+
const MAX_LENGTH = 80;
|
|
50
|
+
|
|
51
|
+
/** How a reserved character is spelled in the file name (`2. Registration: …`). */
|
|
52
|
+
const REPLACEMENT = '-';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The *format* characters (zero-width space, word joiner, byte-order mark…) and
|
|
56
|
+
* whitespace that a title can wear at its edges.
|
|
57
|
+
*/
|
|
58
|
+
const EDGE_NOISE = /^[\s\p{Cf}]+|[\s\p{Cf}]+$/gu;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Trims a candidate down to its text, dropping format characters at the edges.
|
|
62
|
+
*
|
|
63
|
+
* Plain whitespace trimming is not enough: Docusaurus gives every heading an
|
|
64
|
+
* anchor link whose label is a zero-width space, so `textContent` of a heading is
|
|
65
|
+
* `"2. Registration\u200B"`. `filenamify` turns a format character into the
|
|
66
|
+
* replacement rather than dropping it, which would produce
|
|
67
|
+
* `2. Registration-.svg`. Only the *edges*: an interior zero-width joiner is what
|
|
68
|
+
* holds an emoji together.
|
|
69
|
+
*/
|
|
70
|
+
function cleanTitle(value: string | undefined): string {
|
|
71
|
+
return value === undefined ? '' : value.replace(EDGE_NOISE, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What a downloaded figure is called.
|
|
76
|
+
*
|
|
77
|
+
* The name is derived from the page, not from the figure: a diagram or an SVG
|
|
78
|
+
* result has no name of its own, and the reader downloading one is after "the
|
|
79
|
+
* figure from that section", not `diagram.svg`.
|
|
80
|
+
*
|
|
81
|
+
* Three sources, most specific first, each falling back to the next:
|
|
82
|
+
*
|
|
83
|
+
* 1. **The figure's own title** — mermaid's frontmatter (`---\ntitle: …\n---`,
|
|
84
|
+
* which mermaid itself draws above the diagram); only read when `options.source`
|
|
85
|
+
* is given.
|
|
86
|
+
* 2. **The nearest heading above it** — the section the figure belongs to, which
|
|
87
|
+
* on a docs page is what a reader would call it.
|
|
88
|
+
* 3. **The document title** — the browser tab's title, for a figure that sits
|
|
89
|
+
* above every heading on its page.
|
|
90
|
+
*
|
|
91
|
+
* Nothing found → `options.fallback` (or {@link DEFAULT_BASE}), with `extension`
|
|
92
|
+
* appended either way.
|
|
93
|
+
*/
|
|
94
|
+
export function sectionFileName(
|
|
95
|
+
element: Element,
|
|
96
|
+
extension: string,
|
|
97
|
+
options: {source?: string; fallback?: string} = {},
|
|
98
|
+
): string {
|
|
99
|
+
const candidates = [
|
|
100
|
+
options.source === undefined ? undefined : frontmatterTitle(options.source),
|
|
101
|
+
precedingHeading(element),
|
|
102
|
+
element.ownerDocument.title,
|
|
103
|
+
].map(cleanTitle);
|
|
104
|
+
const title = candidates.find((candidate) => candidate !== '');
|
|
105
|
+
if (title === undefined) {
|
|
106
|
+
return `${options.fallback ?? DEFAULT_BASE}.${extension}`;
|
|
107
|
+
}
|
|
108
|
+
// Filenames are a filesystem concern, so they go through a library rather than a
|
|
109
|
+
// hand-rolled character class. Three things it does that matter here and that a
|
|
110
|
+
// browser does *not*: it strips what Windows and macOS reject (`:`, `?`, `*`,
|
|
111
|
+
// `"`, `<`, `>`, `|`, the path separators and control characters), it trims the
|
|
112
|
+
// trailing dots and spaces Windows silently drops, and it avoids the reserved
|
|
113
|
+
// device names (`con`, `nul`, …). The `download` attribute's own sanitisation
|
|
114
|
+
// covers only `/` and `\`.
|
|
115
|
+
//
|
|
116
|
+
// It also normalises Unicode whitespace and drops format characters, which
|
|
117
|
+
// quietly cleans up Docusaurus' heading anchors: the `<a>` it appends to every
|
|
118
|
+
// heading contributes a zero-width space to `textContent`.
|
|
119
|
+
return `${filenamify(title, {replacement: REPLACEMENT, maxLength: MAX_LENGTH})}.${extension}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* mermaid's frontmatter block, if the source opens with one.
|
|
124
|
+
*
|
|
125
|
+
* Only the top-level `title:` key is read. Mermaid's frontmatter is YAML, and the
|
|
126
|
+
* rest of it (a nested `config:`, `displayMode:`, …) is none of this module's
|
|
127
|
+
* business: a key that is always a plain scalar on one line does not justify a
|
|
128
|
+
* YAML parser, and anything indented — a nested key — is skipped by anchoring the
|
|
129
|
+
* match at the line start.
|
|
130
|
+
*/
|
|
131
|
+
function frontmatterTitle(source: string): string | undefined {
|
|
132
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(source.trimStart())?.[1];
|
|
133
|
+
if (block === undefined) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const value = /^title\s*:\s*(.+)$/m.exec(block)?.[1];
|
|
137
|
+
return value === undefined ? undefined : unquote(value.trim());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Strips one pair of matching quotes, so `title: "A: B"` keeps its colon. */
|
|
141
|
+
function unquote(value: string): string {
|
|
142
|
+
const first = value[0];
|
|
143
|
+
return (first === '"' || first === "'") && value.endsWith(first) ? value.slice(1, -1) : value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The text of the last heading that precedes the figure, or `undefined`.
|
|
148
|
+
*
|
|
149
|
+
* The search is scoped to the enclosing `<article>` where there is one: a docs
|
|
150
|
+
* page's navbar, sidebar and footer are full of headings that have nothing to do
|
|
151
|
+
* with this figure, and the article is the one container that holds the page's
|
|
152
|
+
* own content.
|
|
153
|
+
*/
|
|
154
|
+
function precedingHeading(element: Element): string | undefined {
|
|
155
|
+
const scope: ParentNode = element.closest('article') ?? element.ownerDocument;
|
|
156
|
+
let found: string | undefined;
|
|
157
|
+
for (const heading of scope.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
|
158
|
+
// `FOLLOWING` means the heading comes before the element in document order.
|
|
159
|
+
if (!(heading.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING)) {
|
|
160
|
+
// Headings are in document order, so nothing later can precede it either.
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
const text = heading.textContent?.trim();
|
|
164
|
+
if (text) {
|
|
165
|
+
found = text;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return found;
|
|
169
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type {HTMLAttributes} from 'react';
|
|
2
|
+
// Imported as well as re-exported: the `HTMLElementTagNameMap` augmentation below
|
|
3
|
+
// names the classes, and a module augmentation can only refer to local bindings.
|
|
4
|
+
import {DfkFeatures} from './home/DfkFeatures';
|
|
5
|
+
import {DfkHero} from './home/DfkHero';
|
|
6
|
+
import {DfkMermaid} from './mermaid/DfkMermaid';
|
|
7
|
+
import {DfkNextSteps} from './home/DfkNextSteps';
|
|
8
|
+
import {DfkSql} from './sql/DfkSql';
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
|
-
* Browser entry for duckfn-docs-kit: the
|
|
11
|
+
* Browser entry for duckfn-docs-kit: the custom elements it registers and the
|
|
5
12
|
* value types their `set*` methods accept.
|
|
6
13
|
*
|
|
7
14
|
* The components are retained-mode (build once, then mutate held nodes) and
|
|
@@ -13,17 +20,16 @@ import type {HTMLAttributes} from 'react';
|
|
|
13
20
|
* web component, which `registerDfkElements()` registers as a side effect, so
|
|
14
21
|
* this package ships no icon data.
|
|
15
22
|
*
|
|
16
|
-
* `TocToggle` and the remark
|
|
17
|
-
* (`duckfn-docs-kit/toc-toggle/TocToggle`, `duckfn-docs-kit/remark
|
|
23
|
+
* `TocToggle` and the remark plugins keep their own subpaths
|
|
24
|
+
* (`duckfn-docs-kit/toc-toggle/TocToggle`, `duckfn-docs-kit/remark`,
|
|
25
|
+
* `duckfn-docs-kit/sql/remark`, `duckfn-docs-kit/mermaid/remark`) instead of
|
|
18
26
|
* being merged here: a Docusaurus config file must never pull browser code into
|
|
19
27
|
* Node, and a site that only wants the TOC collapse button should not pay for
|
|
20
28
|
* the bundled `iconify-icon`.
|
|
21
29
|
*/
|
|
22
|
-
export {DfkFeatures
|
|
23
|
-
export {DfkHero} from './home/DfkHero';
|
|
24
|
-
export {DfkNextSteps} from './home/DfkNextSteps';
|
|
25
|
-
export {DfkSql} from './sql/DfkSql';
|
|
30
|
+
export {DfkFeatures, DfkHero, DfkMermaid, DfkNextSteps, DfkSql};
|
|
26
31
|
export {registerDfkElements} from './register';
|
|
32
|
+
export type {DfkMermaidConfig, DfkMermaidConfigInput} from './mermaid/config';
|
|
27
33
|
export type {RunnableSqlConfig} from './sql/remark';
|
|
28
34
|
export type {
|
|
29
35
|
FeatureItem,
|
|
@@ -47,6 +53,23 @@ export type {
|
|
|
47
53
|
*/
|
|
48
54
|
export type DfkElementProps = HTMLAttributes<HTMLElement>;
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The kit's tags in the DOM's own tag map, so `document.createElement('dfk-sql')`
|
|
58
|
+
* (and the kit's `el()` helper) is typed as the class it upgrades to. Without
|
|
59
|
+
* this, every place that builds a `dfk-*` element from scratch — `sql/renderers.ts`
|
|
60
|
+
* building a `<dfk-mermaid>` for a `mermaid` result, `docs/src/pages/index.tsx`
|
|
61
|
+
* mounting the home elements — would have to cast the result.
|
|
62
|
+
*/
|
|
63
|
+
declare global {
|
|
64
|
+
interface HTMLElementTagNameMap {
|
|
65
|
+
'dfk-hero': DfkHero;
|
|
66
|
+
'dfk-features': DfkFeatures;
|
|
67
|
+
'dfk-next-steps': DfkNextSteps;
|
|
68
|
+
'dfk-sql': DfkSql;
|
|
69
|
+
'dfk-mermaid': DfkMermaid;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
declare module 'react' {
|
|
51
74
|
namespace JSX {
|
|
52
75
|
interface IntrinsicElements {
|
|
@@ -54,6 +77,7 @@ declare module 'react' {
|
|
|
54
77
|
'dfk-features': DfkElementProps;
|
|
55
78
|
'dfk-next-steps': DfkElementProps;
|
|
56
79
|
'dfk-sql': DfkElementProps;
|
|
80
|
+
'dfk-mermaid': DfkElementProps;
|
|
57
81
|
}
|
|
58
82
|
}
|
|
59
83
|
}
|