@heroiclands/package-build 23.11.0 → 23.12.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 (62) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +1 -0
  3. package/assets/preview/check-sanitizer.go +68 -0
  4. package/assets/preview/gitea-theme.js +36 -0
  5. package/assets/preview/gitea.ini +127 -0
  6. package/assets/styles/content-base.css +119 -16
  7. package/bin/content-commands.mjs +2 -2
  8. package/bin/package-build-preview.mjs +75 -0
  9. package/content-config.mjs +11 -0
  10. package/docs/api.md +105 -46
  11. package/docs/authoring/links-and-markup.md +52 -14
  12. package/docs/commands.md +46 -2
  13. package/docs/configuration.md +21 -4
  14. package/docs/design.md +4 -3
  15. package/docs/guides/book.md +10 -6
  16. package/docs/reference/format-details.md +44 -40
  17. package/docs/reference/note-types.md +0 -1
  18. package/docs/reference/theme-pages.md +1 -1
  19. package/docs/source-preview.md +172 -0
  20. package/engine/base-compiler.mjs +7 -8
  21. package/engine/being-age.mjs +19 -105
  22. package/engine/calendar-notes.mjs +3 -25
  23. package/engine/compile-corpus.mjs +2 -1
  24. package/engine/content-format.yaml +0 -1
  25. package/engine/content-index.mjs +9 -9
  26. package/engine/content-spans.mjs +28 -6
  27. package/engine/current-date.mjs +37 -0
  28. package/engine/definition-list.mjs +58 -0
  29. package/engine/first-use.mjs +4 -2
  30. package/engine/frontmatter-lint.mjs +7 -2
  31. package/engine/helpers.mjs +2 -0
  32. package/engine/index.mjs +2 -0
  33. package/engine/infobox.mjs +1 -5
  34. package/engine/note-vocabulary.mjs +2 -2
  35. package/engine/npc-generate.mjs +7 -6
  36. package/engine/pdf-render.mjs +37 -4
  37. package/engine/preview-current-date.mjs +60 -0
  38. package/engine/site-build.mjs +5 -5
  39. package/engine/site-index.mjs +5 -2
  40. package/engine/site-preview.mjs +641 -325
  41. package/hugo-theme/layouts/_default/list.html +1 -1
  42. package/hugo-theme/layouts/blog/list.html +1 -1
  43. package/hugo-theme/layouts/blog-post/single.html +1 -1
  44. package/hugo-theme/layouts/partials/dictionary-lists.html +53 -0
  45. package/hugo-theme/layouts/partials/page-body.html +1 -1
  46. package/hugo-theme/static/css/style.css +22 -8
  47. package/package.json +8 -2
  48. package/types/content-config.d.mts +8 -0
  49. package/types/engine/base-compiler.d.mts +1 -1
  50. package/types/engine/being-age.d.mts +10 -44
  51. package/types/engine/calendar-notes.d.mts +2 -6
  52. package/types/engine/compile-corpus.d.mts +2 -1
  53. package/types/engine/content-index.d.mts +3 -1
  54. package/types/engine/content-spans.d.mts +4 -2
  55. package/types/engine/current-date.d.mts +9 -0
  56. package/types/engine/definition-list.d.mts +8 -0
  57. package/types/engine/frontmatter-lint.d.mts +6 -2
  58. package/types/engine/index.d.mts +2 -0
  59. package/types/engine/npc-generate.d.mts +2 -2
  60. package/types/engine/preview-current-date.d.mts +8 -0
  61. package/types/engine/site-index.d.mts +5 -1
  62. package/types/engine/site-preview.d.mts +40 -33
@@ -1822,11 +1822,12 @@ link to the whole public note does not inherit a section's secrecy.
1822
1822
 
1823
1823
  The website renders an inline show/hide spoiler; an omitted GM-only target
1824
1824
  retains its reference as text without a dead link. A published secret section
1825
- retains its link. Foundry renders a visible inline spoiler with a **Secret:**
1826
- label, with access to its destination controlled by private packs and page
1827
- permissions. Books print secret styling and a **Secret:** label, retaining text
1828
- without a link when the GM-only destination is omitted. `.show` bypasses the
1829
- automatic spoiler presentation on each surface; an unavailable destination
1825
+ retains its link. Foundry renders a CSS-redacted inline spoiler, revealed on
1826
+ hover or keyboard focus, with destination access controlled by private packs
1827
+ and page permissions. Books ignore inline `.secret` presentation and print
1828
+ references normally, retaining text without a link when the GM-only destination
1829
+ is omitted. `.show` bypasses automatic spoiler presentation on the website and
1830
+ in Foundry; book references already print normally. An unavailable destination
1830
1831
  still has no link.
1831
1832
 
1832
1833
  An immediately following `.show` attribute explicitly reveals the reference:
