@heroiclands/package-build 22.14.3 → 22.14.4

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,11 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.14.4
4
+
5
+ ### Patch Changes
6
+
7
+ **Illustrations** — Named image sizes set the display width on pages and in books. Images without a size use their natural dimensions within the available space.
8
+
3
9
  ## 22.14.3
4
10
 
5
11
  ### Patch Changes
package/docs/api.md CHANGED
@@ -965,12 +965,12 @@ Raw HTML in a note's prose, reported. **A note is markdown.** What markdown cann
965
965
 
966
966
  ### `engine.contentImages`
967
967
 
968
- 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.
968
+ An image saying how wide it is and where it sits. A directive in the curly-attribute convention Pandoc and Kramdown use supplies a width class, a named `size:`, and a `float:` position. The named size sets a maximum display width for each surface.
969
969
 
970
970
  | Export | Signature | Returns | Use it when |
971
971
  | --------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
972
972
  | `IMAGE_CLASSES` | `const IMAGE_CLASSES` | — | The width classes an image may carry, and what each means to a renderer. |
973
- | `IMAGE_SIZES` | `const IMAGE_SIZES` | — | The named sizes an image directive accepts without applying a display size. |
973
+ | `IMAGE_SIZES` | `const IMAGE_SIZES` | — | The named sizes an image directive accepts. |
974
974
  | `IMAGE_FLOATS` | `const IMAGE_FLOATS` | — | The `float:` positions an image may take, and where each puts it. |
975
975
  | `IMAGE_FIGURE_CLASS` | `const IMAGE_FIGURE_CLASS` | — | The class every figure carries, whatever its width or position. |
976
976
  | `IMAGE_PATTERN` | `const IMAGE_PATTERN` | — | A markdown image, with the directive it may carry. |
@@ -1167,6 +1167,7 @@ A note's markdown, and a document plan, rendered as Typst source. **This module
1167
1167
  | `renderBook` | `renderBook({ plan, bodies, title, subtitle, front, fonts, version, preamble, banners })` | {string} A complete `.typ` document. | The whole book, as one Typst document. |
1168
1168
  | `resolveDanglingLabels` | `function resolveDanglingLabels(source, findings` | {string} The same document, with no reference left dangling. | Point every internal link at a label the document actually declares. |
1169
1169
  | `iconNamesIn` | `function iconNamesIn(markdown)` | {string[]} The names, in order of appearance, with repeats. | Every icon name a body uses, so a build can resolve them once. |
1170
+ | `BOOK_IMAGE_WIDTHS` | `const BOOK_IMAGE_WIDTHS` | — | The book measure requested by each named image size. |
1170
1171
  | `bookTypstPreamble` | `function bookTypstPreamble()` | {string} Typst markup. | The Typst definitions the book's page furniture is drawn with. |
1171
1172
 
1172
1173
  ### `engine.pdfFonts`
@@ -2066,10 +2066,10 @@ place for those words and this is it.
2066
2066
 
2067
2067
  ##### Width is a class, and the ordinary width carries no marker
2068
2068
 
2069
- | `class` value | book | website | Foundry journal |
2070
- | ------------- | ------------- | ------------------ | ---------------- |
2071
- | _(none)_ | one column | the text measure | the page measure |
2072
- | `.full-width` | the full page | full content width | full page width |
2069
+ | `class` value | book | website | Foundry journal |
2070
+ | ------------- | ----------------------------- | ------------------ | --------------- |
2071
+ | _(none)_ | natural width within a column | natural width | natural width |
2072
+ | `.full-width` | page measure | full content width | full page width |
2073
2073
 
2074
2074
  The simple case needs no spelling, which is why the ordinary width has no name.
2075
2075
 
@@ -2080,21 +2080,24 @@ refused.
2080
2080
 
2081
2081
  ##### Named size is `size:`
2082
2082
 
