@heroiclands/package-build 24.0.1 → 24.0.2
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 +14 -0
- package/assets/preview/gitea.ini +1 -1
- package/assets/styles/content-base.css +33 -0
- package/bin/content-commands.mjs +9 -7
- package/docs/api.md +6 -5
- package/docs/authoring/links-and-markup.md +164 -16
- package/docs/commands.md +16 -14
- package/docs/configuration.md +16 -0
- package/docs/content-format.md +4 -2
- package/docs/guides/writing-in-invented-scripts.md +68 -6
- package/docs/reference/format-details.md +143 -49
- package/docs/reference/note-types.md +47 -28
- package/docs/source-preview.md +2 -2
- package/engine/content-blocks.mjs +6 -2
- package/engine/content-format.yaml +2 -0
- package/engine/content-index.mjs +5 -1
- package/engine/content-render.mjs +2 -1
- package/engine/content-scripts.mjs +268 -21
- package/engine/content-scroll.mjs +389 -0
- package/engine/font-outlines.mjs +394 -0
- package/engine/language-data.mjs +154 -63
- package/engine/language-rows.mjs +32 -6
- package/engine/language-terms.mjs +32 -8
- package/engine/map-lexicon.mjs +31 -6
- package/engine/note-vocabulary.mjs +58 -6
- package/engine/pdf-fonts.mjs +1 -1
- package/engine/pdf-render.mjs +52 -11
- package/engine/run-rows.mjs +45 -3
- package/engine/script-direction.mjs +67 -0
- package/engine/script-emphasis-terms.mjs +11 -3
- package/engine/script-font-delivery.mjs +42 -9
- package/engine/script-fonts.mjs +7 -0
- package/engine/verse-metre.mjs +1 -0
- package/engine/written-in.mjs +208 -0
- package/package.json +1 -1
- package/site-theme/static/css/style.css +7 -0
- package/types/engine/content-scripts.d.mts +31 -6
- package/types/engine/content-scroll.d.mts +179 -0
- package/types/engine/font-outlines.d.mts +67 -0
- package/types/engine/language-data.d.mts +13 -0
- package/types/engine/language-rows.d.mts +4 -3
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/map-lexicon.d.mts +4 -3
- package/types/engine/pdf-fonts.d.mts +10 -0
- package/types/engine/run-rows.d.mts +28 -6
- package/types/engine/script-direction.d.mts +10 -0
- package/types/engine/script-emphasis-terms.d.mts +10 -3
- package/types/engine/script-font-delivery.d.mts +5 -3
- package/types/engine/script-fonts.d.mts +9 -0
- package/types/engine/written-in.d.mts +76 -0
package/engine/language-rows.mjs
CHANGED
|
@@ -309,11 +309,14 @@ function languagesOf(records) {
|
|
|
309
309
|
* @param {string} [options.prefix] - The prefix of a key sharing an identity
|
|
310
310
|
* column's name.
|
|
311
311
|
* @param {readonly string[]} [options.lists] - The keys always set as lists.
|
|
312
|
+
* @param {(records: Iterable<Record<string, any>>) => Array<[string, Record<string, any>]>} [options.sources] -
|
|
313
|
+
* The records stating the list, with their Addresses; the language records
|
|
314
|
+
* where unstated.
|
|
312
315
|
* @returns {object[]} One row per entry, in record then entry order.
|
|
313
316
|
*/
|
|
314
|
-
function listRows(records, read, keys, { prefix = "", lists = [] } = {}) {
|
|
317
|
+
function listRows(records, read, keys, { prefix = "", lists = [], sources = languagesOf } = {}) {
|
|
315
318
|
const rows = [];
|
|
316
|
-
for (const [language, record] of
|
|
319
|
+
for (const [language, record] of sources(records)) {
|
|
317
320
|
const entries = read(isMap(record.data) ? record.data : {});
|
|
318
321
|
if (!Array.isArray(entries)) continue;
|
|
319
322
|
entries.forEach((entry, position) => {
|
|
@@ -328,6 +331,24 @@ function listRows(records, read, keys, { prefix = "", lists = [] } = {}) {
|
|
|
328
331
|
return rows;
|
|
329
332
|
}
|
|
330
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Each record stating letters and marks, with its Address: every language
|
|
336
|
+
* record, and every `doc` record whose `data.sounds` is a spelling guide's.
|
|
337
|
+
*
|
|
338
|
+
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
339
|
+
* @returns {Array<[string, Record<string, any>]>}
|
|
340
|
+
*/
|
|
341
|
+
function soundSourcesOf(records) {
|
|
342
|
+
const list = [...records];
|
|
343
|
+
const out = languagesOf(list);
|
|
344
|
+
for (const record of list) {
|
|
345
|
+
if (record?.type !== "doc" || record.documents || !isMap(record.data?.sounds)) continue;
|
|
346
|
+
const address = noteAddressOf(record);
|
|
347
|
+
if (address) out.push([address, record]);
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
|
|
331
352
|
/** A language's `data.sounds`, or an empty map. */
|
|
332
353
|
const soundsOf = (data) => (isMap(data.sounds) ? data.sounds : {});
|
|
333
354
|
|
|
@@ -494,8 +515,8 @@ export function scopeRows(records) {
|
|
|
494
515
|
}
|
|
495
516
|
|
|
496
517
|
/**
|
|
497
|
-
* The `letters` rows: one per `data.sounds.letters` entry
|
|
498
|
-
* `tongue` lists.
|
|
518
|
+
* The `letters` rows: one per `data.sounds.letters` entry of a language note
|
|
519
|
+
* or a spelling guide, `letters` and `tongue` lists.
|
|
499
520
|
*
|
|
500
521
|
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
501
522
|
* @returns {object[]}
|
|
@@ -503,6 +524,7 @@ export function scopeRows(records) {
|
|
|
503
524
|
export function letterRows(records) {
|
|
504
525
|
return listRows(records, (data) => soundsOf(data).letters, LETTER_KEYS, {
|
|
505
526
|
lists: ["letters", "tongue"],
|
|
527
|
+
sources: soundSourcesOf,
|
|
506
528
|
});
|
|
507
529
|
}
|
|
508
530
|
|
|
@@ -875,13 +897,17 @@ export function joinRows(records) {
|
|
|
875
897
|
}
|
|
876
898
|
|
|
877
899
|
/**
|
|
878
|
-
* The `marks` rows: one per `data.sounds.marks` entry
|
|
900
|
+
* The `marks` rows: one per `data.sounds.marks` entry of a language note or a
|
|
901
|
+
* spelling guide.
|
|
879
902
|
*
|
|
880
903
|
* @param {Iterable<Record<string, any>>} records - Index records.
|
|
881
904
|
* @returns {object[]}
|
|
882
905
|
*/
|
|
883
906
|
export function markRows(records) {
|
|
884
|
-
return listRows(records, (data) => soundsOf(data).marks, MARK_KEYS, {
|
|
907
|
+
return listRows(records, (data) => soundsOf(data).marks, MARK_KEYS, {
|
|
908
|
+
lists: ["letters"],
|
|
909
|
+
sources: soundSourcesOf,
|
|
910
|
+
});
|
|
885
911
|
}
|
|
886
912
|
|
|
887
913
|
/**
|
|
@@ -130,7 +130,7 @@ export const CLUSTER_USES = Object.freeze(["common", "sparing"]);
|
|
|
130
130
|
export const STRESS_TWO = Object.freeze(["first", "last"]);
|
|
131
131
|
|
|
132
132
|
/** The syllable a word of three or more stresses, counted from its end. */
|
|
133
|
-
export const STRESS_LONGER = Object.freeze(["antepenultimate", "penultimate", "final"]);
|
|
133
|
+
export const STRESS_LONGER = Object.freeze(["first", "antepenultimate", "penultimate", "final"]);
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
136
|
* The diacritics that mark a stress the rule would not choose, with the
|
|
@@ -377,13 +377,20 @@ export const LETTER_FIELDS = Object.freeze([
|
|
|
377
377
|
shape: "a string, such as `lips` or `tongue-tip`",
|
|
378
378
|
describe: "Where in the mouth they are made.",
|
|
379
379
|
}),
|
|
380
|
+
Object.freeze({
|
|
381
|
+
name: "ipa",
|
|
382
|
+
kind: "string",
|
|
383
|
+
shape: "a string in the International Phonetic Alphabet, such as `ʃ`",
|
|
384
|
+
describe: "The sounds they write, in the International Phonetic Alphabet.",
|
|
385
|
+
}),
|
|
380
386
|
Object.freeze({
|
|
381
387
|
name: "tongue",
|
|
382
388
|
kind: "string-or-list",
|
|
383
|
-
shape: "a tag, or a list of them",
|
|
389
|
+
shape: "a tag or a language note's Address, or a list of them",
|
|
384
390
|
entries: STRINGS,
|
|
385
391
|
describe:
|
|
386
|
-
"The tongues that write them,
|
|
392
|
+
"The tongues that write them, each a tag `data.tongues` declares or the Address of a " +
|
|
393
|
+
"language note; every one where absent. A `doc` note's rows name language notes only.",
|
|
387
394
|
}),
|
|
388
395
|
]);
|
|
389
396
|
|
|
@@ -529,6 +536,12 @@ export const FINAL_FIELDS = Object.freeze([
|
|
|
529
536
|
shape: "a string",
|
|
530
537
|
describe: "What it sounds like.",
|
|
531
538
|
}),
|
|
539
|
+
Object.freeze({
|
|
540
|
+
name: "ipa",
|
|
541
|
+
kind: "string",
|
|
542
|
+
shape: "a string in the International Phonetic Alphabet, such as `ʃ`",
|
|
543
|
+
describe: "Its sound, in the International Phonetic Alphabet.",
|
|
544
|
+
}),
|
|
532
545
|
]);
|
|
533
546
|
|
|
534
547
|
/**
|
|
@@ -718,7 +731,8 @@ export const STRESS_FIELDS = Object.freeze([
|
|
|
718
731
|
required: true,
|
|
719
732
|
allowed: STRESS_LONGER,
|
|
720
733
|
shape: `one of ${STRESS_LONGER.join(", ")}`,
|
|
721
|
-
describe:
|
|
734
|
+
describe:
|
|
735
|
+
"The stressed syllable of a word of three or more: `first`, or counted from its end.",
|
|
722
736
|
}),
|
|
723
737
|
Object.freeze({
|
|
724
738
|
name: "mark",
|
|
@@ -914,10 +928,10 @@ export const SOUND_FIELDS = Object.freeze([
|
|
|
914
928
|
Object.freeze({
|
|
915
929
|
name: "letters",
|
|
916
930
|
kind: "list",
|
|
917
|
-
shape: "list of `{ letters, kind, like
|
|
931
|
+
shape: "list of `{ letters, kind, like?, place?, ipa?, tongue? }`",
|
|
918
932
|
entries: Object.freeze({
|
|
919
933
|
kind: "map",
|
|
920
|
-
shape: "a map — `{ letters, kind, like
|
|
934
|
+
shape: "a map — `{ letters, kind, like?, place?, ipa?, tongue? }`",
|
|
921
935
|
fields: LETTER_FIELDS,
|
|
922
936
|
}),
|
|
923
937
|
describe: "The letters the language writes, and the sounds they stand for.",
|
|
@@ -950,10 +964,10 @@ export const SOUND_FIELDS = Object.freeze([
|
|
|
950
964
|
Object.freeze({
|
|
951
965
|
name: "finals",
|
|
952
966
|
kind: "list",
|
|
953
|
-
shape: "list of `{ final, like }`",
|
|
967
|
+
shape: "list of `{ final, like, ipa? }`",
|
|
954
968
|
entries: Object.freeze({
|
|
955
969
|
kind: "map",
|
|
956
|
-
shape: "a map — `{ final, like }`",
|
|
970
|
+
shape: "a map — `{ final, like, ipa? }`",
|
|
957
971
|
fields: FINAL_FIELDS,
|
|
958
972
|
}),
|
|
959
973
|
describe: "The syllable finals, for a language whose syllables are parsed by them.",
|
|
@@ -1047,6 +1061,16 @@ export const SOUND_FIELDS = Object.freeze([
|
|
|
1047
1061
|
}),
|
|
1048
1062
|
]);
|
|
1049
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* The keys of a spelling guide's `data.sounds`: a `doc` note stating the
|
|
1066
|
+
* letters and marks every tongue of a setting is written by.
|
|
1067
|
+
*
|
|
1068
|
+
* @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
|
|
1069
|
+
*/
|
|
1070
|
+
export const GUIDE_SOUND_FIELDS = Object.freeze(
|
|
1071
|
+
SOUND_FIELDS.filter((field) => field.name === "letters" || field.name === "marks"),
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1050
1074
|
/**
|
|
1051
1075
|
* The keys of one `data.soundChanges` entry.
|
|
1052
1076
|
*
|
package/engine/map-lexicon.mjs
CHANGED
|
@@ -55,6 +55,7 @@ import { positionOfFrontmatterPath, positionOfYamlPath } from "./diagnostics.mjs
|
|
|
55
55
|
import { isNoteRecord } from "./index-records.mjs";
|
|
56
56
|
import { nearest } from "./near-miss.mjs";
|
|
57
57
|
import { WRITTEN_ENDINGS } from "./language-terms.mjs";
|
|
58
|
+
import { fromWrittenIn } from "./written-in.mjs";
|
|
58
59
|
import { writingsOf } from "./logographic.mjs";
|
|
59
60
|
import { FONTS_PACKAGE } from "./script-fonts.mjs";
|
|
60
61
|
import { GENERIC_FAMILIES, applyMap } from "./text-maps.mjs";
|
|
@@ -386,14 +387,37 @@ export function fontProblem(map, font) {
|
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
/**
|
|
389
|
-
* The mapping a command uses, or what is wrong with what it was given.
|
|
390
|
-
*
|
|
390
|
+
* The mapping a command uses, or what is wrong with what it was given. The
|
|
391
|
+
* language's `data.writtenIn` fills the map and the face it leaves out, as it
|
|
392
|
+
* fills a run's; after that, a map listing one face defaults to it, and one
|
|
393
|
+
* listing more needs `font`.
|
|
391
394
|
*
|
|
392
395
|
* @param {object} ctx - The run context.
|
|
393
|
-
* @param {{map
|
|
396
|
+
* @param {{map?: string, font?: string, lang?: string}} keys - What was asked for.
|
|
394
397
|
* @returns {{mapping: Mapping}|{problem: string, about: "map"|"lang"|"font"}} The mapping or the problem.
|
|
395
398
|
*/
|
|
396
|
-
function commandMapping(ctx,
|
|
399
|
+
function commandMapping(ctx, keys) {
|
|
400
|
+
const { lang } = keys;
|
|
401
|
+
const filled = fromWrittenIn(keys, ctx?.writtenIn, ctx?.maps);
|
|
402
|
+
const map = keys.map ?? filled.map;
|
|
403
|
+
const font = keys.font ?? filled.font;
|
|
404
|
+
if (map === undefined) {
|
|
405
|
+
if (lang !== undefined && !Object.hasOwn(ctx?.languages ?? {}, lang)) {
|
|
406
|
+
const near = nearest(lang, Object.keys(ctx?.languages ?? {}));
|
|
407
|
+
return {
|
|
408
|
+
about: "lang",
|
|
409
|
+
problem:
|
|
410
|
+
`\`${lang}\` is not a declared language (\`languages\` in ${CONFIG})` +
|
|
411
|
+
(near ? `; did you mean \`${near}\`?` : ""),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
about: "map",
|
|
416
|
+
problem:
|
|
417
|
+
`\`${lang}\` names no script: its note states no \`data.writtenIn\`, so name a ` +
|
|
418
|
+
"map with `--map`",
|
|
419
|
+
};
|
|
420
|
+
}
|
|
397
421
|
const textMap = ctx?.maps?.get(map);
|
|
398
422
|
if (!textMap) {
|
|
399
423
|
const names = [...(ctx?.maps?.keys() ?? [])];
|
|
@@ -543,8 +567,9 @@ export function transliterate(ctx, keys, text) {
|
|
|
543
567
|
* writing, the signs a run writes directly for that sense.
|
|
544
568
|
*
|
|
545
569
|
* @param {object} ctx - The run context.
|
|
546
|
-
* @param {{lang: string, map
|
|
547
|
-
* The language, the map, the face, and whether to list derived forms.
|
|
570
|
+
* @param {{lang: string, map?: string, font?: string, derived?: boolean}} keys -
|
|
571
|
+
* The language, the map, the face, and whether to list derived forms. A map
|
|
572
|
+
* or a face left out is the one the language's `data.writtenIn` names.
|
|
548
573
|
* @returns {{entries: Array<{form: string, native: string, exception?: boolean, source?: "own"|"parts"|"none", ambiguous?: true, class: string|null, gloss: string|null, from?: string}>}|{problem: string, about: string}}
|
|
549
574
|
* The words in `data.words` order, then any derived forms, or the problem.
|
|
550
575
|
*/
|
|
@@ -139,8 +139,8 @@ import {
|
|
|
139
139
|
checkLanguageWords,
|
|
140
140
|
} from "./language-words.mjs";
|
|
141
141
|
import { checkGlossary } from "./glossary-entries.mjs";
|
|
142
|
-
import { LANGUAGE_DATA_FIELDS } from "./language-terms.mjs";
|
|
143
|
-
import { checkLanguageData } from "./language-data.mjs";
|
|
142
|
+
import { GUIDE_SOUND_FIELDS, LANGUAGE_DATA_FIELDS } from "./language-terms.mjs";
|
|
143
|
+
import { checkGuideSounds, checkLanguageData } from "./language-data.mjs";
|
|
144
144
|
import { checkAffiliationReach } from "./culture-reach.mjs";
|
|
145
145
|
import { checkLanguageNames, requiredGivenGenders } from "./language-names.mjs";
|
|
146
146
|
import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
|
|
@@ -163,7 +163,12 @@ import { checkMapField } from "./text-maps.mjs";
|
|
|
163
163
|
import { checkConceptList, checkCoreFlag } from "./core-vocabulary.mjs";
|
|
164
164
|
import { METRE_KINDS, checkMetresField } from "./metre-declaration.mjs";
|
|
165
165
|
import { checkLanguageEmphasis, checkScriptField } from "./script-emphasis.mjs";
|
|
166
|
-
import {
|
|
166
|
+
import { WRITTEN_IN_KEY, checkWrittenIn } from "./written-in.mjs";
|
|
167
|
+
import {
|
|
168
|
+
EMPHASIS_CONVENTIONS,
|
|
169
|
+
LANGUAGE_EMPHASES,
|
|
170
|
+
SCRIPT_DIRECTIONS,
|
|
171
|
+
} from "./script-emphasis-terms.mjs";
|
|
167
172
|
|
|
168
173
|
/**
|
|
169
174
|
* One `data:` key a note type may carry.
|
|
@@ -2182,10 +2187,36 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2182
2187
|
"and `words` marks it with a word, so either is a finding. Only a skill of " +
|
|
2183
2188
|
"`subType: language` states it.",
|
|
2184
2189
|
},
|
|
2190
|
+
{
|
|
2191
|
+
name: WRITTEN_IN_KEY,
|
|
2192
|
+
kind: "map",
|
|
2193
|
+
shape: "a map — `{ map, font }`",
|
|
2194
|
+
check: checkWrittenIn,
|
|
2195
|
+
fields: Object.freeze([
|
|
2196
|
+
{
|
|
2197
|
+
name: "map",
|
|
2198
|
+
kind: "string",
|
|
2199
|
+
shape: "a mapping note's `data.map.name`",
|
|
2200
|
+
required: true,
|
|
2201
|
+
describe: "The map the language's text is written by.",
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
name: "font",
|
|
2205
|
+
kind: "string",
|
|
2206
|
+
shape: "an installed face's slug",
|
|
2207
|
+
required: true,
|
|
2208
|
+
describe: "The face the text is set in, one the map is drawn in.",
|
|
2209
|
+
},
|
|
2210
|
+
]),
|
|
2211
|
+
describe:
|
|
2212
|
+
"The script the language is written in, as the map and the face a run marked " +
|
|
2213
|
+
"with its name alone takes; a run's own `map=` or `font=` overrides it. Only a " +
|
|
2214
|
+
"skill of `subType: language` states it.",
|
|
2215
|
+
},
|
|
2185
2216
|
{
|
|
2186
2217
|
name: "script",
|
|
2187
2218
|
kind: "map",
|
|
2188
|
-
shape: "a map — `{ fonts, emphasis, strongEmphasis? }`",
|
|
2219
|
+
shape: "a map — `{ fonts, emphasis, strongEmphasis?, direction? }`",
|
|
2189
2220
|
check: checkScriptField,
|
|
2190
2221
|
fields: Object.freeze([
|
|
2191
2222
|
{
|
|
@@ -2220,10 +2251,20 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2220
2251
|
allowed: EMPHASIS_CONVENTIONS,
|
|
2221
2252
|
describe: "The convention `**…**` takes instead of `emphasis`'s.",
|
|
2222
2253
|
},
|
|
2254
|
+
{
|
|
2255
|
+
name: "direction",
|
|
2256
|
+
kind: "string",
|
|
2257
|
+
shape: "`vertical-rl`",
|
|
2258
|
+
allowed: SCRIPT_DIRECTIONS,
|
|
2259
|
+
describe:
|
|
2260
|
+
"How the script is written: `vertical-rl` is columns, top to bottom, " +
|
|
2261
|
+
"read from the right, which a `scroll` fence sets. A script stating " +
|
|
2262
|
+
"none is written in lines.",
|
|
2263
|
+
},
|
|
2223
2264
|
]),
|
|
2224
2265
|
describe:
|
|
2225
|
-
"A script note's faces
|
|
2226
|
-
"`subType: script` states it.",
|
|
2266
|
+
"A script note's faces, how it marks emphasis and the direction it is " +
|
|
2267
|
+
"written in. Only a skill of `subType: script` states it.",
|
|
2227
2268
|
},
|
|
2228
2269
|
]),
|
|
2229
2270
|
}),
|
|
@@ -2323,6 +2364,17 @@ export const NOTE_VOCABULARY = Object.freeze({
|
|
|
2323
2364
|
"The culture a setting guide introduces, as a culture lore note. " +
|
|
2324
2365
|
"Required on `settingguide`; no other subType states it.",
|
|
2325
2366
|
},
|
|
2367
|
+
{
|
|
2368
|
+
name: "sounds",
|
|
2369
|
+
kind: "map",
|
|
2370
|
+
shape: "a map — `{ letters?, marks? }`",
|
|
2371
|
+
fields: GUIDE_SOUND_FIELDS,
|
|
2372
|
+
check: checkGuideSounds,
|
|
2373
|
+
describe:
|
|
2374
|
+
"A spelling guide's letters and the marks written on them, for every " +
|
|
2375
|
+
"tongue of the setting: each letter row's `tongue` names the language notes " +
|
|
2376
|
+
"that write it.",
|
|
2377
|
+
},
|
|
2326
2378
|
{
|
|
2327
2379
|
name: "map",
|
|
2328
2380
|
kind: "map",
|
package/engine/pdf-fonts.mjs
CHANGED
|
@@ -55,7 +55,7 @@ import { familyOf } from "./content-icons.mjs";
|
|
|
55
55
|
* @param {Buffer} buf - The font file.
|
|
56
56
|
* @returns {Map<string, {offset: number, length: number}>} Its table directory.
|
|
57
57
|
*/
|
|
58
|
-
function tableDirectory(buf) {
|
|
58
|
+
export function tableDirectory(buf) {
|
|
59
59
|
const tables = new Map();
|
|
60
60
|
if (buf.length < 12) return tables;
|
|
61
61
|
// A TrueType collection points at its first font; a bare font starts at 0.
|
package/engine/pdf-render.mjs
CHANGED
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* A note's markdown, and a document plan, rendered as Typst source.
|
|
16
16
|
*
|
|
17
|
-
* **This module emits text and reads nothing
|
|
18
|
-
* returns a `.typ` document; the filesystem, the note
|
|
19
|
-
* that turns the result into a PDF all live in
|
|
20
|
-
* {@link module:engine/pdf-build}.
|
|
17
|
+
* **This module emits text and reads nothing but a face's outlines.** It takes
|
|
18
|
+
* markdown and a plan and returns a `.typ` document; the filesystem, the note
|
|
19
|
+
* bodies and the compiler that turns the result into a PDF all live in
|
|
20
|
+
* {@link module:engine/pdf-build}. The one file it reads is the TrueType face a
|
|
21
|
+
* `scroll` fence is drawn from, through {@link module:engine/font-outlines}. That split is what lets the outline, the
|
|
21
22
|
* table of contents, every anchor and every link destination be asserted in a
|
|
22
23
|
* unit test with no renderer installed — which is most of what a book has to
|
|
23
24
|
* get right, and all of what a test can check without eyes.
|
|
@@ -95,6 +96,8 @@ import {
|
|
|
95
96
|
verseStanzas,
|
|
96
97
|
} from "./content-verse.mjs";
|
|
97
98
|
import { matchAllOutsideCode, replaceOutsideCode } from "./code-fences.mjs";
|
|
99
|
+
import { panelRoman, scanScrolls, scrollPanels, scrollParagraphs } from "./content-scroll.mjs";
|
|
100
|
+
import { BOOK_INK, BOOK_RUBRIC, fontOutlines, scrollPanelSvg } from "./font-outlines.mjs";
|
|
98
101
|
import { HTML_TAG, htmlMessage } from "./content-html.mjs";
|
|
99
102
|
import { EXPRESSION } from "./markdown-expressions.mjs";
|
|
100
103
|
import { inlineText } from "./name-markup.mjs";
|
|
@@ -498,11 +501,12 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
498
501
|
// is an offset into *this* string, and the renderer below walks slices of it.
|
|
499
502
|
if (!opts.nested) reportUnrenderable(authored, definitions, opts, given);
|
|
500
503
|
// Text marked with a language, a font or a map is rewritten once, ahead of
|
|
501
|
-
// every other pass, line for line; the nested calls below read the result
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
504
|
+
// every other pass, line for line; the nested calls below read the result,
|
|
505
|
+
// and the scroll fences' layouts the rewrite returns.
|
|
506
|
+
const marked =
|
|
507
|
+
opts.nested ? undefined : renderRuns(authored, opts.runs, undefined, { target: "book" });
|
|
508
|
+
const source = marked ? marked.markdown : authored;
|
|
509
|
+
if (marked) sharedOptions.scrolls = marked.scrolls;
|
|
506
510
|
// One map for the whole body, not one per block: a heading inside a
|
|
507
511
|
// blockquote or a list item shares the entry's anchor namespace with every
|
|
508
512
|
// other heading in the same body, because `sectionLabel` scopes by entry
|
|
@@ -522,6 +526,7 @@ export function markdownToTypst(markdown, opts = {}) {
|
|
|
522
526
|
seen: new Map(),
|
|
523
527
|
footnoteState,
|
|
524
528
|
footnotePrefix: opts.footnotePrefix ?? `footnote-${slugify(anchorPrefix || "body")}`,
|
|
529
|
+
scrolls: marked?.scrolls ?? opts.scrolls ?? new Map(),
|
|
525
530
|
};
|
|
526
531
|
// A withheld section is set in the box a `:::secret` is set in, and its
|
|
527
532
|
// heading is set inside it: a printed page honours no reader permission, so
|
|
@@ -752,6 +757,10 @@ function renderBlock(tokens, i, out, ctx) {
|
|
|
752
757
|
}
|
|
753
758
|
case "fence":
|
|
754
759
|
case "code_block": {
|
|
760
|
+
if (token.type === "fence" && /^scroll(?:\s|$)/.test(token.info?.trim() || "")) {
|
|
761
|
+
out.push(scrollBlock(token, ctx));
|
|
762
|
+
return 1;
|
|
763
|
+
}
|
|
755
764
|
if (token.type === "fence" && /^verse(?:\s|$)/.test(token.info?.trim() || "")) {
|
|
756
765
|
const opening = `${token.markup}${token.info}`;
|
|
757
766
|
const poem = scanVerse(`${opening}\n${token.content}${token.markup}`).blocks[0];
|
|
@@ -1199,6 +1208,38 @@ function padCells(cells, columns) {
|
|
|
1199
1208
|
return padded.map((c) => `[${c}]`).join(", ");
|
|
1200
1209
|
}
|
|
1201
1210
|
|
|
1211
|
+
/**
|
|
1212
|
+
* A `scroll` fence as the book sets it: Typst has no vertical writing, so each
|
|
1213
|
+
* panel of at most `rows` × `cols` characters is an image drawn from the
|
|
1214
|
+
* face's outlines, its roman text as alt text. The panels sit in a paragraph
|
|
1215
|
+
* set right to left, so they are read from the right and wrap to the next
|
|
1216
|
+
* line where the measure ends; without `cols`, each column is a panel, and as
|
|
1217
|
+
* many columns sit side by side as fit. A fence the rewrite of marked text did
|
|
1218
|
+
* not set, or whose face has no TrueType file, is set as code.
|
|
1219
|
+
*
|
|
1220
|
+
* @param {object} token - The fence token.
|
|
1221
|
+
* @param {object} ctx - Render context, holding the scroll layouts.
|
|
1222
|
+
* @returns {string} Typst markup.
|
|
1223
|
+
*/
|
|
1224
|
+
function scrollBlock(token, ctx) {
|
|
1225
|
+
const scroll = scanScrolls(`${token.markup}${token.info}\n${token.content}${token.markup}`)
|
|
1226
|
+
.blocks[0];
|
|
1227
|
+
const layout = ctx.scrolls?.get(scrollParagraphs(token.content.split("\n")).join("\n\n"));
|
|
1228
|
+
if (!scroll || scroll.invalid || !layout?.face?.file)
|
|
1229
|
+
return rawBlock(token.content, token.info?.trim() || "");
|
|
1230
|
+
const outlines = fontOutlines(layout.face.file);
|
|
1231
|
+
const { rows, panels } = scrollPanels(layout.paragraphs, scroll);
|
|
1232
|
+
const boxes = panels.map(
|
|
1233
|
+
(panel) =>
|
|
1234
|
+
`#box(image(bytes("${escapeTypstString(scrollPanelSvg(panel, rows, outlines))}"), ` +
|
|
1235
|
+
`format: "svg", height: ${rows}em, alt: "${escapeTypstString(panelRoman(panel, layout.paragraphs))}"))`,
|
|
1236
|
+
);
|
|
1237
|
+
// A zero-width space lets a line break between two columns and adds no
|
|
1238
|
+
// width; panels of several columns are kept a column gap apart.
|
|
1239
|
+
const between = scroll.cols === undefined ? "\u200B" : "#h(1em, weak: true) ";
|
|
1240
|
+
return `\n#block(width: 100%)[#set text(dir: rtl)\n${boxes.join(between)}\n]\n\n`;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1202
1243
|
/**
|
|
1203
1244
|
* A fenced block as Typst raw text.
|
|
1204
1245
|
*
|
|
@@ -1944,10 +1985,10 @@ export function bookTypstPreamble(runs = {}, families = {}) {
|
|
|
1944
1985
|
// The title sets short of the measure, so a plate's last line does not
|
|
1945
1986
|
// run to the trimmed edge of the paper.
|
|
1946
1987
|
"#let book-title-measure = book-text-width - 2.5cm",
|
|
1947
|
-
|
|
1988
|
+
`#let book-ink = rgb("${BOOK_INK}")`,
|
|
1948
1989
|
// A rubric script's emphasis: a vermilion that holds 4.5:1 on the paper
|
|
1949
1990
|
// and stays apart from the ink in lightness.
|
|
1950
|
-
|
|
1991
|
+
`#let book-rubric = rgb("${BOOK_RUBRIC}")`,
|
|
1951
1992
|
'#let book-paper = rgb("#f4efe4")',
|
|
1952
1993
|
'#let book-accent = rgb("#7c3b1e")',
|
|
1953
1994
|
'#let book-head = rgb("#5e2b14")',
|
package/engine/run-rows.mjs
CHANGED
|
@@ -17,6 +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
|
+
* A run's `font` and `map` are the ones it is set with: its own, else those of
|
|
21
|
+
* the div or fence around it, else those its language's note names in
|
|
22
|
+
* `data.writtenIn`, filled by {@link fillWrittenIn} once every record exists.
|
|
23
|
+
*
|
|
20
24
|
* @module
|
|
21
25
|
*/
|
|
22
26
|
|
|
@@ -24,6 +28,9 @@ import { canonicalKey, readCanonicalKey } from "./address.mjs";
|
|
|
24
28
|
import { encodeAddresses } from "./address-values.mjs";
|
|
25
29
|
import { isNoteRecord } from "./index-records.mjs";
|
|
26
30
|
import { scanRuns } from "./content-scripts.mjs";
|
|
31
|
+
import { languageRecordsByName } from "./map-lexicon.mjs";
|
|
32
|
+
import { mapsFromRecords } from "./text-maps.mjs";
|
|
33
|
+
import { fromWrittenIn, writtenInOf } from "./written-in.mjs";
|
|
27
34
|
|
|
28
35
|
/** Every column of the `runs` relation, in order. */
|
|
29
36
|
export const RUN_COLUMNS = Object.freeze([
|
|
@@ -41,12 +48,14 @@ export const RUN_COLUMNS = Object.freeze([
|
|
|
41
48
|
* One run as an index record carries it.
|
|
42
49
|
*
|
|
43
50
|
* @typedef {object} RecordRun
|
|
44
|
-
* @property {"span"|"block"|"verse"} kind - The construct.
|
|
51
|
+
* @property {"span"|"block"|"verse"|"scroll"} kind - The construct.
|
|
45
52
|
* @property {string|null} lang - The language it is set in, as written: its
|
|
46
53
|
* own `lang=`, else the one the div or fence around it states.
|
|
47
54
|
* @property {string|null} tag - The BCP 47 tag it resolves to, or null.
|
|
48
|
-
* @property {string|null} font - Its font, as written and inherited the same
|
|
49
|
-
*
|
|
55
|
+
* @property {string|null} font - Its font, as written and inherited the same
|
|
56
|
+
* way, else its language's script's.
|
|
57
|
+
* @property {string|null} map - Its map, as written and inherited the same way,
|
|
58
|
+
* else its language's script's.
|
|
50
59
|
* @property {number} line - 1-based file line of its opening.
|
|
51
60
|
* @property {number} column - 1-based UTF-16 file column of its opening.
|
|
52
61
|
*/
|
|
@@ -80,6 +89,39 @@ export function recordRuns(body, { context, bodyLine = 1, bodyColumn = 1, syntax
|
|
|
80
89
|
}));
|
|
81
90
|
}
|
|
82
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Give every recorded run the map and face its language's note names in
|
|
94
|
+
* `data.writtenIn`, where the run leaves them unstated, as a run is set. Read
|
|
95
|
+
* once every record exists, since a run's language note may be recorded after
|
|
96
|
+
* the note that marks it.
|
|
97
|
+
*
|
|
98
|
+
* @param {Record<string, any>[]} records - The package's index records; each
|
|
99
|
+
* note record's `runs` is updated in place.
|
|
100
|
+
* @param {{languages?: Readonly<Record<string, {tag: string, note?: string}>>, contentPackage?: string}} config -
|
|
101
|
+
* The configured languages, whose `note` names each language note.
|
|
102
|
+
*/
|
|
103
|
+
export function fillWrittenIn(records, config) {
|
|
104
|
+
const maps = mapsFromRecords(records);
|
|
105
|
+
const scripts = writtenInOf(languageRecordsByName(records, config), maps);
|
|
106
|
+
if (!scripts.size) return;
|
|
107
|
+
for (const record of records) {
|
|
108
|
+
if (!record || !isNoteRecord(record) || !Array.isArray(record.runs)) continue;
|
|
109
|
+
for (const run of record.runs) {
|
|
110
|
+
const filled = fromWrittenIn(
|
|
111
|
+
{
|
|
112
|
+
lang: run.lang ?? undefined,
|
|
113
|
+
map: run.map ?? undefined,
|
|
114
|
+
font: run.font ?? undefined,
|
|
115
|
+
},
|
|
116
|
+
scripts,
|
|
117
|
+
maps,
|
|
118
|
+
);
|
|
119
|
+
if (filled.map !== undefined) run.map = filled.map;
|
|
120
|
+
if (filled.font !== undefined) run.font = filled.font;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
83
125
|
/**
|
|
84
126
|
* The containing page's NOTE identity.
|
|
85
127
|
*
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* **The direction a script is written in.** A script note states it beside its
|
|
16
|
+
* faces:
|
|
17
|
+
*
|
|
18
|
+
* ```yaml
|
|
19
|
+
* data:
|
|
20
|
+
* script:
|
|
21
|
+
* fonts: [teng-chou]
|
|
22
|
+
* emphasis: rubric
|
|
23
|
+
* direction: vertical-rl
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* `vertical-rl` is a script written in columns, top to bottom, the columns
|
|
27
|
+
* read from the right. A script that states no direction is written in lines.
|
|
28
|
+
* Markup never states a direction: a `scroll` fence takes its script's, and is
|
|
29
|
+
* refused in a script written in lines; see
|
|
30
|
+
* {@link module:engine/content-scroll}.
|
|
31
|
+
*
|
|
32
|
+
* @module
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { isNoteRecord } from "./index-records.mjs";
|
|
36
|
+
import { SCRIPT_DIRECTIONS, SCRIPT_SUBTYPE } from "./script-emphasis-terms.mjs";
|
|
37
|
+
|
|
38
|
+
export { SCRIPT_DIRECTIONS } from "./script-emphasis-terms.mjs";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Each face's direction, from the script notes among the index records. The
|
|
42
|
+
* first note to list a face keeps it; a face whose note states no direction,
|
|
43
|
+
* or one the build does not know, is absent.
|
|
44
|
+
*
|
|
45
|
+
* @param {Iterable<Record<string, any>>|undefined} records - Index records.
|
|
46
|
+
* @returns {Map<string, "vertical-rl">} The directions, by face slug.
|
|
47
|
+
*/
|
|
48
|
+
export function directionsFromRecords(records) {
|
|
49
|
+
const out = new Map();
|
|
50
|
+
const listed = new Set();
|
|
51
|
+
for (const record of records ?? []) {
|
|
52
|
+
if (!record || !isNoteRecord(record)) continue;
|
|
53
|
+
if (
|
|
54
|
+
record.type !== "skill" ||
|
|
55
|
+
String(record.subType ?? "").toLowerCase() !== SCRIPT_SUBTYPE
|
|
56
|
+
)
|
|
57
|
+
continue;
|
|
58
|
+
const script = record.data?.script;
|
|
59
|
+
if (!script || typeof script !== "object" || !Array.isArray(script.fonts)) continue;
|
|
60
|
+
for (const slug of script.fonts) {
|
|
61
|
+
if (typeof slug !== "string" || listed.has(slug)) continue;
|
|
62
|
+
listed.add(slug);
|
|
63
|
+
if (SCRIPT_DIRECTIONS.includes(script.direction)) out.set(slug, script.direction);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* **The words a script note and a language note state emphasis
|
|
16
|
-
* the vocabulary as it is declared, so this module imports
|
|
17
|
-
* checks and the conventions' use are in
|
|
15
|
+
* **The words a script note and a language note state emphasis and direction
|
|
16
|
+
* by.** Read by the vocabulary as it is declared, so this module imports
|
|
17
|
+
* nothing; the checks and the conventions' use are in
|
|
18
|
+
* {@link module:engine/script-emphasis} and {@link module:engine/script-direction}.
|
|
18
19
|
*
|
|
19
20
|
* @module
|
|
20
21
|
*/
|
|
@@ -34,5 +35,12 @@ export const EMPHASIS_CLASSES = Object.freeze({
|
|
|
34
35
|
*/
|
|
35
36
|
export const LANGUAGE_EMPHASES = Object.freeze(["words", ...EMPHASIS_CONVENTIONS]);
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The directions a script states it is written in. `vertical-rl` is columns,
|
|
40
|
+
* top to bottom, read from the right; a script stating none is written in
|
|
41
|
+
* lines.
|
|
42
|
+
*/
|
|
43
|
+
export const SCRIPT_DIRECTIONS = Object.freeze(["vertical-rl"]);
|
|
44
|
+
|
|
37
45
|
/** The `subType` of a script note. */
|
|
38
46
|
export const SCRIPT_SUBTYPE = "script";
|