@@ -1850,9 +1851,13 @@ infoboxes, code, comments and embeds are outside this check.
1850
1851
 
1851
1852
  A name is explicit: a `[Name]{.name}` bracket span or an authored link label.
1852
1853
  Use typed `.name` spans for unwritten subjects and ordinary wikilinks for
1853
- indexed subjects, including private ones. Introduce another subject with its
1854
- full canonical name at its first prose use; use plain text for the note's own
1855
- subject and for later mentions and aliases.
1854
+ indexed subjects, including private ones. Mark or link another subject at its
1855
+ first prose use, and use plain text for the note's own subject and for later
1856
+ mentions and aliases. A wikilink identifies its note, so its label is whatever
1857
+ reads naturally in the sentence: `[[place-vedyara|Vedyara]]` for a note whose
1858
+ full name is _Vedyara Region_, and a lowercase article mid-sentence
1859
+ (`[[place-southwall|the Southern Wall]]`). A `.name` span has no note behind
1860
+ it, so it carries the full name.
1856
1861
  Plain prose is not searched for names. Wikilinks, ordinary Markdown links and
1857
1862
  reference links all supply names; a resolved empty wikilink label uses the
1858
1863
  note's full name. Images, code, footnote markers and bare autolinks without
@@ -1866,9 +1871,10 @@ authored labels supply none.
1866
1871
  anchors. Other labels use their whole text as their identity: different
1867
1872
  labels linking to one page are separate names, and a label is not
1868
1873
  automatically an alias of its destination.
1869
- 3. Introduce the full name before a short form or declared alias.
1870
- Possessives and regular plurals count as uses; other indexed forms can be
1871
- declared aliases.
1874
+ 3. A `.name` span or an ordinary Markdown link introduces the full name
1875
+ before a short form or declared alias. A resolved wikilink introduces its
1876
+ note under any label. Possessives and regular plurals count as uses; other
1877
+ indexed forms can be declared aliases.
1872
1878
 
1873
1879
  Bold is emphasis. A whole bold span exactly matching an indexed full name or
1874
1880
  alias receives one advisory to mark or link the name. It supplies no name
@@ -2667,7 +2673,13 @@ local to a note; rendered markers are sequential numbers. A Foundry page
2667
2673
  collects its referenced footnotes in a Footnotes section at the bottom of that
2668
2674
  JournalEntryPage, while a book places them at the bottom of the page of
2669
2675
  reference. A term followed by `: definition` lines creates a definition list.