2083
- | `size` value | display |
2084
- | ------------ | ------------------------ |
2085
- | `auto` | same as omitting `size:` |
2086
- | `small` | same as omitting `size:` |
2087
- | `medium` | same as omitting `size:` |
2088
- | `large` | same as omitting `size:` |
2089
- | `xlarge` | same as omitting `size:` |
2090
- | `full-width` | same as omitting `size:` |
2091
-
2092
- The parser accepts and records these names for Markdown images and embedded
2093
- assets. **A `size:` value does not change the displayed dimensions** on the
2094
- website, in Foundry, or in the book. With no `size:`, the parser records `auto`;
2095
- the existing rendering rules apply. In particular, book images occupy a column
2096
- unless they carry `.full-width`. A `size:` may sit before or after `float:` in
2097
- the same directive, and neither changes the other's behavior.
2083
+ | `size` value | website and Foundry | book |
2084
+ | ------------ | ------------------- | ------------ |
2085
+ | `auto` | natural size | natural size |
2086
+ | `small` | 64 CSS pixels | 1.6 cm |
2087
+ | `medium` | 128 CSS pixels | 3.2 cm |
2088
+ | `large` | 256 CSS pixels | 5.6 cm |
2089
+ | `xlarge` | 512 CSS pixels | 8 cm |
2090
+ | `full-width` | content measure | page measure |
2091
+
2092
+ These are maximum display widths for Markdown images and embedded assets;
2093
+ height follows the file's aspect ratio. The available column, page, or browser
2094
+ measure caps every size, and print also caps image height within the page.
2095
+ Omitting `size:` means `auto`, which uses the file's natural dimensions within
2096
+ those bounds. `full-width` spans the book's page rather than one column. The
2097
+ `.full-width` class also grants page scope in print; when it accompanies a
2098
+ bounded `size:`, the image keeps that bounded width inside the page figure.
2099
+ A `size:` may sit before or after `float:` in the same directive, and neither
2100
+ changes the other's behavior.
2098
2101
 
2099
2102
  ##### Position is `float:`
2100
2103
 
@@ -370,9 +370,10 @@ position together:
370
370
  ```
371
371
 
372
372
  The accepted sizes are `auto`, `small`, `medium`, `large`, `xlarge`, and
373
- `full-width`. Omitting `size:` means `auto`. The build checks the name, but
374
- `size:` does not change the displayed dimensions on the website, in Foundry, or
375
- in the book. See [Images](content-format.md#images) for the rendering rules.
373
+ `full-width`. Omitting `size:` means `auto`, which uses the image's natural
374
+ dimensions within the available measure. The other names set maximum display
375
+ widths for the website, Foundry, and book. See
376
+ [Images](content-format.md#images) for the surface sizes and layout rules.
376
377
 
377
378
  ## Step 6 — check the tree
378
379
 
@@ -25,7 +25,8 @@
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
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
+ * values. The HTML surfaces receive a size class; the book receives a print
29
+ * measure.
29
30
  *
30
31
  * All three are closed, and an unrecognised value is **refused with a located
31
32
  * diagnostic** rather than ignored. Ignoring is the failure worth preventing:
@@ -105,7 +106,7 @@ export const IMAGE_CLASSES = Object.freeze({
105
106
  },
106
107
  });
107
108
 
108
- /** The named sizes an image directive accepts without applying a display size. */
109
+ /** The named sizes an image directive accepts. */
109
110
  export const IMAGE_SIZES = Object.freeze([
110
111
  "auto",
111
112
  "small",
@@ -326,16 +327,17 @@ export function parseImageDirective(raw) {
326
327
  /**
327
328
  * The classes a figure carries, from a parsed directive.
328
329
  *
329
- * @param {{classes?: string[], float?: string}} [directive] - As parsed.
330
+ * @param {{classes?: string[], size?: string, float?: string}} [directive] - As parsed.
330
331
  * @returns {string} A space-separated class list, always naming
331
332
  * {@link IMAGE_FIGURE_CLASS} first.
332
333
  */
333
- export function figureClasses({ classes = [], float = "" } = {}) {
334
+ export function figureClasses({ classes = [], size = "auto", float = "" } = {}) {
334
335
  const names = [IMAGE_FIGURE_CLASS];
335
336
  for (const name of classes) {
336
337
  const spec = IMAGE_CLASSES[/** @type {keyof typeof IMAGE_CLASSES} */ (name)];
337
338
  if (spec) names.push(spec.class);
338
339
  }
340
+ if (size !== "auto" && IMAGE_SIZES.includes(size)) names.push(`note-image-size-${size}`);
339
341
  const position = IMAGE_FLOATS[/** @type {keyof typeof IMAGE_FLOATS} */ (float)];
340
342
  if (position) names.push(position.class);
341
343
  return names.join(" ");
@@ -367,13 +369,14 @@ export function escapeHtml(text) {
367
369
  * @param {string} image.src - The address, resolved for the surface.
368
370
  * @param {string} [image.alt] - The alt text, which is also the caption.
369
371
  * @param {string[]} [image.classes] - Width classes, from the directive.
372
+ * @param {string} [image.size] - Named display size, from the directive.
370
373
  * @param {string} [image.float] - The float position, from the directive.
371
374
  * @returns {string} The figure, as one HTML block.
372
375
  */
373
- export function imageFigureHtml({ src, alt = "", classes = [], float = "" }) {
376
+ export function imageFigureHtml({ src, alt = "", classes = [], size = "auto", float = "" }) {
374
377
  const caption = alt ? `\n<figcaption>${escapeHtml(alt)}</figcaption>` : "";
375
378
  return (
376
- `<figure class="${figureClasses({ classes, float })}">\n` +
379
+ `<figure class="${figureClasses({ classes, size, float })}">\n` +
377
380
  `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}">${caption}\n` +
