@dogsbay/site-files 0.2.0-beta.119 → 0.2.0-beta.121
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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/namespaces.d.ts +86 -0
- package/dist/namespaces.js +175 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named sources: one site built from several content sources, where a source
|
|
3
|
+
* with `name: api` publishes under `/api/` and shows as its own group in the
|
|
4
|
+
* sidebar. The Astro pipeline (dogsbay CLI, import-content.ts) and dogsbay-ssg
|
|
5
|
+
* apply the same rule from here, so the two agree on every URL and nav link.
|
|
6
|
+
*
|
|
7
|
+
* sources: [{ path: ./content }, { name: api, path: ./openapi.yaml }]
|
|
8
|
+
* ./content pages → /<slug> (unnamed: flat, as before)
|
|
9
|
+
* ./openapi.yaml pages → /api/<slug>, nav group "Api"
|
|
10
|
+
*
|
|
11
|
+
* Naming one source never moves another's URLs: an unnamed source stays flat
|
|
12
|
+
* beside named siblings. A single source, named or not, gets no segment.
|
|
13
|
+
*/
|
|
14
|
+
/** A content source, as far as the namespace rule reads it. */
|
|
15
|
+
export interface NamespacedSource {
|
|
16
|
+
name?: string;
|
|
17
|
+
}
|
|
18
|
+
/** A nav entry, as far as prefixing reads it (`@dogsbay/types` NavItem fits). */
|
|
19
|
+
export interface PrefixableNavItem {
|
|
20
|
+
href?: string;
|
|
21
|
+
children?: PrefixableNavItem[];
|
|
22
|
+
}
|
|
23
|
+
/** A source's namespace: its `name:`, or undefined (flat URLs). */
|
|
24
|
+
export declare function sourceNamespace(source: NamespacedSource): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Whether namespaces are in play: two or more distinct namespaces across the
|
|
27
|
+
* sources, an unnamed source counting as one. So `[unnamed, api]` activates
|
|
28
|
+
* it (the unnamed one stays flat, `api` gets `/api/`), and `[api]` alone does
|
|
29
|
+
* not.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isNamespaceAxisActive(sources: readonly NamespacedSource[]): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The URL segment a source's pages go under: `[name]` when namespaces are
|
|
34
|
+
* active and the source has a name, otherwise none. (The Astro pipeline adds
|
|
35
|
+
* locale and version segments after it: `/<name>/<locale>/<version>/<slug>`.)
|
|
36
|
+
*/
|
|
37
|
+
export declare function namespaceSegments(source: NamespacedSource, sources: readonly NamespacedSource[]): string[];
|
|
38
|
+
/** The sidebar label for a namespace: `api` → "Api", `user-guide` → "User Guide". */
|
|
39
|
+
export declare function namespaceLabel(ns: string): string;
|
|
40
|
+
/** A page slug under prefix segments: (`["api"]`, `pets/list`) → `api/pets/list`; `""` → `api`. */
|
|
41
|
+
export declare function prefixSlug(prefix: readonly string[], slug: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Nav hrefs with the prefix segments put after the base path:
|
|
44
|
+
* `/docs/pets/` → `/docs/api/pets/` for basePath `/docs`. External URLs, and
|
|
45
|
+
* hrefs not under the base path, are left alone: nothing is rewritten that
|
|
46
|
+
* the rule does not recognise. Returns new items; the input is not changed.
|
|
47
|
+
*
|
|
48
|
+
* @param basePath `""` or `/<seg>` (the site's combined URL prefix), the
|
|
49
|
+
* prefix the importer already wrote into each href.
|
|
50
|
+
*/
|
|
51
|
+
export declare function prefixNavHrefs<T extends PrefixableNavItem>(items: readonly T[], prefix: readonly string[], basePath: string): T[];
|
|
52
|
+
/**
|
|
53
|
+
* A named source's nav as it joins the site's sidebar: one group labelled
|
|
54
|
+
* with the namespace, holding the source's own nav. An unnamed source's nav
|
|
55
|
+
* joins flat (`undefined` here means "splice the items in as they are").
|
|
56
|
+
*/
|
|
57
|
+
export declare function namespaceNavGroup<T extends PrefixableNavItem>(ns: string | undefined, nav: T[]): {
|
|
58
|
+
label: string;
|
|
59
|
+
children: T[];
|
|
60
|
+
} | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Prefix INTERNAL-ABSOLUTE refs in a page tree with the source's URL
|
|
63
|
+
* segments, matching the slug rewrite — so a versioned/localised page's
|
|
64
|
+
* links and images resolve under the same `/<segs>/...` prefix as the
|
|
65
|
+
* pages themselves. Covers page links (`href`) and asset refs (`src`),
|
|
66
|
+
* in both markdown nodes (`src`/`href` fields) and raw-HTML nodes
|
|
67
|
+
* (`href="..."`/`src="..."` inside an `html` string).
|
|
68
|
+
*
|
|
69
|
+
* Refs at this pipeline stage are ROOT-RELATIVE and carry NO basePath
|
|
70
|
+
* (importers resolve to `/foo`; basePath is composed downstream), so the
|
|
71
|
+
* match anchor is `/`, not `<basePath>/`. A ref is prefixed iff it is an
|
|
72
|
+
* internal absolute path (starts with `/`), and NOT: external
|
|
73
|
+
* (`scheme:`, `//host`), an anchor (`#`), the site home (`/`), under a
|
|
74
|
+
* GLOBAL prefix (taxonomy index paths — one `/tags/` for the whole site,
|
|
75
|
+
* never version-bucketed), or ALREADY carrying this source's full
|
|
76
|
+
* `/<segs>/` prefix (idempotent). Relative refs are left untouched.
|
|
77
|
+
*
|
|
78
|
+
* The idempotency check is a guess: a source under `/api/` whose own folder
|
|
79
|
+
* is also `api` writes `/api/x`, which reads as already prefixed. An importer
|
|
80
|
+
* that invents folder names (format-openapi's tags) avoids those segments
|
|
81
|
+
* (`reservedSlugs`). Changes `tree` in place.
|
|
82
|
+
*
|
|
83
|
+
* Used for a named source (`["api"]`) and, in the Astro pipeline, versioned
|
|
84
|
+
* and localized ones too (`["api", "fr", "v2"]`).
|
|
85
|
+
*/
|
|
86
|
+
export declare function rewriteContentRefs(tree: unknown, prefixSegs: readonly string[], globalPrefixes: ReadonlySet<string>): void;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Named sources: one site built from several content sources, where a source
|
|
3
|
+
* with `name: api` publishes under `/api/` and shows as its own group in the
|
|
4
|
+
* sidebar. The Astro pipeline (dogsbay CLI, import-content.ts) and dogsbay-ssg
|
|
5
|
+
* apply the same rule from here, so the two agree on every URL and nav link.
|
|
6
|
+
*
|
|
7
|
+
* sources: [{ path: ./content }, { name: api, path: ./openapi.yaml }]
|
|
8
|
+
* ./content pages → /<slug> (unnamed: flat, as before)
|
|
9
|
+
* ./openapi.yaml pages → /api/<slug>, nav group "Api"
|
|
10
|
+
*
|
|
11
|
+
* Naming one source never moves another's URLs: an unnamed source stays flat
|
|
12
|
+
* beside named siblings. A single source, named or not, gets no segment.
|
|
13
|
+
*/
|
|
14
|
+
/** A source's namespace: its `name:`, or undefined (flat URLs). */
|
|
15
|
+
export function sourceNamespace(source) {
|
|
16
|
+
return source.name;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether namespaces are in play: two or more distinct namespaces across the
|
|
20
|
+
* sources, an unnamed source counting as one. So `[unnamed, api]` activates
|
|
21
|
+
* it (the unnamed one stays flat, `api` gets `/api/`), and `[api]` alone does
|
|
22
|
+
* not.
|
|
23
|
+
*/
|
|
24
|
+
export function isNamespaceAxisActive(sources) {
|
|
25
|
+
if (sources.length < 2)
|
|
26
|
+
return false;
|
|
27
|
+
return new Set(sources.map(sourceNamespace)).size >= 2;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The URL segment a source's pages go under: `[name]` when namespaces are
|
|
31
|
+
* active and the source has a name, otherwise none. (The Astro pipeline adds
|
|
32
|
+
* locale and version segments after it: `/<name>/<locale>/<version>/<slug>`.)
|
|
33
|
+
*/
|
|
34
|
+
export function namespaceSegments(source, sources) {
|
|
35
|
+
const ns = isNamespaceAxisActive(sources) ? sourceNamespace(source) : undefined;
|
|
36
|
+
return ns === undefined ? [] : [ns];
|
|
37
|
+
}
|
|
38
|
+
/** The sidebar label for a namespace: `api` → "Api", `user-guide` → "User Guide". */
|
|
39
|
+
export function namespaceLabel(ns) {
|
|
40
|
+
return ns
|
|
41
|
+
.split("-")
|
|
42
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
43
|
+
.join(" ");
|
|
44
|
+
}
|
|
45
|
+
/** A page slug under prefix segments: (`["api"]`, `pets/list`) → `api/pets/list`; `""` → `api`. */
|
|
46
|
+
export function prefixSlug(prefix, slug) {
|
|
47
|
+
if (prefix.length === 0)
|
|
48
|
+
return slug;
|
|
49
|
+
if (!slug)
|
|
50
|
+
return prefix.join("/");
|
|
51
|
+
return `${prefix.join("/")}/${slug}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Nav hrefs with the prefix segments put after the base path:
|
|
55
|
+
* `/docs/pets/` → `/docs/api/pets/` for basePath `/docs`. External URLs, and
|
|
56
|
+
* hrefs not under the base path, are left alone: nothing is rewritten that
|
|
57
|
+
* the rule does not recognise. Returns new items; the input is not changed.
|
|
58
|
+
*
|
|
59
|
+
* @param basePath `""` or `/<seg>` (the site's combined URL prefix), the
|
|
60
|
+
* prefix the importer already wrote into each href.
|
|
61
|
+
*/
|
|
62
|
+
export function prefixNavHrefs(items, prefix, basePath) {
|
|
63
|
+
if (prefix.length === 0)
|
|
64
|
+
return items.map((item) => ({ ...item }));
|
|
65
|
+
const match = basePath ? `${basePath}/` : "/";
|
|
66
|
+
const href = (h) => {
|
|
67
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(h) || h.startsWith("mailto:"))
|
|
68
|
+
return h;
|
|
69
|
+
if (!h.startsWith(match))
|
|
70
|
+
return h;
|
|
71
|
+
return `${match}${prefix.join("/")}/${h.slice(match.length)}`;
|
|
72
|
+
};
|
|
73
|
+
return items.map((item) => {
|
|
74
|
+
const next = { ...item };
|
|
75
|
+
if (item.href !== undefined)
|
|
76
|
+
next.href = href(item.href);
|
|
77
|
+
if (item.children && item.children.length > 0) {
|
|
78
|
+
next.children = prefixNavHrefs(item.children, prefix, basePath);
|
|
79
|
+
}
|
|
80
|
+
return next;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A named source's nav as it joins the site's sidebar: one group labelled
|
|
85
|
+
* with the namespace, holding the source's own nav. An unnamed source's nav
|
|
86
|
+
* joins flat (`undefined` here means "splice the items in as they are").
|
|
87
|
+
*/
|
|
88
|
+
export function namespaceNavGroup(ns, nav) {
|
|
89
|
+
return ns === undefined ? undefined : { label: namespaceLabel(ns), children: nav };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Prefix INTERNAL-ABSOLUTE refs in a page tree with the source's URL
|
|
93
|
+
* segments, matching the slug rewrite — so a versioned/localised page's
|
|
94
|
+
* links and images resolve under the same `/<segs>/...` prefix as the
|
|
95
|
+
* pages themselves. Covers page links (`href`) and asset refs (`src`),
|
|
96
|
+
* in both markdown nodes (`src`/`href` fields) and raw-HTML nodes
|
|
97
|
+
* (`href="..."`/`src="..."` inside an `html` string).
|
|
98
|
+
*
|
|
99
|
+
* Refs at this pipeline stage are ROOT-RELATIVE and carry NO basePath
|
|
100
|
+
* (importers resolve to `/foo`; basePath is composed downstream), so the
|
|
101
|
+
* match anchor is `/`, not `<basePath>/`. A ref is prefixed iff it is an
|
|
102
|
+
* internal absolute path (starts with `/`), and NOT: external
|
|
103
|
+
* (`scheme:`, `//host`), an anchor (`#`), the site home (`/`), under a
|
|
104
|
+
* GLOBAL prefix (taxonomy index paths — one `/tags/` for the whole site,
|
|
105
|
+
* never version-bucketed), or ALREADY carrying this source's full
|
|
106
|
+
* `/<segs>/` prefix (idempotent). Relative refs are left untouched.
|
|
107
|
+
*
|
|
108
|
+
* The idempotency check is a guess: a source under `/api/` whose own folder
|
|
109
|
+
* is also `api` writes `/api/x`, which reads as already prefixed. An importer
|
|
110
|
+
* that invents folder names (format-openapi's tags) avoids those segments
|
|
111
|
+
* (`reservedSlugs`). Changes `tree` in place.
|
|
112
|
+
*
|
|
113
|
+
* Used for a named source (`["api"]`) and, in the Astro pipeline, versioned
|
|
114
|
+
* and localized ones too (`["api", "fr", "v2"]`).
|
|
115
|
+
*/
|
|
116
|
+
export function rewriteContentRefs(tree, prefixSegs, globalPrefixes) {
|
|
117
|
+
if (prefixSegs.length === 0)
|
|
118
|
+
return;
|
|
119
|
+
const seg = prefixSegs.join("/");
|
|
120
|
+
const fullPrefix = `/${seg}/`;
|
|
121
|
+
const prefix = (u) => {
|
|
122
|
+
if (!u)
|
|
123
|
+
return u;
|
|
124
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(u))
|
|
125
|
+
return u; // scheme (http:, mailto:)
|
|
126
|
+
if (u.startsWith("//") || u.startsWith("#"))
|
|
127
|
+
return u; // protocol-rel / anchor
|
|
128
|
+
if (!u.startsWith("/"))
|
|
129
|
+
return u; // relative
|
|
130
|
+
if (u === "/" || u === "")
|
|
131
|
+
return u; // site home — global
|
|
132
|
+
if (u.startsWith(fullPrefix) || u === `/${seg}`)
|
|
133
|
+
return u; // already prefixed
|
|
134
|
+
// Global (taxonomy) prefixes live in one un-bucketed namespace.
|
|
135
|
+
const firstSeg = u.slice(1).split(/[/#?]/)[0];
|
|
136
|
+
if (globalPrefixes.has(firstSeg))
|
|
137
|
+
return u;
|
|
138
|
+
return `/${seg}${u}`;
|
|
139
|
+
};
|
|
140
|
+
// Raw HTML: rewrite the VALUE of each href="..."/src="..." attribute.
|
|
141
|
+
const rewriteHtml = (html) => html.replace(/\b(href|src)=("|')(.*?)\2/gi, (_m, attr, q, val) => `${attr}=${q}${prefix(val)}${q}`);
|
|
142
|
+
const walk = (node) => {
|
|
143
|
+
if (Array.isArray(node)) {
|
|
144
|
+
for (const item of node)
|
|
145
|
+
walk(item);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!node || typeof node !== "object")
|
|
149
|
+
return;
|
|
150
|
+
const obj = node;
|
|
151
|
+
if (typeof obj.src === "string")
|
|
152
|
+
obj.src = prefix(obj.src);
|
|
153
|
+
if (typeof obj.href === "string")
|
|
154
|
+
obj.href = prefix(obj.href);
|
|
155
|
+
if (typeof obj.html === "string")
|
|
156
|
+
obj.html = rewriteHtml(obj.html);
|
|
157
|
+
// `::child-links{folder=…}` is a site path too: on a versioned or
|
|
158
|
+
// localized site it names a folder in THIS source's namespace.
|
|
159
|
+
if (obj.type === "child-links" && obj.props && typeof obj.props.folder === "string") {
|
|
160
|
+
const props = obj.props;
|
|
161
|
+
props.folder = prefix(`/${String(props.folder).replace(/^\/+/, "")}`);
|
|
162
|
+
}
|
|
163
|
+
// `scope=section` has no folder to prefix: record the source namespace,
|
|
164
|
+
// or the section resolves to the version segment (`/v2/`).
|
|
165
|
+
if (obj.type === "child-links" && obj.props && obj.props.scope === "section") {
|
|
166
|
+
obj.props.scopeBase = `/${seg}`;
|
|
167
|
+
}
|
|
168
|
+
for (const key of Object.keys(obj)) {
|
|
169
|
+
const val = obj[key];
|
|
170
|
+
if (val && typeof val === "object")
|
|
171
|
+
walk(val);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
walk(tree);
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/site-files",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.121",
|
|
4
4
|
"description": "robots.txt, _headers, llms.txt, sitemaps, redirects and JSON-LD for docs sites; shared by the dogsbay Astro pipeline and dogsbay-ssg, Worker-safe",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|