@heroiclands/package-build 23.5.1 → 23.6.1

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 (44) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/content-commands.mjs +23 -5
  3. package/docs/api.md +9 -0
  4. package/docs/authoring/links-and-markup.md +108 -12
  5. package/docs/content-format.md +16 -0
  6. package/docs/guides/reference-pages.md +89 -0
  7. package/docs/reference/format-details.md +549 -88
  8. package/docs/reference/note-types.md +192 -149
  9. package/engine/base-compiler.mjs +21 -0
  10. package/engine/content-format.yaml +9 -0
  11. package/engine/content-index.mjs +19 -5
  12. package/engine/culture-reach.mjs +330 -0
  13. package/engine/dictionary-layout.mjs +342 -0
  14. package/engine/dictionary-rows.mjs +605 -0
  15. package/engine/glossary-entries.mjs +182 -0
  16. package/engine/glossary-language.mjs +99 -0
  17. package/engine/index.mjs +1 -0
  18. package/engine/infobox.mjs +8 -0
  19. package/engine/language-words.mjs +374 -0
  20. package/engine/literature-notes.mjs +10 -8
  21. package/engine/note-addresses.mjs +25 -0
  22. package/engine/note-vocabulary.mjs +161 -1
  23. package/engine/pack-config.mjs +6 -1
  24. package/engine/path-charset.mjs +120 -0
  25. package/engine/sql-tables.mjs +352 -21
  26. package/engine/word-classes.mjs +167 -0
  27. package/engine/word-rows.mjs +343 -0
  28. package/engine/word-terms.mjs +318 -0
  29. package/package.json +1 -1
  30. package/types/engine/content-index.d.mts +15 -5
  31. package/types/engine/culture-reach.d.mts +66 -0
  32. package/types/engine/dictionary-layout.d.mts +74 -0
  33. package/types/engine/dictionary-rows.d.mts +39 -0
  34. package/types/engine/glossary-entries.d.mts +12 -0
  35. package/types/engine/glossary-language.d.mts +42 -0
  36. package/types/engine/index.d.mts +1 -0
  37. package/types/engine/language-words.d.mts +33 -0
  38. package/types/engine/literature-notes.d.mts +2 -2
  39. package/types/engine/note-addresses.d.mts +17 -0
  40. package/types/engine/path-charset.d.mts +38 -0
  41. package/types/engine/sql-tables.d.mts +8 -2
  42. package/types/engine/word-classes.d.mts +87 -0
  43. package/types/engine/word-rows.d.mts +119 -0
  44. package/types/engine/word-terms.d.mts +72 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 23.6.1
