@heroiclands/package-build 22.11.0 → 22.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.12.1
4
+
5
+ ### Patch Changes
6
+
7
+ **Content authoring**
8
+
9
+ - Image embeds and Markdown images accept named size hints, so notes can carry the intended size while their displayed appearance stays the same.
10
+
11
+ ## 22.12.0
12
+
13
+ ### Minor Changes
14
+
15
+ - **References:** An Address in note data that names no local or declared dependency target now reports an error; missing artwork keeps its fallback warning.
16
+
3
17
  ## 22.11.0
4
18
 
5
19
  ### Minor Changes
package/CONTENT.md CHANGED
@@ -1249,6 +1249,12 @@ somewhere other than where they were made, or not at all:
1249
1249
  reference answered by a vendored manifest lands exactly as it would in a
1250
1250
  wikilink. `--no-references` turns this one off for a tree whose cross-package
1251
1251
  references it cannot see.
1252
+ - **Dead Address reference** — an Address-valued `data:` field naming no local
1253
+ note, asset or entry in a declared dependency index. The finding names the
1254
+ field and the complete Address it names. A short form means this package;
1255
+ another package requires the full Address. `--no-references` also turns off
1256
+ this target check. Art slots report unresolved art through their own fallback
1257
+ warning.
1252
1258
 
1253
1259
  **A schema says what a note may _write_, not what the compiler emits.** Those
1254
1260
  are different, and the difference is the whole calibration of the check: a note
package/docs/api.md CHANGED
@@ -935,16 +935,17 @@ Raw HTML in a note's prose, reported. **A note is markdown.** What markdown cann
935
935
 
936
936
  ### `engine.contentImages`
937
937
 
938
- An image saying how wide it is and where it sits. A markdown image carries no indication of either, so each of the three surfaces decides for itself and the author — who is the one who knows — has no way to say. A directive in the curly-attribute convention Pandoc and Kramdown use closes that, in two closed vocabularies: a width class, and a `float:` position.
938
+ An image saying how wide it is and where it sits. A markdown image carries no indication of either, so each of the three surfaces decides for itself and the author — who is the one who knows — has no way to say. A directive in the curly-attribute convention Pandoc and Kramdown use closes that, in three closed vocabularies: a width class, a named `size:`, and a `float:` position. The named size is accepted without affecting display.
939
939
 
940
940
  | Export | Signature | Returns | Use it when |
941
941
  | --------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
942
942
  | `IMAGE_CLASSES` | `const IMAGE_CLASSES` | — | The width classes an image may carry, and what each means to a renderer. |
943
+ | `IMAGE_SIZES` | `const IMAGE_SIZES` | — | The named sizes an image directive accepts without applying a display size. |
943
944
  | `IMAGE_FLOATS` | `const IMAGE_FLOATS` | — | The `float:` positions an image may take, and where each puts it. |
944
945
  | `IMAGE_FIGURE_CLASS` | `const IMAGE_FIGURE_CLASS` | — | The class every figure carries, whatever its width or position. |
945
946
  | `IMAGE_PATTERN` | `const IMAGE_PATTERN` | — | A markdown image, with the directive it may carry. |
946
947
  | `imageSourceProblem` | `function imageSourceProblem(src)` | {string} The problem, as a finding's sentence, or `""`. | What is wrong with an image's address, or `""` when nothing is. |
947
- | `parseImageDirective` | `function parseImageDirective(raw)` | {{classes: string[], float: string, problems: string[]}} What was written, and what cannot be honoured. | Read the directive on an image. |
948
+ | `parseImageDirective` | `function parseImageDirective(raw)` | {{classes: string[], size: string, float: string, problems: string[]}} What was written, and what cannot be honoured. | Read the directive on an image. |
948
949
  | `figureClasses` | `function figureClasses(directive)` | {string} A space-separated class list. | The classes a figure carries, from a parsed directive. |
949
950
  | `escapeHtml` | `function escapeHtml(text)` | {string} The same value, safe in markup. | Text going inside an HTML attribute or between tags. |
950
951
  | `imageFigureHtml` | `function imageFigureHtml(image)` | {string} The figure, as one HTML block. | One image as the `<figure>` both HTML surfaces render. |
@@ -1315,6 +1315,12 @@ Address-keyed relation maps hold typed targets and standing values. Only generat
1315
1315
  output serializes these properties to Address strings. Runtime Shortcodes,
1316
1316
  Foundry UUIDs, file paths and URLs keep their own representations.
1317
1317
 
1318
+ An Address-valued `data:` field must name a target in this package or a declared
1319
+ dependency index. `content-build lint` reports a missing target as an error at
1320
+ the value or map key that names it. A stub counts as a target for data even
1321
+ though it publishes no page. Art slots use their own missing-art warning and
1322
+ document fallback.
1323
+
1318
1324
  #### An omitted segment defaults from where the link is written
1319
1325
 
