@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
|
@@ -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,14 +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
|
-
} from "./content-verse.mjs";
|
|
95
|
+
import { scanVerse, hasColumns, verseLine, verseGroups } from "./content-verse.mjs";
|
|
95
96
|
import { scanSpans } from "./content-spans.mjs";
|
|
96
97
|
import { scanScrolls, scrollParagraphs } from "./content-scroll.mjs";
|
|
97
98
|
import { directionsFromRecords } from "./script-direction.mjs";
|
|
@@ -111,11 +112,17 @@ import {
|
|
|
111
112
|
import { unknownWordMessage, unknownWords, wordBooksFromRecords } from "./run-words.mjs";
|
|
112
113
|
import { metreBooksFromRecords, metreFindings, metresFromRecords } from "./verse-metre.mjs";
|
|
113
114
|
import { applyMap, mapsFromRecords } from "./text-maps.mjs";
|
|
114
|
-
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";
|
|
115
119
|
|
|
116
120
|
/** The keys this module reads, which no construct passes through as written. */
|
|
117
121
|
export const RUN_KEYS = Object.freeze(["lang", "font", "map"]);
|
|
118
122
|
|
|
123
|
+
/** Explicit opt-in to native-script presentation on this construct. */
|
|
124
|
+
export const NATIVEFONT_CLASS = "nativefont";
|
|
125
|
+
|
|
119
126
|
/** The generic families `font=` may name, each with the class that sets it. */
|
|
120
127
|
export const GENERIC_FONTS = Object.freeze({
|
|
121
128
|
mono: "font-mono",
|
|
@@ -166,9 +173,9 @@ const CONFIG = "package-build.config.yaml";
|
|
|
166
173
|
/**
|
|
167
174
|
* One span, div, verse fence or scroll fence carrying `lang=`, `font=` or
|
|
168
175
|
* `map=`. Its `lang`, `font` and `map` are the ones it is set with: its own,
|
|
169
|
-
* else
|
|
170
|
-
*
|
|
171
|
-
*
|
|
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.
|
|
172
179
|
*
|
|
173
180
|
* @typedef {object} Run
|
|
174
181
|
* @property {"span"|"block"|"verse"|"scroll"} kind - Which construct.
|
|
@@ -183,6 +190,8 @@ const CONFIG = "package-build.config.yaml";
|
|
|
183
190
|
* 0-based opening line.
|
|
184
191
|
* @property {number} end - A span's offset past its attributes; a div's or
|
|
185
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.
|
|
186
195
|
*/
|
|
187
196
|
|
|
188
197
|
/** @type {WeakMap<object, RunContext>} */
|
|
@@ -344,6 +353,38 @@ function closing(text, open) {
|
|
|
344
353
|
return -1;
|
|
345
354
|
}
|
|
346
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
|
+
|
|
347
388
|
/**
|
|
348
389
|
* The markup in text that must be plain, each at its offset.
|
|
349
390
|
*
|
|
@@ -768,6 +809,8 @@ const BLOCK_WHAT = Object.freeze({
|
|
|
768
809
|
* @property {[number, number]|undefined} body - A div's or fence's body, as
|
|
769
810
|
* offsets: the first body line's start to the end of the last.
|
|
770
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.
|
|
771
814
|
* @property {import("./script-emphasis.mjs").LanguageEmphasis|undefined} languageEmphasis -
|
|
772
815
|
* The convention its language's note states.
|
|
773
816
|
* @property {import("./script-emphasis.mjs").Emphasis|undefined} conventions -
|
|
@@ -833,12 +876,30 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
833
876
|
const runKeys = (values) =>
|
|
834
877
|
Object.fromEntries(Object.entries(values).filter(([key]) => RUN_KEYS.includes(key)));
|
|
835
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
|
+
|
|
836
897
|
// Every construct that states a run key, with where it sits.
|
|
837
898
|
const constructs = [];
|
|
838
899
|
const spans = (syntax ? syntax(text).spans : scanSpans(text)).spans;
|
|
839
900
|
for (const span of spans) {
|
|
840
901
|
const keys = Object.fromEntries(span.attrs.filter(([key]) => RUN_KEYS.includes(key)));
|
|
841
|
-
if (!
|
|
902
|
+
if (!span.attrs.length && !span.id) continue;
|
|
842
903
|
const positions = span.attributePositions ?? {};
|
|
843
904
|
constructs.push({
|
|
844
905
|
kind: "span",
|
|
@@ -871,6 +932,15 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
871
932
|
});
|
|
872
933
|
const keys = runKeys(block.attributes);
|
|
873
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
|
+
);
|
|
874
944
|
for (const key of Object.keys(keys))
|
|
875
945
|
report(
|
|
876
946
|
at(key, "key"),
|
|
@@ -878,7 +948,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
878
948
|
);
|
|
879
949
|
} else {
|
|
880
950
|
divs.set(first, block);
|
|
881
|
-
if (Object.keys(keys).length)
|
|
951
|
+
if (Object.keys(keys).length || block.classes?.includes(NATIVEFONT_CLASS))
|
|
882
952
|
constructs.push({
|
|
883
953
|
kind: "block",
|
|
884
954
|
keys,
|
|
@@ -911,7 +981,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
911
981
|
for (const poem of scanVerse(text).blocks) {
|
|
912
982
|
poems.set(poem.start, poem);
|
|
913
983
|
const keys = runKeys(poem.attributes);
|
|
914
|
-
if (!Object.keys(keys).length) continue;
|
|
984
|
+
if (!Object.keys(keys).length && !poem.classes.includes(NATIVEFONT_CLASS)) continue;
|
|
915
985
|
const opening = lines[poem.start];
|
|
916
986
|
constructs.push({
|
|
917
987
|
kind: "verse",
|
|
@@ -969,17 +1039,31 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
969
1039
|
});
|
|
970
1040
|
}
|
|
971
1041
|
|
|
972
|
-
|
|
973
|
-
/** The containers around a construct, outermost first. */
|
|
1042
|
+
/** The constructs around a run, outermost first. */
|
|
974
1043
|
const around = (construct) =>
|
|
975
|
-
|
|
1044
|
+
constructs
|
|
976
1045
|
.filter(
|
|
977
1046
|
(container) =>
|
|
978
1047
|
container !== construct &&
|
|
979
|
-
container.
|
|
980
|
-
|
|
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),
|
|
981
1053
|
)
|
|
982
|
-
.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
|
+
);
|
|
983
1067
|
|
|
984
1068
|
/** A language's tag, or `undefined` when it resolves to none. */
|
|
985
1069
|
const tagOf = (lang) =>
|
|
@@ -988,12 +1072,44 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
988
1072
|
: isLanguageTag(lang) ? lang
|
|
989
1073
|
: undefined;
|
|
990
1074
|
|
|
1075
|
+
const native = (construct) => construct.own.classes.includes(NATIVEFONT_CLASS);
|
|
1076
|
+
|
|
991
1077
|
const faceNames = () => [...faces.keys(), ...Object.keys(GENERIC_FONTS)];
|
|
992
1078
|
|
|
993
1079
|
/** Check the keys a construct states itself. */
|
|
994
1080
|
const checkOwn = (construct) => {
|
|
995
1081
|
const { lang, font, map } = construct.keys;
|
|
996
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
|
+
|
|
997
1113
|
if (lang !== undefined && !Object.hasOwn(languages, lang)) {
|
|
998
1114
|
if (isLanguageTag(lang)) {
|
|
999
1115
|
const declared = Object.keys(languages).find((name) => withinOneEdit(lang, name));
|
|
@@ -1035,10 +1151,102 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1035
1151
|
}
|
|
1036
1152
|
};
|
|
1037
1153
|
|
|
1038
|
-
|
|
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. */
|
|
1039
1246
|
const holesIn = (from, to) =>
|
|
1040
|
-
new Map(
|
|
1041
|
-
|
|
1247
|
+
new Map([
|
|
1248
|
+
...[...verseMarkers].filter(([start]) => start >= from && start < to),
|
|
1249
|
+
...active
|
|
1042
1250
|
.filter(
|
|
1043
1251
|
(construct) =>
|
|
1044
1252
|
construct.kind === "span" &&
|
|
@@ -1046,7 +1254,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1046
1254
|
construct.where.start < to,
|
|
1047
1255
|
)
|
|
1048
1256
|
.map((construct) => [construct.where.start, construct.where.end]),
|
|
1049
|
-
);
|
|
1257
|
+
]);
|
|
1050
1258
|
|
|
1051
1259
|
/**
|
|
1052
1260
|
* Report the markup in inline text that must be plain, and collect the
|
|
@@ -1201,8 +1409,8 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1201
1409
|
* @param {RunDetail} detail - The run.
|
|
1202
1410
|
*/
|
|
1203
1411
|
const lexical = (parts, detail) => {
|
|
1204
|
-
const { lang
|
|
1205
|
-
if (lang === undefined
|
|
1412
|
+
const { lang } = detail.run;
|
|
1413
|
+
if (lang === undefined) return;
|
|
1206
1414
|
const book = ctx.wordBooks?.get(lang);
|
|
1207
1415
|
if (!book) return;
|
|
1208
1416
|
const said = new Set();
|
|
@@ -1229,25 +1437,6 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1229
1437
|
* @param {Record<string, string>} keys - Its run keys, given the face.
|
|
1230
1438
|
*/
|
|
1231
1439
|
const scrollFace = (construct, keys) => {
|
|
1232
|
-
const textMap = keys.map === undefined ? undefined : maps.get(keys.map);
|
|
1233
|
-
if (keys.font === undefined) {
|
|
1234
|
-
if (textMap && textMap.fonts.size === 1) keys.font = [...textMap.fonts.keys()][0];
|
|
1235
|
-
else if (textMap && textMap.fonts.size > 1) {
|
|
1236
|
-
report(
|
|
1237
|
-
construct.at("map", "value"),
|
|
1238
|
-
`the map \`${keys.map}\` is drawn in more than one face; a \`scroll\` fence names its face with \`font=\``,
|
|
1239
|
-
);
|
|
1240
|
-
return;
|
|
1241
|
-
} else if (keys.map === undefined) {
|
|
1242
|
-
report(
|
|
1243
|
-
construct.where,
|
|
1244
|
-
"a `scroll` fence names the face it is set in, with `font=`, a map drawn in one " +
|
|
1245
|
-
"face, with `map=`, or a language whose note names its script in " +
|
|
1246
|
-
"`data.writtenIn`, with `lang=`",
|
|
1247
|
-
);
|
|
1248
|
-
return;
|
|
1249
|
-
} else return;
|
|
1250
|
-
}
|
|
1251
1440
|
if (!ctx.directions || !(faces.has(keys.font) || Object.hasOwn(GENERIC_FONTS, keys.font)))
|
|
1252
1441
|
return;
|
|
1253
1442
|
if (ctx.directions.get(keys.font) === "vertical-rl") return;
|
|
@@ -1263,16 +1452,32 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1263
1452
|
|
|
1264
1453
|
const runs = [];
|
|
1265
1454
|
const details = [];
|
|
1266
|
-
for (const construct of
|
|
1455
|
+
for (const construct of active) {
|
|
1267
1456
|
const unchecked = errors.length;
|
|
1268
1457
|
checkOwn(construct);
|
|
1269
1458
|
// A key that names nothing is the finding; the text it would have set
|
|
1270
1459
|
// is not then checked against a face or a language it cannot reach.
|
|
1271
1460
|
let named = errors.length === unchecked;
|
|
1272
|
-
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1275
|
-
|
|
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);
|
|
1473
|
+
const columnFence = construct.kind === "verse" && hasColumns(construct.node);
|
|
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
|
+
}
|
|
1276
1481
|
if (construct.kind === "scroll" && named) {
|
|
1277
1482
|
scrollFace(construct, keys);
|
|
1278
1483
|
named = errors.length === unchecked;
|
|
@@ -1280,8 +1485,12 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1280
1485
|
const { lang, font, map } = keys;
|
|
1281
1486
|
// A face the run's map is not for, once the face and the map are each
|
|
1282
1487
|
// known, at the construct that states either.
|
|
1283
|
-
const stated =
|
|
1488
|
+
const stated =
|
|
1489
|
+
native(construct) ||
|
|
1490
|
+
construct.keys.map !== undefined ||
|
|
1491
|
+
construct.keys.font !== undefined;
|
|
1284
1492
|
if (
|
|
1493
|
+
named &&
|
|
1285
1494
|
stated &&
|
|
1286
1495
|
map !== undefined &&
|
|
1287
1496
|
maps.has(map) &&
|
|
@@ -1301,13 +1510,16 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1301
1510
|
tag: tagOf(lang),
|
|
1302
1511
|
font,
|
|
1303
1512
|
map,
|
|
1513
|
+
...(native(construct) && construct.keys.lang !== undefined ? { nativefont: true } : {}),
|
|
1304
1514
|
...construct.where,
|
|
1305
1515
|
};
|
|
1306
1516
|
runs.push(run);
|
|
1307
1517
|
const plain =
|
|
1308
1518
|
font !== undefined ||
|
|
1309
1519
|
map !== undefined ||
|
|
1310
|
-
(lang !== undefined &&
|
|
1520
|
+
(lang !== undefined &&
|
|
1521
|
+
Object.hasOwn(languages, lang) &&
|
|
1522
|
+
!/^en(?:-|$)/i.test(run.tag ?? ""));
|
|
1311
1523
|
const detail = {
|
|
1312
1524
|
run,
|
|
1313
1525
|
own: construct.own,
|
|
@@ -1320,6 +1532,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1320
1532
|
holes: new Map(),
|
|
1321
1533
|
body: undefined,
|
|
1322
1534
|
contentEnd: construct.kind === "span" ? construct.node.contentEnd : undefined,
|
|
1535
|
+
reference: undefined,
|
|
1323
1536
|
languageEmphasis: lang === undefined ? undefined : ctx.languageEmphases?.get(lang),
|
|
1324
1537
|
conventions: undefined,
|
|
1325
1538
|
emphasis: [],
|
|
@@ -1327,7 +1540,9 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1327
1540
|
};
|
|
1328
1541
|
detail.conventions = conventionsOf(detail, ctx);
|
|
1329
1542
|
details.push(detail);
|
|
1330
|
-
|
|
1543
|
+
const nonEnglishSpan =
|
|
1544
|
+
construct.kind === "span" && run.tag !== undefined && !/^en(?:-|$)/i.test(run.tag);
|
|
1545
|
+
if (!plain && !nonEnglishSpan) continue;
|
|
1331
1546
|
const marking = [
|
|
1332
1547
|
...(lang === undefined ? [] : [`lang=${lang}`]),
|
|
1333
1548
|
...(font === undefined ? [] : [`font=${font}`]),
|
|
@@ -1337,11 +1552,34 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1337
1552
|
|
|
1338
1553
|
if (construct.kind === "span") {
|
|
1339
1554
|
const span = construct.node;
|
|
1340
|
-
|
|
1341
|
-
|
|
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);
|
|
1342
1580
|
if (named && errors.length === before) {
|
|
1343
1581
|
const parts = outsideDelimiters(
|
|
1344
|
-
|
|
1582
|
+
outsideHoles(from, to, detail.holes),
|
|
1345
1583
|
detail.emphasis,
|
|
1346
1584
|
);
|
|
1347
1585
|
lexical(parts, detail);
|
|
@@ -1444,48 +1682,20 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1444
1682
|
*/
|
|
1445
1683
|
export function scanRuns(source, ctx = {}, syntax) {
|
|
1446
1684
|
const text = String(source ?? "");
|
|
1447
|
-
const { markdown, origins } = expandVerseOrigins(text);
|
|
1448
1685
|
const authored = analyseRuns(text, ctx, syntax);
|
|
1449
1686
|
const metre = metreFindings(scanVerse(text).blocks, text.split("\n"), {
|
|
1450
1687
|
metres: ctx.metres,
|
|
1688
|
+
languages: ctx.languages,
|
|
1451
1689
|
metreBooks: ctx.metreBooks,
|
|
1452
1690
|
lexicons: ctx.lexicons,
|
|
1453
|
-
columnsClass: COLUMNS_CLASS,
|
|
1454
|
-
columnKeys: COLUMN_KEYS,
|
|
1455
1691
|
attributeColumns,
|
|
1456
1692
|
});
|
|
1457
|
-
/** @param {object[]} found @param {object[]} extra */
|
|
1458
1693
|
const merged = (found, extra) =>
|
|
1459
|
-
extra.
|
|
1460
|
-
[...found, ...extra].sort((a, b) => a.line - b.line || a.column - b.column)
|
|
1461
|
-
: found;
|
|
1462
|
-
if (markdown === text)
|
|
1463
|
-
return {
|
|
1464
|
-
...authored,
|
|
1465
|
-
errors: merged(authored.errors, metre.errors),
|
|
1466
|
-
warnings: merged(authored.warnings, metre.warnings),
|
|
1467
|
-
};
|
|
1468
|
-
// A verse in columns is checked as the spans each entry sets, which are
|
|
1469
|
-
// written only in the expansion; every finding is placed back where the
|
|
1470
|
-
// note wrote the text or the entry it comes from, once.
|
|
1471
|
-
const expanded = markdown.split("\n");
|
|
1472
|
-
const checked = analyseRuns(markdown, ctx);
|
|
1473
|
-
const placed = (findings) => {
|
|
1474
|
-
const seen = new Set();
|
|
1475
|
-
return findings
|
|
1476
|
-
.map((finding) => ({ ...finding, ...sourcePosition(finding, expanded, origins) }))
|
|
1477
|
-
.filter((finding) => {
|
|
1478
|
-
const key = `${finding.line}:${finding.column}:${finding.message}`;
|
|
1479
|
-
if (seen.has(key)) return false;
|
|
1480
|
-
seen.add(key);
|
|
1481
|
-
return true;
|
|
1482
|
-
})
|
|
1483
|
-
.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1484
|
-
};
|
|
1694
|
+
[...found, ...extra].sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1485
1695
|
return {
|
|
1486
|
-
|
|
1487
|
-
errors: merged(
|
|
1488
|
-
warnings: merged(
|
|
1696
|
+
...authored,
|
|
1697
|
+
errors: merged(authored.errors, metre.errors),
|
|
1698
|
+
warnings: merged(authored.warnings, metre.warnings),
|
|
1489
1699
|
};
|
|
1490
1700
|
}
|
|
1491
1701
|
|
|
@@ -1537,12 +1747,13 @@ const SCRIPT_BLOCK_CLASS = "script-block";
|
|
|
1537
1747
|
function rewrittenAttributes(detail, search) {
|
|
1538
1748
|
const { run, own, keys, face, generic, textMap } = detail;
|
|
1539
1749
|
const changes =
|
|
1750
|
+
own.classes.includes(NATIVEFONT_CLASS) ||
|
|
1540
1751
|
run.font !== undefined ||
|
|
1541
1752
|
run.map !== undefined ||
|
|
1542
1753
|
(run.tag !== undefined &&
|
|
1543
1754
|
(keys.lang !== run.tag || run.kind === "verse" || run.kind === "scroll"));
|
|
1544
1755
|
if (!changes) return undefined;
|
|
1545
|
-
const classes =
|
|
1756
|
+
const classes = own.classes.filter((name) => name !== NATIVEFONT_CLASS);
|
|
1546
1757
|
const values = [...own.values];
|
|
1547
1758
|
if (face) {
|
|
1548
1759
|
classes.push(SCRIPT_CLASS);
|
|
@@ -1610,10 +1821,9 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
1610
1821
|
* Rewrite text marked with `lang=`, `font=` and `map=` into the plain span,
|
|
1611
1822
|
* div and verse grammar the rest of each chain renders, one line for one line.
|
|
1612
1823
|
*
|
|
1613
|
-
* - **
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1616
|
-
* 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.
|
|
1617
1827
|
* - **The map, if any, is applied first.** Characters Markdown would read are
|
|
1618
1828
|
* then backslash-escaped, so they reach every later pass as characters.
|
|
1619
1829
|
* - **Emphasis in a face** becomes an `<em>` or a `<strong>` carrying its
|
|
@@ -1647,12 +1857,11 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
1647
1857
|
export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
1648
1858
|
const search = options.target === "site";
|
|
1649
1859
|
const book = options.target === "book";
|
|
1650
|
-
const
|
|
1651
|
-
const text = expandVerse(authored);
|
|
1652
|
-
if (text !== authored) syntax = undefined;
|
|
1860
|
+
const text = String(source ?? "");
|
|
1653
1861
|
/** @type {Map<string, import("./content-scroll.mjs").ScrollLayout>} */
|
|
1654
1862
|
const scrolls = new Map();
|
|
1655
|
-
if (!/\b(?:lang|font|map)
|
|
1863
|
+
if (!/\b(?:lang|font|map)=|\bnativefont\b/.test(text))
|
|
1864
|
+
return { markdown: text, errors: [], scrolls };
|
|
1656
1865
|
const { details, errors } = analyseRuns(text, ctx, syntax);
|
|
1657
1866
|
if (!details.length) return { markdown: text, errors, scrolls };
|
|
1658
1867
|
const lines = text.split("\n");
|
|
@@ -1661,8 +1870,8 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1661
1870
|
/** Whether a finding sits inside a run. */
|
|
1662
1871
|
const faulted = (run) => {
|
|
1663
1872
|
const from = (run.line - 1) * 1e6 + run.column;
|
|
1664
|
-
const
|
|
1665
|
-
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;
|
|
1666
1875
|
return errors.some((error) => {
|
|
1667
1876
|
const at = (error.line - 1) * 1e6 + error.column;
|
|
1668
1877
|
return at >= from && at < to;
|
|
@@ -1744,19 +1953,39 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1744
1953
|
const attributes = rewrittenAttributes(detail, search);
|
|
1745
1954
|
if (run.kind === "span") {
|
|
1746
1955
|
const contentEnd = /** @type {number} */ (detail.contentEnd);
|
|
1747
|
-
|
|
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 });
|
|
1748
1982
|
if (detail.plain)
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
book,
|
|
1756
|
-
);
|
|
1757
|
-
else if (attributes === undefined) continue;
|
|
1758
|
-
const braces = attributes ?? text.slice(contentEnd + 2, run.end - 1);
|
|
1759
|
-
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
|
+
}
|
|
1760
1989
|
continue;
|
|
1761
1990
|
}
|
|
1762
1991
|
if (run.kind === "scroll" && attributes !== undefined && !detail.unrolled) {
|