@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.
- package/CHANGELOG.md +18 -0
- package/docs/api.md +1 -0
- package/docs/authoring/frontmatter.md +1 -1
- package/docs/authoring/links-and-markup.md +4 -4
- package/docs/configuration.md +3 -1
- package/docs/guides/book.md +12 -12
- package/docs/reference/format-details.md +28 -16
- package/docs/reference/note-types.md +1 -1
- package/engine/content-index.mjs +2 -39
- package/engine/event-fields.mjs +24 -1
- package/engine/event-rows.mjs +37 -2
- package/engine/event-views.mjs +58 -14
- package/engine/frontmatter-lint.mjs +11 -0
- package/engine/generated-sections.mjs +8 -1
- package/engine/holdings.mjs +8 -2
- package/engine/infobox-render.mjs +16 -5
- package/engine/infobox.mjs +23 -2
- package/engine/literature-works.mjs +8 -2
- package/engine/metadata-index.mjs +3 -0
- package/engine/name-sort.mjs +91 -0
- package/engine/note-frontmatter.mjs +1 -1
- package/engine/note-vocabulary.mjs +1 -1
- package/engine/page-lists.mjs +3 -2
- package/engine/pdf-index.mjs +11 -4
- package/engine/pdf-pages.mjs +2 -1
- package/engine/pdf-render.mjs +43 -13
- package/engine/pdf-toc.mjs +4 -3
- package/engine/related-pages.mjs +12 -2
- package/engine/site-build.mjs +3 -0
- package/engine/sql-tables.mjs +20 -5
- package/package.json +1 -1
- package/types/engine/content-index.d.mts +2 -31
- package/types/engine/event-fields.d.mts +11 -0
- package/types/engine/event-rows.d.mts +4 -1
- package/types/engine/event-views.d.mts +7 -3
- package/types/engine/generated-sections.d.mts +6 -0
- package/types/engine/holdings.d.mts +7 -1
- package/types/engine/infobox.d.mts +11 -0
- package/types/engine/literature-works.d.mts +5 -0
- package/types/engine/name-sort.d.mts +49 -0
- package/types/engine/pdf-render.d.mts +8 -0
- package/types/engine/pdf-toc.d.mts +1 -1
- package/types/engine/related-pages.d.mts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 23.12.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
In PDF books, the page number and section name at the foot of each page are set at the size of the body text.
|
|
8
|
+
|
|
9
|
+
In PDF books, list items sit apart from one another and carry a bullet large enough to see at book size.
|
|
10
|
+
|
|
11
|
+
In PDF books, a reference to another book that already sits in brackets joins them, reading (The Crossing, Thal. p524) rather than nesting a second pair.
|
|
12
|
+
|
|
13
|
+
Chronology sections, and the other event sections, list each event's date first, written in the setting's calendar. A chronology shows its Felt here column only when some event is felt in the place, and prints across the full page in PDF books. A captioned table can ask to span the page in a PDF book with `.full-width`. A wide table floats to where it fits, and the text runs on past its heading rather than leaving an empty page.
|
|
14
|
+
|
|
15
|
+
In PDF books, a profile box too long for the rest of a page continues on the next one between its rows, instead of running off the page over the footer.
|
|
16
|
+
|
|
17
|
+
A note's name may give a `sort` form, the way an index would write it: `sort: Black Flame, The` for _The Black Flame_, or `sort: Hilvan, Doran`. It files the note in the book index, page lists and other name-ordered lists, and it is the note's entry text in the book index; every other list still shows the full name. Authored `sql` fences can order by the new `nameSort` column of `entries`, which is the `sort` form or, without one, the full name.
|
|
18
|
+
|
|
19
|
+
Profile figures read as numbers do: 12,500 under a million, and 11 million or 1.2 million above it, on the website, in Foundry and in PDF books.
|
|
20
|
+
|
|
3
21
|
## 23.12.4
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/docs/api.md
CHANGED
|
@@ -384,6 +384,7 @@ The declared infobox: what a note's summary panel holds, decided once and render
|
|
|
384
384
|
| `assertInfoboxSet` | `assertInfoboxSet(boxes, fm, options)` | `readonly object[]`, throws | refusing a page that carries anything but the boxes its type maps to |
|
|
385
385
|
| `buildInfoboxes` | `buildInfoboxes(fm, options)` | `object[]` | building every box a note carries, against a given set of maps and declarations |
|
|
386
386
|
| `defineInfobox` | `defineInfobox(declaration)` | `object` | declaring one system's half of the infobox |
|
|
387
|
+
| `formatQuantity` | `formatQuantity(value)` | `string` | writing a number as a profile shows it: `12,500` under a million, `11 million` above |
|
|
387
388
|
| `hasRenderableValue` | `hasRenderableValue(kind, value)` | `boolean` | deciding whether a built value is worth a row |
|
|
388
389
|
| `hasValue` | `hasValue(value)` | `boolean` | deciding whether an authored value is worth a row |
|
|
389
390
|
| `humanizeFieldName` | `humanizeFieldName(name)` | `string` | turning a declared key into the label a reader sees |
|
|
@@ -21,7 +21,7 @@ data: {}
|
|
|
21
21
|
---
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
`name` is a map with a required, nonempty `full` display name. Every note may also use `aliases` for an ordered list of other names
|
|
24
|
+
`name` is a map with a required, nonempty `full` display name. Every note may also use `aliases` for an ordered list of other names, and `sort` for the form the name takes in an index, such as `Black Flame, The` or `Hilvan, Doran`. The note infobox lists aliases directly beneath the full name. `sort` files the note wherever notes are ordered by name — the book index, page lists, generated lists of related notes, and the `nameSort` column of the `entries` relation — and is the note's entry text in the book index. Every other list still displays the `full` name. A note with no `sort` files and indexes by its `full` name. For `name: {full: The Black Flame, sort: "Black Flame, The"}`, the book index prints _Black Flame, The_ under **B**, and a page list prints _The Black Flame_ at the same place.
|
|
25
25
|
|
|
26
26
|
`description` is the short page summary. `tags` hold draft state and descriptive labels. The note type's classification belongs in `subType` where it is declared. `data` holds facts about the subject shared by systems. A YAML comment is the place for an author-only note: commented values remain valid YAML but do not enter the content index or generated documents.
|
|
27
27
|
|
|
@@ -197,7 +197,7 @@ The caption attaches to exactly the next supported block:
|
|
|
197
197
|
|
|
198
198
|
The optional `type` attribute overrides inference and accepts `figure`, `table`, `poetry`, `code`, `prose`, and `example`. `type=example` uses **Example 1**, **Example 2**, and so on. It does not allow an unsupported target. A heading, list, thematic break, another caption or end of document after a caption is an error. Caption markers without separating spaces or caption text are errors too.
|
|
199
199
|
|
|
200
|
-
Identifiers are optional and must be unique within the note. Classes are unrestricted; `.border` uses the shared border style, while other classes can be styled by the medium. Named attributes use `key=value`, with quoted values for text containing spaces. Use `#id` and `.class` rather than `id=` and `class=`; event-handler attributes are refused.
|
|
200
|
+
Identifiers are optional and must be unique within the note. Classes are unrestricted; `.border` uses the shared border style, while other classes can be styled by the medium. On a captioned table or SQL fence, `.full-width` sets the table across the page in a PDF book instead of in one column (`: Regional trade routes {.full-width}`); the other surfaces ignore it. Named attributes use `key=value`, with quoted values for text containing spaces. Use `#id` and `.class` rather than `id=` and `class=`; event-handler attributes are refused.
|
|
201
201
|
|
|
202
202
|
Each numbered type has its own counter. Websites and Foundry number within a note; books number across their reading order. Unnumbered captions neither display nor consume a number. A map image can retain its asset-derived **Map** classification when no type is specified.
|
|
203
203
|
|
|
@@ -543,7 +543,7 @@ A column's heading is its SQL alias, so `AS "Name"` is what a reader sees. A cel
|
|
|
543
543
|
| `market` | The market scale as three columns — `value`, `name`, `trade` — so a table can print `village` beside the number a note wrote. |
|
|
544
544
|
| `<package>.notes` | The same relations — `notes`, `entries`, `events`, `names`, `words`, `affixes`, `fields`, `glossary`, `language_lineage`, `dictionary`, `terms`, `register`, `tongues`, `retired`, `scope`, `letters`, `edges`, `clusters`, `stress`, `finals`, `syllables`, `joining`, `joins`, `marks`, `skeleton`, `frames`, `read_past`, `mutations`, `cluster_groups`, `sound_changes`, `elements`, `roots`, `cognates`, `calling_names`, `older`, `negation`, `name_shapes`, `name_edges`, `name_draws`, `held_from` and `near_names` — for each package this one declares a dependency on, in a schema named for it. |
|
|
545
545
|
|
|
546
|
-
Nested frontmatter is addressed exactly as a note writes it: `name.full`, `sohl.weight`, `data.born`. Every Address is a string — the full canonical Address, as in `list_contains(data.parents, 'thalorna-note-place-xerathia')` — whichever package published it. Every note carries a computed `state` column of `full`, `draft` or `stub`. Notes tagged `gm` are absent from the relations on public surfaces.
|
|
546
|
+
Nested frontmatter is addressed exactly as a note writes it: `name.full`, `sohl.weight`, `data.born`. Every Address is a string — the full canonical Address, as in `list_contains(data.parents, 'thalorna-note-place-xerathia')` — whichever package published it. Every note carries a computed `state` column of `full`, `draft` or `stub`, and a `nameSort` column holding its `name.sort` or, without one, its `name.full`, to `ORDER BY`. Notes tagged `gm` are absent from the relations on public surfaces.
|
|
547
547
|
|
|
548
548
|
### Sections the build adds
|
|
549
549
|
|
|
@@ -567,7 +567,7 @@ The vale holds three villages, of which only [[place-ford|Ford]] is walled.
|
|
|
567
567
|
|
|
568
568
|
### Sections the build adds about events
|
|
569
569
|
|
|
570
|
-
A note's events are listed for you on the notes they concern: a place gets a **Chronology** of what happened there and below it, with every `depth: world` event as context, a being, affiliation or people the **Events** it took part in, an affiliation, place or people the **Accounts** it gives, and a note holding events **What followed** them. Each is a section appended after everything you wrote, with a fixed anchor — `chronology`, `events`, `accounts`, `followed` — and it appears only where it has something to list.
|
|
570
|
+
A note's events are listed for you on the notes they concern: a place gets a **Chronology** of what happened there and below it, with every `depth: world` event as context, a being, affiliation or people the **Events** it took part in, an affiliation, place or people the **Accounts** it gives, and a note holding events **What followed** them. Each is a section appended after everything you wrote, with a fixed anchor — `chronology`, `events`, `accounts`, `followed` — and it appears only where it has something to list. Each lists the date first, written in the setting's calendar, followed by what happened; a Chronology adds a **Felt here** column only where some event is felt in the place, and a PDF book prints it across the full page width.
|
|
571
571
|
|
|
572
572
|
To replace one, write the section yourself with the same anchor. The build then adds nothing for it and leaves yours exactly as written:
|
|
573
573
|
|
|
@@ -577,7 +577,7 @@ To replace one, write the section yourself with the same anchor. The build then
|
|
|
577
577
|
The fells keep no reckoning before the raising.
|
|
578
578
|
|
|
579
579
|
```sql
|
|
580
|
-
SELECT summary AS "What happened",
|
|
580
|
+
SELECT summary AS "What happened", whenText AS "When"
|
|
581
581
|
FROM events
|
|
582
582
|
WHERE list_contains("where".locus, 'thalorna-note-place-ironfells')
|
|
583
583
|
ORDER BY whenSort
|
package/docs/configuration.md
CHANGED
|
@@ -1259,7 +1259,9 @@ Each entry of `pdf.books` is one PDF:
|
|
|
1259
1259
|
abbreviation and the page, and the other book's index, when
|
|
1260
1260
|
`pdf.indexSeeAlso` is on, lists the note as `Name: Aelw. p307`. A book
|
|
1261
1261
|
with no abbreviation is named by its full title in italics, `Name (_Book
|
|
1262
|
-
title_, p. 307)`.
|
|
1262
|
+
title_, p. 307)`. A link the text closes a bracket on takes the reference
|
|
1263
|
+
inside that bracket after a comma, so `(see The Crossing)` prints
|
|
1264
|
+
`(see The Crossing, Thal. p524)` rather than nesting brackets. Each abbreviation is used once, since a reader could not
|
|
1263
1265
|
tell which book to open from one shared by two.
|
|
1264
1266
|
- `contents` is the tree written inline — the same `sectionName` /
|
|
1265
1267
|
`contents` / `filter` / `file` shape a tree file holds under its own
|
package/docs/guides/book.md
CHANGED
|
@@ -174,11 +174,11 @@ the Lyrebird Inn (The Tenebrae Volume, p. 214)
|
|
|
174
174
|
|
|
175
175
|
Which form a wikilink takes depends on where its target is printed:
|
|
176
176
|
|
|
177
|
-
| The target is | The link prints
|
|
178
|
-
| ------------------------------------------------------------- |
|
|
179
|
-
| An entry of the same book, including a note the book reprints | An internal destination
|
|
180
|
-
| An entry of another book, and not of this one | `Name (_Book title_, p. N)`, or `Name (Aelw. pN)` with an abbreviation, with no link |
|
|
181
|
-
| A note in no book | The web address of the note's page
|
|
177
|
+
| The target is | The link prints |
|
|
178
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
179
|
+
| An entry of the same book, including a note the book reprints | An internal destination |
|
|
180
|
+
| An entry of another book, and not of this one | `Name (_Book title_, p. N)`, or `Name (Aelw. pN)` with an abbreviation, with no link; inside a bracket the text closes, `(Name, Aelw. pN)` |
|
|
181
|
+
| A note in no book | The web address of the note's page |
|
|
182
182
|
|
|
183
183
|
The page is the page the PDF itself numbers, which is the number printed in the
|
|
184
184
|
footer and used by the contents and the index, so the printed `p. N` is the
|
|
@@ -406,12 +406,12 @@ A book that prints at least one note ends with an **Index** chapter, grouped
|
|
|
406
406
|
under letter headings. Nothing configures it: the build derives it from the
|
|
407
407
|
notes the book prints.
|
|
408
408
|
|
|
409
|
-
| Term | Source
|
|
410
|
-
| ------------------------ |
|
|
411
|
-
| A note's name | `name.full`
|
|
412
|
-
| A name without its head | `name.full`
|
|
413
|
-
| An alias | `name.aliases`
|
|
414
|
-
| An event's in-world name | `data.events[].names`
|
|
409
|
+
| Term | Source | Shown as |
|
|
410
|
+
| ------------------------ | ----------------------------- | ---------------------------------------------------------------------------------- |
|
|
411
|
+
| A note's name | `name.sort`, else `name.full` | the page of the note's entry in **bold**, then the pages of every authored mention |
|
|
412
|
+
| A name without its head | `name.full` | the same pages as the note's entry, filed as `Targud, Kingdom of` |
|
|
413
|
+
| An alias | `name.aliases` | _see_ the note's name |
|
|
414
|
+
| An event's in-world name | `data.events[].names` | _see_ the name of the note that holds the event |
|
|
415
415
|
|
|
416
416
|
A name that opens with a generic word a reader would not look under, such as
|
|
417
417
|
`Kingdom of Targud` or `Ritual: Blessing`, is also filed under the rest of the
|
|
@@ -460,7 +460,7 @@ Two kinds of link add nothing:
|
|
|
460
460
|
|
|
461
461
|
Terms sort on the ASCII fold the content index derives for names (`nameAscii`),
|
|
462
462
|
ignoring case, so an accented name files under its base letter and the order
|
|
463
|
-
matches the table of contents. A term that does not begin with a letter files
|
|
463
|
+
matches the table of contents. A note's own entry reads as its `name.sort` when it declares one, in index form such as `Black Flame, The` or `Hilvan, Doran`, and as its `name.full` otherwise; it files, and takes its letter heading, from that form. A note that declares a `sort` is not also filed under `pdf.indexHeads`. A term that does not begin with a letter files
|
|
464
464
|
first, under `#`. An alias or event name that folds to the same text as the
|
|
465
465
|
note's own name is not listed again.
|
|
466
466
|
|
|
@@ -379,7 +379,13 @@ content, comment out its YAML lines with `#`. Comments remain valid YAML text
|
|
|
379
379
|
and do not become data in the JSONL index, generated pages, or Foundry documents.
|
|
380
380
|
|
|
381
381
|
`name` is a map with a required nonempty `full` string when present. It may
|
|
382
|
-
also contain `aliases`, an ordered list of nonempty strings
|
|
382
|
+
also contain `aliases`, an ordered list of nonempty strings, and `sort`, a nonempty
|
|
383
|
+
string giving the form the name takes in an index, such as `Black Flame, The` or
|
|
384
|
+
`Hilvan, Doran`. `sort` files the note wherever notes are ordered by name (page
|
|
385
|
+
lists, generated lists of related notes, the book index, the `nameSort` column)
|
|
386
|
+
and is the note's entry text in the book index; every other list still displays
|
|
387
|
+
`full`. Without `sort`, the note files and indexes by `full`. Ordering compares
|
|
388
|
+
the ASCII fold of the key (`Árdavon` files beside `Ardavon`). A being with
|
|
383
389
|
`subType: character` may additionally contain nonempty `given` and `clan`
|
|
384
390
|
strings. Other keys under `name` are errors. For a character or NPC, the first
|
|
385
391
|
alias is its nickname. A being's homes belong in `data.homes`.
|
|
@@ -2916,7 +2922,9 @@ reads `NULL` rather than failing. `data.culture` is the one field that reads as
|
|
|
2916
2922
|
a derived value: the note's own culture, or the one it inherits. See
|
|
2917
2923
|
[Culture in the index](#culture-in-the-index).
|
|
2918
2924
|
|
|
2919
|
-
**`notes` leaves out stubs; `entries` does not.** Both carry a derived `
|
|
2925
|
+
**`notes` leaves out stubs; `entries` does not.** Both carry a derived `nameSort`
|
|
2926
|
+
column — the note's `name.sort` when it declares one, otherwise its `name.full`,
|
|
2927
|
+
unfolded — for `ORDER BY`, and a derived `state`
|
|
2920
2928
|
column — `stub`, `draft` or `full` — and `notes` is exactly
|
|
2921
2929
|
`SELECT * FROM entries WHERE state <> 'stub'`. So every table already written
|
|
2922
2930
|
selects what it always selected, and gains `state` for free; a table that should
|
|
@@ -2957,20 +2965,21 @@ query rather than a list kept by hand:
|
|
|
2957
2965
|
|
|
2958
2966
|
````markdown
|
|
2959
2967
|
```sql
|
|
2960
|
-
SELECT note AS _ref,
|
|
2968
|
+
SELECT note AS _ref, whenText AS "When", summary AS "What happened"
|
|
2961
2969
|
FROM events
|
|
2962
2970
|
WHERE kind IN ('war', 'siege', 'battle')
|
|
2963
2971
|
ORDER BY whenSort
|
|
2964
2972
|
```
|
|
2965
2973
|
````
|
|
2966
2974
|
|
|
2967
|
-
| Column | What it holds
|
|
2968
|
-
| ------------------------ |
|
|
2969
|
-
| `note` | The canonical Address of the note the event is written on.
|
|
2970
|
-
| `address` | The event's own Address: `note#id` where the event has an `id`, the note's Address alone where it has none.
|
|
2971
|
-
| `id`, `kind`, `depth`, … | Every key of an event, each its own column under its own name — `id`, `kind`, `depth`, `when`, `until`, `recurs`, `summary`, `standing`, `names`, `where`, `who`, `follows`, `accounts`, `unresolved`, `sources`, `stated`.
|
|
2972
|
-
| `whenSort`, `untilSort` | The date's position on the canonical axis: the canonical year, with a day as its fraction of the year where the calendar states the year's length. `ORDER BY whenSort` is date order.
|
|
2973
|
-
| `
|
|
2975
|
+
| Column | What it holds |
|
|
2976
|
+
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2977
|
+
| `note` | The canonical Address of the note the event is written on. |
|
|
2978
|
+
| `address` | The event's own Address: `note#id` where the event has an `id`, the note's Address alone where it has none. |
|
|
2979
|
+
| `id`, `kind`, `depth`, … | Every key of an event, each its own column under its own name — `id`, `kind`, `depth`, `when`, `until`, `recurs`, `summary`, `standing`, `names`, `where`, `who`, `follows`, `accounts`, `unresolved`, `sources`, `stated`. |
|
|
2980
|
+
| `whenSort`, `untilSort` | The date's position on the canonical axis: the canonical year, with a day as its fraction of the year where the calendar states the year's length. `ORDER BY whenSort` is date order. |
|
|
2981
|
+
| `whenText`, `untilText` | The date as a reader sees it, printed as `dateformat` prints it in the calendar the owning note names in `data.calendar`, else the one the world note names, else in the era the date itself names; `~` marks an approximate date. A date that does not resolve keeps the text its note wrote. |
|
|
2982
|
+
| `whenYear`, `untilYear` | The canonical year alone, for a filter such as `whenYear BETWEEN 200 AND 299`. |
|
|
2974
2983
|
|
|
2975
2984
|
`when` and `until` read as the text the note wrote, `~` included. The sort
|
|
2976
2985
|
keys and years are positions on the canonical axis, which counts a year `0`
|
|
@@ -3498,7 +3507,7 @@ The caption attaches to exactly the next supported block:
|
|
|
3498
3507
|
|
|
3499
3508
|
The optional `type` attribute overrides inference and accepts `figure`, `table`, `poetry`, `code`, `prose`, and `example`. `type=example` uses **Example 1**, **Example 2**, and so on. It does not allow an unsupported target. A heading, list, thematic break, another caption or end of document after a caption is an error. Caption markers without separating spaces or caption text are errors too.
|
|
3500
3509
|
|
|
3501
|
-
Identifiers are optional and must be unique within the note. Classes are unrestricted; `.border` uses the shared border style, while other classes can be styled by the medium. Named attributes use `key=value`, with quoted values for text containing spaces. Use `#id` and `.class` rather than `id=` and `class=`; event-handler attributes are refused.
|
|
3510
|
+
Identifiers are optional and must be unique within the note. Classes are unrestricted; `.border` uses the shared border style, while other classes can be styled by the medium. On a captioned table or SQL fence, `.full-width` sets the table across the page in a PDF book instead of in one column (`: Regional trade routes {.full-width}`); the other surfaces ignore it. Named attributes use `key=value`, with quoted values for text containing spaces. Use `#id` and `.class` rather than `id=` and `class=`; event-handler attributes are refused.
|
|
3502
3511
|
|
|
3503
3512
|
Each numbered type has its own counter. Websites and Foundry number within a note; books number across their reading order. Unnumbered captions neither display nor consume a number. A map image can retain its asset-derived **Map** classification when no type is specified.
|
|
3504
3513
|
|
|
@@ -3763,11 +3772,14 @@ The build appends sections to the end of a note's body that list the events conc
|
|
|
3763
3772
|
|
|
3764
3773
|
| View | Anchor | On | Rows | Columns |
|
|
3765
3774
|
| ------------- | ------------ | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
|
3766
|
-
| Chronology | `chronology` | a `place` | events whose `where.locus` names the place or a place below it in `parents`; events whose `where.reach` names one, with that entry's `how`; every `depth: world` event as context | What happened,
|
|
3767
|
-
| Events | `events` | a `being`, an `affiliation`, a `lore` note of `subType: culture` | events whose `who[].ref` names the note, with the role it played | What happened,
|
|
3768
|
-
| Accounts | `accounts` | an `affiliation`, a `place`, a `lore` note of `subType: culture` | each `accounts[]` entry the note gives, with the event it concerns |
|
|
3769
|
-
| What followed | `followed` | a note holding events | events whose `follows[].event` names one of this note's events, with how | What followed,
|
|
3770
|
-
|
|
3775
|
+
| Chronology | `chronology` | a `place` | events whose `where.locus` names the place or a place below it in `parents`; events whose `where.reach` names one, with that entry's `how`; every `depth: world` event as context | When, What happened, Felt here |
|
|
3776
|
+
| Events | `events` | a `being`, an `affiliation`, a `lore` note of `subType: culture` | events whose `who[].ref` names the note, with the role it played | When, What happened, Role |
|
|
3777
|
+
| Accounts | `accounts` | an `affiliation`, a `place`, a `lore` note of `subType: culture` | each `accounts[]` entry the note gives, with the event it concerns | When, Event, What they say, Agrees |
|
|
3778
|
+
| What followed | `followed` | a note holding events | events whose `follows[].event` names one of this note's events, with how | When, What followed, How |
|
|
3779
|
+
|
|
3780
|
+
- **Every view's first column is When**, the event's date written in the setting's calendar — the `events` relation's `whenText` column. An event whose date does not resolve shows the text its note wrote.
|
|
3781
|
+
- **A chronology's Felt here column appears only where some row is felt in the place.** A place whose rows are all events that happened there, or world events, has the When and What happened columns alone.
|
|
3782
|
+
- **A chronology is always set across the full page width in a PDF book**, whatever its columns.
|
|
3771
3783
|
- **The views follow everything the author wrote**, in the order of the [generated sections](#generated-sections).
|
|
3772
3784
|
- **A view with no rows is not generated.** A chronology is generated only where the place has rows of its own; the world events beside them are context and make no chronology by themselves.
|
|
3773
3785
|
- **An author's own section replaces a view.** A note whose body declares a prose anchor with the view's slug — `# Chronology {#chronology}`, or a block or span with that `id` — gets no generated section for it, and its own section is left exactly as written. This is the one way to turn a view off, and the way to choose a different table: write the section and its own `sql` fence.
|
|
@@ -18,7 +18,7 @@ These are the only keys a note writes at its top level, in the order the formatt
|
|
|
18
18
|
| Key | Meaning |
|
|
19
19
|
| ------------- | ----------------------------------------------------------------------------------------- |
|
|
20
20
|
| `shortcode` | The note's own address segment, unique within its type. |
|
|
21
|
-
| `name` | The display names — a required `full`, and any `aliases`.
|
|
21
|
+
| `name` | The display names — a required `full`, and any `aliases` and `sort`. |
|
|
22
22
|
| `type` | What the note is about, which decides its vocabulary and its document. |
|
|
23
23
|
| `subType` | The type's own genre, where it declares one. |
|
|
24
24
|
| `description` | The short page summary. |
|
package/engine/content-index.mjs
CHANGED
|
@@ -108,8 +108,9 @@ import { analyzeNameSpans } from "./names.mjs";
|
|
|
108
108
|
import fs from "node:fs";
|
|
109
109
|
import path from "node:path";
|
|
110
110
|
|
|
111
|
-
import
|
|
111
|
+
import { asciiName } from "./name-sort.mjs";
|
|
112
112
|
|
|
113
|
+
export { asciiName };
|
|
113
114
|
import { loadForeignIndexes, metadataFileName } from "./metadata-index.mjs";
|
|
114
115
|
import { collectAssetRecords } from "./asset-index.mjs";
|
|
115
116
|
import { checkForeignAssetBindings } from "./asset-bindings.mjs";
|
|
@@ -245,44 +246,6 @@ export function noteAddress(frontmatter, contentPackage) {
|
|
|
245
246
|
};
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
/**
|
|
249
|
-
* A note's display name reduced to printable 7-bit ASCII.
|
|
250
|
-
*
|
|
251
|
-
* Content names carry the setting's orthography — `Kûrbúl Helm`, `Hârn`,
|
|
252
|
-
* `Kèthîra` — and nobody types them. A reader searching the index, or an editor
|
|
253
|
-
* completing a wikilink, needs a form that matches what a keyboard produces, so
|
|
254
|
-
* the record states one rather than leaving every consumer to invent it (and to
|
|
255
|
-
* invent a *different* one, which is how two searches over the same data come
|
|
256
|
-
* to disagree).
|
|
257
|
-
*
|
|
258
|
-
* **Transliterated, not stripped.** `unidecode` — the same table
|
|
259
|
-
* {@link slugify} already runs, so an ASCII name and a slug can never disagree
|
|
260
|
-
* about a character — carries a letter across rather than deleting it:
|
|
261
|
-
* diacritics fold (`â`→`a`, `è`→`e`), ligatures expand (`æ`→`ae`, `Œ`→`OE`,
|
|
262
|
-
* `ß`→`ss`), the runic letters spell out (`þ`→`th`, `Þ`→`Th`, `ð`→`d`), and
|
|
263
|
-
* even a vulgar fraction becomes readable (`¾`→`3/4`). Deleting them instead
|
|
264
|
-
* would collapse `Kûrbúl` to `Krbl`, which is worse than the original.
|
|
265
|
-
*
|
|
266
|
-
* Anything still outside printable ASCII after that becomes a space, and runs
|
|
267
|
-
* of whitespace collapse — a space rather than nothing, so a character that
|
|
268
|
-
* transliterates away cannot silently weld two words together.
|
|
269
|
-
*
|
|
270
|
-
* The value is emitted even when it equals the name, so a consumer matching on
|
|
271
|
-
* it never has to branch on whether the name happened to be ASCII already.
|
|
272
|
-
*
|
|
273
|
-
* @param {unknown} name - The note's `name.full`.
|
|
274
|
-
* @returns {string|null} The ASCII form, or `null` when there is no name, or
|
|
275
|
-
* nothing printable survives.
|
|
276
|
-
*/
|
|
277
|
-
export function asciiName(name) {
|
|
278
|
-
if (typeof name !== "string") return null;
|
|
279
|
-
const folded = unidecode(name)
|
|
280
|
-
.replace(/[^\x20-\x7E]/g, " ")
|
|
281
|
-
.replace(/\s+/g, " ")
|
|
282
|
-
.trim();
|
|
283
|
-
return folded === "" ? null : folded;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
249
|
/**
|
|
287
250
|
* A note's `name.aliases` reduced to printable 7-bit ASCII, in order.
|
|
288
251
|
*
|
package/engine/event-fields.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { ownDocumentSystem, parseAddress, readCanonicalKey, renderAddress } from "./address.mjs";
|
|
26
|
-
import { formatNoteDate, parseNoteDate } from "./note-dates.mjs";
|
|
26
|
+
import { formatDateInCalendar, formatNoteDate, parseNoteDate } from "./note-dates.mjs";
|
|
27
27
|
import { NOTE_VOCABULARY } from "./note-vocabulary.mjs";
|
|
28
28
|
|
|
29
29
|
/** The fields of an event an inline reference may print, in the order a message names them. */
|
|
@@ -161,6 +161,29 @@ export function printEventDate(written, date, dates) {
|
|
|
161
161
|
return printable?.prose ?? printable?.text ?? date.prose ?? date.text ?? String(written);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* A date as `dateformat` prints it in a calendar, where one is named and
|
|
166
|
+
* prints it; otherwise as {@link printEventDate} prints it.
|
|
167
|
+
*
|
|
168
|
+
* @param {unknown} written - The date as the event writes it.
|
|
169
|
+
* @param {object|null} date - The resolved date.
|
|
170
|
+
* @param {object} [dates] - The reckoning context.
|
|
171
|
+
* @param {string} [calendar] - The calendar's Address or shortcode.
|
|
172
|
+
* @returns {string} The printed date; the authored text when nothing resolves.
|
|
173
|
+
*/
|
|
174
|
+
export function printEventDateIn(written, date, dates, calendar) {
|
|
175
|
+
if (date?.known && calendar) {
|
|
176
|
+
try {
|
|
177
|
+
const printable = formatDateInCalendar(date, calendar, dates);
|
|
178
|
+
const text = printable?.prose ?? printable?.text;
|
|
179
|
+
if (text) return text;
|
|
180
|
+
} catch {
|
|
181
|
+
// A calendar that does not resolve leaves the date in its own era.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return printEventDate(written, date, dates);
|
|
185
|
+
}
|
|
186
|
+
|
|
164
187
|
/**
|
|
165
188
|
* One field of one event, as the text a reference prints, or why it cannot.
|
|
166
189
|
*
|
package/engine/event-rows.mjs
CHANGED
|
@@ -25,10 +25,16 @@
|
|
|
25
25
|
* `resolvedDates.events`, position for position with `data.events`, and the
|
|
26
26
|
* sort keys are read from it rather than parsed again here.
|
|
27
27
|
*
|
|
28
|
+
* **`whenText` and `untilText` are the dates as a reader sees them**: printed
|
|
29
|
+
* as the `dateformat` helper prints a date, in the calendar the owning note
|
|
30
|
+
* names in `data.calendar`, else the one the world note names, else in the era
|
|
31
|
+
* the date itself names. A date that does not resolve keeps its authored text.
|
|
32
|
+
*
|
|
28
33
|
* @module
|
|
29
34
|
*/
|
|
30
35
|
|
|
31
36
|
import { ownDocumentSystem, renderAddress } from "./address.mjs";
|
|
37
|
+
import { printEventDateIn } from "./event-fields.mjs";
|
|
32
38
|
import { EVENT_ENTRY_KEYS } from "./note-event-terms.mjs";
|
|
33
39
|
|
|
34
40
|
/** The columns a row carries besides the event's own keys, before them. */
|
|
@@ -37,11 +43,15 @@ const IDENTITY_COLUMNS = Object.freeze(["note", "address"]);
|
|
|
37
43
|
/** The sort keys a row carries, after the event's own keys. */
|
|
38
44
|
export const EVENT_SORT_COLUMNS = Object.freeze(["whenSort", "whenYear", "untilSort", "untilYear"]);
|
|
39
45
|
|
|
46
|
+
/** The printed dates a row carries, after the sort keys. */
|
|
47
|
+
export const EVENT_TEXT_COLUMNS = Object.freeze(["whenText", "untilText"]);
|
|
48
|
+
|
|
40
49
|
/** Every column of the `events` relation, in order. */
|
|
41
50
|
export const EVENT_COLUMNS = Object.freeze([
|
|
42
51
|
...IDENTITY_COLUMNS,
|
|
43
52
|
...EVENT_ENTRY_KEYS,
|
|
44
53
|
...EVENT_SORT_COLUMNS,
|
|
54
|
+
...EVENT_TEXT_COLUMNS,
|
|
45
55
|
]);
|
|
46
56
|
|
|
47
57
|
/** The keys whose authored value is a date, printed as the text it was written as. */
|
|
@@ -84,22 +94,41 @@ function axisKeys(date) {
|
|
|
84
94
|
};
|
|
85
95
|
}
|
|
86
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The calendar the world note names, which displays every date whose own note
|
|
99
|
+
* names none.
|
|
100
|
+
*
|
|
101
|
+
* @param {Record<string, any>[]} records - Index records.
|
|
102
|
+
* @returns {string|undefined} The calendar's Address or shortcode.
|
|
103
|
+
*/
|
|
104
|
+
function worldCalendar(records) {
|
|
105
|
+
for (const record of records) {
|
|
106
|
+
if (record?.type === "place" && record.data?.year?.days && record.data.calendar)
|
|
107
|
+
return String(record.data.calendar);
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
87
112
|
/**
|
|
88
113
|
* The `events` rows of a set of index records.
|
|
89
114
|
*
|
|
90
115
|
* @param {Iterable<Record<string, any>>} records - Index records, every
|
|
91
116
|
* Address a string, already narrowed to the audience being published to.
|
|
117
|
+
* @param {object} [dates] - The reckoning context the dates are printed in.
|
|
92
118
|
* @returns {object[]} One row per event entry, in record then entry order.
|
|
93
119
|
*/
|
|
94
|
-
export function eventRows(records) {
|
|
120
|
+
export function eventRows(records, dates) {
|
|
95
121
|
const rows = [];
|
|
96
|
-
|
|
122
|
+
const all = [...records];
|
|
123
|
+
const world = worldCalendar(all);
|
|
124
|
+
for (const record of all) {
|
|
97
125
|
const events = record?.data?.events;
|
|
98
126
|
if (!Array.isArray(events) || !events.length) continue;
|
|
99
127
|
const note = noteAddressOf(record);
|
|
100
128
|
if (!note) continue;
|
|
101
129
|
const resolved =
|
|
102
130
|
Array.isArray(record.resolvedDates?.events) ? record.resolvedDates.events : [];
|
|
131
|
+
const calendar = record.data?.calendar ? String(record.data.calendar) : world;
|
|
103
132
|
events.forEach((entry, position) => {
|
|
104
133
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return;
|
|
105
134
|
const row = {
|
|
@@ -119,6 +148,12 @@ export function eventRows(records) {
|
|
|
119
148
|
row.whenYear = when.year;
|
|
120
149
|
row.untilSort = until.sort;
|
|
121
150
|
row.untilYear = until.year;
|
|
151
|
+
for (const key of DATE_KEYS) {
|
|
152
|
+
row[`${key}Text`] =
|
|
153
|
+
row[key] === null ?
|
|
154
|
+
null
|
|
155
|
+
: printEventDateIn(row[key], resolved[position]?.[key], dates, calendar);
|
|
156
|
+
}
|
|
122
157
|
rows.push(row);
|
|
123
158
|
});
|
|
124
159
|
}
|
package/engine/event-views.mjs
CHANGED
|
@@ -65,8 +65,9 @@ const BY_DATE = "ORDER BY e.whenSort NULLS LAST, e.address";
|
|
|
65
65
|
*
|
|
66
66
|
* @type {ReadonlyArray<Readonly<{slug: string, heading: string,
|
|
67
67
|
* applies: (record: object) => boolean,
|
|
68
|
-
* query: (self: string[], shape: CorpusShape) => {fence: string, own?: string}|null,
|
|
69
|
-
* owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string
|
|
68
|
+
* query: (self: string[], shape: CorpusShape, columns?: {felt?: boolean}) => {fence: string, own?: string}|null,
|
|
69
|
+
* owners?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>,
|
|
70
|
+
* felt?: (db: object, selves: string[][], shape: CorpusShape) => Promise<Set<string>>}>>}
|
|
70
71
|
*/
|
|
71
72
|
export const EVENT_VIEWS = Object.freeze([
|
|
72
73
|
Object.freeze({
|
|
@@ -75,6 +76,7 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
75
76
|
applies: (record) => record.type === "place",
|
|
76
77
|
query: chronologyQuery,
|
|
77
78
|
owners: chronologyOwners,
|
|
79
|
+
felt: chronologyFeltOwners,
|
|
78
80
|
}),
|
|
79
81
|
Object.freeze({
|
|
80
82
|
slug: "events",
|
|
@@ -91,8 +93,8 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
91
93
|
return {
|
|
92
94
|
fence: [
|
|
93
95
|
"SELECT e.note AS _ref,",
|
|
96
|
+
' e.whenText AS "When",',
|
|
94
97
|
' e.summary AS "What happened",',
|
|
95
|
-
' e."when" AS "When",',
|
|
96
98
|
` array_to_string(list_transform(${named}, w -> w.role), ', ') AS "Role"`,
|
|
97
99
|
"FROM events e",
|
|
98
100
|
`WHERE len(${named}) > 0`,
|
|
@@ -120,8 +122,8 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
120
122
|
return {
|
|
121
123
|
fence: [
|
|
122
124
|
"SELECT e.note AS _ref,",
|
|
125
|
+
' e.whenText AS "When",',
|
|
123
126
|
' e.summary AS "Event",',
|
|
124
|
-
' e."when" AS "When",',
|
|
125
127
|
' e.account.says AS "What they say",',
|
|
126
128
|
' e.account.agrees AS "Agrees"',
|
|
127
129
|
"FROM (SELECT *, unnest(accounts) AS account FROM events) e",
|
|
@@ -151,8 +153,8 @@ export const EVENT_VIEWS = Object.freeze([
|
|
|
151
153
|
return {
|
|
152
154
|
fence: [
|
|
153
155
|
"SELECT e.note AS _ref,",
|
|
156
|
+
' e.whenText AS "When",',
|
|
154
157
|
' e.summary AS "What followed",',
|
|
155
|
-
' e."when" AS "When",',
|
|
156
158
|
` array_to_string(list_transform(${edges}, f -> f.how), ', ') AS "How"`,
|
|
157
159
|
"FROM events e",
|
|
158
160
|
`WHERE len(${edges}) > 0`,
|
|
@@ -237,6 +239,36 @@ function chronologyOwnRows(shape) {
|
|
|
237
239
|
* @returns {Promise<Set<string>>} The roots with at least one own row.
|
|
238
240
|
*/
|
|
239
241
|
async function chronologyOwners(db, selves, shape) {
|
|
242
|
+
return chronologyRoots(db, selves, shape, chronologyOwnRows(shape));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Which places have a row felt there, asked of every place in one query. A
|
|
247
|
+
* chronology prints its `Felt here` column only for these.
|
|
248
|
+
*
|
|
249
|
+
* @param {object} db - From {@link module:engine/sql-tables.openNotesDatabase}.
|
|
250
|
+
* @param {string[][]} selves - Each place's Address, in each form.
|
|
251
|
+
* @param {CorpusShape} shape - What the corpus states.
|
|
252
|
+
* @returns {Promise<Set<string>>} The roots with at least one felt row.
|
|
253
|
+
*/
|
|
254
|
+
async function chronologyFeltOwners(db, selves, shape) {
|
|
255
|
+
if (!shape.reach) return new Set();
|
|
256
|
+
return chronologyRoots(db, selves, shape, [
|
|
257
|
+
`len(list_filter(${chronologyFelt()}, r -> coalesce(r.how, '') <> '')) > 0`,
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The roots among `selves` for which an event satisfies one of `conditions`,
|
|
263
|
+
* over the places at or below each root.
|
|
264
|
+
*
|
|
265
|
+
* @param {object} db - From {@link module:engine/sql-tables.openNotesDatabase}.
|
|
266
|
+
* @param {string[][]} selves - Each place's Address, in each form.
|
|
267
|
+
* @param {CorpusShape} shape - What the corpus states.
|
|
268
|
+
* @param {string[]} conditions - Alternatives over `e` and `here.places`.
|
|
269
|
+
* @returns {Promise<Set<string>>} The roots with at least one such event.
|
|
270
|
+
*/
|
|
271
|
+
async function chronologyRoots(db, selves, shape, conditions) {
|
|
240
272
|
const values = [...new Set(selves.map((self) => self[0]))]
|
|
241
273
|
.map((root) => `(${literal(root)})`)
|
|
242
274
|
.join(", ");
|
|
@@ -258,7 +290,7 @@ async function chronologyOwners(db, selves, shape) {
|
|
|
258
290
|
"here AS (SELECT root, list(place) AS places FROM below GROUP BY root)",
|
|
259
291
|
"SELECT DISTINCT here.root AS root",
|
|
260
292
|
"FROM events e, here",
|
|
261
|
-
`WHERE ${
|
|
293
|
+
`WHERE ${conditions.join(" OR ")}`,
|
|
262
294
|
].join("\n"),
|
|
263
295
|
);
|
|
264
296
|
return new Set(rows.map((row) => String(row.root)));
|
|
@@ -275,10 +307,12 @@ async function chronologyOwners(db, selves, shape) {
|
|
|
275
307
|
*
|
|
276
308
|
* @param {string[]} self - The place's Address, in each form.
|
|
277
309
|
* @param {CorpusShape} shape - What the corpus states.
|
|
310
|
+
* @param {{felt?: boolean}} [columns] - `felt: false` when no row of this
|
|
311
|
+
* place is felt there, which leaves the `Felt here` column out.
|
|
278
312
|
* @returns {{fence: string, own: string}|null} The query, and the one that
|
|
279
313
|
* counts the place's own rows.
|
|
280
314
|
*/
|
|
281
|
-
function chronologyQuery(self, shape) {
|
|
315
|
+
function chronologyQuery(self, shape, columns = {}) {
|
|
282
316
|
if (!shape.locus && !shape.reach) return null;
|
|
283
317
|
const below =
|
|
284
318
|
shape.parents ?
|
|
@@ -298,11 +332,13 @@ function chronologyQuery(self, shape) {
|
|
|
298
332
|
...below,
|
|
299
333
|
"here AS (SELECT list(place) AS places FROM below)",
|
|
300
334
|
"SELECT e.note AS _ref,",
|
|
301
|
-
' e.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
335
|
+
' e.whenText AS "When",',
|
|
336
|
+
shape.reach && columns.felt !== false ?
|
|
337
|
+
' e.summary AS "What happened",'
|
|
338
|
+
: ' e.summary AS "What happened"',
|
|
339
|
+
...(shape.reach && columns.felt !== false ?
|
|
340
|
+
[` array_to_string(list_transform(${felt}, r -> r.how), '; ') AS "Felt here"`]
|
|
341
|
+
: []),
|
|
306
342
|
"FROM events e, here",
|
|
307
343
|
];
|
|
308
344
|
const context = shape.depth ? ["e.depth = 'world'"] : [];
|
|
@@ -394,6 +430,7 @@ export async function eventViewSections(db, records, fileOf, only) {
|
|
|
394
430
|
// A view that can say which notes have rows in one query is asked once, over
|
|
395
431
|
// every note it will be given, and the per-note count is skipped.
|
|
396
432
|
const owned = new Map();
|
|
433
|
+
const felt = new Map();
|
|
397
434
|
for (const view of EVENT_VIEWS) {
|
|
398
435
|
if (!view.owners) continue;
|
|
399
436
|
const selves = [];
|
|
@@ -404,7 +441,10 @@ export async function eventViewSections(db, records, fileOf, only) {
|
|
|
404
441
|
const self = selfForms(record);
|
|
405
442
|
if (view.query(self, shape)) selves.push(self);
|
|
406
443
|
}
|
|
407
|
-
if (selves.length)
|
|
444
|
+
if (selves.length) {
|
|
445
|
+
owned.set(view.slug, await view.owners(db, selves, shape));
|
|
446
|
+
if (view.felt) felt.set(view.slug, await view.felt(db, selves, shape));
|
|
447
|
+
}
|
|
408
448
|
}
|
|
409
449
|
for (const record of all) {
|
|
410
450
|
if (!record?.address || record.documents || !record.shortcode) continue;
|
|
@@ -413,7 +453,11 @@ export async function eventViewSections(db, records, fileOf, only) {
|
|
|
413
453
|
const sections = new Map();
|
|
414
454
|
for (const view of EVENT_VIEWS) {
|
|
415
455
|
if (!view.applies(record) || authoredSection(record, view.slug)) continue;
|
|
416
|
-
const query = view.query(
|
|
456
|
+
const query = view.query(
|
|
457
|
+
self,
|
|
458
|
+
shape,
|
|
459
|
+
felt.has(view.slug) ? { felt: felt.get(view.slug).has(self[0]) } : undefined,
|
|
460
|
+
);
|
|
417
461
|
if (!query) continue;
|
|
418
462
|
const has =
|
|
419
463
|
owned.has(view.slug) ?
|
|
@@ -1285,6 +1285,17 @@ export function lintNote(
|
|
|
1285
1285
|
});
|
|
1286
1286
|
}
|
|
1287
1287
|
}
|
|
1288
|
+
if (
|
|
1289
|
+
Object.hasOwn(name, "sort") &&
|
|
1290
|
+
(typeof name.sort !== "string" || !name.sort.trim())
|
|
1291
|
+
) {
|
|
1292
|
+
findings.push({
|
|
1293
|
+
file: note.file,
|
|
1294
|
+
...nameAt(["sort"]),
|
|
1295
|
+
severity: "error",
|
|
1296
|
+
message: "`name.sort` must be a nonempty string",
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1288
1299
|
for (const key of CHARACTER_NAME_KEYS) {
|
|
1289
1300
|
if (!Object.hasOwn(name, key) || !allowed.has(key)) continue;
|
|
1290
1301
|
if (typeof name[key] === "string" && name[key].trim()) continue;
|