1320
1326
  It is **not** a wildcard and resolution is not a search. Every short form expands
@@ -1854,15 +1860,16 @@ immediately after the closing `]]`, with no space:
1854
1860
 
1855
1861
  ```markdown
1856
1862
  ![[branwldrgr|Brànwâal Dôrgaar]]{float: top-left}
1863
+ ![[branwldrgr|Brànwâal Dôrgaar]]{size: medium, float: top-left}
1857
1864
  ![[thalornamap|Map of Thalorna]]{.full-width}
1858
1865
  ![[thalornamap|Map of Thalorna]]{.full-width, float: top-left}
1859
1866
  ![[thalornamap|Map of Thalorna]]{float: top-left, .full-width}
1860
1867
  ```
1861
1868
 
1862
- **The same parser reads both**, so an embed takes a width class, a `float:`, or
1863
- both together in either order, comma-separated — everything
1869
+ **The same parser reads both**, so an embed takes a width class, a `size:`, a
1870
+ `float:`, or a combination in any order, comma-separated — everything
1864
1871
  [an image's directive](#width-is-a-class-and-the-ordinary-width-carries-no-marker)
1865
- takes and nothing beyond it. The width and position vocabularies are the ones
1872
+ takes and nothing beyond it. The width, size, and position vocabularies are the ones
1866
1873
  above, and they are closed here for the same reason: a directive that is quietly
1867
1874
  ignored looks exactly like one that worked, and a directive holding a problem is
1868
1875
  not honoured in part either.
@@ -2117,6 +2124,7 @@ part of this format follows.
2117
2124
 
2118
2125
  ```markdown
2119
2126
  ![[branwldrgr|Brànwâal Dôrgaar]]{float: top-left}
2127
+ ![[branwldrgr|Brànwâal Dôrgaar]]{size: medium, float: top-left}
2120
2128
 
2121
2129
  ![Map of Thalorna](images/maps/thalorna.webp){.full-width}
2122
2130
  ```
@@ -2140,13 +2148,30 @@ place for those words and this is it.
2140
2148
  | `.full-width` | the full page | full content width | full page width |
2141
2149
 
2142
2150
  The simple case needs no spelling, which is why the ordinary width has no name.
2143
- A third width joins this table as a name.
2144
2151
 
2145
2152
  **No pixel values, ever.** A number means something in a browser and nothing
2146
2153
  coherent in print, and a directive carrying a class and a dimension at once
2147
2154
  gives one question two answers with no rule for which wins. `{width=800}` is
2148
2155
  refused.
2149
2156
 
2157
+ ##### Named size is `size:`
2158
+
2159
+ | `size` value | display |
2160
+ | ------------ | ------------------------ |
2161
+ | `auto` | same as omitting `size:` |
2162
+ | `small` | same as omitting `size:` |
2163
+ | `medium` | same as omitting `size:` |
2164
+ | `large` | same as omitting `size:` |
2165
+ | `xlarge` | same as omitting `size:` |
2166
+ | `full-width` | same as omitting `size:` |
2167
+
2168
+ The parser accepts and records these names for Markdown images and embedded
2169
+ assets. **A `size:` value does not change the displayed dimensions** on the
2170
+ website, in Foundry, or in the book. With no `size:`, the parser records `auto`;
2171
+ the existing rendering rules apply. In particular, book images occupy a column
2172
+ unless they carry `.full-width`. A `size:` may sit before or after `float:` in
2173
+ the same directive, and neither changes the other's behavior.
2174
+
2150
2175
  ##### Position is `float:`
2151
2176
 
2152
2177
  | `float` value | book | website and Foundry journal |
@@ -2176,7 +2201,7 @@ A `.full-width` image that **also states a `float:`** is asking for a float and
2176
2201
  gets one, with no break before it. A float is placed where it fits, which may be
2177
2202
  the next page.
2178
2203
 
2179
- ##### Both vocabularies are closed
2204
+ ##### All three vocabularies are closed
2180
2205
 
2181
2206
  `{.fullwidth}`, `{.full_width}`, `{width=800}` and `{float: middle}` are
2182
2207
  refused, located by file, line and column, and the build fails. An unrecognised
@@ -2188,8 +2213,8 @@ even in the part that parsed: a half-honoured directive is the same silent
2188
2213
  failure in a smaller costume. The image keeps its braces and renders them as
2189
2214
  literal text, so the mistake is visible on the page as well as in the log.
2190
2215
 
2191
- **Nothing else reaches emitted markup.** The two vocabularies and the address
2192
- are all that leave a note; there is no `style`, no `id` and no arbitrary
2216
+ **Nothing else reaches emitted markup.** The width and position classes and the
2217
+ address are all that leave a note; there is no `style`, no `id` and no arbitrary
2193
2218
  attribute, because data is never compiled into markup here. An address carries
2194
2219
  a scheme of `http:` or `https:`, or none at all.
2195
2220
 
@@ -3015,9 +3040,7 @@ If `sohl` is present, this becomes an `affliction` item.
3015
3040
  | `subType` | `system.subType` | NA |
3016
3041
  | `data.transmission` | `system.transmission` | NA |
3017
3042
  | `data.outcome` | `system.outcome` | NA |
3018
- | `data.healingRate` | `system.healingRateBase` | NA |
3019
3043
  | `data.contagionIndex` | `system.contagionIndexBase` | NA |
3020
- | `data.outcomeTraumas` | `system.outcomeTraumas` | NA |
3021
3044
  | `data.onsetDurationFormula` | `system.onsetDurationFormula` | NA |
3022
3045
  | `data.onsetDurationBase` | `system.onsetDurationBase` | NA |
3023
3046
  | `data.healingCheckDurationFormula` | `system.healingCheckDurationFormula` | NA |
@@ -3055,6 +3078,10 @@ that writes one fails the build. They are world times, and `0` is a valid one,
3055
3078
  so there is no blank a note could write either; leave them out and the data
3056
3079
  model's `null` stands.
3057
3080
 
3081
+ `data.healingRate` and `data.outcomeTraumas` are page data. SoHL reads an
3082
+ affliction’s base healing rate from `sohl.healingRateBase`. Its item declaration
3083
+ does not read `data.outcomeTraumas`.
3084
+
3058
3085
  ### type: armorgear
3059
3086
 
3060
3087
  Note: `data.quantity` may not be specified. Quantity is always 1.
@@ -3129,7 +3156,9 @@ if a `sohl` property is present, a SoHL item of type "concoctiongear" will be cr
3129
3156
  | `data.value` | `system.valueBase` | NA |
3130
3157
  | `data.quality` | `system.qualityBase` | NA |
3131
3158
  | `data.durability` | `system.durabilityBase` | NA |
3132
- | `data.quantity` | `system.quantity` | NA |
3159
+
3160
+ `data.quantity` is page data for a concoction. SoHL sets `system.quantity` to
3161
+ `1` for each compendium item.
3133
3162
 
3134
3163
  ### type: containergear
3135
3164
 
@@ -3142,19 +3171,21 @@ Note: `data.quantity` may not be specified; quantity is always set to 1.
3142
3171
  | `value` | `number` | Gear value |
3143
3172
  | `quality` | `number` | Gear quality |
3144
3173
  | `durability` | `number` | Gear durability |
3145
- | `capacity` | `number` | Container capacity (in lbs) |
3174
+ | `capacity` | `number` | HM3 container capacity (in lbs) |
3146
3175
 
3147
3176
  if a `sohl` property is present, a SoHL item of type "containergear" will be created.
3148
3177
 
3149
3178
  if a `hm3` property is present, an HM3 item of type "containergear" will be created.
3150
3179
 
3151
- | shared source | → sohl | → hm3 |
3152
- | ----------------- | ------------------------ | --------------------- |
3153
- | `data.weight` | `system.weightBase` | `system.weight` |
3154
- | `data.value` | `system.valueBase` | `system.value` |
3155
- | `data.quality` | `system.qualityBase` | NA |
3156
- | `data.durability` | `system.durabilityBase` | NA |
3157
- | `data.capacity` | `system.maxCapacityBase` | `system.capacity.max` |
3180
+ | shared source | → sohl | → hm3 |
3181
+ | ----------------- | ----------------------- | --------------------- |
3182
+ | `data.weight` | `system.weightBase` | `system.weight` |
3183
+ | `data.value` | `system.valueBase` | `system.value` |
3184
+ | `data.quality` | `system.qualityBase` | NA |
3185
+ | `data.durability` | `system.durabilityBase` | NA |
3186
+ | `data.capacity` | NA | `system.capacity.max` |
3187
+
3188
+ SoHL reads a container’s capacity in pounds from `sohl.maxCapacity`.
3158
3189
 
3159
3190
  ### type: miscgear
3160
3191
 
@@ -3165,7 +3196,7 @@ if a `hm3` property is present, an HM3 item of type "containergear" will be crea
3165
3196
  | `value` | `number` | Gear value |
3166
3197
  | `quality` | `number` | Gear quality |
3167
3198
  | `durability` | `number` | Gear durability |
3168
- | `quantity` | `number` | Gear quantity (default: 1) |
3199
+ | `quantity` | `number` | HM3 gear quantity (default: 1) |
3169
3200
 
3170
3201
  if a `sohl` property is present, a SoHL item of type "miscgear" will be created.
3171
3202
 
@@ -3177,7 +3208,9 @@ if a `hm3` property is present, an HM3 item of type "miscgear" will be created.
3177
3208
  | `data.value` | `system.valueBase` | `system.value` |
3178
3209
  | `data.quality` | `system.qualityBase` | NA |
3179
3210
  | `data.durability` | `system.durabilityBase` | NA |
3180
- | `data.quantity` | `system.quantity` | `system.quantity` |
3211
+ | `data.quantity` | NA | `system.quantity` |
3212
+
3213
+ SoHL sets `system.quantity` to `1` for each compendium item.
3181
3214
 
3182
3215
  ### type: mystery
3183
3216
 
@@ -3213,10 +3246,12 @@ if a `sohl` property is present, a SoHL item of type "mystery" will be created.
3213
3246
  | --------------------- | ----------------------- | ----- |
3214
3247
  | `subType` | `system.subType` | NA |
3215
3248
  | `data.skillAptitudes` | `system.skillAptitudes` | NA |
3216
- | `data.level` | `system.levelBase` | NA |
3217
3249
  | `data.charges.value` | `system.charges.value` | NA |
3218
3250
  | `data.charges.max` | `system.charges.max` | NA |
3219
3251
 
3252
+ `data.level` is page data for a mystery. SoHL reads the item’s level from
3253
+ `sohl.levelBase`.
3254
+
3220
3255
  ### type: mysticalability
3221
3256
 
3222
3257
  **subType**:
@@ -3250,13 +3285,15 @@ If an `hm3` property is present, an HM3 item is created, and `hm3.type` states w
3250
3285
  `sohl.system` as shortcodes and have no shared source; `data.assocSkill` and
3251
3286
  `data.assocAffiliation` name the same two notes as Addresses, for the page.
3252
3287
 
3253
- | shared source | → sohl | → hm3 |
3254
- | -------------------- | ------------------------- | ------------- |
3255
- | `subType` | `system.subType` | **see above** |
3256
- | `data.masteryLevel` | `system.masteryLevelBase` | NA |
3257
- | `data.level` | `system.levelBase` | NA |
3258
- | `data.charges.value` | `system.charges.value` | NA |
3259
- | `data.charges.max` | `system.charges.max` | NA |
3288
+ | shared source | → sohl | → hm3 |
3289
+ | -------------------- | ---------------------- | ------------- |
3290
+ | `subType` | `system.subType` | **see above** |
3291
+ | `data.charges.value` | `system.charges.value` | NA |
3292
+ | `data.charges.max` | `system.charges.max` | NA |
3293
+
3294
+ `data.masteryLevel` and `data.level` are page data for an ability. SoHL reads
3295
+ the item’s base mastery and level from `sohl.masteryLevelBase` and
3296
+ `sohl.levelBase`.
3260
3297
 
3261
3298
  ### type: projectilegear
3262
3299
 
@@ -3276,7 +3313,7 @@ If an `hm3` property is present, an HM3 item is created, and `hm3.type` states w
3276
3313
  | `value` | `number` | Gear value |
3277
3314
  | `quality` | `number` | Gear quality |
3278
3315
  | `durability` | `number` | Gear durability |
3279
- | `quantity` | `number` | Gear quantity (default: 1) |
3316
+ | `quantity` | `number` | HM3 gear quantity (default: 1) |
3280
3317
 
3281
3318
  if a `sohl` property is present, a SoHL item of type "projectilegear" will be created.
3282
3319
 
@@ -3293,7 +3330,9 @@ HM3 side while remaining distinct on the SoHL side.
3293
3330
  | `data.value` | `system.valueBase` | `system.value` |
3294
3331
  | `data.quality` | `system.qualityBase` | NA |
3295
3332
  | `data.durability` | `system.durabilityBase` | NA |
3296
- | `data.quantity` | `system.quantity` | `system.quantity` |
3333
+ | `data.quantity` | NA | `system.quantity` |
3334
+
3335
+ SoHL sets `system.quantity` to `1` for each compendium item.
3297
3336
 
3298
3337
  ### type: skill
3299
3338
 
@@ -3313,7 +3352,7 @@ HM3 side while remaining distinct on the SoHL side.
3313
3352
  | `data` property | Values | Description |
3314
3353
  | ------------------ | --------- | ------------------------------------------ |
3315
3354
  | `templatePriority` | `number` | Template priority, _null_ = not a template |
3316
- | `masteryLevel` | `number` | Mastery Level |
3355
+ | `masteryLevel` | `number` | HM3 mastery level |
3317
3356
  | `parentSkill` | `Address` | Parent skill this skill specializes |
3318
3357
 
3319
3358
  if a `sohl` property is present, a SoHL item of type "skill" will be created.
@@ -3326,10 +3365,12 @@ Note: `hm3.system.type` (skill types) use the values "Craft", "Physical", "Commu
3326
3365
  skill this one specializes, and has no shared source; `data.parentSkill` names that
3327
3366
  skill as an Address, for the page.
3328
3367
 
3329
- | shared source | → sohl | → hm3 |
3330
- | ------------------- | ------------------------- | --------------------- |
3331
- | `subType` | `system.subType` | See notes above |
3332
- | `data.masteryLevel` | `system.masteryLevelBase` | `system.masteryLevel` |
3368
+ | shared source | → sohl | → hm3 |
3369
+ | ------------------- | ---------------- | --------------------- |
3370
+ | `subType` | `system.subType` | See notes above |
3371
+ | `data.masteryLevel` | NA | `system.masteryLevel` |
3372
+
3373
+ SoHL reads a skill’s base mastery from `sohl.masteryLevelBase`.
3333
3374
 
3334
3375
  ### type: trauma
3335
3376
 
@@ -332,6 +332,18 @@ an error. A `data:` block and a `sohl:` / `hm3:` block are closed: a misspelled
332
332
  key there is a finding that names the key you meant.
333
333
  [`content-format.md`](content-format.md) is the specification.
334
334
 
335
+ An image stands in its own paragraph. Its directive accepts a named size and a
336
+ position together:
337
+
338
+ ```markdown
339
+ ![[icon-anubis|Anubis]]{size: medium, float: top-left}
340
+ ```
341
+
342
+ The accepted sizes are `auto`, `small`, `medium`, `large`, `xlarge`, and
343
+ `full-width`. Omitting `size:` means `auto`. The build checks the name, but
344
+ `size:` does not change the displayed dimensions on the website, in Foundry, or
345
+ in the book. See [Images](content-format.md#images) for the rendering rules.
346
+
335
347
  ## Step 6 — check the tree
336
348
 
337
349
  Four checks, and they are quick enough to run continuously while authoring.
@@ -19,14 +19,15 @@
19
19
  * ![Brànwâal Dôrgaar](images/beings/branwldrgr-portrait.webp){float: top-left}
20
20
  * ![Map of Thalorna](images/map.webp){.full-width}
21
21
  *
22
- * ## Two closed vocabularies, and closed is the point
22
+ * ## Three closed vocabularies, and closed is the point
23
23
  *
24
24
  * **Width is a class**, and the ordinary width carries no marker at all — the
25
25
  * simple case needs no spelling. {@link IMAGE_CLASSES} holds the one class
26
26
  * there is. **Position is `float:`**, and {@link IMAGE_FLOATS} holds the five
27
- * values it takes.
27
+ * values it takes. **Named size is `size:`**, and {@link IMAGE_SIZES} holds its
28
+ * values. Size is parsed but does not change the figure's current presentation.
28
29
  *
29
- * Both are closed, and an unrecognised value is **refused with a located
30
+ * All three are closed, and an unrecognised value is **refused with a located
30
31
  * diagnostic** rather than ignored. Ignoring is the failure worth preventing:
31
32
  * `{.fullwidth}` rendering as an ordinary image looks exactly like a directive
32
33
  * that worked, so the author publishes a page that is not the one they asked
@@ -36,8 +37,7 @@
36
37
  *
37
38
  * **No pixel values.** A number means something in a browser and nothing
38
39
  * coherent in print, and a directive carrying both a class and a dimension
39
- * gives one question two answers with no rule for which wins. A third width
40
- * joins the vocabulary as a name.
40
+ * gives one question two answers with no rule for which wins.
41
41
  *
42
42
  * **Not `markdown-it-attrs`.** It is a general attribute injector: it will set
43
43
  * `style`, `id` or `onclick`, and those reach emitted markup on the website and
@@ -105,6 +105,16 @@ export const IMAGE_CLASSES = Object.freeze({
105
105
  },
106
106
  });
107
107
 
108
+ /** The named sizes an image directive accepts without applying a display size. */
109
+ export const IMAGE_SIZES = Object.freeze([
110
+ "auto",
111
+ "small",
112
+ "medium",
113
+ "large",
114
+ "xlarge",
115
+ "full-width",
116
+ ]);
117
+
108
118
  /**
109
119
  * The `float:` positions an image may take, and where each puts it.
110
120
  *
@@ -213,16 +223,18 @@ export function imageSourceProblem(src) {
213
223
  *
214
224
  * Values are validated here rather than at the point of rendering, so a mistake
215
225
  * is one finding with a position rather than three surfaces quietly drawing
216
- * something else. A directive that holds a problem yields **no** class and no
226
+ * something else. A directive that holds a problem yields **no** class, size or
217
227
  * float: a half-honoured directive is the silent failure in a smaller costume.
218
228
  *
219
229
  * @param {string} [raw] - The text between the braces, braces included or not.
220
- * @returns {{classes: string[], float: string, problems: string[]}} What was
230
+ * @returns {{classes: string[], size: string, float: string, problems: string[]}} What was
221
231
  * written, and what cannot be honoured.
222
232
  */
223
233
  export function parseImageDirective(raw) {
224
234
  /** @type {string[]} */
225
235
  const classes = [];
236
+ let size = "auto";
237
+ let sizeSeen = false;
226
238
  let float = "";
227
239
  /** @type {string[]} */
228
240
  const problems = [];
@@ -230,7 +242,7 @@ export function parseImageDirective(raw) {
230
242
  const inner = String(raw ?? "")
231
243
  .replace(/^\{/, "")
232
244
  .replace(/\}$/, "");
233
- if (!inner.trim()) return { classes, float, problems };
245
+ if (!inner.trim()) return { classes, size, float, problems };
234
246
 
235
247
  // Split on commas, not whitespace: `float: top-left` is one pair with a
236
248
  // space in it, and the space after the colon is the spelling people write.
@@ -259,17 +271,32 @@ export function parseImageDirective(raw) {
259
271
  const colon = part.indexOf(":");
260
272
  if (colon === -1) {
261
273
  problems.push(
262
- `\`${part}\` is neither a width class nor \`float: <position>\` — an ` +
263
- "image states its width as a class and its position as `float:`, " +
274
+ `\`${part}\` is neither a width class nor \`size: <name>\` or ` +
275
+ "`float: <position>` — an image states its width as a class and its position as `float:`, " +
264
276
  "and it states no dimensions at all",
265
277
  );
266
278
  continue;
267
279
  }
268
280
  const key = part.slice(0, colon).trim();
269
281
  const value = part.slice(colon + 1).trim();
282
+ if (key === "size") {
283
+ if (!IMAGE_SIZES.includes(value)) {
284
+ problems.push(
285
+ `\`size: ${value}\` is not a size — the ones there are: ${IMAGE_SIZES.join(", ")}`,
286
+ );
287
+ continue;
288
+ }
289
+ if (sizeSeen) {
290
+ problems.push("`size:` is written twice, and an image has one named size");
291
+ continue;
292
+ }
293
+ size = value;
294
+ sizeSeen = true;
295
+ continue;
296
+ }
270
297
  if (key !== "float") {
271
298
  problems.push(
272
- `\`${key}\` is not an image attribute — \`float\` is the only one, ` +
299
+ `\`${key}\` is not an image attribute — use \`size\` or \`float\`, ` +
273
300
  "and width is a class rather than an attribute",
274
301
  );
275
302
  continue;
@@ -292,8 +319,8 @@ export function parseImageDirective(raw) {
292
319
  problems.push("an image states one width, and this states more than one");
293
320
  }
294
321
  // Nothing partial: a directive with a problem in it is not honoured at all.
295
- if (problems.length) return { classes: [], float: "", problems };
296
- return { classes, float, problems };
322
+ if (problems.length) return { classes: [], size: "auto", float: "", problems };
323
+ return { classes, size, float, problems };
297
324
  }
298
325
 
299
326
  /**
@@ -594,7 +621,7 @@ export function lintContentImages(contentBase, { skipDirectories = [], config }
594
621
  * **Hugo is handed markdown, not a rendered page.** The site emitter writes a
595
622
  * note's body through verbatim, so a `{…}` directive left in it reaches the
596
623
  * page as its own literal braces: Goldmark's block-attribute parser is off, and
597
- * turning it on would accept `style` and `id` alongside the two vocabularies,
624
+ * turning it on would accept `style` and `id` alongside the closed vocabularies,
598
625
  * which is the injection surface this rule exists to avoid. So the directive is
599
626
  * resolved here, into markup Goldmark passes through.
600
627
  *
@@ -68,7 +68,7 @@ import { positionInFrontmatter, positionOfFrontmatterPath } from "./diagnostics.
68
68
  import { pathnameProblem } from "./pathnames.mjs";
69
69
  import { checkHomepageAddressFields } from "./homepage.mjs";
70
70
  import { RETIRED_TYPES, RENAMED_TYPES, currentType, renamedTypeMessage } from "./ids.mjs";
71
- import { parseAddress, acceptsType } from "./address.mjs";
71
+ import { parseAddress, acceptsType, renderAddress } from "./address.mjs";
72
72
  import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
73
73
  import { isAddressSegment } from "./address-charset.mjs";
74
74
  // The one place the "every pack not named" key is spelled. Imported rather
@@ -421,7 +421,7 @@ function dataBlock(fm) {
421
421
  * was loaded to recognise it would be worse than not checking.
422
422
  * @returns {object[]} Findings.
423
423
  */
424
- function checkDataContainer(note, { type, fields, packs, addressContext }) {
424
+ function checkDataContainer(note, { type, fields, packs, addressContext, index }) {
425
425
  const findings = [];
426
426
  const { present, entries, malformed } = dataBlock(note.fm ?? {});
427
427
  if (!present) return findings;
@@ -504,7 +504,7 @@ function checkDataContainer(note, { type, fields, packs, addressContext }) {
504
504
  });
505
505
  continue;
506
506
  }
507
- findings.push(...checkDataReferences(note, field, value, segments, addressContext));
507
+ findings.push(...checkDataReferences(note, field, value, segments, addressContext, index));
508
508
  // A `scalar-or-map` written in its map form is checked entry by entry,
509
509
  // because that is the correction an author has to make: one key's
510
510
  // value, not the whole map. Quoting the map back would name every
@@ -521,15 +521,16 @@ function checkDataContainer(note, { type, fields, packs, addressContext }) {
521
521
  }
522
522
 
523
523
  /**
524
- * Validate typed entries without resolving their targets.
524
+ * Validate typed entries and resolve declared non-art Address targets.
525
525
  * @param {object} note - The authored note.
526
526
  * @param {object} field - Its data field declaration.
527
527
  * @param {unknown} value - The field value.
528
528
  * @param {string[]} segments - The field's path under `data`.
529
529
  * @param {object} context - The builder's Address context.
530
+ * @param {object} [index] - The local and declared dependency Address index.
530
531
  * @returns {object[]} Located findings.
531
532
  */
532
- function checkDataReferences(note, field, value, segments, context) {
533
+ function checkDataReferences(note, field, value, segments, context, index) {
533
534
  const checks = [];
534
535
  if (field.kind === "address") checks.push({ value, path: [], kind: "address" });
535
536
  if (field.entryKind) {
@@ -573,10 +574,16 @@ function checkDataReferences(note, field, value, segments, context) {
573
574
  let reason;
574
575
  if (!matchesKind(check.value, check.kind, defaults))
575
576
  reason = `should be ${check.kind === "address" ? "an Address" : "a Shortcode"}`;
576
- else if (check.kind === "address" && accepted) {
577
+ else if (check.kind === "address") {
577
578
  const tuple = parseAddress(check.value, defaults, { declared: true });
578
- if (!acceptsType(tuple, accepted))
579
+ if (accepted && !acceptsType(tuple, accepted))
579
580
  reason = `accepts ${accepted.join(" or ")}, not ${tuple.type}`;
581
+ else if (
582
+ index?.addressHit &&
583
+ !ART_SLOTS.some((slot) => slot.key === field.name) &&
584
+ !index.addressHit(renderAddress(tuple))
585
+ )
586
+ reason = `names ${renderAddress(tuple)}, which does not resolve in this package or its declared dependencies`;
580
587
  }
581
588
  if (!reason) continue;
582
589
  const path = ["data", ...segments, ...check.path];
@@ -1519,6 +1526,7 @@ export function lintNote(
1519
1526
  fields,
1520
1527
  packs,
1521
1528
  addressContext: referenceContext,
1529
+ index,
1522
1530
  }),
1523
1531
  );
1524
1532
  findings.push(...checkSubType(note, { type, entry }));
@@ -124,15 +124,12 @@ export const HELD_SUBTYPES = Object.freeze(["settlement", "site", "structure"]);
124
124
  * system, only the shortcode {@link module:engine/address} would resolve it
125
125
  * to anyway.
126
126
  *
127
- * **The match this shortcode feeds is deliberately package-blind.** `places`
128
- * and `affiliations` merge this package's own notes with every fetched
129
- * dependency's into one shortcode space, first declaration wins — the way a
130
- * dependency's generic geography attaches to whatever a consumer calls the
131
- * same shortcode, which is the point of _A dependency's places and
132
- * affiliations take part_ above. Comparing the full Address instead would
133
- * ask an entry's package to agree with the citing note's, and a fetched
134
- * entry's own `parents` or `domains` is written in *its* package while the
135
- * blend it takes part in is *this* one's — see
127
+ * **The holdings graph has package-blind Shortcode identity.** `places` and
128
+ * `affiliations` merge local and fetched notes into one shortcode space; the
129
+ * first declaration wins. A fetched place attaches to a local parent with
130
+ * the same shortcode, and a fetched affiliation can name both local and
131
+ * fetched domains. The authored entries remain Addresses; this graph uses
132
+ * their shortcode projection for containment and tenure. See
136
133
  * `tests/site-holdings.test.ts`'s `empire.domains: ["mill", "abroad"]`, where
137
134
  * `mill` is local and `abroad` is the dependency's own, both bare, both
138
135
  * correct only because neither package is asked.
@@ -321,6 +318,7 @@ export function holdingsPages(nodes) {
321
318
  // Read through the one reader, so a caller handing in what a note
322
319
  // wrote — an address, a wikilink, a repeat — is read as `holdingsNode`
323
320
  // reads it.
321
+ // Shortcode identity: the first node of each type owns this graph key.
324
322
  by.set(shortcode, {
325
323
  ...node,
326
324
  shortcode,
@@ -108,15 +108,14 @@ const PLACE_TYPES = Object.freeze(new Set(["place"]));
108
108
  * since this position asks neither its package nor its system, only the
109
109
  * shortcode {@link module:engine/address} would resolve it to anyway.
110
110
  *
111
- * **The match this shortcode feeds is deliberately package-blind.**
112
- * `world.places` merges this package's own places with every fetched
113
- * dependency's into one shortcode space, the way a dependency's place
114
- * attaches to the local containment tree it is drawn beside — see
111
+ * **The map graph has package-blind Shortcode identity.** `world.places`
112
+ * merges local and fetched places by shortcode, with local declarations
113
+ * taking precedence. A fetched place attaches to the local containment tree
114
+ * by its parent's shortcode. The authored parent remains an Address; the map
115
+ * uses its shortcode projection for containment, layout, and DOT nodes. See
115
116
  * `tests/map.test.ts`'s _loads a dependency's places beside the package's
116
117
  * own_, where a fetched place's bare `parents: ["world"]` reaches this
117
- * package's own root. Comparing the full Address instead would ask the
118
- * dependency's own package to agree with this one's, which the feature is
119
- * built to cross.
118
+ * package's own root.
120
119
  *
121
120
  * @param {unknown} parent - One `parents` entry.
122
121
  * @returns {string} Its shortcode, or `""` for an entry that names nothing.
@@ -193,6 +192,7 @@ export function placesFromRecords(records, { contentBase, base }) {
193
192
  // lint's finding.
194
193
  if (places.has(shortcode)) continue;
195
194
  const data = record.data && typeof record.data === "object" ? record.data : {};
195
+ // Shortcode identity: local places claim the map node first.
196
196
  places.set(shortcode, {
197
197
  shortcode,
198
198
  name,
@@ -243,6 +243,7 @@ function addForeignPlaces(world, foreignIndex) {
243
243
  if (entry?.type !== "place") continue;
244
244
  const shortcode = readCanonicalKey(canonical)?.shortcode?.toLowerCase() ?? "";
245
245
  if (!shortcode || world.places.has(shortcode)) continue;
246
+ // Shortcode identity: fetched places fill only unclaimed map nodes.
246
247
  world.places.set(shortcode, {
247
248
  shortcode,
248
249
  name: String(entry.name ?? shortcode),
@@ -242,8 +242,10 @@ export function foreignNode(canonical, entry) {
242
242
  /**
243
243
  * The corpus a link index describes.
244
244
  *
245
- * A shortcode is declared once per type and the first declaration wins, so a
246
- * local note shadows a dependency's the way the link resolver answers.
245
+ * Population containment and tenure use the same package-blind Shortcode
246
+ * identity as holdings and maps. Each type has one node per shortcode, with
247
+ * local notes taking precedence over fetched entries. A fetched place can
248
+ * attach to local geography, while its authored `parents` remains an Address.
247
249
  *
248
250
  * @param {object} index - The link index.
249
251
  * @returns {Corpus} The corpus.
@@ -263,6 +265,7 @@ function corpusOf(index) {
263
265
  if (!node) return;
264
266
  const by = node.type === "place" ? places : affiliations;
265
267
  if (by.has(node.shortcode)) return;
268
+ // Shortcode identity: the first node of each type owns this graph key.
266
269
  by.set(node.shortcode, node);
267
270
  // Not a canonical Address: package-blind by design, so a local place
268
271
  // shadows a foreign one of the same type and shortcode. `/` keeps that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.11.0",
3
+ "version": "22.12.1",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -16,15 +16,16 @@ export function imageSourceProblem(src: string): string;
16
16
  *
17
17
  * Values are validated here rather than at the point of rendering, so a mistake
18
18
  * is one finding with a position rather than three surfaces quietly drawing
19
- * something else. A directive that holds a problem yields **no** class and no
19
+ * something else. A directive that holds a problem yields **no** class, size or
20
20
  * float: a half-honoured directive is the silent failure in a smaller costume.
21
21
  *
22
22
  * @param {string} [raw] - The text between the braces, braces included or not.
23
- * @returns {{classes: string[], float: string, problems: string[]}} What was
23
+ * @returns {{classes: string[], size: string, float: string, problems: string[]}} What was
24
24
  * written, and what cannot be honoured.
25
25
  */
26
26
  export function parseImageDirective(raw?: string): {
27
27
  classes: string[];
28
+ size: string;
28
29
  float: string;
29
30
  problems: string[];
30
31
  };
@@ -182,7 +183,7 @@ export function lintContentImages(contentBase: string, { skipDirectories, config
182
183
  * **Hugo is handed markdown, not a rendered page.** The site emitter writes a
183
184
  * note's body through verbatim, so a `{…}` directive left in it reaches the
184
185
  * page as its own literal braces: Goldmark's block-attribute parser is off, and
185
- * turning it on would accept `style` and `id` alongside the two vocabularies,
186
+ * turning it on would accept `style` and `id` alongside the closed vocabularies,
186
187
  * which is the injection surface this rule exists to avoid. So the directive is
187
188
  * resolved here, into markup Goldmark passes through.
188
189
  *
@@ -248,6 +249,8 @@ export const IMAGE_CLASSES: Readonly<Record<string, {
248
249
  scope: string;
249
250
  describe: string;
250
251
  }>>;
252
+ /** The named sizes an image directive accepts without applying a display size. */
253
+ export const IMAGE_SIZES: readonly string[];
251
254
  /**
252
255
  * The `float:` positions an image may take, and where each puts it.
253
256
  *