@heroiclands/package-build 24.0.1 → 24.0.3
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 +32 -0
- package/assets/preview/gitea.ini +1 -1
- package/assets/styles/content-base.css +33 -0
- package/bin/content-commands.mjs +17 -17
- package/content-config.mjs +1 -52
- package/docs/api.md +20 -13
- package/docs/authoring/links-and-markup.md +165 -17
- package/docs/commands.md +20 -20
- package/docs/configuration.md +29 -69
- package/docs/content-format.md +4 -2
- package/docs/guides/site.md +3 -1
- package/docs/guides/writing-in-invented-scripts.md +68 -6
- package/docs/reference/format-details.md +160 -118
- package/docs/reference/note-types.md +49 -29
- package/docs/source-preview.md +2 -2
- package/engine/content-blocks.mjs +6 -2
- package/engine/content-charset.mjs +1 -0
- 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/dictionary-layout.mjs +16 -3
- package/engine/font-outlines.mjs +394 -0
- package/engine/foreign-catalog.mjs +11 -122
- package/engine/language-data.mjs +154 -63
- package/engine/language-rows.mjs +35 -9
- package/engine/language-terms.mjs +39 -9
- package/engine/map-lexicon.mjs +31 -6
- package/engine/note-vocabulary.mjs +58 -6
- package/engine/pdf-build.mjs +9 -178
- 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/site-reach.mjs +23 -0
- package/engine/site-render.mjs +18 -3
- package/engine/verse-metre.mjs +1 -0
- package/engine/written-in.mjs +208 -0
- package/package.json +2 -1
- package/site-theme/static/css/style.css +7 -0
- package/types/content-config.d.mts +1 -19
- package/types/engine/content-scripts.d.mts +31 -6
- package/types/engine/content-scroll.d.mts +179 -0
- package/types/engine/dictionary-layout.d.mts +5 -0
- package/types/engine/font-outlines.d.mts +67 -0
- package/types/engine/foreign-catalog.d.mts +0 -38
- package/types/engine/language-data.d.mts +13 -0
- package/types/engine/language-rows.d.mts +5 -4
- package/types/engine/language-terms.d.mts +7 -0
- package/types/engine/map-lexicon.d.mts +4 -3
- package/types/engine/pdf-build.d.mts +5 -27
- 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/site-reach.d.mts +11 -0
- package/types/engine/written-in.d.mts +76 -0
- package/engine/asset-replacement.mjs +0 -93
- package/types/engine/asset-replacement.d.mts +0 -22
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* **Text marked with a language, a font and a mapping.** Three keys on the
|
|
16
|
-
* attribute grammar spans, divs and
|
|
17
|
-
* and independent of the others:
|
|
16
|
+
* attribute grammar spans, divs, verse fences and scroll fences already share,
|
|
17
|
+
* each optional and independent of the others:
|
|
18
18
|
*
|
|
19
19
|
* ```markdown
|
|
20
20
|
* [Daimela, helthos.]{lang=vyl}
|
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
* ```verse {form=epic lang=vyl font=semaron-kuriale map=semaron}
|
|
28
28
|
* ```
|
|
29
29
|
*
|
|
30
|
-
* - **`lang=`** gives the language tag
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* - **`lang=`** gives the language tag. It is a name declared in the
|
|
31
|
+
* package's `languages`, which gives its tag, or otherwise a well-formed BCP
|
|
32
|
+
* 47 tag, checked by form alone. A declared language whose note names its
|
|
33
|
+
* script in `data.writtenIn` gives a run the map and face it leaves
|
|
34
|
+
* unstated; see {@link module:engine/written-in}.
|
|
33
35
|
* - **`font=`** is one of the installed faces, by its class without `hl-`
|
|
34
36
|
* (`semaron-kuriale`), or `mono`, `serif` or `sans`.
|
|
35
37
|
* - **`map=`** names a mapping note's `data.map`, which rewrites the text
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
* word is a finding at its first character, once per run; see
|
|
51
53
|
* {@link module:engine/run-words}.
|
|
52
54
|
* - **A run inside a div or a verse fence inherits its keys**, and its own
|
|
53
|
-
* keys override them.
|
|
55
|
+
* keys override them. The language's script fills what is still unstated.
|
|
54
56
|
* - **Text with a font, a map or a declared language is plain.** A span or a
|
|
55
57
|
* verse holds no emphasis, link, image, footnote, wikilink, code or HTML,
|
|
56
58
|
* and no span but a marked one; a div holds paragraphs and line breaks only.
|
|
@@ -65,9 +67,15 @@
|
|
|
65
67
|
* neither whitespace nor in the face's `codepoints` is a finding, at the
|
|
66
68
|
* source character it came from. `mono`, `serif` and `sans` are not checked.
|
|
67
69
|
*
|
|
70
|
+
* - **A scroll fence is set in a face whose script note states
|
|
71
|
+
* `direction: vertical-rl`.** One that names no `font=` takes its
|
|
72
|
+
* language's script, else the face of a map drawn in one; one that names no
|
|
73
|
+
* face, or whose script is written in lines, is a finding. See {@link module:engine/content-scroll}.
|
|
74
|
+
*
|
|
68
75
|
* {@link scanRuns} reads the markup through {@link module:engine/content-spans.scanSpans},
|
|
69
|
-
* {@link module:engine/content-blocks.scanBlocks}
|
|
70
|
-
* {@link module:engine/content-verse.scanVerse}
|
|
76
|
+
* {@link module:engine/content-blocks.scanBlocks},
|
|
77
|
+
* {@link module:engine/content-verse.scanVerse} and
|
|
78
|
+
* {@link module:engine/content-scroll.scanScrolls}, so what counts as a span, a
|
|
71
79
|
* div or a fence is theirs to say.
|
|
72
80
|
*
|
|
73
81
|
* @module
|
|
@@ -85,6 +93,8 @@ import {
|
|
|
85
93
|
sourcePosition,
|
|
86
94
|
} from "./content-verse.mjs";
|
|
87
95
|
import { scanSpans } from "./content-spans.mjs";
|
|
96
|
+
import { scanScrolls, scrollParagraphs } from "./content-scroll.mjs";
|
|
97
|
+
import { directionsFromRecords } from "./script-direction.mjs";
|
|
88
98
|
import { isLanguageTag } from "./language-tag.mjs";
|
|
89
99
|
import { distance, nearest } from "./near-miss.mjs";
|
|
90
100
|
import { emphasesFromRecords, languageEmphasesOf } from "./script-emphasis.mjs";
|
|
@@ -101,6 +111,7 @@ import {
|
|
|
101
111
|
import { unknownWordMessage, unknownWords, wordBooksFromRecords } from "./run-words.mjs";
|
|
102
112
|
import { metreBooksFromRecords, metreFindings, metresFromRecords } from "./verse-metre.mjs";
|
|
103
113
|
import { applyMap, mapsFromRecords } from "./text-maps.mjs";
|
|
114
|
+
import { fromWrittenIn, writtenInOf } from "./written-in.mjs";
|
|
104
115
|
|
|
105
116
|
/** The keys this module reads, which no construct passes through as written. */
|
|
106
117
|
export const RUN_KEYS = Object.freeze(["lang", "font", "map"]);
|
|
@@ -127,6 +138,9 @@ const CONFIG = "package-build.config.yaml";
|
|
|
127
138
|
* @property {ReadonlyMap<string, import("./text-maps.mjs").TextMap>} [maps] -
|
|
128
139
|
* The mapping notes' maps by name, from
|
|
129
140
|
* {@link module:engine/text-maps.mapsFromRecords}.
|
|
141
|
+
* @property {ReadonlyMap<string, import("./written-in.mjs").WrittenIn>} [writtenIn] -
|
|
142
|
+
* The script each configured language's note names, by language name, from
|
|
143
|
+
* {@link module:engine/written-in.writtenInOf}.
|
|
130
144
|
* @property {ReadonlyMap<string, import("./map-lexicon.mjs").Lexicon>} [lexicons] -
|
|
131
145
|
* Each configured language's lexicon by name, from
|
|
132
146
|
* {@link module:engine/map-lexicon.lexiconsFromRecords}.
|
|
@@ -144,15 +158,20 @@ const CONFIG = "package-build.config.yaml";
|
|
|
144
158
|
* @property {ReadonlyMap<string, import("./script-emphasis.mjs").LanguageEmphasis>} [languageEmphases] -
|
|
145
159
|
* The convention each configured language's note states, by language name,
|
|
146
160
|
* from {@link module:engine/script-emphasis.languageEmphasesOf}.
|
|
161
|
+
* @property {ReadonlyMap<string, "vertical-rl">} [directions] - Each face whose
|
|
162
|
+
* script is written in columns, from the script notes, by
|
|
163
|
+
* {@link module:engine/script-direction.directionsFromRecords}.
|
|
147
164
|
*/
|
|
148
165
|
|
|
149
166
|
/**
|
|
150
|
-
* One span, div or
|
|
151
|
-
* `lang`, `font` and `map` are the ones it is set with: its own,
|
|
152
|
-
* innermost div or fence around it that states one
|
|
167
|
+
* One span, div, verse fence or scroll fence carrying `lang=`, `font=` or
|
|
168
|
+
* `map=`. Its `lang`, `font` and `map` are the ones it is set with: its own,
|
|
169
|
+
* else the innermost div or fence around it that states one, else its
|
|
170
|
+
* language's script. A scroll fence that still names no face takes the face of
|
|
171
|
+
* its map where the map is drawn in one.
|
|
153
172
|
*
|
|
154
173
|
* @typedef {object} Run
|
|
155
|
-
* @property {"span"|"block"|"verse"} kind - Which construct.
|
|
174
|
+
* @property {"span"|"block"|"verse"|"scroll"} kind - Which construct.
|
|
156
175
|
* @property {string|undefined} lang - The language as written.
|
|
157
176
|
* @property {string|undefined} tag - The BCP 47 tag it resolves to; `undefined`
|
|
158
177
|
* when it resolves to none.
|
|
@@ -207,15 +226,19 @@ export function runContext(config, records) {
|
|
|
207
226
|
if (!byRecords) withMaps.set(config, (byRecords = new WeakMap()));
|
|
208
227
|
const known = byRecords.get(records);
|
|
209
228
|
if (known) return known;
|
|
229
|
+
const maps = mapsFromRecords(records);
|
|
230
|
+
const languageRecords = languageRecordsByName(records, config);
|
|
210
231
|
const context = Object.freeze({
|
|
211
232
|
...base,
|
|
212
|
-
maps
|
|
233
|
+
maps,
|
|
234
|
+
writtenIn: writtenInOf(languageRecords, maps),
|
|
213
235
|
lexicons: lexiconsFromRecords(records, config),
|
|
214
236
|
wordBooks: wordBooksFromRecords(records, config),
|
|
215
237
|
metres: metresFromRecords(records),
|
|
216
238
|
metreBooks: metreBooksFromRecords(records, config),
|
|
217
239
|
emphases: emphasesFromRecords(records),
|
|
218
|
-
languageEmphases: languageEmphasesOf(
|
|
240
|
+
languageEmphases: languageEmphasesOf(languageRecords),
|
|
241
|
+
directions: directionsFromRecords(records),
|
|
219
242
|
});
|
|
220
243
|
byRecords.set(records, context);
|
|
221
244
|
return context;
|
|
@@ -643,6 +666,26 @@ function setText(raw, mapping, lineStart) {
|
|
|
643
666
|
return { text: out.join("\n"), pieces };
|
|
644
667
|
}
|
|
645
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Set a stretch of a scroll fence's text: the author's escapes removed and the
|
|
671
|
+
* map applied to all of it, its spaces included, and nothing escaped, since
|
|
672
|
+
* the fence's body is raw.
|
|
673
|
+
*
|
|
674
|
+
* @param {string} raw - The text, on one line.
|
|
675
|
+
* @param {import("./map-lexicon.mjs").Mapping|undefined} mapping - The run's
|
|
676
|
+
* map, face and lexicon.
|
|
677
|
+
* @returns {{text: string, pieces: Array<{from: string, to: string}>}} The set
|
|
678
|
+
* text, and each piece of it.
|
|
679
|
+
*/
|
|
680
|
+
function setScrollText(raw, mapping) {
|
|
681
|
+
const plain = unescapeRoman(raw);
|
|
682
|
+
return applyMap(mapping?.map, plain.text, {
|
|
683
|
+
font: mapping?.font,
|
|
684
|
+
native: mapping?.native,
|
|
685
|
+
writing: mapping?.writing,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
646
689
|
/**
|
|
647
690
|
* The parts of a range of text that no hole covers.
|
|
648
691
|
*
|
|
@@ -664,6 +707,29 @@ function outsideHoles(from, to, holes) {
|
|
|
664
707
|
return parts;
|
|
665
708
|
}
|
|
666
709
|
|
|
710
|
+
/**
|
|
711
|
+
* The paragraphs of a fence's body: each run of nonblank lines.
|
|
712
|
+
*
|
|
713
|
+
* @param {string[]} lines - The source, by line.
|
|
714
|
+
* @param {number} first - The body's first line, 0-based.
|
|
715
|
+
* @param {number} last - The line past it: the closing fence.
|
|
716
|
+
* @returns {Array<[number, number]>} Each paragraph's first line and the line
|
|
717
|
+
* past its last.
|
|
718
|
+
*/
|
|
719
|
+
function paragraphRanges(lines, first, last) {
|
|
720
|
+
const ranges = [];
|
|
721
|
+
let top;
|
|
722
|
+
for (let line = first; line <= last; line++) {
|
|
723
|
+
const blank = line === last || !lines[line].trim();
|
|
724
|
+
if (!blank && top === undefined) top = line;
|
|
725
|
+
else if (blank && top !== undefined) {
|
|
726
|
+
ranges.push([top, line]);
|
|
727
|
+
top = undefined;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return ranges;
|
|
731
|
+
}
|
|
732
|
+
|
|
667
733
|
/** `U+XXXX` for a code point. */
|
|
668
734
|
function codeName(point) {
|
|
669
735
|
return `U+${point.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
@@ -708,6 +774,8 @@ const BLOCK_WHAT = Object.freeze({
|
|
|
708
774
|
* What its emphasis is set by: its language's convention, else its face's
|
|
709
775
|
* script's; `undefined` sets it plain.
|
|
710
776
|
* @property {Delimiter[]} emphasis - The emphasis it holds, by absolute offset.
|
|
777
|
+
* @property {boolean} [unrolled] - A scroll fence whose opening has a finding,
|
|
778
|
+
* which is left as written.
|
|
711
779
|
*/
|
|
712
780
|
|
|
713
781
|
/**
|
|
@@ -871,6 +939,36 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
871
939
|
});
|
|
872
940
|
}
|
|
873
941
|
|
|
942
|
+
// Every scroll fence is a construct, keys or none: its text is set in a
|
|
943
|
+
// face, so one that names none is a finding.
|
|
944
|
+
for (const scroll of scanScrolls(text).blocks) {
|
|
945
|
+
const opening = lines[scroll.start];
|
|
946
|
+
constructs.push({
|
|
947
|
+
kind: "scroll",
|
|
948
|
+
keys: runKeys(scroll.attributes),
|
|
949
|
+
node: scroll,
|
|
950
|
+
line0: scroll.start,
|
|
951
|
+
last: scroll.end,
|
|
952
|
+
at: (key, part) => ({
|
|
953
|
+
line: scroll.start + 1,
|
|
954
|
+
column: attributeColumns(opening, key)[part],
|
|
955
|
+
}),
|
|
956
|
+
where: {
|
|
957
|
+
line: scroll.start + 1,
|
|
958
|
+
column: scroll.indent.length + 1,
|
|
959
|
+
start: scroll.start,
|
|
960
|
+
end: scroll.end,
|
|
961
|
+
},
|
|
962
|
+
own: {
|
|
963
|
+
id: "",
|
|
964
|
+
classes: scroll.classes ?? [],
|
|
965
|
+
values: Object.entries(scroll.attributes ?? {}).filter(
|
|
966
|
+
([key]) => !RUN_KEYS.includes(key),
|
|
967
|
+
),
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
874
972
|
const containers = constructs.filter((construct) => construct.kind !== "span");
|
|
875
973
|
/** The containers around a construct, outermost first. */
|
|
876
974
|
const around = (construct) =>
|
|
@@ -1122,6 +1220,47 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1122
1220
|
}
|
|
1123
1221
|
};
|
|
1124
1222
|
|
|
1223
|
+
/**
|
|
1224
|
+
* Give a scroll fence the face it is set in, and report a fence that names
|
|
1225
|
+
* none or whose script is written in lines. A fence that names no face
|
|
1226
|
+
* takes its map's, where the map is drawn in one.
|
|
1227
|
+
*
|
|
1228
|
+
* @param {object} construct - The scroll fence.
|
|
1229
|
+
* @param {Record<string, string>} keys - Its run keys, given the face.
|
|
1230
|
+
*/
|
|
1231
|
+
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
|
+
if (!ctx.directions || !(faces.has(keys.font) || Object.hasOwn(GENERIC_FONTS, keys.font)))
|
|
1252
|
+
return;
|
|
1253
|
+
if (ctx.directions.get(keys.font) === "vertical-rl") return;
|
|
1254
|
+
report(
|
|
1255
|
+
construct.keys.font !== undefined ? construct.at("font", "value")
|
|
1256
|
+
: construct.keys.map !== undefined ? construct.at("map", "value")
|
|
1257
|
+
: construct.keys.lang !== undefined ? construct.at("lang", "value")
|
|
1258
|
+
: construct.where,
|
|
1259
|
+
`a \`scroll\` fence sets a script written in columns, and the script of \`${keys.font}\` ` +
|
|
1260
|
+
"is written in lines; set it in a face whose script note states `direction: vertical-rl`, or use a div",
|
|
1261
|
+
);
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1125
1264
|
const runs = [];
|
|
1126
1265
|
const details = [];
|
|
1127
1266
|
for (const construct of constructs) {
|
|
@@ -1129,8 +1268,15 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1129
1268
|
checkOwn(construct);
|
|
1130
1269
|
// A key that names nothing is the finding; the text it would have set
|
|
1131
1270
|
// is not then checked against a face or a language it cannot reach.
|
|
1132
|
-
|
|
1271
|
+
let named = errors.length === unchecked;
|
|
1133
1272
|
const keys = Object.assign({}, ...around(construct).map((c) => c.keys), construct.keys);
|
|
1273
|
+
// The language's script fills what the run leaves unstated, once its
|
|
1274
|
+
// own keys are known to name something.
|
|
1275
|
+
if (named) Object.assign(keys, fromWrittenIn(keys, ctx.writtenIn, maps));
|
|
1276
|
+
if (construct.kind === "scroll" && named) {
|
|
1277
|
+
scrollFace(construct, keys);
|
|
1278
|
+
named = errors.length === unchecked;
|
|
1279
|
+
}
|
|
1134
1280
|
const { lang, font, map } = keys;
|
|
1135
1281
|
// A face the run's map is not for, once the face and the map are each
|
|
1136
1282
|
// known, at the construct that states either.
|
|
@@ -1177,6 +1323,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1177
1323
|
languageEmphasis: lang === undefined ? undefined : ctx.languageEmphases?.get(lang),
|
|
1178
1324
|
conventions: undefined,
|
|
1179
1325
|
emphasis: [],
|
|
1326
|
+
...(construct.kind === "scroll" && construct.node.invalid ? { unrolled: true } : {}),
|
|
1180
1327
|
};
|
|
1181
1328
|
detail.conventions = conventionsOf(detail, ctx);
|
|
1182
1329
|
details.push(detail);
|
|
@@ -1248,6 +1395,16 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1248
1395
|
);
|
|
1249
1396
|
else if (Object.hasOwn(BLOCK_WHAT, token.type)) said(top, BLOCK_WHAT[token.type]);
|
|
1250
1397
|
}
|
|
1398
|
+
} else if (construct.kind === "scroll") {
|
|
1399
|
+
// A paragraph is reflowed as one text, so its emphasis may cross
|
|
1400
|
+
// the lines it is written on.
|
|
1401
|
+
for (const [top, past] of paragraphRanges(lines, first, last))
|
|
1402
|
+
plainInline(
|
|
1403
|
+
text.slice(starts[top], starts[past - 1] + lines[past - 1].length),
|
|
1404
|
+
starts[top],
|
|
1405
|
+
marking,
|
|
1406
|
+
detail,
|
|
1407
|
+
);
|
|
1251
1408
|
} else {
|
|
1252
1409
|
for (let line = first; line < last; line++)
|
|
1253
1410
|
plainInline(lines[line], starts[line], marking, detail, detail.holes);
|
|
@@ -1382,7 +1539,8 @@ function rewrittenAttributes(detail, search) {
|
|
|
1382
1539
|
const changes =
|
|
1383
1540
|
run.font !== undefined ||
|
|
1384
1541
|
run.map !== undefined ||
|
|
1385
|
-
(run.tag !== undefined &&
|
|
1542
|
+
(run.tag !== undefined &&
|
|
1543
|
+
(keys.lang !== run.tag || run.kind === "verse" || run.kind === "scroll"));
|
|
1386
1544
|
if (!changes) return undefined;
|
|
1387
1545
|
const classes = [...own.classes];
|
|
1388
1546
|
const values = [...own.values];
|
|
@@ -1469,6 +1627,10 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
1469
1627
|
* - **A div or fence in a right-to-left face** carries `dir=rtl`. No direction
|
|
1470
1628
|
* mark, `<bdo>` or `<bdi>` is ever written: the code points carry their own
|
|
1471
1629
|
* direction.
|
|
1630
|
+
* - **A scroll fence's body** is rewritten to one line per paragraph, its
|
|
1631
|
+
* lines joined by what the map writes for a space, and left unescaped,
|
|
1632
|
+
* since the body is raw. Its layout, each character with the source text it
|
|
1633
|
+
* was written from, is returned for the book, which draws it.
|
|
1472
1634
|
*
|
|
1473
1635
|
* A run with a finding is left as written; {@link scanRuns} reports it.
|
|
1474
1636
|
*
|
|
@@ -1478,8 +1640,9 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
1478
1640
|
* scanner projections.
|
|
1479
1641
|
* @param {{target?: "site"|"foundry"|"book"}} [options] - The target the
|
|
1480
1642
|
* Markdown is rendered for; only `site` writes the search marker.
|
|
1481
|
-
* @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>}}
|
|
1482
|
-
* The rewritten Markdown,
|
|
1643
|
+
* @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>, scrolls: Map<string, import("./content-scroll.mjs").ScrollLayout>}}
|
|
1644
|
+
* The rewritten Markdown, the findings of {@link scanRuns}, and each
|
|
1645
|
+
* rewritten scroll fence's layout, by its rewritten body.
|
|
1483
1646
|
*/
|
|
1484
1647
|
export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
1485
1648
|
const search = options.target === "site";
|
|
@@ -1487,9 +1650,11 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1487
1650
|
const authored = String(source ?? "");
|
|
1488
1651
|
const text = expandVerse(authored);
|
|
1489
1652
|
if (text !== authored) syntax = undefined;
|
|
1490
|
-
|
|
1653
|
+
/** @type {Map<string, import("./content-scroll.mjs").ScrollLayout>} */
|
|
1654
|
+
const scrolls = new Map();
|
|
1655
|
+
if (!/\b(?:lang|font|map)=/.test(text)) return { markdown: text, errors: [], scrolls };
|
|
1491
1656
|
const { details, errors } = analyseRuns(text, ctx, syntax);
|
|
1492
|
-
if (!details.length) return { markdown: text, errors };
|
|
1657
|
+
if (!details.length) return { markdown: text, errors, scrolls };
|
|
1493
1658
|
const lines = text.split("\n");
|
|
1494
1659
|
const starts = lineStarts(text);
|
|
1495
1660
|
|
|
@@ -1507,6 +1672,72 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1507
1672
|
/** @type {Array<{from: number, to: number, text: string}>} */
|
|
1508
1673
|
const edits = [];
|
|
1509
1674
|
|
|
1675
|
+
/**
|
|
1676
|
+
* A scroll fence's body set as one line per paragraph, and its layout.
|
|
1677
|
+
*
|
|
1678
|
+
* @param {RunDetail} detail - The scroll fence.
|
|
1679
|
+
* @returns {{text: string, layout: import("./content-scroll.mjs").ScrollLayout}}
|
|
1680
|
+
* The body and the layout.
|
|
1681
|
+
*/
|
|
1682
|
+
const setScroll = (detail) => {
|
|
1683
|
+
const join = setScrollText(" ", detail.mapping);
|
|
1684
|
+
const texts = [];
|
|
1685
|
+
const paragraphs = [];
|
|
1686
|
+
for (const [top, past] of paragraphRanges(lines, detail.run.start + 1, detail.run.end)) {
|
|
1687
|
+
let out = "";
|
|
1688
|
+
/** @type {import("./content-scroll.mjs").ScrollCell[]} */
|
|
1689
|
+
const cells = [];
|
|
1690
|
+
const conventions = [];
|
|
1691
|
+
let pending = "";
|
|
1692
|
+
const emit = (pieces) => {
|
|
1693
|
+
for (const piece of pieces) {
|
|
1694
|
+
const chars = [...piece.to];
|
|
1695
|
+
if (!chars.length) {
|
|
1696
|
+
pending += piece.from;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
const emphasis = conventions.at(-1);
|
|
1700
|
+
chars.forEach((char, at) =>
|
|
1701
|
+
cells.push({
|
|
1702
|
+
text: char,
|
|
1703
|
+
roman: at ? "" : pending + piece.from,
|
|
1704
|
+
...(emphasis ? { emphasis } : {}),
|
|
1705
|
+
}),
|
|
1706
|
+
);
|
|
1707
|
+
pending = "";
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
const set = (from, to) => {
|
|
1711
|
+
const result = setScrollText(text.slice(from, to), detail.mapping);
|
|
1712
|
+
out += result.text;
|
|
1713
|
+
emit(result.pieces);
|
|
1714
|
+
};
|
|
1715
|
+
for (let line = top; line < past; line++) {
|
|
1716
|
+
if (line > top) {
|
|
1717
|
+
out += join.text;
|
|
1718
|
+
emit(join.pieces);
|
|
1719
|
+
}
|
|
1720
|
+
const written = lines[line];
|
|
1721
|
+
const from = starts[line] + written.length - written.trimStart().length;
|
|
1722
|
+
const to = starts[line] + written.trimEnd().length;
|
|
1723
|
+
let at = from;
|
|
1724
|
+
for (const delimiter of detail.emphasis) {
|
|
1725
|
+
if (delimiter.offset < from || delimiter.offset >= to) continue;
|
|
1726
|
+
if (delimiter.offset > at) set(at, delimiter.offset);
|
|
1727
|
+
out += emphasisTag(delimiter, detail.conventions, book);
|
|
1728
|
+
if (delimiter.open) conventions.push(detail.conventions?.[delimiter.tag]);
|
|
1729
|
+
else conventions.pop();
|
|
1730
|
+
at = delimiter.offset + delimiter.length;
|
|
1731
|
+
}
|
|
1732
|
+
if (at < to) set(at, to);
|
|
1733
|
+
}
|
|
1734
|
+
if (pending && cells.length) cells[cells.length - 1].roman += pending;
|
|
1735
|
+
texts.push(out);
|
|
1736
|
+
paragraphs.push(cells);
|
|
1737
|
+
}
|
|
1738
|
+
return { text: texts.join("\n\n"), layout: { face: detail.face, paragraphs } };
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1510
1741
|
for (const detail of details) {
|
|
1511
1742
|
const { run } = detail;
|
|
1512
1743
|
if (faulted(run)) continue;
|
|
@@ -1528,6 +1759,22 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1528
1759
|
edits.push({ from: run.start, to: run.end, text: `[${inner}]{${braces}}` });
|
|
1529
1760
|
continue;
|
|
1530
1761
|
}
|
|
1762
|
+
if (run.kind === "scroll" && attributes !== undefined && !detail.unrolled) {
|
|
1763
|
+
const open = lines[run.start].indexOf("{");
|
|
1764
|
+
const close = lines[run.start].lastIndexOf("}");
|
|
1765
|
+
edits.push({
|
|
1766
|
+
from: starts[run.start] + open + 1,
|
|
1767
|
+
to: starts[run.start] + close,
|
|
1768
|
+
text: attributes,
|
|
1769
|
+
});
|
|
1770
|
+
if (detail.plain && detail.body) {
|
|
1771
|
+
const { text: body, layout } = setScroll(detail);
|
|
1772
|
+
edits.push({ from: detail.body[0], to: detail.body[1], text: body });
|
|
1773
|
+
scrolls.set(scrollParagraphs(body.split("\n")).join("\n\n"), layout);
|
|
1774
|
+
}
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
if (run.kind === "scroll") continue;
|
|
1531
1778
|
if (attributes !== undefined) {
|
|
1532
1779
|
const open = lines[run.start].indexOf("{");
|
|
1533
1780
|
const close = lines[run.start].lastIndexOf("}");
|
|
@@ -1548,5 +1795,5 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1548
1795
|
let markdown = text;
|
|
1549
1796
|
for (const edit of edits.sort((a, b) => b.from - a.from))
|
|
1550
1797
|
markdown = markdown.slice(0, edit.from) + edit.text + markdown.slice(edit.to);
|
|
1551
|
-
return { markdown, errors };
|
|
1798
|
+
return { markdown, errors, scrolls };
|
|
1552
1799
|
}
|