4
+
5
+ ### Patch Changes
6
+
7
+ **Dictionaries and glossaries**
8
+
9
+ - A language's Suffixes and Fields tables can be set from its frontmatter, by querying `affixes` and `fields` in an `sql` fence.
10
+ - An affix can state its own tone pattern in `tones`.
11
+ - `key=false` leaves the key off a dictionary or glossary fence, so a page with one fence per heading states the key once.
12
+ - A daughter language's words built from its parent's words and affixes no longer warn that a piece matches nothing, and their glosses print.
13
+ - Dictionaries abbreviate particle as _ptcl._, since _part._ reads as participle.
14
+
15
+ **Diagnostics**
16
+
17
+ - File paths stay relative to the working directory when `PACKAGE_BUILD_CONFIG` names the configuration through a symlinked path, as macOS temporary directories are.
18
+
19
+ ## 23.6.0
20
+
21
+ ### Minor Changes
22
+
23
+ - `package-build lint` refuses a content or picture path holding a character outside printable ASCII, reporting it as an error at the path. Rename the file using ASCII only; addresses come from the shortcode, so no address changes.
24
+
25
+ ### Patch Changes
26
+
27
+ **Dictionaries and glossaries**
28
+
29
+ - A language note lists its words in `data.words`, with the class modifiers, fields and affixes they use; the compiled Item is unchanged.
30
+ - A language names its parent in `data.family` and reads the parent's words as its own; lint reports a parent that is no language or a family that loops.
31
+ - A language states the tag a dictionary prints for it in `data.abbreviation`, such as `Prov.`.
32
+ - Cultures and affiliations list their own glossary terms in `data.glossary`. These terms mark words and ranks common, give respellings, let in single-word titles, and add or replace definitions.
33
+ - A glossary term that names no language takes its affiliation's, a parent affiliation's or its culture's; lint reports a term that ends up with none.
34
+ - Lint warns about an affiliation that belongs to no culture, since its ranks and offices reach no glossary.
35
+ - An `sql` fence sets its rows as a dictionary of words with `layout=dictionary`, or as a glossary of terms with `layout=glossary`: bold headwords under letter headings, a key of only the abbreviations used, numbered senses and citations.
36
+ - One query lists a language's words, including inherited ones, or its common words on any page. One query lists a culture's or an affiliation's glossary with its ranks and offices, or the whole setting's dictionary and glossary.
37
+ - `package-build content-index` also writes the dictionary and the glossary beside the index, for reading with `jq` or an editor.
38
+
39
+ **Checks and compile**
40
+
41
+ - The content checks and the compile run several times faster on a large tree.
42
+ - Two notes that pin the same `id` are reported at the second note, naming the first, instead of as dozens of items that duplicate themselves.
43
+
3
44
  ## 23.5.1
4
45
 
5
46
  ### Patch Changes
@@ -69,6 +69,7 @@ import { checkArtSlotSizes } from "../engine/art-fields.mjs";
69
69
  import { lintContentTree } from "../engine/content-lint.mjs";
70
70
  import { lintNoteStates } from "../engine/stub-lint.mjs";
71
71
  import { lintContentCharset } from "../engine/content-charset.mjs";
72
+ import { lintPathCharset } from "../engine/path-charset.mjs";
72
73
  import { lintContentHtml } from "../engine/content-html.mjs";
73
74
  import { lintContentIcons } from "../engine/content-icons.mjs";
74
75
  import { lintContentImages } from "../engine/content-images.mjs";
@@ -1093,6 +1094,16 @@ function lintCommand() {
1093
1094
  skipDirectories: config.skipDirectories,
1094
1095
  });
1095
1096
 
1097
+ // The spelling of every content and picture path, held to
1098
+ // printable ASCII. Walked from the directory entries rather
1099
+ // than the parsed notes, so a picture, which has no note,
1100
+ // is covered by the same rule.
1101
+ const pathCharset = lintPathCharset({
1102
+ contentBase: root,
1103
+ assetsBase: config.paths.assets,
1104
+ skipDirectories: config.skipDirectories,
1105
+ });
1106
+
1096
1107
  // The names the charset check leaves room for. An icon
1097
1108
  // the registry does not declare renders as its own literal
1098
1109
  // text, which is visible but easy to publish, so it is reported