378
381
  `</figure>`
379
382
  );
@@ -644,10 +647,16 @@ export function renderImageFigures(body, resolveSrc = (src) => src) {
644
647
  let last = 0;
645
648
  for (const image of imagesIn(text)) {
646
649
  if (!image.block || imageSourceProblem(image.src) || image.title) continue;
647
- const { classes, float, problems } = parseImageDirective(image.directive);
650
+ const { classes, size, float, problems } = parseImageDirective(image.directive);
648
651
  if (problems.length) continue;
649
652
  out += text.slice(last, image.index);
650
- out += imageFigureHtml({ src: resolveSrc(image.src), alt: image.alt, classes, float });
653
+ out += imageFigureHtml({
654
+ src: resolveSrc(image.src),
655
+ alt: image.alt,
656
+ classes,
657
+ size,
658
+ float,
659
+ });
651
660
  last = image.index + image.length;
652
661
  }
653
662
  return out + text.slice(last);
@@ -696,6 +705,7 @@ export function imagePlugin(resolveSrc = (src) => src) {
696
705
  src: resolveSrc(token.attrGet("src") ?? ""),
697
706
  alt: token.content ?? "",
698
707
  classes: token.meta.classes,
708
+ size: token.meta.size,
699
709
  float: token.meta.float,
700
710
  })}\n`;
701
711
  };
@@ -733,12 +743,12 @@ function attachImageDirectives(tokens) {
733
743
  directive = brace[0];
734
744
  }
735
745
 
736
- const { classes, float, problems } = parseImageDirective(directive);
746
+ const { classes, size, float, problems } = parseImageDirective(directive);
737
747
  // Not ours to consume: leaving the braces in place is what makes an
738
748
  // unrecognised value visible on the page instead of passing as ordinary.
739
749
  if (problems.length) continue;
740
750
 
741
- image.meta = { ...(image.meta ?? {}), block: true, classes, float };
751
+ image.meta = { ...(image.meta ?? {}), block: true, classes, size, float };
742
752
  if (trailing) trailing.content = trailing.content.slice(directive.length);
743
753
  // The figure is a block, so the paragraph that held it renders nothing.
744
754
  tokens[i].hidden = true;
@@ -66,6 +66,16 @@ import MarkdownIt from "markdown-it";
66
66
  import { bookDraftNoticePreamble } from "./draft-notice.mjs";
67
67
  import { iconPlugin, ICON_PATTERN } from "./content-icons.mjs";
68
68
  import { IMAGE_CLASSES, IMAGE_FLOATS, imagePlugin } from "./content-images.mjs";
69
+
70
+ /** Requested print width for every named image size. */
71
+ export const BOOK_IMAGE_WIDTHS = Object.freeze({
72
+ auto: "auto",
73
+ small: "1.6cm",
74
+ medium: "3.2cm",
75
+ large: "5.6cm",
76
+ xlarge: "8cm",
77
+ "full-width": '"full-width"',
78
+ });
69
79
  import { slugify } from "./content-slug.mjs";
70
80
 
71
81
  /**
@@ -706,17 +716,19 @@ function renderLink(href, inner, ctx) {
706
716
  */
707
717
  function renderImage(token, ctx) {
708
718
  const alt = token.content || token.attrGet?.("alt") || "";
709
- const caption =
710
- alt ? `\n #text(size: 7.6pt, style: "italic", fill: luma(45%))[${escapeTypst(alt)}]` : "";
719
+ const caption = alt ? `[${escapeTypst(alt)}]` : "none";
711
720
  const staged = ctx.images.get(token.attrGet?.("src") ?? "");
712
- if (!staged) return caption ? `\n#block(below: 0.6em)[${caption}\n]\n\n` : "";
721
+ if (!staged)
722
+ return alt ?
723
+ `\n#block(below: 0.6em)[#text(size: 7.6pt, style: "italic", fill: luma(45%))${caption}]\n\n`
724
+ : "";
713
725
 
714
- const figure =
715
- `#block(width: 100%, below: 0.6em)[\n` +
716
- ` #image("${escapeTypstString(staged)}", width: 100%)${caption}\n]`;
726
+ const size = BOOK_IMAGE_WIDTHS[token.meta?.size] ?? BOOK_IMAGE_WIDTHS.auto;
727
+ const figure = `#book-image("${escapeTypstString(staged)}", requested: ${size}, caption: ${caption})`;
717
728
 
