shelving 1.208.0 → 1.210.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.
Files changed (231) hide show
  1. package/README.md +3 -27
  2. package/extract/DirectoryExtractor.d.ts +1 -1
  3. package/extract/DirectoryExtractor.js +8 -8
  4. package/extract/Extractor.d.ts +1 -1
  5. package/extract/Extractor.js +10 -2
  6. package/extract/FileExtractor.d.ts +8 -6
  7. package/extract/FileExtractor.js +14 -9
  8. package/extract/MarkdownExtractor.d.ts +12 -9
  9. package/extract/MarkdownExtractor.js +11 -22
  10. package/extract/TypescriptExtractor.d.ts +3 -1
  11. package/extract/TypescriptExtractor.js +60 -6
  12. package/markup/rule/index.d.ts +3 -0
  13. package/markup/rule/index.js +2 -0
  14. package/markup/rule/link.js +6 -6
  15. package/markup/util/options.d.ts +9 -4
  16. package/package.json +3 -3
  17. package/ui/app/App.module.css +63 -33
  18. package/ui/block/Address.module.css +1 -1
  19. package/ui/block/Block.d.ts +8 -0
  20. package/ui/block/Block.js +8 -0
  21. package/ui/block/{Element.module.css → Block.module.css} +2 -2
  22. package/ui/block/Block.tsx +15 -0
  23. package/ui/block/Blockquote.module.css +4 -4
  24. package/ui/block/Card.d.ts +16 -8
  25. package/ui/block/Card.js +14 -5
  26. package/ui/block/Card.module.css +43 -4
  27. package/ui/block/Card.tsx +24 -10
  28. package/ui/block/Definitions.d.ts +30 -0
  29. package/ui/block/Definitions.js +25 -0
  30. package/ui/block/Definitions.module.css +60 -0
  31. package/ui/block/Definitions.tsx +45 -0
  32. package/ui/block/Divider.module.css +2 -2
  33. package/ui/block/Figure.module.css +2 -2
  34. package/ui/block/{Elements.d.ts → Flex.d.ts} +7 -7
  35. package/ui/block/Flex.js +10 -0
  36. package/ui/block/{Elements.module.css → Flex.module.css} +3 -3
  37. package/ui/block/{Elements.tsx → Flex.tsx} +10 -10
  38. package/ui/block/Heading.module.css +19 -3
  39. package/ui/block/Image.module.css +1 -1
  40. package/ui/block/List.module.css +1 -1
  41. package/ui/block/Paragraph.module.css +2 -2
  42. package/ui/block/Preformatted.d.ts +8 -1
  43. package/ui/block/Preformatted.js +8 -2
  44. package/ui/block/Preformatted.module.css +15 -3
  45. package/ui/block/Preformatted.tsx +10 -2
  46. package/ui/block/Prose.js +2 -1
  47. package/ui/block/Prose.module.css +1 -1
  48. package/ui/block/Prose.tsx +2 -0
  49. package/ui/block/Section.module.css +2 -2
  50. package/ui/block/Subheading.module.css +18 -3
  51. package/ui/block/Table.module.css +7 -7
  52. package/ui/block/Video.module.css +11 -11
  53. package/ui/block/index.d.ts +3 -2
  54. package/ui/block/index.js +3 -2
  55. package/ui/block/index.ts +3 -2
  56. package/ui/dialog/Dialog.module.css +3 -3
  57. package/ui/dialog/Modal.module.css +4 -4
  58. package/ui/docs/DirectoryCard.d.ts +6 -1
  59. package/ui/docs/DirectoryCard.js +8 -5
  60. package/ui/docs/DirectoryCard.tsx +19 -8
  61. package/ui/docs/DirectoryPage.d.ts +1 -1
  62. package/ui/docs/DirectoryPage.js +4 -2
  63. package/ui/docs/DirectoryPage.tsx +10 -5
  64. package/ui/docs/DocumentationCard.d.ts +6 -1
  65. package/ui/docs/DocumentationCard.js +11 -5
  66. package/ui/docs/DocumentationCard.tsx +28 -14
  67. package/ui/docs/DocumentationPage.d.ts +2 -2
  68. package/ui/docs/DocumentationPage.js +13 -3
  69. package/ui/docs/DocumentationPage.tsx +57 -47
  70. package/ui/docs/FileCard.d.ts +6 -1
  71. package/ui/docs/FileCard.js +8 -5
  72. package/ui/docs/FileCard.tsx +19 -8
  73. package/ui/docs/FilePage.d.ts +1 -1
  74. package/ui/docs/FilePage.js +7 -2
  75. package/ui/docs/FilePage.tsx +14 -5
  76. package/ui/form/ArrayInput.d.ts +1 -1
  77. package/ui/form/ArrayInput.js +5 -5
  78. package/ui/form/ArrayInput.tsx +11 -10
  79. package/ui/form/ArrayRadioInputs.js +2 -2
  80. package/ui/form/ArrayRadioInputs.tsx +3 -3
  81. package/ui/form/Button.d.ts +6 -6
  82. package/ui/form/Button.js +10 -17
  83. package/ui/form/Button.module.css +9 -9
  84. package/ui/form/Button.tsx +15 -27
  85. package/ui/form/ButtonInput.d.ts +1 -1
  86. package/ui/form/ButtonInput.js +5 -15
  87. package/ui/form/ButtonInput.tsx +7 -24
  88. package/ui/form/CheckboxInput.js +2 -2
  89. package/ui/form/CheckboxInput.tsx +2 -2
  90. package/ui/form/ChoiceRadioInputs.js +2 -2
  91. package/ui/form/ChoiceRadioInputs.tsx +3 -3
  92. package/ui/form/Clickable.d.ts +12 -4
  93. package/ui/form/Clickable.js +12 -7
  94. package/ui/form/Clickable.tsx +21 -16
  95. package/ui/form/DataInput.js +2 -2
  96. package/ui/form/DataInput.tsx +3 -3
  97. package/ui/form/DictionaryInput.d.ts +1 -1
  98. package/ui/form/DictionaryInput.js +5 -5
  99. package/ui/form/DictionaryInput.tsx +11 -10
  100. package/ui/form/Field.module.css +2 -2
  101. package/ui/form/FormFooter.js +2 -2
  102. package/ui/form/FormFooter.tsx +3 -3
  103. package/ui/form/Input.d.ts +8 -8
  104. package/ui/form/Input.js +11 -24
  105. package/ui/form/Input.module.css +12 -10
  106. package/ui/form/Input.tsx +11 -35
  107. package/ui/form/OutputInput.d.ts +7 -0
  108. package/ui/form/OutputInput.js +8 -0
  109. package/ui/form/OutputInput.tsx +17 -0
  110. package/ui/form/Popover.module.css +5 -5
  111. package/ui/form/QueryInput.js +1 -1
  112. package/ui/form/QueryInput.tsx +1 -1
  113. package/ui/form/RadioInput.js +2 -2
  114. package/ui/form/RadioInput.tsx +2 -2
  115. package/ui/form/SubmitButton.d.ts +1 -1
  116. package/ui/form/SubmitButton.js +3 -5
  117. package/ui/form/SubmitButton.tsx +3 -13
  118. package/ui/form/index.d.ts +1 -0
  119. package/ui/form/index.js +1 -0
  120. package/ui/form/index.ts +1 -0
  121. package/ui/inline/Code.module.css +3 -2
  122. package/ui/inline/Deleted.module.css +1 -1
  123. package/ui/inline/Inserted.module.css +1 -1
  124. package/ui/inline/Link.js +3 -2
  125. package/ui/inline/Link.module.css +1 -1
  126. package/ui/inline/Link.tsx +2 -2
  127. package/ui/inline/Mark.module.css +5 -4
  128. package/ui/layout/Layout.module.css +6 -6
  129. package/ui/layout/SidebarLayout.d.ts +1 -0
  130. package/ui/layout/SidebarLayout.js +3 -2
  131. package/ui/layout/SidebarLayout.module.css +7 -7
  132. package/ui/layout/SidebarLayout.tsx +4 -3
  133. package/ui/menu/Menu.d.ts +5 -1
  134. package/ui/menu/Menu.js +6 -2
  135. package/ui/menu/Menu.module.css +38 -24
  136. package/ui/menu/Menu.tsx +6 -6
  137. package/ui/menu/MenuItem.d.ts +11 -10
  138. package/ui/menu/MenuItem.js +11 -13
  139. package/ui/menu/MenuItem.tsx +20 -22
  140. package/ui/misc/Color.d.ts +10 -2
  141. package/ui/misc/Color.module.css +52 -0
  142. package/ui/misc/Color.tsx +10 -2
  143. package/ui/misc/Mapper.d.ts +51 -0
  144. package/ui/misc/Mapper.js +55 -0
  145. package/ui/misc/Mapper.tsx +89 -0
  146. package/ui/misc/Markup.d.ts +16 -0
  147. package/ui/misc/Markup.js +19 -0
  148. package/ui/misc/Markup.tsx +27 -0
  149. package/ui/misc/Status.d.ts +8 -18
  150. package/ui/misc/Status.module.css +0 -65
  151. package/ui/misc/Status.tsx +8 -18
  152. package/ui/misc/StatusIcon.js +0 -1
  153. package/ui/misc/StatusIcon.tsx +0 -1
  154. package/ui/misc/Tag.d.ts +2 -3
  155. package/ui/misc/Tag.js +8 -5
  156. package/ui/misc/Tag.module.css +5 -5
  157. package/ui/misc/Tag.tsx +11 -12
  158. package/ui/misc/Typography.d.ts +33 -0
  159. package/ui/misc/Typography.js +5 -0
  160. package/ui/misc/Typography.module.css +54 -0
  161. package/ui/misc/Typography.tsx +41 -0
  162. package/ui/misc/index.d.ts +3 -0
  163. package/ui/misc/index.js +3 -0
  164. package/ui/misc/index.tsx +3 -0
  165. package/ui/notice/Notice.d.ts +2 -2
  166. package/ui/notice/Notice.js +4 -4
  167. package/ui/notice/Notice.module.css +3 -3
  168. package/ui/notice/Notice.tsx +5 -5
  169. package/ui/notice/Notices.js +2 -2
  170. package/ui/notice/Notices.module.css +3 -3
  171. package/ui/notice/Notices.tsx +2 -2
  172. package/ui/page/HTML.js +1 -1
  173. package/ui/page/HTML.tsx +1 -1
  174. package/ui/page/Head.js +19 -24
  175. package/ui/page/Head.tsx +16 -21
  176. package/ui/router/Navigation.js +2 -2
  177. package/ui/router/Navigation.tsx +2 -2
  178. package/ui/router/Router.js +1 -1
  179. package/ui/router/Router.tsx +1 -1
  180. package/ui/tree/TreeApp.d.ts +1 -1
  181. package/ui/tree/TreeApp.js +3 -3
  182. package/ui/tree/TreeApp.tsx +3 -3
  183. package/ui/tree/TreeCards.d.ts +20 -17
  184. package/ui/tree/TreeCards.js +13 -17
  185. package/ui/tree/TreeCards.tsx +23 -26
  186. package/ui/tree/TreeMenu.d.ts +29 -10
  187. package/ui/tree/TreeMenu.js +30 -5
  188. package/ui/tree/TreeMenu.tsx +49 -17
  189. package/ui/tree/TreePage.d.ts +11 -15
  190. package/ui/tree/TreePage.js +12 -18
  191. package/ui/tree/TreePage.tsx +14 -26
  192. package/ui/tree/TreeSidebar.d.ts +16 -0
  193. package/ui/tree/TreeSidebar.js +14 -0
  194. package/ui/tree/TreeSidebar.tsx +28 -0
  195. package/ui/tree/index.d.ts +1 -3
  196. package/ui/tree/index.js +1 -3
  197. package/ui/tree/index.ts +1 -3
  198. package/ui/util/css.d.ts +13 -9
  199. package/ui/util/css.js +4 -12
  200. package/ui/util/css.ts +21 -20
  201. package/ui/util/meta.d.ts +37 -12
  202. package/ui/util/meta.js +52 -3
  203. package/ui/util/meta.ts +101 -15
  204. package/util/element.d.ts +50 -52
  205. package/util/element.js +37 -75
  206. package/util/index.d.ts +1 -0
  207. package/util/index.js +1 -0
  208. package/util/link.d.ts +43 -0
  209. package/util/link.js +57 -0
  210. package/util/path.d.ts +26 -11
  211. package/util/path.js +12 -20
  212. package/util/transform.d.ts +4 -2
  213. package/util/transform.js +6 -1
  214. package/util/uri.d.ts +9 -10
  215. package/util/uri.js +14 -20
  216. package/util/url.d.ts +14 -9
  217. package/util/url.js +33 -27
  218. package/ui/block/Element.d.ts +0 -9
  219. package/ui/block/Element.js +0 -7
  220. package/ui/block/Element.tsx +0 -14
  221. package/ui/block/Elements.js +0 -10
  222. package/ui/tree/TreeCards.module.css +0 -31
  223. package/ui/tree/TreeMenuItem.d.ts +0 -9
  224. package/ui/tree/TreeMenuItem.js +0 -12
  225. package/ui/tree/TreeMenuItem.tsx +0 -18
  226. package/ui/tree/TreePathContext.d.ts +0 -12
  227. package/ui/tree/TreePathContext.js +0 -18
  228. package/ui/tree/TreePathContext.tsx +0 -21
  229. package/ui/tree/TreeRenderer.d.ts +0 -28
  230. package/ui/tree/TreeRenderer.js +0 -41
  231. package/ui/tree/TreeRenderer.tsx +0 -73
