shelving 1.204.0 → 1.205.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/package.json +1 -1
- package/ui/page/Head.js +30 -18
- package/ui/page/Head.tsx +40 -23
- package/ui/tree/TreeApp.d.ts +8 -6
- package/ui/tree/TreeApp.js +7 -6
- package/ui/tree/TreeApp.tsx +14 -11
- package/ui/tree/TreeCards.d.ts +3 -3
- package/ui/tree/TreeCards.js +1 -1
- package/ui/tree/TreeCards.tsx +3 -3
- package/ui/tree/TreeMenu.d.ts +5 -5
- package/ui/tree/TreeMenu.js +3 -3
- package/ui/tree/TreeMenu.tsx +6 -6
- package/ui/tree/TreePage.d.ts +5 -5
- package/ui/tree/TreePage.js +4 -3
- package/ui/tree/TreePage.tsx +6 -6
- package/ui/util/meta.d.ts +1 -1
- package/ui/util/meta.ts +1 -1
- package/util/element.d.ts +25 -29
- package/util/element.js +39 -37
- package/util/path.d.ts +10 -5
- package/util/path.js +14 -4
package/package.json
CHANGED
package/ui/page/Head.js
CHANGED
|
@@ -1,34 +1,46 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
|
-
import { notNullish } from "../../util/null.js";
|
|
3
|
+
import { isNullish, notNullish } from "../../util/null.js";
|
|
4
4
|
import { getProps } from "../../util/object.js";
|
|
5
|
-
import { isDefined } from "../../util/undefined.js";
|
|
6
5
|
import { requireMeta } from "../misc/Meta.js";
|
|
7
6
|
import { joinTitles } from "../util/meta.js";
|
|
8
7
|
/** Meta tags with a capital first letter and hyphens, e.g. `Content-Security-Policy` or `Accept`, are `http-equiv=""` tags. */
|
|
9
8
|
const R_HTTP_EQUIV = /^[A-Z][a-zA-Z0-9]*(-[A-Z][a-zA-Z0-9]*)*$/;
|
|
10
9
|
/** Use the details from the current page data context to set the document `<title>`, meta tags, and history state. */
|
|
11
10
|
export function Head() {
|
|
12
|
-
const { url, title, base, app, links, tags } = requireMeta();
|
|
11
|
+
const { url, title, base, app, links, tags, stylesheets, modules, scripts } = requireMeta();
|
|
13
12
|
useEffect(() => {
|
|
14
13
|
if (typeof window === "undefined")
|
|
15
14
|
return;
|
|
16
15
|
if (url)
|
|
17
16
|
window.history.replaceState(null, "", url);
|
|
18
17
|
}, [url]);
|
|
19
|
-
return (_jsxs("head", { children: [_jsx("title", { children: joinTitles(title, app) }), base && _jsx("base", { href: base.href }), tags &&
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
return (_jsxs("head", { children: [_jsx("title", { children: joinTitles(title, app) }), base && _jsx("base", { href: base.href }), tags && getProps(tags).map(_renderTags), links && getProps(links).map(_renderLinks), stylesheets?.map(_renderStylesheets), modules?.map(_renderModules), scripts?.map(_renderScripts)] }));
|
|
19
|
+
}
|
|
20
|
+
function _renderTags([k, x]) {
|
|
21
|
+
if (notNullish(x)) {
|
|
22
|
+
const y = x === true ? "yes" : x === false ? "no" : x;
|
|
23
|
+
if (k.startsWith("og:"))
|
|
24
|
+
return _jsx("meta", { property: k, content: y }, k); // Tags that start with `og:` use `property=""`
|
|
25
|
+
if (k.match(R_HTTP_EQUIV))
|
|
26
|
+
return _jsx("meta", { httpEquiv: k, content: y }, k); // Tags that are in `Snake-Case` use `http-equiv=""`
|
|
27
|
+
return _jsx("meta", { name: k, content: y }, k); // All other tags use `content=""`
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function _renderLinks([k, v]) {
|
|
32
|
+
if (notNullish(v)) {
|
|
33
|
+
const type = k.endsWith("icon") ? "image/x-icon" : "text/css";
|
|
34
|
+
return _jsx("link", { rel: k, href: v, type: type }, k);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function _renderStylesheets(v) {
|
|
39
|
+
return isNullish(v) ? null : _jsx("link", { rel: "stylesheet", type: "text/css", href: v }, v);
|
|
40
|
+
}
|
|
41
|
+
function _renderModules(v) {
|
|
42
|
+
return isNullish(v) ? null : _jsx("script", { type: "module", src: v, defer: true }, v);
|
|
43
|
+
}
|
|
44
|
+
function _renderScripts(v) {
|
|
45
|
+
return isNullish(v) ? null : _jsx("script", { type: "text/javascript", src: v, defer: true }, v);
|
|
34
46
|
}
|
package/ui/page/Head.tsx
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { type ReactElement, useEffect } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import type { ArrayItem } from "../../util/array.js";
|
|
3
|
+
import { isNullish, notNullish } from "../../util/null.js";
|
|
4
|
+
import { getProps, type Prop } from "../../util/object.js";
|
|
5
5
|
import { requireMeta } from "../misc/Meta.js";
|
|
6
|
-
import { joinTitles } from "../util/meta.js";
|
|
6
|
+
import { joinTitles, type MetaAssets, type MetaLinks, type MetaTags } from "../util/meta.js";
|
|
7
7
|
|
|
8
8
|
/** Meta tags with a capital first letter and hyphens, e.g. `Content-Security-Policy` or `Accept`, are `http-equiv=""` tags. */
|
|
9
9
|
const R_HTTP_EQUIV = /^[A-Z][a-zA-Z0-9]*(-[A-Z][a-zA-Z0-9]*)*$/;
|
|
10
10
|
|
|
11
11
|
/** Use the details from the current page data context to set the document `<title>`, meta tags, and history state. */
|
|
12
12
|
export function Head(): ReactElement {
|
|
13
|
-
const { url, title, base, app, links, tags } = requireMeta();
|
|
13
|
+
const { url, title, base, app, links, tags, stylesheets, modules, scripts } = requireMeta();
|
|
14
14
|
|
|
15
15
|
useEffect(() => {
|
|
16
16
|
if (typeof window === "undefined") return;
|
|
@@ -21,24 +21,41 @@ export function Head(): ReactElement {
|
|
|
21
21
|
<head>
|
|
22
22
|
<title>{joinTitles(title, app)}</title>
|
|
23
23
|
{base && <base href={base.href} />}
|
|
24
|
-
{tags &&
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (k.startsWith("og:")) return <meta key={k} property={k} content={y} />; // Tags that start with `og:` use `property=""`
|
|
30
|
-
if (k.match(R_HTTP_EQUIV)) return <meta key={k} httpEquiv={k} content={y} />; // Tags that are in `Snake-Case` use `http-equiv=""`
|
|
31
|
-
return <meta key={k} name={k} content={y} />; // All other tags use `content=""`
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
34
|
-
})
|
|
35
|
-
.filter(isDefined)}
|
|
36
|
-
{links &&
|
|
37
|
-
getProps(links).map(([k, v]) =>
|
|
38
|
-
notNullish(v) ? (
|
|
39
|
-
<link key={k} rel={k} href={v} type={v.endsWith(".png") ? "image/png" : v.endsWith(".ico") ? "image/x-icon" : undefined} />
|
|
40
|
-
) : null,
|
|
41
|
-
)}
|
|
24
|
+
{tags && getProps(tags).map(_renderTags)}
|
|
25
|
+
{links && getProps(links).map(_renderLinks)}
|
|
26
|
+
{stylesheets?.map(_renderStylesheets)}
|
|
27
|
+
{modules?.map(_renderModules)}
|
|
28
|
+
{scripts?.map(_renderScripts)}
|
|
42
29
|
</head>
|
|
43
30
|
);
|
|
44
31
|
}
|
|
32
|
+
|
|
33
|
+
function _renderTags([k, x]: Prop<MetaTags>): ReactElement | null {
|
|
34
|
+
if (notNullish(x)) {
|
|
35
|
+
const y = x === true ? "yes" : x === false ? "no" : x;
|
|
36
|
+
if (k.startsWith("og:")) return <meta key={k} property={k} content={y} />; // Tags that start with `og:` use `property=""`
|
|
37
|
+
if (k.match(R_HTTP_EQUIV)) return <meta key={k} httpEquiv={k} content={y} />; // Tags that are in `Snake-Case` use `http-equiv=""`
|
|
38
|
+
return <meta key={k} name={k} content={y} />; // All other tags use `content=""`
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _renderLinks([k, v]: Prop<MetaLinks>): ReactElement | null {
|
|
44
|
+
if (notNullish(v)) {
|
|
45
|
+
const type = k.endsWith("icon") ? "image/x-icon" : "text/css";
|
|
46
|
+
return <link key={k} rel={k} href={v} type={type} />;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function _renderStylesheets(v: ArrayItem<MetaAssets>): ReactElement | null {
|
|
52
|
+
return isNullish(v) ? null : <link key={v} rel="stylesheet" type="text/css" href={v} />;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function _renderModules(v: ArrayItem<MetaAssets>): ReactElement | null {
|
|
56
|
+
return isNullish(v) ? null : <script key={v} type="module" src={v} defer />;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _renderScripts(v: ArrayItem<MetaAssets>): ReactElement | null {
|
|
60
|
+
return isNullish(v) ? null : <script key={v} type="text/javascript" src={v} defer />;
|
|
61
|
+
}
|
package/ui/tree/TreeApp.d.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
import { type AppProps } from "../app/App.js";
|
|
2
|
+
import type { TreeElement } from "../../util/index.js";
|
|
4
3
|
import type { Routes } from "../router/Routes.js";
|
|
5
|
-
|
|
4
|
+
import type { PossibleMeta } from "../util/index.js";
|
|
5
|
+
export interface TreeAppProps extends PossibleMeta {
|
|
6
6
|
/** The tree elements to display. */
|
|
7
|
-
|
|
7
|
+
tree: TreeElement;
|
|
8
8
|
/** Additional routes (merged with the default tree route). */
|
|
9
9
|
routes?: Routes | undefined;
|
|
10
|
+
/** Children is optional and defaults to `<RouterOutput />` */
|
|
11
|
+
children?: ReactElement | undefined;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
14
|
* Top-level app component for a tree-based documentation site.
|
|
13
15
|
* - Wraps `<App>` with routing, error catching, and a sidebar layout.
|
|
14
16
|
* - The sidebar shows a `<TreeMenu>` of top-level elements.
|
|
15
|
-
* -
|
|
17
|
+
* - `/` renders the root via `<TreePage>`; `/**` catches every deeper path and feeds the full sub-path into `<TreePage>`.
|
|
16
18
|
* - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
|
|
17
19
|
* Override by wrapping with `<TreePageMapping>`, `<TreeMenuMapping>`, or `<TreeCardMapping>`.
|
|
18
20
|
*/
|
|
19
|
-
export declare function TreeApp({
|
|
21
|
+
export declare function TreeApp({ tree, routes, children, ...appProps }: TreeAppProps): ReactElement;
|
package/ui/tree/TreeApp.js
CHANGED
|
@@ -2,22 +2,23 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { App } from "../app/App.js";
|
|
3
3
|
import { SidebarLayout } from "../layout/SidebarLayout.js";
|
|
4
4
|
import { PageCatcher } from "../misc/Catcher.js";
|
|
5
|
-
import { Router } from "../router/Router.js";
|
|
5
|
+
import { Router, RouterOutput } from "../router/Router.js";
|
|
6
6
|
import { TreeMenu } from "./TreeMenu.js";
|
|
7
7
|
import { TreePage } from "./TreePage.js";
|
|
8
|
-
const TREE_ROUTE = "/{**path}";
|
|
9
8
|
/**
|
|
10
9
|
* Top-level app component for a tree-based documentation site.
|
|
11
10
|
* - Wraps `<App>` with routing, error catching, and a sidebar layout.
|
|
12
11
|
* - The sidebar shows a `<TreeMenu>` of top-level elements.
|
|
13
|
-
* -
|
|
12
|
+
* - `/` renders the root via `<TreePage>`; `/**` catches every deeper path and feeds the full sub-path into `<TreePage>`.
|
|
14
13
|
* - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
|
|
15
14
|
* Override by wrapping with `<TreePageMapping>`, `<TreeMenuMapping>`, or `<TreeCardMapping>`.
|
|
16
15
|
*/
|
|
17
|
-
export function TreeApp({
|
|
16
|
+
export function TreeApp({ tree, routes = {}, children = _jsx(RouterOutput, {}), ...appProps }) {
|
|
18
17
|
const allRoutes = {
|
|
19
18
|
...routes,
|
|
20
|
-
|
|
19
|
+
"/": () => _jsx(TreePage, { tree: tree }),
|
|
20
|
+
// `**` captures the multi-segment remainder under index `"0"` (named placeholders are single-segment only).
|
|
21
|
+
"/**": ({ 0: sub }) => _jsx(TreePage, { path: `/${sub ?? ""}`, tree: tree }),
|
|
21
22
|
};
|
|
22
|
-
return (_jsx(App, { ...appProps, children: _jsx(Router, { routes: allRoutes, children: _jsx(PageCatcher, { children: _jsx(SidebarLayout, { sidebar: _jsx(TreeMenu, {
|
|
23
|
+
return (_jsx(App, { ...appProps, children: _jsx(Router, { routes: allRoutes, children: _jsx(PageCatcher, { children: _jsx(SidebarLayout, { sidebar: _jsx(TreeMenu, { tree: tree }), children: children }) }) }) }));
|
|
23
24
|
}
|
package/ui/tree/TreeApp.tsx
CHANGED
|
@@ -1,42 +1,45 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TreeElement } from "../../util/index.js";
|
|
3
3
|
import type { AbsolutePath } from "../../util/path.js";
|
|
4
|
-
import { App
|
|
4
|
+
import { App } from "../app/App.js";
|
|
5
5
|
import { SidebarLayout } from "../layout/SidebarLayout.js";
|
|
6
6
|
import { PageCatcher } from "../misc/Catcher.js";
|
|
7
|
-
import { Router } from "../router/Router.js";
|
|
7
|
+
import { Router, RouterOutput } from "../router/Router.js";
|
|
8
8
|
import type { Routes } from "../router/Routes.js";
|
|
9
|
+
import type { PossibleMeta } from "../util/index.js";
|
|
9
10
|
import { TreeMenu } from "./TreeMenu.js";
|
|
10
11
|
import { TreePage } from "./TreePage.js";
|
|
11
12
|
|
|
12
|
-
export interface TreeAppProps extends
|
|
13
|
+
export interface TreeAppProps extends PossibleMeta {
|
|
13
14
|
/** The tree elements to display. */
|
|
14
|
-
|
|
15
|
+
tree: TreeElement;
|
|
15
16
|
/** Additional routes (merged with the default tree route). */
|
|
16
17
|
routes?: Routes | undefined;
|
|
18
|
+
/** Children is optional and defaults to `<RouterOutput />` */
|
|
19
|
+
children?: ReactElement | undefined;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
const TREE_ROUTE = "/{**path}" as AbsolutePath;
|
|
20
|
-
|
|
21
22
|
/**
|
|
22
23
|
* Top-level app component for a tree-based documentation site.
|
|
23
24
|
* - Wraps `<App>` with routing, error catching, and a sidebar layout.
|
|
24
25
|
* - The sidebar shows a `<TreeMenu>` of top-level elements.
|
|
25
|
-
* -
|
|
26
|
+
* - `/` renders the root via `<TreePage>`; `/**` catches every deeper path and feeds the full sub-path into `<TreePage>`.
|
|
26
27
|
* - Element rendering uses the default mappings on `<TreePage>`, `<TreeMenu>`, `<TreeCards>`.
|
|
27
28
|
* Override by wrapping with `<TreePageMapping>`, `<TreeMenuMapping>`, or `<TreeCardMapping>`.
|
|
28
29
|
*/
|
|
29
|
-
export function TreeApp({
|
|
30
|
+
export function TreeApp({ tree, routes = {}, children = <RouterOutput />, ...appProps }: TreeAppProps): ReactElement {
|
|
30
31
|
const allRoutes: Routes = {
|
|
31
32
|
...routes,
|
|
32
|
-
|
|
33
|
+
"/": () => <TreePage tree={tree} />,
|
|
34
|
+
// `**` captures the multi-segment remainder under index `"0"` (named placeholders are single-segment only).
|
|
35
|
+
"/**": ({ 0: sub }) => <TreePage path={`/${sub ?? ""}` as AbsolutePath} tree={tree} />,
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
return (
|
|
36
39
|
<App {...appProps}>
|
|
37
40
|
<Router routes={allRoutes}>
|
|
38
41
|
<PageCatcher>
|
|
39
|
-
<SidebarLayout sidebar={<TreeMenu
|
|
42
|
+
<SidebarLayout sidebar={<TreeMenu tree={tree} />}>{children}</SidebarLayout>
|
|
40
43
|
</PageCatcher>
|
|
41
44
|
</Router>
|
|
42
45
|
</App>
|
package/ui/tree/TreeCards.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TreeElements } from "../../util/element.js";
|
|
3
3
|
/**
|
|
4
4
|
* Default mappings for the most common tree element types.
|
|
5
5
|
* - Consumers can override individual entries via `<TreeCardMapping>`.
|
|
@@ -7,7 +7,7 @@ import type { Elements } from "../../util/element.js";
|
|
|
7
7
|
export declare const TreeCardMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps>, TreeCardMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps>;
|
|
8
8
|
export interface TreeCardsProps {
|
|
9
9
|
/** Elements to render as cards. */
|
|
10
|
-
children
|
|
10
|
+
children: TreeElements;
|
|
11
11
|
}
|
|
12
|
-
/** Grid of cards
|
|
12
|
+
/** Grid of cards rendered from a flat collection of tree elements. */
|
|
13
13
|
export declare function TreeCards({ children }: TreeCardsProps): ReactNode;
|
package/ui/tree/TreeCards.js
CHANGED
|
@@ -13,7 +13,7 @@ export const [TreeCardMapping, TreeCardMapper] = createMapper({
|
|
|
13
13
|
"tree-file": FileCard,
|
|
14
14
|
"tree-documentation": DocumentationCard,
|
|
15
15
|
});
|
|
16
|
-
/** Grid of cards
|
|
16
|
+
/** Grid of cards rendered from a flat collection of tree elements. */
|
|
17
17
|
export function TreeCards({ children }) {
|
|
18
18
|
return (_jsx("div", { className: TREE_CARDS_CSS.grid, children: _jsx(TreeCardMapper, { children: children }) }));
|
|
19
19
|
}
|
package/ui/tree/TreeCards.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TreeElements } from "../../util/element.js";
|
|
3
3
|
import { DirectoryCard } from "../docs/DirectoryCard.js";
|
|
4
4
|
import { DocumentationCard } from "../docs/DocumentationCard.js";
|
|
5
5
|
import { FileCard } from "../docs/FileCard.js";
|
|
@@ -18,10 +18,10 @@ export const [TreeCardMapping, TreeCardMapper] = createMapper({
|
|
|
18
18
|
|
|
19
19
|
export interface TreeCardsProps {
|
|
20
20
|
/** Elements to render as cards. */
|
|
21
|
-
children
|
|
21
|
+
children: TreeElements;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
/** Grid of cards
|
|
24
|
+
/** Grid of cards rendered from a flat collection of tree elements. */
|
|
25
25
|
export function TreeCards({ children }: TreeCardsProps): ReactNode {
|
|
26
26
|
return (
|
|
27
27
|
<div className={TREE_CARDS_CSS.grid}>
|
package/ui/tree/TreeMenu.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TreeElement } from "../../util/element.js";
|
|
3
3
|
/**
|
|
4
4
|
* Default mappings for the most common tree element types.
|
|
5
5
|
* - Consumers can override individual entries via `<TreeMenuMapping>`.
|
|
@@ -7,8 +7,8 @@ import type { Elements } from "../../util/element.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare const TreeMenuMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps>, TreeMenuMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps>;
|
|
9
9
|
export interface TreeMenuProps {
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/** Root element whose children become the navigation links. */
|
|
11
|
+
tree: TreeElement;
|
|
12
12
|
}
|
|
13
|
-
/** Sidebar navigation menu built from a tree
|
|
14
|
-
export declare function TreeMenu({
|
|
13
|
+
/** Sidebar navigation menu built from the children of a root tree element. */
|
|
14
|
+
export declare function TreeMenu({ tree }: TreeMenuProps): ReactNode;
|
package/ui/tree/TreeMenu.js
CHANGED
|
@@ -12,7 +12,7 @@ export const [TreeMenuMapping, TreeMenuMapper] = createMapper({
|
|
|
12
12
|
"tree-directory": DirectoryMenuItem,
|
|
13
13
|
"tree-file": FileMenuItem,
|
|
14
14
|
});
|
|
15
|
-
/** Sidebar navigation menu built from a tree
|
|
16
|
-
export function TreeMenu({
|
|
17
|
-
return (_jsx("nav", { className: TREE_MENU_CSS.menu, children: _jsx("ul", { className: TREE_MENU_CSS.list, children: _jsx(TreeMenuMapper, { children: children }) }) }));
|
|
15
|
+
/** Sidebar navigation menu built from the children of a root tree element. */
|
|
16
|
+
export function TreeMenu({ tree }) {
|
|
17
|
+
return (_jsx("nav", { className: TREE_MENU_CSS.menu, children: _jsx("ul", { className: TREE_MENU_CSS.list, children: _jsx(TreeMenuMapper, { children: tree.props.children }) }) }));
|
|
18
18
|
}
|
package/ui/tree/TreeMenu.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TreeElement } from "../../util/element.js";
|
|
3
3
|
import { DirectoryMenuItem } from "../docs/DirectoryMenuItem.js";
|
|
4
4
|
import { FileMenuItem } from "../docs/FileMenuItem.js";
|
|
5
5
|
import { createMapper } from "../misc/Mapper.js";
|
|
@@ -16,16 +16,16 @@ export const [TreeMenuMapping, TreeMenuMapper] = createMapper({
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
export interface TreeMenuProps {
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/** Root element whose children become the navigation links. */
|
|
20
|
+
tree: TreeElement;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/** Sidebar navigation menu built from a tree
|
|
24
|
-
export function TreeMenu({
|
|
23
|
+
/** Sidebar navigation menu built from the children of a root tree element. */
|
|
24
|
+
export function TreeMenu({ tree }: TreeMenuProps): ReactNode {
|
|
25
25
|
return (
|
|
26
26
|
<nav className={TREE_MENU_CSS.menu}>
|
|
27
27
|
<ul className={TREE_MENU_CSS.list}>
|
|
28
|
-
<TreeMenuMapper>{children}</TreeMenuMapper>
|
|
28
|
+
<TreeMenuMapper>{tree.props.children}</TreeMenuMapper>
|
|
29
29
|
</ul>
|
|
30
30
|
</nav>
|
|
31
31
|
);
|
package/ui/tree/TreePage.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import { type
|
|
2
|
+
import { type TreeElement } from "../../util/element.js";
|
|
3
3
|
import { type AbsolutePath } from "../../util/path.js";
|
|
4
4
|
/**
|
|
5
5
|
* Default mappings for the most common tree element types.
|
|
@@ -8,13 +8,13 @@ import { type AbsolutePath } from "../../util/path.js";
|
|
|
8
8
|
export declare const TreePageMapping: import("react").FunctionComponent<import("../misc/Mapper.js").MappingProps>, TreePageMapper: import("react").FunctionComponent<import("../misc/Mapper.js").MapperProps>;
|
|
9
9
|
export interface TreePageProps {
|
|
10
10
|
path?: AbsolutePath;
|
|
11
|
-
|
|
12
|
-
elements?: Elements;
|
|
11
|
+
tree: TreeElement;
|
|
13
12
|
}
|
|
14
13
|
/**
|
|
15
14
|
* Resolve a URL path to a tree element and render it.
|
|
16
|
-
* - Uses `
|
|
15
|
+
* - Uses `resolveElementPath()` to walk the tree matching each path segment to a descendant's `key`.
|
|
16
|
+
* - `/` renders `tree` itself; deeper paths render the matching descendant.
|
|
17
17
|
* - Delegates rendering to the component registered in the element mapper.
|
|
18
18
|
* - Throws `NotFoundError` if no element matches at any level.
|
|
19
19
|
*/
|
|
20
|
-
export declare function TreePage({ path,
|
|
20
|
+
export declare function TreePage({ path, tree }: TreePageProps): ReactNode;
|
package/ui/tree/TreePage.js
CHANGED
|
@@ -17,12 +17,13 @@ export const [TreePageMapping, TreePageMapper] = createMapper({
|
|
|
17
17
|
});
|
|
18
18
|
/**
|
|
19
19
|
* Resolve a URL path to a tree element and render it.
|
|
20
|
-
* - Uses `
|
|
20
|
+
* - Uses `resolveElementPath()` to walk the tree matching each path segment to a descendant's `key`.
|
|
21
|
+
* - `/` renders `tree` itself; deeper paths render the matching descendant.
|
|
21
22
|
* - Delegates rendering to the component registered in the element mapper.
|
|
22
23
|
* - Throws `NotFoundError` if no element matches at any level.
|
|
23
24
|
*/
|
|
24
|
-
export function TreePage({ path = "/",
|
|
25
|
-
const element = resolveElementPath(
|
|
25
|
+
export function TreePage({ path = "/", tree }) {
|
|
26
|
+
const element = resolveElementPath(tree, splitAbsolutePath(path));
|
|
26
27
|
if (!element)
|
|
27
28
|
throw new NotFoundError("Element not found", { received: path });
|
|
28
29
|
return _jsx(TreePageMapper, { children: element });
|
package/ui/tree/TreePage.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { NotFoundError } from "../../error/RequestError.js";
|
|
3
|
-
import { type
|
|
3
|
+
import { resolveElementPath, type TreeElement } from "../../util/element.js";
|
|
4
4
|
import { type AbsolutePath, splitAbsolutePath } from "../../util/path.js";
|
|
5
5
|
import { DirectoryPage } from "../docs/DirectoryPage.js";
|
|
6
6
|
import { DocumentationPage } from "../docs/DocumentationPage.js";
|
|
@@ -19,18 +19,18 @@ export const [TreePageMapping, TreePageMapper] = createMapper({
|
|
|
19
19
|
|
|
20
20
|
export interface TreePageProps {
|
|
21
21
|
path?: AbsolutePath;
|
|
22
|
-
|
|
23
|
-
elements?: Elements;
|
|
22
|
+
tree: TreeElement;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
26
|
* Resolve a URL path to a tree element and render it.
|
|
28
|
-
* - Uses `
|
|
27
|
+
* - Uses `resolveElementPath()` to walk the tree matching each path segment to a descendant's `key`.
|
|
28
|
+
* - `/` renders `tree` itself; deeper paths render the matching descendant.
|
|
29
29
|
* - Delegates rendering to the component registered in the element mapper.
|
|
30
30
|
* - Throws `NotFoundError` if no element matches at any level.
|
|
31
31
|
*/
|
|
32
|
-
export function TreePage({ path = "/",
|
|
33
|
-
const element = resolveElementPath(
|
|
32
|
+
export function TreePage({ path = "/", tree }: TreePageProps): ReactNode {
|
|
33
|
+
const element = resolveElementPath(tree, splitAbsolutePath(path));
|
|
34
34
|
if (!element) throw new NotFoundError("Element not found", { received: path });
|
|
35
35
|
return <TreePageMapper>{element}</TreePageMapper>;
|
|
36
36
|
}
|
package/ui/util/meta.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface MetaData {
|
|
|
34
34
|
readonly links?: MetaLinks | undefined;
|
|
35
35
|
readonly modules?: MetaAssets | undefined;
|
|
36
36
|
readonly scripts?: MetaAssets | undefined;
|
|
37
|
-
readonly
|
|
37
|
+
readonly stylesheets?: MetaAssets | undefined;
|
|
38
38
|
}
|
|
39
39
|
/** Input metadata that can be parsed and converted to proper metadata. */
|
|
40
40
|
export interface PossibleMeta extends Omit<MetaData, "url"> {
|
package/ui/util/meta.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface MetaData {
|
|
|
37
37
|
readonly links?: MetaLinks | undefined;
|
|
38
38
|
readonly modules?: MetaAssets | undefined;
|
|
39
39
|
readonly scripts?: MetaAssets | undefined;
|
|
40
|
-
readonly
|
|
40
|
+
readonly stylesheets?: MetaAssets | undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Input metadata that can be parsed and converted to proper metadata. */
|
package/util/element.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ImmutableArray } from "./array.js";
|
|
2
2
|
import type { AbsolutePath } from "./path.js";
|
|
3
3
|
import type { Query } from "./query.js";
|
|
4
|
-
import type { Segments } from "./string.js";
|
|
5
4
|
/** Set of valid props for an element. */
|
|
6
5
|
export interface ElementProps {
|
|
7
6
|
readonly [key: string]: unknown;
|
|
@@ -151,48 +150,45 @@ export declare function queryElements(elements: Elements, query: Query<Element>,
|
|
|
151
150
|
*/
|
|
152
151
|
export declare function filterElements(elements: Elements, match: (element: Element) => boolean, depth?: number): Iterable<Element>;
|
|
153
152
|
/**
|
|
154
|
-
* Resolve an element in a tree by walking a sequence of keys
|
|
155
|
-
* -
|
|
156
|
-
* -
|
|
157
|
-
* - If `
|
|
158
|
-
* - Returns `undefined` if no
|
|
153
|
+
* Resolve an element in a tree by walking a sequence of keys from `root`.
|
|
154
|
+
* - The `root` element's own key is never matched against path segments — it's the container, not a step in the path.
|
|
155
|
+
* - Each segment matches the `key` of an immediate child of the current element.
|
|
156
|
+
* - If `path` is empty, returns `root` itself.
|
|
157
|
+
* - Returns `undefined` if no descendant matches at any level.
|
|
159
158
|
*
|
|
160
159
|
* Splitting the path:
|
|
161
|
-
* - We accept a raw `Segments` array
|
|
160
|
+
* - We accept a raw `Segments` array, so callers can join paths later however they wish.
|
|
162
161
|
* - Element paths have no canonical string representation so we use `Segments` instead.
|
|
163
|
-
* - To split the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(
|
|
164
|
-
* - To split the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(
|
|
162
|
+
* - To split the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(root), splitDataPath)`
|
|
163
|
+
* - To split the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(root), splitAbsolutePath)`
|
|
165
164
|
*
|
|
166
|
-
* @param
|
|
167
|
-
* @param path An array of path segments
|
|
168
|
-
* - Element paths have no canonical string representation so we always us the `Segments` format.
|
|
165
|
+
* @param root The root element to walk from. Its own `key` is treated as a label, not a path segment.
|
|
166
|
+
* @param path An array of path segments naming descendants of `root`.
|
|
169
167
|
*
|
|
170
|
-
* @example
|
|
171
|
-
* @example
|
|
172
|
-
* @example resolveElement(elements, "") // First keyed root element
|
|
168
|
+
* @example resolveElementPath(root, ["util", "array"]) // Element with key "array" inside child with key "util"
|
|
169
|
+
* @example resolveElementPath(root, []) // `root` itself
|
|
173
170
|
*/
|
|
174
|
-
export declare function resolveElementPath(
|
|
171
|
+
export declare function resolveElementPath(root: TreeElement, path: readonly string[]): Element | undefined;
|
|
175
172
|
/**
|
|
176
|
-
* Deeply iterate a tree
|
|
177
|
-
* -
|
|
178
|
-
* -
|
|
179
|
-
* -
|
|
173
|
+
* Deeply iterate a tree from `root` and yield path segments for each reachable element.
|
|
174
|
+
* - Yields `[]` for `root` itself.
|
|
175
|
+
* - Yields `[key]` for each immediate child, `[key, key]` for grandchildren, etc.
|
|
176
|
+
* - The `root` element's own key never appears in any yielded path — it's the container.
|
|
177
|
+
* - Children with `undefined` or `null` keys are skipped (and their descendants are not yielded).
|
|
180
178
|
*
|
|
181
179
|
* Joining the paths:
|
|
182
|
-
* - We return a `Segments` array for each element, so
|
|
180
|
+
* - We return a `Segments` array for each element, so callers can join paths later however they wish.
|
|
183
181
|
* - Element paths have no canonical string representation so we use `Segments` instead.
|
|
184
|
-
* - To join the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(
|
|
185
|
-
* - To join the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(
|
|
182
|
+
* - To join the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(root), joinDataPath)`
|
|
183
|
+
* - To join the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(root), joinAbsolutePath)`
|
|
186
184
|
*
|
|
187
|
-
* @param
|
|
185
|
+
* @param root The root element to walk from. Its own `key` is treated as a label, not a path segment.
|
|
188
186
|
* @param depth Controls how many levels of children to recurse into (defaults to infinite depth).
|
|
189
|
-
* - `depth=0` yields
|
|
187
|
+
* - `depth=0` yields only `[]` (the root itself).
|
|
190
188
|
*
|
|
191
|
-
* @returns Iterable set of path segment arrays, each representing one
|
|
192
|
-
* - Element paths have no canonical string representation so we always us the `Segments` format.
|
|
189
|
+
* @returns Iterable set of path segment arrays, each representing one descendant (or the root).
|
|
193
190
|
*/
|
|
194
|
-
export declare function getElementPaths(
|
|
195
|
-
export declare function _getElementPaths(elements: Elements, depth: number, prefix?: Segments): Iterable<Segments>;
|
|
191
|
+
export declare function getElementPaths(root: TreeElement, depth?: number): Iterable<readonly string[]>;
|
|
196
192
|
/** Combine two `Elements`, preserving both if both are set. */
|
|
197
193
|
export declare function mergeElements<T extends Elements>(a: T, b: T): T;
|
|
198
194
|
export declare function mergeElements(a: Elements, b: Elements): Elements;
|
package/util/element.js
CHANGED
|
@@ -70,32 +70,29 @@ export function* filterElements(elements, match, depth = Infinity) {
|
|
|
70
70
|
yield element;
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* Resolve an element in a tree by walking a sequence of keys
|
|
74
|
-
* -
|
|
75
|
-
* -
|
|
76
|
-
* - If `
|
|
77
|
-
* - Returns `undefined` if no
|
|
73
|
+
* Resolve an element in a tree by walking a sequence of keys from `root`.
|
|
74
|
+
* - The `root` element's own key is never matched against path segments — it's the container, not a step in the path.
|
|
75
|
+
* - Each segment matches the `key` of an immediate child of the current element.
|
|
76
|
+
* - If `path` is empty, returns `root` itself.
|
|
77
|
+
* - Returns `undefined` if no descendant matches at any level.
|
|
78
78
|
*
|
|
79
79
|
* Splitting the path:
|
|
80
|
-
* - We accept a raw `Segments` array
|
|
80
|
+
* - We accept a raw `Segments` array, so callers can join paths later however they wish.
|
|
81
81
|
* - Element paths have no canonical string representation so we use `Segments` instead.
|
|
82
|
-
* - To split the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(
|
|
83
|
-
* - To split the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(
|
|
82
|
+
* - To split the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(root), splitDataPath)`
|
|
83
|
+
* - To split the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(root), splitAbsolutePath)`
|
|
84
84
|
*
|
|
85
|
-
* @param
|
|
86
|
-
* @param path An array of path segments
|
|
87
|
-
* - Element paths have no canonical string representation so we always us the `Segments` format.
|
|
85
|
+
* @param root The root element to walk from. Its own `key` is treated as a label, not a path segment.
|
|
86
|
+
* @param path An array of path segments naming descendants of `root`.
|
|
88
87
|
*
|
|
89
|
-
* @example
|
|
90
|
-
* @example
|
|
91
|
-
* @example resolveElement(elements, "") // First keyed root element
|
|
88
|
+
* @example resolveElementPath(root, ["util", "array"]) // Element with key "array" inside child with key "util"
|
|
89
|
+
* @example resolveElementPath(root, []) // `root` itself
|
|
92
90
|
*/
|
|
93
|
-
export function resolveElementPath(
|
|
94
|
-
let current =
|
|
95
|
-
let found;
|
|
91
|
+
export function resolveElementPath(root, path) {
|
|
92
|
+
let current = root;
|
|
96
93
|
for (const segment of path) {
|
|
97
|
-
found
|
|
98
|
-
for (const el of getElements(current, 0)) {
|
|
94
|
+
let found;
|
|
95
|
+
for (const el of getElements(current.props.children, 0)) {
|
|
99
96
|
if (el.key === segment) {
|
|
100
97
|
found = el;
|
|
101
98
|
break;
|
|
@@ -103,35 +100,40 @@ export function resolveElementPath(elements, path) {
|
|
|
103
100
|
}
|
|
104
101
|
if (!found)
|
|
105
102
|
return undefined;
|
|
106
|
-
current = found
|
|
103
|
+
current = found;
|
|
107
104
|
}
|
|
108
|
-
return
|
|
105
|
+
return current;
|
|
109
106
|
}
|
|
110
107
|
/**
|
|
111
|
-
* Deeply iterate a tree
|
|
112
|
-
* -
|
|
113
|
-
* -
|
|
114
|
-
* -
|
|
108
|
+
* Deeply iterate a tree from `root` and yield path segments for each reachable element.
|
|
109
|
+
* - Yields `[]` for `root` itself.
|
|
110
|
+
* - Yields `[key]` for each immediate child, `[key, key]` for grandchildren, etc.
|
|
111
|
+
* - The `root` element's own key never appears in any yielded path — it's the container.
|
|
112
|
+
* - Children with `undefined` or `null` keys are skipped (and their descendants are not yielded).
|
|
115
113
|
*
|
|
116
114
|
* Joining the paths:
|
|
117
|
-
* - We return a `Segments` array for each element, so
|
|
115
|
+
* - We return a `Segments` array for each element, so callers can join paths later however they wish.
|
|
118
116
|
* - Element paths have no canonical string representation so we use `Segments` instead.
|
|
119
|
-
* - To join the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(
|
|
120
|
-
* - To join the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(
|
|
117
|
+
* - To join the keys in `a.b.c` dotted data format use `mapItems(getElementPaths(root), joinDataPath)`
|
|
118
|
+
* - To join the keys in `/a/b/c` absolute path format use `mapItems(getElementPaths(root), joinAbsolutePath)`
|
|
121
119
|
*
|
|
122
|
-
* @param
|
|
120
|
+
* @param root The root element to walk from. Its own `key` is treated as a label, not a path segment.
|
|
123
121
|
* @param depth Controls how many levels of children to recurse into (defaults to infinite depth).
|
|
124
|
-
* - `depth=0` yields
|
|
122
|
+
* - `depth=0` yields only `[]` (the root itself).
|
|
125
123
|
*
|
|
126
|
-
* @returns Iterable set of path segment arrays, each representing one
|
|
127
|
-
* - Element paths have no canonical string representation so we always us the `Segments` format.
|
|
124
|
+
* @returns Iterable set of path segment arrays, each representing one descendant (or the root).
|
|
128
125
|
*/
|
|
129
|
-
export function getElementPaths(
|
|
130
|
-
return _getElementPaths(
|
|
126
|
+
export function getElementPaths(root, depth = Infinity) {
|
|
127
|
+
return _getElementPaths(root, depth);
|
|
128
|
+
}
|
|
129
|
+
function* _getElementPaths(root, depth) {
|
|
130
|
+
yield [];
|
|
131
|
+
if (depth > 0)
|
|
132
|
+
yield* _getDescendantPaths(root.props.children, depth - 1);
|
|
131
133
|
}
|
|
132
|
-
|
|
134
|
+
function* _getDescendantPaths(elements, depth, prefix) {
|
|
133
135
|
for (const { key, props } of getElements(elements, 0)) {
|
|
134
|
-
// Skip `null` or `undefined` keys.
|
|
136
|
+
// Skip `null` or `undefined` keys (and their descendants).
|
|
135
137
|
if (isNullish(key))
|
|
136
138
|
continue;
|
|
137
139
|
// Make the path and yield it.
|
|
@@ -139,7 +141,7 @@ export function* _getElementPaths(elements, depth, prefix) {
|
|
|
139
141
|
yield keys;
|
|
140
142
|
// Recurse into the children.
|
|
141
143
|
if (depth > 0 && props.children)
|
|
142
|
-
yield*
|
|
144
|
+
yield* _getDescendantPaths(props.children, depth - 1, keys);
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
export function mergeElements(a, b) {
|
package/util/path.d.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Reuseable utilities for dealing with filesystem paths.
|
|
3
3
|
*/
|
|
4
4
|
import type { AnyCaller } from "./function.js";
|
|
5
|
-
import { type Segments } from "./index.js";
|
|
6
5
|
import type { Nullish } from "./null.js";
|
|
7
6
|
/** Absolute path string starts with `/` slash. */
|
|
8
7
|
export type AbsolutePath = `/` | `/${string}`;
|
|
@@ -52,7 +51,13 @@ export declare function matchPathPrefix(target: PossiblePath, base: PossiblePath
|
|
|
52
51
|
export declare function isPathActive(target: AbsolutePath, current: AbsolutePath): boolean;
|
|
53
52
|
/** Is a target path proud (i.e. is the current path, or is a child of the current path)? */
|
|
54
53
|
export declare function isPathProud(target: AbsolutePath, current: AbsolutePath): boolean;
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Get the "segments" in an absolute path.
|
|
56
|
+
* - `splitAbsolutePath("/")` returns `[]` — the root has no segments.
|
|
57
|
+
*/
|
|
58
|
+
export declare function splitAbsolutePath(path: AbsolutePath | readonly string[]): readonly string[];
|
|
59
|
+
/**
|
|
60
|
+
* Join a set of path segments to form an absolute path.
|
|
61
|
+
* - `joinAbsolutePath([])` returns `"/"` — an empty segment list represents the root.
|
|
62
|
+
*/
|
|
63
|
+
export declare function joinAbsolutePath(path: AbsolutePath | readonly string[]): AbsolutePath;
|
package/util/path.js
CHANGED
|
@@ -74,11 +74,21 @@ export function isPathActive(target, current) {
|
|
|
74
74
|
export function isPathProud(target, current) {
|
|
75
75
|
return target === current || (target !== "/" && target.startsWith(`${current}/`));
|
|
76
76
|
}
|
|
77
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Get the "segments" in an absolute path.
|
|
79
|
+
* - `splitAbsolutePath("/")` returns `[]` — the root has no segments.
|
|
80
|
+
*/
|
|
78
81
|
export function splitAbsolutePath(path) {
|
|
79
|
-
|
|
82
|
+
if (typeof path !== "string")
|
|
83
|
+
return path;
|
|
84
|
+
if (path === "/")
|
|
85
|
+
return [];
|
|
86
|
+
return splitString(path.slice(1), "/", 1, undefined, splitAbsolutePath);
|
|
80
87
|
}
|
|
81
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Join a set of path segments to form an absolute path.
|
|
90
|
+
* - `joinAbsolutePath([])` returns `"/"` — an empty segment list represents the root.
|
|
91
|
+
*/
|
|
82
92
|
export function joinAbsolutePath(path) {
|
|
83
|
-
return typeof path === "string" ? path : `/${path.join("")}`;
|
|
93
|
+
return typeof path === "string" ? path : `/${path.join("/")}`;
|
|
84
94
|
}
|