@heroiclands/package-build 15.0.0 → 16.0.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.
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The `{#slug}` anchors a note's body declares, with where each one sits.
3
+ *
4
+ * Only headings carrying an explicit anchor are collected. A bare `#` heading
5
+ * also starts a journal page, but it declares no slug, so nothing can address
6
+ * it with `#…` — listing it would offer a link that cannot be written.
7
+ *
8
+ * @param {string} body - The note's markdown body, frontmatter already removed.
9
+ * @param {number} [bodyLine] - The 1-based file line the body starts on, from
10
+ * `parseMarkdownFile`. Anchors are reported at their position in the **file**,
11
+ * so an editor can jump straight to one; passing nothing numbers from the body.
12
+ * @returns {Array<{slug: string, name: string, level: number, line: number}>}
13
+ * In document order.
14
+ */
15
+ export function collectAnchors(body: string, bodyLine?: number): Array<{
16
+ slug: string;
17
+ name: string;
18
+ level: number;
19
+ line: number;
20
+ }>;
21
+ /**
22
+ * The address a wikilink writes to reach a note, or `null` when it has none.
23
+ *
24
+ * A wikilink target is an address: `being-aurochs` locally, or
25
+ * `sohl-being-aurochs` from another package (`readQualifier` also accepts
26
+ * `being/aurochs`, the same two fields with a different separator). Both forms
27
+ * are already derivable from `type` and `shortcode`, which every record
28
+ * carries — so this field adds no information. What it adds is the *rule*:
29
+ * the lowercasing and the hyphen join live in one place, and a consumer that
30
+ * reimplements them slightly differently gets a lookup that matches nothing and
31
+ * says nothing about why. That is a real failure, not a hypothetical one — it
32
+ * is precisely how a resolver keyed on a bare `type/shortcode` silently misses
33
+ * every canonical `pkg-type-shortcode` entry.
34
+ *
35
+ * Derived by the same functions the link manifest and the site build use, so an
36
+ * index cannot disagree with either about where a note lives.
37
+ *
38
+ * @param {Record<string, any>} frontmatter - The note's parsed frontmatter.
39
+ * @param {string} contentPackage - The package the tree compiles as.
40
+ * @returns {{slug: string, canonical: string}|null} `slug` is what goes inside
41
+ * `[[…]]` within this package; `canonical` is the package-qualified key the
42
+ * manifest files the note under. `null` for a note with no type or no
43
+ * shortcode, which has no address at all and is stated as such rather than
44
+ * left for every reader to rediscover.
45
+ */
46
+ export function noteAddress(frontmatter: Record<string, any>, contentPackage: string): {
47
+ slug: string;
48
+ canonical: string;
49
+ } | null;
50
+ /**
51
+ * Recursively sort an object's keys, so serialization is order-independent.
52
+ *
53
+ * Arrays keep their order — it is authored — but every object inside one is
54
+ * sorted too. Anything that is not a plain object is returned as it is.
55
+ *
56
+ * @param {unknown} value - The value to normalize.
57
+ * @returns {unknown} The value with every plain object's keys in sorted order.
58
+ */
59
+ export function sortKeysDeep(value: unknown): unknown;
60
+ /**
61
+ * A note's display name reduced to printable 7-bit ASCII.
62
+ *
63
+ * Content names carry the setting's orthography — `Kûrbúl Helm`, `Hârn`,
64
+ * `Kèthîra` — and nobody types them. A reader searching the index, or an editor
65
+ * completing a wikilink, needs a form that matches what a keyboard produces, so
66
+ * the record states one rather than leaving every consumer to invent it (and to
67
+ * invent a *different* one, which is how two searches over the same data come
68
+ * to disagree).
69
+ *
70
+ * **Transliterated, not stripped.** `unidecode` — the same table
71
+ * {@link slugify} already runs, so an ASCII name and a slug can never disagree
72
+ * about a character — carries a letter across rather than deleting it:
73
+ * diacritics fold (`â`→`a`, `è`→`e`), ligatures expand (`æ`→`ae`, `Œ`→`OE`,
74
+ * `ß`→`ss`), the runic letters spell out (`þ`→`th`, `Þ`→`Th`, `ð`→`d`), and
75
+ * even a vulgar fraction becomes readable (`¾`→`3/4`). Deleting them instead
76
+ * would collapse `Kûrbúl` to `Krbl`, which is worse than the original.
77
+ *
78
+ * Anything still outside printable ASCII after that becomes a space, and runs
79
+ * of whitespace collapse — a space rather than nothing, so a character that
80
+ * transliterates away cannot silently weld two words together.
81
+ *
82
+ * The value is emitted even when it equals the name, so a consumer matching on
83
+ * it never has to branch on whether the name happened to be ASCII already.
84
+ *
85
+ * @param {unknown} name - The note's `name.full`.
86
+ * @returns {string|null} The ASCII form, or `null` when there is no name, or
87
+ * nothing printable survives.
88
+ */
89
+ export function asciiName(name: unknown): string | null;
90
+ /**
91
+ * A note's `name.aliases` reduced to printable 7-bit ASCII, in order.
92
+ *
93
+ * An alias is the name a reader is at least as likely to reach for as the
94
+ * canonical one — `Killer Whale` for an orca, `Ice Bear` for a polar bear,
95
+ * `Ix'balam` for a jaguar — so anything searching or completing over the index
96
+ * has to match them too, and needs the same keyboard-typeable form
97
+ * {@link asciiName} gives the primary name.
98
+ *
99
+ * Order is the authored order, so a caller can pair an entry with the alias it
100
+ * came from. An alias that is not a non-empty string, or that leaves nothing
101
+ * printable behind, is dropped rather than left as a hole — the array is a set
102
+ * of names to match, and a null in it is not one.
103
+ *
104
+ * @param {unknown} aliases - The note's `name.aliases`; may be absent or null.
105
+ * @returns {Array<string>} Possibly empty, never null: a note with no aliases
106
+ * has an empty set of them, which is a fact rather than a missing value, and
107
+ * a consumer iterating it should not have to check first.
108
+ */
109
+ export function asciiAliases(aliases: unknown): Array<string>;
110
+ /**
111
+ * Build one index record from a note's frontmatter and its place in the tree.
112
+ *
113
+ * @param {object} options - Options.
114
+ * @param {Record<string, any>} options.frontmatter - The note's parsed frontmatter.
115
+ * @param {string} options.relPath - Its path below the content root, POSIX-separated.
116
+ * @param {string} options.contentPackage - The package the tree compiles as.
117
+ * @param {string} [options.body] - The note's markdown body, for its anchors.
118
+ * @param {number} [options.bodyLine] - The 1-based file line the body starts on.
119
+ * @returns {Record<string, any>} The record, keys sorted at every depth.
120
+ * @throws {Error} When the note carries a key this module derives, which would
121
+ * otherwise be overwritten without a word.
122
+ */
123
+ export function buildIndexRecord({ frontmatter, relPath, contentPackage, body, bodyLine }: {
124
+ frontmatter: Record<string, any>;
125
+ relPath: string;
126
+ contentPackage: string;
127
+ body?: string | undefined;
128
+ bodyLine?: number | undefined;
129
+ }): Record<string, any>;
130
+ /**
131
+ * Read a content tree into index records, in the order they will be written.
132
+ *
133
+ * @param {string} contentBase - The content tree to walk.
134
+ * @param {object} options - Options.
135
+ * @param {string} options.contentPackage - The package the tree compiles as.
136
+ * @param {Array<string>} [options.skipDirectories] - Directory names to skip.
137
+ * @returns {Array<Record<string, any>>} The records, in a total order that does
138
+ * not depend on directory-read order.
139
+ */
140
+ export function collectContentIndex(contentBase: string, { contentPackage, skipDirectories }: {
141
+ contentPackage: string;
142
+ skipDirectories?: string[] | undefined;
143
+ }): Array<Record<string, any>>;
144
+ /**
145
+ * Serialize records as JSON Lines.
146
+ *
147
+ * @param {Array<Record<string, any>>} records - From {@link collectContentIndex}.
148
+ * @returns {string} One compact JSON object per line, newline-terminated. An
149
+ * empty set serializes to the empty string rather than to a lone newline, so
150
+ * the file is exactly the lines it holds.
151
+ */
152
+ export function serializeContentIndex(records: Array<Record<string, any>>): string;
153
+ /**
154
+ * Emit this package's content index.
155
+ *
156
+ * @param {object} [options] - Options.
157
+ * @param {string} [options.contentBase] - The content tree; defaults to the
158
+ * configured `paths.content`.
159
+ * @param {string} [options.outDir] - Where to write; defaults to the configured
160
+ * `paths.contentIndex`.
161
+ * @param {object} [options.config] - A resolved configuration; loaded when omitted.
162
+ * @returns {{file: string, notes: number, bytes: number}} Where it was written,
163
+ * how many notes it holds, and its size.
164
+ * @throws {Error} When the content tree is absent, or when it yields no note at
165
+ * all — an empty index is indistinguishable from a mis-pointed tree, and a
166
+ * reader would take it as the authoritative statement that this package has
167
+ * no content.
168
+ */
169
+ export function emitContentIndex({ contentBase, outDir, config }?: {
170
+ contentBase?: string | undefined;
171
+ outDir?: string | undefined;
172
+ config?: object | undefined;
173
+ }): {
174
+ file: string;
175
+ notes: number;
176
+ bytes: number;
177
+ };
178
+ /**
179
+ * The keys this module adds to a record, which a note therefore may not carry
180
+ * itself.
181
+ *
182
+ * `package` is the note's distribution unit — the configured `contentPackage`,
183
+ * since a note declaring its own is a hard error (package-build#56) — and it
184
+ * matches what the content-table expander puts on the same field, so a query
185
+ * reads the same value from either. `file` namespaces the note's place in the
186
+ * tree, again matching the expander's `file.*`.
187
+ *
188
+ * Both are checked rather than assumed: `folder` is real frontmatter on most
189
+ * notes, so the neighbouring names are close enough to a real key that a silent
190
+ * overwrite is a plausible future rather than a hypothetical one.
191
+ *
192
+ * @type {ReadonlyArray<string>}
193
+ */
194
+ export const DERIVED_KEYS: ReadonlyArray<string>;
@@ -17,6 +17,7 @@ export * as contentAddress from "./content-address.mjs";
17
17
  export * as foreignManifests from "./foreign-manifests.mjs";
18
18
  export * as kbManifest from "./kb-manifest.mjs";
19
19
  export * as manifestEmit from "./manifest-emit.mjs";
20
+ export * as contentIndex from "./content-index.mjs";
20
21
  export * as siteBuild from "./site-build.mjs";
21
22
  export * as contentLint from "./content-lint.mjs";
22
23
  export * as contentLinks from "./content-links.mjs";