@heroiclands/package-build 25.2.1 → 25.3.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.
@@ -86,13 +86,7 @@ import { scanSpans } from "./content-spans.mjs";
86
86
  import { GENERIC_FONTS, renderRuns, scanRuns } from "./content-scripts.mjs";
87
87
  import { scanFigures } from "./content-figures.mjs";
88
88
  import { BLOCK_CONTAINERS, scanBlocks, BLOCK_NAMES } from "./content-blocks.mjs";
89
- import {
90
- hasColumns,
91
- lineNumber,
92
- rightToLeft,
93
- scanVerse,
94
- verseColumnCount,
95
- } from "./content-verse.mjs";
89
+ import { rightToLeft, scanVerse } from "./content-verse.mjs";
96
90
  import { matchAllOutsideCode, replaceOutsideCode } from "./code-fences.mjs";
97
91
  import { panelRoman, scanScrolls, scrollPanels, scrollParagraphs } from "./content-scroll.mjs";
98
92
  import { BOOK_INK, BOOK_RUBRIC, fontOutlines, scrollPanelSvg } from "./font-outlines.mjs";
@@ -522,7 +516,10 @@ export function markdownToTypst(markdown, opts = {}) {
522
516
  const marked =
523
517
  opts.nested ? undefined : renderRuns(authored, opts.runs, undefined, { target: "book" });
524
518
  const source = marked ? marked.markdown : authored;
525
- if (marked) sharedOptions.scrolls = marked.scrolls;
519
+ if (marked) {
520
+ sharedOptions.scrolls = marked.scrolls;
521
+ sharedOptions.verses = marked.verses;
522
+ }
526
523
  // One map for the whole body, not one per block: a heading inside a
527
524
  // blockquote or a list item shares the entry's anchor namespace with every
528
525
  // other heading in the same body, because `sectionLabel` scopes by entry
@@ -543,6 +540,7 @@ export function markdownToTypst(markdown, opts = {}) {
543
540
  footnoteState,
544
541
  footnotePrefix: opts.footnotePrefix ?? `footnote-${slugify(anchorPrefix || "body")}`,
545
542
  scrolls: marked?.scrolls ?? opts.scrolls ?? new Map(),
543
+ verses: marked?.verses ?? opts.verses ?? new Map(),
546
544
  };
547
545
  // A withheld section is set in the box a `:::secret` is set in, and its
548
546
  // heading is set inside it: a printed page honours no reader permission, so
@@ -691,7 +689,7 @@ export function markdownToTypst(markdown, opts = {}) {
691
689
  /** Render one fragment with the note's footnote definitions available. */
692
690
  function renderMarkdownSegment(source, md, ctx, definitions) {
693
691
  const input = definitions ? `${source}\n\n${definitions}` : source;
694
- const tokens = md.parse(input, {});
692
+ const tokens = md.parse(input, { verses: ctx.verses });
695
693
  const footnoteStart = tokens.findIndex((token) => token.type === "footnote_block_open");
696
694
  const visible = footnoteStart < 0 ? tokens : tokens.slice(0, footnoteStart);
697
695
  const footnotes = new Map();
@@ -779,34 +777,19 @@ function renderBlock(tokens, i, out, ctx) {
779
777
  }
780
778
  if (token.type === "fence" && /^verse(?:\s|$)/.test(token.info?.trim() || "")) {
781
779
  const opening = `${token.markup}${token.info}`;
782
- const scanned = scanVerse(`${opening}\n${token.content}${token.markup}`);
780
+ const scanned = scanVerse(`${opening}\n${token.content}${token.markup}`, {
781
+ checkWorks: false,
782
+ });
783
783
  // Findings are collected before rendering. Keep malformed verse
784
784
  // literal rather than assigning invalid parts to display cells.
785
- if (scanned.errors.length) {
785
+ if (scanned.errors.length && !token.meta?.versePaired) {
786
786
  out.push(rawBlock(token.content, token.info));
787
787
  return 1;
788
788
  }
789
789
  const poem = scanned.blocks[0];
790
- const step = Number(poem?.attributes.numstep ?? 0) || 0;
791
- const content =
792
- hasColumns(poem) ? verseGrid(token, poem, step, ctx)
793
- : step ? numberedVerse(token, step, ctx)
794
- : token.meta.verseStanzas
795
- .map((stanza) =>
796
- stanza[0]?.stanzaBreak ?
797
- "#v(0.5em)"
798
- : stanza
799
- .map(({ children, level }) => {
800
- const verse = renderInline({ children }, ctx);
801
- return `${level ? `#h(${level * 1.25}em)` : ""}${verse}`;
802
- })
803
- .join(" \\\n"),
804
- )
805
- .join("\n\n");
790
+ const content = workVerse(token, poem, ctx);
806
791
  const anchor = poem?.id ? ` <${sectionLabel(ctx.anchorPrefix, poem.id)}>` : "";
807
- out.push(
808
- `\n#block(width: 100%)[${scriptWrapped(content, poem?.classes, poem?.attributes)}]${anchor}\n`,
809
- );
792
+ out.push(`\n#block(width: 100%)[${content}]${anchor}\n`);
810
793
  } else out.push(rawBlock(token.content, token.info?.trim() || ""));
811
794
  return 1;
812
795
  }
@@ -1300,90 +1283,51 @@ function typstLanguage(tag) {
1300
1283
  /** The classes that set a run in a font: a face's run carries `script`. */
1301
1284
  const FONT_CLASSES = Object.freeze(["script", ...Object.values(GENERIC_FONTS)]);
1302
1285
 
1303
- /** A line number in the book: muted, as on the page. */
1304
- const typstNumber = (number) => (number ? `[#text(fill: luma(45%))[${number}]]` : "[]");
1305
-
1306
- /**
1307
- * A numbered verse without columns: a grid of each line's number and the
1308
- * line, with a row of space between stanzas.
1309
- *
1310
- * @param {object} token - The verse's fence token.
1311
- * @param {number} step - Its `numstep`.
1312
- * @param {object} ctx - Render context.
1313
- * @returns {string} Typst markup.
1314
- */
1315
- function numberedVerse(token, step, ctx) {
1316
- let count = 0;
1317
- const stanzas = token.meta.verseStanzas.map((stanza) =>
1318
- stanza[0]?.stanzaBreak ?
1319
- " grid.cell(colspan: 2)[#v(0.5em)],"
1320
- : stanza
1321
- .map(({ children, level }) => {
1322
- const verse = renderInline({ children }, ctx);
1323
- return ` ${typstNumber(lineNumber(++count, step))}, [${level ? `#h(${level * 1.25}em)` : ""}${verse}],`;
1324
- })
1325
- .join("\n"),
1326
- );
1327
- return [
1328
- "#grid(columns: (2.5em, 1fr), column-gutter: 1em, row-gutter: 0.35em,",
1329
- stanzas.join("\n grid.cell(colspan: 2)[#v(0.5em)],\n"),
1330
- ")",
1331
- ].join("\n");
1332
- }
1333
-
1334
- /**
1335
- * A verse in columns: a grid with a row per verse line, its number first when
1336
- * the verse is numbered, then each column's entries stacked, the first in the
1337
- * normal tone and each later one grey. A part whose text runs right to left
1338
- * is set right to left inside its cell; the grid stays left to right so
1339
- * numbered columns retain their authored order. A `***` line is a row of space
1340
- * across the grid.
1341
- *
1342
- * @param {object} token - The verse's fence token with parsed logical rows.
1343
- * @param {object} poem - The verse, from `scanVerse`.
1344
- * @param {number} step - Its `numstep`.
1345
- * @param {object} ctx - Render context.
1346
- * @returns {string} Typst markup.
1347
- */
1348
- function verseGrid(token, poem, step, ctx) {
1349
- const count = verseColumnCount(poem);
1350
- const width = count + (step ? 1 : 0);
1351
- const columns = [...(step ? ["2.5em"] : []), ...Array(count).fill("1fr")];
1352
- const rows = [];
1353
- let row = 0;
1354
- for (const group of token.meta.verseGroups) {
1355
- if (group.stanzaBreak) {
1356
- rows.push(` grid.cell(colspan: ${width})[#v(0.5em)],`);
1357
- continue;
1358
- }
1359
- const cells = Array.from({ length: count }, () => []);
1360
- for (const part of group.parts) {
1361
- const rtl = rightToLeft(part.text);
1362
- const secondary = cells[part.column - 1].length > 0;
1363
- const text = renderInline(
1364
- { children: part.children },
1365
- secondary ? { ...ctx, textFill: "luma(45%)" } : ctx,
1286
+ /** The logical lines of one stanza, with a shared label and paired scripts. */
1287
+ function workVerse(token, poem, ctx) {
1288
+ const paired = token.meta.versePaired;
1289
+ const labelled = Boolean(poem?.attributes.work);
1290
+ const nativeClasses = paired?.classes ?? poem?.classes;
1291
+ const nativeAttributes = paired?.attributes ?? poem?.attributes;
1292
+ return (token.meta.verseGroups ?? [])
1293
+ .map((group) => {
1294
+ if (group.cantoBreak) {
1295
+ const mark =
1296
+ group.children?.length ? renderInline({ children: group.children }, ctx) : "";
1297
+ return `${mark ? scriptWrapped(mark, nativeClasses, nativeAttributes) + "\n" : ""}#v(0.5em)`;
1298
+ }
1299
+ const label = labelled ? `#text(size: 0.75em, fill: luma(45%))[${group.label}]` : "";
1300
+ const lineText = (line, roman = false) => {
1301
+ const text = renderInline(
1302
+ { children: roman ? line.romanChildren : line.children },
1303
+ ctx,
1304
+ );
1305
+ const padded = `${line.level ? `#h(${line.level * 1.25}em)` : ""}${text}`;
1306
+ if (roman || !paired) return padded;
1307
+ const wrapped = scriptWrapped(padded, nativeClasses, nativeAttributes);
1308
+ return rightToLeft(line.text) ? `#set text(dir: rtl)\n${wrapped}` : wrapped;
1309
+ };
1310
+ if (!paired) {
1311
+ const lines = scriptWrapped(
1312
+ group.lines.map((line) => lineText(line)).join(" \\\n"),
1313
+ nativeClasses,
1314
+ nativeAttributes,
1315
+ );
1316
+ return labelled ?
1317
+ `#grid(columns: (2.5em, 1fr), column-gutter: 0.5em, [${label}], [${lines}])`
1318
+ : lines;
1319
+ }
1320
+ const cells = group.lines.map((line, index) =>
1321
+ [
1322
+ ...(labelled ? [index === 0 ? `[${label}]` : "[]"] : []),
1323
+ `[${lineText(line)}]`,
1324
+ `[${lineText(line, true)}]`,
1325
+ ].join(", "),
1366
1326
  );
1367
- const padded =
1368
- part.indent ?
1369
- `#pad(${rtl ? "right" : "left"}: ${part.indent * 1.25}em)[${text}]`
1370
- : text;
1371
- cells[part.column - 1].push(rtl ? `#set text(dir: rtl)\n${padded}` : padded);
1372
- }
1373
- const rendered = cells.map((stacked) => {
1374
- if (!stacked.length) return "[]";
1375
- if (stacked.length === 1) return `[${stacked[0]}]`;
1376
- return `[#stack(dir: ttb, spacing: 0.3em, ${stacked.map((part, k) => (k ? `[#text(fill: luma(45%))[${part}]]` : `[${part}]`)).join(", ")})]`;
1377
- });
1378
- const number = step ? [typstNumber(lineNumber(++row, step))] : [];
1379
- rows.push(` ${[...number, ...rendered].join(", ")},`);
1380
- }
1381
- return [
1382
- "#set text(dir: ltr)",
1383
- `#grid(columns: (${columns.join(", ")}), column-gutter: 1.5em, row-gutter: 0.65em,`,
1384
- ...rows,
1385
- ")",
1386
- ].join("\n");
1327
+ const columns = labelled ? "2.5em, 1fr, 1fr" : "1fr, 1fr";
1328
+ return `#set text(dir: ltr)\n#grid(columns: (${columns}), column-gutter: 1.5em, row-gutter: 0.35em,\n ${cells.join(",\n ")}\n)`;
1329
+ })
1330
+ .join("\n\n");
1387
1331
  }
1388
1332
 
1389
1333
  /**
@@ -1719,8 +1663,7 @@ function pageOfLabel(label) {
1719
1663
  * @returns {string} Typst markup.
1720
1664
  */
1721
1665
  function renderLink(href, inner, ctx, { bracketed = false } = {}) {
1722
- // A secondary verse reading keeps its label gray inside the global blue
1723
- // link presentation, including any native spans or emphasis in that label.
1666
+ // A caller's explicit text fill applies inside the link's label.
1724
1667
  if (ctx.textFill) inner = `#text(fill: ${ctx.textFill})[${inner}]`;
1725
1668
  const url = String(href ?? "");
1726
1669
  const fragment = /#([^?]*)/.exec(url)?.[1] ?? "";
@@ -2012,9 +1955,14 @@ export function bookTypstPreamble(runs = {}, families = {}) {
2012
1955
  " }\n" +
2013
1956
  " if lang != none { text(lang: lang, styled) } else { styled }\n" +
2014
1957
  "}",
2015
- // Span classes reach the book intact.
1958
+ // Plain semantic hooks retain the span classes without choosing their appearance.
1959
+ "#let book-direction(body) = body",
1960
+ "#let book-speaker(body) = body",
2016
1961
  "#let book-span(body, classes: (), lang: none) = {\n" +
2017
- " box(book-script-text(body, classes: classes, lang: lang))\n" +
1962
+ " let styled = book-script-text(body, classes: classes, lang: lang)\n" +
1963
+ ' if "direction" in classes { styled = book-direction(styled) }\n' +
1964
+ ' if "speaker" in classes { styled = book-speaker(styled) }\n' +
1965
+ " box(styled)\n" +
2018
1966
  "}",
2019
1967
  // Use the file's natural measure for auto. Every requested width is
2020
1968
  // bounded by the available measure and by the height of a page.
@@ -12,16 +12,15 @@
12
12
  */
13
13
 
14
14
  /** Logical verse text and separately located structural boundaries. @module */
15
- import { verseLine } from "./content-verse.mjs";
15
+ import { parseVerse } from "./content-verse.mjs";
16
16
  import { plainRomanRun } from "./roman-run.mjs";
17
17
 
18
18
  /**
19
- * Remove visual indentation before checking or encoding Roman verse. Each
20
- * content line keeps its own boundary, including the last line before a stanza
21
- * or canto; those extra structural controls do not consume its line boundary.
22
- * The caller writes the boundaries through its selected encoding.
19
+ * Project verse text with explicit authored line, stanza and canto boundaries.
20
+ * Source newlines within a logical line are whitespace. Delimiters and visual
21
+ * indentation do not reach the encoder as letters.
23
22
  * @param {string} source - Complete authored source.
24
- * @param {Parameters<typeof plainRomanRun>[1] & {isCantoLine?: (line: string) => boolean}} [options] - Run bounds, spans and layout projection.
23
+ * @param {Parameters<typeof plainRomanRun>[1] & {model?: ReturnType<typeof parseVerse>, modelOffset?: number}} [options] - Run bounds, spans and layout projection.
25
24
  * @returns {ReturnType<typeof plainRomanRun> & {boundaries: Array<{kind: "line"|"stanza"|"canto", at: number, sourceOffset: number}>, errors: Array<{offset: number, message: string}>}} Visible text, positions and structural events.
26
25
  */
27
26
  export function plainRomanVerse(source, options = {}) {
@@ -29,28 +28,60 @@ export function plainRomanVerse(source, options = {}) {
29
28
  from = 0,
30
29
  to = source.length,
31
30
  exclude = [],
32
- isCantoLine = (line) => /^\s*---\s+\{\.canto\}\s*$/.test(line),
31
+ model: sharedModel,
32
+ modelOffset = from,
33
33
  } = options;
34
- const lines = [];
35
- const layout = [...exclude];
36
- let at = from;
37
- for (const match of source.slice(from, to).matchAll(/([^\r\n]*)(\r\n|\r|\n|$)/g)) {
38
- if (!match[0]) continue;
39
- const line = match[1];
40
- const end = at + line.length;
41
- const kind =
42
- isCantoLine(line) ? "canto"
43
- : line.trim() ? "content"
44
- : "blank";
45
- lines.push({ from: at, to: end, kind });
46
- if (kind === "canto") layout.push([at, end]);
47
- else if (kind === "content") {
48
- const { markerStart, markerLength } = verseLine(line);
49
- if (markerLength) layout.push([at + markerStart, at + markerStart + markerLength]);
50
- }
51
- at += match[0].length;
34
+ const model = sharedModel ?? parseVerse(source.slice(from, to));
35
+ const base = sharedModel ? modelOffset : from;
36
+ const boundaries = model.boundaries
37
+ .map((boundary) => ({ ...boundary, sourceOffset: base + boundary.sourceOffset }))
38
+ .filter((boundary) => boundary.sourceOffset >= from && boundary.sourceOffset <= to);
39
+ const projected = plainRomanRun(source, {
40
+ ...options,
41
+ from,
42
+ to,
43
+ exclude: [
44
+ ...exclude,
45
+ ...model.layout
46
+ .map(([first, past]) => [Math.max(from, base + first), Math.min(to, base + past)])
47
+ .filter(([first, past]) => past > first),
48
+ ],
49
+ });
50
+ const text = [],
51
+ offsets = [],
52
+ positions = [],
53
+ ends = [];
54
+ const pending = [...boundaries];
55
+ let boundaryIndex = 0;
56
+ const appendBoundary = (boundary) => {
57
+ text.push("\n");
58
+ offsets.push(boundary.sourceOffset);
59
+ };
60
+ for (let index = 0; index < projected.text.length; index++) {
61
+ ends[index] = text.length;
62
+ while (
63
+ pending[boundaryIndex] &&
64
+ pending[boundaryIndex].sourceOffset <= projected.offsets[index]
65
+ )
66
+ appendBoundary(pending[boundaryIndex++]);
67
+ positions[index] = text.length;
68
+ const char = projected.text[index];
69
+ if (char === "\n" && projected.text[index - 1] === "\r") continue;
70
+ text.push(/[\r\n]/.test(char) ? " " : char);
71
+ offsets.push(projected.offsets[index]);
52
72
  }
53
- const projected = plainRomanRun(source, { ...options, from, to, exclude: layout });
73
+ ends[projected.text.length] = text.length;
74
+ while (pending[boundaryIndex]) appendBoundary(pending[boundaryIndex++]);
75
+ positions[projected.text.length] = text.length;
76
+ projected.text = text.join("");
77
+ projected.offsets = offsets;
78
+ projected.spans = projected.spans.map((span) => ({
79
+ ...span,
80
+ from: positions[span.from],
81
+ to: ends[span.to],
82
+ contentFrom: positions[span.contentFrom],
83
+ contentTo: ends[span.contentTo],
84
+ }));
54
85
  const logicalAt = (offset) => {
55
86
  let first = 0,
56
87
  past = projected.offsets.length;
@@ -61,35 +92,15 @@ export function plainRomanVerse(source, options = {}) {
61
92
  }
62
93
  return first;
63
94
  };
64
- /** @type {Array<{kind: "line"|"stanza"|"canto", at: number, sourceOffset: number}>} */
65
- const boundaries = [];
66
- for (let index = 0; index < lines.length; index++) {
67
- const line = lines[index];
68
- if (line.kind === "content")
69
- boundaries.push({ kind: "line", at: logicalAt(line.to), sourceOffset: line.to });
70
- else if (line.kind === "canto")
71
- boundaries.push({ kind: "canto", at: logicalAt(line.from), sourceOffset: line.from });
72
- else if (lines[index - 1]?.kind === "content") {
73
- const following = lines.slice(index + 1).find((next) => next.kind !== "blank");
74
- if (following?.kind === "content")
75
- boundaries.push({
76
- kind: "stanza",
77
- at: logicalAt(line.from),
78
- sourceOffset: line.from,
79
- });
80
- }
81
- }
82
- const errors = [];
83
- for (let index = 0; index < projected.text.length; index++) {
84
- if (projected.text[index] === "⟦") {
85
- const close = projected.text.indexOf("⟧", index + 1);
86
- index = close < 0 ? projected.text.length : close;
87
- } else if (projected.text[index] === "/")
88
- errors.push({
89
- offset: projected.offsets[index],
90
- message:
91
- "a verse fence uses authored line endings for verse boundaries; write no literal `/` inside it",
92
- });
93
- }
94
- return { ...projected, boundaries, errors };
95
+ return {
96
+ ...projected,
97
+ boundaries: boundaries.map(({ kind, sourceOffset }) => ({
98
+ kind,
99
+ sourceOffset,
100
+ at: logicalAt(sourceOffset),
101
+ })),
102
+ errors: model.errors
103
+ .map(({ offset, message }) => ({ offset: base + offset, message }))
104
+ .filter((error) => error.offset >= from && error.offset < to),
105
+ };
95
106
  }
@@ -60,7 +60,7 @@ import { WORD } from "./word-pattern.mjs";
60
60
 
61
61
  import MarkdownIt from "markdown-it";
62
62
  import { spanMarkdownPlugin } from "./content-spans.mjs";
63
- import { STANZA_BREAK, verseLine } from "./content-verse.mjs";
63
+ import { parseVerse } from "./content-verse.mjs";
64
64
  import { inlineText } from "./name-markup.mjs";
65
65
 
66
66
  const verseParser = new MarkdownIt().use(spanMarkdownPlugin);
@@ -311,13 +311,6 @@ export function metreFindings(blocks, lines, ctx) {
311
311
  column: ctx.attributeColumns(opening, key)[part],
312
312
  });
313
313
  const error = (position, message) => errors.push({ ...position, message });
314
- if (Number(block.attributes.cols ?? 1) > 1) {
315
- error(
316
- at("scan", "key"),
317
- "`scan=` counts the beats of one language's lines; a verse with cols>1 groups independent physical lines",
318
- );
319
- continue;
320
- }
321
314
  const metre = metres.get(name);
322
315
  if (!metre) {
323
316
  const near = nearest(name, [...metres.keys()]);
@@ -346,40 +339,40 @@ export function metreFindings(blocks, lines, ctx) {
346
339
  );
347
340
  continue;
348
341
  }
349
- for (let line = block.start + 1; line < block.end; line++) {
350
- const text = lines[line];
351
- if (!text.trim() || STANZA_BREAK.test(text.trim())) continue;
352
- const reading = verseLine(text);
353
- const tokens = verseParser.parseInline(reading.text, {})[0]?.children ?? [];
354
- const tag = (name) => (ctx.languages?.[name]?.tag ?? name).toLowerCase();
355
- const foreign = tokens.find(
356
- (token) =>
357
- token.type === "span_open" &&
358
- token.attrGet("lang") &&
359
- tag(token.attrGet("lang")) !== tag(lang),
360
- );
361
- if (foreign) {
362
- error(
363
- {
364
- line: line + 1,
365
- column:
366
- reading.textColumn +
367
- foreign.meta.start +
368
- (reading.markerStart <= reading.textColumn - 1 + foreign.meta.start ?
369
- reading.markerLength
370
- : 0),
371
- },
372
- "`scan=` counts one language's beats; this verse line contains another language",
342
+ const body = lines.slice(block.start + 1, block.end).join("\n");
343
+ const model = block.model ?? parseVerse(body);
344
+ const sourcePosition = (offset) => {
345
+ const preceding = body.slice(0, offset);
346
+ return {
347
+ line: block.start + 2 + preceding.split("\n").length - 1,
348
+ column: preceding.length - preceding.lastIndexOf("\n"),
349
+ };
350
+ };
351
+ for (const group of model.groups) {
352
+ if (group.cantoBreak) continue;
353
+ for (const reading of group.lines) {
354
+ const tokens = verseParser.parseInline(reading.text, {})[0]?.children ?? [];
355
+ const tag = (name) => (ctx.languages?.[name]?.tag ?? name).toLowerCase();
356
+ const foreign = tokens.find(
357
+ (token) =>
358
+ token.type === "span_open" &&
359
+ token.attrGet("lang") &&
360
+ tag(token.attrGet("lang")) !== tag(lang),
373
361
  );
374
- continue;
362
+ if (foreign) {
363
+ error(
364
+ sourcePosition(reading.offsets[foreign.meta.start] ?? reading.from),
365
+ "`scan=` counts one language's beats; this verse line contains another language",
366
+ );
367
+ continue;
368
+ }
369
+ const beats = lineBeats(inlineText(tokens), book);
370
+ if (metre.beats >= beats.min && metre.beats <= beats.max) continue;
371
+ warnings.push({
372
+ ...sourcePosition(reading.from),
373
+ message: metreMessage(beats, metre),
374
+ });
375
375
  }
376
- const beats = lineBeats(inlineText(tokens), book);
377
- if (metre.beats >= beats.min && metre.beats <= beats.max) continue;
378
- warnings.push({
379
- line: line + 1,
380
- column: /^\s*/.exec(text)[0].length + 1,
381
- message: metreMessage(beats, metre),
382
- });
383
376
  }
384
377
  }
385
378
  return { errors, warnings };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "25.2.1",
3
+ "version": "25.3.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package \u2014 content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",