@@ -1175,6 +1186,7 @@ function lintCommand() {
1175
1186
  ...frontmatter.findings,
1176
1187
  ...schemaFindings,
1177
1188
  ...charset.findings,
1189
+ ...pathCharset.findings,
1178
1190
  ...icons.findings,
1179
1191
  ...html.findings,
1180
1192
  ...images.findings,
@@ -1593,16 +1605,22 @@ function contentIndexCommand() {
1593
1605
  handler: (argv) => {
1594
1606
  try {
1595
1607
  const config = loadPackConfig();
1596
- const { file, notes, bytes, full, draft, stub } = emitContentIndex({
1597
- config,
1598
- ...(argv.root ? { contentBase: argv.root } : {}),
1599
- ...(argv.out ? { outDir: argv.out } : {}),
1600
- });
1608
+ const { file, notes, bytes, full, draft, stub, dictionary, terms } =
1609
+ emitContentIndex({
1610
+ config,
1611
+ ...(argv.root ? { contentBase: argv.root } : {}),
1612
+ ...(argv.out ? { outDir: argv.out } : {}),
1613
+ });
1601
1614
  log.info(
1602
1615
  `${config.contentPackage} → ${path.relative(process.cwd(), file)} ` +
1603
1616
  `(${notes} notes — ${full} full, ${draft} draft, ` +
1604
1617
  `${stub} stub — ${Math.round(bytes / 1024)} KiB)`,
1605
1618
  );
1619
+ for (const [relation, written] of Object.entries({ dictionary, terms }))
1620
+ log.info(
1621
+ `${config.contentPackage} → ${path.relative(process.cwd(), written.file)} ` +
1622
+ `(${written.rows} ${relation} rows)`,
1623
+ );
1606
1624
  } catch (err) {
1607
1625
  reportFailure(err);
1608
1626
  process.exitCode = 1;
package/docs/api.md CHANGED
@@ -1052,6 +1052,15 @@ The charset authored content is held to, so a book can choose its face. The pack
1052
1052
  | `checkText` | `function checkText(text, file)` | {Array<{file: string, line: number, column: number, severity: "warning", message: string}>} What is wrong, in file order. | Check one file's text against the charset and the normalization rule. |
1053
1053
  | `lintContentCharset` | `function lintContentCharset(contentBase,` | `{{findings: Array<{file: string, line: number, column: number, severity: "warning", message: string}>, files: number}}` The findings, and how many files produced them. | Walk a content tree and check every authored file in it. |
1054
1054
 
1055
+ ### `engine.pathCharset`
1056
+
1057
+ The characters a content or picture path may hold: printable 7-bit ASCII, U+0020 to U+007E. A note's identity comes from its `shortcode`, so a path's spelling changes no address; a non-ASCII name changes only how the file travels between filesystems, shells, archives and CDN keys.
1058
+
1059
+ | Export | Signature | Returns | Use it when |
1060
+ | ------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
1061
+ | `offendingPathCharacters` | `function offendingPathCharacters(text)` | {string[]} The distinct characters outside U+0020 to U+007E, in order of first appearance. | Whether a path, or one component of it, is clean. |
1062
+ | `lintPathCharset` | `lintPathCharset({ contentBase, assetsBase, skipDirectories })` | `{{findings: Array<{file: string, severity: "error", message: string}>, files: number}}` The findings, and how many entries were checked. | Walk the content tree and the picture roots and report every entry named outside printable ASCII. |
1063
+
1055
1064
  ### `engine.contentIcons`
1056
1065
 
1057
1066
  Naming an interface icon in a note, without drawing it there. The user guide describes Foundry's interface, and it did so by pasting Unicode lookalikes of the icons the sheets actually draw: `☆` for the improve flag, `✎` for the formula editor, `◆` in the success-value table, `★★★` for mastery. The system renders every one of those with **Font Awesome** — a `fa-regular fa-star`, a `fa-solid fa-pen-to-square` — so the note and the screen it describes were drawing different pictures, and drifting apart with every sheet change.
@@ -309,13 +309,20 @@ A column's heading is its SQL alias, so `AS "Name"` is what a reader sees. A cel
309
309
 
310
310
  ### What a query can read
311
311
 
312
- | Relation | What it holds |
313
- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
314
- | `notes` | Every note except a stub. This is the one to use unless a stub is wanted. |
315
- | `entries` | Every note, stubs included. |
316
- | `events` | One row per event a `lore`, `place` or `affiliation` note writes in `data.events`, with `whenSort` to order them by date. |
317
- | `market` | The market scale as three columns — `value`, `name`, `trade` — so a table can print `village` beside the number a note wrote. |
318
- | `<package>.notes` | The same relations — `notes`, `entries` and `events` — for each package this one declares a dependency on, in a schema named for it. |
312
+ | Relation | What it holds |
313
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
314
+ | `notes` | Every note except a stub. This is the one to use unless a stub is wanted. |
315
+ | `entries` | Every note, stubs included. |
316
+ | `events` | One row per event a `lore`, `place` or `affiliation` note writes in `data.events`, with `whenSort` to order them by date. |
317
+ | `words` | One row per word a language note writes in `data.words`, with that note as its `language`, its `position` and what it is built from. |
318
+ | `affixes` | One row per affix a language note writes in `data.affixes`, with that note as its `language`, its `position`, and each key the affix states: `form`, `gloss`, `attaches`, `examples` and `tones`. |
319
+ | `fields` | One row per field a language note writes in `data.fields`, with that note as its `language`, its `position`, and the field's `name` and what it `holds`. |
320
+ | `glossary` | One row per entry a culture or an affiliation writes in `data.glossary`, with the `note` it belongs to and the `language` it resolves to. |
321
+ | `language_lineage` | One row per language note and each language it inherits words from through `data.family`, itself included at `depth` 0. Join it to `words` for a language's words with inherited ones. |
322
+ | `dictionary` | One row per word per language it is listed under, inherited words included, with its `depth`. See [dictionaries and glossaries](#dictionaries-and-glossaries). |
323
+ | `terms` | One row per glossary entry, rank or office per note that owns it, with the `cultures` it reaches. See [dictionaries and glossaries](#dictionaries-and-glossaries). |
324
+ | `market` | The market scale as three columns — `value`, `name`, `trade` — so a table can print `village` beside the number a note wrote. |
325
+ | `<package>.notes` | The same relations — `notes`, `entries`, `events`, `words`, `affixes`, `fields`, `glossary`, `language_lineage`, `dictionary` and `terms` — for each package this one declares a dependency on, in a schema named for it. |
319
326
 
320
327
  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.
321
328
 
@@ -412,14 +419,36 @@ ORDER BY e.whenSort, e.address
412
419
 
413
420
  An event with no `where.locus` keeps its row with an empty **Where** cell.
414
421
 
422
+ ### A language's affixes and fields
423
+
424
+ `affixes` and `fields` hold the entries of a language note's `data.affixes` and `data.fields`, so a page can set them as tables. A Suffixes table is a `layout=table` fence over `affixes`; the `tones` key is the tone pattern an affix carries:
425
+
426
+ ````markdown
427
+ ```sql
428
+ SELECT form AS "Suffix", attaches AS "Stem", gloss AS "Makes", array_to_string(examples, ', ') AS "Example", tones AS "Tones"
429
+ FROM affixes
430
+ WHERE language = 'thalorna-note-skill-ekpelulng' AND form LIKE '-%'
431
+ ORDER BY position
432
+ ```
433
+ ````
434
+
435
+ | Suffix | Stem | Makes | Example | Tones |
436
+ | ------ | ----- | -------------------------------------------- | ------- | ----- |
437
+ | -ndo | added | the keeper of it: one who tends and holds it | luwando | L |
438
+ | -ma | added | the child of it: one who is fed by it | yesoma | H |
439
+
440
+ `ORDER BY position` keeps the order the note states them in. The same shape serves `fields`, with `name` and `holds`.
441
+
415
442
  ### Fence attributes
416
443
 
417
- A braced list may follow the language on the opening fence. Two attributes are accepted, and anything else is reported by name.
444
+ A braced list may follow the language on the opening fence. Four attributes are accepted, and anything else is reported by name.
418
445
 
419
- | Attribute | Value | Default | What it does |
420
- | --------------- | --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
421
- | `allow-empty` | `true` or `false` | `false` | Whether a query selecting no rows is allowed. A zero-row result is a finding otherwise, because a table that silently prints nothing is the commonest way a query goes wrong unnoticed. |
422
- | `section-level` | an integer from 1 through 6 | `2` | The heading level given to each group a `_section` column creates. |
446
+ | Attribute | Value | Default | What it does |
447
+ | --------------- | ----------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
448
+ | `allow-empty` | `true` or `false` | `false` | Whether a query selecting no rows is allowed. A zero-row result is a finding otherwise, because a table that silently prints nothing is the commonest way a query goes wrong unnoticed. |
449
+ | `section-level` | an integer from 1 through 6 | `2` | The heading level given to each group a `_section` column creates, and to each letter of a dictionary or a glossary. |
450
+ | `key` | `true` or `false` | `true` | Whether a dictionary or a glossary prints its key. `false` leaves it out, for a page that states the key once above several fences. |
451
+ | `layout` | `table`, `dictionary` or `glossary` | `table` | How the result is set: as a table, as a dictionary of words, or as a glossary of terms — see [dictionaries and glossaries](#dictionaries-and-glossaries). |
423
452
 
424
453
  ````markdown
425
454
  ```sql {allow-empty=true section-level=3}
@@ -445,6 +474,73 @@ FROM notes WHERE type IN ('lore', 'place') ORDER BY type, name.full
445
474
 
446
475
  See [SQL details](../reference/format-details.md#content-tables) for query options.
447
476
 
477
+ ### Dictionaries and glossaries
478
+
479
+ Two layouts set a result as paragraphs under letter headings rather than as a table. **`layout=dictionary` sets words**, read from the `dictionary` relation: each with its class, its etymology, its senses and the notes it is cited from. **`layout=glossary` sets terms**, read from the `terms` relation: a culture's or an affiliation's own glossary entries and an affiliation's ranks and offices, each sense tagged with the body or culture it comes from. Each needs the `name`, `senses` and `usage` columns, and a query that leaves one out is reported, naming the column.
480
+
481
+ **The query decides what appears**, and any note may carry either fence. These are the common ones:
482
+
483
+ | What | Layout | Query |
484
+ | ----------------------------------------- | ------------ | --------------------------------------------------------------------------------- |
485
+ | A language's dictionary | `dictionary` | `SELECT * FROM dictionary WHERE language = '<the language's Address>'` |
486
+ | A language's common words, on any page | `dictionary` | `SELECT * FROM dictionary WHERE language = '<the language's Address>' AND common` |
487
+ | A culture's glossary | `glossary` | `SELECT * FROM terms WHERE list_contains(cultures, '<the culture's Address>')` |
488
+ | An affiliation's glossary | `glossary` | `SELECT * FROM terms WHERE owner = '<the affiliation's Address>'` |
489
+ | The setting's _Dictionary of the Tongues_ | `dictionary` | `SELECT * EXCLUDE (common) FROM dictionary WHERE depth = 0` |
490
+ | The setting's _Glossary_ | `glossary` | `SELECT * EXCLUDE (common) FROM terms` |
491
+
492
+ A language's dictionary lists the words it inherits through `data.family` beside its own; `depth = 0` lists each word once, under the language that states it. A culture's glossary holds the culture's own terms and the ranks, offices and glossary terms of every affiliation that reaches it. The two setting pages leave out `common`, so they mark no entry: which words are common is a culture's judgement.
493
+
494
+ The most common Vylari words, on the Vylarian culture's page:
495
+
496
+ ````markdown
497
+ ## Words Everyone Knows {#common-words}
498
+
499
+ ```sql {layout=dictionary section-level=3}
500
+ SELECT * FROM dictionary WHERE language = 'thalorna-note-skill-vylarilng' AND common
501
+ ```
502
+ ````
503
+
504
+ A culture's glossary:
505
+
506
+ ````markdown
507
+ ## Glossary {#glossary}
508
+
509
+ ```sql {layout=glossary section-level=3}
510
+ SELECT * FROM terms WHERE list_contains(cultures, 'thalorna-note-lore-provenzianclt')
511
+ ```
512
+ ````
513
+
514
+ A page that sets several fences, one per heading, states its key once and sets `key=false` on each fence, so the abbreviations are not repeated under every heading:
515
+
516
+ ````markdown
517
+ ```sql {layout=dictionary key=false section-level=4}
518
+ SELECT * FROM dictionary WHERE language = 'thalorna-note-skill-vylarilng' AND depth = 0
519
+ ```
520
+ ````
521
+
522
+ Either layout prints, in order:
523
+
524
+ - **a key**, once, unless the fence sets `key=false`, listing only the abbreviations the result uses — a language's tag, and for words a class or a modifier — each with what it stands for, and the common mark where an entry carries it;
525
+ - **a heading per letter**, at `section-level`, accents taken off and apostrophes skipped, so `'ostal` files under O and `Écuyer` under E;
526
+ - **one paragraph per entry**: `•` for a common entry, the headword in bold, its pronunciation between slashes, its language tag in italics, the senses, and after a dash the notes it is cited from, as links.
527
+
528
+ A word also carries its class beside the tag and its etymology in brackets:
529
+
530
+ ```markdown
531
+ • **solmère** /sol-MEH-reh/ _n. m._ [fr. Élav. _sólmerë_] Longing; the ache for a time that is gone. — [[thalorna-note-lore-lastshrine|The Song of the Last Shrine]]
532
+ ```
533
+
534
+ A term carries no class, and each of its senses names its source. A glossary sets the rows sharing a headword and a language as one entry, numbering their senses in the order the rows arrive and printing a sense several bodies give once, with all of them:
535
+
536
+ ```markdown
537
+ **Sworn Knight** **1** (_Duchy of Aubrac_) Holds land of the duke. **2** (_Kingdom of Provenzia_) Holds land of the crown. **3** (_Provenzia_) A knight bound by oath to a lord. — [[thalorna-note-affiliation-duchy|Duchy of Aubrac]], [[thalorna-note-affiliation-kingdom|Kingdom of Provenzia]], [[thalorna-note-lore-provenzianclt|Provenzia]]
538
+ ```
539
+
540
+ The entries are ordered by the layout, not by the query: by letter, then by `name_sort`. **The language tag prints where the entry's language is not the page's own**: a language note's own language, a culture's `data.language`, or the language an affiliation's glossary resolves to. It is the language note's `data.abbreviation`, or its full name where it states none. The etymology never repeats the entry's own language: a borrowed word prints `[fr. Élav. _sólmerë_]`, a built word its pieces each with its gloss, `[_am-_ "love" + _-ère_ "one who"]`, an inherited word its older form, `[*_hur_]` or `[Hel. *_thalass_]`, and a root word its root and pattern. A citation of a note with no published page prints as its name.
541
+
542
+ A culture's or an affiliation's `data.glossary` shapes both: an entry without a definition marks a word or a rank common, gives it a sort key or a respelling, and lets in a rank whose title is a single word; an entry with a definition replaces the senses of the owner's own rank or office, or is a term of the owner's own. The [format reference](../reference/format-details.md#dictionaries-and-glossaries) gives every column and rule.
543
+
448
544
  ## Page lists
449
545
 
450
546
  A fence marked `pagelist` is replaced by a list of the pages carrying a tag. Where a content table answers "what do these notes say?", a page list answers "which pages belong to this group?" — and the group is expressed by intent rather than enumerated, so a page joins it by being tagged and leaves it by being untagged.
@@ -91,6 +91,18 @@ errors, reported at the `ranks` key.
91
91
 
92
92
  An Item's `{#description}` page is its readable description. The note Address `<package>-note-<type>-<shortcode>#description` resolves to a JournalEntryPage UUID. The build writes the same `@UUID[...]` pointer to SoHL Item `system.docHtml` and HM3 Item `system.description`. An Item note without that anchor uses its first page as the description. Other pages and anchors remain available for links.
93
93
 
94
+ ## File and folder names
95
+
96
+ Every character in the path of a content file or a picture is printable 7-bit ASCII, U+0020 to U+007E. The rule covers each file and folder name under the content tree and under the package's `icons/` and `images/` roots. Letters, digits, spaces, hyphens, underscores, dots and apostrophes are all inside it, so `Knight's_Shield.md` is a valid name. An accented letter, any other non-ASCII character, and a control character are outside it.
97
+
98
+ `package-build lint` reports each offending name as an error located at the path, with no line or column, and names the characters by code point:
99
+
100
+ ```text
101
+ assets/content/Misc_Gear/Scribe/A_Sílhari_Manual.md: error: the path holds U+00ED (í), outside printable ASCII; rename the file using ASCII only
102
+ ```
103
+
104
+ The fix is to rename the file, `A_Sílhari_Manual.md` to `A_Silhari_Manual.md`. A note's identity comes from its `shortcode`, and a picture's from its basename without the extension, so the rename changes no address and breaks no link. The accented spelling belongs in the note's `name`, where it displays as written.
105
+
94
106
  ## A complete minimal note
95
107
 
96
108
  Place this at `assets/content/Places/Harbor.md`:
@@ -129,4 +141,8 @@ for a complete example, parent ordering and findings.
129
141
 
130
142
  Places, affiliations, lore and beings may name their culture in `data.culture`, and a `doc` of `subType: settingguide` must. A place or affiliation that names none inherits the culture of its parents. See [culture in the index](reference/format-details.md#culture-in-the-index).
131
143
 
144
+ An affiliation may name the trade, temple or order tongue it keeps in its own `data.language`. An affiliation that reaches no culture is a warning, in a tree holding a culture note. A language needs no culture and no affiliation. See [type: affiliation](reference/format-details.md#type-affiliation).
145
+
146
+ A language note (`type: skill`, `subType: language`) states its words in frontmatter: `data.words` states each word as a map of its form, class, field and gloss, with its source, the pieces it is built from and the notes it is attested in, beside `data.classModifiers`, `data.fields` and `data.affixes`. The language of a word is the note it sits on, and none of it reaches the compiled Item. A daughter language names its parent in `data.family` and reads the parent's words as its own; the `language_lineage` relation joins a language to its ancestors. Classes come from the [master word classes](reference/format-details.md#word-classes). A culture or an affiliation states its own glossary terms in `data.glossary`, and every entry resolves to a language — its own `language`, else the affiliation's, its parents' or its culture's, else the culture's — or is an error at the entry. The `words`, `affixes`, `fields`, `glossary` and `language_lineage` relations hold them for `sql` fences, the `dictionary` relation lists each word under every language that has it, inherited words included, and the `terms` relation each glossary entry, rank and office with the cultures it reaches; a fence sets them with `layout=dictionary` or `layout=glossary`. A language note names the tag a dictionary prints for it in `data.abbreviation`. See [a language's words](reference/format-details.md#a-languages-words), [glossary terms](reference/format-details.md#glossary-terms) and [dictionaries and glossaries](reference/format-details.md#dictionaries-and-glossaries).
147
+
132
148
  Blockquote alerts use `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, or `[!]`, with optional attributes after the marker. They replace the former info and warn div fences; secret blocks remain supported. See [alerts](authoring/links-and-markup.md#alerts).
@@ -163,6 +163,95 @@ frontmatter remains queryable. An Item or Actor note can have both a readable
163
163
  Consumers can use the index's own addresses and file paths rather than
164
164
  reconstructing them from filenames.
165
165
 
166
+ ## Set words as a dictionary and terms as a glossary
167
+
168
+ A language's words and a people's terms are each one `sql` fence, and nothing
169
+ is copied into the note: the words come from the language notes' `data.words`,
170
+ the terms from the `data.glossary` entries of cultures and affiliations and
171
+ from the affiliations' ranks and offices, and a section changes when they do.
172
+ `layout=dictionary` sets rows of the `dictionary` relation and
173
+ `layout=glossary` rows of the `terms` relation. The query decides what
174
+ appears, so any note may carry either.
175
+
176
+ A language note lists its words, with the words it inherits from its parent
177
+ language:
178
+
179
+ ````markdown
180
+ # Dictionary {#dictionary}
181
+
182
+ ```sql {layout=dictionary section-level=2}
183
+ SELECT * FROM dictionary WHERE language = 'thalorna-note-skill-provnzlng'
184
+ ```
185
+ ````
186
+
187
+ A culture note lists the words of its tongue a player is expected to know, and
188
+ its glossary: its own terms and the ranks, offices and terms of every
189
+ affiliation that reaches it.
190
+
191
+ ````markdown
192
+ # Words Everyone Knows {#common-words}
193
+
194
+ ```sql {layout=dictionary section-level=2}
195
+ SELECT * FROM dictionary WHERE language = 'thalorna-note-skill-provnzlng' AND common
196
+ ```
197
+
198
+ # Glossary {#glossary}
199
+
200
+ ```sql {layout=glossary section-level=2}
201
+ SELECT * FROM terms WHERE list_contains(cultures, 'thalorna-note-lore-provenzianclt')
202
+ ```
203
+ ````
204
+
205
+ The glossary renders as a key, a heading per letter and one paragraph per
206
+ term, each sense tagged with the body or culture it comes from and each term
207
+ in another tongue tagged with it:
208
+
209
+ ```markdown
210
+ **Key:** _Lit._ Liturgic Language; • a common word, which a player is expected to know by heart.
211
+
212
+ ## K
213
+
214
+ **Knight-Captain** **1** (_Kingdom of Provenzia_) A knight who leads a company. **2** (_Kingdom of Provenzia_) Commands the guard in the field. — [[thalorna-note-affiliation-kingdom|Kingdom of Provenzia]]
215
+
216
+ ## O
217
+
218
+ **Ordained Priest** _Lit._ (_Church of the Rite_) Ordained to serve a parish. — [[thalorna-note-affiliation-church|Church of the Rite]]
219
+ ```
220
+
221
+ The culture shapes both through its own `data.glossary`.
222
+ `{name: solmère, common: true, pronunciation: sol-MEH-reh}` marks a word of its
223
+ tongue common, so the first fence lists it, and gives it a respelling;
224
+ `{name: Subject, common: true}` marks a rank and lets in a title of one word,
225
+ which no glossary holds unless an entry names it; and
226
+ `{name: blood-field, definition: …}` is a term of the culture's own.
227
+
228
+ An affiliation lists its own terms with `WHERE owner = '<its Address>'`. The
229
+ setting's two pages list everything, each word once and each term under every
230
+ owner that holds it:
231
+
232
+ ````markdown
233
+ ```sql {layout=dictionary}
234
+ SELECT * EXCLUDE (common) FROM dictionary WHERE depth = 0
235
+ ```
236
+ ````
237
+
238
+ ````markdown
239
+ ```sql {layout=glossary}
240
+ SELECT * EXCLUDE (common) FROM terms
241
+ ```
242
+ ````
243
+
244
+ `package-build content-index` also writes both relations beside the index, for
245
+ reading with `jq`:
246
+
247
+ ```bash
248
+ jq -r 'select(.cultures | index("thalorna-note-lore-provenzianclt")) | .name' \
249
+ build/content-index/thalorna-terms.jsonl
250
+ ```
251
+
252
+ The [format reference](../reference/format-details.md#dictionaries-and-glossaries)
253
+ gives every column and every rule.
254
+
166
255
  ## Resolve another package
167
256
 
168
257
  In a Foundry package, declare a content-bearing dependency under