2670
- See the [authoring examples](../authoring/links-and-markup.md#footnotes-and-definition-lists).
2676
+ After a blank line, indent continuation paragraphs and blocks by four spaces
2677
+ to keep them in the same definition; indent a code block's opening fence,
2678
+ contents and closing fence. Another `:` starts a separate definition for the
2679
+ same term. See the [authoring examples](../authoring/links-and-markup.md#footnotes-and-definition-lists). Definition lists use dictionary hanging indents: bold, nonwrapping terms,
2680
+ normal definitions, a 150px shared column (100px at browser widths up to 600px),
2681
+ a `.75em` term gap and `.5em` entry spacing. A long term pushes only the first
2682
+ definition line beyond the shared column; continuation lines return to it.
2671
2683
 
2672
2684
  ```
2673
2685
  # Heading {#id .class1 .class2 attr="value"}
@@ -3528,11 +3540,13 @@ The exact `.secret` class marks the enclosed text as an inline spoiler:
3528
3540
  ```
3529
3541
 
3530
3542
  The website provides an inline show/hide button and preserves interactive links
3531
- inside the revealed content. Foundry prints a visible inline `secret` span
3532
- with a **Secret:** label; the span does not withhold its text from players.
3533
- The book prints the text with secret styling and a **Secret:** label.
3534
- Inline emphasis and combined `.name` classification survive rendering. Published HTML retains the text, so opening
3535
- the spoiler or inspecting the source reveals it.
3543
+ inside the revealed content. Foundry renders a focusable, CSS-redacted inline
3544
+ span that reveals its text and nested content on hover or keyboard focus.
3545
+ Native Foundry secret sections retain their own controls. Books ignore `.secret`
3546
+ on inline spans, preserving their text, emphasis, links, anchors and `.name`
3547
+ bold styling. Secret sections and `:::secret` fences keep their visible labelled
3548
+ boxes. Published HTML retains the text, so opening the spoiler or inspecting the
3549
+ source reveals it.
3536
3550
 
3537
3551
  This span has no effect on a destination's audience. Ordinary wikilinks derive
3538
3552
  spoilers from their resolved destinations; `.show` bypasses that automatic
@@ -4555,7 +4569,7 @@ three fields are built around.
4555
4569
  unrecorded. Both spellings of `died` mean dead.
4556
4570
  - **`age` is optional and is an override.** Written, it wins over anything the
4557
4571
  dates say. Unwritten beside a dated `born`, it is the age that follows from
4558
- the date and the package's declared present — see below. Unwritten beside
4572
+ the date and the configured current date — see below. Unwritten beside
4559
4573
  `born: unknown` or an absent `born`, the age is unknown.
4560
4574
 
4561
4575
  Most people in a setting do not know their own birth date, so an age beside
@@ -4568,22 +4582,15 @@ read it.
4568
4582
 
4569
4583
  #### The age a dated `born` computes to
4570
4584
 
4571
- A package states the day its setting stops on `data.present`, on its own
4572
- `world` or `celestial` `place` note — see _What a body states about itself_
4573
- under `type: place`. Against that date, a being with a dated `born` and no
4574
- authored `age` compiles the age it implies: the two dates compared, not the
4575
- two years subtracted, so a birthday that falls later in the year than the
4576
- present counts one less than the years alone would say. A package that
4577
- declares no present computes no age and says nothing about it — the same
4578
- silence a package with no calendar mechanism meets.
4579
-
4580
- **An authored `age` beside a dated `born` is expected to agree with what the
4581
- date computes.** Where it does not, `package-build lint` raises a warning
4582
- naming both values, so a stale age left behind after a `born` was corrected
4583
- does not go unnoticed. The warning is silent beside `born: unknown` or an
4584
- absent `born` — there is nothing there to disagree with — and silent wherever
4585
- the package declares no present, for the same reason no age is computed at
4586
- all.
4585
+ The root configuration's `currentDate` states the setting's neutral current
4586
+ date, defaulting to `"720.1"`. A being with a dated `born` and no authored `age`
4587
+ compiles the age implied by those dates. A birthday later in the year counts one
4588
+ less than the difference between the years. A year-only birth date uses day one.
4589
+ Unknown and absent birth dates supply no computed age.
4590
+
4591
+ An authored `age` beside a dated `born` is expected to agree with the configured
4592
+ current date. `package-build lint` warns when the values disagree. Authored ages,
4593
+ including zero and estimates such as `~34`, remain unchanged in built output.
4587
4594
 
4588
4595
  Use `born:` for a being's birth date.
4589
4596
 
@@ -6989,7 +6996,6 @@ level with no background prints nothing.
6989
6996
  | `year.hoursPerDay` | `number` | How many hours the day divides into |
6990
6997
  | `year.minutesPerHour` | `number` | How many minutes the hour divides into |
6991
6998
  | `year.secondsPerMinute` | `number` | How many seconds the minute divides into |
6992
- | `present` | `date` | The day the setting stops and play begins |
6993
6999
  | `body.diameterKm` | `number` | The body's diameter, in kilometres |
6994
7000
  | `body.orbitalRadiusKm` | `number` | How far it orbits from the body it circles, in kilometres |
6995
7001
  | `body.orbit` | `string` | The orbit's shape — `circular` means the cycle never varies |
@@ -7021,7 +7027,6 @@ about the world's year.
7021
7027
  data:
7022
7028
  world: { equatorialCircumferenceKm: 40000, surfaceGravityG: 1.0, axialTiltDegrees: 23.5 }
7023
7029
  year: { days: 365, hoursPerDay: 24, minutesPerHour: 60, secondsPerMinute: 60 }
7024
- present: 720
7025
7030
 
7026
7031
  # its moon — a note of its own, because a moon is a body and not a property
7027
7032
  data:
@@ -7039,14 +7044,13 @@ else wrote down.
7039
7044
  can say which one a calendar divides; a `moon` with no `year` anywhere is
7040
7045
  refused, because a cycle counted in days has no year to be measured against.
7041
7046
 
7042
- **`present` is the day an age is computed against**, written in the grammar a
7043
- note's dates are written in, so a setting whose present is best said in an era
7044
- says it that way.
7047
+ The root configuration's `currentDate` supplies the neutral date used for age
7048
+ calculations throughout the setting. Its default is `"720.1"`.
7045
7049
 
7046
7050
  **`world.axialTiltDegrees`, the circumference, the gravity, the moon's diameter
7047
7051
  and its orbital radius are read by nobody.** They are there so a reader knows
7048
7052
  what kind of world this is. What the build consumes is the year, the day's
7049
- divisions, the present, the moon's cycle and epoch, and whether its orbit is
7053
+ divisions, the moon's cycle and epoch, and whether its orbit is
7050
7054
  circular and inclined.
7051
7055
 
7052
7056
  #### What a settlement's market supports
@@ -885,7 +885,6 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
885
885
  | `data.year.hoursPerDay` | number | How many hours the day divides into. |
886
886
  | `data.year.minutesPerHour` | number | How many minutes the hour divides into. |
887
887
  | `data.year.secondsPerMinute` | number | How many seconds the minute divides into. |
888
- | `data.present` | a date | The day the setting stops and play begins — what an age is computed against. |
889
888
  | `data.body.diameterKm` | number | The body's diameter, in kilometres. |
890
889
  | `data.body.orbitalRadiusKm` | number | How far the body orbits from the one it circles, in kilometres. |
891
890
  | `data.body.orbit` | string | The orbit's shape — `circular` means the cycle never varies. |
@@ -515,4 +515,4 @@ A row is a linked title and, when the page carries one, its description.
515
515
 
516
516
  ## Alerts
517
517
 
518
- The content renderer emits an `aside` with `alert` and `alert-note`, `alert-tip`, `alert-important`, `alert-warning` classes. The `alert-title` header contains a Font Awesome icon and the type's label. Shared styles supply a colored left rule, colored header and ordinary readable body; alerts do not use `details` and never start collapsed. Authored identifiers, additional classes and named attributes belong to the entire aside. Secret blocks retain their separate expandable presentation on the website.
518
+ The content renderer emits an `aside` with `alert` and `alert-note`, `alert-tip`, `alert-important`, `alert-warning` classes. The `alert-title` header contains a Font Awesome icon and the type's label. Shared styles supply a colored left rule, colored header, tinted background and readable body text. The website uses the dark palette; Foundry selects a light or dark palette from its color scheme. Alerts do not use `details` and never start collapsed. Authored identifiers, additional classes and named attributes belong to the entire aside. Secret blocks retain their separate expandable presentation on the website.
@@ -0,0 +1,172 @@
1
+ ---
2
+ shortcode: source-preview
3
+ name: { full: "Source preview and Gitea renderer" }
4
+ type: doc
5
+ subType: reference
6
+ ---
7
+
8
+ # Source preview and Gitea renderer
9
+
10
+ Preview renders only the supplied Markdown buffer. It works with ordinary
11
+ Markdown, optional YAML frontmatter, and local Heroic Lands extensions, without
12
+ configuration, a content tree, dependency caches, a content index or Pandoc.
13
+
14
+ ```sh
15
+ package-build-preview < note.md > preview.html
16
+ package-build-preview --dark < note.md > preview.html
17
+ package-build-preview --current-date 740.1 < note.md > preview.html
18
+ package-build-preview --css > preview.css
19
+ ```
20
+
21
+ The command writes an HTML fragment by default. Include `preview.css` in the
22
+ hosting page to apply the syntax colors, panels and tinted alerts. Light mode
23
+ is the default; `--dark` is explicit and independent of browser preferences.
24
+ `--css` emits both scoped palettes and does not read stdin. `--help` lists the
25
+ options. Unsupported arguments exit 1. Findings go to stderr; success exits 0,
26
+ and render errors exit 1. A partial preview may accompany error findings.
27
+
28
+ Wikilinks, image and portrait links retain their complete source: blue brackets,
29
+ purple addresses, bold foreground labels and grey attributes. Entire spans are bold, including their visible brackets and attributes.
30
+ Definition-list terms are bold and definitions use normal text. SQL and pagelist fences remain grey source;
31
+ they never execute. Calendar expressions, dependent expressions, references and
32
+ maps remain green source. Local expressions can evaluate against authored
33
+ frontmatter. Missing ages are calculated from neutral birth dates and the root
34
+ `currentDate` setting, defaulting to `"720.1"`, as for other package outputs.
35
+ Authored ages, including `0`, take precedence. Unknown or calendar-dependent
36
+ birth dates omit the age row; `{{data.age}}` is empty when unavailable. Generated sections,
37
+ related pages and See also are absent. Preview performs no publication or link
38
+ checks. Alerts render with their icons, titles, Markdown bodies and tinted
39
+ backgrounds, including nested alerts. Secret blocks are closed disclosure
40
+ spoilers. An authored `.secret` heading hides its section through the next
41
+ heading at the same or a higher level, while retaining its anchor. Secret spans
42
+ redact their label until hover or keyboard focus; explicit `.secret` wikilink
43
+ and image attributes similarly redact their inner source. The authored outer
44
+ brackets and attributes remain visible. This uses scoped CSS and a focusable
45
+ wrapper, with no preview JavaScript required.
46
+
47
+ Applications import `renderPreview` and `previewStyles` from
48
+ `@heroiclands/package-build/preview`. The HTML wrapper has `hl-preview` and
49
+ exactly one mode class: `hl-preview-light` or `hl-preview-dark`. See the
50
+ [API reference](api.md) for the function contract.
51
+
52
+ ## Current date
53
+
54
+ Declare the setting-wide date as a quoted root configuration value:
55
+
56
+ ```yaml
57
+ currentDate: "720.1"
58
+ ```
59
+
60
+ The standalone command discovers the nearest `package-build.config.yaml`,
61
+ `.yml` or `.mjs` from its working directory. YAML reads only `currentDate`; it
62
+ requires no other package fields, content or index. MJS executes its trusted
63
+ configuration module. Missing configuration or an absent setting uses `"720.1"`.
64
+ Invalid explicit values fail with a configuration diagnostic. Use
65
+ `--current-date DATE` to override discovery for one invocation. Dates must use
66
+ neutral `<year>[.<day>[:HHMMSS]]` syntax; named-calendar dates are refused.
67
+
68
+ The pure `renderPreview` API accepts `currentDate` and never reads configuration.
69
+ Hosts can opt into `await readPreviewCurrentDate(projectRoot)` to read the root
70
+ setting before rendering. Repeated calls observe configuration edits. This is
71
+ the same root setting used by compilation, website output and other toolchain
72
+ consumers; there is no separate preview date setting.
73
+
74
+ ## Gitea installation
75
+
76
+ Install Node 24 or newer and this package in the Gitea runtime, including the
77
+ container image when applicable. The examples assume installation at
78
+ `/opt/heroiclands/node_modules/@heroiclands/package-build` and a usable Node
79
+ executable on Gitea's PATH. Pin the package version in the installation's
80
+ `package.json` and lockfile.
81
+
82
+ Copy [the complete renderer configuration](../assets/preview/gitea.ini) into
83
+ `app.ini`. It replaces whole-file rendering for the listed Markdown extensions;
84
+ it does not replace Gitea's issue-comment or pull-request discussion renderer.
85
+ The renderer section must appear before its sanitizer sections. Its command
86
+ reads stdin and emits HTML; it needs no repository checkout. Change the absolute
87
+ command path to match the installation. Gitea normally uses the shared default
88
+ `"720.1"` because it receives no repository checkout. To select the deployment's
89
+ setting-wide date explicitly, use:
90
+
91
+ ```ini
92
+ RENDER_COMMAND = /opt/heroiclands/node_modules/.bin/package-build-preview --current-date 740.1
93
+ ```
94
+
95
+ Keep this override aligned with the setting's root configuration. Without an
96
+ override, run Gitea from a directory with no package-build configuration so
97
+ unrelated local configuration cannot select the date.
98
+
99
+ The supplied configuration keeps sanitization enabled and disables Gitea's
100
+ post-processing so preserved source and literal links remain intact. It permits
101
+ only the preview's class families on the elements that need them, plus the SVG
102
+ attributes needed for alert icons. Gitea's default SVG rules retain `viewBox`,
103
+ `width`, `height`, `aria-hidden`, path `d` and `fill-rule`; the extra rules retain
104
+ `fill="currentColor"` and circle coordinates. The wrapper mode class, syntax
105
+ span classes, fence classes and infobox wrapper must survive sanitization for
106
+ styling to work. Source is escaped text, so no additional source-data attributes
107
+ are required. The `secretFocus` rule retains only `tabindex="0"` on span
108
+ wrappers so keyboard users can reveal redacted source labels. Do not enable
109
+ arbitrary styles, scripts or data-URI images.
110
+
111
+ Install the stylesheet and optional theme adapter beneath the Gitea custom
112
+ path. Here `custom` means the actual instance's `CustomPath`:
113
+
114
+ ```sh
115
+ mkdir -p custom/public/assets/css custom/public/assets/js
116
+ /opt/heroiclands/node_modules/.bin/package-build-preview --css > custom/public/assets/css/heroiclands-preview.css
117
+ cp /opt/heroiclands/node_modules/@heroiclands/package-build/assets/preview/gitea-theme.js custom/public/assets/js/heroiclands-preview.js
118
+ ```
119
+
120
+ Add to `custom/templates/custom/header.tmpl`, preserving existing content:
121
+
122
+ ```html
123
+ <link rel="stylesheet" href="{{AppSubUrl}}/assets/css/heroiclands-preview.css" />
124
+ <script defer src="{{AppSubUrl}}/assets/js/heroiclands-preview.js"></script>
125
+ ```
126
+
127
+ Restart Gitea after configuration/template changes. Regenerate the CSS asset
128
+ when updating the package. These are administrator installation instructions;
129
+ package-build does not deploy or change the server configuration.
130
+
131
+ ## Gitea theme selection
132
+
133
+ The external renderer runs on the server and cannot see a reader's active
134
+ theme. The installed browser adapter reads Gitea's root CSS variable
135
+ `--is-dark-theme`, selecting dark for `true` and light for `false`. It follows
136
+ `data-theme` changes, stylesheet loads and newly inserted preview nodes. It
137
+ changes only `.markup.markdown .hl-preview` mode classes; the surrounding Gitea
138
+ interface keeps its own colors. A custom theme that omits the variable retains
139
+ the initial renderer mode.
140
+
141
+ Without the browser adapter, use the default command for fixed light rendering,
142
+ or append `--dark` to `RENDER_COMMAND` for fixed dark rendering. Explicit CLI
143
+ mode never changes itself. Theme adaptation happens only when the administrator
144
+ installs the separate browser script.
145
+
146
+ ## Sanitizer validation
147
+
148
+ [The validation harness](../assets/preview/check-sanitizer.go) applies the
149
+ shipped INI rules using the actual `bluemonday` sanitizer and the relevant
150
+ Gitea SVG defaults. It requires a Go toolchain and a temporary module with
151
+ `github.com/microcosm-cc/bluemonday` at the version used by the target Gitea
152
+ release. Run it against generated preview HTML:
153
+
154
+ ```sh
155
+ go run check-sanitizer.go /path/to/gitea.ini < preview.html > sanitized.html
156
+ ```
157
+
158
+ Check both light and dark fixtures containing all four alert types, a custom
159
+ title, nesting, wikilinks and attributes, SQL/pagelist fences, infoboxes, poetry
160
+ and footnotes. All wrapper/token classes and icon shapes should remain, while
161
+ injected scripts and event-handler attributes should disappear. This replay
162
+ checks the sanitizer primitives; also inspect the result in the actual Gitea
163
+ version before deployment, since defaults can vary between releases.
164
+
165
+ Configuration and installation follow Gitea's
166
+ [external renderer documentation](https://docs.gitea.com/administration/external-renderers/).
167
+ The relevant implementation contracts are its
168
+ [command adapter](https://github.com/go-gitea/gitea/blob/main/modules/markup/external/external.go),
169
+ [sanitizer defaults](https://github.com/go-gitea/gitea/blob/main/modules/markup/sanitizer_default.go),
170
+ [renderer settings](https://github.com/go-gitea/gitea/blob/main/modules/setting/markup.go),
171
+ [root theme attribute](https://github.com/go-gitea/gitea/blob/main/templates/base/head.tmpl),
172
+ and [theme scheme variable](https://github.com/go-gitea/gitea/blob/main/web_src/css/themes/theme-gitea-dark.css).
@@ -103,10 +103,11 @@ import { packForType } from "./ids.mjs";
103
103
  import { resolveNoteId } from "./note-ids.mjs";
104
104
  import { carriesSystemBlock } from "./system-block.mjs";
105
105
  import { checkAuthoredSystemData, checkEmittedSystemData } from "./schema-check.mjs";
106
- // A being's computed `age`, filled in against the package's own declared
107
- // present before this pass reads `data:` for anything else — see
106
+ // A being's computed `age`, filled in against the configured setting
107
+ // current date before this pass reads `data:` for anything else — see
108
108
  // `engine/being-age.mjs`.
109
- import { applyComputedBeingAge, presentAmongRecords } from "./being-age.mjs";
109
+ import { applyComputedBeingAge } from "./being-age.mjs";
110
+ import { normalizeCurrentDate } from "./current-date.mjs";
110
111
  import { reckoningContext } from "./reckoning-markers.mjs";
111
112
  import { loadPackConfig } from "./pack-config.mjs";
112
113
  import { annotateAssetReferences, assetReferenceLookup } from "./asset-references.mjs";
@@ -632,10 +633,8 @@ export class BasePackCompiler {
632
633
  if (this.constructor.convertsWikilinks) {
633
634
  this.sqlTables = this.corpus.sqlTables;
634
635
  }
635
- // The package's declared present, read once from the same corpus — so
636
- // a being's computed `age` reaches every compiled document, not only
637
- // the content index.
638
- this.worldPresent = presentAmongRecords(this.corpus.records);
636
+ // Use the compile corpus's setting date.
637
+ this.currentDate = normalizeCurrentDate(this.corpus.currentDate);
639
638
  this.unresolvedLinks = 0;
640
639
  }
641
640
 
@@ -1277,7 +1276,7 @@ export class BasePackCompiler {
1277
1276
  stats.skippedOther++;
1278
1277
  continue;
1279
1278
  }
1280
- applyComputedBeingAge(fm, this.worldPresent, dates);
1279
+ applyComputedBeingAge(fm, this.currentDate, dates);
1281
1280
  // `package:` is refused before `selects`, so a note is answered
1282
1281
  // whichever pass would have claimed it: a note's package is the
1283
1282
  // repository's configured one, so declaring it restates a
@@ -11,51 +11,9 @@
11
11
  * SPDX-License-Identifier: GPL-3.0-or-later
12
12
  */
13
13
 
14
- /**
15
- * **The middle state of `age`: absent beside a dated `born`, it follows from
16
- * the date.**
17
- *
18
- * `age` is a three-state override — authored it wins, absent beside a dated
19
- * `born` it is computed, absent beside `born: unknown` or an absent `born` it
20
- * stays unknown. This module builds the middle state, which nothing else in
21
- * the toolchain does: `born` and `age` are read straight through to every
22
- * compiled surface exactly as authored, unless a note's own package states the
23
- * day the setting stops.
24
- *
25
- * ## Where the present comes from
26
- *
27
- * A world's `present` is a fact about the world, not about a calendar or a
28
- * build configuration — it is `data.present` on the package's own `place` note
29
- * of `subType: world` (or `celestial`), read through
30
- * {@link module:engine/calendar-notes.worldInvariants}. A package with no such
31
- * note states no present, computes no age, and says nothing about it: the
32
- * absence is silent, exactly as a package that declares no calendar is asked
33
- * no calendar questions.
34
- *
35
- * ## The arithmetic is on two dates, not on two years
36
- *
37
- * A year subtracted from a year is wrong whenever the birthday has not yet
38
- * happened in the present's year — the common shape, not the exception. So
39
- * {@link computeAge} compares the two dates' canonical day ordinals where an
40
- * era marker supplies them. The birthday that still lies ahead in the current
41
- * year does not count as a completed year. An unmarked slash date has no
42
- * calendar to convert, so it uses its written month and day as its ordinal.
43
- *
44
- * ## `~` follows the convention {@link module:engine/note-dates} sets
45
- *
46
- * A date's `~` marks the value approximate without changing what it parses to,
47
- * so a renderer keeps the authored text and a comparison still has a number.
48
- * {@link parseAgeMagnitude} draws the same distinction for `age` — the tilde,
49
- * the magnitude — without routing through {@link module:engine/note-dates}
50
- * itself: `age` is a plain count of years, not a date, so it has no month, no
51
- * day, no era, and — unlike a date — no refusal of `0`. Reusing the date
52
- * grammar's month/day/era machinery here would accept an age nothing about the
53
- * field means, and refuse the one age an infant can have.
54
- *
55
- * @module
56
- */
14
+ /** Authored ages and setting-wide current-date age calculations. @module */
57
15
 
58
- import { worldInvariants } from "./calendar-notes.mjs";
16
+ import { DEFAULT_CURRENT_DATE } from "./current-date.mjs";
59
17
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
60
18
  import { parseNoteDate } from "./note-dates.mjs";
61
19
  import { reckoningContext } from "./reckoning-markers.mjs";
@@ -107,11 +65,9 @@ export function parseAgeMagnitude(raw) {
107
65
  *
108
66
  * @param {unknown} bornRaw - The being's authored `born`, or `undefined`/`null`
109
67
  * where it wrote none.
110
- * @param {unknown} presentRaw - The package's `data.present`, as
111
- * {@link module:engine/calendar-notes.worldInvariants} read it, or
112
- * `undefined`/`null` where the package states none.
68
+ * @param {unknown} presentRaw - The configured neutral current date.
113
69
  * @returns {number|null} The age, or `null` where `born` is `unknown` or
114
- * absent, where no present is declared, or where either date fails to parse.
70
+ * absent or future, or where either date fails to parse.
115
71
  */
116
72
  export function computeAge(bornRaw, presentRaw, dateContext = {}) {
117
73
  if (bornRaw === undefined || bornRaw === null) return null;
@@ -126,11 +82,15 @@ export function computeAge(bornRaw, presentRaw, dateContext = {}) {
126
82
  // A resolved marker supplies a canonical day. An unmarked slash date uses
127
83
  // its written month and day. Missing parts read as the first day that the
128
84
  // value can name.
129
- const bornOrdinal = born.canonicalDay ?? (born.month ?? 1) * 100 + (born.day ?? 1);
130
- const presentOrdinal = present.canonicalDay ?? (present.month ?? 1) * 100 + (present.day ?? 1);
85
+ const bornOrdinal =
86
+ born.canonicalDay ?? (born.month === null ? 1 : (born.month ?? 1) * 100 + (born.day ?? 1));
87
+ const presentOrdinal =
88
+ present.canonicalDay ??
89
+ (present.month === null ? 1 : (present.month ?? 1) * 100 + (present.day ?? 1));
131
90
  const notYetHappened = bornOrdinal > presentOrdinal;
132
91
 
133
- return present.canonicalYear - born.canonicalYear - (notYetHappened ? 1 : 0);
92
+ const age = present.canonicalYear - born.canonicalYear - (notYetHappened ? 1 : 0);
93
+ return age < 0 ? null : age;
134
94
  }
135
95
 
136
96
  /**
@@ -146,8 +106,7 @@ export function computeAge(bornRaw, presentRaw, dateContext = {}) {
146
106
  * itself untouched.
147
107
  *
148
108
  * @param {object} fm - A note's frontmatter, as freshly parsed.
149
- * @param {unknown} presentRaw - The package's declared present, or
150
- * `undefined`/`null` where it states none.
109
+ * @param {unknown} presentRaw - The configured neutral current date.
151
110
  * @returns {void}
152
111
  */
153
112
  export function applyComputedBeingAge(fm, presentRaw, dateContext = {}) {
@@ -171,63 +130,21 @@ export function applyComputedBeingAge(fm, presentRaw, dateContext = {}) {
171
130
  }
172
131
 
173
132
  /**
174
- * A being's `data.present`, read from every `place` note a batch of records or
175
- * frontmatter objects carries.
176
- *
177
- * Two shapes reach this by way of two thin adapters, because the callers that
178
- * need a present hold their notes two different ways: a flat index record
179
- * (`type` and `data` at the top level) and a raw walk's `{frontmatter}` pair.
180
- * Neither shape is the `{fm, type}` pair {@link worldInvariants} reads, so each
181
- * adapter states the one conversion its caller needs rather than asking every
182
- * caller to know it.
183
- *
184
- * @param {readonly object[]|undefined} records - Flat index records — a
185
- * content-index record, or anything else spreading a note's frontmatter to
186
- * its own top level.
187
- * @returns {unknown} The present, or `null` where no `place` note in the batch
188
- * declares one.
189
- */
190
- export function presentAmongRecords(records) {
191
- return worldInvariants({
192
- notes: (records ?? []).map((record) => ({ fm: record, type: record?.type })),
193
- }).present;
194
- }
195
-
196
- /**
197
- * The present read from a batch of raw frontmatter objects.
198
- *
199
- * @param {readonly (object|null|undefined)[]|undefined} frontmatters - One
200
- * entry per note walked, as parsed — never a record.
201
- * @returns {unknown} The present, or `null` where none is declared.
202
- */
203
- export function presentAmongFrontmatters(frontmatters) {
204
- return worldInvariants({
205
- notes: (frontmatters ?? []).map((fm) => ({ fm, type: fm?.type })),
206
- }).present;
207
- }
208
-
209
- /**
210
- * Check a being's authored `age` against what `born` and the package's present
211
- * compute.
133
+ * Check an authored age against the birth date and configured current date.
212
134
  *
213
135
  * Registered as `being`'s whole-note `check` in `note-vocabulary.mjs`, run the
214
136
  * way `checkPlace` and `checkCalendarNote` are — beside every other check the
215
137
  * closed `data:` container earns, with the same `index`.
216
138
  *
217
- * Two silences, both deliberate: beside `born: unknown` or an absent `born`
218
- * there is nothing to disagree with, and a package that declares no present
219
- * computes no age to disagree with either — both cases are read by
220
- * {@link computeAge} returning `null`, and neither says a word.
139
+ * An unknown or absent birth date supplies no computed age to compare.
221
140
  *
222
- * @param {object} note - The note, as the link index hands it over —
223
- * {@link module:engine/calendar-notes.worldInvariants}'s `index.notes` shape.
141
+ * @param {object} note - The note, as the link index hands it over.
224
142
  * @param {object} [opts]
225
- * @param {object} [opts.index] - The link index, holding the package's `place`
226
- * notes. Its absence skips the check, the way a reference check with no
227
- * index skips reporting every reference as dead.
143
+ * @param {object} [opts.index] - Calendar context for authored birth dates.
144
+ * @param {string} [opts.currentDate="720.1"] - The configured neutral current date.
228
145
  * @returns {object[]} Findings — at most one `warning`, naming both values.
229
146
  */
230
- export function checkBeingAge(note, { index } = {}) {
147
+ export function checkBeingAge(note, { index, currentDate = DEFAULT_CURRENT_DATE } = {}) {
231
148
  if (!isBeingType(note?.type ?? note?.fm?.type)) return [];
232
149
  const data = dataOf(note?.fm);
233
150
  if (!data) return [];
@@ -235,10 +152,7 @@ export function checkBeingAge(note, { index } = {}) {
235
152
  const bornRaw = data.born;
236
153
  const ageRaw = data.age;
237
154
  if (ageRaw === undefined || ageRaw === null || ageRaw === "") return [];
238
- if (!index) return [];
239
-
240
- const present = worldInvariants(index).present;
241
- const computed = computeAge(bornRaw, present, reckoningContext(index));
155
+ const computed = computeAge(bornRaw, currentDate, reckoningContext(index));
242
156
  if (computed === null) return [];
243
157
 
244
158
  const authored = parseAgeMagnitude(ageRaw);