@heroiclands/package-build 23.12.4 → 23.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/docs/api.md +1 -0
  3. package/docs/authoring/frontmatter.md +1 -1
  4. package/docs/authoring/links-and-markup.md +4 -4
  5. package/docs/configuration.md +3 -1
  6. package/docs/guides/book.md +12 -12
  7. package/docs/reference/format-details.md +28 -16
  8. package/docs/reference/note-types.md +1 -1
  9. package/engine/content-index.mjs +2 -39
  10. package/engine/event-fields.mjs +24 -1
  11. package/engine/event-rows.mjs +37 -2
  12. package/engine/event-views.mjs +58 -14
  13. package/engine/frontmatter-lint.mjs +11 -0
  14. package/engine/generated-sections.mjs +8 -1
  15. package/engine/holdings.mjs +8 -2
  16. package/engine/infobox-render.mjs +16 -5
  17. package/engine/infobox.mjs +23 -2
  18. package/engine/literature-works.mjs +8 -2
  19. package/engine/metadata-index.mjs +3 -0
  20. package/engine/name-sort.mjs +91 -0
  21. package/engine/note-frontmatter.mjs +1 -1
  22. package/engine/note-vocabulary.mjs +1 -1
  23. package/engine/page-lists.mjs +3 -2
  24. package/engine/pdf-index.mjs +11 -4
  25. package/engine/pdf-pages.mjs +2 -1
  26. package/engine/pdf-render.mjs +43 -13
  27. package/engine/pdf-toc.mjs +4 -3
  28. package/engine/related-pages.mjs +12 -2
  29. package/engine/site-build.mjs +3 -0
  30. package/engine/sql-tables.mjs +20 -5
  31. package/package.json +1 -1
  32. package/types/engine/content-index.d.mts +2 -31
  33. package/types/engine/event-fields.d.mts +11 -0
  34. package/types/engine/event-rows.d.mts +4 -1
  35. package/types/engine/event-views.d.mts +7 -3
  36. package/types/engine/generated-sections.d.mts +6 -0
  37. package/types/engine/holdings.d.mts +7 -1
  38. package/types/engine/infobox.d.mts +11 -0
  39. package/types/engine/literature-works.d.mts +5 -0
  40. package/types/engine/name-sort.d.mts +49 -0
  41. package/types/engine/pdf-render.d.mts +8 -0
  42. package/types/engine/pdf-toc.d.mts +1 -1
  43. package/types/engine/related-pages.d.mts +5 -0
@@ -32,6 +32,7 @@ import { expandContentTables } from "./content-tables.mjs";
32
32
  import { NOTE_VOCABULARY, isGmNote } from "./note-vocabulary.mjs";
33
33
  import { encodeAddresses, flattenPublishedAddresses } from "./address-values.mjs";
34
34
  import { EVENT_COLUMNS, EVENT_SORT_COLUMNS, eventRows } from "./event-rows.mjs";
35
+ import { reckoningContext } from "./reckoning-markers.mjs";
35
36
  import { NAME_COLUMNS, namesRows } from "./name-rows.mjs";
