@heroiclands/package-build 25.0.0 → 25.1.1
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 +26 -0
- package/README.md +1 -0
- package/bin/content-commands.mjs +30 -96
- package/bin/encoding-commands.mjs +146 -0
- package/bin/package-build.mjs +35 -24
- package/config.mjs +25 -0
- package/convert.mjs +182 -0
- package/docs/api.md +144 -6
- package/docs/authoring/links-and-markup.md +260 -481
- package/docs/commands.md +110 -119
- package/docs/configuration.md +31 -13
- package/docs/content-format.md +11 -4
- package/docs/diagnostics.md +43 -16
- package/docs/encoding-core.md +193 -0
- package/docs/guides/book.md +2 -2
- package/docs/guides/module-project.md +23 -1
- package/docs/guides/site.md +2 -2
- package/docs/guides/writing-in-invented-scripts.md +458 -575
- package/docs/reference/format-details.md +292 -404
- package/docs/reference/note-types.md +83 -63
- package/docs/source-preview.md +8 -0
- package/e2e.mjs +1 -1
- package/engine/base-compiler.mjs +27 -9
- package/engine/content-charset.mjs +13 -1
- package/engine/content-format.yaml +6 -2
- package/engine/content-index.mjs +6 -2
- package/engine/content-links.mjs +1 -0
- package/engine/content-scripts.mjs +614 -459
- package/engine/content-scroll.mjs +4 -4
- package/engine/content-spans.mjs +40 -9
- package/engine/content-verse.mjs +1 -1
- package/engine/converter-data.mjs +70 -0
- package/engine/dictionary-rows.mjs +2 -34
- package/engine/encoding-integrity.mjs +94 -0
- package/engine/encoding-language.mjs +506 -0
- package/engine/encoding-lexicon.mjs +701 -0
- package/engine/encoding-notes.mjs +186 -0
- package/engine/encoding-standard.mjs +42 -0
- package/engine/encoding-template.mjs +370 -0
- package/engine/encoding-terms.mjs +178 -0
- package/engine/encoding.mjs +952 -0
- package/engine/extension-attributes.mjs +2 -0
- package/engine/helpers.mjs +1 -1
- package/engine/index.mjs +7 -0
- package/engine/language-terms.mjs +17 -16
- package/engine/{logographic.mjs → language-writings.mjs} +29 -38
- package/engine/markup-validation.mjs +1 -1
- package/engine/note-vocabulary.mjs +4 -158
- package/engine/pdf-render.mjs +31 -3
- package/engine/roman-characters.mjs +135 -16
- package/engine/roman-run.mjs +149 -0
- package/engine/roman-verse.mjs +95 -0
- package/engine/run-rows.mjs +29 -31
- package/engine/run-words.mjs +2 -2
- package/engine/script-emphasis-terms.mjs +3 -0
- package/engine/script-emphasis.mjs +3 -10
- package/engine/script-font-delivery.mjs +9 -12
- package/engine/script-fonts.mjs +2 -1
- package/engine/site-build.mjs +14 -5
- package/engine/site-preview.mjs +12 -2
- package/engine/sql-tables.mjs +36 -31
- package/engine/verse-metre.mjs +1 -1
- package/engine/word-forms.mjs +1 -1
- package/engine/word-inflections.mjs +48 -0
- package/engine/word-rows.mjs +1 -1
- package/engine/word-terms.mjs +16 -17
- package/package.json +11 -5
- package/types/config.d.mts +5 -0
- package/types/convert.d.mts +44 -0
- package/types/e2e.d.mts +1 -1
- package/types/engine/base-compiler.d.mts +4 -2
- package/types/engine/content-index.d.mts +4 -1
- package/types/engine/content-scripts.d.mts +103 -42
- package/types/engine/converter-data.d.mts +11 -0
- package/types/engine/dictionary-rows.d.mts +1 -25
- package/types/engine/encoding-integrity.d.mts +14 -0
- package/types/engine/encoding-language.d.mts +36 -0
- package/types/engine/encoding-lexicon.d.mts +259 -0
- package/types/engine/encoding-notes.d.mts +39 -0
- package/types/engine/encoding-standard.d.mts +11 -0
- package/types/engine/encoding-template.d.mts +44 -0
- package/types/engine/encoding-terms.d.mts +143 -0
- package/types/engine/encoding.d.mts +155 -0
- package/types/engine/helpers.d.mts +1 -1
- package/types/engine/index.d.mts +4 -0
- package/types/engine/language-terms.d.mts +1 -1
- package/types/engine/{logographic.d.mts → language-writings.d.mts} +6 -6
- package/types/engine/markup-validation.d.mts +1 -1
- package/types/engine/pdf-render.d.mts +1 -1
- package/types/engine/roman-characters.d.mts +2 -2
- package/types/engine/roman-run.d.mts +101 -0
- package/types/engine/roman-verse.d.mts +23 -0
- package/types/engine/run-rows.d.mts +13 -20
- package/types/engine/script-emphasis-terms.d.mts +2 -0
- package/types/engine/script-emphasis.d.mts +1 -1
- package/types/engine/script-font-delivery.d.mts +2 -3
- package/types/engine/script-fonts.d.mts +5 -0
- package/types/engine/site-preview.d.mts +1 -1
- package/types/engine/word-inflections.d.mts +25 -0
- package/engine/map-lexicon.mjs +0 -1045
- package/engine/text-maps.mjs +0 -654
- package/engine/written-in.mjs +0 -204
- package/types/engine/map-lexicon.d.mts +0 -330
- package/types/engine/text-maps.d.mts +0 -189
- package/types/engine/written-in.d.mts +0 -76
|
@@ -12,80 +12,31 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Text marked with a language, font or encoding document on shared attributes.
|
|
16
|
+
* `enc=` resolves a document Address and infers its configured language and
|
|
17
|
+
* font; explicit selections must agree. `enc=default`, including an omitted
|
|
18
|
+
* selection, retains Roman text. A language alias or BCP 47 tag identifies
|
|
19
|
+
* text independently of conversion, and an installed font or generic family
|
|
20
|
+
* selects its face independently.
|
|
18
21
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* Configured language notes supply vocabulary, Roman additions and emphasis.
|
|
23
|
+
* Encoded words read lexical native writings, complete parts and encoding
|
|
24
|
+
* mappings in that order. Source validation sees the complete logical run,
|
|
25
|
+
* including class-only emphasis and name children, while physical rendering
|
|
26
|
+
* preserves their wrappers. Children stating run keys make a separate choice.
|
|
27
|
+
* A native name's braces write the shared name controls.
|
|
22
28
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* ```
|
|
29
|
-
*
|
|
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}.
|
|
35
|
-
* - **`font=`** is one of the installed faces, by its class without `hl-`
|
|
36
|
-
* (`semaron-kuriale`), or `mono`, `serif` or `sans`.
|
|
37
|
-
* - **`map=`** names a mapping note's `data.map`, which rewrites the text
|
|
38
|
-
* before it is set; see {@link module:engine/text-maps}. Without it the text
|
|
39
|
-
* is set as typed. A run's `font=` is one of the faces the map lists in its
|
|
40
|
-
* `fonts`, whose own rows come before the shared ones.
|
|
41
|
-
* - **`lang=` with `map=` reads the language's lexicon first**: a word whose
|
|
42
|
-
* entry states a native form for the map is written by it, and the rows
|
|
43
|
-
* spell every other word; see {@link module:engine/map-lexicon}.
|
|
44
|
-
* - **A logographic map writes every word from the lexicon**: by the word's
|
|
45
|
-
* own writing or its parts', and a word with neither, or whose form stands
|
|
46
|
-
* for entries written differently, is a finding at the word, once per run;
|
|
47
|
-
* see {@link module:engine/logographic}.
|
|
48
|
-
* - **Every word of a run with `lang=` and `map=` is a word of its language**
|
|
49
|
-
* where the language's `note` names its language note: a lexicon form, a
|
|
50
|
-
* word of a name its own register lists, or a regular form of a lexicon
|
|
51
|
-
* form. Any other
|
|
52
|
-
* word is a finding at its first character, once per run; see
|
|
53
|
-
* {@link module:engine/run-words}.
|
|
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
|
|
67
|
-
* language's note states, else its face's script note, else plainly; see
|
|
68
|
-
* {@link module:engine/script-emphasis}. A language that marks emphasis
|
|
69
|
-
* with a word makes either a finding at the emphasis.
|
|
70
|
-
* The text is read as written, so `[`, `]`, `"`, `<<`, `>>`, `!` and
|
|
71
|
-
* `@#$%^&` are characters, and need no escaping. A real language's tag 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.
|
|
74
|
-
* - **A face draws only what it lists.** After the map, a character that is
|
|
75
|
-
* neither whitespace nor in the face's `codepoints` is a finding, at the
|
|
76
|
-
* source character it came from. `mono`, `serif` and `sans` are not checked.
|
|
77
|
-
*
|
|
78
|
-
* - **A scroll fence is set in a face whose script note states
|
|
79
|
-
* `direction: vertical-rl`.** One that names no `font=` takes its
|
|
80
|
-
* language's script, else the face of a map drawn in one; one that names no
|
|
81
|
-
* face, or whose script is written in lines, is a finding. See {@link module:engine/content-scroll}.
|
|
82
|
-
*
|
|
83
|
-
* {@link scanRuns} reads the markup through {@link module:engine/content-spans.scanSpans},
|
|
84
|
-
* {@link module:engine/content-blocks.scanBlocks},
|
|
85
|
-
* {@link module:engine/content-verse.scanVerse} and
|
|
86
|
-
* {@link module:engine/content-scroll.scanScrolls}, so what counts as a span, a
|
|
87
|
-
* div or a fence is theirs to say.
|
|
29
|
+
* Plain configured-language runs restrict Markdown consistently across site,
|
|
30
|
+
* Foundry and book targets. Non-English spans admit no bold or italic markup;
|
|
31
|
+
* explicit emphasis classes follow the language's or script's convention.
|
|
32
|
+
* English text with the default encoding retains ordinary Markdown grammar.
|
|
33
|
+
* A selected font must draw every visible encoded output character.
|
|
88
34
|
*
|
|
35
|
+
* Span, div, verse and scroll scanners retain original UTF-16 source offsets.
|
|
36
|
+
* Public diagnostics count Unicode code-point columns. A scroll's face must
|
|
37
|
+
* belong to a script written vertically, and parallel verse selects encoding
|
|
38
|
+
* on each reading rather than the fence. Every native content line retains
|
|
39
|
+
* its line boundary before a stanza or canto.
|
|
89
40
|
* @module
|
|
90
41
|
*/
|
|
91
42
|
|
|
@@ -105,25 +56,25 @@ import {
|
|
|
105
56
|
fontProblem,
|
|
106
57
|
languageRecordsByName,
|
|
107
58
|
lexiconsFromRecords,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
unwrittenMessage,
|
|
111
|
-
} from "./map-lexicon.mjs";
|
|
59
|
+
encodingFor,
|
|
60
|
+
} from "./encoding-lexicon.mjs";
|
|
112
61
|
import { unknownWordMessage, unknownWords, wordBooksFromRecords } from "./run-words.mjs";
|
|
113
62
|
import { metreBooksFromRecords, metreFindings, metresFromRecords } from "./verse-metre.mjs";
|
|
114
|
-
import {
|
|
115
|
-
import {
|
|
63
|
+
import { encodeWithLanguage } from "./encoding-language.mjs";
|
|
64
|
+
import { encodingsFromRecords, encodingAddress } from "./encoding-notes.mjs";
|
|
116
65
|
import { romanInventoriesOf, romanTextProblems } from "./roman-characters.mjs";
|
|
117
|
-
import {
|
|
66
|
+
import { plainRomanRun } from "./roman-run.mjs";
|
|
67
|
+
import { plainRomanVerse } from "./roman-verse.mjs";
|
|
68
|
+
import { encodeText } from "./encoding.mjs";
|
|
118
69
|
import { WIKILINK } from "./wikilink-syntax.mjs";
|
|
119
70
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
120
71
|
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
121
72
|
|
|
122
73
|
/** The keys this module reads, which no construct passes through as written. */
|
|
123
|
-
export const RUN_KEYS = Object.freeze(["lang", "font", "
|
|
74
|
+
export const RUN_KEYS = Object.freeze(["lang", "font", "enc"]);
|
|
124
75
|
|
|
125
76
|
/** Explicit opt-in to native-script presentation on this construct. */
|
|
126
|
-
|
|
77
|
+
const NATIVEFONT_CLASS = "nativefont";
|
|
127
78
|
|
|
128
79
|
/** The generic families `font=` may name, each with the class that sets it. */
|
|
129
80
|
export const GENERIC_FONTS = Object.freeze({
|
|
@@ -139,22 +90,22 @@ const CONFIG = "package-build.config.yaml";
|
|
|
139
90
|
* What a run is checked against.
|
|
140
91
|
*
|
|
141
92
|
* @typedef {object} RunContext
|
|
93
|
+
* @property {string} [contentPackage] - The citing content package.
|
|
142
94
|
* @property {Readonly<Record<string, {tag: string}>>} [languages] - The
|
|
143
95
|
* configured languages.
|
|
144
96
|
* @property {ReadonlyMap<string, import("./script-fonts.mjs").Face>} [faces] -
|
|
145
97
|
* The installed faces by slug, from
|
|
146
98
|
* {@link module:engine/script-fonts.resolveScriptFonts}.
|
|
147
|
-
* @property {ReadonlyMap<string, import("./
|
|
148
|
-
* The
|
|
149
|
-
*
|
|
150
|
-
* @property {ReadonlyMap<string,
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* @property {ReadonlyMap<string, import("./map-lexicon.mjs").Lexicon>} [lexicons] -
|
|
99
|
+
* @property {ReadonlyMap<string, ReturnType<import("./encoding.mjs").compileEncoding>>} [encodings] -
|
|
100
|
+
* The compiled encoding documents, keyed by canonical and local Addresses.
|
|
101
|
+
* @property {ReadonlyArray<{file?: string, line?: number, column?: number, message: string}>} [encodingFindings] - Encoding-document or font-code findings.
|
|
102
|
+
* @property {ReadonlyMap<string, {data: object, codes: Record<string,string>}>} [encodingDefinitions] - Serialized encoding documents and their selected font codes.
|
|
103
|
+
* @property {ReadonlyMap<string, Record<string, any>>} [languageRecords] - The configured language notes.
|
|
104
|
+
* @property {ReadonlyMap<string, import("./encoding-lexicon.mjs").Lexicon>} [lexicons] -
|
|
154
105
|
* Each configured language's lexicon by name, from
|
|
155
|
-
* {@link module:engine/
|
|
106
|
+
* {@link module:engine/encoding-lexicon.lexiconsFromRecords}.
|
|
156
107
|
* @property {ReadonlyMap<string, import("./run-words.mjs").WordBook>} [wordBooks] -
|
|
157
|
-
* What each configured language's
|
|
108
|
+
* What each configured language's encoded runs are checked against, from
|
|
158
109
|
* {@link module:engine/run-words.wordBooksFromRecords}.
|
|
159
110
|
* @property {ReadonlyMap<string, import("./metre-declaration.mjs").Metre>} [metres] -
|
|
160
111
|
* The metres the `doc` notes declare, by name.
|
|
@@ -175,11 +126,9 @@ const CONFIG = "package-build.config.yaml";
|
|
|
175
126
|
*/
|
|
176
127
|
|
|
177
128
|
/**
|
|
178
|
-
* One span, div, verse fence or scroll fence carrying
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* defaults are selected only by this construct's explicit `.nativefont`
|
|
182
|
-
* opt-in; marked children make their own presentation choice.
|
|
129
|
+
* One span, div, verse fence or scroll fence carrying run attributes.
|
|
130
|
+
* Values combine explicit choices, class-only inheritance, encoding-document
|
|
131
|
+
* language/font defaults and the Roman default encoding.
|
|
183
132
|
*
|
|
184
133
|
* @typedef {object} Run
|
|
185
134
|
* @property {"span"|"block"|"verse"|"scroll"} kind - Which construct.
|
|
@@ -187,32 +136,30 @@ const CONFIG = "package-build.config.yaml";
|
|
|
187
136
|
* @property {string|undefined} tag - The BCP 47 tag it resolves to; `undefined`
|
|
188
137
|
* when it resolves to none.
|
|
189
138
|
* @property {string|undefined} font - The font as written.
|
|
190
|
-
* @property {string|undefined}
|
|
139
|
+
* @property {string|undefined} enc - The resolved document Address, or default for Roman text.
|
|
191
140
|
* @property {number} line - 1-based line of the construct's opening.
|
|
192
141
|
* @property {number} column - 1-based UTF-16 column of its opening.
|
|
193
142
|
* @property {number} start - A span's offset of `[`; a div's or fence's
|
|
194
143
|
* 0-based opening line.
|
|
195
144
|
* @property {number} end - A span's offset past its attributes; a div's or
|
|
196
145
|
* fence's 0-based closing line.
|
|
197
|
-
* @property {boolean} [nativefont] - True only for an explicit native opt-in
|
|
198
|
-
* with its own language; retained by index records for deferred defaults.
|
|
199
146
|
*/
|
|
200
147
|
|
|
201
148
|
/** @type {WeakMap<object, RunContext>} */
|
|
202
149
|
const contexts = new WeakMap();
|
|
203
150
|
|
|
204
151
|
/** @type {WeakMap<object, WeakMap<object, RunContext>>} */
|
|
205
|
-
const
|
|
152
|
+
const withEncodings = new WeakMap();
|
|
206
153
|
|
|
207
154
|
/**
|
|
208
|
-
* The context a resolved configuration gives, and the
|
|
155
|
+
* The context a resolved configuration gives, and the encodings an index holds.
|
|
209
156
|
*
|
|
210
|
-
* @param {{languages?: object, rootDir?: string}|undefined} config - The
|
|
157
|
+
* @param {{languages?: object, rootDir?: string, contentPackage?:string, paths?:{content?:string}}|undefined} config - The
|
|
211
158
|
* resolved configuration.
|
|
212
159
|
* @param {Iterable<Record<string, any>>} [records] - The content index's
|
|
213
|
-
* records, whose
|
|
160
|
+
* records, whose language notes supply lexical writings and script choices.
|
|
214
161
|
* @returns {RunContext} Its languages, the faces installed where it sits, and
|
|
215
|
-
* the
|
|
162
|
+
* the encodings. A fonts package that cannot be read gives no faces; a run naming
|
|
216
163
|
* one is reported.
|
|
217
164
|
*/
|
|
218
165
|
export function runContext(config, records) {
|
|
@@ -220,14 +167,19 @@ export function runContext(config, records) {
|
|
|
220
167
|
let base = contexts.get(config);
|
|
221
168
|
if (!base) {
|
|
222
169
|
let faces;
|
|
170
|
+
let fonts;
|
|
223
171
|
if (config.rootDir) {
|
|
224
172
|
try {
|
|
225
|
-
|
|
173
|
+
fonts = resolveScriptFonts(config.rootDir);
|
|
174
|
+
faces = fonts?.faces;
|
|
226
175
|
} catch {
|
|
227
176
|
faces = undefined;
|
|
228
177
|
}
|
|
229
178
|
}
|
|
230
179
|
base = Object.freeze({
|
|
180
|
+
fonts,
|
|
181
|
+
encodings: new Map(),
|
|
182
|
+
encodingFindings: [],
|
|
231
183
|
languages: config.languages ?? {},
|
|
232
184
|
faces: faces ?? new Map(),
|
|
233
185
|
contentPackage: config.contentPackage,
|
|
@@ -235,16 +187,19 @@ export function runContext(config, records) {
|
|
|
235
187
|
contexts.set(config, base);
|
|
236
188
|
}
|
|
237
189
|
if (!records || typeof records !== "object") return base;
|
|
238
|
-
let byRecords =
|
|
239
|
-
if (!byRecords)
|
|
190
|
+
let byRecords = withEncodings.get(config);
|
|
191
|
+
if (!byRecords) withEncodings.set(config, (byRecords = new WeakMap()));
|
|
240
192
|
const known = byRecords.get(records);
|
|
241
193
|
if (known) return known;
|
|
242
|
-
const
|
|
194
|
+
const loaded = encodingsFromRecords(records, config, base.fonts);
|
|
195
|
+
const encodings = loaded.encodings;
|
|
243
196
|
const languageRecords = languageRecordsByName(records, config);
|
|
244
197
|
const context = Object.freeze({
|
|
245
198
|
...base,
|
|
246
|
-
|
|
247
|
-
|
|
199
|
+
encodings,
|
|
200
|
+
encodingDefinitions: loaded.definitions,
|
|
201
|
+
encodingFindings: loaded.findings,
|
|
202
|
+
languageRecords,
|
|
248
203
|
romanInventories: romanInventoriesOf(languageRecords, config.languages),
|
|
249
204
|
lexicons: lexiconsFromRecords(records, config),
|
|
250
205
|
wordBooks: wordBooksFromRecords(records, config),
|
|
@@ -595,28 +550,6 @@ function emphasisDelimiters(text, holes = new Map()) {
|
|
|
595
550
|
return delimiters;
|
|
596
551
|
}
|
|
597
552
|
|
|
598
|
-
/**
|
|
599
|
-
* The parts of ranges of text that no delimiter covers.
|
|
600
|
-
*
|
|
601
|
-
* @param {Array<[number, number]>} parts - The ranges, as offsets.
|
|
602
|
-
* @param {readonly Delimiter[]} delimiters - Delimiters, by absolute offset.
|
|
603
|
-
* @returns {Array<[number, number]>} The ranges, split around each delimiter.
|
|
604
|
-
*/
|
|
605
|
-
function outsideDelimiters(parts, delimiters) {
|
|
606
|
-
if (!delimiters.length) return parts;
|
|
607
|
-
const out = [];
|
|
608
|
-
for (const [from, to] of parts) {
|
|
609
|
-
let at = from;
|
|
610
|
-
for (const delimiter of delimiters) {
|
|
611
|
-
if (delimiter.offset < at || delimiter.offset >= to) continue;
|
|
612
|
-
if (delimiter.offset > at) out.push([at, delimiter.offset]);
|
|
613
|
-
at = delimiter.offset + delimiter.length;
|
|
614
|
-
}
|
|
615
|
-
if (at < to) out.push([at, to]);
|
|
616
|
-
}
|
|
617
|
-
return out;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
553
|
/** A line split into its indentation, its text, a trailing break and its trailing space. */
|
|
621
554
|
const LINE_PARTS = /^(\s*)(.*?)(\\?)(\s*)$/;
|
|
622
555
|
|
|
@@ -630,7 +563,7 @@ const LINE_PARTS = /^(\s*)(.*?)(\\?)(\s*)$/;
|
|
|
630
563
|
* @returns {string} The text, safe in a paragraph.
|
|
631
564
|
*/
|
|
632
565
|
function escapeRoman(line, lineStart = true) {
|
|
633
|
-
const escaped = line.replace(/[\\`*_[\]
|
|
566
|
+
const escaped = line.replace(/[\\`*_[\]<>"'!&#~|{}-]/g, "\\$&");
|
|
634
567
|
return lineStart ?
|
|
635
568
|
escaped.replace(/^([-+=:])/, "\\$1").replace(/^(\d+)([.)])/, "$1\\$2")
|
|
636
569
|
: escaped;
|
|
@@ -660,19 +593,47 @@ function unescapeRoman(core) {
|
|
|
660
593
|
return { text, offsets };
|
|
661
594
|
}
|
|
662
595
|
|
|
596
|
+
/** Apply a selected encoding, retaining source text only for recoverable rendering. */
|
|
597
|
+
function applyEncoding(encoding, text, options = {}) {
|
|
598
|
+
if (!encoding)
|
|
599
|
+
return {
|
|
600
|
+
text,
|
|
601
|
+
pieces: [...text].map((char, index) => ({
|
|
602
|
+
from: char,
|
|
603
|
+
to: char,
|
|
604
|
+
at: index,
|
|
605
|
+
row: false,
|
|
606
|
+
})),
|
|
607
|
+
};
|
|
608
|
+
const result = encodeWithLanguage(encoding, text, options);
|
|
609
|
+
return {
|
|
610
|
+
text: result.findings.length ? text : result.text,
|
|
611
|
+
pieces: [
|
|
612
|
+
...result.pieces.map((one) => ({ ...one, at: one.start, row: Boolean(one.row) })),
|
|
613
|
+
...result.findings.map((one) => ({
|
|
614
|
+
from: text.slice(one.offset, one.offset + 1),
|
|
615
|
+
to: "",
|
|
616
|
+
at: one.offset,
|
|
617
|
+
row: false,
|
|
618
|
+
problem: one.message,
|
|
619
|
+
})),
|
|
620
|
+
],
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
663
624
|
/**
|
|
664
|
-
* Set a piece of a run's text: an author's escapes removed, the
|
|
625
|
+
* Set a piece of a run's text: an author's escapes removed, the enc applied,
|
|
665
626
|
* and the characters Markdown would read escaped again. Every line is set on
|
|
666
627
|
* its own, so the line count holds.
|
|
667
628
|
*
|
|
668
629
|
* @param {string} raw - The text as written.
|
|
669
|
-
* @param {import("./
|
|
670
|
-
*
|
|
630
|
+
* @param {ReturnType<import("./encoding-lexicon.mjs").encodingFor>|undefined} selection - The run's
|
|
631
|
+
* enc, face and lexicon.
|
|
671
632
|
* @param {boolean} lineStart - Whether `raw` starts its line.
|
|
672
|
-
* @returns {{text: string, pieces: Array<{offset: number, from: string, to: string, row: boolean, native?: boolean,
|
|
633
|
+
* @returns {{text: string, pieces: Array<{offset: number, from: string, to: string, row: boolean, native?: boolean, problem?: string}>}}
|
|
673
634
|
* The set text, and each piece of it with the offset in `raw` it came from.
|
|
674
635
|
*/
|
|
675
|
-
function setText(raw,
|
|
636
|
+
function setText(raw, selection, lineStart) {
|
|
676
637
|
const out = [];
|
|
677
638
|
const pieces = [];
|
|
678
639
|
let base = 0;
|
|
@@ -683,26 +644,22 @@ function setText(raw, mapping, lineStart) {
|
|
|
683
644
|
if (core === "") out.push(line);
|
|
684
645
|
else {
|
|
685
646
|
const plain = unescapeRoman(core);
|
|
686
|
-
const
|
|
687
|
-
font:
|
|
688
|
-
|
|
689
|
-
writing: mapping?.writing,
|
|
647
|
+
const encoded = applyEncoding(selection?.encoding, plain.text, {
|
|
648
|
+
font: selection?.font,
|
|
649
|
+
lexicon: selection?.lexicon,
|
|
690
650
|
});
|
|
691
|
-
for (const piece of
|
|
651
|
+
for (const piece of encoded.pieces)
|
|
692
652
|
pieces.push({
|
|
693
653
|
offset: base + lead.length + (plain.offsets[piece.at] ?? 0),
|
|
694
654
|
from: piece.from,
|
|
695
655
|
to: piece.to,
|
|
696
656
|
row: piece.row,
|
|
697
657
|
...(piece.native ? { native: true } : {}),
|
|
698
|
-
...(piece.
|
|
699
|
-
{}
|
|
700
|
-
: { uncapitalised: piece.uncapitalised }),
|
|
701
|
-
...(piece.unwritten === undefined ? {} : { unwritten: piece.unwritten }),
|
|
658
|
+
...(piece.problem === undefined ? {} : { problem: piece.problem }),
|
|
702
659
|
});
|
|
703
660
|
out.push(
|
|
704
661
|
lead +
|
|
705
|
-
escapeRoman(
|
|
662
|
+
escapeRoman(encoded.text, index > 0 || lineStart || lead !== "") +
|
|
706
663
|
breakMark +
|
|
707
664
|
trail,
|
|
708
665
|
);
|
|
@@ -714,21 +671,20 @@ function setText(raw, mapping, lineStart) {
|
|
|
714
671
|
|
|
715
672
|
/**
|
|
716
673
|
* Set a stretch of a scroll fence's text: the author's escapes removed and the
|
|
717
|
-
*
|
|
674
|
+
* enc applied to all of it, its spaces included, and nothing escaped, since
|
|
718
675
|
* the fence's body is raw.
|
|
719
676
|
*
|
|
720
677
|
* @param {string} raw - The text, on one line.
|
|
721
|
-
* @param {import("./
|
|
722
|
-
*
|
|
678
|
+
* @param {ReturnType<import("./encoding-lexicon.mjs").encodingFor>|undefined} selection - The run's
|
|
679
|
+
* enc, face and lexicon.
|
|
723
680
|
* @returns {{text: string, pieces: Array<{from: string, to: string}>}} The set
|
|
724
681
|
* text, and each piece of it.
|
|
725
682
|
*/
|
|
726
|
-
function setScrollText(raw,
|
|
683
|
+
function setScrollText(raw, selection) {
|
|
727
684
|
const plain = unescapeRoman(raw);
|
|
728
|
-
return
|
|
729
|
-
font:
|
|
730
|
-
|
|
731
|
-
writing: mapping?.writing,
|
|
685
|
+
return applyEncoding(selection?.encoding, plain.text, {
|
|
686
|
+
font: selection?.font,
|
|
687
|
+
lexicon: selection?.lexicon,
|
|
732
688
|
});
|
|
733
689
|
}
|
|
734
690
|
|
|
@@ -807,8 +763,8 @@ const BLOCK_WHAT = Object.freeze({
|
|
|
807
763
|
* @property {boolean} plain - Whether its text is set.
|
|
808
764
|
* @property {import("./script-fonts.mjs").Face|undefined} face - Its face.
|
|
809
765
|
* @property {string|undefined} generic - Its generic family's class.
|
|
810
|
-
* @property {import("./
|
|
811
|
-
* @property {import("./
|
|
766
|
+
* @property {ReturnType<import("./encoding.mjs").compileEncoding>|undefined} encoding - Its enc.
|
|
767
|
+
* @property {ReturnType<import("./encoding-lexicon.mjs").encodingFor>|undefined} selection - Its enc,
|
|
812
768
|
* face and lexicon, which its text is set by.
|
|
813
769
|
* @property {Map<number, number>} holes - The marked spans inside it.
|
|
814
770
|
* @property {[number, number]|undefined} body - A div's or fence's body, as
|
|
@@ -822,6 +778,10 @@ const BLOCK_WHAT = Object.freeze({
|
|
|
822
778
|
* What its emphasis is set by: its language's convention, else its face's
|
|
823
779
|
* script's; `undefined` sets it plain.
|
|
824
780
|
* @property {Delimiter[]} emphasis - The emphasis it holds, by absolute offset.
|
|
781
|
+
* @property {boolean} [checkRoman] - Its inline source structure permits Roman
|
|
782
|
+
* inventory validation, independently of encoding and face resolution.
|
|
783
|
+
* @property {boolean} [checkText] - Its selection and source structure permit
|
|
784
|
+
* native encoding and output glyph validation.
|
|
825
785
|
* @property {boolean} [unrolled] - A scroll fence whose opening has a finding,
|
|
826
786
|
* which is left as written.
|
|
827
787
|
*/
|
|
@@ -847,10 +807,10 @@ function wordsMessage(lang) {
|
|
|
847
807
|
* @returns {import("./script-emphasis.mjs").Emphasis|undefined} The conventions.
|
|
848
808
|
*/
|
|
849
809
|
function conventionsOf({ face, languageEmphasis }, ctx) {
|
|
850
|
-
if (
|
|
810
|
+
if (languageEmphasis === "words") return undefined;
|
|
851
811
|
if (languageEmphasis === "rubric" || languageEmphasis === "overline")
|
|
852
812
|
return { em: languageEmphasis, strong: languageEmphasis };
|
|
853
|
-
return ctx.emphases?.get(face.slug);
|
|
813
|
+
return face ? ctx.emphases?.get(face.slug) : undefined;
|
|
854
814
|
}
|
|
855
815
|
|
|
856
816
|
/**
|
|
@@ -860,7 +820,7 @@ function conventionsOf({ face, languageEmphasis }, ctx) {
|
|
|
860
820
|
* @param {RunContext} ctx - The names.
|
|
861
821
|
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Session
|
|
862
822
|
* scanner projections.
|
|
863
|
-
* @returns {{runs: Run[], details: RunDetail[], errors: Array<{line: number, column: number, message: string}>, warnings: Array<{line: number, column: number, message: string}>, spans: object[], divs: Map<number, object>, poems: Map<number, object>}}
|
|
823
|
+
* @returns {{runs: Run[], details: RunDetail[], errors: Array<{file?: string, line: number, column: number, message: string}>, warnings: Array<{line: number, column: number, message: string}>, spans: object[], divs: Map<number, object>, poems: Map<number, object>}}
|
|
864
824
|
* What {@link scanRuns} returns, and what {@link renderRuns} rewrites by.
|
|
865
825
|
*/
|
|
866
826
|
function analyseRuns(source, ctx, syntax) {
|
|
@@ -869,7 +829,12 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
869
829
|
const starts = lineStarts(text);
|
|
870
830
|
const languages = ctx.languages ?? {};
|
|
871
831
|
const faces = ctx.faces ?? new Map();
|
|
872
|
-
const
|
|
832
|
+
const encodings = ctx.encodings ?? new Map();
|
|
833
|
+
const encodingUnavailable = ctx.encodingFindings?.[0];
|
|
834
|
+
const encodingNamed = (value) =>
|
|
835
|
+
value === "default" ? undefined : (
|
|
836
|
+
(encodings.get(value) ?? encodings.get(encodingAddress(value, ctx.contentPackage)))
|
|
837
|
+
);
|
|
873
838
|
const errors = [];
|
|
874
839
|
const warnings = [];
|
|
875
840
|
const footnotes = new Set(
|
|
@@ -944,7 +909,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
944
909
|
line0: first,
|
|
945
910
|
where: { line: first + 1, column: 1 },
|
|
946
911
|
}),
|
|
947
|
-
|
|
912
|
+
"`.nativefont` is not an accepted class; select an encoding with `enc=`",
|
|
948
913
|
);
|
|
949
914
|
for (const key of Object.keys(keys))
|
|
950
915
|
report(
|
|
@@ -1066,6 +1031,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1066
1031
|
(construct) =>
|
|
1067
1032
|
construct.kind === "scroll" ||
|
|
1068
1033
|
construct.own.classes.includes(NATIVEFONT_CLASS) ||
|
|
1034
|
+
construct.own.classes.some((name) => name === "emph" || name === "strong") ||
|
|
1069
1035
|
Object.keys(construct.keys).length ||
|
|
1070
1036
|
around(construct).some((container) => Object.keys(container.keys).length),
|
|
1071
1037
|
);
|
|
@@ -1077,43 +1043,25 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1077
1043
|
: isLanguageTag(lang) ? lang
|
|
1078
1044
|
: undefined;
|
|
1079
1045
|
|
|
1080
|
-
const native = (construct) =>
|
|
1046
|
+
const native = (construct) =>
|
|
1047
|
+
construct.keys.enc !== undefined && construct.keys.enc !== "default";
|
|
1081
1048
|
|
|
1082
1049
|
const faceNames = () => [...faces.keys(), ...Object.keys(GENERIC_FONTS)];
|
|
1083
1050
|
|
|
1084
1051
|
/** Check the keys a construct states itself. */
|
|
1085
1052
|
const checkOwn = (construct) => {
|
|
1086
|
-
const { lang, font,
|
|
1053
|
+
const { lang, font, enc } = construct.keys;
|
|
1087
1054
|
const { at } = construct;
|
|
1088
|
-
if (
|
|
1055
|
+
if (construct.own.classes.includes(NATIVEFONT_CLASS))
|
|
1089
1056
|
report(
|
|
1090
1057
|
nativeAt(construct),
|
|
1091
|
-
"`.nativefont`
|
|
1058
|
+
"`.nativefont` is not an accepted class; select an encoding with `enc=`",
|
|
1092
1059
|
);
|
|
1093
1060
|
if (native(construct) && construct.kind === "verse" && hasColumns(construct.node))
|
|
1094
1061
|
report(
|
|
1095
|
-
|
|
1096
|
-
"a verse with cols>1
|
|
1062
|
+
construct.at("enc", "value"),
|
|
1063
|
+
"a verse with cols>1 selects encoding on individual runs, not the fence",
|
|
1097
1064
|
);
|
|
1098
|
-
if (!native(construct)) {
|
|
1099
|
-
if (construct.kind === "scroll")
|
|
1100
|
-
report(
|
|
1101
|
-
construct.where,
|
|
1102
|
-
"a scroll requires `.nativefont` and an explicit `lang=` on the same construct",
|
|
1103
|
-
);
|
|
1104
|
-
else {
|
|
1105
|
-
if (font !== undefined && faces.has(font))
|
|
1106
|
-
report(
|
|
1107
|
-
at("font", "value"),
|
|
1108
|
-
`native \`font=${font}\` requires \`.nativefont\` and an explicit \`lang=\` on the same construct`,
|
|
1109
|
-
);
|
|
1110
|
-
if (map !== undefined && maps.has(map))
|
|
1111
|
-
report(
|
|
1112
|
-
at("map", "value"),
|
|
1113
|
-
`\`map=${map}\` requires \`.nativefont\` and an explicit \`lang=\` on the same construct`,
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
1065
|
|
|
1118
1066
|
if (lang !== undefined && !Object.hasOwn(languages, lang)) {
|
|
1119
1067
|
if (isLanguageTag(lang)) {
|
|
@@ -1146,45 +1094,71 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1146
1094
|
(near ? `; did you mean \`${near}\`?` : ""),
|
|
1147
1095
|
);
|
|
1148
1096
|
}
|
|
1149
|
-
if (
|
|
1150
|
-
const near = nearest(
|
|
1097
|
+
if (enc !== undefined && enc !== "default" && !encodingNamed(enc) && !encodingUnavailable) {
|
|
1098
|
+
const near = nearest(enc, [...encodings.keys()]);
|
|
1151
1099
|
report(
|
|
1152
|
-
at("
|
|
1153
|
-
`\`${
|
|
1100
|
+
at("enc", "value"),
|
|
1101
|
+
`\`${enc}\` does not resolve to an encoding document` +
|
|
1154
1102
|
(near ? `; did you mean \`${near}\`?` : ""),
|
|
1155
1103
|
);
|
|
1156
1104
|
}
|
|
1157
1105
|
};
|
|
1158
1106
|
|
|
1159
1107
|
const nativeDefaults = (construct, keys) => {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
if (keys.font === undefined)
|
|
1164
|
-
report(
|
|
1165
|
-
nativeAt(construct),
|
|
1166
|
-
`\`.nativefont\` has no default \`font=\` for \`${keys.lang}\`; state \`font=\` explicitly`,
|
|
1167
|
-
);
|
|
1168
|
-
if (keys.map !== undefined && !maps.has(keys.map))
|
|
1169
|
-
report(
|
|
1170
|
-
nativeAt(construct),
|
|
1171
|
-
`native default \`map=${keys.map}\` is unavailable; state \`map=\` explicitly`,
|
|
1172
|
-
);
|
|
1108
|
+
if (encodingUnavailable) return;
|
|
1109
|
+
const encoding = encodings.get(keys.enc);
|
|
1110
|
+
if (!encoding) return;
|
|
1173
1111
|
if (
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1112
|
+
encoding.language !== undefined &&
|
|
1113
|
+
keys.lang !== undefined &&
|
|
1114
|
+
keys.lang !== encoding.language &&
|
|
1115
|
+
tagOf(keys.lang)?.toLowerCase() !== tagOf(encoding.language)?.toLowerCase()
|
|
1177
1116
|
)
|
|
1178
1117
|
report(
|
|
1179
|
-
|
|
1180
|
-
`
|
|
1118
|
+
construct.at(construct.keys.lang !== undefined ? "lang" : "enc", "value"),
|
|
1119
|
+
`Encoding \`${encoding.name}\` writes language \`${encoding.language}\`, not \`${keys.lang}\``,
|
|
1181
1120
|
);
|
|
1121
|
+
if (!keys.font || (!faces.has(keys.font) && !Object.hasOwn(GENERIC_FONTS, keys.font)))
|
|
1122
|
+
report(construct.at("enc", "value"), `Encoding font \`${keys.font}\` is unavailable`);
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
// A class-only child marks the parent's text. An explicit run key makes
|
|
1126
|
+
// a separate presentation choice, including an explicit language switch.
|
|
1127
|
+
const presentations = new Map();
|
|
1128
|
+
const presentationOf = (construct) => {
|
|
1129
|
+
if (presentations.has(construct)) return presentations.get(construct);
|
|
1130
|
+
const ancestors = around(construct);
|
|
1131
|
+
const inherits = construct.kind === "span" && !Object.keys(construct.keys).length;
|
|
1132
|
+
const parent = inherits ? ancestors.at(-1) : undefined;
|
|
1133
|
+
const inherited = parent ? presentationOf(parent) : undefined;
|
|
1134
|
+
const keys = inherited ? { ...inherited.keys } : {};
|
|
1135
|
+
if (!inherited)
|
|
1136
|
+
for (const ancestor of ancestors) {
|
|
1137
|
+
const inheritedKeys = presentationOf(ancestor).keys;
|
|
1138
|
+
if (inheritedKeys.lang !== undefined) keys.lang = inheritedKeys.lang;
|
|
1139
|
+
if (!native(ancestor) && ancestor.keys.font !== undefined)
|
|
1140
|
+
keys.font = ancestor.keys.font;
|
|
1141
|
+
}
|
|
1142
|
+
if (native(construct)) delete keys.font;
|
|
1143
|
+
Object.assign(keys, construct.keys);
|
|
1144
|
+
keys.enc ??= "default";
|
|
1145
|
+
const selected = encodingNamed(keys.enc);
|
|
1146
|
+
if (selected) {
|
|
1147
|
+
keys.enc = selected.name;
|
|
1148
|
+
keys.lang ??= selected.language;
|
|
1149
|
+
keys.font ??= selected.font ?? selected.fonts[0];
|
|
1150
|
+
} else if (!inherits && keys.font === undefined && ancestors.some(native)) {
|
|
1151
|
+
keys.font = "serif";
|
|
1152
|
+
}
|
|
1153
|
+
const result = { keys };
|
|
1154
|
+
presentations.set(construct, result);
|
|
1155
|
+
return result;
|
|
1182
1156
|
};
|
|
1183
1157
|
|
|
1184
1158
|
const rawReferences = [];
|
|
1185
1159
|
// Raw wikilinks belong to the resolver, not the span rewriter. Validate
|
|
1186
1160
|
// their own control attributes with the same grammar before resolution,
|
|
1187
|
-
// without
|
|
1161
|
+
// without selection a destination or consuming the trailing attributes.
|
|
1188
1162
|
for (const match of matchAllOutsideCode(text, new RegExp(WIKILINK.source, "g"))) {
|
|
1189
1163
|
const close = match[0].indexOf("]]") + 1;
|
|
1190
1164
|
const from = match.index + close + 2;
|
|
@@ -1204,41 +1178,47 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1204
1178
|
};
|
|
1205
1179
|
const unchecked = errors.length;
|
|
1206
1180
|
checkOwn(construct);
|
|
1181
|
+
const keys = presentationOf(construct).keys;
|
|
1207
1182
|
const bar = match[1].indexOf("|");
|
|
1208
1183
|
if (bar >= 0 && errors.length === unchecked) {
|
|
1209
|
-
const ancestors = around(construct);
|
|
1210
|
-
const inherited = ancestors.filter((parent) => parent.keys.lang !== undefined).at(-1)
|
|
1211
|
-
?.keys.lang;
|
|
1212
1184
|
rawReferences.push({
|
|
1213
|
-
lang:
|
|
1185
|
+
lang: keys.lang,
|
|
1186
|
+
font: keys.font,
|
|
1214
1187
|
from: match.index + 2 + bar + 1,
|
|
1215
1188
|
to: match.index + close - 1,
|
|
1216
1189
|
where: construct.where,
|
|
1217
1190
|
construct,
|
|
1218
1191
|
});
|
|
1219
1192
|
}
|
|
1220
|
-
if (errors.length !== unchecked || !
|
|
1221
|
-
|
|
1193
|
+
if (errors.length !== unchecked || keys.enc === "default" || !encodingNamed(keys.enc))
|
|
1194
|
+
continue;
|
|
1222
1195
|
nativeDefaults(construct, keys);
|
|
1223
|
-
if (errors.length !== unchecked || keys.
|
|
1196
|
+
if (errors.length !== unchecked || keys.enc === undefined || keys.font === undefined)
|
|
1224
1197
|
continue;
|
|
1225
|
-
const problem = fontProblem(
|
|
1198
|
+
const problem = fontProblem(encodings.get(keys.enc), keys.font);
|
|
1226
1199
|
if (problem)
|
|
1227
1200
|
report(
|
|
1228
|
-
construct.at(construct.keys.
|
|
1201
|
+
construct.at(construct.keys.font !== undefined ? "font" : "enc", "value"),
|
|
1229
1202
|
problem,
|
|
1230
1203
|
);
|
|
1231
1204
|
}
|
|
1232
1205
|
|
|
1233
1206
|
const verseMarkers = new Map();
|
|
1207
|
+
const verseContexts = new Map();
|
|
1208
|
+
const verseReadings = new Map();
|
|
1234
1209
|
for (const poem of scanVerse(text).blocks) {
|
|
1210
|
+
for (let line = poem.start + 1; line < poem.end; line++) verseContexts.set(line, poem);
|
|
1235
1211
|
for (let line = poem.start + 1; line < poem.end; line++)
|
|
1236
|
-
if (
|
|
1212
|
+
if (
|
|
1213
|
+
STANZA_BREAK.test(lines[line].trim()) ||
|
|
1214
|
+
/^---\s+\{\.canto\}$/.test(lines[line].trim())
|
|
1215
|
+
)
|
|
1237
1216
|
verseMarkers.set(starts[line], starts[line] + lines[line].length);
|
|
1238
1217
|
if (hasColumns(poem)) {
|
|
1239
1218
|
for (const group of verseGroups(poem, lines)) {
|
|
1240
1219
|
if (group.stanzaBreak) continue;
|
|
1241
1220
|
for (const part of group.parts) {
|
|
1221
|
+
verseReadings.set(part.line, part);
|
|
1242
1222
|
const start = starts[part.line];
|
|
1243
1223
|
if (part.prefixLength)
|
|
1244
1224
|
verseMarkers.set(
|
|
@@ -1317,117 +1297,13 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1317
1297
|
}
|
|
1318
1298
|
};
|
|
1319
1299
|
|
|
1320
|
-
/**
|
|
1321
|
-
* Report each character a face does not draw, once per run, at the source
|
|
1322
|
-
* character it came from.
|
|
1323
|
-
*
|
|
1324
|
-
* @param {Array<[number, number]>} parts - The text to check, as offsets.
|
|
1325
|
-
* @param {RunDetail} detail - The run.
|
|
1326
|
-
*/
|
|
1327
|
-
const covered = (parts, detail) => {
|
|
1328
|
-
const { face, textMap, mapping } = detail;
|
|
1329
|
-
if (!face) return;
|
|
1330
|
-
const said = new Set();
|
|
1331
|
-
for (const [from, to] of parts) {
|
|
1332
|
-
const lineStart = from === 0 || text[from - 1] === "\n";
|
|
1333
|
-
for (const piece of setText(text.slice(from, to), mapping, lineStart).pieces) {
|
|
1334
|
-
// A word with no writing is its own finding, not a face's.
|
|
1335
|
-
if (piece.unwritten) continue;
|
|
1336
|
-
for (const char of piece.to) {
|
|
1337
|
-
const point = /** @type {number} */ (char.codePointAt(0));
|
|
1338
|
-
if (/\s/u.test(char) || face.draws.has(point)) continue;
|
|
1339
|
-
const key = piece.row ? `${piece.from}\u0000${point}` : char;
|
|
1340
|
-
if (said.has(key)) continue;
|
|
1341
|
-
said.add(key);
|
|
1342
|
-
const where = positionOf(starts, from + piece.offset);
|
|
1343
|
-
if (piece.native)
|
|
1344
|
-
report(
|
|
1345
|
-
where,
|
|
1346
|
-
`\`${piece.from}\` is written ${codeName(point)} by the lexicon of ` +
|
|
1347
|
-
`\`${detail.run.lang}\` for the map \`${textMap?.name}\`, which the font ` +
|
|
1348
|
-
`\`${face.slug}\` does not draw`,
|
|
1349
|
-
);
|
|
1350
|
-
else if (piece.row)
|
|
1351
|
-
report(
|
|
1352
|
-
where,
|
|
1353
|
-
`\`${piece.from}\` maps to ${codeName(point)} in the map \`${textMap?.name}\`, ` +
|
|
1354
|
-
`which the font \`${face.slug}\` does not draw`,
|
|
1355
|
-
);
|
|
1356
|
-
else
|
|
1357
|
-
report(
|
|
1358
|
-
where,
|
|
1359
|
-
`\`${char}\` (${codeName(point)}) is not drawn by the font \`${face.slug}\`` +
|
|
1360
|
-
(textMap ?
|
|
1361
|
-
`, and the map \`${textMap.name}\` has no row for it`
|
|
1362
|
-
: ""),
|
|
1363
|
-
);
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
};
|
|
1368
|
-
|
|
1369
|
-
/**
|
|
1370
|
-
* Report each capitalised word whose native form the rows cannot
|
|
1371
|
-
* capitalise, at the word.
|
|
1372
|
-
*
|
|
1373
|
-
* @param {Array<[number, number]>} parts - The text to check, as offsets.
|
|
1374
|
-
* @param {RunDetail} detail - The run.
|
|
1375
|
-
*/
|
|
1376
|
-
const capitals = (parts, detail) => {
|
|
1377
|
-
const { mapping } = detail;
|
|
1378
|
-
if (!mapping?.native) return;
|
|
1379
|
-
for (const [from, to] of parts) {
|
|
1380
|
-
const lineStart = from === 0 || text[from - 1] === "\n";
|
|
1381
|
-
for (const piece of setText(text.slice(from, to), mapping, lineStart).pieces)
|
|
1382
|
-
if (piece.uncapitalised !== undefined)
|
|
1383
|
-
report(
|
|
1384
|
-
positionOf(starts, from + piece.offset),
|
|
1385
|
-
uncapitalisedMessage(
|
|
1386
|
-
piece.from,
|
|
1387
|
-
piece.uncapitalised,
|
|
1388
|
-
detail.run.lang,
|
|
1389
|
-
mapping.map.name,
|
|
1390
|
-
),
|
|
1391
|
-
);
|
|
1392
|
-
}
|
|
1393
|
-
};
|
|
1394
|
-
|
|
1395
|
-
/**
|
|
1396
|
-
* Report each word a logographic map gives no writing, once per run, at
|
|
1397
|
-
* the word.
|
|
1398
|
-
*
|
|
1399
|
-
* @param {Array<[number, number]>} parts - The text to check, as offsets.
|
|
1400
|
-
* @param {RunDetail} detail - The run.
|
|
1401
|
-
*/
|
|
1402
|
-
const writings = (parts, detail) => {
|
|
1403
|
-
const { mapping } = detail;
|
|
1404
|
-
if (!mapping?.map.logographic) return;
|
|
1405
|
-
const said = new Set();
|
|
1406
|
-
for (const [from, to] of parts) {
|
|
1407
|
-
const lineStart = from === 0 || text[from - 1] === "\n";
|
|
1408
|
-
for (const piece of setText(text.slice(from, to), mapping, lineStart).pieces) {
|
|
1409
|
-
if (piece.unwritten === undefined || said.has(piece.from)) continue;
|
|
1410
|
-
said.add(piece.from);
|
|
1411
|
-
report(
|
|
1412
|
-
positionOf(starts, from + piece.offset),
|
|
1413
|
-
unwrittenMessage(
|
|
1414
|
-
piece.from,
|
|
1415
|
-
piece.unwritten,
|
|
1416
|
-
detail.run.lang,
|
|
1417
|
-
mapping.map.name,
|
|
1418
|
-
),
|
|
1419
|
-
);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
};
|
|
1423
|
-
|
|
1424
1300
|
/** The configured source inventory for either an alias or its BCP 47 tag. */
|
|
1425
1301
|
const inventoryOf = (lang) =>
|
|
1426
1302
|
lang === undefined ? undefined : (
|
|
1427
1303
|
(ctx.romanInventories?.get(lang) ?? ctx.romanInventories?.get(lang.toLowerCase()))
|
|
1428
1304
|
);
|
|
1429
1305
|
|
|
1430
|
-
/** Check visible source before
|
|
1306
|
+
/** Check visible source before selection or lexicon-native replacement. */
|
|
1431
1307
|
const roman = (parts, detail) => {
|
|
1432
1308
|
const inventory = inventoryOf(detail.run.lang);
|
|
1433
1309
|
if (!inventory) return;
|
|
@@ -1439,60 +1315,16 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1439
1315
|
return;
|
|
1440
1316
|
}
|
|
1441
1317
|
for (const [from, to] of parts) {
|
|
1442
|
-
|
|
1443
|
-
for (const
|
|
1444
|
-
|
|
1445
|
-
const breakMark =
|
|
1446
|
-
/(?<!\\)(?:\\\\)*\\$/.test(line) && text[base + line.length] === "\n";
|
|
1447
|
-
const plain = unescapeRoman(breakMark ? line.slice(0, -1) : line);
|
|
1448
|
-
for (const problem of romanTextProblems(plain.text, inventory, (char) => {
|
|
1449
|
-
const point = char.codePointAt(0);
|
|
1450
|
-
return (
|
|
1451
|
-
detail.run.nativefont === true &&
|
|
1452
|
-
isSettingScriptCodePoint(point) &&
|
|
1453
|
-
detail.face?.draws.has(point)
|
|
1454
|
-
);
|
|
1455
|
-
}))
|
|
1456
|
-
report(
|
|
1457
|
-
positionOf(starts, base + (plain.offsets[problem.offset] ?? 0)),
|
|
1458
|
-
problem.message,
|
|
1459
|
-
);
|
|
1460
|
-
base += line.length + 1;
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
};
|
|
1464
|
-
|
|
1465
|
-
/**
|
|
1466
|
-
* Report each word of a mapped run that is not a word of its language, once
|
|
1467
|
-
* per run, at its first character.
|
|
1468
|
-
*
|
|
1469
|
-
* @param {Array<[number, number]>} parts - The text to check, as offsets.
|
|
1470
|
-
* @param {RunDetail} detail - The run.
|
|
1471
|
-
*/
|
|
1472
|
-
const lexical = (parts, detail) => {
|
|
1473
|
-
const { lang } = detail.run;
|
|
1474
|
-
if (lang === undefined) return;
|
|
1475
|
-
const book = ctx.wordBooks?.get(lang);
|
|
1476
|
-
if (!book) return;
|
|
1477
|
-
const said = new Set();
|
|
1478
|
-
for (const [from, to] of parts) {
|
|
1479
|
-
let base = from;
|
|
1480
|
-
for (const line of text.slice(from, to).split("\n")) {
|
|
1481
|
-
const plain = unescapeRoman(line);
|
|
1482
|
-
for (const { offset, word } of unknownWords(plain.text, book, said))
|
|
1483
|
-
report(
|
|
1484
|
-
positionOf(starts, base + (plain.offsets[offset] ?? 0)),
|
|
1485
|
-
unknownWordMessage(word, lang),
|
|
1486
|
-
);
|
|
1487
|
-
base += line.length + 1;
|
|
1488
|
-
}
|
|
1318
|
+
const plain = plainRomanRun(text, { from, to });
|
|
1319
|
+
for (const problem of romanTextProblems(plain.text, inventory))
|
|
1320
|
+
report(positionOf(starts, plain.offsets[problem.offset] ?? from), problem.message);
|
|
1489
1321
|
}
|
|
1490
1322
|
};
|
|
1491
1323
|
|
|
1492
1324
|
/**
|
|
1493
1325
|
* Give a scroll fence the face it is set in, and report a fence that names
|
|
1494
1326
|
* none or whose script is written in lines. A fence that names no face
|
|
1495
|
-
* takes its
|
|
1327
|
+
* takes its enc's, where the enc is drawn in one.
|
|
1496
1328
|
*
|
|
1497
1329
|
* @param {object} construct - The scroll fence.
|
|
1498
1330
|
* @param {Record<string, string>} keys - Its run keys, given the face.
|
|
@@ -1503,7 +1335,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1503
1335
|
if (ctx.directions.get(keys.font) === "vertical-rl") return;
|
|
1504
1336
|
report(
|
|
1505
1337
|
construct.keys.font !== undefined ? construct.at("font", "value")
|
|
1506
|
-
: construct.keys.
|
|
1338
|
+
: construct.keys.enc !== undefined ? construct.at("enc", "value")
|
|
1507
1339
|
: construct.keys.lang !== undefined ? construct.at("lang", "value")
|
|
1508
1340
|
: construct.where,
|
|
1509
1341
|
`a \`scroll\` fence sets a script written in columns, and the script of \`${keys.font}\` ` +
|
|
@@ -1516,69 +1348,52 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1516
1348
|
for (const construct of active) {
|
|
1517
1349
|
const unchecked = errors.length;
|
|
1518
1350
|
checkOwn(construct);
|
|
1519
|
-
// A key that names nothing is the finding; the text it would have set
|
|
1520
|
-
// is not then checked against a face or a language it cannot reach.
|
|
1521
1351
|
let named = errors.length === unchecked;
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
// children, so a parallel Roman run never takes a native neighbour's face.
|
|
1525
|
-
const ancestors = around(construct);
|
|
1526
|
-
const keys = {};
|
|
1527
|
-
for (const ancestor of ancestors) {
|
|
1528
|
-
if (ancestor.keys.lang !== undefined) keys.lang = ancestor.keys.lang;
|
|
1529
|
-
if (!native(ancestor) && Object.hasOwn(GENERIC_FONTS, ancestor.keys.font ?? ""))
|
|
1530
|
-
keys.font = ancestor.keys.font;
|
|
1531
|
-
}
|
|
1532
|
-
if (native(construct)) delete keys.font;
|
|
1533
|
-
Object.assign(keys, construct.keys);
|
|
1534
|
-
const columnFence = construct.kind === "verse" && hasColumns(construct.node);
|
|
1535
|
-
if (named && native(construct) && !columnFence) {
|
|
1352
|
+
const { keys } = presentationOf(construct);
|
|
1353
|
+
if (named && native(construct)) {
|
|
1536
1354
|
nativeDefaults(construct, keys);
|
|
1537
1355
|
named = errors.length === unchecked;
|
|
1538
|
-
} else if (!native(construct) && keys.font === undefined && ancestors.some(native)) {
|
|
1539
|
-
// Reset CSS/Typst inheritance from a surrounding native face.
|
|
1540
|
-
keys.font = "serif";
|
|
1541
1356
|
}
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
named = errors.length === unchecked;
|
|
1545
|
-
}
|
|
1546
|
-
const { lang, font, map } = keys;
|
|
1547
|
-
// A face the run's map is not for, once the face and the map are each
|
|
1357
|
+
const { lang, font, enc } = keys;
|
|
1358
|
+
// A face the run's enc is not for, once the face and the enc are each
|
|
1548
1359
|
// known, at the construct that states either.
|
|
1549
1360
|
const stated =
|
|
1550
1361
|
native(construct) ||
|
|
1551
|
-
construct.keys.
|
|
1362
|
+
construct.keys.enc !== undefined ||
|
|
1552
1363
|
construct.keys.font !== undefined;
|
|
1553
1364
|
if (
|
|
1554
1365
|
named &&
|
|
1555
1366
|
stated &&
|
|
1556
|
-
|
|
1557
|
-
|
|
1367
|
+
enc !== undefined &&
|
|
1368
|
+
encodings.has(enc) &&
|
|
1558
1369
|
font !== undefined &&
|
|
1559
1370
|
(faces.has(font) || Object.hasOwn(GENERIC_FONTS, font))
|
|
1560
1371
|
) {
|
|
1561
|
-
const problem = fontProblem(
|
|
1372
|
+
const problem = fontProblem(encodings.get(enc), font);
|
|
1562
1373
|
if (problem)
|
|
1563
1374
|
report(
|
|
1564
|
-
construct.at(construct.keys.
|
|
1375
|
+
construct.at(construct.keys.font !== undefined ? "font" : "enc", "value"),
|
|
1565
1376
|
problem,
|
|
1566
1377
|
);
|
|
1567
1378
|
}
|
|
1379
|
+
named = errors.length === unchecked;
|
|
1380
|
+
if (construct.kind === "scroll" && named) {
|
|
1381
|
+
scrollFace(construct, keys);
|
|
1382
|
+
named = errors.length === unchecked;
|
|
1383
|
+
}
|
|
1568
1384
|
const run = {
|
|
1569
1385
|
kind: construct.kind,
|
|
1570
1386
|
lang,
|
|
1571
1387
|
tag: tagOf(lang),
|
|
1572
1388
|
font,
|
|
1573
|
-
|
|
1574
|
-
...(native(construct) && construct.keys.lang !== undefined ? { nativefont: true } : {}),
|
|
1389
|
+
enc,
|
|
1575
1390
|
...construct.where,
|
|
1576
1391
|
};
|
|
1577
1392
|
runs.push(run);
|
|
1578
1393
|
const plain =
|
|
1579
1394
|
inventoryOf(lang) !== undefined ||
|
|
1580
1395
|
font !== undefined ||
|
|
1581
|
-
|
|
1396
|
+
(enc !== undefined && enc !== "default") ||
|
|
1582
1397
|
(lang !== undefined &&
|
|
1583
1398
|
Object.hasOwn(languages, lang) &&
|
|
1584
1399
|
!/^en(?:-|$)/i.test(run.tag ?? ""));
|
|
@@ -1589,8 +1404,8 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1589
1404
|
plain,
|
|
1590
1405
|
face: font === undefined ? undefined : faces.get(font),
|
|
1591
1406
|
generic: font === undefined ? undefined : GENERIC_FONTS[font],
|
|
1592
|
-
|
|
1593
|
-
|
|
1407
|
+
encoding: enc === undefined ? undefined : encodings.get(enc),
|
|
1408
|
+
selection: enc === undefined ? undefined : encodingFor(ctx, { enc, font, lang }),
|
|
1594
1409
|
holes: new Map(),
|
|
1595
1410
|
body: undefined,
|
|
1596
1411
|
contentEnd: construct.kind === "span" ? construct.node.contentEnd : undefined,
|
|
@@ -1604,11 +1419,23 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1604
1419
|
details.push(detail);
|
|
1605
1420
|
const nonEnglishSpan =
|
|
1606
1421
|
construct.kind === "span" && run.tag !== undefined && !/^en(?:-|$)/i.test(run.tag);
|
|
1422
|
+
const emphasis = construct.own.classes.filter(
|
|
1423
|
+
(name) => name === "emph" || name === "strong",
|
|
1424
|
+
);
|
|
1425
|
+
if (emphasis.length > 1)
|
|
1426
|
+
report(construct.where, "a span takes `.emph` or `.strong`, not both");
|
|
1427
|
+
else if (emphasis[0] === "strong" && nonEnglishSpan)
|
|
1428
|
+
report(
|
|
1429
|
+
construct.where,
|
|
1430
|
+
`a span in non-English language \`${lang}\` holds no \`.strong\` emphasis`,
|
|
1431
|
+
);
|
|
1432
|
+
else if (emphasis[0] === "emph" && detail.languageEmphasis === "words")
|
|
1433
|
+
report(construct.where, wordsMessage(lang));
|
|
1607
1434
|
if (!plain && !nonEnglishSpan) continue;
|
|
1608
1435
|
const marking = [
|
|
1609
1436
|
...(lang === undefined ? [] : [`lang=${lang}`]),
|
|
1610
1437
|
...(font === undefined ? [] : [`font=${font}`]),
|
|
1611
|
-
...(
|
|
1438
|
+
...(enc === undefined ? [] : [`enc=${enc}`]),
|
|
1612
1439
|
].join(" ");
|
|
1613
1440
|
const before = errors.length;
|
|
1614
1441
|
|
|
@@ -1639,19 +1466,8 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1639
1466
|
}
|
|
1640
1467
|
if (plain && errors.length === before)
|
|
1641
1468
|
plainInline(inline, from, marking, detail, detail.holes);
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
outsideHoles(from, to, detail.holes),
|
|
1645
|
-
detail.emphasis,
|
|
1646
|
-
);
|
|
1647
|
-
roman(parts, detail);
|
|
1648
|
-
if (named) {
|
|
1649
|
-
lexical(parts, detail);
|
|
1650
|
-
capitals(parts, detail);
|
|
1651
|
-
writings(parts, detail);
|
|
1652
|
-
covered(parts, detail);
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1469
|
+
detail.checkRoman = errors.length === before;
|
|
1470
|
+
if (named && detail.checkRoman) detail.checkText = true;
|
|
1655
1471
|
continue;
|
|
1656
1472
|
}
|
|
1657
1473
|
|
|
@@ -1712,30 +1528,263 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1712
1528
|
for (let line = first; line < last; line++)
|
|
1713
1529
|
plainInline(lines[line], starts[line], marking, detail, detail.holes);
|
|
1714
1530
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1531
|
+
detail.checkRoman = errors.length === before;
|
|
1532
|
+
if (named && detail.checkRoman) detail.checkText = true;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const boundsOf = (detail) =>
|
|
1536
|
+
detail.reference ??
|
|
1537
|
+
(detail.run.kind === "span" ? [detail.run.start + 1, detail.contentEnd] : detail.body);
|
|
1538
|
+
const parentOf = (detail) => {
|
|
1539
|
+
if (detail.run.kind !== "span" || Object.keys(detail.keys).length) return undefined;
|
|
1540
|
+
return details
|
|
1541
|
+
.filter(
|
|
1542
|
+
(parent) =>
|
|
1543
|
+
parent !== detail &&
|
|
1544
|
+
boundsOf(parent) &&
|
|
1545
|
+
boundsOf(parent)[0] <= detail.run.start &&
|
|
1546
|
+
boundsOf(parent)[1] >= detail.run.end,
|
|
1547
|
+
)
|
|
1548
|
+
.sort((a, b) => boundsOf(a)[1] - boundsOf(a)[0] - (boundsOf(b)[1] - boundsOf(b)[0]))[0];
|
|
1549
|
+
};
|
|
1550
|
+
const ownerOf = (detail) => {
|
|
1551
|
+
const parent = parentOf(detail);
|
|
1552
|
+
return parent ? ownerOf(parent) : detail;
|
|
1553
|
+
};
|
|
1554
|
+
const wholeVerseReading = (detail) => {
|
|
1555
|
+
if (detail.run.kind === "verse") return true;
|
|
1556
|
+
if (detail.run.kind !== "span") return false;
|
|
1557
|
+
const line = detail.run.line - 1;
|
|
1558
|
+
const poem = verseContexts.get(line);
|
|
1559
|
+
if (!poem) return false;
|
|
1560
|
+
const end = starts[line] + lines[line].length;
|
|
1561
|
+
if (detail.run.end > end) return false;
|
|
1562
|
+
const reading = verseReadings.get(line);
|
|
1563
|
+
const from =
|
|
1564
|
+
hasColumns(poem) ? starts[line] + (reading?.textColumn ?? 1) - 1 : starts[line];
|
|
1565
|
+
return (
|
|
1566
|
+
from <= detail.run.start &&
|
|
1567
|
+
/^[ \t.]*$/.test(text.slice(from, detail.run.start)) &&
|
|
1568
|
+
/^[ \t\r]*$/.test(text.slice(detail.run.end, end))
|
|
1569
|
+
);
|
|
1570
|
+
};
|
|
1571
|
+
const spanDetails = new Map(
|
|
1572
|
+
details
|
|
1573
|
+
.filter((detail) => detail.run.kind === "span")
|
|
1574
|
+
.map((detail) => [detail.run.start, detail]),
|
|
1575
|
+
);
|
|
1576
|
+
for (const owner of details.filter((detail) => ownerOf(detail) === detail)) {
|
|
1577
|
+
const bounds = boundsOf(owner);
|
|
1578
|
+
if (!bounds || !owner.checkRoman) continue;
|
|
1579
|
+
const owned = details.filter((detail) => ownerOf(detail) === owner);
|
|
1580
|
+
for (const detail of owned) detail.owner = owner;
|
|
1581
|
+
const excluded = [
|
|
1582
|
+
...[...verseMarkers].filter(([start]) => start >= bounds[0] && start < bounds[1]),
|
|
1583
|
+
...owned.flatMap((detail) =>
|
|
1584
|
+
detail.emphasis.map((delimiter) => [
|
|
1585
|
+
delimiter.offset,
|
|
1586
|
+
delimiter.offset + delimiter.length,
|
|
1587
|
+
]),
|
|
1588
|
+
),
|
|
1589
|
+
];
|
|
1590
|
+
const projectionOptions = {
|
|
1591
|
+
from: bounds[0],
|
|
1592
|
+
to: bounds[1],
|
|
1593
|
+
spans,
|
|
1594
|
+
exclude: excluded,
|
|
1595
|
+
includeSpan: (span) =>
|
|
1596
|
+
spanDetails.has(span.start) && ownerOf(spanDetails.get(span.start)) === owner,
|
|
1597
|
+
};
|
|
1598
|
+
const verse = wholeVerseReading(owner);
|
|
1599
|
+
const projection =
|
|
1600
|
+
verse ?
|
|
1601
|
+
plainRomanVerse(text, {
|
|
1602
|
+
...projectionOptions,
|
|
1603
|
+
isCantoLine: (line) =>
|
|
1604
|
+
STANZA_BREAK.test(line.trim()) || /^---\s+\{\.canto\}$/.test(line.trim()),
|
|
1605
|
+
})
|
|
1606
|
+
: plainRomanRun(text, projectionOptions);
|
|
1607
|
+
const before = errors.length;
|
|
1608
|
+
const inventory = inventoryOf(owner.run.lang);
|
|
1609
|
+
if (inventory && !inventory.characters)
|
|
1610
|
+
report(
|
|
1611
|
+
{ line: owner.run.line, column: owner.run.column },
|
|
1612
|
+
`the language note for \`${inventory.language}\` must declare \`data.roman\``,
|
|
1613
|
+
);
|
|
1614
|
+
else if (inventory)
|
|
1615
|
+
for (const problem of romanTextProblems(projection.text, inventory))
|
|
1616
|
+
report(
|
|
1617
|
+
positionOf(starts, projection.offsets[problem.offset] ?? bounds[0]),
|
|
1618
|
+
problem.message,
|
|
1619
|
+
);
|
|
1620
|
+
for (const problem of projection.errors ?? [])
|
|
1621
|
+
report(positionOf(starts, problem.offset), problem.message);
|
|
1622
|
+
if (!verse && verseContexts.has(owner.run.line - 1))
|
|
1623
|
+
for (const match of projection.text.matchAll(/\//g)) {
|
|
1624
|
+
if (
|
|
1625
|
+
projection.text.lastIndexOf("⟦", match.index) >
|
|
1626
|
+
projection.text.lastIndexOf("⟧", match.index)
|
|
1627
|
+
)
|
|
1628
|
+
continue;
|
|
1629
|
+
report(
|
|
1630
|
+
positionOf(starts, projection.offsets[match.index]),
|
|
1631
|
+
"a verse fence uses authored line endings for verse boundaries; write no literal `/` inside it",
|
|
1632
|
+
);
|
|
1723
1633
|
}
|
|
1634
|
+
const book = ctx.wordBooks?.get(owner.run.lang);
|
|
1635
|
+
if (book) {
|
|
1636
|
+
const mask = projection.text.split("");
|
|
1637
|
+
for (const match of projection.text.matchAll(/⟦[^]*?(?:⟧|$)/g))
|
|
1638
|
+
mask.fill(" ", match.index, match.index + match[0].length);
|
|
1639
|
+
for (const span of projection.spans.filter((span) => !span.included))
|
|
1640
|
+
mask.fill(" ", span.from, span.to);
|
|
1641
|
+
for (const problem of unknownWords(mask.join(""), book))
|
|
1642
|
+
report(
|
|
1643
|
+
positionOf(starts, projection.offsets[problem.offset] ?? bounds[0]),
|
|
1644
|
+
unknownWordMessage(problem.word, owner.run.lang),
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
// Roman spelling and structure belong to the authored source even
|
|
1648
|
+
// when its selected encoding or face cannot be resolved. Only the
|
|
1649
|
+
// native conversion and glyph checks depend on that selection.
|
|
1650
|
+
if (!owner.checkText) continue;
|
|
1651
|
+
if (!owner.selection) {
|
|
1652
|
+
const marker = projection.text.indexOf("⟦");
|
|
1653
|
+
if (marker >= 0 && !encodingUnavailable)
|
|
1654
|
+
report(
|
|
1655
|
+
positionOf(starts, projection.offsets[marker]),
|
|
1656
|
+
"literal codage names no encoding; state `enc=`",
|
|
1657
|
+
);
|
|
1658
|
+
continue;
|
|
1724
1659
|
}
|
|
1660
|
+
if (errors.length !== before) continue;
|
|
1661
|
+
const result = encodeWithLanguage(owner.selection.encoding, projection.text, {
|
|
1662
|
+
font: owner.selection.font,
|
|
1663
|
+
lexicon: owner.selection.lexicon,
|
|
1664
|
+
});
|
|
1665
|
+
const opaque = projection.spans.filter((span) => !span.included);
|
|
1666
|
+
const braces = new Map(
|
|
1667
|
+
projection.spans
|
|
1668
|
+
.filter((span) => span.included && span.name)
|
|
1669
|
+
.flatMap((span) => [
|
|
1670
|
+
[span.from, { anchor: span.source.start + 1 }],
|
|
1671
|
+
[span.to - 1, { anchor: span.source.contentEnd, closing: true }],
|
|
1672
|
+
]),
|
|
1673
|
+
);
|
|
1674
|
+
const pieces = result.pieces
|
|
1675
|
+
.filter(
|
|
1676
|
+
(piece) => !opaque.some((span) => piece.start >= span.from && piece.end <= span.to),
|
|
1677
|
+
)
|
|
1678
|
+
.filter((piece) => !verse || !/[\r\n]/.test(piece.from) || piece.from.startsWith("⟦"))
|
|
1679
|
+
.map((piece) => {
|
|
1680
|
+
const brace = braces.get(piece.start);
|
|
1681
|
+
return {
|
|
1682
|
+
...piece,
|
|
1683
|
+
offset: projection.offsets[piece.start] ?? bounds[0],
|
|
1684
|
+
anchor: brace?.anchor ?? projection.offsets[piece.start] ?? bounds[0],
|
|
1685
|
+
closing: brace?.closing ?? false,
|
|
1686
|
+
};
|
|
1687
|
+
});
|
|
1688
|
+
for (const finding of result.findings) {
|
|
1689
|
+
if (!opaque.some((span) => finding.offset >= span.from && finding.offset < span.to))
|
|
1690
|
+
report(
|
|
1691
|
+
positionOf(starts, projection.offsets[finding.offset] ?? bounds[0]),
|
|
1692
|
+
finding.message,
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
if (verse) {
|
|
1696
|
+
for (const boundary of projection.boundaries) {
|
|
1697
|
+
const target =
|
|
1698
|
+
boundary.kind === "line" ?
|
|
1699
|
+
encodeText(owner.selection.encoding, "/", { font: owner.selection.font })
|
|
1700
|
+
: owner.selection.encoding.controls[
|
|
1701
|
+
boundary.kind === "stanza" ? "verseStanza" : "verseCanto"
|
|
1702
|
+
];
|
|
1703
|
+
if (target === undefined) continue;
|
|
1704
|
+
pieces.push({
|
|
1705
|
+
from: "",
|
|
1706
|
+
to: target,
|
|
1707
|
+
start: boundary.at,
|
|
1708
|
+
end: boundary.at,
|
|
1709
|
+
offset: boundary.sourceOffset,
|
|
1710
|
+
anchor: boundary.sourceOffset,
|
|
1711
|
+
boundary: boundary.kind,
|
|
1712
|
+
closing: true,
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
pieces.sort((a, b) => a.anchor - b.anchor || a.start - b.start);
|
|
1717
|
+
const said = new Set();
|
|
1718
|
+
if (owner.face)
|
|
1719
|
+
for (const piece of pieces)
|
|
1720
|
+
for (const char of piece.to) {
|
|
1721
|
+
const point = char.codePointAt(0);
|
|
1722
|
+
if (
|
|
1723
|
+
/\s/u.test(char) ||
|
|
1724
|
+
/\p{Default_Ignorable_Code_Point}/u.test(char) ||
|
|
1725
|
+
owner.face.draws.has(point) ||
|
|
1726
|
+
said.has(point)
|
|
1727
|
+
)
|
|
1728
|
+
continue;
|
|
1729
|
+
said.add(point);
|
|
1730
|
+
report(
|
|
1731
|
+
positionOf(starts, piece.offset),
|
|
1732
|
+
`\`${piece.from}\` encodes to ${codeName(point)} in \`${owner.selection.encoding.name}\`, which the font \`${owner.face.slug}\` does not draw`,
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
const plan = { owner, bounds, projection, pieces, verse, emitted: new Set() };
|
|
1736
|
+
for (const detail of owned) detail.plan = plan;
|
|
1725
1737
|
}
|
|
1726
1738
|
|
|
1727
1739
|
for (const reference of rawReferences) {
|
|
1728
|
-
const font = reference.
|
|
1740
|
+
const font = reference.font;
|
|
1729
1741
|
roman([[reference.from, reference.to]], {
|
|
1730
1742
|
run: {
|
|
1731
1743
|
lang: reference.lang,
|
|
1732
|
-
|
|
1744
|
+
|
|
1733
1745
|
...reference.where,
|
|
1734
1746
|
},
|
|
1735
1747
|
face: font === undefined ? undefined : faces.get(font),
|
|
1736
1748
|
});
|
|
1737
1749
|
}
|
|
1738
1750
|
|
|
1751
|
+
const firstEncodingRun =
|
|
1752
|
+
active.find(native) ??
|
|
1753
|
+
rawReferences.find((reference) => native(reference.construct))?.construct;
|
|
1754
|
+
if (encodingUnavailable && firstEncodingRun) {
|
|
1755
|
+
if (encodingUnavailable.file)
|
|
1756
|
+
errors.push({
|
|
1757
|
+
file: encodingUnavailable.file,
|
|
1758
|
+
line: encodingUnavailable.line ?? 1,
|
|
1759
|
+
column: encodingUnavailable.column ?? 1,
|
|
1760
|
+
message: encodingUnavailable.message,
|
|
1761
|
+
});
|
|
1762
|
+
else report(nativeAt(firstEncodingRun), encodingUnavailable.message);
|
|
1763
|
+
}
|
|
1764
|
+
for (const match of matchAllOutsideCode(text, /⟦/g)) {
|
|
1765
|
+
if (encodingUnavailable && firstEncodingRun) continue;
|
|
1766
|
+
if (
|
|
1767
|
+
details.some(
|
|
1768
|
+
(detail) =>
|
|
1769
|
+
detail.selection &&
|
|
1770
|
+
boundsOf(detail) &&
|
|
1771
|
+
boundsOf(detail)[0] <= match.index &&
|
|
1772
|
+
match.index < boundsOf(detail)[1],
|
|
1773
|
+
)
|
|
1774
|
+
)
|
|
1775
|
+
continue;
|
|
1776
|
+
const where = positionOf(starts, match.index);
|
|
1777
|
+
if (
|
|
1778
|
+
!errors.some(
|
|
1779
|
+
(error) =>
|
|
1780
|
+
error.line === where.line &&
|
|
1781
|
+
error.column === where.column &&
|
|
1782
|
+
error.message.includes("literal codage"),
|
|
1783
|
+
)
|
|
1784
|
+
)
|
|
1785
|
+
report(where, "literal codage names no encoding; state `enc=`");
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1739
1788
|
const order = (a, b) => a.line - b.line || a.column - b.column;
|
|
1740
1789
|
runs.sort(order);
|
|
1741
1790
|
details.sort((a, b) => order(a.run, b.run));
|
|
@@ -1744,21 +1793,44 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1744
1793
|
return { runs, details, errors, warnings, spans, divs, poems };
|
|
1745
1794
|
}
|
|
1746
1795
|
|
|
1796
|
+
/**
|
|
1797
|
+
* Diagnostic columns count Unicode code points. Parsing, edits and run records
|
|
1798
|
+
* keep their UTF-16 source coordinates; foreign-file loader positions already
|
|
1799
|
+
* use the encoding file's own code-point columns.
|
|
1800
|
+
* @param {string} source - Original authored text.
|
|
1801
|
+
* @param {Array<{file?: string, line: number, column: number, message: string}>} findings - Internal findings.
|
|
1802
|
+
* @returns {Array<{file?: string, line: number, column: number, message: string}>} Public diagnostic positions.
|
|
1803
|
+
*/
|
|
1804
|
+
function diagnosticColumns(source, findings) {
|
|
1805
|
+
const lines = source.split("\n");
|
|
1806
|
+
return findings.map((finding) =>
|
|
1807
|
+
finding.file ? finding : (
|
|
1808
|
+
{
|
|
1809
|
+
...finding,
|
|
1810
|
+
column:
|
|
1811
|
+
[...(lines[finding.line - 1] ?? "").slice(0, Math.max(0, finding.column - 1))]
|
|
1812
|
+
.length + 1,
|
|
1813
|
+
}
|
|
1814
|
+
),
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1747
1818
|
/**
|
|
1748
1819
|
* Find the spans, divs and verse fences marked with `lang=`, `font=` or
|
|
1749
|
-
* `
|
|
1820
|
+
* `enc=`, and every fault in their markup and their text.
|
|
1750
1821
|
*
|
|
1751
1822
|
* @param {string} source - Authored Markdown: a note body, or a whole file
|
|
1752
1823
|
* whose frontmatter is blanked.
|
|
1753
1824
|
* @param {RunContext} [ctx] - The configured languages, the installed faces
|
|
1754
|
-
* and the
|
|
1755
|
-
*
|
|
1825
|
+
* and the encodings. Without them every `lang=` must be a tag, every face and
|
|
1826
|
+
* enc is unknown, and only `mono`, `serif` and `sans` are fonts.
|
|
1756
1827
|
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Session
|
|
1757
1828
|
* scanner projections, so the source's spans and blocks are scanned once.
|
|
1758
|
-
* @returns {{runs: Run[], errors: Array<{line: number, column: number, message: string}>, warnings: Array<{line: number, column: number, message: string}>}}
|
|
1829
|
+
* @returns {{runs: Run[], errors: Array<{file?: string, line: number, column: number, message: string}>, warnings: Array<{line: number, column: number, message: string}>}}
|
|
1759
1830
|
* The runs in source order, and each finding at its 1-based position. A
|
|
1760
1831
|
* warning is a `lang=` read as a tag that sits within one edit of a
|
|
1761
|
-
* declared name.
|
|
1832
|
+
* declared name. Diagnostic columns count Unicode code points; each run's
|
|
1833
|
+
* source offsets and column remain UTF-16 units.
|
|
1762
1834
|
*/
|
|
1763
1835
|
export function scanRuns(source, ctx = {}, syntax) {
|
|
1764
1836
|
const text = String(source ?? "");
|
|
@@ -1773,9 +1845,9 @@ export function scanRuns(source, ctx = {}, syntax) {
|
|
|
1773
1845
|
const merged = (found, extra) =>
|
|
1774
1846
|
[...found, ...extra].sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1775
1847
|
return {
|
|
1776
|
-
|
|
1777
|
-
errors: merged(authored.errors, metre.errors),
|
|
1778
|
-
warnings: merged(authored.warnings, metre.warnings),
|
|
1848
|
+
runs: authored.runs,
|
|
1849
|
+
errors: diagnosticColumns(text, merged(authored.errors, metre.errors)),
|
|
1850
|
+
warnings: diagnosticColumns(text, merged(authored.warnings, metre.warnings)),
|
|
1779
1851
|
};
|
|
1780
1852
|
}
|
|
1781
1853
|
|
|
@@ -1825,15 +1897,29 @@ const SCRIPT_BLOCK_CLASS = "script-block";
|
|
|
1825
1897
|
* @returns {string|undefined} The text between the braces.
|
|
1826
1898
|
*/
|
|
1827
1899
|
function rewrittenAttributes(detail, search) {
|
|
1828
|
-
const { run, own, keys, face, generic,
|
|
1900
|
+
const { run, own, keys, face, generic, encoding } = detail;
|
|
1901
|
+
const emphasis = own.classes.find((name) => name === "emph" || name === "strong");
|
|
1902
|
+
if (emphasis && !Object.keys(keys).length) {
|
|
1903
|
+
const convention = detail.conventions?.[emphasis === "emph" ? "em" : "strong"];
|
|
1904
|
+
return attributeText({
|
|
1905
|
+
lang: undefined,
|
|
1906
|
+
...own,
|
|
1907
|
+
classes: [...own.classes, ...(convention ? [EMPHASIS_CLASSES[convention]] : [])],
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1829
1910
|
const changes =
|
|
1830
1911
|
own.classes.includes(NATIVEFONT_CLASS) ||
|
|
1831
1912
|
run.font !== undefined ||
|
|
1832
|
-
run.
|
|
1913
|
+
run.enc !== undefined ||
|
|
1833
1914
|
(run.tag !== undefined &&
|
|
1834
1915
|
(keys.lang !== run.tag || run.kind === "verse" || run.kind === "scroll"));
|
|
1835
1916
|
if (!changes) return undefined;
|
|
1836
|
-
const
|
|
1917
|
+
const convention =
|
|
1918
|
+
emphasis ? detail.conventions?.[emphasis === "emph" ? "em" : "strong"] : undefined;
|
|
1919
|
+
const classes = [
|
|
1920
|
+
...own.classes.filter((name) => name !== NATIVEFONT_CLASS),
|
|
1921
|
+
...(convention ? [EMPHASIS_CLASSES[convention]] : []),
|
|
1922
|
+
];
|
|
1837
1923
|
const values = [...own.values];
|
|
1838
1924
|
if (face) {
|
|
1839
1925
|
classes.push(SCRIPT_CLASS);
|
|
@@ -1842,7 +1928,7 @@ function rewrittenAttributes(detail, search) {
|
|
|
1842
1928
|
if (run.kind !== "span" && face.direction === "rtl") values.push(["dir", "rtl"]);
|
|
1843
1929
|
if (search) values.push(["data-pagefind-ignore", "true"]);
|
|
1844
1930
|
} else if (generic) classes.push(generic);
|
|
1845
|
-
if (run.
|
|
1931
|
+
if (run.enc !== undefined && run.enc !== "default" && !encoding) return undefined;
|
|
1846
1932
|
return attributeText({ lang: run.tag, id: own.id, classes, values });
|
|
1847
1933
|
}
|
|
1848
1934
|
|
|
@@ -1880,6 +1966,37 @@ function emphasisTag(delimiter, conventions, book) {
|
|
|
1880
1966
|
* @returns {string} The set text.
|
|
1881
1967
|
*/
|
|
1882
1968
|
function setRange(text, from, to, detail, lineStart, book) {
|
|
1969
|
+
if (detail.plan) {
|
|
1970
|
+
const events = detail.plan.pieces.filter(
|
|
1971
|
+
(piece) =>
|
|
1972
|
+
piece.boundary !== "canto" &&
|
|
1973
|
+
((piece.anchor >= from && piece.anchor < to) ||
|
|
1974
|
+
(piece.anchor === to && piece.closing)),
|
|
1975
|
+
);
|
|
1976
|
+
for (const piece of events) detail.plan.emitted.add(piece);
|
|
1977
|
+
const breaks = [...text.slice(from, to).matchAll(/\r\n|\r|\n/g)].map((match) => ({
|
|
1978
|
+
anchor: from + match.index,
|
|
1979
|
+
text: match[0],
|
|
1980
|
+
newline: true,
|
|
1981
|
+
}));
|
|
1982
|
+
const emphasis = detail.emphasis
|
|
1983
|
+
.filter((delimiter) => delimiter.offset >= from && delimiter.offset < to)
|
|
1984
|
+
.map((delimiter) => ({
|
|
1985
|
+
anchor: delimiter.offset,
|
|
1986
|
+
text: emphasisTag(delimiter, detail.conventions, book),
|
|
1987
|
+
markup: true,
|
|
1988
|
+
}));
|
|
1989
|
+
const output = [...events, ...breaks, ...emphasis].sort(
|
|
1990
|
+
(a, b) => a.anchor - b.anchor || Number(a.newline) - Number(b.newline),
|
|
1991
|
+
);
|
|
1992
|
+
return output
|
|
1993
|
+
.map((event) =>
|
|
1994
|
+
event.newline || event.markup ?
|
|
1995
|
+
event.text
|
|
1996
|
+
: escapeRoman(event.to, lineStart && event.anchor === from),
|
|
1997
|
+
)
|
|
1998
|
+
.join("");
|
|
1999
|
+
}
|
|
1883
2000
|
let out = "";
|
|
1884
2001
|
let at = from;
|
|
1885
2002
|
for (const delimiter of detail.emphasis) {
|
|
@@ -1887,24 +2004,25 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
1887
2004
|
if (delimiter.offset > at)
|
|
1888
2005
|
out += setText(
|
|
1889
2006
|
text.slice(at, delimiter.offset),
|
|
1890
|
-
detail.
|
|
2007
|
+
detail.selection,
|
|
1891
2008
|
at === from && lineStart,
|
|
1892
2009
|
).text;
|
|
1893
2010
|
out += emphasisTag(delimiter, detail.conventions, book);
|
|
1894
2011
|
at = delimiter.offset + delimiter.length;
|
|
1895
2012
|
}
|
|
1896
|
-
if (at < to)
|
|
2013
|
+
if (at < to)
|
|
2014
|
+
out += setText(text.slice(at, to), detail.selection, at === from && lineStart).text;
|
|
1897
2015
|
return out;
|
|
1898
2016
|
}
|
|
1899
2017
|
|
|
1900
2018
|
/**
|
|
1901
|
-
* Rewrite text marked with `lang=`, `font=` and `
|
|
2019
|
+
* Rewrite text marked with `lang=`, `font=` and `enc=` into the plain span,
|
|
1902
2020
|
* div and verse grammar the rest of each chain renders, one line for one line.
|
|
1903
2021
|
*
|
|
1904
2022
|
* - **Numbered verse prefixes and indentation markers** stay outside run text.
|
|
1905
2023
|
* Each authored run chooses its own language and presentation; physical
|
|
1906
2024
|
* lines and source positions remain unchanged.
|
|
1907
|
-
* - **The
|
|
2025
|
+
* - **The enc, if any, is applied first.** Characters Markdown would read are
|
|
1908
2026
|
* then backslash-escaped, so they reach every later pass as characters.
|
|
1909
2027
|
* - **Emphasis in a face** becomes an `<em>` or a `<strong>` carrying its
|
|
1910
2028
|
* convention's class, or no class where none is stated. The book reads
|
|
@@ -1918,19 +2036,19 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
1918
2036
|
* mark, `<bdo>` or `<bdi>` is ever written: the code points carry their own
|
|
1919
2037
|
* direction.
|
|
1920
2038
|
* - **A scroll fence's body** is rewritten to one line per paragraph, its
|
|
1921
|
-
* lines joined by what the
|
|
2039
|
+
* lines joined by what the enc writes for a space, and left unescaped,
|
|
1922
2040
|
* since the body is raw. Its layout, each character with the source text it
|
|
1923
2041
|
* was written from, is returned for the book, which draws it.
|
|
1924
2042
|
*
|
|
1925
2043
|
* A run with a finding is left as written; {@link scanRuns} reports it.
|
|
1926
2044
|
*
|
|
1927
2045
|
* @param {string} source - Authored Markdown: a note body.
|
|
1928
|
-
* @param {RunContext} [ctx] - The configured languages, faces and
|
|
2046
|
+
* @param {RunContext} [ctx] - The configured languages, faces and encodings.
|
|
1929
2047
|
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Session
|
|
1930
2048
|
* scanner projections.
|
|
1931
2049
|
* @param {{target?: "site"|"foundry"|"book"}} [options] - The target the
|
|
1932
2050
|
* Markdown is rendered for; only `site` writes the search marker.
|
|
1933
|
-
* @returns {{markdown: string, errors: Array<{line: number, column: number, message: string}>, scrolls: Map<string, import("./content-scroll.mjs").ScrollLayout>}}
|
|
2051
|
+
* @returns {{markdown: string, errors: Array<{file?: string, line: number, column: number, message: string}>, scrolls: Map<string, import("./content-scroll.mjs").ScrollLayout>}}
|
|
1934
2052
|
* The rewritten Markdown, the findings of {@link scanRuns}, and each
|
|
1935
2053
|
* rewritten scroll fence's layout, by its rewritten body.
|
|
1936
2054
|
*/
|
|
@@ -1940,19 +2058,22 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1940
2058
|
const text = String(source ?? "");
|
|
1941
2059
|
/** @type {Map<string, import("./content-scroll.mjs").ScrollLayout>} */
|
|
1942
2060
|
const scrolls = new Map();
|
|
1943
|
-
if (!/\b(?:lang|font|
|
|
2061
|
+
if (!/\b(?:lang|font|enc)=|\bnativefont\b|\.(?:emph|strong)\b|⟦/.test(text))
|
|
1944
2062
|
return { markdown: text, errors: [], scrolls };
|
|
1945
2063
|
const { details, errors } = analyseRuns(text, ctx, syntax);
|
|
1946
|
-
if (!details.length)
|
|
2064
|
+
if (!details.length)
|
|
2065
|
+
return { markdown: text, errors: diagnosticColumns(text, errors), scrolls };
|
|
1947
2066
|
const lines = text.split("\n");
|
|
1948
2067
|
const starts = lineStarts(text);
|
|
1949
2068
|
|
|
1950
2069
|
/** Whether a finding sits inside a run. */
|
|
1951
2070
|
const faulted = (run) => {
|
|
2071
|
+
if (run.enc !== "default" && errors.some((error) => error.file)) return true;
|
|
1952
2072
|
const from = (run.line - 1) * 1e6 + run.column;
|
|
1953
2073
|
const end = run.kind === "span" ? positionOf(starts, run.end) : undefined;
|
|
1954
2074
|
const to = end ? (end.line - 1) * 1e6 + end.column : (run.end + 1) * 1e6;
|
|
1955
2075
|
return errors.some((error) => {
|
|
2076
|
+
if (error.file) return false;
|
|
1956
2077
|
const at = (error.line - 1) * 1e6 + error.column;
|
|
1957
2078
|
return at >= from && at < to;
|
|
1958
2079
|
});
|
|
@@ -1969,7 +2090,7 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1969
2090
|
* The body and the layout.
|
|
1970
2091
|
*/
|
|
1971
2092
|
const setScroll = (detail) => {
|
|
1972
|
-
const join = setScrollText(" ", detail.
|
|
2093
|
+
const join = setScrollText(" ", detail.selection);
|
|
1973
2094
|
const texts = [];
|
|
1974
2095
|
const paragraphs = [];
|
|
1975
2096
|
for (const [top, past] of paragraphRanges(lines, detail.run.start + 1, detail.run.end)) {
|
|
@@ -1980,7 +2101,9 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1980
2101
|
let pending = "";
|
|
1981
2102
|
const emit = (pieces) => {
|
|
1982
2103
|
for (const piece of pieces) {
|
|
1983
|
-
const chars = [...piece.to]
|
|
2104
|
+
const chars = [...piece.to].filter(
|
|
2105
|
+
(char) => !/\p{Default_Ignorable_Code_Point}/u.test(char),
|
|
2106
|
+
);
|
|
1984
2107
|
if (!chars.length) {
|
|
1985
2108
|
pending += piece.from;
|
|
1986
2109
|
continue;
|
|
@@ -1997,7 +2120,13 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1997
2120
|
}
|
|
1998
2121
|
};
|
|
1999
2122
|
const set = (from, to) => {
|
|
2000
|
-
const
|
|
2123
|
+
const pieces = detail.plan?.pieces.filter(
|
|
2124
|
+
(piece) => piece.anchor >= from && piece.anchor < to,
|
|
2125
|
+
);
|
|
2126
|
+
const result =
|
|
2127
|
+
pieces ?
|
|
2128
|
+
{ text: pieces.map((piece) => piece.to).join(""), pieces }
|
|
2129
|
+
: setScrollText(text.slice(from, to), detail.selection);
|
|
2001
2130
|
out += result.text;
|
|
2002
2131
|
emit(result.pieces);
|
|
2003
2132
|
};
|
|
@@ -2029,7 +2158,7 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2029
2158
|
|
|
2030
2159
|
for (const detail of details) {
|
|
2031
2160
|
const { run } = detail;
|
|
2032
|
-
if (faulted(run)) continue;
|
|
2161
|
+
if (faulted(run) || (detail.owner && faulted(detail.owner.run))) continue;
|
|
2033
2162
|
const attributes = rewrittenAttributes(detail, search);
|
|
2034
2163
|
if (run.kind === "span") {
|
|
2035
2164
|
const contentEnd = /** @type {number} */ (detail.contentEnd);
|
|
@@ -2057,7 +2186,13 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2057
2186
|
});
|
|
2058
2187
|
continue;
|
|
2059
2188
|
}
|
|
2060
|
-
|
|
2189
|
+
const emphasis = detail.own.classes.some(
|
|
2190
|
+
(name) => name === "emph" || name === "strong",
|
|
2191
|
+
);
|
|
2192
|
+
if (emphasis && book && detail.face && !detail.conventions) {
|
|
2193
|
+
edits.push({ from: run.start, to: run.start + 1, text: "" });
|
|
2194
|
+
edits.push({ from: contentEnd, to: run.end, text: "" });
|
|
2195
|
+
} else if (attributes !== undefined)
|
|
2061
2196
|
edits.push({ from: contentEnd + 2, to: run.end - 1, text: attributes });
|
|
2062
2197
|
if (detail.plain)
|
|
2063
2198
|
for (const [from, to] of detail.reference ?
|
|
@@ -2101,8 +2236,28 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2101
2236
|
}
|
|
2102
2237
|
}
|
|
2103
2238
|
|
|
2239
|
+
for (const detail of details) {
|
|
2240
|
+
if (!detail.plan || detail.plan.owner !== detail || faulted(detail.run)) continue;
|
|
2241
|
+
for (const piece of detail.plan.pieces.filter(
|
|
2242
|
+
(piece) => piece.boundary && !detail.plan.emitted.has(piece),
|
|
2243
|
+
)) {
|
|
2244
|
+
if (piece.boundary === "canto") {
|
|
2245
|
+
const end = text.indexOf("\n", piece.anchor);
|
|
2246
|
+
edits.push({ from: piece.anchor, to: end < 0 ? text.length : end, text: piece.to });
|
|
2247
|
+
} else {
|
|
2248
|
+
// A final independent child may occupy the rest of the line.
|
|
2249
|
+
// Its boundary belongs after that child, in the enclosing face.
|
|
2250
|
+
edits.push({
|
|
2251
|
+
from: piece.anchor,
|
|
2252
|
+
to: piece.anchor,
|
|
2253
|
+
text: escapeRoman(piece.to, false),
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2104
2259
|
let markdown = text;
|
|
2105
2260
|
for (const edit of edits.sort((a, b) => b.from - a.from))
|
|
2106
2261
|
markdown = markdown.slice(0, edit.from) + edit.text + markdown.slice(edit.to);
|
|
2107
|
-
return { markdown, errors, scrolls };
|
|
2262
|
+
return { markdown, errors: diagnosticColumns(text, errors), scrolls };
|
|
2108
2263
|
}
|