@heroiclands/package-build 24.0.4 → 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 +36 -0
- package/README.md +6 -0
- package/assets/styles/content-base.css +25 -10
- package/docs/authoring/links-and-markup.md +215 -179
- 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 +53 -35
- 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 +348 -140
- package/engine/content-spans.mjs +13 -1
- package/engine/content-verse.mjs +158 -373
- package/engine/note-vocabulary.mjs +5 -5
- package/engine/pdf-render.mjs +63 -44
- package/engine/run-rows.mjs +18 -20
- 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/run-rows.d.mts +16 -9
- package/types/engine/verse-metre.d.mts +4 -3
|
@@ -51,18 +51,26 @@
|
|
|
51
51
|
* form. Any other
|
|
52
52
|
* word is a finding at its first character, once per run; see
|
|
53
53
|
* {@link module:engine/run-words}.
|
|
54
|
-
* - **A run inside a div or
|
|
55
|
-
* keys override them.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
54
|
+
* - **A run inside a span, div or verse fence inherits its keys**, and its
|
|
55
|
+
* own keys override them. A span switching to a different language selects
|
|
56
|
+
* that language's script defaults and its own explicit font and map. A span
|
|
57
|
+
* in the same language keeps the surrounding script choices.
|
|
58
|
+
* - **Text with a font, a map or a declared non-English language is plain.** A span or a
|
|
59
|
+
* verse holds no emphasis, image, footnote, wikilink, code or HTML, and no
|
|
60
|
+
* span but a marked one. A standalone resolved reference sets only its
|
|
61
|
+
* label; links embedded in longer native prose remain forbidden. A div
|
|
62
|
+
* holds paragraphs and line breaks only.
|
|
63
|
+
* - **A non-English span holds no bold or italic markup**, including in a
|
|
64
|
+
* script face. English tags and configured names resolving to English
|
|
65
|
+
* retain their emphasis under the font's rules. Escaped delimiters are text.
|
|
66
|
+
* - **Other text set in a face takes `*…*` and `**…**`**, set by the convention its
|
|
60
67
|
* language's note states, else its face's script note, else plainly; see
|
|
61
68
|
* {@link module:engine/script-emphasis}. A language that marks emphasis
|
|
62
69
|
* with a word makes either a finding at the emphasis.
|
|
63
70
|
* The text is read as written, so `[`, `]`, `"`, `<<`, `>>`, `!` and
|
|
64
71
|
* `@#$%^&` are characters, and need no escaping. A real language's tag alone
|
|
65
|
-
* marks text and leaves its Markdown alone
|
|
72
|
+
* marks text and leaves its Markdown alone, subject to the non-English span
|
|
73
|
+
* rule. Verse indentation periods remain layout rather than mapped text.
|
|
66
74
|
* - **A face draws only what it lists.** After the map, a character that is
|
|
67
75
|
* neither whitespace nor in the face's `codepoints` is a finding, at the
|
|
68
76
|
* source character it came from. `mono`, `serif` and `sans` are not checked.
|
|
@@ -84,17 +92,7 @@
|
|
|
84
92
|
import MarkdownIt from "markdown-it";
|
|
85
93
|
|
|
86
94
|
import { scanBlocks, BLOCK_CONTAINERS } from "./content-blocks.mjs";
|
|
87
|
-
import {
|
|
88
|
-
COLUMN_KEYS,
|
|
89
|
-
COLUMNS_CLASS,
|
|
90
|
-
expandVerse,
|
|
91
|
-
expandVerseOrigins,
|
|
92
|
-
scanVerse,
|
|
93
|
-
sourcePosition,
|
|
94
|
-
hasColumns,
|
|
95
|
-
verseColumns,
|
|
96
|
-
STANZA_BREAK,
|
|
97
|
-
} from "./content-verse.mjs";
|
|
95
|
+
import { scanVerse, hasColumns, verseLine, verseGroups } from "./content-verse.mjs";
|
|
98
96
|
import { scanSpans } from "./content-spans.mjs";
|
|
99
97
|
import { scanScrolls, scrollParagraphs } from "./content-scroll.mjs";
|
|
100
98
|
import { directionsFromRecords } from "./script-direction.mjs";
|
|
@@ -114,11 +112,17 @@ import {
|
|
|
114
112
|
import { unknownWordMessage, unknownWords, wordBooksFromRecords } from "./run-words.mjs";
|
|
115
113
|
import { metreBooksFromRecords, metreFindings, metresFromRecords } from "./verse-metre.mjs";
|
|
116
114
|
import { applyMap, mapsFromRecords } from "./text-maps.mjs";
|
|
117
|
-
import {
|
|
115
|
+
import { writtenInOf } from "./written-in.mjs";
|
|
116
|
+
import { WIKILINK, parseWikilink } from "./wikilink-syntax.mjs";
|
|
117
|
+
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
118
|
+
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
118
119
|
|
|
119
120
|
/** The keys this module reads, which no construct passes through as written. */
|
|
120
121
|
export const RUN_KEYS = Object.freeze(["lang", "font", "map"]);
|
|
121
122
|
|
|
123
|
+
/** Explicit opt-in to native-script presentation on this construct. */
|
|
124
|
+
export const NATIVEFONT_CLASS = "nativefont";
|
|
125
|
+
|
|
122
126
|
/** The generic families `font=` may name, each with the class that sets it. */
|
|
123
127
|
export const GENERIC_FONTS = Object.freeze({
|
|
124
128
|
mono: "font-mono",
|
|
@@ -169,9 +173,9 @@ const CONFIG = "package-build.config.yaml";
|
|
|
169
173
|
/**
|
|
170
174
|
* One span, div, verse fence or scroll fence carrying `lang=`, `font=` or
|
|
171
175
|
* `map=`. Its `lang`, `font` and `map` are the ones it is set with: its own,
|
|
172
|
-
* else
|
|
173
|
-
*
|
|
174
|
-
*
|
|
176
|
+
* else inherited language or an ordinary prose family. Native map/face
|
|
177
|
+
* defaults are selected only by this construct's explicit `.nativefont`
|
|
178
|
+
* opt-in; marked children make their own presentation choice.
|
|
175
179
|
*
|
|
176
180
|
* @typedef {object} Run
|
|
177
181
|
* @property {"span"|"block"|"verse"|"scroll"} kind - Which construct.
|
|
@@ -186,6 +190,8 @@ const CONFIG = "package-build.config.yaml";
|
|
|
186
190
|
* 0-based opening line.
|
|
187
191
|
* @property {number} end - A span's offset past its attributes; a div's or
|
|
188
192
|
* fence's 0-based closing line.
|
|
193
|
+
* @property {boolean} [nativefont] - True only for an explicit native opt-in
|
|
194
|
+
* with its own language; retained by index records for deferred defaults.
|
|
189
195
|
*/
|
|
190
196
|
|
|
191
197
|
/** @type {WeakMap<object, RunContext>} */
|
|
@@ -347,6 +353,38 @@ function closing(text, open) {
|
|
|
347
353
|
return -1;
|
|
348
354
|
}
|
|
349
355
|
|
|
356
|
+
/**
|
|
357
|
+
* The label of exactly one resolved reference, as offsets within its span.
|
|
358
|
+
* Targets and the resolver's draft decoration are presentation, not words of
|
|
359
|
+
* the label's language. No surrounding prose belongs to this atom.
|
|
360
|
+
*
|
|
361
|
+
* @param {string} text - A span's entire content after wikilink resolution.
|
|
362
|
+
* @returns {[number, number]|undefined} The visible label's bounds.
|
|
363
|
+
*/
|
|
364
|
+
function referenceLabel(text) {
|
|
365
|
+
let offset = 0;
|
|
366
|
+
const draft =
|
|
367
|
+
/^<span class="sohl-draft-link" title="Draft — not yet written">([\s\S]*)<\/span>$/.exec(
|
|
368
|
+
text,
|
|
369
|
+
);
|
|
370
|
+
if (draft) {
|
|
371
|
+
offset = text.indexOf(">") + 1;
|
|
372
|
+
text = draft[1];
|
|
373
|
+
} else if (text.endsWith(" (draft)")) {
|
|
374
|
+
text = text.slice(0, -8);
|
|
375
|
+
}
|
|
376
|
+
const uuid = /^@UUID\[[^\]\r\n]+\]\{([^{}\r\n]*)\}$/.exec(text);
|
|
377
|
+
if (uuid) {
|
|
378
|
+
const start = text.indexOf("]{") + 2;
|
|
379
|
+
return [offset + start, offset + text.length - 1];
|
|
380
|
+
}
|
|
381
|
+
if (!text.startsWith("[")) return undefined;
|
|
382
|
+
const end = closing(text, 0);
|
|
383
|
+
if (end < 0 || text[end + 1] !== "(") return undefined;
|
|
384
|
+
if (closing(text, end + 1) !== text.length - 1) return undefined;
|
|
385
|
+
return [offset + 1, offset + end];
|
|
386
|
+
}
|
|
387
|
+
|
|
350
388
|
/**
|
|
351
389
|
* The markup in text that must be plain, each at its offset.
|
|
352
390
|
*
|
|
@@ -771,6 +809,8 @@ const BLOCK_WHAT = Object.freeze({
|
|
|
771
809
|
* @property {[number, number]|undefined} body - A div's or fence's body, as
|
|
772
810
|
* offsets: the first body line's start to the end of the last.
|
|
773
811
|
* @property {number|undefined} contentEnd - A span's offset of its closing `]`.
|
|
812
|
+
* @property {[number, number]|undefined} reference - A resolved reference's
|
|
813
|
+
* visible label bounds, by absolute offset; its target stays untouched.
|
|
774
814
|
* @property {import("./script-emphasis.mjs").LanguageEmphasis|undefined} languageEmphasis -
|
|
775
815
|
* The convention its language's note states.
|
|
776
816
|
* @property {import("./script-emphasis.mjs").Emphasis|undefined} conventions -
|
|
@@ -836,12 +876,30 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
836
876
|
const runKeys = (values) =>
|
|
837
877
|
Object.fromEntries(Object.entries(values).filter(([key]) => RUN_KEYS.includes(key)));
|
|
838
878
|
|
|
879
|
+
const nativeAt = (construct) => {
|
|
880
|
+
const opening = lines[construct.line0];
|
|
881
|
+
const from =
|
|
882
|
+
construct.kind === "span" ?
|
|
883
|
+
construct.node.contentEnd + 2
|
|
884
|
+
: starts[construct.line0] + opening.indexOf("{") + 1;
|
|
885
|
+
const end =
|
|
886
|
+
construct.kind === "span" ?
|
|
887
|
+
construct.where.end
|
|
888
|
+
: starts[construct.line0] + opening.lastIndexOf("}") + 1;
|
|
889
|
+
const to = text[end - 1] === "}" ? end - 1 : end;
|
|
890
|
+
// Attribute tokens keep quoted values intact, so a class-looking value
|
|
891
|
+
// or longer class cannot steal the opt-in's diagnostic location.
|
|
892
|
+
for (const token of text.slice(from, to).matchAll(/(?:[^\s"\\]|\\.|"(?:\\.|[^"\\])*")+/g))
|
|
893
|
+
if (token[0] === ".nativefont") return positionOf(starts, from + token.index);
|
|
894
|
+
return construct.where;
|
|
895
|
+
};
|
|
896
|
+
|
|
839
897
|
// Every construct that states a run key, with where it sits.
|
|
840
898
|
const constructs = [];
|
|
841
899
|
const spans = (syntax ? syntax(text).spans : scanSpans(text)).spans;
|
|
842
900
|
for (const span of spans) {
|
|
843
901
|
const keys = Object.fromEntries(span.attrs.filter(([key]) => RUN_KEYS.includes(key)));
|
|
844
|
-
if (!
|
|
902
|
+
if (!span.attrs.length && !span.id) continue;
|
|
845
903
|
const positions = span.attributePositions ?? {};
|
|
846
904
|
constructs.push({
|
|
847
905
|
kind: "span",
|
|
@@ -874,6 +932,15 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
874
932
|
});
|
|
875
933
|
const keys = runKeys(block.attributes);
|
|
876
934
|
if (block.name !== "div") {
|
|
935
|
+
if (block.classes?.includes(NATIVEFONT_CLASS))
|
|
936
|
+
report(
|
|
937
|
+
nativeAt({
|
|
938
|
+
kind: "block",
|
|
939
|
+
line0: first,
|
|
940
|
+
where: { line: first + 1, column: 1 },
|
|
941
|
+
}),
|
|
942
|
+
`\`.nativefont\` is read on a span, a div, a verse fence or a scroll fence, not on a ${block.name} block`,
|
|
943
|
+
);
|
|
877
944
|
for (const key of Object.keys(keys))
|
|
878
945
|
report(
|
|
879
946
|
at(key, "key"),
|
|
@@ -881,7 +948,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
881
948
|
);
|
|
882
949
|
} else {
|
|
883
950
|
divs.set(first, block);
|
|
884
|
-
if (Object.keys(keys).length)
|
|
951
|
+
if (Object.keys(keys).length || block.classes?.includes(NATIVEFONT_CLASS))
|
|
885
952
|
constructs.push({
|
|
886
953
|
kind: "block",
|
|
887
954
|
keys,
|
|
@@ -911,25 +978,10 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
911
978
|
collect(0, text);
|
|
912
979
|
|
|
913
980
|
const poems = new Map();
|
|
914
|
-
const romanColumnLines = new Set();
|
|
915
981
|
for (const poem of scanVerse(text).blocks) {
|
|
916
982
|
poems.set(poem.start, poem);
|
|
917
|
-
// Expansion writes one span per display entry. An entry with no map
|
|
918
|
-
// keeps the source spelling and page font, even when its language
|
|
919
|
-
// normally selects a script. The fence itself holds only structure.
|
|
920
|
-
if (poem.classes.includes(COLUMNS_CLASS)) {
|
|
921
|
-
const entries = verseColumns(poem, lines[poem.start]).flatMap(
|
|
922
|
-
(column) => column.entries,
|
|
923
|
-
);
|
|
924
|
-
let entry = 0;
|
|
925
|
-
for (let line = poem.start + 1; line < poem.end; line++) {
|
|
926
|
-
if (!lines[line].trim() || STANZA_BREAK.test(lines[line].trim())) continue;
|
|
927
|
-
if (entries[entry++ % entries.length]?.map === undefined)
|
|
928
|
-
romanColumnLines.add(line);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
983
|
const keys = runKeys(poem.attributes);
|
|
932
|
-
if (!Object.keys(keys).length) continue;
|
|
984
|
+
if (!Object.keys(keys).length && !poem.classes.includes(NATIVEFONT_CLASS)) continue;
|
|
933
985
|
const opening = lines[poem.start];
|
|
934
986
|
constructs.push({
|
|
935
987
|
kind: "verse",
|
|
@@ -987,17 +1039,31 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
987
1039
|
});
|
|
988
1040
|
}
|
|
989
1041
|
|
|
990
|
-
|
|
991
|
-
/** The containers around a construct, outermost first. */
|
|
1042
|
+
/** The constructs around a run, outermost first. */
|
|
992
1043
|
const around = (construct) =>
|
|
993
|
-
|
|
1044
|
+
constructs
|
|
994
1045
|
.filter(
|
|
995
1046
|
(container) =>
|
|
996
1047
|
container !== construct &&
|
|
997
|
-
container.
|
|
998
|
-
|
|
1048
|
+
(container.kind === "span" ?
|
|
1049
|
+
construct.kind === "span" &&
|
|
1050
|
+
container.where.start < construct.where.start &&
|
|
1051
|
+
container.where.end >= construct.where.end
|
|
1052
|
+
: container.line0 < construct.line0 && construct.line0 < container.last),
|
|
999
1053
|
)
|
|
1000
|
-
.sort((a, b) =>
|
|
1054
|
+
.sort((a, b) =>
|
|
1055
|
+
a.kind !== "span" && b.kind !== "span" ? a.line0 - b.line0
|
|
1056
|
+
: a.kind !== "span" ? -1
|
|
1057
|
+
: b.kind !== "span" ? 1
|
|
1058
|
+
: a.where.start - b.where.start,
|
|
1059
|
+
);
|
|
1060
|
+
const active = constructs.filter(
|
|
1061
|
+
(construct) =>
|
|
1062
|
+
construct.kind === "scroll" ||
|
|
1063
|
+
construct.own.classes.includes(NATIVEFONT_CLASS) ||
|
|
1064
|
+
Object.keys(construct.keys).length ||
|
|
1065
|
+
around(construct).some((container) => Object.keys(container.keys).length),
|
|
1066
|
+
);
|
|
1001
1067
|
|
|
1002
1068
|
/** A language's tag, or `undefined` when it resolves to none. */
|
|
1003
1069
|
const tagOf = (lang) =>
|
|
@@ -1006,12 +1072,44 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1006
1072
|
: isLanguageTag(lang) ? lang
|
|
1007
1073
|
: undefined;
|
|
1008
1074
|
|
|
1075
|
+
const native = (construct) => construct.own.classes.includes(NATIVEFONT_CLASS);
|
|
1076
|
+
|
|
1009
1077
|
const faceNames = () => [...faces.keys(), ...Object.keys(GENERIC_FONTS)];
|
|
1010
1078
|
|
|
1011
1079
|
/** Check the keys a construct states itself. */
|
|
1012
1080
|
const checkOwn = (construct) => {
|
|
1013
1081
|
const { lang, font, map } = construct.keys;
|
|
1014
1082
|
const { at } = construct;
|
|
1083
|
+
if (native(construct) && lang === undefined)
|
|
1084
|
+
report(
|
|
1085
|
+
nativeAt(construct),
|
|
1086
|
+
"`.nativefont` requires an explicit `lang=` on the same construct",
|
|
1087
|
+
);
|
|
1088
|
+
if (native(construct) && construct.kind === "verse" && hasColumns(construct.node))
|
|
1089
|
+
report(
|
|
1090
|
+
nativeAt(construct),
|
|
1091
|
+
"a verse with cols>1 opts individual runs into `.nativefont`, not the fence",
|
|
1092
|
+
);
|
|
1093
|
+
if (!native(construct)) {
|
|
1094
|
+
if (construct.kind === "scroll")
|
|
1095
|
+
report(
|
|
1096
|
+
construct.where,
|
|
1097
|
+
"a scroll requires `.nativefont` and an explicit `lang=` on the same construct",
|
|
1098
|
+
);
|
|
1099
|
+
else {
|
|
1100
|
+
if (font !== undefined && faces.has(font))
|
|
1101
|
+
report(
|
|
1102
|
+
at("font", "value"),
|
|
1103
|
+
`native \`font=${font}\` requires \`.nativefont\` and an explicit \`lang=\` on the same construct`,
|
|
1104
|
+
);
|
|
1105
|
+
if (map !== undefined && maps.has(map))
|
|
1106
|
+
report(
|
|
1107
|
+
at("map", "value"),
|
|
1108
|
+
`\`map=${map}\` requires \`.nativefont\` and an explicit \`lang=\` on the same construct`,
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1015
1113
|
if (lang !== undefined && !Object.hasOwn(languages, lang)) {
|
|
1016
1114
|
if (isLanguageTag(lang)) {
|
|
1017
1115
|
const declared = Object.keys(languages).find((name) => withinOneEdit(lang, name));
|
|
@@ -1053,10 +1151,102 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1053
1151
|
}
|
|
1054
1152
|
};
|
|
1055
1153
|
|
|
1056
|
-
|
|
1154
|
+
const nativeDefaults = (construct, keys) => {
|
|
1155
|
+
const defaults = ctx.writtenIn?.get(keys.lang);
|
|
1156
|
+
for (const key of ["map", "font"])
|
|
1157
|
+
if (keys[key] === undefined && defaults?.[key] !== undefined) keys[key] = defaults[key];
|
|
1158
|
+
if (keys.font === undefined)
|
|
1159
|
+
report(
|
|
1160
|
+
nativeAt(construct),
|
|
1161
|
+
`\`.nativefont\` has no default \`font=\` for \`${keys.lang}\`; state \`font=\` explicitly`,
|
|
1162
|
+
);
|
|
1163
|
+
if (keys.map !== undefined && !maps.has(keys.map))
|
|
1164
|
+
report(
|
|
1165
|
+
nativeAt(construct),
|
|
1166
|
+
`native default \`map=${keys.map}\` is unavailable; state \`map=\` explicitly`,
|
|
1167
|
+
);
|
|
1168
|
+
if (
|
|
1169
|
+
keys.font !== undefined &&
|
|
1170
|
+
!faces.has(keys.font) &&
|
|
1171
|
+
!Object.hasOwn(GENERIC_FONTS, keys.font)
|
|
1172
|
+
)
|
|
1173
|
+
report(
|
|
1174
|
+
nativeAt(construct),
|
|
1175
|
+
`native default \`font=${keys.font}\` is unavailable; state \`font=\` explicitly`,
|
|
1176
|
+
);
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
// Raw wikilinks belong to the resolver, not the span rewriter. Validate
|
|
1180
|
+
// their own control attributes with the same grammar before resolution,
|
|
1181
|
+
// without mapping a destination or consuming the trailing attributes.
|
|
1182
|
+
for (const match of matchAllOutsideCode(text, new RegExp(WIKILINK.source, "g"))) {
|
|
1183
|
+
const parsed = parseWikilink(match[1], match[0]);
|
|
1184
|
+
if (!parsed.attributes) continue;
|
|
1185
|
+
const close = match[0].indexOf("]]") + 1;
|
|
1186
|
+
const from = match.index + close + 2;
|
|
1187
|
+
const attrs = parseExtensionAttributes(match[0].slice(close + 2, -1), { positions: true });
|
|
1188
|
+
const at = positionOf(starts, match.index);
|
|
1189
|
+
const construct = {
|
|
1190
|
+
kind: "span",
|
|
1191
|
+
keys: runKeys(attrs.values),
|
|
1192
|
+
node: { contentEnd: match.index + close },
|
|
1193
|
+
line0: at.line - 1,
|
|
1194
|
+
where: { ...at, start: match.index, end: match.index + match[0].length },
|
|
1195
|
+
own: { classes: attrs.classes },
|
|
1196
|
+
at: (key, part) =>
|
|
1197
|
+
attrs.positions?.[key]?.[part] ?
|
|
1198
|
+
positionOf(starts, from + attrs.positions[key][part].start)
|
|
1199
|
+
: at,
|
|
1200
|
+
};
|
|
1201
|
+
const unchecked = errors.length;
|
|
1202
|
+
checkOwn(construct);
|
|
1203
|
+
if (errors.length !== unchecked || !native(construct)) continue;
|
|
1204
|
+
const keys = { ...construct.keys };
|
|
1205
|
+
nativeDefaults(construct, keys);
|
|
1206
|
+
if (errors.length !== unchecked || keys.map === undefined || keys.font === undefined)
|
|
1207
|
+
continue;
|
|
1208
|
+
const problem = fontProblem(maps.get(keys.map), keys.font);
|
|
1209
|
+
if (problem)
|
|
1210
|
+
report(
|
|
1211
|
+
construct.at(construct.keys.map !== undefined ? "map" : "font", "value"),
|
|
1212
|
+
problem,
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const verseMarkers = new Map();
|
|
1217
|
+
for (const poem of scanVerse(text).blocks) {
|
|
1218
|
+
if (hasColumns(poem)) {
|
|
1219
|
+
for (const group of verseGroups(poem, lines)) {
|
|
1220
|
+
if (group.stanzaBreak) continue;
|
|
1221
|
+
for (const part of group.parts) {
|
|
1222
|
+
const start = starts[part.line];
|
|
1223
|
+
if (part.prefixLength)
|
|
1224
|
+
verseMarkers.set(
|
|
1225
|
+
start + part.sourceColumn - 1,
|
|
1226
|
+
start + part.textColumn - 1,
|
|
1227
|
+
);
|
|
1228
|
+
if (part.markerLength)
|
|
1229
|
+
verseMarkers.set(
|
|
1230
|
+
start + part.markerStart,
|
|
1231
|
+
start + part.markerStart + part.markerLength,
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
} else
|
|
1236
|
+
for (let line = poem.start + 1; line < poem.end; line++) {
|
|
1237
|
+
const { markerStart, markerLength } = verseLine(lines[line]);
|
|
1238
|
+
if (markerLength) {
|
|
1239
|
+
const from = starts[line] + markerStart;
|
|
1240
|
+
verseMarkers.set(from, from + markerLength);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** Nested spans and verse markers excluded from a run, by absolute offset. */
|
|
1057
1246
|
const holesIn = (from, to) =>
|
|
1058
|
-
new Map(
|
|
1059
|
-
|
|
1247
|
+
new Map([
|
|
1248
|
+
...[...verseMarkers].filter(([start]) => start >= from && start < to),
|
|
1249
|
+
...active
|
|
1060
1250
|
.filter(
|
|
1061
1251
|
(construct) =>
|
|
1062
1252
|
construct.kind === "span" &&
|
|
@@ -1064,7 +1254,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1064
1254
|
construct.where.start < to,
|
|
1065
1255
|
)
|
|
1066
1256
|
.map((construct) => [construct.where.start, construct.where.end]),
|
|
1067
|
-
);
|
|
1257
|
+
]);
|
|
1068
1258
|
|
|
1069
1259
|
/**
|
|
1070
1260
|
* Report the markup in inline text that must be plain, and collect the
|
|
@@ -1219,8 +1409,8 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1219
1409
|
* @param {RunDetail} detail - The run.
|
|
1220
1410
|
*/
|
|
1221
1411
|
const lexical = (parts, detail) => {
|
|
1222
|
-
const { lang
|
|
1223
|
-
if (lang === undefined
|
|
1412
|
+
const { lang } = detail.run;
|
|
1413
|
+
if (lang === undefined) return;
|
|
1224
1414
|
const book = ctx.wordBooks?.get(lang);
|
|
1225
1415
|
if (!book) return;
|
|
1226
1416
|
const said = new Set();
|
|
@@ -1247,25 +1437,6 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1247
1437
|
* @param {Record<string, string>} keys - Its run keys, given the face.
|
|
1248
1438
|
*/
|
|
1249
1439
|
const scrollFace = (construct, keys) => {
|
|
1250
|
-
const textMap = keys.map === undefined ? undefined : maps.get(keys.map);
|
|
1251
|
-
if (keys.font === undefined) {
|
|
1252
|
-
if (textMap && textMap.fonts.size === 1) keys.font = [...textMap.fonts.keys()][0];
|
|
1253
|
-
else if (textMap && textMap.fonts.size > 1) {
|
|
1254
|
-
report(
|
|
1255
|
-
construct.at("map", "value"),
|
|
1256
|
-
`the map \`${keys.map}\` is drawn in more than one face; a \`scroll\` fence names its face with \`font=\``,
|
|
1257
|
-
);
|
|
1258
|
-
return;
|
|
1259
|
-
} else if (keys.map === undefined) {
|
|
1260
|
-
report(
|
|
1261
|
-
construct.where,
|
|
1262
|
-
"a `scroll` fence names the face it is set in, with `font=`, a map drawn in one " +
|
|
1263
|
-
"face, with `map=`, or a language whose note names its script in " +
|
|
1264
|
-
"`data.writtenIn`, with `lang=`",
|
|
1265
|
-
);
|
|
1266
|
-
return;
|
|
1267
|
-
} else return;
|
|
1268
|
-
}
|
|
1269
1440
|
if (!ctx.directions || !(faces.has(keys.font) || Object.hasOwn(GENERIC_FONTS, keys.font)))
|
|
1270
1441
|
return;
|
|
1271
1442
|
if (ctx.directions.get(keys.font) === "vertical-rl") return;
|
|
@@ -1281,19 +1452,32 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1281
1452
|
|
|
1282
1453
|
const runs = [];
|
|
1283
1454
|
const details = [];
|
|
1284
|
-
for (const construct of
|
|
1455
|
+
for (const construct of active) {
|
|
1285
1456
|
const unchecked = errors.length;
|
|
1286
1457
|
checkOwn(construct);
|
|
1287
1458
|
// A key that names nothing is the finding; the text it would have set
|
|
1288
1459
|
// is not then checked against a face or a language it cannot reach.
|
|
1289
1460
|
let named = errors.length === unchecked;
|
|
1290
|
-
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1293
|
-
const
|
|
1461
|
+
// Language and ordinary prose-family choices inherit. Native map/face
|
|
1462
|
+
// presentation belongs to this construct alone, including same-language
|
|
1463
|
+
// children, so a parallel Roman run never takes a native neighbour's face.
|
|
1464
|
+
const ancestors = around(construct);
|
|
1465
|
+
const keys = {};
|
|
1466
|
+
for (const ancestor of ancestors) {
|
|
1467
|
+
if (ancestor.keys.lang !== undefined) keys.lang = ancestor.keys.lang;
|
|
1468
|
+
if (!native(ancestor) && Object.hasOwn(GENERIC_FONTS, ancestor.keys.font ?? ""))
|
|
1469
|
+
keys.font = ancestor.keys.font;
|
|
1470
|
+
}
|
|
1471
|
+
if (native(construct)) delete keys.font;
|
|
1472
|
+
Object.assign(keys, construct.keys);
|
|
1294
1473
|
const columnFence = construct.kind === "verse" && hasColumns(construct.node);
|
|
1295
|
-
if (named &&
|
|
1296
|
-
|
|
1474
|
+
if (named && native(construct) && !columnFence) {
|
|
1475
|
+
nativeDefaults(construct, keys);
|
|
1476
|
+
named = errors.length === unchecked;
|
|
1477
|
+
} else if (!native(construct) && keys.font === undefined && ancestors.some(native)) {
|
|
1478
|
+
// Reset CSS/Typst inheritance from a surrounding native face.
|
|
1479
|
+
keys.font = "serif";
|
|
1480
|
+
}
|
|
1297
1481
|
if (construct.kind === "scroll" && named) {
|
|
1298
1482
|
scrollFace(construct, keys);
|
|
1299
1483
|
named = errors.length === unchecked;
|
|
@@ -1301,8 +1485,12 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1301
1485
|
const { lang, font, map } = keys;
|
|
1302
1486
|
// A face the run's map is not for, once the face and the map are each
|
|
1303
1487
|
// known, at the construct that states either.
|
|
1304
|
-
const stated =
|
|
1488
|
+
const stated =
|
|
1489
|
+
native(construct) ||
|
|
1490
|
+
construct.keys.map !== undefined ||
|
|
1491
|
+
construct.keys.font !== undefined;
|
|
1305
1492
|
if (
|
|
1493
|
+
named &&
|
|
1306
1494
|
stated &&
|
|
1307
1495
|
map !== undefined &&
|
|
1308
1496
|
maps.has(map) &&
|
|
@@ -1322,13 +1510,16 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1322
1510
|
tag: tagOf(lang),
|
|
1323
1511
|
font,
|
|
1324
1512
|
map,
|
|
1513
|
+
...(native(construct) && construct.keys.lang !== undefined ? { nativefont: true } : {}),
|
|
1325
1514
|
...construct.where,
|
|
1326
1515
|
};
|
|
1327
1516
|
runs.push(run);
|
|
1328
1517
|
const plain =
|
|
1329
1518
|
font !== undefined ||
|
|
1330
1519
|
map !== undefined ||
|
|
1331
|
-
(lang !== undefined &&
|
|
1520
|
+
(lang !== undefined &&
|
|
1521
|
+
Object.hasOwn(languages, lang) &&
|
|
1522
|
+
!/^en(?:-|$)/i.test(run.tag ?? ""));
|
|
1332
1523
|
const detail = {
|
|
1333
1524
|
run,
|
|
1334
1525
|
own: construct.own,
|
|
@@ -1341,6 +1532,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1341
1532
|
holes: new Map(),
|
|
1342
1533
|
body: undefined,
|
|
1343
1534
|
contentEnd: construct.kind === "span" ? construct.node.contentEnd : undefined,
|
|
1535
|
+
reference: undefined,
|
|
1344
1536
|
languageEmphasis: lang === undefined ? undefined : ctx.languageEmphases?.get(lang),
|
|
1345
1537
|
conventions: undefined,
|
|
1346
1538
|
emphasis: [],
|
|
@@ -1348,7 +1540,9 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1348
1540
|
};
|
|
1349
1541
|
detail.conventions = conventionsOf(detail, ctx);
|
|
1350
1542
|
details.push(detail);
|
|
1351
|
-
|
|
1543
|
+
const nonEnglishSpan =
|
|
1544
|
+
construct.kind === "span" && run.tag !== undefined && !/^en(?:-|$)/i.test(run.tag);
|
|
1545
|
+
if (!plain && !nonEnglishSpan) continue;
|
|
1352
1546
|
const marking = [
|
|
1353
1547
|
...(lang === undefined ? [] : [`lang=${lang}`]),
|
|
1354
1548
|
...(font === undefined ? [] : [`font=${font}`]),
|
|
@@ -1358,11 +1552,34 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1358
1552
|
|
|
1359
1553
|
if (construct.kind === "span") {
|
|
1360
1554
|
const span = construct.node;
|
|
1361
|
-
|
|
1362
|
-
|
|
1555
|
+
let from = span.start + 1;
|
|
1556
|
+
let to = span.contentEnd;
|
|
1557
|
+
const label = referenceLabel(text.slice(from, to));
|
|
1558
|
+
if (label) {
|
|
1559
|
+
detail.reference = [from + label[0], from + label[1]];
|
|
1560
|
+
[from, to] = detail.reference;
|
|
1561
|
+
}
|
|
1562
|
+
const inline = text.slice(from, to);
|
|
1563
|
+
detail.holes = label ? new Map() : holesIn(from, to);
|
|
1564
|
+
if (nonEnglishSpan) {
|
|
1565
|
+
const relative = new Map(
|
|
1566
|
+
[...detail.holes].map(([from, to]) => [
|
|
1567
|
+
from - (span.start + 1),
|
|
1568
|
+
to - (span.start + 1),
|
|
1569
|
+
]),
|
|
1570
|
+
);
|
|
1571
|
+
for (const delimiter of emphasisDelimiters(inline, relative))
|
|
1572
|
+
if (delimiter.open)
|
|
1573
|
+
report(
|
|
1574
|
+
positionOf(starts, from + delimiter.offset),
|
|
1575
|
+
`a span in non-English language \`${lang}\` holds no bold or italic markup`,
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
if (plain && errors.length === before)
|
|
1579
|
+
plainInline(inline, from, marking, detail, detail.holes);
|
|
1363
1580
|
if (named && errors.length === before) {
|
|
1364
1581
|
const parts = outsideDelimiters(
|
|
1365
|
-
|
|
1582
|
+
outsideHoles(from, to, detail.holes),
|
|
1366
1583
|
detail.emphasis,
|
|
1367
1584
|
);
|
|
1368
1585
|
lexical(parts, detail);
|
|
@@ -1465,48 +1682,20 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1465
1682
|
*/
|
|
1466
1683
|
export function scanRuns(source, ctx = {}, syntax) {
|
|
1467
1684
|
const text = String(source ?? "");
|
|
1468
|
-
const { markdown, origins } = expandVerseOrigins(text);
|
|
1469
1685
|
const authored = analyseRuns(text, ctx, syntax);
|
|
1470
1686
|
const metre = metreFindings(scanVerse(text).blocks, text.split("\n"), {
|
|
1471
1687
|
metres: ctx.metres,
|
|
1688
|
+
languages: ctx.languages,
|
|
1472
1689
|
metreBooks: ctx.metreBooks,
|
|
1473
1690
|
lexicons: ctx.lexicons,
|
|
1474
|
-
columnsClass: COLUMNS_CLASS,
|
|
1475
|
-
columnKeys: COLUMN_KEYS,
|
|
1476
1691
|
attributeColumns,
|
|
1477
1692
|
});
|
|
1478
|
-
/** @param {object[]} found @param {object[]} extra */
|
|
1479
1693
|
const merged = (found, extra) =>
|
|
1480
|
-
extra.
|
|
1481
|
-
[...found, ...extra].sort((a, b) => a.line - b.line || a.column - b.column)
|
|
1482
|
-
: found;
|
|
1483
|
-
if (markdown === text)
|
|
1484
|
-
return {
|
|
1485
|
-
...authored,
|
|
1486
|
-
errors: merged(authored.errors, metre.errors),
|
|
1487
|
-
warnings: merged(authored.warnings, metre.warnings),
|
|
1488
|
-
};
|
|
1489
|
-
// A verse in columns is checked as the spans each entry sets, which are
|
|
1490
|
-
// written only in the expansion; every finding is placed back where the
|
|
1491
|
-
// note wrote the text or the entry it comes from, once.
|
|
1492
|
-
const expanded = markdown.split("\n");
|
|
1493
|
-
const checked = analyseRuns(markdown, ctx);
|
|
1494
|
-
const placed = (findings) => {
|
|
1495
|
-
const seen = new Set();
|
|
1496
|
-
return findings
|
|
1497
|
-
.map((finding) => ({ ...finding, ...sourcePosition(finding, expanded, origins) }))
|
|
1498
|
-
.filter((finding) => {
|
|
1499
|
-
const key = `${finding.line}:${finding.column}:${finding.message}`;
|
|
1500
|
-
if (seen.has(key)) return false;
|
|
1501
|
-
seen.add(key);
|
|
1502
|
-
return true;
|
|
1503
|
-
})
|
|
1504
|
-
.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1505
|
-
};
|
|
1694
|
+
[...found, ...extra].sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1506
1695
|
return {
|
|
1507
|
-
|
|
1508
|
-
errors: merged(
|
|
1509
|
-
warnings: merged(
|
|
1696
|
+
...authored,
|
|
1697
|
+
errors: merged(authored.errors, metre.errors),
|
|
1698
|
+
warnings: merged(authored.warnings, metre.warnings),
|
|
1510
1699
|
};
|
|
1511
1700
|
}
|
|
1512
1701
|
|
|
@@ -1558,12 +1747,13 @@ const SCRIPT_BLOCK_CLASS = "script-block";
|
|
|
1558
1747
|
function rewrittenAttributes(detail, search) {
|
|
1559
1748
|
const { run, own, keys, face, generic, textMap } = detail;
|
|
1560
1749
|
const changes =
|
|
1750
|
+
own.classes.includes(NATIVEFONT_CLASS) ||
|
|
1561
1751
|
run.font !== undefined ||
|
|
1562
1752
|
run.map !== undefined ||
|
|
1563
1753
|
(run.tag !== undefined &&
|
|
1564
1754
|
(keys.lang !== run.tag || run.kind === "verse" || run.kind === "scroll"));
|
|
1565
1755
|
if (!changes) return undefined;
|
|
1566
|
-
const classes =
|
|
1756
|
+
const classes = own.classes.filter((name) => name !== NATIVEFONT_CLASS);
|
|
1567
1757
|
const values = [...own.values];
|
|
1568
1758
|
if (face) {
|
|
1569
1759
|
classes.push(SCRIPT_CLASS);
|
|
@@ -1631,10 +1821,9 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
1631
1821
|
* Rewrite text marked with `lang=`, `font=` and `map=` into the plain span,
|
|
1632
1822
|
* div and verse grammar the rest of each chain renders, one line for one line.
|
|
1633
1823
|
*
|
|
1634
|
-
* - **
|
|
1635
|
-
*
|
|
1636
|
-
*
|
|
1637
|
-
* rewrite that changes the number of lines.
|
|
1824
|
+
* - **Numbered verse prefixes and indentation markers** stay outside run text.
|
|
1825
|
+
* Each authored run chooses its own language and presentation; physical
|
|
1826
|
+
* lines and source positions remain unchanged.
|
|
1638
1827
|
* - **The map, if any, is applied first.** Characters Markdown would read are
|
|
1639
1828
|
* then backslash-escaped, so they reach every later pass as characters.
|
|
1640
1829
|
* - **Emphasis in a face** becomes an `<em>` or a `<strong>` carrying its
|
|
@@ -1668,12 +1857,11 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
1668
1857
|
export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
1669
1858
|
const search = options.target === "site";
|
|
1670
1859
|
const book = options.target === "book";
|
|
1671
|
-
const
|
|
1672
|
-
const text = expandVerse(authored);
|
|
1673
|
-
if (text !== authored) syntax = undefined;
|
|
1860
|
+
const text = String(source ?? "");
|
|
1674
1861
|
/** @type {Map<string, import("./content-scroll.mjs").ScrollLayout>} */
|
|
1675
1862
|
const scrolls = new Map();
|
|
1676
|
-
if (!/\b(?:lang|font|map)
|
|
1863
|
+
if (!/\b(?:lang|font|map)=|\bnativefont\b/.test(text))
|
|
1864
|
+
return { markdown: text, errors: [], scrolls };
|
|
1677
1865
|
const { details, errors } = analyseRuns(text, ctx, syntax);
|
|
1678
1866
|
if (!details.length) return { markdown: text, errors, scrolls };
|
|
1679
1867
|
const lines = text.split("\n");
|
|
@@ -1682,8 +1870,8 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1682
1870
|
/** Whether a finding sits inside a run. */
|
|
1683
1871
|
const faulted = (run) => {
|
|
1684
1872
|
const from = (run.line - 1) * 1e6 + run.column;
|
|
1685
|
-
const
|
|
1686
|
-
const to =
|
|
1873
|
+
const end = run.kind === "span" ? positionOf(starts, run.end) : undefined;
|
|
1874
|
+
const to = end ? (end.line - 1) * 1e6 + end.column : (run.end + 1) * 1e6;
|
|
1687
1875
|
return errors.some((error) => {
|
|
1688
1876
|
const at = (error.line - 1) * 1e6 + error.column;
|
|
1689
1877
|
return at >= from && at < to;
|
|
@@ -1765,19 +1953,39 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1765
1953
|
const attributes = rewrittenAttributes(detail, search);
|
|
1766
1954
|
if (run.kind === "span") {
|
|
1767
1955
|
const contentEnd = /** @type {number} */ (detail.contentEnd);
|
|
1768
|
-
|
|
1956
|
+
if (book && detail.reference) {
|
|
1957
|
+
// The book's link renderer adds page and volume cues after
|
|
1958
|
+
// the label. Keep those in the page font, while the authored
|
|
1959
|
+
// presentation attributes still enclose the whole reference.
|
|
1960
|
+
const empty = { id: "", classes: [], values: [] };
|
|
1961
|
+
const native =
|
|
1962
|
+
rewrittenAttributes({ ...detail, own: empty }, search) ??
|
|
1963
|
+
attributeText({ lang: run.tag, ...empty });
|
|
1964
|
+
const [from, to] = detail.reference;
|
|
1965
|
+
const label = `[${setRange(text, from, to, detail, false, book)}]{${native}}`;
|
|
1966
|
+
const reference =
|
|
1967
|
+
text.slice(run.start + 1, from) + label + text.slice(to, contentEnd);
|
|
1968
|
+
const outer = attributeText({
|
|
1969
|
+
lang: undefined,
|
|
1970
|
+
...detail.own,
|
|
1971
|
+
classes: detail.own.classes.filter((name) => name !== NATIVEFONT_CLASS),
|
|
1972
|
+
});
|
|
1973
|
+
edits.push({
|
|
1974
|
+
from: run.start,
|
|
1975
|
+
to: run.end,
|
|
1976
|
+
text: outer ? `[${reference}]{${outer}}` : reference,
|
|
1977
|
+
});
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
if (attributes !== undefined)
|
|
1981
|
+
edits.push({ from: contentEnd + 2, to: run.end - 1, text: attributes });
|
|
1769
1982
|
if (detail.plain)
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
book,
|
|
1777
|
-
);
|
|
1778
|
-
else if (attributes === undefined) continue;
|
|
1779
|
-
const braces = attributes ?? text.slice(contentEnd + 2, run.end - 1);
|
|
1780
|
-
edits.push({ from: run.start, to: run.end, text: `[${inner}]{${braces}}` });
|
|
1983
|
+
for (const [from, to] of detail.reference ?
|
|
1984
|
+
[detail.reference]
|
|
1985
|
+
: outsideHoles(run.start + 1, contentEnd, detail.holes)) {
|
|
1986
|
+
const set = setRange(text, from, to, detail, false, book);
|
|
1987
|
+
if (set !== text.slice(from, to)) edits.push({ from, to, text: set });
|
|
1988
|
+
}
|
|
1781
1989
|
continue;
|
|
1782
1990
|
}
|
|
1783
1991
|
if (run.kind === "scroll" && attributes !== undefined && !detail.unrolled) {
|