36
37
  import {
37
38
  GLOSSARY_COLUMNS,
@@ -66,6 +67,7 @@ import { parseMarkdownFile } from "./helpers.mjs";
66
67
  import { sqlQueriesInMarkdown } from "./markdown-expressions.mjs";
67
68
  // The record accessors only — see `engine/index-records.mjs`.
68
69
  import { authoredFrontmatter, isNoteRecord, noteFile, sortKeysDeep } from "./index-records.mjs";
70
+ import { nameSortOf } from "./name-sort.mjs";
69
71
 
70
72
  /** Rendered in a cell whose value is absent. */
71
73
  const EMPTY_CELL = "—";
@@ -438,11 +440,25 @@ export async function openNotesDatabase(
438
440
  */
439
441
  const WITH_STUBS_SCHEMA = "__with_stubs";
440
442
 
443
+ /**
444
+ * The record with its `nameSort` stated: the declared `name.sort`, otherwise
445
+ * `name.full`. A record with no name has none.
446
+ *
447
+ * @param {object} record - An index record.
448
+ * @returns {object} The record, with `nameSort` where it has a name.
449
+ */
450
+ function withNameSort(record) {
451
+ const key = nameSortOf(record);
452
+ return key === undefined ? record : { ...record, nameSort: key };
453
+ }
454
+
441
455
  /**
442
456
  * Create the two relations an index is read through, and the ladder one of them
443
457
  * derives.
444
458
  *
445
- * **`entries` is every row**, stubs included, plus a derived `state` column:
459
+ * **`entries` is every row**, stubs included, plus a derived `nameSort` column
460
+ * (the note's `name.sort`, otherwise its `name.full`) and a derived `state`
461
+ * column:
446
462
  *
447
463
  * ```text
448
464
  * address IS NULL → stub
@@ -484,7 +500,7 @@ async function createRelations(connection, file, records, prefix = "", audience
484
500
  records
485
501
  .map((record) =>
486
502
  JSON.stringify({
487
- record: sortKeysDeep(record),
503
+ record: sortKeysDeep(withNameSort(record)),
488
504
  visible: audience !== "public" || !isGmNote(record),
489
505
  }),
490
506
  )
@@ -535,9 +551,8 @@ async function createRelations(connection, file, records, prefix = "", audience
535
551
  * @returns {Promise<void>}
536
552
  */
537
553
  async function createEventRelation(connection, file, records, prefix, audience) {
538
- const rows = eventRows(
539
- audience === "public" ? records.filter((record) => !isGmNote(record)) : records,
540
- );
554
+ const visible = audience === "public" ? records.filter((record) => !isGmNote(record)) : records;
555
+ const rows = eventRows(visible, reckoningContext({ notes: records }));
541
556
  if (!rows.length) {
542
557
  const columns = EVENT_COLUMNS.map((column) => {
543
558
  const type = EVENT_SORT_COLUMNS.includes(column) ? "DOUBLE" : "VARCHAR";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "23.12.4",
3
+ "version": "23.12.5",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -31,36 +31,6 @@ export function noteAddress(frontmatter: Record<string, any>, contentPackage: st
31
31
  slug: string;
32
32
  canonical: string;
33
33
  } | null;
34
- /**
35
- * A note's display name reduced to printable 7-bit ASCII.
36
- *
37
- * Content names carry the setting's orthography — `Kûrbúl Helm`, `Hârn`,
38
- * `Kèthîra` — and nobody types them. A reader searching the index, or an editor
39
- * completing a wikilink, needs a form that matches what a keyboard produces, so
40
- * the record states one rather than leaving every consumer to invent it (and to
41
- * invent a *different* one, which is how two searches over the same data come
42
- * to disagree).
43
- *
44
- * **Transliterated, not stripped.** `unidecode` — the same table
45
- * {@link slugify} already runs, so an ASCII name and a slug can never disagree
46
- * about a character — carries a letter across rather than deleting it:
47
- * diacritics fold (`â`→`a`, `è`→`e`), ligatures expand (`æ`→`ae`, `Œ`→`OE`,
48
- * `ß`→`ss`), the runic letters spell out (`þ`→`th`, `Þ`→`Th`, `ð`→`d`), and
49
- * even a vulgar fraction becomes readable (`¾`→`3/4`). Deleting them instead
50
- * would collapse `Kûrbúl` to `Krbl`, which is worse than the original.
51
- *
52
- * Anything still outside printable ASCII after that becomes a space, and runs
53
- * of whitespace collapse — a space rather than nothing, so a character that
54
- * transliterates away cannot silently weld two words together.
55
- *
56
- * The value is emitted even when it equals the name, so a consumer matching on
57
- * it never has to branch on whether the name happened to be ASCII already.
58
- *
59
- * @param {unknown} name - The note's `name.full`.
60
- * @returns {string|null} The ASCII form, or `null` when there is no name, or
61
- * nothing printable survives.
62
- */
63
- export function asciiName(name: unknown): string | null;
64
34
  /**
65
35
  * A note's `name.aliases` reduced to printable 7-bit ASCII, in order.
66
36
  *
@@ -449,6 +419,7 @@ export type PreparedContentIndex = {
449
419
  */
450
420
  syntax?: import("./content-session.mjs").ContentSyntax | undefined;
451
421
  };
422
+ import { asciiName } from "./name-sort.mjs";
452
423
  import { collectAnchors } from "./anchors.mjs";
453
424
  import { authoredFrontmatter } from "./index-records.mjs";
454
425
  import { DERIVED_KEYS } from "./index-records.mjs";
@@ -458,4 +429,4 @@ import { isStub } from "./index-records.mjs";
458
429
  import { noteFile } from "./index-records.mjs";
459
430
  import { recordPath } from "./index-records.mjs";
460
431
  import { sortKeysDeep } from "./index-records.mjs";
461
- export { collectAnchors, authoredFrontmatter, DERIVED_KEYS, isAssetRecord, isNoteRecord, isStub, noteFile, recordPath, sortKeysDeep };
432
+ export { asciiName, collectAnchors, authoredFrontmatter, DERIVED_KEYS, isAssetRecord, isNoteRecord, isStub, noteFile, recordPath, sortKeysDeep };
@@ -46,6 +46,17 @@ export function eventNoteIndex(records: Iterable<Record<string, any>>, { content
46
46
  * @returns {string} The printed date.
47
47
  */
48
48
  export function printEventDate(written: unknown, date: object | null, dates?: object): string;
49
+ /**
50
+ * A date as `dateformat` prints it in a calendar, where one is named and
51
+ * prints it; otherwise as {@link printEventDate} prints it.
52
+ *
53
+ * @param {unknown} written - The date as the event writes it.
54
+ * @param {object|null} date - The resolved date.
55
+ * @param {object} [dates] - The reckoning context.
56
+ * @param {string} [calendar] - The calendar's Address or shortcode.
57
+ * @returns {string} The printed date; the authored text when nothing resolves.
58
+ */
59
+ export function printEventDateIn(written: unknown, date: object | null, dates?: object, calendar?: string): string;
49
60
  /**
50
61
  * One field of one event, as the text a reference prints, or why it cannot.
51
62
  *
@@ -3,10 +3,13 @@
3
3
  *
4
4
  * @param {Iterable<Record<string, any>>} records - Index records, every
5
5
  * Address a string, already narrowed to the audience being published to.
6
+ * @param {object} [dates] - The reckoning context the dates are printed in.
6
7
  * @returns {object[]} One row per event entry, in record then entry order.
7
8
  */
8
- export function eventRows(records: Iterable<Record<string, any>>): object[];
9
+ export function eventRows(records: Iterable<Record<string, any>>, dates?: object): object[];
9
10
  /** The sort keys a row carries, after the event's own keys. */
10
11
  export const EVENT_SORT_COLUMNS: readonly string[];
12
+ /** The printed dates a row carries, after the sort keys. */
13
+ export const EVENT_TEXT_COLUMNS: readonly string[];
11
14
  /** Every column of the `events` relation, in order. */
12
15
  export const EVENT_COLUMNS: any;
@@ -34,18 +34,22 @@ export function eventViewSections(db: object, records: Iterable<object>, fileOf:
34
34
  *
35
35
  * @type {ReadonlyArray<Readonly<{slug: string, heading: string,
36
36
  * applies: (record: object) => boolean,
37
- * query: (self: string[], shape: CorpusShape) => {fence: string, own?: string}|null,
38
- * owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>}>>}
37
+ * query: (self: string[], shape: CorpusShape, columns?: {felt?: boolean}) => {fence: string, own?: string}|null,
38
+ * owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>,
39
+ * felt?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>}>>}
39
40
  */
40
41
  export const EVENT_VIEWS: ReadonlyArray<Readonly<{
41
42
  slug: string;
42
43
  heading: string;
43
44
  applies: (record: object) => boolean;
44
- query: (self: string[], shape: CorpusShape) => {
45
+ query: (self: string[], shape: CorpusShape, columns?: {
46
+ felt?: boolean;
47
+ }) => {
45
48
  fence: string;
46
49
  own?: string;
47
50
  } | null;
48
51
  owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>;
52
+ felt?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>;
49
53
  }>>;
50
54
  export type CorpusShape = {
51
55
  /**
@@ -110,6 +110,12 @@ export function splitGenerated(markdown: string): {
110
110
  *
111
111
  * @module
112
112
  */
113
+ /**
114
+ * The anchor of the Chronology section, which the book sets across the page.
115
+ *
116
+ * @type {string}
117
+ */
118
+ export const CHRONOLOGY_SLUG: string;
113
119
  /**
114
120
  * Every generated section, in the order it follows the author's text: where a
115
121
  * place sits and who governs it, then what happened, then what was written
@@ -1,10 +1,11 @@
1
- /** @typedef {{title:string,url?:string,address:string,type:string,subType?:string}} HoldingsEntry */
1
+ /** @typedef {{title:string,sort?:string,url?:string,address:string,type:string,subType?:string}} HoldingsEntry */
2
2
  /**
3
3
  * @typedef {object} HoldingsNode
4
4
  * @property {string} shortcode
5
5
  * @property {"place"|"affiliation"} type
6
6
  * @property {string} [subType]
7
7
  * @property {string} title
8
+ * @property {string} [sort] The note's `name.sort`; the list files by it in place of `title`.
8
9
  * @property {string} [url]
9
10
  * @property {string} [package] Owning content package.
10
11
  * @property {string} [system] Own document system.
@@ -60,6 +61,7 @@ export function holdingsPages(nodes: Iterable<HoldingsNode | null>, { government
60
61
  export function checkGovernment(note: object): object[];
61
62
  export type HoldingsEntry = {
62
63
  title: string;
64
+ sort?: string;
63
65
  url?: string;
64
66
  address: string;
65
67
  type: string;
@@ -70,6 +72,10 @@ export type HoldingsNode = {
70
72
  type: "place" | "affiliation";
71
73
  subType?: string | undefined;
72
74
  title: string;
75
+ /**
76
+ * The note's `name.sort`; the list files by it in place of `title`.
77
+ */
78
+ sort?: string | undefined;
73
79
  url?: string | undefined;
74
80
  /**
75
81
  * Owning content package.
@@ -94,6 +94,17 @@ export function hasRenderableValue(kind: string, value: unknown): boolean;
94
94
  * @returns {string} One of {@link INFOBOX_VALUE_KINDS}.
95
95
  */
96
96
  export function valueKindOf(field: object, value: unknown): string;
97
+ /**
98
+ * A number as a reader reads it in a profile: under a million in full with
99
+ * thousands separators (`850`, `12,500`), and from a million up in words with
100
+ * at most one decimal (`11 million`, `1.2 million`, `3 billion`). Every figure
101
+ * the setting gives at that scale is an estimate, and the words say so.
102
+ * Decimals below a million keep the digits they were written with.
103
+ *
104
+ * @param {number} value - The number.
105
+ * @returns {string} The text the row shows.
106
+ */
107
+ export function formatQuantity(value: number): string;
97
108
  /**
98
109
  * One row, with its unit on it.
99
110
  *
@@ -38,6 +38,7 @@ export function worksPages(nodes: Iterable<WorksNode | null>, { types }: {
38
38
  }>;
39
39
  export type WorkEntry = {
40
40
  title: string;
41
+ sort?: string;
41
42
  url?: string;
42
43
  address: string;
43
44
  form?: string;
@@ -48,6 +49,10 @@ export type WorksNode = {
48
49
  */
49
50
  key: string;
50
51
  title: string;
52
+ /**
53
+ * The note's `name.sort`; the list files by it in place of `title`.
54
+ */
55
+ sort?: string | undefined;
51
56
  /**
52
57
  * Absent for a note that publishes no page.
53
58
  */
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A note's display name reduced to printable 7-bit ASCII.
3
+ *
4
+ * Content names carry the setting's orthography — `Kûrbúl Helm`, `Hârn`,
5
+ * `Kèthîra` — and nobody types them. A reader searching the index, or an editor
6
+ * completing a wikilink, needs a form that matches what a keyboard produces, so
7
+ * the record states one rather than leaving every consumer to invent it (and to
8
+ * invent a *different* one, which is how two searches over the same data come
9
+ * to disagree).
10
+ *
11
+ * **Transliterated, not stripped.** `unidecode` — the same table
12
+ * {@link slugify} already runs, so an ASCII name and a slug can never disagree
13
+ * about a character — carries a letter across rather than deleting it:
14
+ * diacritics fold (`â`→`a`, `è`→`e`), ligatures expand (`æ`→`ae`, `Œ`→`OE`,
15
+ * `ß`→`ss`), the runic letters spell out (`þ`→`th`, `Þ`→`Th`, `ð`→`d`), and
16
+ * even a vulgar fraction becomes readable (`¾`→`3/4`). Deleting them instead
17
+ * would collapse `Kûrbúl` to `Krbl`, which is worse than the original.
18
+ *
19
+ * Anything still outside printable ASCII after that becomes a space, and runs
20
+ * of whitespace collapse — a space rather than nothing, so a character that
21
+ * transliterates away cannot silently weld two words together.
22
+ *
23
+ * The value is emitted even when it equals the name, so a consumer matching on
24
+ * it never has to branch on whether the name happened to be ASCII already.
25
+ *
26
+ * @param {unknown} name - The note's `name.full`.
27
+ * @returns {string|null} The ASCII form, or `null` when there is no name, or
28
+ * nothing printable survives.
29
+ */
30
+ export function asciiName(name: unknown): string | null;
31
+ /**
32
+ * The name a note files under: its `name.sort` when it declares one, otherwise
33
+ * its `name.full`.
34
+ *
35
+ * @param {unknown} record - An index record or a note's frontmatter.
36
+ * @returns {string|undefined} The unfolded key, or `undefined` for a record
37
+ * with no name.
38
+ */
39
+ export function nameSortOf(record: unknown): string | undefined;
40
+ /**
41
+ * The key a name-ordered list compares notes on: {@link nameSortOf} folded to
42
+ * printable ASCII by {@link asciiName}, so an accented sort key files beside
43
+ * its base letters. Case is left for the caller, which decides whether its
44
+ * comparison ignores it.
45
+ *
46
+ * @param {unknown} record - An index record or a note's frontmatter.
47
+ * @returns {string} The key, or "" for a record with no name.
48
+ */
49
+ export function nameSortKey(record: unknown): string;
@@ -169,6 +169,10 @@ export function absolutePageUrl(url: string, site?: string): string;
169
169
  * carried, then the book and the page. A book with an abbreviation reads
170
170
  * `Name (Aelw. p12)`; one without reads `Name (_Book title_, p. 12)`.
171
171
  *
172
+ * A link the text already closes a bracket on, as in `(see Name)`, takes the
173
+ * reference inside that bracket after a comma, `(see Name, Aelw. p12)`, so the
174
+ * brackets never nest.
175
+ *
172
176
  * The space before the page number, or between the abbreviation and the page,
173
177
  * does not break, so a number never starts a line of its own.
174
178
  *
@@ -176,12 +180,16 @@ export function absolutePageUrl(url: string, site?: string): string;
176
180
  * @param {{title: string, abbreviation?: string, page: string|number}} away -
177
181
  * The home book's title, its abbreviation when it declares one, and the page
178
182
  * the entry starts on.
183
+ * @param {{bracketed?: boolean}} [opts] - `bracketed` when the text closes a
184
+ * bracket right after the link.
179
185
  * @returns {string} Typst markup.
180
186
  */
181
187
  export function crossBookReference(inner: string, away: {
182
188
  title: string;
183
189
  abbreviation?: string;
184
190
  page: string | number;
191
+ }, { bracketed }?: {
192
+ bracketed?: boolean;
185
193
  }): string;
186
194
  /**
187
195
  * The Typst definitions the book's page furniture is drawn with.
@@ -72,7 +72,7 @@ export function runTreeFilters(nodes: object[], db: {
72
72
  * without a renderer or a PDF. Only how the result *looks* needs eyes.
73
73
  *
74
74
  * **Notes are sorted, prose is not.** A section's entries come out in
75
- * `nameAscii` order — the ASCII fold `buildIndexRecord` already derives, so a
75
+ * name-sort order (`name.sort`, else `name.full`, folded to ASCII), so a
76
76
  * circumflex sorts with its letter instead of after `Z` as a raw codepoint
77
77
  * comparison would put it. Prose keeps the position it was written in, because
78
78
  * its place in the sequence is the author's statement.
@@ -59,6 +59,11 @@ export type RelatedEntry = {
59
59
  * The note's `type`, which the theme groups by.
60
60
  */
61
61
  type: string;
62
+ /**
63
+ * The note's `name.sort`. It orders the list in place
64
+ * of `title` and is not published.
65
+ */
66
+ sort?: string | undefined;
62
67
  };
63
68
  /**
64
69
  * The `related` block one page publishes.