718
729
  const width = token.meta?.classes?.[0];
719
- const scope = IMAGE_CLASSES[width]?.scope ?? "column";
730
+ const scope =
731
+ token.meta?.size === "full-width" ? "parent" : (IMAGE_CLASSES[width]?.scope ?? "column");
720
732
  const float = IMAGE_FLOATS[token.meta?.float];
721
733
  // In the flow where it was written: no class asking for the page, and no
722
734
  // position asking for the top or the bottom of the column.
@@ -804,6 +816,17 @@ export function bookTypstPreamble() {
804
816
  "#let book-page-height = 11in",
805
817
  "#let book-text-width = book-page-width - 2 * book-margin",
806
818
  "#let book-text-height = book-page-height - 2 * book-margin",
819
+ // Use the file's natural measure for auto. Every requested width is
820
+ // bounded by the available measure and by the height of a page.
821
+ "#let book-image(path, requested: auto, caption: none) = layout(size => {\n" +
822
+ " let natural = measure(image(path))\n" +
823
+ ' let target = if requested == auto { natural.width } else if requested == "full-width" { size.width } else { requested }\n' +
824
+ " let width = calc.min(target, size.width, book-text-height * 0.8 * (natural.width / natural.height))\n" +
825
+ " block(width: width, below: 0.6em)[\n" +
826
+ ' #image(path, width: 100%, fit: "contain")\n' +
827
+ ' #if caption != none { text(size: 7.6pt, style: "italic", fill: luma(45%))[#caption] }\n' +
828
+ " ]\n" +
829
+ "})",
807
830
  // The title sets short of the measure, so a plate's last line does not
808
831
  // run to the trimmed edge of the paper.
809
832
  "#let book-title-measure = book-text-width - 2.5cm",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.14.3",
3
+ "version": "22.14.4",
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",
@@ -32,12 +32,13 @@ export function parseImageDirective(raw?: string): {
32
32
  /**
33
33
  * The classes a figure carries, from a parsed directive.
34
34
  *
35
- * @param {{classes?: string[], float?: string}} [directive] - As parsed.
35
+ * @param {{classes?: string[], size?: string, float?: string}} [directive] - As parsed.
36
36
  * @returns {string} A space-separated class list, always naming
37
37
  * {@link IMAGE_FIGURE_CLASS} first.
38
38
  */
39
- export function figureClasses({ classes, float }?: {
39
+ export function figureClasses({ classes, size, float }?: {
40
40
  classes?: string[];
41
+ size?: string;
41
42
  float?: string;
42
43
  }): string;
43
44
  /**
@@ -58,13 +59,15 @@ export function escapeHtml(text: string): string;
58
59
  * @param {string} image.src - The address, resolved for the surface.
59
60
  * @param {string} [image.alt] - The alt text, which is also the caption.
60
61
  * @param {string[]} [image.classes] - Width classes, from the directive.
62
+ * @param {string} [image.size] - Named display size, from the directive.
61
63
  * @param {string} [image.float] - The float position, from the directive.
62
64
  * @returns {string} The figure, as one HTML block.
63
65
  */
64
- export function imageFigureHtml({ src, alt, classes, float }: {
66
+ export function imageFigureHtml({ src, alt, classes, size, float }: {
65
67
  src: string;
66
68
  alt?: string | undefined;
67
69
  classes?: string[] | undefined;
70
+ size?: string | undefined;
68
71
  float?: string | undefined;
69
72
  }): string;
70
73
  /**
@@ -249,7 +252,7 @@ export const IMAGE_CLASSES: Readonly<Record<string, {
249
252
  scope: string;
250
253
  describe: string;
251
254
  }>>;
252
- /** The named sizes an image directive accepts without applying a display size. */
255
+ /** The named sizes an image directive accepts. */
253
256
  export const IMAGE_SIZES: readonly string[];
254
257
  /**
255
258
  * The `float:` positions an image may take, and where each puts it.
@@ -252,3 +252,12 @@ export function resolveDanglingLabels(source: string, findings?: object[]): stri
252
252
  * @returns {string[]} The names, in order of appearance, with repeats.
253
253
  */
254
254
  export function iconNamesIn(markdown: string): string[];
255
+ /** Requested print width for every named image size. */
256
+ export const BOOK_IMAGE_WIDTHS: Readonly<{
257
+ auto: "auto";
258
+ small: "1.6cm";
259
+ medium: "3.2cm";
260
+ large: "5.6cm";
261
+ xlarge: "8cm";
262
+ "full-width": "\"full-width\"";
263
+ }>;