@heroiclands/package-build 23.10.0 → 23.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +28 -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 +131 -45
  11. package/docs/authoring/first-note.md +10 -0
  12. package/docs/authoring/frontmatter.md +1 -1
  13. package/docs/authoring/links-and-markup.md +111 -6
  14. package/docs/commands.md +46 -2
  15. package/docs/configuration.md +21 -4
  16. package/docs/content-format.md +4 -2
  17. package/docs/design.md +19 -0
  18. package/docs/guides/book.md +11 -0
  19. package/docs/guides/site.md +7 -0
  20. package/docs/reference/format-details.md +82 -27
  21. package/docs/reference/note-types.md +0 -1
  22. package/docs/reference/theme-pages.md +1 -1
  23. package/docs/source-preview.md +172 -0
  24. package/engine/anchors.mjs +36 -4
  25. package/engine/base-compiler.mjs +7 -8
  26. package/engine/being-age.mjs +19 -105
  27. package/engine/calendar-notes.mjs +3 -25
  28. package/engine/compile-corpus.mjs +2 -1
  29. package/engine/content-format.yaml +0 -1
  30. package/engine/content-index.mjs +9 -9
  31. package/engine/content-links.mjs +41 -18
  32. package/engine/content-spans.mjs +54 -7
  33. package/engine/current-date.mjs +37 -0
  34. package/engine/definition-list.mjs +58 -0
  35. package/engine/frontmatter-lint.mjs +7 -2
  36. package/engine/helpers.mjs +6 -2
  37. package/engine/index.mjs +2 -0
  38. package/engine/infobox.mjs +1 -5
  39. package/engine/note-vocabulary.mjs +2 -2
  40. package/engine/npc-generate.mjs +7 -6
  41. package/engine/pdf-render.mjs +36 -3
  42. package/engine/preview-current-date.mjs +60 -0
  43. package/engine/site-build.mjs +5 -5
  44. package/engine/site-index.mjs +28 -6
  45. package/engine/site-preview.mjs +641 -325
  46. package/engine/web-wikilinks.mjs +33 -12
  47. package/engine/wikilink-syntax.mjs +65 -8
  48. package/engine/wikilinks.mjs +29 -11
  49. package/hugo-theme/layouts/_default/list.html +1 -1
  50. package/hugo-theme/layouts/blog/list.html +1 -1
  51. package/hugo-theme/layouts/blog-post/single.html +1 -1
  52. package/hugo-theme/layouts/partials/dictionary-lists.html +53 -0
  53. package/hugo-theme/layouts/partials/footer.html +2 -0
  54. package/hugo-theme/layouts/partials/page-body.html +1 -1
  55. package/hugo-theme/static/css/style.css +39 -9
  56. package/hugo-theme/static/js/inline-spoilers.js +23 -0
  57. package/package.json +8 -2
  58. package/types/content-config.d.mts +8 -0
  59. package/types/engine/anchors.d.mts +2 -1
  60. package/types/engine/base-compiler.d.mts +1 -1
  61. package/types/engine/being-age.d.mts +10 -44
  62. package/types/engine/calendar-notes.d.mts +2 -6
  63. package/types/engine/compile-corpus.d.mts +2 -1
  64. package/types/engine/content-index.d.mts +3 -1
  65. package/types/engine/content-spans.d.mts +7 -1
  66. package/types/engine/current-date.d.mts +9 -0
  67. package/types/engine/definition-list.d.mts +8 -0
  68. package/types/engine/frontmatter-lint.d.mts +6 -2
  69. package/types/engine/index.d.mts +2 -0
  70. package/types/engine/npc-generate.d.mts +2 -2
  71. package/types/engine/preview-current-date.d.mts +8 -0
  72. package/types/engine/site-index.d.mts +5 -1
  73. package/types/engine/site-preview.d.mts +40 -33
  74. package/types/engine/wikilink-syntax.d.mts +40 -3