package/README.md CHANGED
@@ -2,40 +2,16 @@
2
2
 
3
3
  [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) [![Release](https://github.com/dhoulb/shelving/actions/workflows/release.yaml/badge.svg)](https://github.com/dhoulb/shelving/actions/workflows/release.yaml) [![npm](https://img.shields.io/npm/dm/shelving.svg)](https://www.npmjs.com/package/shelving)
4
4
 
5
- Shelving is a TypeScript toolkit for working with typed data. At its core it is a schema validation library — every schema has a `validate()` method that returns a typed value or throws a human-readable error. On top of that it provides a database provider abstraction, an API provider abstraction, observable state stores, React integration, and a large set of typed utility functions.
5
+ TypeScript data toolkit schema validation, database and API providers, observable state stores, React integration, and typed utility functions.
6
6
 
7
- > Note: Shelving is in active development and does not yet follow semver.
7
+ ## Documentation
8
8
 
9
- ## Installation
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(path: Path): Promise<DirectoryElement>;
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, splitAbsolutePath } from "../util/index.js";
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(path) {
64
- return this._extractDirectory(requirePath(path, this._base, this.extract));
63
+ extract(source) {
64
+ return this._extractDirectory(requirePath(source, this._base, this.extract));
65
65
  }
66
- async _extractDirectory(path) {
67
- const name = splitAbsolutePath(path).at(-1) ?? "";
68
- const entries = await readdir(path, { withFileTypes: true });
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(path, entry);
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
- path,
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.
@@ -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 `path` are taken from `primary` (the higher-priority element).
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
  */
@@ -21,10 +21,18 @@ export function mergeTreeElements(primary, secondary) {
21
21
  props: {
22
22
  ...primary.props,
23
23
  title: primary.props.title,
24
- path: primary.props.path,
24
+ source: primary.props.source,
25
25
  description: primary.props.description ?? secondary.props.description,
26
- content: mergeElements(primary.props.content, secondary.props.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 `key` to the slugified basename without extension.
8
- * - Sets `name` to the display-ready basename without extension (e.g. `"OptionalSchema"`, not `"OptionalSchema.ts"`).
9
- * - Does NOT set `title` `title` is only set by subclasses that have a confident source for one
10
- * (e.g. `MarkdownExtractor` uses the first `<h1>`). Renderers fall back to `name` when missing.
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 cards.
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
  }
@@ -1,30 +1,35 @@
1
+ import { RequiredError } from "../error/RequiredError.js";
1
2
  import { splitFileExtension } from "../util/file.js";
2
- import { isAbsolutePath, splitAbsolutePath } from "../util/index.js";
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 `key` to the slugified basename without extension.
9
- * - Sets `name` to the display-ready basename without extension (e.g. `"OptionalSchema"`, not `"OptionalSchema.ts"`).
10
- * - Does NOT set `title` `title` is only set by subclasses that have a confident source for one
11
- * (e.g. `MarkdownExtractor` uses the first `<h1>`). Renderers fall back to `name` when missing.
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 path = file.name ?? "unnamed";
17
- const filename = isAbsolutePath(path) ? (splitAbsolutePath(path).at(-1) ?? "unnamed") : path;
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: this.extractProps(base, await file.text()),
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 cards.
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 { MarkupOptions } from "../markup/util/options.js";
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 that parses a Markdown file into a tree element.
6
- * - Parses the file content using the markup module.
7
- * - Sets `title` from the first `<h1>` heading if one is present — otherwise leaves it undefined
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
- private readonly _options;
14
- constructor(options?: MarkupOptions);
15
- extractProps(name: string, text: string): FileElementProps;
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 that parses a Markdown file into a tree element.
7
- * - Parses the file content using the markup module.
8
- * - Sets `title` from the first `<h1>` heading if one is present — otherwise leaves it undefined
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
- const content = this.render(text);
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
- /** Find the first `h1` element and extract its text content. */
29
- function _getFirstHeadingText(markdown) {
30
- for (const el of getElements(markdown))
31
- if (el.type === "h1")
32
- return getElementText(el.props.children) || undefined;
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 description encountered; fill in if `existing` had none.
37
- description: a.description ?? b.description,
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
- description: jsDoc?.description,
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 description = _getJSDoc(member, source)?.description;
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, description, kind: "method", signatures: [signature] },
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, description, kind: "property", signatures: type ? [type] : undefined },
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
@@ -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";
@@ -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";
@@ -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 { getURI, HTTP_SCHEMES } from "../../util/uri.js";
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 { base, schemes = HTTP_SCHEMES, rel } = options;
11
- const uri = getURL(unsafeHref, base) ?? getURI(unsafeHref);
12
- const href = uri && schemes.includes(uri.protocol) ? uri.href : undefined;
13
- const children = title ? renderMarkup(title, options, "link") : uri ? formatURI(uri) : "";
10
+ const { url, root, schemes = HTTP_SCHEMES, rel } = options;
11
+ const link = getLink(unsafeHref, url, root);
12
+ const href = link && schemes.includes(link.protocol) ? link?.href : undefined;
13
+ const children = title ? renderMarkup(title, options, "link") : link ? formatURI(link) : "";
14
14
  return {
15
15
  key,
16
16
  $$typeof: REACT_ELEMENT_TYPE,
@@ -1,5 +1,5 @@
1
1
  import type { URISchemes } from "../../util/uri.js";
2
- import type { URLString } from "../../util/url.js";
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
- * Set the base URL that any relative URLs will be relative to
15
- * @default `window.location.href` in browser environments, and `undefined` in server environments.
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 base?: URLString | undefined;
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.208.0",
3
+ "version": "1.210.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.20260514.1",
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",
@@ -76,7 +76,7 @@
76
76
  "build": "bun run --sequential build:*",
77
77
  "build:0:setup": "rm -rf ./dist && mkdir -p ./dist",
78
78
  "build:1:copy": "cp package.json dist/package.json && cp LICENSE.md dist/LICENSE.md && cp README.md dist/README.md && cp .npmignore dist/.npmignore && cp -r modules/ui dist/ui",
79
- "build:2:emit": "tsgo",
79
+ "build:2:emit": "tsgo -p tsconfig.build.json",
80
80
  "build:3:syntax": "bun run ./dist/index.js",
81
81
  "build:4:unit": "bun test ./dist/**/*.test.js --concurrent --only-failures --bail"
82
82
  },