@heroiclands/package-build 24.1.1 → 25.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +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 +154 -11
- package/docs/authoring/links-and-markup.md +260 -473
- package/docs/commands.md +118 -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 +135 -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 +453 -569
- package/docs/reference/format-details.md +290 -380
- package/docs/reference/note-types.md +80 -62
- package/docs/source-preview.md +8 -0
- package/e2e.mjs +1 -1
- package/engine/base-compiler.mjs +27 -9
- package/engine/content-charset.mjs +21 -1
- package/engine/content-format.yaml +7 -2
- package/engine/content-index.mjs +6 -2
- package/engine/content-links.mjs +1 -0
- package/engine/content-scripts.mjs +658 -423
- 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 +699 -0
- package/engine/encoding-notes.mjs +186 -0
- package/engine/encoding-standard.mjs +42 -0
- package/engine/encoding-template.mjs +313 -0
- package/engine/encoding-terms.mjs +141 -0
- package/engine/encoding.mjs +946 -0
- package/engine/extension-attributes.mjs +2 -0
- package/engine/helpers.mjs +1 -1
- package/engine/index.mjs +10 -0
- package/engine/language-data.mjs +4 -1
- package/engine/language-terms.mjs +23 -12
- 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 +232 -0
- 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-charset.d.mts +8 -0
- package/types/engine/content-index.d.mts +4 -1
- package/types/engine/content-scripts.d.mts +108 -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 +43 -0
- package/types/engine/encoding-terms.d.mts +105 -0
- package/types/engine/encoding.d.mts +155 -0
- package/types/engine/helpers.d.mts +1 -1
- package/types/engine/index.d.mts +5 -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 +45 -0
- 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 -1034
- 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,87 +12,38 @@
|
|
|
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
|
|
|
92
43
|
import MarkdownIt from "markdown-it";
|
|
93
44
|
|
|
94
45
|
import { scanBlocks, BLOCK_CONTAINERS } from "./content-blocks.mjs";
|
|
95
|
-
import { scanVerse, hasColumns, verseLine, verseGroups } from "./content-verse.mjs";
|
|
46
|
+
import { scanVerse, hasColumns, verseLine, verseGroups, STANZA_BREAK } from "./content-verse.mjs";
|
|
96
47
|
import { scanSpans } from "./content-spans.mjs";
|
|
97
48
|
import { scanScrolls, scrollParagraphs } from "./content-scroll.mjs";
|
|
98
49
|
import { directionsFromRecords } from "./script-direction.mjs";
|
|
@@ -105,23 +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 {
|
|
116
|
-
import {
|
|
63
|
+
import { encodeWithLanguage } from "./encoding-language.mjs";
|
|
64
|
+
import { encodingsFromRecords, encodingAddress } from "./encoding-notes.mjs";
|
|
65
|
+
import { romanInventoriesOf, romanTextProblems } from "./roman-characters.mjs";
|
|
66
|
+
import { plainRomanRun } from "./roman-run.mjs";
|
|
67
|
+
import { plainRomanVerse } from "./roman-verse.mjs";
|
|
68
|
+
import { encodeText } from "./encoding.mjs";
|
|
69
|
+
import { WIKILINK } from "./wikilink-syntax.mjs";
|
|
117
70
|
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
118
71
|
import { parseExtensionAttributes } from "./extension-attributes.mjs";
|
|
119
72
|
|
|
120
73
|
/** The keys this module reads, which no construct passes through as written. */
|
|
121
|
-
export const RUN_KEYS = Object.freeze(["lang", "font", "
|
|
74
|
+
export const RUN_KEYS = Object.freeze(["lang", "font", "enc"]);
|
|
122
75
|
|
|
123
76
|
/** Explicit opt-in to native-script presentation on this construct. */
|
|
124
|
-
|
|
77
|
+
const NATIVEFONT_CLASS = "nativefont";
|
|
125
78
|
|
|
126
79
|
/** The generic families `font=` may name, each with the class that sets it. */
|
|
127
80
|
export const GENERIC_FONTS = Object.freeze({
|
|
@@ -137,28 +90,30 @@ const CONFIG = "package-build.config.yaml";
|
|
|
137
90
|
* What a run is checked against.
|
|
138
91
|
*
|
|
139
92
|
* @typedef {object} RunContext
|
|
93
|
+
* @property {string} [contentPackage] - The citing content package.
|
|
140
94
|
* @property {Readonly<Record<string, {tag: string}>>} [languages] - The
|
|
141
95
|
* configured languages.
|
|
142
96
|
* @property {ReadonlyMap<string, import("./script-fonts.mjs").Face>} [faces] -
|
|
143
97
|
* The installed faces by slug, from
|
|
144
98
|
* {@link module:engine/script-fonts.resolveScriptFonts}.
|
|
145
|
-
* @property {ReadonlyMap<string, import("./
|
|
146
|
-
* The
|
|
147
|
-
*
|
|
148
|
-
* @property {ReadonlyMap<string,
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* @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] -
|
|
152
105
|
* Each configured language's lexicon by name, from
|
|
153
|
-
* {@link module:engine/
|
|
106
|
+
* {@link module:engine/encoding-lexicon.lexiconsFromRecords}.
|
|
154
107
|
* @property {ReadonlyMap<string, import("./run-words.mjs").WordBook>} [wordBooks] -
|
|
155
|
-
* What each configured language's
|
|
108
|
+
* What each configured language's encoded runs are checked against, from
|
|
156
109
|
* {@link module:engine/run-words.wordBooksFromRecords}.
|
|
157
110
|
* @property {ReadonlyMap<string, import("./metre-declaration.mjs").Metre>} [metres] -
|
|
158
111
|
* The metres the `doc` notes declare, by name.
|
|
159
112
|
* @property {ReadonlyMap<string, import("./verse-metre.mjs").MetreBook>} [metreBooks] -
|
|
160
113
|
* What each configured language's verse is counted by, from
|
|
161
114
|
* {@link module:engine/verse-metre.metreBooksFromRecords}.
|
|
115
|
+
* @property {ReadonlyMap<string, import("./roman-characters.mjs").RomanInventory>} [romanInventories] -
|
|
116
|
+
* Exact Roman source inventories by configured alias and BCP 47 tag.
|
|
162
117
|
* @property {ReadonlyMap<string, import("./script-emphasis.mjs").Emphasis>} [emphases] -
|
|
163
118
|
* Each face's emphasis conventions, from the script notes, by
|
|
164
119
|
* {@link module:engine/script-emphasis.emphasesFromRecords}.
|
|
@@ -171,11 +126,9 @@ const CONFIG = "package-build.config.yaml";
|
|
|
171
126
|
*/
|
|
172
127
|
|
|
173
128
|
/**
|
|
174
|
-
* One span, div, verse fence or scroll fence carrying
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* defaults are selected only by this construct's explicit `.nativefont`
|
|
178
|
-
* 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.
|
|
179
132
|
*
|
|
180
133
|
* @typedef {object} Run
|
|
181
134
|
* @property {"span"|"block"|"verse"|"scroll"} kind - Which construct.
|
|
@@ -183,32 +136,30 @@ const CONFIG = "package-build.config.yaml";
|
|
|
183
136
|
* @property {string|undefined} tag - The BCP 47 tag it resolves to; `undefined`
|
|
184
137
|
* when it resolves to none.
|
|
185
138
|
* @property {string|undefined} font - The font as written.
|
|
186
|
-
* @property {string|undefined}
|
|
139
|
+
* @property {string|undefined} enc - The resolved document Address, or default for Roman text.
|
|
187
140
|
* @property {number} line - 1-based line of the construct's opening.
|
|
188
141
|
* @property {number} column - 1-based UTF-16 column of its opening.
|
|
189
142
|
* @property {number} start - A span's offset of `[`; a div's or fence's
|
|
190
143
|
* 0-based opening line.
|
|
191
144
|
* @property {number} end - A span's offset past its attributes; a div's or
|
|
192
145
|
* fence's 0-based closing line.
|
|
193
|
-
* @property {boolean} [nativefont] - True only for an explicit native opt-in
|
|
194
|
-
* with its own language; retained by index records for deferred defaults.
|
|
195
146
|
*/
|
|
196
147
|
|
|
197
148
|
/** @type {WeakMap<object, RunContext>} */
|
|
198
149
|
const contexts = new WeakMap();
|
|
199
150
|
|
|
200
151
|
/** @type {WeakMap<object, WeakMap<object, RunContext>>} */
|
|
201
|
-
const
|
|
152
|
+
const withEncodings = new WeakMap();
|
|
202
153
|
|
|
203
154
|
/**
|
|
204
|
-
* The context a resolved configuration gives, and the
|
|
155
|
+
* The context a resolved configuration gives, and the encodings an index holds.
|
|
205
156
|
*
|
|
206
|
-
* @param {{languages?: object, rootDir?: string}|undefined} config - The
|
|
157
|
+
* @param {{languages?: object, rootDir?: string, contentPackage?:string, paths?:{content?:string}}|undefined} config - The
|
|
207
158
|
* resolved configuration.
|
|
208
159
|
* @param {Iterable<Record<string, any>>} [records] - The content index's
|
|
209
|
-
* records, whose
|
|
160
|
+
* records, whose language notes supply lexical writings and script choices.
|
|
210
161
|
* @returns {RunContext} Its languages, the faces installed where it sits, and
|
|
211
|
-
* the
|
|
162
|
+
* the encodings. A fonts package that cannot be read gives no faces; a run naming
|
|
212
163
|
* one is reported.
|
|
213
164
|
*/
|
|
214
165
|
export function runContext(config, records) {
|
|
@@ -216,14 +167,19 @@ export function runContext(config, records) {
|
|
|
216
167
|
let base = contexts.get(config);
|
|
217
168
|
if (!base) {
|
|
218
169
|
let faces;
|
|
170
|
+
let fonts;
|
|
219
171
|
if (config.rootDir) {
|
|
220
172
|
try {
|
|
221
|
-
|
|
173
|
+
fonts = resolveScriptFonts(config.rootDir);
|
|
174
|
+
faces = fonts?.faces;
|
|
222
175
|
} catch {
|
|
223
176
|
faces = undefined;
|
|
224
177
|
}
|
|
225
178
|
}
|
|
226
179
|
base = Object.freeze({
|
|
180
|
+
fonts,
|
|
181
|
+
encodings: new Map(),
|
|
182
|
+
encodingFindings: [],
|
|
227
183
|
languages: config.languages ?? {},
|
|
228
184
|
faces: faces ?? new Map(),
|
|
229
185
|
contentPackage: config.contentPackage,
|
|
@@ -231,16 +187,20 @@ export function runContext(config, records) {
|
|
|
231
187
|
contexts.set(config, base);
|
|
232
188
|
}
|
|
233
189
|
if (!records || typeof records !== "object") return base;
|
|
234
|
-
let byRecords =
|
|
235
|
-
if (!byRecords)
|
|
190
|
+
let byRecords = withEncodings.get(config);
|
|
191
|
+
if (!byRecords) withEncodings.set(config, (byRecords = new WeakMap()));
|
|
236
192
|
const known = byRecords.get(records);
|
|
237
193
|
if (known) return known;
|
|
238
|
-
const
|
|
194
|
+
const loaded = encodingsFromRecords(records, config, base.fonts);
|
|
195
|
+
const encodings = loaded.encodings;
|
|
239
196
|
const languageRecords = languageRecordsByName(records, config);
|
|
240
197
|
const context = Object.freeze({
|
|
241
198
|
...base,
|
|
242
|
-
|
|
243
|
-
|
|
199
|
+
encodings,
|
|
200
|
+
encodingDefinitions: loaded.definitions,
|
|
201
|
+
encodingFindings: loaded.findings,
|
|
202
|
+
languageRecords,
|
|
203
|
+
romanInventories: romanInventoriesOf(languageRecords, config.languages),
|
|
244
204
|
lexicons: lexiconsFromRecords(records, config),
|
|
245
205
|
wordBooks: wordBooksFromRecords(records, config),
|
|
246
206
|
metres: metresFromRecords(records),
|
|
@@ -590,28 +550,6 @@ function emphasisDelimiters(text, holes = new Map()) {
|
|
|
590
550
|
return delimiters;
|
|
591
551
|
}
|
|
592
552
|
|
|
593
|
-
/**
|
|
594
|
-
* The parts of ranges of text that no delimiter covers.
|
|
595
|
-
*
|
|
596
|
-
* @param {Array<[number, number]>} parts - The ranges, as offsets.
|
|
597
|
-
* @param {readonly Delimiter[]} delimiters - Delimiters, by absolute offset.
|
|
598
|
-
* @returns {Array<[number, number]>} The ranges, split around each delimiter.
|
|
599
|
-
*/
|
|
600
|
-
function outsideDelimiters(parts, delimiters) {
|
|
601
|
-
if (!delimiters.length) return parts;
|
|
602
|
-
const out = [];
|
|
603
|
-
for (const [from, to] of parts) {
|
|
604
|
-
let at = from;
|
|
605
|
-
for (const delimiter of delimiters) {
|
|
606
|
-
if (delimiter.offset < at || delimiter.offset >= to) continue;
|
|
607
|
-
if (delimiter.offset > at) out.push([at, delimiter.offset]);
|
|
608
|
-
at = delimiter.offset + delimiter.length;
|
|
609
|
-
}
|
|
610
|
-
if (at < to) out.push([at, to]);
|
|
611
|
-
}
|
|
612
|
-
return out;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
553
|
/** A line split into its indentation, its text, a trailing break and its trailing space. */
|
|
616
554
|
const LINE_PARTS = /^(\s*)(.*?)(\\?)(\s*)$/;
|
|
617
555
|
|
|
@@ -625,7 +563,7 @@ const LINE_PARTS = /^(\s*)(.*?)(\\?)(\s*)$/;
|
|
|
625
563
|
* @returns {string} The text, safe in a paragraph.
|
|
626
564
|
*/
|
|
627
565
|
function escapeRoman(line, lineStart = true) {
|
|
628
|
-
const escaped = line.replace(/[\\`*_[\]
|
|
566
|
+
const escaped = line.replace(/[\\`*_[\]<>"'!&#~|{}-]/g, "\\$&");
|
|
629
567
|
return lineStart ?
|
|
630
568
|
escaped.replace(/^([-+=:])/, "\\$1").replace(/^(\d+)([.)])/, "$1\\$2")
|
|
631
569
|
: escaped;
|
|
@@ -655,19 +593,47 @@ function unescapeRoman(core) {
|
|
|
655
593
|
return { text, offsets };
|
|
656
594
|
}
|
|
657
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
|
+
|
|
658
624
|
/**
|
|
659
|
-
* 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,
|
|
660
626
|
* and the characters Markdown would read escaped again. Every line is set on
|
|
661
627
|
* its own, so the line count holds.
|
|
662
628
|
*
|
|
663
629
|
* @param {string} raw - The text as written.
|
|
664
|
-
* @param {import("./
|
|
665
|
-
*
|
|
630
|
+
* @param {ReturnType<import("./encoding-lexicon.mjs").encodingFor>|undefined} selection - The run's
|
|
631
|
+
* enc, face and lexicon.
|
|
666
632
|
* @param {boolean} lineStart - Whether `raw` starts its line.
|
|
667
|
-
* @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}>}}
|
|
668
634
|
* The set text, and each piece of it with the offset in `raw` it came from.
|
|
669
635
|
*/
|
|
670
|
-
function setText(raw,
|
|
636
|
+
function setText(raw, selection, lineStart) {
|
|
671
637
|
const out = [];
|
|
672
638
|
const pieces = [];
|
|
673
639
|
let base = 0;
|
|
@@ -678,26 +644,22 @@ function setText(raw, mapping, lineStart) {
|
|
|
678
644
|
if (core === "") out.push(line);
|
|
679
645
|
else {
|
|
680
646
|
const plain = unescapeRoman(core);
|
|
681
|
-
const
|
|
682
|
-
font:
|
|
683
|
-
|
|
684
|
-
writing: mapping?.writing,
|
|
647
|
+
const encoded = applyEncoding(selection?.encoding, plain.text, {
|
|
648
|
+
font: selection?.font,
|
|
649
|
+
lexicon: selection?.lexicon,
|
|
685
650
|
});
|
|
686
|
-
for (const piece of
|
|
651
|
+
for (const piece of encoded.pieces)
|
|
687
652
|
pieces.push({
|
|
688
653
|
offset: base + lead.length + (plain.offsets[piece.at] ?? 0),
|
|
689
654
|
from: piece.from,
|
|
690
655
|
to: piece.to,
|
|
691
656
|
row: piece.row,
|
|
692
657
|
...(piece.native ? { native: true } : {}),
|
|
693
|
-
...(piece.
|
|
694
|
-
{}
|
|
695
|
-
: { uncapitalised: piece.uncapitalised }),
|
|
696
|
-
...(piece.unwritten === undefined ? {} : { unwritten: piece.unwritten }),
|
|
658
|
+
...(piece.problem === undefined ? {} : { problem: piece.problem }),
|
|
697
659
|
});
|
|
698
660
|
out.push(
|
|
699
661
|
lead +
|
|
700
|
-
escapeRoman(
|
|
662
|
+
escapeRoman(encoded.text, index > 0 || lineStart || lead !== "") +
|
|
701
663
|
breakMark +
|
|
702
664
|
trail,
|
|
703
665
|
);
|
|
@@ -709,21 +671,20 @@ function setText(raw, mapping, lineStart) {
|
|
|
709
671
|
|
|
710
672
|
/**
|
|
711
673
|
* Set a stretch of a scroll fence's text: the author's escapes removed and the
|
|
712
|
-
*
|
|
674
|
+
* enc applied to all of it, its spaces included, and nothing escaped, since
|
|
713
675
|
* the fence's body is raw.
|
|
714
676
|
*
|
|
715
677
|
* @param {string} raw - The text, on one line.
|
|
716
|
-
* @param {import("./
|
|
717
|
-
*
|
|
678
|
+
* @param {ReturnType<import("./encoding-lexicon.mjs").encodingFor>|undefined} selection - The run's
|
|
679
|
+
* enc, face and lexicon.
|
|
718
680
|
* @returns {{text: string, pieces: Array<{from: string, to: string}>}} The set
|
|
719
681
|
* text, and each piece of it.
|
|
720
682
|
*/
|
|
721
|
-
function setScrollText(raw,
|
|
683
|
+
function setScrollText(raw, selection) {
|
|
722
684
|
const plain = unescapeRoman(raw);
|
|
723
|
-
return
|
|
724
|
-
font:
|
|
725
|
-
|
|
726
|
-
writing: mapping?.writing,
|
|
685
|
+
return applyEncoding(selection?.encoding, plain.text, {
|
|
686
|
+
font: selection?.font,
|
|
687
|
+
lexicon: selection?.lexicon,
|
|
727
688
|
});
|
|
728
689
|
}
|
|
729
690
|
|
|
@@ -802,8 +763,8 @@ const BLOCK_WHAT = Object.freeze({
|
|
|
802
763
|
* @property {boolean} plain - Whether its text is set.
|
|
803
764
|
* @property {import("./script-fonts.mjs").Face|undefined} face - Its face.
|
|
804
765
|
* @property {string|undefined} generic - Its generic family's class.
|
|
805
|
-
* @property {import("./
|
|
806
|
-
* @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,
|
|
807
768
|
* face and lexicon, which its text is set by.
|
|
808
769
|
* @property {Map<number, number>} holes - The marked spans inside it.
|
|
809
770
|
* @property {[number, number]|undefined} body - A div's or fence's body, as
|
|
@@ -817,6 +778,10 @@ const BLOCK_WHAT = Object.freeze({
|
|
|
817
778
|
* What its emphasis is set by: its language's convention, else its face's
|
|
818
779
|
* script's; `undefined` sets it plain.
|
|
819
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.
|
|
820
785
|
* @property {boolean} [unrolled] - A scroll fence whose opening has a finding,
|
|
821
786
|
* which is left as written.
|
|
822
787
|
*/
|
|
@@ -842,10 +807,10 @@ function wordsMessage(lang) {
|
|
|
842
807
|
* @returns {import("./script-emphasis.mjs").Emphasis|undefined} The conventions.
|
|
843
808
|
*/
|
|
844
809
|
function conventionsOf({ face, languageEmphasis }, ctx) {
|
|
845
|
-
if (
|
|
810
|
+
if (languageEmphasis === "words") return undefined;
|
|
846
811
|
if (languageEmphasis === "rubric" || languageEmphasis === "overline")
|
|
847
812
|
return { em: languageEmphasis, strong: languageEmphasis };
|
|
848
|
-
return ctx.emphases?.get(face.slug);
|
|
813
|
+
return face ? ctx.emphases?.get(face.slug) : undefined;
|
|
849
814
|
}
|
|
850
815
|
|
|
851
816
|
/**
|
|
@@ -855,7 +820,7 @@ function conventionsOf({ face, languageEmphasis }, ctx) {
|
|
|
855
820
|
* @param {RunContext} ctx - The names.
|
|
856
821
|
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Session
|
|
857
822
|
* scanner projections.
|
|
858
|
-
* @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>}}
|
|
859
824
|
* What {@link scanRuns} returns, and what {@link renderRuns} rewrites by.
|
|
860
825
|
*/
|
|
861
826
|
function analyseRuns(source, ctx, syntax) {
|
|
@@ -864,7 +829,12 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
864
829
|
const starts = lineStarts(text);
|
|
865
830
|
const languages = ctx.languages ?? {};
|
|
866
831
|
const faces = ctx.faces ?? new Map();
|
|
867
|
-
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
|
+
);
|
|
868
838
|
const errors = [];
|
|
869
839
|
const warnings = [];
|
|
870
840
|
const footnotes = new Set(
|
|
@@ -939,7 +909,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
939
909
|
line0: first,
|
|
940
910
|
where: { line: first + 1, column: 1 },
|
|
941
911
|
}),
|
|
942
|
-
|
|
912
|
+
"`.nativefont` is not an accepted class; select an encoding with `enc=`",
|
|
943
913
|
);
|
|
944
914
|
for (const key of Object.keys(keys))
|
|
945
915
|
report(
|
|
@@ -1061,6 +1031,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1061
1031
|
(construct) =>
|
|
1062
1032
|
construct.kind === "scroll" ||
|
|
1063
1033
|
construct.own.classes.includes(NATIVEFONT_CLASS) ||
|
|
1034
|
+
construct.own.classes.some((name) => name === "emph" || name === "strong") ||
|
|
1064
1035
|
Object.keys(construct.keys).length ||
|
|
1065
1036
|
around(construct).some((container) => Object.keys(container.keys).length),
|
|
1066
1037
|
);
|
|
@@ -1072,43 +1043,25 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1072
1043
|
: isLanguageTag(lang) ? lang
|
|
1073
1044
|
: undefined;
|
|
1074
1045
|
|
|
1075
|
-
const native = (construct) =>
|
|
1046
|
+
const native = (construct) =>
|
|
1047
|
+
construct.keys.enc !== undefined && construct.keys.enc !== "default";
|
|
1076
1048
|
|
|
1077
1049
|
const faceNames = () => [...faces.keys(), ...Object.keys(GENERIC_FONTS)];
|
|
1078
1050
|
|
|
1079
1051
|
/** Check the keys a construct states itself. */
|
|
1080
1052
|
const checkOwn = (construct) => {
|
|
1081
|
-
const { lang, font,
|
|
1053
|
+
const { lang, font, enc } = construct.keys;
|
|
1082
1054
|
const { at } = construct;
|
|
1083
|
-
if (
|
|
1055
|
+
if (construct.own.classes.includes(NATIVEFONT_CLASS))
|
|
1084
1056
|
report(
|
|
1085
1057
|
nativeAt(construct),
|
|
1086
|
-
"`.nativefont`
|
|
1058
|
+
"`.nativefont` is not an accepted class; select an encoding with `enc=`",
|
|
1087
1059
|
);
|
|
1088
1060
|
if (native(construct) && construct.kind === "verse" && hasColumns(construct.node))
|
|
1089
1061
|
report(
|
|
1090
|
-
|
|
1091
|
-
"a verse with cols>1
|
|
1062
|
+
construct.at("enc", "value"),
|
|
1063
|
+
"a verse with cols>1 selects encoding on individual runs, not the fence",
|
|
1092
1064
|
);
|
|
1093
|
-
if (!native(construct)) {
|
|
1094
|
-
if (construct.kind === "scroll")
|
|
1095
|
-
report(
|
|
1096
|
-
construct.where,
|
|
1097
|
-
"a scroll requires `.nativefont` and an explicit `lang=` on the same construct",
|
|
1098
|
-
);
|
|
1099
|
-
else {
|
|
1100
|
-
if (font !== undefined && faces.has(font))
|
|
1101
|
-
report(
|
|
1102
|
-
at("font", "value"),
|
|
1103
|
-
`native \`font=${font}\` requires \`.nativefont\` and an explicit \`lang=\` on the same construct`,
|
|
1104
|
-
);
|
|
1105
|
-
if (map !== undefined && maps.has(map))
|
|
1106
|
-
report(
|
|
1107
|
-
at("map", "value"),
|
|
1108
|
-
`\`map=${map}\` requires \`.nativefont\` and an explicit \`lang=\` on the same construct`,
|
|
1109
|
-
);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
1065
|
|
|
1113
1066
|
if (lang !== undefined && !Object.hasOwn(languages, lang)) {
|
|
1114
1067
|
if (isLanguageTag(lang)) {
|
|
@@ -1141,47 +1094,72 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1141
1094
|
(near ? `; did you mean \`${near}\`?` : ""),
|
|
1142
1095
|
);
|
|
1143
1096
|
}
|
|
1144
|
-
if (
|
|
1145
|
-
const near = nearest(
|
|
1097
|
+
if (enc !== undefined && enc !== "default" && !encodingNamed(enc) && !encodingUnavailable) {
|
|
1098
|
+
const near = nearest(enc, [...encodings.keys()]);
|
|
1146
1099
|
report(
|
|
1147
|
-
at("
|
|
1148
|
-
`\`${
|
|
1100
|
+
at("enc", "value"),
|
|
1101
|
+
`\`${enc}\` does not resolve to an encoding document` +
|
|
1149
1102
|
(near ? `; did you mean \`${near}\`?` : ""),
|
|
1150
1103
|
);
|
|
1151
1104
|
}
|
|
1152
1105
|
};
|
|
1153
1106
|
|
|
1154
1107
|
const nativeDefaults = (construct, keys) => {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (keys.font === undefined)
|
|
1159
|
-
report(
|
|
1160
|
-
nativeAt(construct),
|
|
1161
|
-
`\`.nativefont\` has no default \`font=\` for \`${keys.lang}\`; state \`font=\` explicitly`,
|
|
1162
|
-
);
|
|
1163
|
-
if (keys.map !== undefined && !maps.has(keys.map))
|
|
1164
|
-
report(
|
|
1165
|
-
nativeAt(construct),
|
|
1166
|
-
`native default \`map=${keys.map}\` is unavailable; state \`map=\` explicitly`,
|
|
1167
|
-
);
|
|
1108
|
+
if (encodingUnavailable) return;
|
|
1109
|
+
const encoding = encodings.get(keys.enc);
|
|
1110
|
+
if (!encoding) return;
|
|
1168
1111
|
if (
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1112
|
+
encoding.language !== undefined &&
|
|
1113
|
+
keys.lang !== undefined &&
|
|
1114
|
+
keys.lang !== encoding.language &&
|
|
1115
|
+
tagOf(keys.lang)?.toLowerCase() !== tagOf(encoding.language)?.toLowerCase()
|
|
1172
1116
|
)
|
|
1173
1117
|
report(
|
|
1174
|
-
|
|
1175
|
-
`
|
|
1118
|
+
construct.at(construct.keys.lang !== undefined ? "lang" : "enc", "value"),
|
|
1119
|
+
`Encoding \`${encoding.name}\` writes language \`${encoding.language}\`, not \`${keys.lang}\``,
|
|
1176
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`);
|
|
1177
1123
|
};
|
|
1178
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;
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
const rawReferences = [];
|
|
1179
1159
|
// Raw wikilinks belong to the resolver, not the span rewriter. Validate
|
|
1180
1160
|
// their own control attributes with the same grammar before resolution,
|
|
1181
|
-
// without
|
|
1161
|
+
// without selection a destination or consuming the trailing attributes.
|
|
1182
1162
|
for (const match of matchAllOutsideCode(text, new RegExp(WIKILINK.source, "g"))) {
|
|
1183
|
-
const parsed = parseWikilink(match[1], match[0]);
|
|
1184
|
-
if (!parsed.attributes) continue;
|
|
1185
1163
|
const close = match[0].indexOf("]]") + 1;
|
|
1186
1164
|
const from = match.index + close + 2;
|
|
1187
1165
|
const attrs = parseExtensionAttributes(match[0].slice(close + 2, -1), { positions: true });
|
|
@@ -1200,25 +1178,47 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1200
1178
|
};
|
|
1201
1179
|
const unchecked = errors.length;
|
|
1202
1180
|
checkOwn(construct);
|
|
1203
|
-
|
|
1204
|
-
const
|
|
1181
|
+
const keys = presentationOf(construct).keys;
|
|
1182
|
+
const bar = match[1].indexOf("|");
|
|
1183
|
+
if (bar >= 0 && errors.length === unchecked) {
|
|
1184
|
+
rawReferences.push({
|
|
1185
|
+
lang: keys.lang,
|
|
1186
|
+
font: keys.font,
|
|
1187
|
+
from: match.index + 2 + bar + 1,
|
|
1188
|
+
to: match.index + close - 1,
|
|
1189
|
+
where: construct.where,
|
|
1190
|
+
construct,
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
if (errors.length !== unchecked || keys.enc === "default" || !encodingNamed(keys.enc))
|
|
1194
|
+
continue;
|
|
1205
1195
|
nativeDefaults(construct, keys);
|
|
1206
|
-
if (errors.length !== unchecked || keys.
|
|
1196
|
+
if (errors.length !== unchecked || keys.enc === undefined || keys.font === undefined)
|
|
1207
1197
|
continue;
|
|
1208
|
-
const problem = fontProblem(
|
|
1198
|
+
const problem = fontProblem(encodings.get(keys.enc), keys.font);
|
|
1209
1199
|
if (problem)
|
|
1210
1200
|
report(
|
|
1211
|
-
construct.at(construct.keys.
|
|
1201
|
+
construct.at(construct.keys.font !== undefined ? "font" : "enc", "value"),
|
|
1212
1202
|
problem,
|
|
1213
1203
|
);
|
|
1214
1204
|
}
|
|
1215
1205
|
|
|
1216
1206
|
const verseMarkers = new Map();
|
|
1207
|
+
const verseContexts = new Map();
|
|
1208
|
+
const verseReadings = new Map();
|
|
1217
1209
|
for (const poem of scanVerse(text).blocks) {
|
|
1210
|
+
for (let line = poem.start + 1; line < poem.end; line++) verseContexts.set(line, poem);
|
|
1211
|
+
for (let line = poem.start + 1; line < poem.end; line++)
|
|
1212
|
+
if (
|
|
1213
|
+
STANZA_BREAK.test(lines[line].trim()) ||
|
|
1214
|
+
/^---\s+\{\.canto\}$/.test(lines[line].trim())
|
|
1215
|
+
)
|
|
1216
|
+
verseMarkers.set(starts[line], starts[line] + lines[line].length);
|
|
1218
1217
|
if (hasColumns(poem)) {
|
|
1219
1218
|
for (const group of verseGroups(poem, lines)) {
|
|
1220
1219
|
if (group.stanzaBreak) continue;
|
|
1221
1220
|
for (const part of group.parts) {
|
|
1221
|
+
verseReadings.set(part.line, part);
|
|
1222
1222
|
const start = starts[part.line];
|
|
1223
1223
|
if (part.prefixLength)
|
|
1224
1224
|
verseMarkers.set(
|
|
@@ -1297,141 +1297,34 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1297
1297
|
}
|
|
1298
1298
|
};
|
|
1299
1299
|
|
|
1300
|
-
/**
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
* @param {RunDetail} detail - The run.
|
|
1306
|
-
*/
|
|
1307
|
-
const covered = (parts, detail) => {
|
|
1308
|
-
const { face, textMap, mapping } = detail;
|
|
1309
|
-
if (!face) return;
|
|
1310
|
-
const said = new Set();
|
|
1311
|
-
for (const [from, to] of parts) {
|
|
1312
|
-
const lineStart = from === 0 || text[from - 1] === "\n";
|
|
1313
|
-
for (const piece of setText(text.slice(from, to), mapping, lineStart).pieces) {
|
|
1314
|
-
// A word with no writing is its own finding, not a face's.
|
|
1315
|
-
if (piece.unwritten) continue;
|
|
1316
|
-
for (const char of piece.to) {
|
|
1317
|
-
const point = /** @type {number} */ (char.codePointAt(0));
|
|
1318
|
-
if (/\s/u.test(char) || face.draws.has(point)) continue;
|
|
1319
|
-
const key = piece.row ? `${piece.from}\u0000${point}` : char;
|
|
1320
|
-
if (said.has(key)) continue;
|
|
1321
|
-
said.add(key);
|
|
1322
|
-
const where = positionOf(starts, from + piece.offset);
|
|
1323
|
-
if (piece.native)
|
|
1324
|
-
report(
|
|
1325
|
-
where,
|
|
1326
|
-
`\`${piece.from}\` is written ${codeName(point)} by the lexicon of ` +
|
|
1327
|
-
`\`${detail.run.lang}\` for the map \`${textMap?.name}\`, which the font ` +
|
|
1328
|
-
`\`${face.slug}\` does not draw`,
|
|
1329
|
-
);
|
|
1330
|
-
else if (piece.row)
|
|
1331
|
-
report(
|
|
1332
|
-
where,
|
|
1333
|
-
`\`${piece.from}\` maps to ${codeName(point)} in the map \`${textMap?.name}\`, ` +
|
|
1334
|
-
`which the font \`${face.slug}\` does not draw`,
|
|
1335
|
-
);
|
|
1336
|
-
else
|
|
1337
|
-
report(
|
|
1338
|
-
where,
|
|
1339
|
-
`\`${char}\` (${codeName(point)}) is not drawn by the font \`${face.slug}\`` +
|
|
1340
|
-
(textMap ?
|
|
1341
|
-
`, and the map \`${textMap.name}\` has no row for it`
|
|
1342
|
-
: ""),
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
};
|
|
1348
|
-
|
|
1349
|
-
/**
|
|
1350
|
-
* Report each capitalised word whose native form the rows cannot
|
|
1351
|
-
* capitalise, at the word.
|
|
1352
|
-
*
|
|
1353
|
-
* @param {Array<[number, number]>} parts - The text to check, as offsets.
|
|
1354
|
-
* @param {RunDetail} detail - The run.
|
|
1355
|
-
*/
|
|
1356
|
-
const capitals = (parts, detail) => {
|
|
1357
|
-
const { mapping } = detail;
|
|
1358
|
-
if (!mapping?.native) return;
|
|
1359
|
-
for (const [from, to] of parts) {
|
|
1360
|
-
const lineStart = from === 0 || text[from - 1] === "\n";
|
|
1361
|
-
for (const piece of setText(text.slice(from, to), mapping, lineStart).pieces)
|
|
1362
|
-
if (piece.uncapitalised !== undefined)
|
|
1363
|
-
report(
|
|
1364
|
-
positionOf(starts, from + piece.offset),
|
|
1365
|
-
uncapitalisedMessage(
|
|
1366
|
-
piece.from,
|
|
1367
|
-
piece.uncapitalised,
|
|
1368
|
-
detail.run.lang,
|
|
1369
|
-
mapping.map.name,
|
|
1370
|
-
),
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
};
|
|
1300
|
+
/** The configured source inventory for either an alias or its BCP 47 tag. */
|
|
1301
|
+
const inventoryOf = (lang) =>
|
|
1302
|
+
lang === undefined ? undefined : (
|
|
1303
|
+
(ctx.romanInventories?.get(lang) ?? ctx.romanInventories?.get(lang.toLowerCase()))
|
|
1304
|
+
);
|
|
1374
1305
|
|
|
1375
|
-
/**
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
const said = new Set();
|
|
1386
|
-
for (const [from, to] of parts) {
|
|
1387
|
-
const lineStart = from === 0 || text[from - 1] === "\n";
|
|
1388
|
-
for (const piece of setText(text.slice(from, to), mapping, lineStart).pieces) {
|
|
1389
|
-
if (piece.unwritten === undefined || said.has(piece.from)) continue;
|
|
1390
|
-
said.add(piece.from);
|
|
1391
|
-
report(
|
|
1392
|
-
positionOf(starts, from + piece.offset),
|
|
1393
|
-
unwrittenMessage(
|
|
1394
|
-
piece.from,
|
|
1395
|
-
piece.unwritten,
|
|
1396
|
-
detail.run.lang,
|
|
1397
|
-
mapping.map.name,
|
|
1398
|
-
),
|
|
1399
|
-
);
|
|
1400
|
-
}
|
|
1306
|
+
/** Check visible source before selection or lexicon-native replacement. */
|
|
1307
|
+
const roman = (parts, detail) => {
|
|
1308
|
+
const inventory = inventoryOf(detail.run.lang);
|
|
1309
|
+
if (!inventory) return;
|
|
1310
|
+
if (!inventory.characters) {
|
|
1311
|
+
report(
|
|
1312
|
+
{ line: detail.run.line, column: detail.run.column },
|
|
1313
|
+
`the language note for \`${inventory.language}\` must declare \`data.roman\``,
|
|
1314
|
+
);
|
|
1315
|
+
return;
|
|
1401
1316
|
}
|
|
1402
|
-
};
|
|
1403
|
-
|
|
1404
|
-
/**
|
|
1405
|
-
* Report each word of a mapped run that is not a word of its language, once
|
|
1406
|
-
* per run, at its first character.
|
|
1407
|
-
*
|
|
1408
|
-
* @param {Array<[number, number]>} parts - The text to check, as offsets.
|
|
1409
|
-
* @param {RunDetail} detail - The run.
|
|
1410
|
-
*/
|
|
1411
|
-
const lexical = (parts, detail) => {
|
|
1412
|
-
const { lang } = detail.run;
|
|
1413
|
-
if (lang === undefined) return;
|
|
1414
|
-
const book = ctx.wordBooks?.get(lang);
|
|
1415
|
-
if (!book) return;
|
|
1416
|
-
const said = new Set();
|
|
1417
1317
|
for (const [from, to] of parts) {
|
|
1418
|
-
|
|
1419
|
-
for (const
|
|
1420
|
-
|
|
1421
|
-
for (const { offset, word } of unknownWords(plain.text, book, said))
|
|
1422
|
-
report(
|
|
1423
|
-
positionOf(starts, base + (plain.offsets[offset] ?? 0)),
|
|
1424
|
-
unknownWordMessage(word, lang),
|
|
1425
|
-
);
|
|
1426
|
-
base += line.length + 1;
|
|
1427
|
-
}
|
|
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);
|
|
1428
1321
|
}
|
|
1429
1322
|
};
|
|
1430
1323
|
|
|
1431
1324
|
/**
|
|
1432
1325
|
* Give a scroll fence the face it is set in, and report a fence that names
|
|
1433
1326
|
* none or whose script is written in lines. A fence that names no face
|
|
1434
|
-
* takes its
|
|
1327
|
+
* takes its enc's, where the enc is drawn in one.
|
|
1435
1328
|
*
|
|
1436
1329
|
* @param {object} construct - The scroll fence.
|
|
1437
1330
|
* @param {Record<string, string>} keys - Its run keys, given the face.
|
|
@@ -1442,7 +1335,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1442
1335
|
if (ctx.directions.get(keys.font) === "vertical-rl") return;
|
|
1443
1336
|
report(
|
|
1444
1337
|
construct.keys.font !== undefined ? construct.at("font", "value")
|
|
1445
|
-
: construct.keys.
|
|
1338
|
+
: construct.keys.enc !== undefined ? construct.at("enc", "value")
|
|
1446
1339
|
: construct.keys.lang !== undefined ? construct.at("lang", "value")
|
|
1447
1340
|
: construct.where,
|
|
1448
1341
|
`a \`scroll\` fence sets a script written in columns, and the script of \`${keys.font}\` ` +
|
|
@@ -1455,68 +1348,52 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1455
1348
|
for (const construct of active) {
|
|
1456
1349
|
const unchecked = errors.length;
|
|
1457
1350
|
checkOwn(construct);
|
|
1458
|
-
// A key that names nothing is the finding; the text it would have set
|
|
1459
|
-
// is not then checked against a face or a language it cannot reach.
|
|
1460
1351
|
let named = errors.length === unchecked;
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
// children, so a parallel Roman run never takes a native neighbour's face.
|
|
1464
|
-
const ancestors = around(construct);
|
|
1465
|
-
const keys = {};
|
|
1466
|
-
for (const ancestor of ancestors) {
|
|
1467
|
-
if (ancestor.keys.lang !== undefined) keys.lang = ancestor.keys.lang;
|
|
1468
|
-
if (!native(ancestor) && Object.hasOwn(GENERIC_FONTS, ancestor.keys.font ?? ""))
|
|
1469
|
-
keys.font = ancestor.keys.font;
|
|
1470
|
-
}
|
|
1471
|
-
if (native(construct)) delete keys.font;
|
|
1472
|
-
Object.assign(keys, construct.keys);
|
|
1473
|
-
const columnFence = construct.kind === "verse" && hasColumns(construct.node);
|
|
1474
|
-
if (named && native(construct) && !columnFence) {
|
|
1352
|
+
const { keys } = presentationOf(construct);
|
|
1353
|
+
if (named && native(construct)) {
|
|
1475
1354
|
nativeDefaults(construct, keys);
|
|
1476
1355
|
named = errors.length === unchecked;
|
|
1477
|
-
} else if (!native(construct) && keys.font === undefined && ancestors.some(native)) {
|
|
1478
|
-
// Reset CSS/Typst inheritance from a surrounding native face.
|
|
1479
|
-
keys.font = "serif";
|
|
1480
1356
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
named = errors.length === unchecked;
|
|
1484
|
-
}
|
|
1485
|
-
const { lang, font, map } = keys;
|
|
1486
|
-
// 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
|
|
1487
1359
|
// known, at the construct that states either.
|
|
1488
1360
|
const stated =
|
|
1489
1361
|
native(construct) ||
|
|
1490
|
-
construct.keys.
|
|
1362
|
+
construct.keys.enc !== undefined ||
|
|
1491
1363
|
construct.keys.font !== undefined;
|
|
1492
1364
|
if (
|
|
1493
1365
|
named &&
|
|
1494
1366
|
stated &&
|
|
1495
|
-
|
|
1496
|
-
|
|
1367
|
+
enc !== undefined &&
|
|
1368
|
+
encodings.has(enc) &&
|
|
1497
1369
|
font !== undefined &&
|
|
1498
1370
|
(faces.has(font) || Object.hasOwn(GENERIC_FONTS, font))
|
|
1499
1371
|
) {
|
|
1500
|
-
const problem = fontProblem(
|
|
1372
|
+
const problem = fontProblem(encodings.get(enc), font);
|
|
1501
1373
|
if (problem)
|
|
1502
1374
|
report(
|
|
1503
|
-
construct.at(construct.keys.
|
|
1375
|
+
construct.at(construct.keys.font !== undefined ? "font" : "enc", "value"),
|
|
1504
1376
|
problem,
|
|
1505
1377
|
);
|
|
1506
1378
|
}
|
|
1379
|
+
named = errors.length === unchecked;
|
|
1380
|
+
if (construct.kind === "scroll" && named) {
|
|
1381
|
+
scrollFace(construct, keys);
|
|
1382
|
+
named = errors.length === unchecked;
|
|
1383
|
+
}
|
|
1507
1384
|
const run = {
|
|
1508
1385
|
kind: construct.kind,
|
|
1509
1386
|
lang,
|
|
1510
1387
|
tag: tagOf(lang),
|
|
1511
1388
|
font,
|
|
1512
|
-
|
|
1513
|
-
...(native(construct) && construct.keys.lang !== undefined ? { nativefont: true } : {}),
|
|
1389
|
+
enc,
|
|
1514
1390
|
...construct.where,
|
|
1515
1391
|
};
|
|
1516
1392
|
runs.push(run);
|
|
1517
1393
|
const plain =
|
|
1394
|
+
inventoryOf(lang) !== undefined ||
|
|
1518
1395
|
font !== undefined ||
|
|
1519
|
-
|
|
1396
|
+
(enc !== undefined && enc !== "default") ||
|
|
1520
1397
|
(lang !== undefined &&
|
|
1521
1398
|
Object.hasOwn(languages, lang) &&
|
|
1522
1399
|
!/^en(?:-|$)/i.test(run.tag ?? ""));
|
|
@@ -1527,8 +1404,8 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1527
1404
|
plain,
|
|
1528
1405
|
face: font === undefined ? undefined : faces.get(font),
|
|
1529
1406
|
generic: font === undefined ? undefined : GENERIC_FONTS[font],
|
|
1530
|
-
|
|
1531
|
-
|
|
1407
|
+
encoding: enc === undefined ? undefined : encodings.get(enc),
|
|
1408
|
+
selection: enc === undefined ? undefined : encodingFor(ctx, { enc, font, lang }),
|
|
1532
1409
|
holes: new Map(),
|
|
1533
1410
|
body: undefined,
|
|
1534
1411
|
contentEnd: construct.kind === "span" ? construct.node.contentEnd : undefined,
|
|
@@ -1542,11 +1419,23 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1542
1419
|
details.push(detail);
|
|
1543
1420
|
const nonEnglishSpan =
|
|
1544
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));
|
|
1545
1434
|
if (!plain && !nonEnglishSpan) continue;
|
|
1546
1435
|
const marking = [
|
|
1547
1436
|
...(lang === undefined ? [] : [`lang=${lang}`]),
|
|
1548
1437
|
...(font === undefined ? [] : [`font=${font}`]),
|
|
1549
|
-
...(
|
|
1438
|
+
...(enc === undefined ? [] : [`enc=${enc}`]),
|
|
1550
1439
|
].join(" ");
|
|
1551
1440
|
const before = errors.length;
|
|
1552
1441
|
|
|
@@ -1577,16 +1466,8 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1577
1466
|
}
|
|
1578
1467
|
if (plain && errors.length === before)
|
|
1579
1468
|
plainInline(inline, from, marking, detail, detail.holes);
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
outsideHoles(from, to, detail.holes),
|
|
1583
|
-
detail.emphasis,
|
|
1584
|
-
);
|
|
1585
|
-
lexical(parts, detail);
|
|
1586
|
-
capitals(parts, detail);
|
|
1587
|
-
writings(parts, detail);
|
|
1588
|
-
covered(parts, detail);
|
|
1589
|
-
}
|
|
1469
|
+
detail.checkRoman = errors.length === before;
|
|
1470
|
+
if (named && detail.checkRoman) detail.checkText = true;
|
|
1590
1471
|
continue;
|
|
1591
1472
|
}
|
|
1592
1473
|
|
|
@@ -1647,13 +1528,261 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1647
1528
|
for (let line = first; line < last; line++)
|
|
1648
1529
|
plainInline(lines[line], starts[line], marking, detail, detail.holes);
|
|
1649
1530
|
}
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|
+
);
|
|
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;
|
|
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
|
+
}
|
|
1656
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;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
for (const reference of rawReferences) {
|
|
1740
|
+
const font = reference.font;
|
|
1741
|
+
roman([[reference.from, reference.to]], {
|
|
1742
|
+
run: {
|
|
1743
|
+
lang: reference.lang,
|
|
1744
|
+
|
|
1745
|
+
...reference.where,
|
|
1746
|
+
},
|
|
1747
|
+
face: font === undefined ? undefined : faces.get(font),
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
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=`");
|
|
1657
1786
|
}
|
|
1658
1787
|
|
|
1659
1788
|
const order = (a, b) => a.line - b.line || a.column - b.column;
|
|
@@ -1664,21 +1793,44 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1664
1793
|
return { runs, details, errors, warnings, spans, divs, poems };
|
|
1665
1794
|
}
|
|
1666
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
|
+
|
|
1667
1818
|
/**
|
|
1668
1819
|
* Find the spans, divs and verse fences marked with `lang=`, `font=` or
|
|
1669
|
-
* `
|
|
1820
|
+
* `enc=`, and every fault in their markup and their text.
|
|
1670
1821
|
*
|
|
1671
1822
|
* @param {string} source - Authored Markdown: a note body, or a whole file
|
|
1672
1823
|
* whose frontmatter is blanked.
|
|
1673
1824
|
* @param {RunContext} [ctx] - The configured languages, the installed faces
|
|
1674
|
-
* and the
|
|
1675
|
-
*
|
|
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.
|
|
1676
1827
|
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Session
|
|
1677
1828
|
* scanner projections, so the source's spans and blocks are scanned once.
|
|
1678
|
-
* @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}>}}
|
|
1679
1830
|
* The runs in source order, and each finding at its 1-based position. A
|
|
1680
1831
|
* warning is a `lang=` read as a tag that sits within one edit of a
|
|
1681
|
-
* declared name.
|
|
1832
|
+
* declared name. Diagnostic columns count Unicode code points; each run's
|
|
1833
|
+
* source offsets and column remain UTF-16 units.
|
|
1682
1834
|
*/
|
|
1683
1835
|
export function scanRuns(source, ctx = {}, syntax) {
|
|
1684
1836
|
const text = String(source ?? "");
|
|
@@ -1693,9 +1845,9 @@ export function scanRuns(source, ctx = {}, syntax) {
|
|
|
1693
1845
|
const merged = (found, extra) =>
|
|
1694
1846
|
[...found, ...extra].sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1695
1847
|
return {
|
|
1696
|
-
|
|
1697
|
-
errors: merged(authored.errors, metre.errors),
|
|
1698
|
-
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)),
|
|
1699
1851
|
};
|
|
1700
1852
|
}
|
|
1701
1853
|
|
|
@@ -1745,15 +1897,29 @@ const SCRIPT_BLOCK_CLASS = "script-block";
|
|
|
1745
1897
|
* @returns {string|undefined} The text between the braces.
|
|
1746
1898
|
*/
|
|
1747
1899
|
function rewrittenAttributes(detail, search) {
|
|
1748
|
-
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
|
+
}
|
|
1749
1910
|
const changes =
|
|
1750
1911
|
own.classes.includes(NATIVEFONT_CLASS) ||
|
|
1751
1912
|
run.font !== undefined ||
|
|
1752
|
-
run.
|
|
1913
|
+
run.enc !== undefined ||
|
|
1753
1914
|
(run.tag !== undefined &&
|
|
1754
1915
|
(keys.lang !== run.tag || run.kind === "verse" || run.kind === "scroll"));
|
|
1755
1916
|
if (!changes) return undefined;
|
|
1756
|
-
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
|
+
];
|
|
1757
1923
|
const values = [...own.values];
|
|
1758
1924
|
if (face) {
|
|
1759
1925
|
classes.push(SCRIPT_CLASS);
|
|
@@ -1762,7 +1928,7 @@ function rewrittenAttributes(detail, search) {
|
|
|
1762
1928
|
if (run.kind !== "span" && face.direction === "rtl") values.push(["dir", "rtl"]);
|
|
1763
1929
|
if (search) values.push(["data-pagefind-ignore", "true"]);
|
|
1764
1930
|
} else if (generic) classes.push(generic);
|
|
1765
|
-
if (run.
|
|
1931
|
+
if (run.enc !== undefined && run.enc !== "default" && !encoding) return undefined;
|
|
1766
1932
|
return attributeText({ lang: run.tag, id: own.id, classes, values });
|
|
1767
1933
|
}
|
|
1768
1934
|
|
|
@@ -1800,6 +1966,37 @@ function emphasisTag(delimiter, conventions, book) {
|
|
|
1800
1966
|
* @returns {string} The set text.
|
|
1801
1967
|
*/
|
|
1802
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
|
+
}
|
|
1803
2000
|
let out = "";
|
|
1804
2001
|
let at = from;
|
|
1805
2002
|
for (const delimiter of detail.emphasis) {
|
|
@@ -1807,24 +2004,25 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
1807
2004
|
if (delimiter.offset > at)
|
|
1808
2005
|
out += setText(
|
|
1809
2006
|
text.slice(at, delimiter.offset),
|
|
1810
|
-
detail.
|
|
2007
|
+
detail.selection,
|
|
1811
2008
|
at === from && lineStart,
|
|
1812
2009
|
).text;
|
|
1813
2010
|
out += emphasisTag(delimiter, detail.conventions, book);
|
|
1814
2011
|
at = delimiter.offset + delimiter.length;
|
|
1815
2012
|
}
|
|
1816
|
-
if (at < to)
|
|
2013
|
+
if (at < to)
|
|
2014
|
+
out += setText(text.slice(at, to), detail.selection, at === from && lineStart).text;
|
|
1817
2015
|
return out;
|
|
1818
2016
|
}
|
|
1819
2017
|
|
|
1820
2018
|
/**
|
|
1821
|
-
* Rewrite text marked with `lang=`, `font=` and `
|
|
2019
|
+
* Rewrite text marked with `lang=`, `font=` and `enc=` into the plain span,
|
|
1822
2020
|
* div and verse grammar the rest of each chain renders, one line for one line.
|
|
1823
2021
|
*
|
|
1824
2022
|
* - **Numbered verse prefixes and indentation markers** stay outside run text.
|
|
1825
2023
|
* Each authored run chooses its own language and presentation; physical
|
|
1826
2024
|
* lines and source positions remain unchanged.
|
|
1827
|
-
* - **The
|
|
2025
|
+
* - **The enc, if any, is applied first.** Characters Markdown would read are
|
|
1828
2026
|
* then backslash-escaped, so they reach every later pass as characters.
|
|
1829
2027
|
* - **Emphasis in a face** becomes an `<em>` or a `<strong>` carrying its
|
|
1830
2028
|
* convention's class, or no class where none is stated. The book reads
|
|
@@ -1838,19 +2036,19 @@ function setRange(text, from, to, detail, lineStart, book) {
|
|
|
1838
2036
|
* mark, `<bdo>` or `<bdi>` is ever written: the code points carry their own
|
|
1839
2037
|
* direction.
|
|
1840
2038
|
* - **A scroll fence's body** is rewritten to one line per paragraph, its
|
|
1841
|
-
* lines joined by what the
|
|
2039
|
+
* lines joined by what the enc writes for a space, and left unescaped,
|
|
1842
2040
|
* since the body is raw. Its layout, each character with the source text it
|
|
1843
2041
|
* was written from, is returned for the book, which draws it.
|
|
1844
2042
|
*
|
|
1845
2043
|
* A run with a finding is left as written; {@link scanRuns} reports it.
|
|
1846
2044
|
*
|
|
1847
2045
|
* @param {string} source - Authored Markdown: a note body.
|
|
1848
|
-
* @param {RunContext} [ctx] - The configured languages, faces and
|
|
2046
|
+
* @param {RunContext} [ctx] - The configured languages, faces and encodings.
|
|
1849
2047
|
* @param {import("./content-session.mjs").ContentSyntax} [syntax] - Session
|
|
1850
2048
|
* scanner projections.
|
|
1851
2049
|
* @param {{target?: "site"|"foundry"|"book"}} [options] - The target the
|
|
1852
2050
|
* Markdown is rendered for; only `site` writes the search marker.
|
|
1853
|
-
* @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>}}
|
|
1854
2052
|
* The rewritten Markdown, the findings of {@link scanRuns}, and each
|
|
1855
2053
|
* rewritten scroll fence's layout, by its rewritten body.
|
|
1856
2054
|
*/
|
|
@@ -1860,19 +2058,22 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1860
2058
|
const text = String(source ?? "");
|
|
1861
2059
|
/** @type {Map<string, import("./content-scroll.mjs").ScrollLayout>} */
|
|
1862
2060
|
const scrolls = new Map();
|
|
1863
|
-
if (!/\b(?:lang|font|
|
|
2061
|
+
if (!/\b(?:lang|font|enc)=|\bnativefont\b|\.(?:emph|strong)\b|⟦/.test(text))
|
|
1864
2062
|
return { markdown: text, errors: [], scrolls };
|
|
1865
2063
|
const { details, errors } = analyseRuns(text, ctx, syntax);
|
|
1866
|
-
if (!details.length)
|
|
2064
|
+
if (!details.length)
|
|
2065
|
+
return { markdown: text, errors: diagnosticColumns(text, errors), scrolls };
|
|
1867
2066
|
const lines = text.split("\n");
|
|
1868
2067
|
const starts = lineStarts(text);
|
|
1869
2068
|
|
|
1870
2069
|
/** Whether a finding sits inside a run. */
|
|
1871
2070
|
const faulted = (run) => {
|
|
2071
|
+
if (run.enc !== "default" && errors.some((error) => error.file)) return true;
|
|
1872
2072
|
const from = (run.line - 1) * 1e6 + run.column;
|
|
1873
2073
|
const end = run.kind === "span" ? positionOf(starts, run.end) : undefined;
|
|
1874
2074
|
const to = end ? (end.line - 1) * 1e6 + end.column : (run.end + 1) * 1e6;
|
|
1875
2075
|
return errors.some((error) => {
|
|
2076
|
+
if (error.file) return false;
|
|
1876
2077
|
const at = (error.line - 1) * 1e6 + error.column;
|
|
1877
2078
|
return at >= from && at < to;
|
|
1878
2079
|
});
|
|
@@ -1889,7 +2090,7 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1889
2090
|
* The body and the layout.
|
|
1890
2091
|
*/
|
|
1891
2092
|
const setScroll = (detail) => {
|
|
1892
|
-
const join = setScrollText(" ", detail.
|
|
2093
|
+
const join = setScrollText(" ", detail.selection);
|
|
1893
2094
|
const texts = [];
|
|
1894
2095
|
const paragraphs = [];
|
|
1895
2096
|
for (const [top, past] of paragraphRanges(lines, detail.run.start + 1, detail.run.end)) {
|
|
@@ -1900,7 +2101,9 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1900
2101
|
let pending = "";
|
|
1901
2102
|
const emit = (pieces) => {
|
|
1902
2103
|
for (const piece of pieces) {
|
|
1903
|
-
const chars = [...piece.to]
|
|
2104
|
+
const chars = [...piece.to].filter(
|
|
2105
|
+
(char) => !/\p{Default_Ignorable_Code_Point}/u.test(char),
|
|
2106
|
+
);
|
|
1904
2107
|
if (!chars.length) {
|
|
1905
2108
|
pending += piece.from;
|
|
1906
2109
|
continue;
|
|
@@ -1917,7 +2120,13 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1917
2120
|
}
|
|
1918
2121
|
};
|
|
1919
2122
|
const set = (from, to) => {
|
|
1920
|
-
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);
|
|
1921
2130
|
out += result.text;
|
|
1922
2131
|
emit(result.pieces);
|
|
1923
2132
|
};
|
|
@@ -1949,7 +2158,7 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1949
2158
|
|
|
1950
2159
|
for (const detail of details) {
|
|
1951
2160
|
const { run } = detail;
|
|
1952
|
-
if (faulted(run)) continue;
|
|
2161
|
+
if (faulted(run) || (detail.owner && faulted(detail.owner.run))) continue;
|
|
1953
2162
|
const attributes = rewrittenAttributes(detail, search);
|
|
1954
2163
|
if (run.kind === "span") {
|
|
1955
2164
|
const contentEnd = /** @type {number} */ (detail.contentEnd);
|
|
@@ -1977,7 +2186,13 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1977
2186
|
});
|
|
1978
2187
|
continue;
|
|
1979
2188
|
}
|
|
1980
|
-
|
|
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)
|
|
1981
2196
|
edits.push({ from: contentEnd + 2, to: run.end - 1, text: attributes });
|
|
1982
2197
|
if (detail.plain)
|
|
1983
2198
|
for (const [from, to] of detail.reference ?
|
|
@@ -2021,8 +2236,28 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
2021
2236
|
}
|
|
2022
2237
|
}
|
|
2023
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
|
+
|
|
2024
2259
|
let markdown = text;
|
|
2025
2260
|
for (const edit of edits.sort((a, b) => b.from - a.from))
|
|
2026
2261
|
markdown = markdown.slice(0, edit.from) + edit.text + markdown.slice(edit.to);
|
|
2027
|
-
return { markdown, errors, scrolls };
|
|
2262
|
+
return { markdown, errors: diagnosticColumns(text, errors), scrolls };
|
|
2028
2263
|
}
|