shelving 1.208.0 → 1.209.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 +3 -27
- package/extract/DirectoryExtractor.d.ts +1 -1
- package/extract/DirectoryExtractor.js +8 -8
- package/extract/Extractor.d.ts +1 -1
- package/extract/Extractor.js +10 -2
- package/extract/FileExtractor.d.ts +8 -6
- package/extract/FileExtractor.js +14 -9
- package/extract/MarkdownExtractor.d.ts +12 -9
- package/extract/MarkdownExtractor.js +11 -22
- package/extract/TypescriptExtractor.d.ts +3 -1
- package/extract/TypescriptExtractor.js +60 -6
- package/markup/rule/index.d.ts +3 -0
- package/markup/rule/index.js +2 -0
- package/markup/rule/link.js +6 -6
- package/markup/util/options.d.ts +9 -4
- package/package.json +2 -2
- package/ui/app/App.module.css +63 -33
- package/ui/block/Address.module.css +1 -1
- package/ui/block/Block.d.ts +8 -0
- package/ui/block/Block.js +8 -0
- package/ui/block/{Element.module.css → Block.module.css} +2 -2
- package/ui/block/Block.tsx +15 -0
- package/ui/block/Blockquote.module.css +4 -4
- package/ui/block/Card.d.ts +16 -8
- package/ui/block/Card.js +13 -4
- package/ui/block/Card.module.css +43 -4
- package/ui/block/Card.tsx +24 -9
- package/ui/block/Definitions.d.ts +30 -0
- package/ui/block/Definitions.js +25 -0
- package/ui/block/Definitions.module.css +60 -0
- package/ui/block/Definitions.tsx +45 -0
- package/ui/block/Divider.module.css +2 -2
- package/ui/block/Figure.module.css +2 -2
- package/ui/block/{Elements.d.ts → Flex.d.ts} +7 -7
- package/ui/block/Flex.js +10 -0
- package/ui/block/{Elements.module.css → Flex.module.css} +3 -3
- package/ui/block/{Elements.tsx → Flex.tsx} +10 -10
- package/ui/block/Heading.module.css +19 -3
- package/ui/block/Image.module.css +1 -1
- package/ui/block/List.module.css +1 -1
- package/ui/block/Paragraph.module.css +2 -2
- package/ui/block/Preformatted.d.ts +8 -1
- package/ui/block/Preformatted.js +8 -2
- package/ui/block/Preformatted.module.css +15 -3
- package/ui/block/Preformatted.tsx +10 -2
- package/ui/block/Prose.js +2 -1
- package/ui/block/Prose.module.css +1 -1
- package/ui/block/Prose.tsx +2 -0
- package/ui/block/Section.module.css +2 -2
- package/ui/block/Subheading.module.css +18 -3
- package/ui/block/Table.module.css +7 -7
- package/ui/block/Video.module.css +11 -11
- package/ui/block/index.d.ts +3 -2
- package/ui/block/index.js +3 -2
- package/ui/block/index.ts +3 -2
- package/ui/dialog/Dialog.module.css +3 -3
- package/ui/dialog/Modal.module.css +4 -4
- package/ui/docs/DirectoryCard.d.ts +6 -1
- package/ui/docs/DirectoryCard.js +8 -5
- package/ui/docs/DirectoryCard.tsx +19 -8
- package/ui/docs/DirectoryPage.d.ts +1 -1
- package/ui/docs/DirectoryPage.js +7 -2
- package/ui/docs/DirectoryPage.tsx +14 -5
- package/ui/docs/DocumentationCard.d.ts +6 -1
- package/ui/docs/DocumentationCard.js +11 -5
- package/ui/docs/DocumentationCard.tsx +28 -14
- package/ui/docs/DocumentationPage.d.ts +2 -2
- package/ui/docs/DocumentationPage.js +13 -3
- package/ui/docs/DocumentationPage.tsx +57 -47
- package/ui/docs/FileCard.d.ts +6 -1
- package/ui/docs/FileCard.js +8 -5
- package/ui/docs/FileCard.tsx +19 -8
- package/ui/docs/FilePage.d.ts +1 -1
- package/ui/docs/FilePage.js +7 -2
- package/ui/docs/FilePage.tsx +14 -5
- package/ui/form/ArrayInput.js +4 -4
- package/ui/form/ArrayInput.tsx +7 -7
- package/ui/form/ArrayRadioInputs.js +2 -2
- package/ui/form/ArrayRadioInputs.tsx +3 -3
- package/ui/form/Button.d.ts +5 -5
- package/ui/form/Button.js +7 -7
- package/ui/form/Button.module.css +9 -9
- package/ui/form/Button.tsx +13 -14
- package/ui/form/ChoiceRadioInputs.js +2 -2
- package/ui/form/ChoiceRadioInputs.tsx +3 -3
- package/ui/form/Clickable.d.ts +4 -3
- package/ui/form/Clickable.js +3 -3
- package/ui/form/Clickable.tsx +6 -5
- package/ui/form/DataInput.js +2 -2
- package/ui/form/DataInput.tsx +3 -3
- package/ui/form/DictionaryInput.js +4 -4
- package/ui/form/DictionaryInput.tsx +7 -7
- package/ui/form/Field.module.css +2 -2
- package/ui/form/FormFooter.js +2 -2
- package/ui/form/FormFooter.tsx +3 -3
- package/ui/form/Input.js +4 -4
- package/ui/form/Input.module.css +12 -10
- package/ui/form/Input.tsx +4 -4
- package/ui/form/Popover.module.css +5 -5
- package/ui/form/QueryInput.js +1 -1
- package/ui/form/QueryInput.tsx +1 -1
- package/ui/form/SubmitButton.d.ts +1 -1
- package/ui/form/SubmitButton.js +3 -5
- package/ui/form/SubmitButton.tsx +3 -13
- package/ui/inline/Code.module.css +3 -2
- package/ui/inline/Deleted.module.css +1 -1
- package/ui/inline/Inserted.module.css +1 -1
- package/ui/inline/Link.module.css +1 -1
- package/ui/inline/Mark.module.css +5 -4
- package/ui/layout/Layout.module.css +6 -6
- package/ui/layout/SidebarLayout.d.ts +1 -0
- package/ui/layout/SidebarLayout.js +3 -2
- package/ui/layout/SidebarLayout.module.css +7 -7
- package/ui/layout/SidebarLayout.tsx +4 -3
- package/ui/menu/Menu.d.ts +5 -1
- package/ui/menu/Menu.js +6 -2
- package/ui/menu/Menu.module.css +38 -24
- package/ui/menu/Menu.tsx +6 -6
- package/ui/menu/MenuItem.d.ts +11 -8
- package/ui/menu/MenuItem.js +9 -10
- package/ui/menu/MenuItem.tsx +17 -16
- package/ui/misc/Color.d.ts +8 -0
- package/ui/misc/Color.module.css +52 -0
- package/ui/misc/Color.tsx +8 -0
- package/ui/misc/Mapper.d.ts +51 -0
- package/ui/misc/Mapper.js +55 -0
- package/ui/misc/Mapper.tsx +89 -0
- package/ui/misc/Markup.d.ts +15 -0
- package/ui/misc/Markup.js +15 -0
- package/ui/misc/Markup.tsx +23 -0
- package/ui/misc/Status.d.ts +6 -16
- package/ui/misc/Status.module.css +0 -65
- package/ui/misc/Status.tsx +6 -16
- package/ui/misc/StatusIcon.js +0 -1
- package/ui/misc/StatusIcon.tsx +0 -1
- package/ui/misc/Tag.module.css +5 -5
- package/ui/misc/Typography.d.ts +33 -0
- package/ui/misc/Typography.js +5 -0
- package/ui/misc/Typography.module.css +54 -0
- package/ui/misc/Typography.tsx +41 -0
- package/ui/misc/index.d.ts +3 -0
- package/ui/misc/index.js +3 -0
- package/ui/misc/index.tsx +3 -0
- package/ui/notice/Notice.d.ts +2 -2
- package/ui/notice/Notice.js +4 -4
- package/ui/notice/Notice.module.css +3 -3
- package/ui/notice/Notice.tsx +5 -5
- package/ui/notice/Notices.js +2 -2
- package/ui/notice/Notices.module.css +3 -3
- package/ui/notice/Notices.tsx +2 -2
- package/ui/page/Head.js +2 -2
- package/ui/page/Head.tsx +2 -1
- package/ui/tree/TreeApp.d.ts +1 -1
- package/ui/tree/TreeApp.js +3 -3
- package/ui/tree/TreeApp.tsx +3 -3
- package/ui/tree/TreeCards.d.ts +20 -17
- package/ui/tree/TreeCards.js +13 -17
- package/ui/tree/TreeCards.tsx +23 -26
- package/ui/tree/TreeMenu.d.ts +29 -10
- package/ui/tree/TreeMenu.js +30 -5
- package/ui/tree/TreeMenu.tsx +49 -17
- package/ui/tree/TreePage.d.ts +11 -15
- package/ui/tree/TreePage.js +12 -18
- package/ui/tree/TreePage.tsx +14 -26
- package/ui/tree/TreeSidebar.d.ts +16 -0
- package/ui/tree/TreeSidebar.js +14 -0
- package/ui/tree/TreeSidebar.tsx +28 -0
- package/ui/tree/index.d.ts +1 -3
- package/ui/tree/index.js +1 -3
- package/ui/tree/index.ts +1 -3
- package/util/element.d.ts +50 -52
- package/util/element.js +37 -75
- package/util/index.d.ts +1 -0
- package/util/index.js +1 -0
- package/util/link.d.ts +43 -0
- package/util/link.js +56 -0
- package/util/path.d.ts +26 -11
- package/util/path.js +12 -20
- package/util/uri.d.ts +9 -10
- package/util/uri.js +14 -20
- package/util/url.d.ts +11 -6
- package/util/url.js +22 -20
- package/ui/block/Element.d.ts +0 -9
- package/ui/block/Element.js +0 -7
- package/ui/block/Element.tsx +0 -14
- package/ui/block/Elements.js +0 -10
- package/ui/tree/TreeCards.module.css +0 -31
- package/ui/tree/TreeMenuItem.d.ts +0 -9
- package/ui/tree/TreeMenuItem.js +0 -12
- package/ui/tree/TreeMenuItem.tsx +0 -18
- package/ui/tree/TreePathContext.d.ts +0 -12
- package/ui/tree/TreePathContext.js +0 -18
- package/ui/tree/TreePathContext.tsx +0 -21
- package/ui/tree/TreeRenderer.d.ts +0 -28
- package/ui/tree/TreeRenderer.js +0 -41
- package/ui/tree/TreeRenderer.tsx +0 -73
package/README.md
CHANGED
|
@@ -2,40 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://conventionalcommits.org) [](https://github.com/dhoulb/shelving/actions/workflows/release.yaml) [](https://www.npmjs.com/package/shelving)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
TypeScript data toolkit — schema validation, database and API providers, observable state stores, React integration, and typed utility functions.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Documentation
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Full documentation is published at **<https://dhoulb.github.io/shelving/>**.
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
npm install shelving
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Shelving is an ES module. Import from the main package or from individual module subpaths:
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import { STRING, DataSchema } from "shelving"
|
|
19
|
-
import { MemoryDBProvider } from "shelving/db"
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Modules
|
|
23
|
-
|
|
24
|
-
| Module | Description |
|
|
25
|
-
|---|---|
|
|
26
|
-
| [schema](modules/schema/README.md) | Schema validation — the foundation of everything |
|
|
27
|
-
| [db](modules/db/README.md) | Database provider abstraction (Collections, providers, queries) |
|
|
28
|
-
| [api](modules/api/README.md) | API provider abstraction (Endpoints, providers, caching) |
|
|
29
|
-
| [store](modules/store/README.md) | Observable state containers, Suspense-compatible |
|
|
30
|
-
| [sequence](modules/sequence/README.md) | Async-iterable utilities (`DeferredSequence`) |
|
|
31
|
-
| [react](modules/react/README.md) | React hooks for stores and sequences |
|
|
32
|
-
| [error](modules/error/README.md) | Typed error classes |
|
|
33
|
-
| [util](modules/util/README.md) | Typed helpers for arrays, objects, strings, data, queries, updates |
|
|
34
|
-
| [markup](modules/markup/README.md) | Markdown renderer for user-facing content |
|
|
35
|
-
| [cloudflare](modules/cloudflare/README.md) | Cloudflare Workers providers (KV, D1) |
|
|
36
|
-
| [firestore](modules/firestore/README.md) | Firestore providers (client, lite, server) |
|
|
37
|
-
| [bun](modules/bun/README.md) | Bun PostgreSQL provider |
|
|
38
|
-
|
|
39
15
|
## Changelog
|
|
40
16
|
|
|
41
17
|
See [Releases](https://github.com/dhoulb/shelving/releases).
|
|
@@ -36,7 +36,7 @@ export declare class DirectoryExtractor extends Extractor<Path, DirectoryElement
|
|
|
36
36
|
private readonly _base;
|
|
37
37
|
private readonly _ignore;
|
|
38
38
|
constructor({ index, extractors, base, ignore }?: DirectoryExtractorOptions);
|
|
39
|
-
extract(
|
|
39
|
+
extract(source: Path): Promise<DirectoryElement>;
|
|
40
40
|
private _extractDirectory;
|
|
41
41
|
private _extractChild;
|
|
42
42
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { mergeElements } from "../util/element.js";
|
|
3
3
|
import { splitFileExtension } from "../util/file.js";
|
|
4
|
-
import { anyMatch, requirePath,
|
|
4
|
+
import { anyMatch, requirePath, splitPath } from "../util/index.js";
|
|
5
5
|
import { requireSlug } from "../util/string.js";
|
|
6
6
|
import { Extractor, mergeTreeElements } from "./Extractor.js";
|
|
7
7
|
import { FileExtractor } from "./FileExtractor.js";
|
|
@@ -60,12 +60,12 @@ export class DirectoryExtractor extends Extractor {
|
|
|
60
60
|
this._base = base;
|
|
61
61
|
this._ignore = ignore;
|
|
62
62
|
}
|
|
63
|
-
extract(
|
|
64
|
-
return this._extractDirectory(requirePath(
|
|
63
|
+
extract(source) {
|
|
64
|
+
return this._extractDirectory(requirePath(source, this._base, this.extract));
|
|
65
65
|
}
|
|
66
|
-
async _extractDirectory(
|
|
67
|
-
const name =
|
|
68
|
-
const entries = await readdir(
|
|
66
|
+
async _extractDirectory(source) {
|
|
67
|
+
const name = splitPath(source).at(-1) ?? "";
|
|
68
|
+
const entries = await readdir(source, { withFileTypes: true });
|
|
69
69
|
// Keep track of the current index entry and children by key, so we can merge same-key elements.
|
|
70
70
|
let index;
|
|
71
71
|
const items = {};
|
|
@@ -74,7 +74,7 @@ export class DirectoryExtractor extends Extractor {
|
|
|
74
74
|
if (anyMatch(entry.name, ...this._ignore))
|
|
75
75
|
continue;
|
|
76
76
|
// Extract the child element and possibly merge it.
|
|
77
|
-
const child = await this._extractChild(
|
|
77
|
+
const child = await this._extractChild(source, entry);
|
|
78
78
|
if (child) {
|
|
79
79
|
// Is this entry an index? If so, we'll treat it as the directory itself and merge it with any existing index entry if needed.
|
|
80
80
|
if (anyMatch(entry.name, ...this._indexes)) {
|
|
@@ -91,7 +91,7 @@ export class DirectoryExtractor extends Extractor {
|
|
|
91
91
|
type: "tree-directory",
|
|
92
92
|
key: requireSlug(name),
|
|
93
93
|
props: {
|
|
94
|
-
|
|
94
|
+
source,
|
|
95
95
|
name,
|
|
96
96
|
// `title` is only set when the absorbed index file has a confident one (e.g. README H1).
|
|
97
97
|
// Renderers fall back to `name` otherwise.
|
package/extract/Extractor.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare abstract class Extractor<I, O extends TreeElement = TreeElement>
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Merge two file elements with the same `key`.
|
|
20
|
-
* - `title` and `
|
|
20
|
+
* - `title` and `source` are taken from `primary` (the higher-priority element).
|
|
21
21
|
* - `description` is taken from `primary` if set, otherwise from `secondary`.
|
|
22
22
|
* - `content` and `children` from both are concatenated (primary first).
|
|
23
23
|
*/
|
package/extract/Extractor.js
CHANGED
|
@@ -21,10 +21,18 @@ export function mergeTreeElements(primary, secondary) {
|
|
|
21
21
|
props: {
|
|
22
22
|
...primary.props,
|
|
23
23
|
title: primary.props.title,
|
|
24
|
-
|
|
24
|
+
source: primary.props.source,
|
|
25
25
|
description: primary.props.description ?? secondary.props.description,
|
|
26
|
-
content:
|
|
26
|
+
content: _mergeContent(primary.props.content, secondary.props.content),
|
|
27
27
|
children: mergeElements(primary.props.children, secondary.props.children),
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
/** Merge two markup content strings — primary first, secondary appended after a blank line. Returns `undefined` if both are empty. */
|
|
32
|
+
function _mergeContent(a, b) {
|
|
33
|
+
if (!a)
|
|
34
|
+
return b;
|
|
35
|
+
if (!b)
|
|
36
|
+
return a;
|
|
37
|
+
return `${a}\n\n${b}`;
|
|
38
|
+
}
|
|
@@ -4,19 +4,21 @@ import { Extractor } from "./Extractor.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Base extractor for a file in a tree.
|
|
6
6
|
* - Reads the file's content as text and stores it in `content`.
|
|
7
|
-
* - Sets `
|
|
8
|
-
* - Sets `name` to the
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
7
|
+
* - Sets `source` to the file's absolute path (`BunFile.name`); throws `RequiredError` if missing or non-absolute.
|
|
8
|
+
* - Sets `name` to the basename without extension, preserving case (e.g. `"OptionalSchema"` from `"OptionalSchema.ts"`); URL paths use `name`.
|
|
9
|
+
* - Sets `key` to the slugified `name` (e.g. `"optionalschema"`) — only used by React for reconciliation and by `DirectoryExtractor` to merge same-key siblings (e.g. `TEMPLATE.md` + `template.ts`).
|
|
10
|
+
* - Does NOT set `title` — `title` is only set by subclasses that have a confident source for one (e.g. `MarkdownExtractor` uses the first `<h1>`). Renderers fall back to `name` when missing.
|
|
11
11
|
* - Subclasses (e.g. `MarkdownExtractor`, `TypescriptExtractor`) override `extractProps()` to parse the content into richer elements.
|
|
12
12
|
*/
|
|
13
13
|
export declare class FileExtractor extends Extractor<BunFile, FileElement> {
|
|
14
14
|
extract(file: BunFile): Promise<FileElement>;
|
|
15
15
|
/**
|
|
16
16
|
* Build the file element props from the extracted content.
|
|
17
|
-
* - `name` is the basename without extension (e.g. `"array"`) — display-ready, used by menus and
|
|
17
|
+
* - `name` is the basename without extension (e.g. `"array"`) — display-ready, used by menus, cards, and URL paths.
|
|
18
18
|
* - Override to parse `text` into richer elements (content/children/description) and to set
|
|
19
19
|
* `title` if a confident title is available.
|
|
20
20
|
*/
|
|
21
|
-
extractProps(name: string, content: string): FileElementProps
|
|
21
|
+
extractProps(name: string, content: string): Partial<FileElementProps> & {
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
22
24
|
}
|
package/extract/FileExtractor.js
CHANGED
|
@@ -1,30 +1,35 @@
|
|
|
1
|
+
import { RequiredError } from "../error/RequiredError.js";
|
|
1
2
|
import { splitFileExtension } from "../util/file.js";
|
|
2
|
-
import { isAbsolutePath,
|
|
3
|
+
import { isAbsolutePath, splitPath } from "../util/index.js";
|
|
3
4
|
import { requireSlug } from "../util/string.js";
|
|
4
5
|
import { Extractor } from "./Extractor.js";
|
|
5
6
|
/**
|
|
6
7
|
* Base extractor for a file in a tree.
|
|
7
8
|
* - Reads the file's content as text and stores it in `content`.
|
|
8
|
-
* - Sets `
|
|
9
|
-
* - Sets `name` to the
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
9
|
+
* - Sets `source` to the file's absolute path (`BunFile.name`); throws `RequiredError` if missing or non-absolute.
|
|
10
|
+
* - Sets `name` to the basename without extension, preserving case (e.g. `"OptionalSchema"` from `"OptionalSchema.ts"`); URL paths use `name`.
|
|
11
|
+
* - Sets `key` to the slugified `name` (e.g. `"optionalschema"`) — only used by React for reconciliation and by `DirectoryExtractor` to merge same-key siblings (e.g. `TEMPLATE.md` + `template.ts`).
|
|
12
|
+
* - Does NOT set `title` — `title` is only set by subclasses that have a confident source for one (e.g. `MarkdownExtractor` uses the first `<h1>`). Renderers fall back to `name` when missing.
|
|
12
13
|
* - Subclasses (e.g. `MarkdownExtractor`, `TypescriptExtractor`) override `extractProps()` to parse the content into richer elements.
|
|
13
14
|
*/
|
|
14
15
|
export class FileExtractor extends Extractor {
|
|
15
16
|
async extract(file) {
|
|
16
|
-
const
|
|
17
|
-
|
|
17
|
+
const source = file.name;
|
|
18
|
+
if (!source || !isAbsolutePath(source))
|
|
19
|
+
throw new RequiredError("FileExtractor requires an absolute file path", { received: source });
|
|
20
|
+
const filename = splitPath(source).at(-1) ?? "unnamed";
|
|
18
21
|
const [base = filename] = splitFileExtension(filename);
|
|
22
|
+
const text = await file.text();
|
|
23
|
+
const props = { ...this.extractProps(base, text), source };
|
|
19
24
|
return {
|
|
20
25
|
type: "tree-file",
|
|
21
26
|
key: requireSlug(base),
|
|
22
|
-
props
|
|
27
|
+
props,
|
|
23
28
|
};
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* Build the file element props from the extracted content.
|
|
27
|
-
* - `name` is the basename without extension (e.g. `"array"`) — display-ready, used by menus and
|
|
32
|
+
* - `name` is the basename without extension (e.g. `"array"`) — display-ready, used by menus, cards, and URL paths.
|
|
28
33
|
* - Override to parse `text` into richer elements (content/children/description) and to set
|
|
29
34
|
* `title` if a confident title is available.
|
|
30
35
|
*/
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { Elements, FileElementProps } from "../util/element.js";
|
|
1
|
+
import type { FileElementProps } from "../util/element.js";
|
|
3
2
|
import { FileExtractor } from "./FileExtractor.js";
|
|
4
3
|
/**
|
|
5
|
-
* File extractor
|
|
6
|
-
* -
|
|
7
|
-
* - Sets `title` from the first
|
|
4
|
+
* File extractor for Markdown files.
|
|
5
|
+
* - Stores the raw markdown text as `content`; rendering happens at output time via `<Markup>`.
|
|
6
|
+
* - Sets `title` from the first `# h1` heading if one is present — otherwise leaves it undefined
|
|
8
7
|
* (a confident title only).
|
|
9
8
|
*/
|
|
10
9
|
export declare class MarkdownExtractor extends FileExtractor {
|
|
11
10
|
/** Markdown contributes the canonical title/path when merging same-key elements. */
|
|
12
11
|
readonly priority = 10;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
render(text: string): Elements;
|
|
12
|
+
extractProps(name: string, text: string): Partial<FileElementProps> & {
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
17
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Find the first `# h1` heading in a markdown source string and return its text, or `undefined` if none.
|
|
18
|
+
* - Looks for a line starting with a single `#` followed by whitespace; doesn't match `##`+.
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractMarkdownTitle(text: string): string | undefined;
|
|
@@ -1,33 +1,22 @@
|
|
|
1
|
-
import { renderMarkup } from "../markup/render.js";
|
|
2
|
-
import { MARKUP_RULES } from "../markup/rule/index.js";
|
|
3
|
-
import { getElements, getElementText } from "../util/element.js";
|
|
4
1
|
import { FileExtractor } from "./FileExtractor.js";
|
|
5
2
|
/**
|
|
6
|
-
* File extractor
|
|
7
|
-
* -
|
|
8
|
-
* - Sets `title` from the first
|
|
3
|
+
* File extractor for Markdown files.
|
|
4
|
+
* - Stores the raw markdown text as `content`; rendering happens at output time via `<Markup>`.
|
|
5
|
+
* - Sets `title` from the first `# h1` heading if one is present — otherwise leaves it undefined
|
|
9
6
|
* (a confident title only).
|
|
10
7
|
*/
|
|
11
8
|
export class MarkdownExtractor extends FileExtractor {
|
|
12
9
|
/** Markdown contributes the canonical title/path when merging same-key elements. */
|
|
13
10
|
priority = 10;
|
|
14
|
-
_options;
|
|
15
|
-
constructor(options = { rules: MARKUP_RULES }) {
|
|
16
|
-
super();
|
|
17
|
-
this._options = options;
|
|
18
|
-
}
|
|
19
11
|
extractProps(name, text) {
|
|
20
|
-
|
|
21
|
-
const title = _getFirstHeadingText(content);
|
|
22
|
-
return { name, title, content };
|
|
23
|
-
}
|
|
24
|
-
render(text) {
|
|
25
|
-
return renderMarkup(text, this._options);
|
|
12
|
+
return { name, title: extractMarkdownTitle(text), content: text };
|
|
26
13
|
}
|
|
27
14
|
}
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Find the first `# h1` heading in a markdown source string and return its text, or `undefined` if none.
|
|
17
|
+
* - Looks for a line starting with a single `#` followed by whitespace; doesn't match `##`+.
|
|
18
|
+
*/
|
|
19
|
+
export function extractMarkdownTitle(text) {
|
|
20
|
+
const match = text.match(/^#\s+(.+?)\s*$/m);
|
|
21
|
+
return match?.[1];
|
|
33
22
|
}
|
|
@@ -9,5 +9,7 @@ import { FileExtractor } from "./FileExtractor.js";
|
|
|
9
9
|
* - Does not set `title` — TS source files have no confident title source. The renderer falls back to `name`.
|
|
10
10
|
*/
|
|
11
11
|
export declare class TypescriptExtractor extends FileExtractor {
|
|
12
|
-
extractProps(name: string, text: string): FileElementProps
|
|
12
|
+
extractProps(name: string, text: string): Partial<FileElementProps> & {
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
13
15
|
}
|
|
@@ -33,8 +33,8 @@ function _mergeOverloads(existing, next) {
|
|
|
33
33
|
const b = next.props;
|
|
34
34
|
const merged = {
|
|
35
35
|
...a,
|
|
36
|
-
// Keep first
|
|
37
|
-
|
|
36
|
+
// Keep first content encountered; fill in if `existing` had none.
|
|
37
|
+
content: a.content ?? b.content,
|
|
38
38
|
// Append signatures.
|
|
39
39
|
signatures: _concat(a.signatures, b.signatures),
|
|
40
40
|
// Append params, returns, throws, examples — never dedupe (per spec).
|
|
@@ -97,7 +97,7 @@ function _extractStatement(statement, source) {
|
|
|
97
97
|
props: {
|
|
98
98
|
name,
|
|
99
99
|
kind,
|
|
100
|
-
|
|
100
|
+
content: _buildJSDocContent(jsDoc?.description, jsDoc?.unhandled),
|
|
101
101
|
signatures: signature ? [signature] : undefined,
|
|
102
102
|
params,
|
|
103
103
|
returns,
|
|
@@ -107,6 +107,18 @@ function _extractStatement(statement, source) {
|
|
|
107
107
|
},
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Combine the JSDoc leading-description text and any unhandled `@rule` blocks into a single markup content string.
|
|
112
|
+
* - Unhandled rules (anything not `@param`/`@returns`/`@throws`/`@example`) are appended after the description, separated by blank lines, with their `@name` preserved.
|
|
113
|
+
* - Returns `undefined` if both are empty.
|
|
114
|
+
*/
|
|
115
|
+
function _buildJSDocContent(description, unhandled) {
|
|
116
|
+
if (!description)
|
|
117
|
+
return unhandled;
|
|
118
|
+
if (!unhandled)
|
|
119
|
+
return description;
|
|
120
|
+
return `${description}\n\n${unhandled}`;
|
|
121
|
+
}
|
|
110
122
|
/** Check if a statement has an `export` modifier. */
|
|
111
123
|
function _isExported(statement) {
|
|
112
124
|
const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
|
|
@@ -199,7 +211,8 @@ function _getClassMembers(statement, source) {
|
|
|
199
211
|
if (modifiers?.some(m => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword))
|
|
200
212
|
continue;
|
|
201
213
|
}
|
|
202
|
-
const
|
|
214
|
+
const memberJSDoc = _getJSDoc(member, source);
|
|
215
|
+
const content = _buildJSDocContent(memberJSDoc?.description, memberJSDoc?.unhandled);
|
|
203
216
|
if (ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) {
|
|
204
217
|
const params = member.parameters.map(p => p.getText(source)).join(", ");
|
|
205
218
|
const ret = member.type ? member.type.getText(source) : "void";
|
|
@@ -217,7 +230,7 @@ function _getClassMembers(statement, source) {
|
|
|
217
230
|
members.push({
|
|
218
231
|
type: "tree-documentation",
|
|
219
232
|
key,
|
|
220
|
-
props: { name,
|
|
233
|
+
props: { name, content, kind: "method", signatures: [signature] },
|
|
221
234
|
});
|
|
222
235
|
}
|
|
223
236
|
}
|
|
@@ -226,12 +239,14 @@ function _getClassMembers(statement, source) {
|
|
|
226
239
|
members.push({
|
|
227
240
|
type: "tree-documentation",
|
|
228
241
|
key: requireSlug(name),
|
|
229
|
-
props: { name,
|
|
242
|
+
props: { name, content, kind: "property", signatures: type ? [type] : undefined },
|
|
230
243
|
});
|
|
231
244
|
}
|
|
232
245
|
}
|
|
233
246
|
return members.length ? members : undefined;
|
|
234
247
|
}
|
|
248
|
+
/** `@rule` names handled by dedicated parsers — everything else is appended to `unhandled` as raw markup. */
|
|
249
|
+
const _HANDLED_RULES = new Set(["param", "params", "return", "returns", "throw", "throws", "example", "examples"]);
|
|
235
250
|
/** Extract JSDoc from a node. */
|
|
236
251
|
function _getJSDoc(node, source) {
|
|
237
252
|
const ranges = ts.getLeadingCommentRanges(source.text, node.pos);
|
|
@@ -250,15 +265,54 @@ function _getJSDoc(node, source) {
|
|
|
250
265
|
const returns = _parseJSDocReturns(text);
|
|
251
266
|
const throws = _parseJSDocThrows(text);
|
|
252
267
|
const examples = _parseJSDocExamples(text);
|
|
268
|
+
const unhandled = _parseJSDocUnhandled(text);
|
|
253
269
|
return {
|
|
254
270
|
description: description || undefined,
|
|
255
271
|
params: params.length ? params : undefined,
|
|
256
272
|
returns: returns.length ? returns : undefined,
|
|
257
273
|
throws: throws.length ? throws : undefined,
|
|
258
274
|
examples: examples.length ? examples : undefined,
|
|
275
|
+
unhandled,
|
|
259
276
|
};
|
|
260
277
|
}
|
|
261
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Walk the JSDoc body for `@rule` blocks not handled by dedicated parsers (param/returns/throws/example).
|
|
281
|
+
* - Each rule block extends from its `@name` line up to the next `@rule` or the end of the docblock.
|
|
282
|
+
* - Returns the unhandled blocks joined by blank lines as `@name body`, preserved verbatim for downstream markup rendering.
|
|
283
|
+
* - Returns `undefined` if every rule is handled or there are none.
|
|
284
|
+
*/
|
|
285
|
+
function _parseJSDocUnhandled(text) {
|
|
286
|
+
const body = text
|
|
287
|
+
.replace(/^\/\*\*\s*/, "")
|
|
288
|
+
.replace(/\s*\*\/$/, "")
|
|
289
|
+
.split("\n")
|
|
290
|
+
.map(l => l.replace(/^\s*\*\s?/, ""))
|
|
291
|
+
.join("\n");
|
|
292
|
+
const sections = [];
|
|
293
|
+
let currentName;
|
|
294
|
+
let currentLines = [];
|
|
295
|
+
const flush = () => {
|
|
296
|
+
if (currentName && !_HANDLED_RULES.has(currentName)) {
|
|
297
|
+
sections.push(`@${currentName} ${currentLines.join("\n")}`.trimEnd());
|
|
298
|
+
}
|
|
299
|
+
currentName = undefined;
|
|
300
|
+
currentLines = [];
|
|
301
|
+
};
|
|
302
|
+
for (const line of body.split("\n")) {
|
|
303
|
+
const match = line.match(/^@(\w+)\s*(.*)$/);
|
|
304
|
+
if (match) {
|
|
305
|
+
flush();
|
|
306
|
+
currentName = match[1];
|
|
307
|
+
currentLines = match[2] ? [match[2]] : [];
|
|
308
|
+
}
|
|
309
|
+
else if (currentName) {
|
|
310
|
+
currentLines.push(line);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
flush();
|
|
314
|
+
return sections.length ? sections.join("\n\n") : undefined;
|
|
315
|
+
}
|
|
262
316
|
/** Parse a JSDoc comment block into its description text. */
|
|
263
317
|
function _parseJSDocComment(text) {
|
|
264
318
|
const lines = text
|
package/markup/rule/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MarkupOptions } from "../util/options.js";
|
|
1
2
|
import type { MarkupRules } from "../util/rule.js";
|
|
2
3
|
/** Markup rules that work in a block context. */
|
|
3
4
|
export declare const MARKUP_RULES_BLOCK: MarkupRules;
|
|
@@ -30,6 +31,8 @@ export declare const MARKUP_RULES_INLINE: MarkupRules;
|
|
|
30
31
|
* - If the first thing in the definition isn't a URL, then it's recognised as a sidenote/footnote and tapping it will scroll you to that point (and popup the definition like Marco Arment's Bigfoot code).
|
|
31
32
|
*/
|
|
32
33
|
export declare const MARKUP_RULES: MarkupRules;
|
|
34
|
+
/** Default markup options — uses `MARKUP_RULES` with no other overrides. */
|
|
35
|
+
export declare const MARKUP_OPTIONS: MarkupOptions;
|
|
33
36
|
export * from "./blockquote.js";
|
|
34
37
|
export * from "./code.js";
|
|
35
38
|
export * from "./fenced.js";
|
package/markup/rule/index.js
CHANGED
|
@@ -48,6 +48,8 @@ export const MARKUP_RULES_INLINE = [CODE_RULE, LINK_RULE, AUTOLINK_RULE, INLINE_
|
|
|
48
48
|
* - If the first thing in the definition isn't a URL, then it's recognised as a sidenote/footnote and tapping it will scroll you to that point (and popup the definition like Marco Arment's Bigfoot code).
|
|
49
49
|
*/
|
|
50
50
|
export const MARKUP_RULES = [...MARKUP_RULES_BLOCK, ...MARKUP_RULES_INLINE];
|
|
51
|
+
/** Default markup options — uses `MARKUP_RULES` with no other overrides. */
|
|
52
|
+
export const MARKUP_OPTIONS = { rules: MARKUP_RULES };
|
|
51
53
|
export * from "./blockquote.js";
|
|
52
54
|
export * from "./code.js";
|
|
53
55
|
export * from "./fenced.js";
|
package/markup/rule/link.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { formatURI } from "../../util/format.js";
|
|
2
|
+
import { getLink } from "../../util/link.js";
|
|
2
3
|
import { getRegExp } from "../../util/regexp.js";
|
|
3
|
-
import {
|
|
4
|
-
import { getURL } from "../../util/url.js";
|
|
4
|
+
import { HTTP_SCHEMES } from "../../util/uri.js";
|
|
5
5
|
import { renderMarkup } from "../render.js";
|
|
6
6
|
import { REACT_ELEMENT_TYPE } from "../util/internal.js";
|
|
7
7
|
import { createMarkupRule } from "../util/rule.js";
|
|
8
8
|
/** Render `<a href="">` if the link is a valid one, or `<a>` (with no `href`) if it isn't. */
|
|
9
9
|
function renderLinkMarkupRule({ groups: { title, href: unsafeHref } }, options, key) {
|
|
10
|
-
const {
|
|
11
|
-
const
|
|
12
|
-
const href =
|
|
13
|
-
const children = title ? renderMarkup(title, options, "link") :
|
|
10
|
+
const { url, root, schemes = HTTP_SCHEMES, rel } = options;
|
|
11
|
+
const resolved = getLink(unsafeHref, url, root);
|
|
12
|
+
const href = resolved && schemes.some(s => resolved.startsWith(s)) ? resolved : undefined;
|
|
13
|
+
const children = title ? renderMarkup(title, options, "link") : resolved ? formatURI(resolved) : "";
|
|
14
14
|
return {
|
|
15
15
|
key,
|
|
16
16
|
$$typeof: REACT_ELEMENT_TYPE,
|
package/markup/util/options.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { URISchemes } from "../../util/uri.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ImmutableURL } from "../../util/url.js";
|
|
3
3
|
import type { MarkupRules } from "./rule.js";
|
|
4
4
|
/** The current parsing options (represents the current state of the parsing). */
|
|
5
5
|
export type MarkupOptions = {
|
|
@@ -11,10 +11,15 @@ export type MarkupOptions = {
|
|
|
11
11
|
*/
|
|
12
12
|
readonly rel?: string | undefined;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* @default
|
|
14
|
+
* Current page URL — used as the base for resolving relative refs (`./foo`, `#x`, bare segments) in link hrefs.
|
|
15
|
+
* @default Falls back to `root` if not set.
|
|
16
16
|
*/
|
|
17
|
-
readonly
|
|
17
|
+
readonly url?: ImmutableURL | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Site root URL — used as the base for resolving site-absolute path hrefs (`/foo`), honoring its subfolder.
|
|
20
|
+
* @default Falls back to `url` if not set.
|
|
21
|
+
*/
|
|
22
|
+
readonly root?: ImmutableURL | undefined;
|
|
18
23
|
/**
|
|
19
24
|
* Valid URI schemes/protocols for URLs and URIs.
|
|
20
25
|
* @example ["http:", "https:"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shelving",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.209.0",
|
|
4
4
|
"author": "Dave Houlbrooke <dave@shax.com>",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"@types/bun": "^1.3.14",
|
|
16
16
|
"@types/react": "^19.2.14",
|
|
17
17
|
"@types/react-dom": "^19.2.3",
|
|
18
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
18
|
+
"@typescript/native-preview": "^7.0.0-dev.20260516.1",
|
|
19
19
|
"firebase": "^12.13.0",
|
|
20
20
|
"react": "^19.3.0-canary-fef12a01-20260413",
|
|
21
21
|
"react-dom": "^19.3.0-canary-fef12a01-20260413",
|