@heroiclands/package-build 24.0.3 → 24.1.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.
- package/CHANGELOG.md +53 -0
- package/README.md +6 -0
- package/assets/styles/content-base.css +25 -10
- package/docs/authoring/links-and-markup.md +221 -176
- package/docs/commands.md +5 -2
- package/docs/configuration.md +6 -3
- package/docs/content-format.md +4 -3
- package/docs/diagnostics.md +57 -29
- package/docs/guides/book.md +1 -1
- package/docs/guides/module-project.md +2 -2
- package/docs/guides/site.md +2 -2
- package/docs/guides/writing-in-invented-scripts.md +175 -125
- package/docs/reference/format-details.md +65 -38
- package/docs/reference/note-types.md +10 -10
- package/docs/reference/theme-pages.md +18 -9
- package/engine/content-render.mjs +23 -2
- package/engine/content-scripts.mjs +349 -120
- package/engine/content-spans.mjs +13 -1
- package/engine/content-verse.mjs +158 -373
- package/engine/core-report.mjs +10 -2
- package/engine/map-lexicon.mjs +37 -5
- package/engine/note-vocabulary.mjs +5 -5
- package/engine/pdf-render.mjs +63 -44
- package/engine/run-rows.mjs +18 -20
- package/engine/run-words.mjs +7 -7
- package/engine/verse-metre.mjs +36 -8
- package/engine/written-in.mjs +9 -13
- package/package.json +1 -1
- package/site-theme/static/css/style.css +19 -10
- package/types/engine/content-scripts.d.mts +18 -7
- package/types/engine/content-verse.d.mts +37 -163
- package/types/engine/map-lexicon.d.mts +3 -1
- package/types/engine/run-rows.d.mts +16 -9
- package/types/engine/run-words.d.mts +2 -1
- package/types/engine/verse-metre.d.mts +4 -3
package/engine/core-report.mjs
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* The list is the `data.concepts` of the package's `doc` notes. A language
|
|
19
19
|
* must cover it where it states `data.core: true`. A word covers every
|
|
20
20
|
* concept its `concept` names, and a language reads the words of its
|
|
21
|
-
* `data.family` chain as its own
|
|
21
|
+
* `data.family` chain as its own, with the nearest definition of each form
|
|
22
|
+
* winning. A missing concept is a fact to work through,
|
|
22
23
|
* never a finding.
|
|
23
24
|
*
|
|
24
25
|
* @module
|
|
@@ -114,9 +115,16 @@ export function coreReport(records, { language } = {}) {
|
|
|
114
115
|
concepts: concepts.length,
|
|
115
116
|
languages: chosen.map((record) => {
|
|
116
117
|
const have = new Set();
|
|
118
|
+
const forms = new Set();
|
|
117
119
|
for (const key of chain(noteAddressOf(record) ?? ""))
|
|
118
|
-
for (const word of byKey.get(key)?.data?.words ?? [])
|
|
120
|
+
for (const word of byKey.get(key)?.data?.words ?? []) {
|
|
121
|
+
if (!isMap(word) || typeof word.form !== "string") continue;
|
|
122
|
+
// Match dictionary headwords: NFC, retaining case and word separators.
|
|
123
|
+
const form = word.form.normalize("NFC");
|
|
124
|
+
if (forms.has(form)) continue;
|
|
125
|
+
forms.add(form);
|
|
119
126
|
for (const id of idsOf(word)) have.add(id);
|
|
127
|
+
}
|
|
120
128
|
const missing = [];
|
|
121
129
|
for (const group of groups) {
|
|
122
130
|
const members = concepts.filter((concept) => concept.group === group);
|
package/engine/map-lexicon.mjs
CHANGED
|
@@ -59,7 +59,7 @@ import { fromWrittenIn } from "./written-in.mjs";
|
|
|
59
59
|
import { writingsOf } from "./logographic.mjs";
|
|
60
60
|
import { FONTS_PACKAGE } from "./script-fonts.mjs";
|
|
61
61
|
import { GENERIC_FAMILIES, applyMap } from "./text-maps.mjs";
|
|
62
|
-
import { isLanguageRecord, noteAddressOf } from "./word-rows.mjs";
|
|
62
|
+
import { isLanguageRecord, lineageChain, noteAddressOf } from "./word-rows.mjs";
|
|
63
63
|
import { derivedForms } from "./word-forms.mjs";
|
|
64
64
|
import { LANGUAGE_SUBTYPE, glossText } from "./word-terms.mjs";
|
|
65
65
|
|
|
@@ -183,7 +183,7 @@ function lexiconOf(language, data) {
|
|
|
183
183
|
* @returns {string|undefined} The Address.
|
|
184
184
|
*/
|
|
185
185
|
function noteKey(note, contentPackage, types) {
|
|
186
|
-
if (
|
|
186
|
+
if (!contentPackage) return undefined;
|
|
187
187
|
const tuple = parseAddress(
|
|
188
188
|
note,
|
|
189
189
|
{ package: contentPackage, system: "note", type: "skill", types: new Set(types) },
|
|
@@ -248,7 +248,9 @@ function languageNotes(languages, contentPackage, records) {
|
|
|
248
248
|
|
|
249
249
|
/**
|
|
250
250
|
* Each configured language's lexicon, read from the language note its `note`
|
|
251
|
-
* names
|
|
251
|
+
* names and its family chain. The nearest exact NFC form supplies the whole
|
|
252
|
+
* word; grammar, registers and other settings remain the daughter's own.
|
|
253
|
+
* A language with no `note`, or one naming no language note, has none.
|
|
252
254
|
*
|
|
253
255
|
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
254
256
|
* @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
|
|
@@ -256,9 +258,39 @@ function languageNotes(languages, contentPackage, records) {
|
|
|
256
258
|
* @returns {Map<string, Lexicon>} The lexicons by language name.
|
|
257
259
|
*/
|
|
258
260
|
export function lexiconsFromRecords(records, config = {}) {
|
|
261
|
+
const list = [...(records ?? [])];
|
|
262
|
+
const byKey = recordsByKey(list, config.contentPackage);
|
|
263
|
+
const types = new Set(["skill", ...list.map((record) => record?.type).filter(Boolean)]);
|
|
264
|
+
const lineage = {
|
|
265
|
+
has: (key) => isLanguage(byKey.get(key)),
|
|
266
|
+
parentOf: (key) => {
|
|
267
|
+
const record = byKey.get(key);
|
|
268
|
+
return noteKey(record.data?.family, record.package ?? config.contentPackage, types);
|
|
269
|
+
},
|
|
270
|
+
};
|
|
259
271
|
const lexicons = new Map();
|
|
260
|
-
for (const [name, record] of
|
|
261
|
-
|
|
272
|
+
for (const [name, { key, record }] of languageNotes(
|
|
273
|
+
config.languages,
|
|
274
|
+
config.contentPackage,
|
|
275
|
+
list,
|
|
276
|
+
)) {
|
|
277
|
+
if (!record) continue;
|
|
278
|
+
const chain = lineageChain(key, lineage);
|
|
279
|
+
let data = record.data;
|
|
280
|
+
if (chain.length > 1) {
|
|
281
|
+
const words = new Map();
|
|
282
|
+
for (const ancestor of chain)
|
|
283
|
+
for (const word of listOf(byKey.get(ancestor).data?.words)) {
|
|
284
|
+
if (!isMap(word) || typeof word.form !== "string") continue;
|
|
285
|
+
// The nearest complete definition wins before word matching
|
|
286
|
+
// folds case or splits compounds. Only words are inherited.
|
|
287
|
+
const form = word.form.normalize("NFC");
|
|
288
|
+
if (!words.has(form)) words.set(form, word);
|
|
289
|
+
}
|
|
290
|
+
data = { ...data, words: [...words.values()] };
|
|
291
|
+
}
|
|
292
|
+
lexicons.set(name, lexiconOf(name, data));
|
|
293
|
+
}
|
|
262
294
|
return lexicons;
|
|
263
295
|
}
|
|
264
296
|
|
|
@@ -2209,9 +2209,9 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2209
2209
|
},
|
|
2210
2210
|
]),
|
|
2211
2211
|
describe:
|
|
2212
|
-
"The script the language is written in, as the map and
|
|
2213
|
-
"with
|
|
2214
|
-
"skill of `subType: language` states it.",
|
|
2212
|
+
"The script the language is written in, as the default map and face selected " +
|
|
2213
|
+
"by a run with explicit `lang=` and `.nativefont`; a run's own `map=` or " +
|
|
2214
|
+
"`font=` overrides its half. Only a skill of `subType: language` states it.",
|
|
2215
2215
|
},
|
|
2216
2216
|
{
|
|
2217
2217
|
name: "script",
|
|
@@ -2451,8 +2451,8 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2451
2451
|
},
|
|
2452
2452
|
]),
|
|
2453
2453
|
describe:
|
|
2454
|
-
"A mapping, which text marked
|
|
2455
|
-
"before it is set. See " +
|
|
2454
|
+
"A mapping, which native text marked with `lang=`, `.nativefont` and its " +
|
|
2455
|
+
"`map=` name is rewritten by before it is set. See " +
|
|
2456
2456
|
"[mapping notes](format-details.md#mapping-notes).",
|
|
2457
2457
|
},
|
|
2458
2458
|
{
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -87,13 +87,11 @@ 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
89
|
import {
|
|
90
|
-
|
|
91
|
-
STANZA_BREAK,
|
|
90
|
+
hasColumns,
|
|
92
91
|
lineNumber,
|
|
93
92
|
rightToLeft,
|
|
94
93
|
scanVerse,
|
|
95
|
-
|
|
96
|
-
verseStanzas,
|
|
94
|
+
verseColumnCount,
|
|
97
95
|
} from "./content-verse.mjs";
|
|
98
96
|
import { matchAllOutsideCode, replaceOutsideCode } from "./code-fences.mjs";
|
|
99
97
|
import { panelRoman, scanScrolls, scrollPanels, scrollParagraphs } from "./content-scroll.mjs";
|
|
@@ -763,20 +761,28 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
763
761
|
}
|
|
764
762
|
if (token.type === "fence" && /^verse(?:\s|$)/.test(token.info?.trim() || "")) {
|
|
765
763
|
const opening = `${token.markup}${token.info}`;
|
|
766
|
-
const
|
|
764
|
+
const scanned = scanVerse(`${opening}\n${token.content}${token.markup}`);
|
|
765
|
+
// Findings are collected before rendering. Keep malformed verse
|
|
766
|
+
// literal rather than assigning invalid parts to display cells.
|
|
767
|
+
if (scanned.errors.length) {
|
|
768
|
+
out.push(rawBlock(token.content, token.info));
|
|
769
|
+
return 1;
|
|
770
|
+
}
|
|
771
|
+
const poem = scanned.blocks[0];
|
|
767
772
|
const step = Number(poem?.attributes.numstep ?? 0) || 0;
|
|
768
773
|
const content =
|
|
769
|
-
poem
|
|
770
|
-
verseGrid(token, poem, opening, step, ctx)
|
|
774
|
+
hasColumns(poem) ? verseGrid(token, poem, step, ctx)
|
|
771
775
|
: step ? numberedVerse(token, step, ctx)
|
|
772
776
|
: token.meta.verseStanzas
|
|
773
777
|
.map((stanza) =>
|
|
774
|
-
stanza
|
|
775
|
-
.
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
778
|
+
stanza[0]?.stanzaBreak ?
|
|
779
|
+
"#v(0.5em)"
|
|
780
|
+
: stanza
|
|
781
|
+
.map(({ children, level }) => {
|
|
782
|
+
const verse = renderInline({ children }, ctx);
|
|
783
|
+
return `${level ? `#h(${level * 1.25}em)` : ""}${verse}`;
|
|
784
|
+
})
|
|
785
|
+
.join(" \\\n"),
|
|
780
786
|
)
|
|
781
787
|
.join("\n\n");
|
|
782
788
|
const anchor = poem?.id ? ` <${sectionLabel(ctx.anchorPrefix, poem.id)}>` : "";
|
|
@@ -1291,12 +1297,14 @@ const typstNumber = (number) => (number ? `[#text(fill: luma(45%))[${number}]]`
|
|
|
1291
1297
|
function numberedVerse(token, step, ctx) {
|
|
1292
1298
|
let count = 0;
|
|
1293
1299
|
const stanzas = token.meta.verseStanzas.map((stanza) =>
|
|
1294
|
-
stanza
|
|
1295
|
-
.
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
+
stanza[0]?.stanzaBreak ?
|
|
1301
|
+
" grid.cell(colspan: 2)[#v(0.5em)],"
|
|
1302
|
+
: stanza
|
|
1303
|
+
.map(({ children, level }) => {
|
|
1304
|
+
const verse = renderInline({ children }, ctx);
|
|
1305
|
+
return ` ${typstNumber(lineNumber(++count, step))}, [${level ? `#h(${level * 1.25}em)` : ""}${verse}],`;
|
|
1306
|
+
})
|
|
1307
|
+
.join("\n"),
|
|
1300
1308
|
);
|
|
1301
1309
|
return [
|
|
1302
1310
|
"#grid(columns: (2.5em, 1fr), column-gutter: 1em, row-gutter: 0.35em,",
|
|
@@ -1309,44 +1317,52 @@ function numberedVerse(token, step, ctx) {
|
|
|
1309
1317
|
* A verse in columns: a grid with a row per verse line, its number first when
|
|
1310
1318
|
* the verse is numbered, then each column's entries stacked, the first in the
|
|
1311
1319
|
* normal tone and each later one grey. A part whose text runs right to left
|
|
1312
|
-
* is set right to left inside its cell
|
|
1313
|
-
*
|
|
1320
|
+
* is set right to left inside its cell; the grid stays left to right so
|
|
1321
|
+
* numbered columns retain their authored order. A `***` line is a row of space
|
|
1314
1322
|
* across the grid.
|
|
1315
1323
|
*
|
|
1316
|
-
* @param {object} token - The
|
|
1324
|
+
* @param {object} token - The verse's fence token with parsed logical rows.
|
|
1317
1325
|
* @param {object} poem - The verse, from `scanVerse`.
|
|
1318
|
-
* @param {string} opening - Its opening line.
|
|
1319
1326
|
* @param {number} step - Its `numstep`.
|
|
1320
1327
|
* @param {object} ctx - Render context.
|
|
1321
1328
|
* @returns {string} Typst markup.
|
|
1322
1329
|
*/
|
|
1323
|
-
function verseGrid(token, poem,
|
|
1324
|
-
const
|
|
1325
|
-
const width =
|
|
1326
|
-
const columns = [...(step ? ["2.5em"] : []), ...
|
|
1327
|
-
const raw = verseStanzas(token.content);
|
|
1330
|
+
function verseGrid(token, poem, step, ctx) {
|
|
1331
|
+
const count = verseColumnCount(poem);
|
|
1332
|
+
const width = count + (step ? 1 : 0);
|
|
1333
|
+
const columns = [...(step ? ["2.5em"] : []), ...Array(count).fill("1fr")];
|
|
1328
1334
|
const rows = [];
|
|
1329
|
-
let
|
|
1330
|
-
token.meta.
|
|
1331
|
-
if (
|
|
1335
|
+
let row = 0;
|
|
1336
|
+
for (const group of token.meta.verseGroups) {
|
|
1337
|
+
if (group.stanzaBreak) {
|
|
1332
1338
|
rows.push(` grid.cell(colspan: ${width})[#v(0.5em)],`);
|
|
1333
|
-
|
|
1339
|
+
continue;
|
|
1334
1340
|
}
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1341
|
+
const cells = Array.from({ length: count }, () => []);
|
|
1342
|
+
for (const part of group.parts) {
|
|
1343
|
+
const rtl = rightToLeft(part.text);
|
|
1344
|
+
const secondary = cells[part.column - 1].length > 0;
|
|
1345
|
+
const text = renderInline(
|
|
1346
|
+
{ children: part.children },
|
|
1347
|
+
secondary ? { ...ctx, textFill: "luma(45%)" } : ctx,
|
|
1348
|
+
);
|
|
1349
|
+
const padded =
|
|
1350
|
+
part.indent ?
|
|
1351
|
+
`#pad(${rtl ? "right" : "left"}: ${part.indent * 1.25}em)[${text}]`
|
|
1352
|
+
: text;
|
|
1353
|
+
cells[part.column - 1].push(rtl ? `#set text(dir: rtl)\n${padded}` : padded);
|
|
1354
|
+
}
|
|
1355
|
+
const rendered = cells.map((stacked) => {
|
|
1356
|
+
if (!stacked.length) return "[]";
|
|
1342
1357
|
if (stacked.length === 1) return `[${stacked[0]}]`;
|
|
1343
1358
|
return `[#stack(dir: ttb, spacing: 0.3em, ${stacked.map((part, k) => (k ? `[#text(fill: luma(45%))[${part}]]` : `[${part}]`)).join(", ")})]`;
|
|
1344
1359
|
});
|
|
1345
|
-
const number = step ? [typstNumber(lineNumber(++
|
|
1346
|
-
rows.push(` ${[...number, ...
|
|
1347
|
-
}
|
|
1360
|
+
const number = step ? [typstNumber(lineNumber(++row, step))] : [];
|
|
1361
|
+
rows.push(` ${[...number, ...rendered].join(", ")},`);
|
|
1362
|
+
}
|
|
1348
1363
|
return [
|
|
1349
|
-
|
|
1364
|
+
"#set text(dir: ltr)",
|
|
1365
|
+
`#grid(columns: (${columns.join(", ")}), column-gutter: 1.5em, row-gutter: 0.65em,`,
|
|
1350
1366
|
...rows,
|
|
1351
1367
|
")",
|
|
1352
1368
|
].join("\n");
|
|
@@ -1440,7 +1456,7 @@ function renderInline(token, ctx) {
|
|
|
1440
1456
|
case "script_emphasis_open":
|
|
1441
1457
|
out.push(
|
|
1442
1458
|
child.meta?.convention === "rubric" ?
|
|
1443
|
-
|
|
1459
|
+
`#text(fill: ${ctx.textFill ?? "book-rubric"})[`
|
|
1444
1460
|
: "#overline[",
|
|
1445
1461
|
);
|
|
1446
1462
|
break;
|
|
@@ -1675,6 +1691,9 @@ function pageOfLabel(label) {
|
|
|
1675
1691
|
* @returns {string} Typst markup.
|
|
1676
1692
|
*/
|
|
1677
1693
|
function renderLink(href, inner, ctx, { bracketed = false } = {}) {
|
|
1694
|
+
// A secondary verse reading keeps its label gray inside the global blue
|
|
1695
|
+
// link presentation, including any native spans or emphasis in that label.
|
|
1696
|
+
if (ctx.textFill) inner = `#text(fill: ${ctx.textFill})[${inner}]`;
|
|
1678
1697
|
const url = String(href ?? "");
|
|
1679
1698
|
const fragment = /#([^?]*)/.exec(url)?.[1] ?? "";
|
|
1680
1699
|
if (url.startsWith("#") && fragment)
|
package/engine/run-rows.mjs
CHANGED
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
* column value and none is a key, so two notes writing one tag in different
|
|
18
18
|
* case are two rows. Documentation records and assets contribute no rows.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Native runs retain their explicit opt-in so language defaults can be filled
|
|
21
|
+
* by {@link fillWrittenIn} once every record exists. Roman language-only runs
|
|
22
|
+
* retain null map/font fields; ordinary generic-family inheritance is already
|
|
23
|
+
* projected by the shared scanner.
|
|
23
24
|
*
|
|
24
25
|
* @module
|
|
25
26
|
*/
|
|
@@ -30,7 +31,7 @@ import { isNoteRecord } from "./index-records.mjs";
|
|
|
30
31
|
import { scanRuns } from "./content-scripts.mjs";
|
|
31
32
|
import { languageRecordsByName } from "./map-lexicon.mjs";
|
|
32
33
|
import { mapsFromRecords } from "./text-maps.mjs";
|
|
33
|
-
import {
|
|
34
|
+
import { writtenInOf } from "./written-in.mjs";
|
|
34
35
|
|
|
35
36
|
/** Every column of the `runs` relation, in order. */
|
|
36
37
|
export const RUN_COLUMNS = Object.freeze([
|
|
@@ -52,10 +53,12 @@ export const RUN_COLUMNS = Object.freeze([
|
|
|
52
53
|
* @property {string|null} lang - The language it is set in, as written: its
|
|
53
54
|
* own `lang=`, else the one the div or fence around it states.
|
|
54
55
|
* @property {string|null} tag - The BCP 47 tag it resolves to, or null.
|
|
55
|
-
* @property {string|null} font - Its font,
|
|
56
|
-
*
|
|
57
|
-
* @property {string|null} map - Its map
|
|
58
|
-
*
|
|
56
|
+
* @property {string|null} font - Its explicit font, an inherited ordinary
|
|
57
|
+
* prose family, or the configured default for an opted-in native run.
|
|
58
|
+
* @property {string|null} map - Its explicit map or the configured map of an
|
|
59
|
+
* opted-in native run; null for Roman language-only text.
|
|
60
|
+
* @property {boolean} [nativefont] - Explicit native opt-in on this construct;
|
|
61
|
+
* absent on Roman runs. This internal flag is not a SQL column.
|
|
59
62
|
* @property {number} line - 1-based file line of its opening.
|
|
60
63
|
* @property {number} column - 1-based UTF-16 file column of its opening.
|
|
61
64
|
*/
|
|
@@ -84,13 +87,14 @@ export function recordRuns(body, { context, bodyLine = 1, bodyColumn = 1, syntax
|
|
|
84
87
|
tag: run.tag ?? null,
|
|
85
88
|
font: run.font ?? null,
|
|
86
89
|
map: run.map ?? null,
|
|
90
|
+
...(run.nativefont ? { nativefont: true } : {}),
|
|
87
91
|
line: bodyLine + run.line - 1,
|
|
88
92
|
column: run.column + (run.line === 1 ? bodyColumn - 1 : 0),
|
|
89
93
|
}));
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
/**
|
|
93
|
-
* Give
|
|
97
|
+
* Give explicitly native recorded runs the map and face their language names in
|
|
94
98
|
* `data.writtenIn`, where the run leaves them unstated, as a run is set. Read
|
|
95
99
|
* once every record exists, since a run's language note may be recorded after
|
|
96
100
|
* the note that marks it.
|
|
@@ -107,17 +111,11 @@ export function fillWrittenIn(records, config) {
|
|
|
107
111
|
for (const record of records) {
|
|
108
112
|
if (!record || !isNoteRecord(record) || !Array.isArray(record.runs)) continue;
|
|
109
113
|
for (const run of record.runs) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
},
|
|
116
|
-
scripts,
|
|
117
|
-
maps,
|
|
118
|
-
);
|
|
119
|
-
if (filled.map !== undefined) run.map = filled.map;
|
|
120
|
-
if (filled.font !== undefined) run.font = filled.font;
|
|
114
|
+
if (!run.nativefont) continue;
|
|
115
|
+
const script = scripts.get(run.lang);
|
|
116
|
+
if (!script) continue;
|
|
117
|
+
if (run.map == null) run.map = script.map;
|
|
118
|
+
if (run.font == null) run.font = script.font;
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
121
|
}
|
package/engine/run-words.mjs
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* @module
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import { lexiconsFromRecords } from "./map-lexicon.mjs";
|
|
40
40
|
import { RESERVED_TONGUE } from "./language-terms.mjs";
|
|
41
41
|
import { addWords, foldWord, lexiconWords } from "./word-forms.mjs";
|
|
42
42
|
import { WORD } from "./word-pattern.mjs";
|
|
@@ -82,7 +82,8 @@ export function registeredNameWords(data) {
|
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* Each configured language's word book: one per language whose `note` names a
|
|
85
|
-
* language note.
|
|
85
|
+
* language note. Its effective lexicon shares the renderer's nearest inherited
|
|
86
|
+
* words, and derives forms and registered names from its own settings.
|
|
86
87
|
*
|
|
87
88
|
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
88
89
|
* @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
|
|
@@ -92,11 +93,10 @@ export function registeredNameWords(data) {
|
|
|
92
93
|
export function wordBooksFromRecords(records, config = {}) {
|
|
93
94
|
const list = [...(records ?? [])];
|
|
94
95
|
const books = new Map();
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const names = registeredNameWords(record.data);
|
|
96
|
+
const lexicons = lexiconsFromRecords(list, config);
|
|
97
|
+
for (const [name, lexicon] of lexicons) {
|
|
98
|
+
const own = lexiconWords(lexicon.data);
|
|
99
|
+
const names = registeredNameWords(lexicon.data);
|
|
100
100
|
books.set(
|
|
101
101
|
name,
|
|
102
102
|
Object.freeze({
|
package/engine/verse-metre.mjs
CHANGED
|
@@ -58,6 +58,13 @@ import { parseWordClass } from "./word-classes.mjs";
|
|
|
58
58
|
import { derivedForms, foldWord } from "./word-forms.mjs";
|
|
59
59
|
import { WORD } from "./word-pattern.mjs";
|
|
60
60
|
|
|
61
|
+
import MarkdownIt from "markdown-it";
|
|
62
|
+
import { spanMarkdownPlugin } from "./content-spans.mjs";
|
|
63
|
+
import { STANZA_BREAK, verseLine } from "./content-verse.mjs";
|
|
64
|
+
import { inlineText } from "./name-markup.mjs";
|
|
65
|
+
|
|
66
|
+
const verseParser = new MarkdownIt().use(spanMarkdownPlugin);
|
|
67
|
+
|
|
61
68
|
export { METRE_KINDS, METRE_NAME, checkMetresField } from "./metre-declaration.mjs";
|
|
62
69
|
|
|
63
70
|
/** The bases whose one-syllable words carry a beat. */
|
|
@@ -285,7 +292,7 @@ export function metreMessage({ min, max }, metre) {
|
|
|
285
292
|
* @param {ReadonlyArray<{start: number, end: number, indent: string, attributes: Record<string, string>, classes: string[]}>} blocks -
|
|
286
293
|
* The fences, from {@link module:engine/content-verse.scanVerse}.
|
|
287
294
|
* @param {string[]} lines - The source's lines.
|
|
288
|
-
* @param {{metres?: ReadonlyMap<string, import("./metre-declaration.mjs").Metre>, metreBooks?: ReadonlyMap<string, MetreBook>, lexicons?: ReadonlyMap<string, unknown>,
|
|
295
|
+
* @param {{languages?: Readonly<Record<string, {tag: string}>>, metres?: ReadonlyMap<string, import("./metre-declaration.mjs").Metre>, metreBooks?: ReadonlyMap<string, MetreBook>, lexicons?: ReadonlyMap<string, unknown>, attributeColumns: (line: string, key: string) => {key: number, value: number}}} ctx -
|
|
289
296
|
* The metres and books, and how the fence says its columns and where its
|
|
290
297
|
* keys sit.
|
|
291
298
|
* @returns {{errors: object[], warnings: object[]}} The findings at 1-based
|
|
@@ -304,13 +311,10 @@ export function metreFindings(blocks, lines, ctx) {
|
|
|
304
311
|
column: ctx.attributeColumns(opening, key)[part],
|
|
305
312
|
});
|
|
306
313
|
const error = (position, message) => errors.push({ ...position, message });
|
|
307
|
-
if (
|
|
308
|
-
block.classes.includes(ctx.columnsClass) ||
|
|
309
|
-
ctx.columnKeys.some((key) => block.attributes[key] !== undefined)
|
|
310
|
-
) {
|
|
314
|
+
if (Number(block.attributes.cols ?? 1) > 1) {
|
|
311
315
|
error(
|
|
312
316
|
at("scan", "key"),
|
|
313
|
-
"`scan=` counts the beats of one language's lines; a verse
|
|
317
|
+
"`scan=` counts the beats of one language's lines; a verse with cols>1 groups independent physical lines",
|
|
314
318
|
);
|
|
315
319
|
continue;
|
|
316
320
|
}
|
|
@@ -344,8 +348,32 @@ export function metreFindings(blocks, lines, ctx) {
|
|
|
344
348
|
}
|
|
345
349
|
for (let line = block.start + 1; line < block.end; line++) {
|
|
346
350
|
const text = lines[line];
|
|
347
|
-
if (!text.trim()) continue;
|
|
348
|
-
const
|
|
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",
|
|
373
|
+
);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const beats = lineBeats(inlineText(tokens), book);
|
|
349
377
|
if (metre.beats >= beats.min && metre.beats <= beats.max) continue;
|
|
350
378
|
warnings.push({
|
|
351
379
|
line: line + 1,
|
package/engine/written-in.mjs
CHANGED
|
@@ -20,20 +20,16 @@
|
|
|
20
20
|
* writtenIn: { map: semaron, font: semaron-kuriale }
|
|
21
21
|
* ```
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* These are native-presentation defaults, not an effect of identifying a
|
|
24
|
+
* language. A rendering run requests them with its own `.nativefont` class
|
|
25
|
+
* and explicit `lang=`; plain `[Zaron tholmeza]{lang=vyl}` stays Roman.
|
|
26
|
+
* The renderer fills the map and font independently, honoring explicit
|
|
27
|
+
* overrides and reporting an incompatible pair rather than silently omitting
|
|
28
|
+
* a default. A nested marked run makes its own presentation choice.
|
|
28
29
|
*
|
|
29
|
-
* -
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* - **Both** take nothing.
|
|
33
|
-
*
|
|
34
|
-
* The default belongs to the run's own language, after a div or fence around
|
|
35
|
-
* it has given it its keys: a span marked `lang=en` inside a div of a language
|
|
36
|
-
* with a default takes no face from it.
|
|
30
|
+
* The low-level `fromWrittenIn` helper also serves explicit conversion
|
|
31
|
+
* commands. It returns compatible default suggestions to those callers;
|
|
32
|
+
* rendering opt-in and its required-font diagnostics belong to the run reader.
|
|
37
33
|
*
|
|
38
34
|
* @module
|
|
39
35
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "24.0
|
|
3
|
+
"version": "24.1.0",
|
|
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",
|
|
@@ -1384,7 +1384,7 @@ a.tag:hover {
|
|
|
1384
1384
|
background: var(--bg-card);
|
|
1385
1385
|
}
|
|
1386
1386
|
|
|
1387
|
-
/* Verse
|
|
1387
|
+
/* Verse groups are paragraphs; explicit stanza separators add their own gap. */
|
|
1388
1388
|
.single-content .verse {
|
|
1389
1389
|
margin: 1.5rem 0;
|
|
1390
1390
|
line-height: 1.7;
|
|
@@ -1404,14 +1404,16 @@ a.tag:hover {
|
|
|
1404
1404
|
white-space: pre-wrap;
|
|
1405
1405
|
}
|
|
1406
1406
|
|
|
1407
|
-
.single-content .verse .verse-line.i1 { --verse-indent: 1.25em; }
|
|
1408
|
-
.single-content .verse .verse-line.i2 { --verse-indent: 2.5em; }
|
|
1409
|
-
.single-content .verse .verse-line.i3 { --verse-indent: 3.75em; }
|
|
1410
|
-
.single-content .verse .verse-line.i4 { --verse-indent: 5em; }
|
|
1411
|
-
.single-content .verse .verse-line.i5 { --verse-indent: 6.25em; }
|
|
1412
|
-
.single-content .verse .verse-line.i6 { --verse-indent: 7.5em; }
|
|
1413
|
-
.single-content .verse .verse-line.i7 { --verse-indent: 8.75em; }
|
|
1414
|
-
.single-content .verse .verse-line.i8 { --verse-indent: 10em; }
|
|
1407
|
+
.single-content .verse :is(.verse-line, .verse-part).i1 { --verse-indent: 1.25em; }
|
|
1408
|
+
.single-content .verse :is(.verse-line, .verse-part).i2 { --verse-indent: 2.5em; }
|
|
1409
|
+
.single-content .verse :is(.verse-line, .verse-part).i3 { --verse-indent: 3.75em; }
|
|
1410
|
+
.single-content .verse :is(.verse-line, .verse-part).i4 { --verse-indent: 5em; }
|
|
1411
|
+
.single-content .verse :is(.verse-line, .verse-part).i5 { --verse-indent: 6.25em; }
|
|
1412
|
+
.single-content .verse :is(.verse-line, .verse-part).i6 { --verse-indent: 7.5em; }
|
|
1413
|
+
.single-content .verse :is(.verse-line, .verse-part).i7 { --verse-indent: 8.75em; }
|
|
1414
|
+
.single-content .verse :is(.verse-line, .verse-part).i8 { --verse-indent: 10em; }
|
|
1415
|
+
|
|
1416
|
+
.single-content .verse .verse-stanza-break { display: block; height: 0.5em; }
|
|
1415
1417
|
|
|
1416
1418
|
/* Numbered verse carries its numbers in a margin. A verse in columns is one
|
|
1417
1419
|
grid per verse line: its number, then each column's parts stacked, the
|
|
@@ -1446,12 +1448,14 @@ a.tag:hover {
|
|
|
1446
1448
|
display: grid;
|
|
1447
1449
|
grid-auto-flow: column;
|
|
1448
1450
|
grid-auto-columns: 1fr;
|
|
1451
|
+
direction: ltr;
|
|
1452
|
+
grid-template-columns: repeat(var(--verse-columns, 2), minmax(0, 1fr));
|
|
1449
1453
|
column-gap: 1.5rem;
|
|
1450
1454
|
margin-bottom: 0.5em;
|
|
1451
1455
|
}
|
|
1452
1456
|
|
|
1453
1457
|
.single-content .verse-numbered.verse-columns .verse-row {
|
|
1454
|
-
grid-template-columns: 2.5em;
|
|
1458
|
+
grid-template-columns: 2.5em repeat(var(--verse-columns, 2), minmax(0, 1fr));
|
|
1455
1459
|
}
|
|
1456
1460
|
|
|
1457
1461
|
.single-content .verse-columns .verse-number {
|
|
@@ -1466,12 +1470,17 @@ a.tag:hover {
|
|
|
1466
1470
|
|
|
1467
1471
|
.single-content .verse-columns .verse-part {
|
|
1468
1472
|
font-family: var(--font-body);
|
|
1473
|
+
padding-inline-start: var(--verse-indent, 0em);
|
|
1469
1474
|
}
|
|
1470
1475
|
|
|
1471
1476
|
.single-content .verse-columns .verse-secondary {
|
|
1472
1477
|
color: var(--text-muted);
|
|
1473
1478
|
}
|
|
1474
1479
|
|
|
1480
|
+
.single-content .verse-columns .verse-secondary * {
|
|
1481
|
+
color: inherit;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1475
1484
|
.single-content .verse-columns .verse-part[dir="rtl"] {
|
|
1476
1485
|
text-align: start;
|
|
1477
1486
|
}
|