@dogsbay/types 0.2.0-beta.17 → 0.2.0-beta.170
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/directives.d.ts +124 -0
- package/dist/directives.js +26 -0
- package/dist/format.d.ts +311 -5
- package/dist/format.js +0 -1
- package/dist/frontmatter-check.d.ts +42 -0
- package/dist/frontmatter-check.js +223 -0
- package/dist/frontmatter-schema.d.ts +96 -0
- package/dist/frontmatter-schema.js +243 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +6 -2
- package/dist/json.d.ts +0 -1
- package/dist/json.js +0 -1
- package/dist/limits.d.ts +66 -0
- package/dist/limits.js +151 -0
- package/dist/meta.d.ts +26 -5
- package/dist/meta.js +34 -7
- package/dist/plugin.d.ts +3 -4
- package/dist/plugin.js +0 -1
- package/dist/text-map.d.ts +163 -0
- package/dist/text-map.js +559 -0
- package/dist/tree.d.ts +54 -3
- package/dist/tree.js +0 -1
- package/package.json +3 -3
- package/dist/format.d.ts.map +0 -1
- package/dist/format.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/json.d.ts.map +0 -1
- package/dist/json.js.map +0 -1
- package/dist/meta.d.ts.map +0 -1
- package/dist/meta.js.map +0 -1
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js.map +0 -1
- package/dist/tree.d.ts.map +0 -1
- package/dist/tree.js.map +0 -1
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import limits: the RESOURCE limits an import puts on someone else's repo, in one place
|
|
3
|
+
* (plans/import-limits.md). They exist so a hostile repo cannot make an import read gigabytes or
|
|
4
|
+
* build a table of a million rows; for the operator's own repo they are only a way for content
|
|
5
|
+
* to turn into a note, so `trusted` sets none.
|
|
6
|
+
*
|
|
7
|
+
* Who may set them: the OPERATOR (CLI flags, a host's `migrate()` options), never the repo being
|
|
8
|
+
* imported. Safety rules (containment, no repo code run, escaping) are NOT limits and are not
|
|
9
|
+
* here: they hold whether the repo is trusted or not.
|
|
10
|
+
*
|
|
11
|
+
* Each default is measured against the largest real value it guards, with the headroom noted.
|
|
12
|
+
* A limit that trips is REPORTED (`LIMIT_REACHED`), never silent.
|
|
13
|
+
*/
|
|
14
|
+
export interface ImportLimits {
|
|
15
|
+
/** One file read as data (bytes). */
|
|
16
|
+
fileBytes: number;
|
|
17
|
+
/** Files in one data folder. */
|
|
18
|
+
folderFiles: number;
|
|
19
|
+
/** All data read in one import (bytes). */
|
|
20
|
+
importBytes: number;
|
|
21
|
+
/** Data one catalogue (a table built from data files) may read (bytes). */
|
|
22
|
+
catalogueBytes: number;
|
|
23
|
+
/** Rows in one catalogue. */
|
|
24
|
+
catalogueRows: number;
|
|
25
|
+
/** Characters of text in one table cell. */
|
|
26
|
+
cellChars: number;
|
|
27
|
+
/** Bytes of one code example the import transforms (TypeScript → JavaScript with prettier). */
|
|
28
|
+
exampleBytes: number;
|
|
29
|
+
}
|
|
30
|
+
export type LimitName = keyof ImportLimits;
|
|
31
|
+
/**
|
|
32
|
+
* The defaults, for a repo that is not trusted. Measured on cloudflare-docs (6e85ac29,
|
|
33
|
+
* 2026-11-03), the largest real use of each today:
|
|
34
|
+
*/
|
|
35
|
+
export declare const DEFAULT_LIMITS: Readonly<ImportLimits>;
|
|
36
|
+
/** What each limit guards, for the report. */
|
|
37
|
+
export declare const LIMIT_DESCRIPTIONS: Readonly<Record<LimitName, string>>;
|
|
38
|
+
/** The ledger code a tripped limit is reported under. */
|
|
39
|
+
export declare const LIMIT_REACHED = "limit-reached";
|
|
40
|
+
/**
|
|
41
|
+
* The limits an import runs with: the defaults, or none when `trusted`, then each given value.
|
|
42
|
+
* A given value must be a positive number (or Infinity); anything else throws, naming it — a typo
|
|
43
|
+
* must not run with a limit the operator did not mean.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveLimits(given?: Partial<Record<string, unknown>>, opts?: {
|
|
46
|
+
trusted?: boolean;
|
|
47
|
+
}): ImportLimits;
|
|
48
|
+
/**
|
|
49
|
+
* One `--limit name=value` flag: `catalogueBytes=500MB`, `catalogueRows=50000`, `cellChars=none`.
|
|
50
|
+
* Sizes take `KB`/`MB`/`GB` (powers of 1,000) or `KiB`/`MiB`/`GiB`; `none` is no limit.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseLimit(flag: string): [LimitName, number];
|
|
53
|
+
/** A limit's value as a person reads it: `256 MB`, `20,000`, `none`. */
|
|
54
|
+
export declare function formatLimit(name: LimitName, value: number): string;
|
|
55
|
+
/**
|
|
56
|
+
* How an operator raises a limit, by where the import runs: the CLI's flags, `site build`'s config,
|
|
57
|
+
* a host's options (the playground). The report names the one that works where it is read.
|
|
58
|
+
*/
|
|
59
|
+
export type LimitHost = "cli" | "config" | "host";
|
|
60
|
+
/**
|
|
61
|
+
* The report for a tripped limit: what was not done, the measured value against the limit, and
|
|
62
|
+
* how to raise it where the import runs.
|
|
63
|
+
*/
|
|
64
|
+
export declare function limitMessage(what: string, name: LimitName, measured: number | undefined, limit: number, host?: LimitHost): string;
|
|
65
|
+
/** UTF-8 bytes of a string, without encoding it: limits on bytes count bytes, not UTF-16 units. */
|
|
66
|
+
export declare function utf8Bytes(s: string): number;
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import limits: the RESOURCE limits an import puts on someone else's repo, in one place
|
|
3
|
+
* (plans/import-limits.md). They exist so a hostile repo cannot make an import read gigabytes or
|
|
4
|
+
* build a table of a million rows; for the operator's own repo they are only a way for content
|
|
5
|
+
* to turn into a note, so `trusted` sets none.
|
|
6
|
+
*
|
|
7
|
+
* Who may set them: the OPERATOR (CLI flags, a host's `migrate()` options), never the repo being
|
|
8
|
+
* imported. Safety rules (containment, no repo code run, escaping) are NOT limits and are not
|
|
9
|
+
* here: they hold whether the repo is trusted or not.
|
|
10
|
+
*
|
|
11
|
+
* Each default is measured against the largest real value it guards, with the headroom noted.
|
|
12
|
+
* A limit that trips is REPORTED (`LIMIT_REACHED`), never silent.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The defaults, for a repo that is not trusted. Measured on cloudflare-docs (6e85ac29,
|
|
16
|
+
* 2026-11-03), the largest real use of each today:
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_LIMITS = Object.freeze({
|
|
19
|
+
// largest model JSON 496 KB → 32×
|
|
20
|
+
fileBytes: 16_000_000,
|
|
21
|
+
// catalog-models holds 199 files → 100×
|
|
22
|
+
folderFiles: 20_000,
|
|
23
|
+
// all the data files in Cloudflare's src/content (JSON/YAML outside the pages): 15.6 MB → 64×;
|
|
24
|
+
// the whole src/content, pages included, is 54.7 MB
|
|
25
|
+
importBytes: 1_000_000_000,
|
|
26
|
+
// the model catalogue reads 14.1 MB of JSON → 18×
|
|
27
|
+
catalogueBytes: 256_000_000,
|
|
28
|
+
// 272 models, 193 fields → 70×
|
|
29
|
+
catalogueRows: 20_000,
|
|
30
|
+
// longest model description 611 characters → 16×
|
|
31
|
+
cellChars: 10_000,
|
|
32
|
+
// largest <TypeScriptExample> 11.5 KB (median 409 bytes) → 87×; 1 MB is ~1.8 s of prettier
|
|
33
|
+
exampleBytes: 1_000_000,
|
|
34
|
+
});
|
|
35
|
+
/** What each limit guards, for the report. */
|
|
36
|
+
export const LIMIT_DESCRIPTIONS = Object.freeze({
|
|
37
|
+
fileBytes: "bytes in one data file",
|
|
38
|
+
folderFiles: "files in one data folder",
|
|
39
|
+
importBytes: "data read in one import",
|
|
40
|
+
catalogueBytes: "data one catalogue reads",
|
|
41
|
+
catalogueRows: "rows in one catalogue",
|
|
42
|
+
cellChars: "characters in one table cell",
|
|
43
|
+
exampleBytes: "bytes in one code example to transform",
|
|
44
|
+
});
|
|
45
|
+
/** The ledger code a tripped limit is reported under. */
|
|
46
|
+
export const LIMIT_REACHED = "limit-reached";
|
|
47
|
+
/** Every limit unset: what `trusted` gives. */
|
|
48
|
+
const NONE = Object.freeze({
|
|
49
|
+
fileBytes: Infinity,
|
|
50
|
+
folderFiles: Infinity,
|
|
51
|
+
importBytes: Infinity,
|
|
52
|
+
catalogueBytes: Infinity,
|
|
53
|
+
catalogueRows: Infinity,
|
|
54
|
+
cellChars: Infinity,
|
|
55
|
+
exampleBytes: Infinity,
|
|
56
|
+
});
|
|
57
|
+
const isLimitName = (k) => Object.hasOwn(DEFAULT_LIMITS, k);
|
|
58
|
+
/**
|
|
59
|
+
* The limits an import runs with: the defaults, or none when `trusted`, then each given value.
|
|
60
|
+
* A given value must be a positive number (or Infinity); anything else throws, naming it — a typo
|
|
61
|
+
* must not run with a limit the operator did not mean.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveLimits(given, opts = {}) {
|
|
64
|
+
const out = { ...(opts.trusted ? NONE : DEFAULT_LIMITS) };
|
|
65
|
+
for (const [k, v] of Object.entries(given ?? {})) {
|
|
66
|
+
if (v === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
if (!isLimitName(k))
|
|
69
|
+
throw new Error(`No such limit: ${k} (there is ${Object.keys(DEFAULT_LIMITS).join(", ")})`);
|
|
70
|
+
// a whole number ≥ 1, or Infinity (none): `cellChars=0.5` cut a cell to one character and an ellipsis
|
|
71
|
+
if (typeof v !== "number" || !(v === Infinity || (Number.isInteger(v) && v >= 1)))
|
|
72
|
+
throw new Error(`Limit ${k} must be a whole number of at least 1 (or none), not ${JSON.stringify(v)}`);
|
|
73
|
+
out[k] = v;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* One `--limit name=value` flag: `catalogueBytes=500MB`, `catalogueRows=50000`, `cellChars=none`.
|
|
79
|
+
* Sizes take `KB`/`MB`/`GB` (powers of 1,000) or `KiB`/`MiB`/`GiB`; `none` is no limit.
|
|
80
|
+
*/
|
|
81
|
+
export function parseLimit(flag) {
|
|
82
|
+
const m = /^([A-Za-z]+)=(.+)$/.exec(flag.trim());
|
|
83
|
+
if (!m)
|
|
84
|
+
throw new Error(`--limit takes name=value, not ${JSON.stringify(flag)}`);
|
|
85
|
+
const [, name, raw] = m;
|
|
86
|
+
if (!isLimitName(name))
|
|
87
|
+
throw new Error(`No such limit: ${name} (there is ${Object.keys(DEFAULT_LIMITS).join(", ")})`);
|
|
88
|
+
if (/^(none|unlimited|infinity)$/i.test(raw))
|
|
89
|
+
return [name, Infinity];
|
|
90
|
+
const v = /^(\d+(?:\.\d+)?)\s*([KMG]i?B)?$/i.exec(raw.replace(/_/g, ""));
|
|
91
|
+
if (!v)
|
|
92
|
+
throw new Error(`--limit ${name}: ${JSON.stringify(raw)} is not a number (or 500MB, or none)`);
|
|
93
|
+
const unit = (v[2] ?? "").toUpperCase();
|
|
94
|
+
if (unit && !name.endsWith("Bytes"))
|
|
95
|
+
throw new Error(`--limit ${name} is a count, not a size: ${JSON.stringify(raw)}`);
|
|
96
|
+
const scale = { "": 1, KB: 1e3, MB: 1e6, GB: 1e9, KIB: 1024, MIB: 1024 ** 2, GIB: 1024 ** 3 };
|
|
97
|
+
// a size rounds to whole bytes; a count must be whole
|
|
98
|
+
const n = unit ? Math.round(Number(v[1]) * scale[unit]) : Number(v[1]);
|
|
99
|
+
// only the word `none` means no limit: 400 digits is a typo, not Infinity
|
|
100
|
+
if (!Number.isFinite(n) || n > Number.MAX_SAFE_INTEGER)
|
|
101
|
+
throw new Error(`--limit ${name}: ${JSON.stringify(raw)} is too large (use none for no limit)`);
|
|
102
|
+
if (!Number.isInteger(n) || n < 1)
|
|
103
|
+
throw new Error(`--limit ${name} must be a whole number of at least 1, not ${JSON.stringify(raw)}`);
|
|
104
|
+
return [name, n];
|
|
105
|
+
}
|
|
106
|
+
/** A limit's value as a person reads it: `256 MB`, `20,000`, `none`. */
|
|
107
|
+
export function formatLimit(name, value) {
|
|
108
|
+
if (!Number.isFinite(value))
|
|
109
|
+
return "none";
|
|
110
|
+
if (name.endsWith("Bytes")) {
|
|
111
|
+
if (value >= 1e9)
|
|
112
|
+
return `${+(value / 1e9).toFixed(1)} GB`;
|
|
113
|
+
if (value >= 1e6)
|
|
114
|
+
return `${+(value / 1e6).toFixed(1)} MB`;
|
|
115
|
+
if (value >= 1e3)
|
|
116
|
+
return `${+(value / 1e3).toFixed(1)} KB`;
|
|
117
|
+
return `${value} bytes`;
|
|
118
|
+
}
|
|
119
|
+
return value.toLocaleString("en-US");
|
|
120
|
+
}
|
|
121
|
+
const HOW = {
|
|
122
|
+
cli: (name) => `--limit ${name}=…, or --trusted for a repo you trust`,
|
|
123
|
+
config: (name) => `\`limits: { ${name}: … }\` in dogsbay.config.yml, or \`limits: { trusted: true }\``,
|
|
124
|
+
host: (name) => `the import's \`limits.${name}\` option, or \`trusted\` for a repo you trust`,
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* The report for a tripped limit: what was not done, the measured value against the limit, and
|
|
128
|
+
* how to raise it where the import runs.
|
|
129
|
+
*/
|
|
130
|
+
export function limitMessage(what, name, measured, limit, host = "cli") {
|
|
131
|
+
const over = measured === undefined ? `over the ${formatLimit(name, limit)} limit` : `${formatLimit(name, measured)}, over the ${formatLimit(name, limit)} limit`;
|
|
132
|
+
return `${what}: ${LIMIT_DESCRIPTIONS[name]} ${over} (${HOW[host](name)})`;
|
|
133
|
+
}
|
|
134
|
+
/** UTF-8 bytes of a string, without encoding it: limits on bytes count bytes, not UTF-16 units. */
|
|
135
|
+
export function utf8Bytes(s) {
|
|
136
|
+
let n = 0;
|
|
137
|
+
for (let i = 0; i < s.length; i++) {
|
|
138
|
+
const c = s.charCodeAt(i);
|
|
139
|
+
if (c < 0x80)
|
|
140
|
+
n += 1;
|
|
141
|
+
else if (c < 0x800)
|
|
142
|
+
n += 2;
|
|
143
|
+
else if (c >= 0xd800 && c <= 0xdbff && i + 1 < s.length && (s.charCodeAt(i + 1) & 0xfc00) === 0xdc00) {
|
|
144
|
+
n += 4;
|
|
145
|
+
i++;
|
|
146
|
+
}
|
|
147
|
+
else
|
|
148
|
+
n += 3;
|
|
149
|
+
}
|
|
150
|
+
return n;
|
|
151
|
+
}
|
package/dist/meta.d.ts
CHANGED
|
@@ -19,13 +19,19 @@ export interface PageMeta {
|
|
|
19
19
|
tags?: string[];
|
|
20
20
|
/**
|
|
21
21
|
* Broader grouping. Auto-derived from path when frontmatter
|
|
22
|
-
* doesn't supply a valid value
|
|
23
|
-
*
|
|
24
|
-
* `
|
|
22
|
+
* doesn't supply a valid value, emitted as **cumulative slash-
|
|
23
|
+
* paths** so each value uniquely identifies its position in the
|
|
24
|
+
* hierarchy (`content/guides/oauth/setup.md` →
|
|
25
|
+
* `["guides", "guides/oauth"]`). The bare top segment is
|
|
26
|
+
* preserved so simple queries like `category:guides` still match
|
|
27
|
+
* the whole subtree. Override with `category: foo` or
|
|
28
|
+
* `category: [foo, bar]` (explicit values pass through
|
|
29
|
+
* unchanged — no auto-prefix).
|
|
25
30
|
*
|
|
26
31
|
* No first-class "belongs nowhere" sentinel in v1: `category: []`
|
|
27
32
|
* falls through to slug-derived default. See
|
|
28
|
-
* plans/metadata-and-taxonomies.md
|
|
33
|
+
* plans/metadata-and-taxonomies.md and
|
|
34
|
+
* plans/hierarchical-facets.md.
|
|
29
35
|
*/
|
|
30
36
|
category?: string[];
|
|
31
37
|
/**
|
|
@@ -41,6 +47,16 @@ export interface PageMeta {
|
|
|
41
47
|
/** ISO date strings. Consumers parse to Date when needed. */
|
|
42
48
|
updated?: string;
|
|
43
49
|
created?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Post authors. Accepts a single string or a list in frontmatter;
|
|
52
|
+
* always normalized to a list here so consumers have one shape.
|
|
53
|
+
*
|
|
54
|
+
* Added for the blog capability (plans/blog-capability.md) — it was
|
|
55
|
+
* the only field a post needs that PageMeta did not already carry.
|
|
56
|
+
* Absent means "no byline", never "unknown author": the layout omits
|
|
57
|
+
* the row entirely rather than rendering a placeholder.
|
|
58
|
+
*/
|
|
59
|
+
author?: string[];
|
|
44
60
|
/** Sort hint for navigation/list pages. Lower weight first. */
|
|
45
61
|
weight?: number;
|
|
46
62
|
/**
|
|
@@ -77,6 +93,12 @@ export interface ParseMetaOptions {
|
|
|
77
93
|
*/
|
|
78
94
|
taxonomyNames?: readonly string[];
|
|
79
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Closed enum for status validation. Exported so the frontmatter CHECKER
|
|
98
|
+
* (packages/commands) reports exactly the values this parser drops, instead
|
|
99
|
+
* of restating the list a third time.
|
|
100
|
+
*/
|
|
101
|
+
export declare const STATUS_VALUES: readonly PageStatus[];
|
|
80
102
|
/**
|
|
81
103
|
* Parse and normalize frontmatter into `PageMeta`. Pure function;
|
|
82
104
|
* no I/O. Unknown frontmatter keys are not lifted — they stay in
|
|
@@ -97,4 +119,3 @@ export interface ParseMetaOptions {
|
|
|
97
119
|
* `meta.taxonomies` as string arrays.
|
|
98
120
|
*/
|
|
99
121
|
export declare function parseMeta(frontmatter: Record<string, unknown> | undefined, options?: ParseMetaOptions): PageMeta;
|
|
100
|
-
//# sourceMappingURL=meta.d.ts.map
|
package/dist/meta.js
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
*
|
|
9
9
|
* See plans/metadata-and-taxonomies.md for the design rationale.
|
|
10
10
|
*/
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Closed enum for status validation. Exported so the frontmatter CHECKER
|
|
13
|
+
* (packages/commands) reports exactly the values this parser drops, instead
|
|
14
|
+
* of restating the list a third time.
|
|
15
|
+
*/
|
|
16
|
+
export const STATUS_VALUES = [
|
|
13
17
|
"draft",
|
|
14
18
|
"preview",
|
|
15
19
|
"stable",
|
|
@@ -58,6 +62,9 @@ export function parseMeta(frontmatter, options = {}) {
|
|
|
58
62
|
const created = coerceIsoDate(fm.created);
|
|
59
63
|
if (created)
|
|
60
64
|
meta.created = created;
|
|
65
|
+
const author = coerceStringArray(fm.author);
|
|
66
|
+
if (author)
|
|
67
|
+
meta.author = author;
|
|
61
68
|
if (typeof fm.weight === "number" && Number.isFinite(fm.weight)) {
|
|
62
69
|
meta.weight = fm.weight;
|
|
63
70
|
}
|
|
@@ -71,13 +78,29 @@ export function parseMeta(frontmatter, options = {}) {
|
|
|
71
78
|
}
|
|
72
79
|
/**
|
|
73
80
|
* Auto-derive `category` from a slug. The leaf segment is treated
|
|
74
|
-
* as the page filename and dropped; remaining segments
|
|
75
|
-
*
|
|
81
|
+
* as the page filename and dropped; remaining segments emit as
|
|
82
|
+
* **cumulative slash-paths**, so each value uniquely identifies its
|
|
83
|
+
* position in the document hierarchy.
|
|
76
84
|
*
|
|
77
85
|
* - `"guides/auth"` → `["guides"]`
|
|
78
|
-
* - `"guides/oauth/setup"` → `["guides", "oauth"]`
|
|
86
|
+
* - `"guides/oauth/setup"` → `["guides", "guides/oauth"]`
|
|
87
|
+
* - `"installing/installing_aws/ipi/foo"` → `["installing", "installing/installing_aws", "installing/installing_aws/ipi"]`
|
|
79
88
|
* - `"intro"` → undefined (no parent path)
|
|
80
89
|
* - `""` / undefined → undefined
|
|
90
|
+
*
|
|
91
|
+
* Why cumulative instead of bare segments: a leaf name like `ipi`
|
|
92
|
+
* appears at multiple positions across a large corpus (under
|
|
93
|
+
* `installing/installing_aws/`, `installing/installing_azure/`, …).
|
|
94
|
+
* Bare segments would collide as Pagefind filter values, so the
|
|
95
|
+
* search dialog's hierarchical-facet tree can't distinguish
|
|
96
|
+
* positions and tree-click cascades leak across siblings. Cumulative
|
|
97
|
+
* paths disambiguate fully and also let the search dialog detect
|
|
98
|
+
* the slash-encoded shape automatically (no nav.json needed to build
|
|
99
|
+
* the tree). The containment semantic is preserved — every page
|
|
100
|
+
* under `installing/` still carries the bare-prefix `"installing"`
|
|
101
|
+
* value as its top-level entry, so existing filter queries like
|
|
102
|
+
* `category:installing` continue to match the whole subtree.
|
|
103
|
+
* See plans/hierarchical-facets.md ("Open follow-ups").
|
|
81
104
|
*/
|
|
82
105
|
function deriveCategoryFromSlug(slug) {
|
|
83
106
|
if (!slug)
|
|
@@ -85,7 +108,12 @@ function deriveCategoryFromSlug(slug) {
|
|
|
85
108
|
const parts = slug.split("/").filter((p) => p.length > 0);
|
|
86
109
|
if (parts.length < 2)
|
|
87
110
|
return undefined;
|
|
88
|
-
|
|
111
|
+
const parents = parts.slice(0, -1);
|
|
112
|
+
const out = [];
|
|
113
|
+
for (let i = 0; i < parents.length; i++) {
|
|
114
|
+
out.push(parents.slice(0, i + 1).join("/"));
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
89
117
|
}
|
|
90
118
|
function coerceStringArray(value) {
|
|
91
119
|
if (typeof value === "string") {
|
|
@@ -127,4 +155,3 @@ function collectTaxonomies(fm, taxonomyNames) {
|
|
|
127
155
|
}
|
|
128
156
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
129
157
|
}
|
|
130
|
-
//# sourceMappingURL=meta.js.map
|
package/dist/plugin.d.ts
CHANGED
|
@@ -73,9 +73,9 @@ export interface PluginVisibleConfig {
|
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
75
|
* Audit rule contract — re-stated here to avoid a circular dep
|
|
76
|
-
* between `@dogsbay/types` and `@dogsbay/
|
|
76
|
+
* between `@dogsbay/types` and `@dogsbay/site-check` (where the canonical
|
|
77
77
|
* audit module lives). The shape MUST stay in sync with
|
|
78
|
-
* `packages/
|
|
78
|
+
* `@dogsbay/site-check` (`packages/site-check/src/types.ts`) — adapted via a runtime
|
|
79
79
|
* pass-through in the loader.
|
|
80
80
|
*/
|
|
81
81
|
export interface PluginRule {
|
|
@@ -87,7 +87,7 @@ export interface PluginRule {
|
|
|
87
87
|
/**
|
|
88
88
|
* The audit runner narrows this on the rule's declared `stage`.
|
|
89
89
|
* Plugins import the concrete context types from
|
|
90
|
-
* `@dogsbay/
|
|
90
|
+
* `@dogsbay/site-check` if they need typed access.
|
|
91
91
|
*/
|
|
92
92
|
run(ctx: unknown): PluginIssue[];
|
|
93
93
|
}
|
|
@@ -266,4 +266,3 @@ export type PluginConfigEntry = string | {
|
|
|
266
266
|
/** Options forwarded to the plugin's factory. */
|
|
267
267
|
options?: unknown;
|
|
268
268
|
};
|
|
269
|
-
//# sourceMappingURL=plugin.d.ts.map
|
package/dist/plugin.js
CHANGED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text maps: where each line of a stage's output came from in that stage's input.
|
|
3
|
+
*
|
|
4
|
+
* Several stages rewrite a page's text between its file and the parser (example expansion, minja
|
|
5
|
+
* templating, includes, data directives; for DITA, the converter). Each returns a map of what it
|
|
6
|
+
* wrote, and the maps compose, so a block the parser places at a line of the final text can be
|
|
7
|
+
* traced back to the file and lines that made it (plans/content-source-maps.md).
|
|
8
|
+
*
|
|
9
|
+
* Lines are 1-based. A map lists its output in order: every output line is in exactly one segment.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* A branch of an `{% if %}` or `{% switch %}` the output was rendered under, in the segment's input
|
|
13
|
+
* (its file): another variant of the page may show other text, or none, there.
|
|
14
|
+
*/
|
|
15
|
+
export interface TextCondition {
|
|
16
|
+
branch: "if" | "elif" | "else" | "case";
|
|
17
|
+
/** The condition as written (`product == "acme"`; a case: `subject == value`); none for else */
|
|
18
|
+
test?: string;
|
|
19
|
+
/** Lines of the branch's tag */
|
|
20
|
+
from: number;
|
|
21
|
+
to: number;
|
|
22
|
+
/** Lines of the whole block, its opening tag to its end tag */
|
|
23
|
+
block: {
|
|
24
|
+
from: number;
|
|
25
|
+
to: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Output lines copied from the input, one for one: output line k of the segment is input line `from + k - 1`. */
|
|
29
|
+
export interface CopySegment {
|
|
30
|
+
kind: "copy";
|
|
31
|
+
lines: number;
|
|
32
|
+
from: number;
|
|
33
|
+
/** The branches it was rendered under, outermost first */
|
|
34
|
+
conditions?: TextCondition[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Template output (a variable, a filter, lines whitespace control joined) standing for input lines
|
|
38
|
+
* `from`–`to`. The rendered words are not the source's: to change them, edit those source lines
|
|
39
|
+
* (with their `{{ }}` as written), not the output.
|
|
40
|
+
*/
|
|
41
|
+
export interface ExprSegment {
|
|
42
|
+
kind: "expr";
|
|
43
|
+
lines: number;
|
|
44
|
+
from: number;
|
|
45
|
+
to: number;
|
|
46
|
+
conditions?: TextCondition[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Another file's text, included at input lines `from`–`to`. `map` maps these output lines into
|
|
50
|
+
* that file's lines (they may have been rewritten there too); without it, only the include is known.
|
|
51
|
+
*/
|
|
52
|
+
export interface IncludeSegment {
|
|
53
|
+
kind: "include";
|
|
54
|
+
lines: number;
|
|
55
|
+
from: number;
|
|
56
|
+
to: number;
|
|
57
|
+
file: string;
|
|
58
|
+
map?: TextMap;
|
|
59
|
+
/** The branches the include tag was rendered under, in the including text */
|
|
60
|
+
conditions?: TextCondition[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Output with no text of its own to edit: a data table, an example from the examples folder, a
|
|
64
|
+
* DITA related-links list. `from`–`to` is what asked for it (the directive), when anything did.
|
|
65
|
+
*/
|
|
66
|
+
export interface GeneratedSegment {
|
|
67
|
+
kind: "generated";
|
|
68
|
+
lines: number;
|
|
69
|
+
from?: number;
|
|
70
|
+
to?: number;
|
|
71
|
+
conditions?: TextCondition[];
|
|
72
|
+
}
|
|
73
|
+
export type TextSegment = CopySegment | ExprSegment | IncludeSegment | GeneratedSegment;
|
|
74
|
+
export interface TextMap {
|
|
75
|
+
segments: TextSegment[];
|
|
76
|
+
}
|
|
77
|
+
/** Where one output line came from (`lookupTextLine`). */
|
|
78
|
+
export interface TextOrigin {
|
|
79
|
+
kind: TextSegment["kind"];
|
|
80
|
+
/** The file the position is in: undefined for the text the first map was given (the page). */
|
|
81
|
+
file?: string;
|
|
82
|
+
/** The line, for a copied line. */
|
|
83
|
+
line?: number;
|
|
84
|
+
/** The input lines that made it (a copied line: `line` both times); absent for generated output with no source. */
|
|
85
|
+
from?: number;
|
|
86
|
+
to?: number;
|
|
87
|
+
/** For `include` with no map: the file included. */
|
|
88
|
+
included?: string;
|
|
89
|
+
/** The includes it came through, outermost first: each in `file` (undefined: the page) at lines `from`–`to`. */
|
|
90
|
+
via: Array<{
|
|
91
|
+
file?: string;
|
|
92
|
+
from: number;
|
|
93
|
+
to: number;
|
|
94
|
+
}>;
|
|
95
|
+
/**
|
|
96
|
+
* The `{% if %}` / `{% switch %}` branches it was rendered under, outermost first, each in `file`
|
|
97
|
+
* (undefined: the page): through an include, the include tag's and then the included file's.
|
|
98
|
+
*/
|
|
99
|
+
conditions?: Array<TextCondition & {
|
|
100
|
+
file?: string;
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* For a range (`rangeOrigin`): some source lines between `from` and `to` are not in it (a false
|
|
104
|
+
* `{% if %}` removed them), so its text is not the source's even where every line is copied.
|
|
105
|
+
*/
|
|
106
|
+
gaps?: true;
|
|
107
|
+
}
|
|
108
|
+
/** A map of text passed through unchanged. */
|
|
109
|
+
export declare function identityTextMap(lines: number): TextMap;
|
|
110
|
+
/** How many output lines a map covers. */
|
|
111
|
+
export declare function textMapLines(map: TextMap): number;
|
|
112
|
+
/** The segments for output lines `start`–`start + count - 1`, cut to fit; lines past the map's end are generated. */
|
|
113
|
+
export declare function sliceTextMap(map: TextMap, start: number, count: number): TextMap;
|
|
114
|
+
/**
|
|
115
|
+
* Two stages as one: `first` maps the first stage's output to its input (the file), `second` maps
|
|
116
|
+
* the second stage's output to ITS input, which is the first stage's output. The result maps the
|
|
117
|
+
* second stage's output to the file.
|
|
118
|
+
*/
|
|
119
|
+
export declare function composeTextMaps(first: TextMap, second: TextMap): TextMap;
|
|
120
|
+
/** Where output line `line` came from; undefined past the map's end. */
|
|
121
|
+
export declare function lookupTextLine(map: TextMap, line: number): TextOrigin | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Every output line's origin, in order: what `lookupTextLine` gives for each, in one walk of the
|
|
124
|
+
* map (a page's blocks look up ranges of it, so a parse stays linear in the page).
|
|
125
|
+
*/
|
|
126
|
+
export declare function textLineOrigins(map: TextMap): TextOrigin[];
|
|
127
|
+
/**
|
|
128
|
+
* One origin for several lines (a block): in the deepest file they all share, from the first
|
|
129
|
+
* line to the last there. Lines that went further into an include count as that include's tag.
|
|
130
|
+
* The kind is the least editable of the lines': generated, then template output, then an include,
|
|
131
|
+
* then copied text. Undefined for no lines.
|
|
132
|
+
*/
|
|
133
|
+
export declare function rangeOrigin(origins: readonly TextOrigin[]): TextOrigin | undefined;
|
|
134
|
+
/** Where output lines `from`–`to` came from, as one origin (`rangeOrigin`). */
|
|
135
|
+
export declare function lookupTextRange(map: TextMap, from: number, to: number): TextOrigin | undefined;
|
|
136
|
+
/** Where one output line came from, for `textMapOfLines`: an input line copied, or a span (or nothing) it stands for. */
|
|
137
|
+
export type LineSource = number | {
|
|
138
|
+
kind: "expr" | "generated";
|
|
139
|
+
from?: number;
|
|
140
|
+
to?: number;
|
|
141
|
+
};
|
|
142
|
+
/** The map of an output whose line k came from `sources[k - 1]`: consecutive copies and equal spans become one segment. */
|
|
143
|
+
export declare function textMapOfLines(sources: readonly LineSource[]): TextMap;
|
|
144
|
+
/**
|
|
145
|
+
* The map of a rewrite nobody recorded (a host's tidy-up pass after templating): lines equal in both
|
|
146
|
+
* are copies, found by a line diff; a changed run stands for the lines it replaced (`expr`), an
|
|
147
|
+
* inserted run for nothing (`generated`). Lines are compared as written.
|
|
148
|
+
*/
|
|
149
|
+
export declare function diffTextMap(before: string, after: string): TextMap;
|
|
150
|
+
/**
|
|
151
|
+
* A map whose include segments counted the lines of text a host's loader changed before the
|
|
152
|
+
* renderer read it, put back in the files' own lines: each included file's segments (their nested
|
|
153
|
+
* maps, recursively) move down by `linesBefore(file)`, the lines the loader removed from the top
|
|
154
|
+
* of that file (its front matter, say). `file` is the name the include segment gives. The page's
|
|
155
|
+
* own segments are left as they are.
|
|
156
|
+
*/
|
|
157
|
+
export declare function offsetIncludeMaps(map: TextMap, linesBefore: (file: string) => number): TextMap;
|
|
158
|
+
/**
|
|
159
|
+
* How far a leading YAML front matter block (`---` … `---`) moves the rest of the text down: line 1
|
|
160
|
+
* of the text after it is line `1 + frontMatterLines(text)` of the text. 0 when there is none. What a
|
|
161
|
+
* loader stripping it removed from the top, for `offsetIncludeMaps`.
|
|
162
|
+
*/
|
|
163
|
+
export declare function frontMatterLines(text: string): number;
|