package/docs/design.md CHANGED
@@ -21,6 +21,25 @@ An Address identifies a subject by package, system, type, and shortcode. It is i
21
21
 
22
22
  The content index uses JSON Lines. A record can retain the note's full frontmatter, anchors, and outgoing links, and streams without loading one giant JSON array. It is derived, disposable, and stable for a fixed source tree and toolchain version. SQL queries and editor navigation read that index rather than guessing from filenames.
23
23
 
24
+ ## Reference presentation and access
25
+
26
+ An indexed subject has one ordinary wikilink syntax regardless of its audience.
27
+ The renderer derives spoiler presentation from the actual destination, so an
28
+ author does not duplicate a privacy declaration on every mention. The `.show`
29
+ override reveals a particular reference without changing the destination's
30
+ audience. A separate inline `.secret` span marks authored text as a spoiler.
31
+
32
+ Concealment and access remain separate decisions. Public site and book builds
33
+ omit GM-only notes; a reference to an omitted destination carries no unusable
34
+ link. Foundry's private packs and page permissions govern destination access.
35
+ Web inline spoilers use a separate show/hide control beside their content, so
36
+ a revealed link is independently usable. Foundry inline spoilers use a CSS
37
+ redaction revealed on hover or keyboard focus; they have no native secret-section
38
+ permission behavior. Books ignore inline `.secret` and preserve normal text
39
+ formatting. A spoiler in published HTML remains readable in the source. See the
40
+ [authoring contract](authoring/links-and-markup.md#references-to-secret-subjects)
41
+ for syntax and surface presentation.
42
+
24
43
  ## Build boundaries
25
44
 
26
45
  The content pass reads `assets/content/` and addressable assets. It emits the content index, page source, and Foundry documents. Site rendering and book typesetting consume those outputs; their layout decisions do not become fields of a Foundry DataModel. Packaging stages only the files a Foundry manifest declares. The build checks emitted fields against the target system schema because Foundry can otherwise discard an unknown field without an error.
@@ -522,6 +522,17 @@ Anchors on headings are scoped to their entries, so two entries can use the
522
522
  same heading anchor. The build reports an unresolved destination while
523
523
  keeping the source available for inspection.
524
524
 
525
+ A reference to a GM-only note or secret heading prints normally. GM-only notes
526
+ remain omitted, and references to them print as text without a link. `.show`
527
+ does not add an omitted destination to the book. Inline `[text]{.secret}` spans
528
+ print as ordinary text, preserving emphasis, links, anchors and `.name` bold
529
+ styling; the book ignores their `.secret` class.
530
+
531
+ Secret sections and `:::secret` fences keep their visible contents in pale
532
+ lavender boxes (`#f2eefb`) with a purple left border and title (`#5c4b8a`). The
533
+ default title is **! Secret**; a fence can supply its own title. See
534
+ [secret references](../authoring/links-and-markup.md#references-to-secret-subjects).
535
+
525
536
  The [generated sections](../reference/format-details.md#generated-sections)
526
537
  an entry carries — **Within**, **Governed by**, **In song and story**, the
527
538
  event views and the rest — name notes the build chose rather than the author,
@@ -118,6 +118,13 @@ Wikilinks resolve through the address index. Each emitted page receives
118
118
  derived `related` entries for the links its author wrote and the backlinks
119
119
  other authors wrote to it.
120
120
 
121
+ A wikilink to a GM-only note or secret heading conceals its whole reference in
122
+ an inline show/hide spoiler. `.show` reveals the reference without changing the
123
+ destination's audience. GM-only pages remain omitted, so references to them
124
+ have no link; published secret sections retain theirs. Spoiler text remains in
125
+ the generated HTML and is readable in page source. See
126
+ [secret references](../authoring/links-and-markup.md#references-to-secret-subjects).
127
+
121
128
  What lies within a place, who governs it, what an affiliation governs, the
122
129
  works naming a note and the map from a place reach the page as
123
130
  [generated sections](../reference/format-details.md#generated-sections): H1
@@ -515,7 +515,10 @@ is declared, so a near miss is a finding that names what you probably meant.
515
515
 
516
516
  `gm` marks a note for GM access. It is excluded from public site pages, SQL
517
517
  tables on the public site, and the book. Foundry compiles it only to private
518
- compendiums. A regular note cannot link to it; a GM note can.
518
+ compendiums. Any note may reference it with an ordinary wikilink; the
519
+ reference is automatically a spoiler. The `.show` override reveals that
520
+ reference without changing the destination's audience or access permissions;
521
+ see [references to secret subjects](#references-to-secret-subjects).
519
522
 
520
523
  **`draft` marks unfinished content.** It does two things and nothing
521
524
  else: a link _into_ a draft note renders marked, and the note itself states that
@@ -1808,6 +1811,38 @@ name at every citation with no link edited. `[[x|]]` is labelled; `[[x]]` is not
1808
1811
  It is the label that is required, not a target: the anchor-only form above
1809
1812
  carries its pipe like every other link.
1810
1813
 
1814
+ #### References to secret subjects
1815
+
1816
+ Use `[[doc-divineactn|The Truth of Divine Action]]` for an indexed subject,
1817
+ including one with a GM-only audience. The renderer derives spoiler presentation
1818
+ from the resolved destination and conceals the whole reference, including its
1819
+ label. A public destination has no automatic concealment. An anchor inside secret
1820
+ content is a secret destination, including a same-note anchor reference. A
1821
+ link to the whole public note does not inherit a section's secrecy.
1822
+
1823
+ The website renders an inline show/hide spoiler; an omitted GM-only target
1824
+ retains its reference as text without a dead link. A published secret section
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
1831
+ still has no link.
1832
+
1833
+ An immediately following `.show` attribute explicitly reveals the reference:
1834
+
1835
+ ```markdown
1836
+ [[doc-divineactn|The Truth of Divine Action]]{.show}
1837
+ [[doc-magictruth|The Truth of Magic]]{.show}
1838
+ ```
1839
+
1840
+ This override changes the reference's presentation only. It does not publish a
1841
+ GM-only note, alter Foundry permissions or reveal the destination's content.
1842
+ It is redundant for public destinations. `[text]{.secret}` separately marks
1843
+ text itself as a spoiler; it declares no destination audience. See
1844
+ [inline spans](#inline-spans) for inline spoiler syntax and surface behavior.
1845
+
1811
1846
  #### Marking names in prose
1812
1847
 
1813
1848
  `package-build lint` reports name-marking warnings in authored body prose.
@@ -1815,7 +1850,10 @@ The warnings leave a valid package passing. Headings, frontmatter, tables,
1815
1850
  infoboxes, code, comments and embeds are outside this check.
1816
1851
 
1817
1852
  A name is explicit: a `[Name]{.name}` bracket span or an authored link label.
1818
- Use `.name` for names without indexed notes and links for names with notes.
1853
+ Use typed `.name` spans for unwritten subjects and ordinary wikilinks for
1854
+ indexed subjects, including private ones. Introduce another subject with its
1855
+ full canonical name at its first prose use; use plain text for the note's own
1856
+ subject and for later mentions and aliases.
1819
1857
  Plain prose is not searched for names. Wikilinks, ordinary Markdown links and
1820
1858
  reference links all supply names; a resolved empty wikilink label uses the
1821
1859
  note's full name. Images, code, footnote markers and bare autolinks without
@@ -1856,7 +1894,7 @@ An optional `data-type` on a `.name` span states what the named thing is:
1856
1894
 
1857
1895
  ```markdown
1858
1896
  [Ravenmoor]{.name data-type="place/settlement"}
1859
- [Aldric Venn]{.name data-type="being"}
1897
+ [Aldric Venn]{.name data-type="being/npc"}
1860
1898
  ```
1861
1899
 
1862
1900
  Use a declared [note type](note-types.md), optionally followed by `/` and one
@@ -2630,7 +2668,13 @@ local to a note; rendered markers are sequential numbers. A Foundry page
2630
2668
  collects its referenced footnotes in a Footnotes section at the bottom of that
2631
2669
  JournalEntryPage, while a book places them at the bottom of the page of
2632
2670
  reference. A term followed by `: definition` lines creates a definition list.
2633
- See the [authoring examples](../authoring/links-and-markup.md#footnotes-and-definition-lists).
2671
+ After a blank line, indent continuation paragraphs and blocks by four spaces
2672
+ to keep them in the same definition; indent a code block's opening fence,
2673
+ contents and closing fence. Another `:` starts a separate definition for the
2674
+ same term. See the [authoring examples](../authoring/links-and-markup.md#footnotes-and-definition-lists). Definition lists use dictionary hanging indents: bold, nonwrapping terms,
2675
+ normal definitions, a 150px shared column (100px at browser widths up to 600px),
2676
+ a `.75em` term gap and `.5em` entry spacing. A long term pushes only the first
2677
+ definition line beyond the shared column; continuation lines return to it.
2634
2678
 
2635
2679
  ```
2636
2680
  # Heading {#id .class1 .class2 attr="value"}
@@ -3483,6 +3527,27 @@ The watch wears [a _blue_ sash]{#watch-sash .uniform lang=en}.
3483
3527
 
3484
3528
  The exact `.name` class marks a name, with optional `data-type="type/subType"`;
3485
3529
  see [marking names in prose](#marking-names-in-prose) for the vocabulary and index.
3530
+ The exact `.secret` class marks the enclosed text as an inline spoiler:
3531
+
3532
+ ```markdown
3533
+ [The hidden **passage**]{.secret}
3534
+ [Aldric Venn]{.name .secret data-type="being/npc"}
3535
+ ```
3536
+
3537
+ The website provides an inline show/hide button and preserves interactive links
3538
+ inside the revealed content. Foundry renders a focusable, CSS-redacted inline
3539
+ span that reveals its text and nested content on hover or keyboard focus.
3540
+ Native Foundry secret sections retain their own controls. Books ignore `.secret`
3541
+ on inline spans, preserving their text, emphasis, links, anchors and `.name`
3542
+ bold styling. Secret sections and `:::secret` fences keep their visible labelled
3543
+ boxes. Published HTML retains the text, so opening the spoiler or inspecting the
3544
+ source reveals it.
3545
+
3546
+ This span has no effect on a destination's audience. Ordinary wikilinks derive
3547
+ spoilers from their resolved destinations; `.show` bypasses that automatic
3548
+ concealment without cancelling an enclosing authored `.secret`. An inline span
3549
+ does not change the scope of `.secret` headings or `:::secret` blocks, and it
3550
+ does not start or withhold a JournalEntryPage.
3486
3551
 
3487
3552
  Inline Markdown remains active inside the span. Bracket spans have lower priority than URL links, image links, reference links, footnotes and wikilinks. A bracketed form immediately preceded by `!`, followed by `(` or another `[`, or beginning with `[[` is not a span, even when the link is unresolved. Ordinary links retain their existing syntax. Escape a bracket when it should be literal, or put examples inside a code span or fence. A span identifier addresses that point in the note; it does not start a journal page.
3488
3553
 
@@ -4499,7 +4564,7 @@ three fields are built around.
4499
4564
  unrecorded. Both spellings of `died` mean dead.
4500
4565
  - **`age` is optional and is an override.** Written, it wins over anything the
4501
4566
  dates say. Unwritten beside a dated `born`, it is the age that follows from
4502
- the date and the package's declared present — see below. Unwritten beside
4567
+ the date and the configured current date — see below. Unwritten beside
4503
4568
  `born: unknown` or an absent `born`, the age is unknown.
4504
4569
 
4505
4570
  Most people in a setting do not know their own birth date, so an age beside
@@ -4512,22 +4577,15 @@ read it.
4512
4577
 
4513
4578
  #### The age a dated `born` computes to
4514
4579
 
4515
- A package states the day its setting stops on `data.present`, on its own
4516
- `world` or `celestial` `place` note — see _What a body states about itself_
4517
- under `type: place`. Against that date, a being with a dated `born` and no
4518
- authored `age` compiles the age it implies: the two dates compared, not the
4519
- two years subtracted, so a birthday that falls later in the year than the
4520
- present counts one less than the years alone would say. A package that
4521
- declares no present computes no age and says nothing about it — the same
4522
- silence a package with no calendar mechanism meets.
4523
-
4524
- **An authored `age` beside a dated `born` is expected to agree with what the
4525
- date computes.** Where it does not, `package-build lint` raises a warning
4526
- naming both values, so a stale age left behind after a `born` was corrected
4527
- does not go unnoticed. The warning is silent beside `born: unknown` or an
4528
- absent `born` — there is nothing there to disagree with — and silent wherever
4529
- the package declares no present, for the same reason no age is computed at
4530
- all.
4580
+ The root configuration's `currentDate` states the setting's neutral current
4581
+ date, defaulting to `"720.1"`. A being with a dated `born` and no authored `age`
4582
+ compiles the age implied by those dates. A birthday later in the year counts one
4583
+ less than the difference between the years. A year-only birth date uses day one.
4584
+ Unknown and absent birth dates supply no computed age.
4585
+
4586
+ An authored `age` beside a dated `born` is expected to agree with the configured
4587
+ current date. `package-build lint` warns when the values disagree. Authored ages,
4588
+ including zero and estimates such as `~34`, remain unchanged in built output.
4531
4589
 
4532
4590
  Use `born:` for a being's birth date.
4533
4591
 
@@ -6933,7 +6991,6 @@ level with no background prints nothing.
6933
6991
  | `year.hoursPerDay` | `number` | How many hours the day divides into |
6934
6992
  | `year.minutesPerHour` | `number` | How many minutes the hour divides into |
6935
6993
  | `year.secondsPerMinute` | `number` | How many seconds the minute divides into |
6936
- | `present` | `date` | The day the setting stops and play begins |
6937
6994
  | `body.diameterKm` | `number` | The body's diameter, in kilometres |
6938
6995
  | `body.orbitalRadiusKm` | `number` | How far it orbits from the body it circles, in kilometres |
6939
6996
  | `body.orbit` | `string` | The orbit's shape — `circular` means the cycle never varies |
@@ -6965,7 +7022,6 @@ about the world's year.
6965
7022
  data:
6966
7023
  world: { equatorialCircumferenceKm: 40000, surfaceGravityG: 1.0, axialTiltDegrees: 23.5 }
6967
7024
  year: { days: 365, hoursPerDay: 24, minutesPerHour: 60, secondsPerMinute: 60 }
6968
- present: 720
6969
7025
 
6970
7026
  # its moon — a note of its own, because a moon is a body and not a property
6971
7027
  data:
@@ -6983,14 +7039,13 @@ else wrote down.
6983
7039
  can say which one a calendar divides; a `moon` with no `year` anywhere is
6984
7040
  refused, because a cycle counted in days has no year to be measured against.
6985
7041
 
6986
- **`present` is the day an age is computed against**, written in the grammar a
6987
- note's dates are written in, so a setting whose present is best said in an era
6988
- says it that way.
7042
+ The root configuration's `currentDate` supplies the neutral date used for age
7043
+ calculations throughout the setting. Its default is `"720.1"`.
6989
7044
 
6990
7045
  **`world.axialTiltDegrees`, the circumference, the gravity, the moon's diameter
6991
7046
  and its orbital radius are read by nobody.** They are there so a reader knows
6992
7047
  what kind of world this is. What the build consumes is the year, the day's
6993
- divisions, the present, the moon's cycle and epoch, and whether its orbit is
7048
+ divisions, the moon's cycle and epoch, and whether its orbit is
6994
7049
  circular and inclined.
6995
7050
 
6996
7051
  #### 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).
@@ -41,7 +41,7 @@ import { scanPoetry } from "./content-poetry.mjs";
41
41
  import { scanSpans } from "./content-spans.mjs";
42
42
  import { slugify } from "./content-slug.mjs";
43
43
  import { scanFigures } from "./content-figures.mjs";
44
- import { scanHeadingAttributes } from "./heading-attributes.mjs";
44
+ import { withheldSections, scanHeadingAttributes } from "./heading-attributes.mjs";
45
45
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
46
46
 
47
47
  /**
@@ -67,11 +67,12 @@ export const ANCHOR_KINDS = Object.freeze(["prose", "event"]);
67
67
  * {@link module:engine/content-figures.scanFigures}.
68
68
  * @param {import("./content-session.mjs").ContentSyntax} [syntax] - Exact-text scanner projections.
69
69
  * @returns {Array<{slug: string, name: string, level: number, line: number,
70
- * kind: string}>} In document order.
70
+ * kind: string, secret?: boolean}>} In document order.
71
71
  */
72
72
  export function collectAnchors(body, bodyLine = 1, resolveRole, syntax) {
73
73
  const anchors = [];
74
- for (const heading of scanHeadingAttributes(body, bodyLine).headings) {
74
+ const headings = scanHeadingAttributes(body, bodyLine).headings;
75
+ for (const heading of headings) {
75
76
  if (!heading.id) continue;
76
77
  anchors.push({
77
78
  slug: heading.id,
@@ -91,9 +92,12 @@ export function collectAnchors(body, bodyLine = 1, resolveRole, syntax) {
91
92
  kind: "prose",
92
93
  });
93
94
  }
95
+ const secretBlocks = [];
94
96
  const blockAnchors = (source, firstLine) => {
95
97
  const sourceLines = source.split("\n");
96
98
  for (const block of (syntax ? syntax(source).blocks : scanBlocks(source)).blocks) {
99
+ if (block.name === "secret")
100
+ secretBlocks.push({ start: firstLine + block.start, end: firstLine + block.end });
97
101
  if (block.id)
98
102
  anchors.push({
99
103
  slug: slugify(block.id),
@@ -129,7 +133,8 @@ export function collectAnchors(body, bodyLine = 1, resolveRole, syntax) {
129
133
  kind: "prose",
130
134
  });
131
135
  }
132
- for (const span of (syntax ? syntax(body).spans : scanSpans(body)).spans) {
136
+ const spans = (syntax ? syntax(body).spans : scanSpans(body)).spans;
137
+ for (const span of spans) {
133
138
  if (span.id)
134
139
  anchors.push({
135
140
  slug: slugify(span.id),
@@ -139,6 +144,33 @@ export function collectAnchors(body, bodyLine = 1, resolveRole, syntax) {
139
144
  kind: "prose",
140
145
  });
141
146
  }
147
+ const sections =
148
+ headings.some((heading) => heading.classes.includes("secret")) ?
149
+ withheldSections(body).sections
150
+ : [];
151
+ const secretSpans = spans.filter((span) =>
152
+ span.attrs.some(([key, value]) => key === "class" && value.split(/\s+/).includes("secret")),
153
+ );
154
+ const secretSpanAnchors = new Set(
155
+ spans
156
+ .filter(
157
+ (target) =>
158
+ target.id &&
159
+ secretSpans.some(
160
+ (parent) => parent.start <= target.start && parent.end >= target.end,
161
+ ),
162
+ )
163
+ .map((span) => span.id),
164
+ );
165
+ for (const anchor of anchors) {
166
+ const line = anchor.line - bodyLine;
167
+ if (
168
+ sections.some((section) => line >= section.start && line < section.end) ||
169
+ secretBlocks.some((block) => anchor.line >= block.start && anchor.line <= block.end) ||
170
+ secretSpanAnchors.has(anchor.slug)
171
+ )
172
+ anchor.secret = true;
173
+ }
142
174
  return anchors.sort((a, b) => a.line - b.line);
143
175
  }
144
176
 
@@ -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