@heroiclands/package-build 23.12.8 → 24.0.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 +84 -0
- package/README.md +3 -2
- package/assets/preview/gitea.ini +12 -7
- package/assets/styles/content-base.css +293 -24
- package/bin/content-commands.mjs +370 -213
- package/bin/package-build-preview.mjs +2 -2
- package/bin/package-build.mjs +3 -2
- package/ci/check-banners.mjs +7 -7
- package/config.mjs +21 -6
- package/content-config.mjs +199 -80
- package/docs/api.md +110 -89
- package/docs/authoring/links-and-markup.md +469 -97
- package/docs/commands.md +259 -78
- package/docs/configuration.md +211 -103
- package/docs/content-format.md +4 -4
- package/docs/diagnostics.md +44 -2
- package/docs/getting-started.md +1 -1
- package/docs/guides/icons.md +1 -1
- package/docs/guides/module-project.md +5 -8
- package/docs/guides/release-and-deploy.md +2 -2
- package/docs/guides/site.md +112 -23
- package/docs/guides/system-project.md +1 -1
- package/docs/guides/writing-in-invented-scripts.md +1247 -0
- package/docs/index.md +1 -0
- package/docs/project-setup.md +28 -30
- package/docs/reference/format-details.md +677 -319
- package/docs/reference/note-types.md +47 -10
- package/docs/reference/theme-pages.md +178 -108
- package/docs/source-preview.md +16 -2
- package/engine/anchors.mjs +2 -2
- package/engine/asset-shipping.mjs +22 -10
- package/engine/base-compiler.mjs +62 -104
- package/engine/changelog-lint.mjs +1 -1
- package/engine/code-fences.mjs +3 -3
- package/engine/content-address.mjs +1 -1
- package/engine/content-alerts.mjs +8 -4
- package/engine/content-blocks.mjs +29 -14
- package/engine/content-embeds.mjs +1 -1
- package/engine/content-expand.mjs +376 -0
- package/engine/content-figures.mjs +18 -11
- package/engine/content-format.yaml +8 -0
- package/engine/content-glyphs.mjs +213 -0
- package/engine/content-images.mjs +6 -7
- package/engine/content-index.mjs +1 -1
- package/engine/content-links.mjs +3 -3
- package/engine/content-lint.mjs +1 -1
- package/engine/content-render.mjs +453 -0
- package/engine/content-scripts.mjs +376 -43
- package/engine/content-spans.mjs +6 -3
- package/engine/content-styles.mjs +91 -0
- package/engine/content-symbols.mjs +186 -0
- package/engine/content-verse.mjs +738 -0
- package/engine/core-report.mjs +169 -0
- package/engine/core-vocabulary.mjs +208 -0
- package/engine/dictionary-html.mjs +119 -0
- package/engine/dictionary-rows.mjs +15 -4
- package/engine/field-reference.mjs +5 -5
- package/engine/first-use.mjs +17 -27
- package/engine/frontmatter-lint.mjs +1 -1
- package/engine/heading-attributes.mjs +89 -0
- package/engine/helpers.mjs +61 -101
- package/engine/homepage.mjs +17 -29
- package/engine/html-minify.mjs +70 -0
- package/engine/index.mjs +8 -5
- package/engine/key-case-collisions.mjs +136 -0
- package/engine/language-data.mjs +203 -6
- package/engine/language-rows.mjs +8 -2
- package/engine/language-terms.mjs +186 -16
- package/engine/language-words.mjs +12 -0
- package/engine/logographic.mjs +255 -0
- package/engine/map-lexicon.mjs +262 -47
- package/engine/map-places.mjs +5 -4
- package/engine/markdown-expressions.mjs +0 -2
- package/engine/markdownlint-run.mjs +9 -3
- package/engine/markup-validation.mjs +21 -0
- package/engine/metre-declaration.mjs +134 -0
- package/engine/names.mjs +36 -9
- package/engine/note-matter.mjs +66 -0
- package/engine/note-vocabulary.mjs +187 -3
- package/engine/pdf-build.mjs +69 -133
- package/engine/pdf-render.mjs +205 -58
- package/engine/post-fields.mjs +157 -0
- package/engine/preview-index.mjs +130 -0
- package/engine/preview-source.mjs +450 -0
- package/engine/project-init.mjs +5 -12
- package/engine/prose-config.mjs +3 -3
- package/engine/read-stdin.mjs +29 -0
- package/engine/run-rows.mjs +2 -2
- package/engine/run-words.mjs +6 -112
- package/engine/script-emphasis-terms.mjs +38 -0
- package/engine/script-emphasis.mjs +295 -0
- package/engine/script-font-delivery.mjs +4 -4
- package/engine/site-build.mjs +260 -315
- package/engine/site-config.mjs +26 -238
- package/engine/site-files.mjs +175 -0
- package/engine/site-index.mjs +9 -4
- package/engine/site-preview.mjs +52 -525
- package/engine/site-render.mjs +593 -0
- package/engine/site-root.mjs +22 -13
- package/engine/site-search.mjs +161 -0
- package/engine/site-serve.mjs +273 -0
- package/engine/site-theme-banner.mjs +154 -0
- package/engine/site-theme-urls.mjs +125 -0
- package/engine/site-theme.mjs +926 -0
- package/engine/sql-tables.mjs +20 -3
- package/engine/text-maps.mjs +57 -10
- package/engine/typographer.mjs +248 -0
- package/engine/verse-metre.mjs +357 -0
- package/engine/wikilink-syntax.mjs +29 -47
- package/engine/wikilinks.mjs +758 -279
- package/engine/word-forms.mjs +364 -0
- package/engine/word-rows.mjs +12 -2
- package/engine/word-terms.mjs +38 -4
- package/manifest.mjs +3 -4
- package/package.json +11 -8
- package/{hugo-theme → site-theme}/data/banners.yaml +6 -10
- package/site-theme/layouts/base.hbs +31 -0
- package/site-theme/layouts/blog-list.hbs +30 -0
- package/site-theme/layouts/blog-post.hbs +40 -0
- package/site-theme/layouts/home.hbs +35 -0
- package/site-theme/layouts/list.hbs +20 -0
- package/site-theme/layouts/not-found.hbs +31 -0
- package/site-theme/layouts/page.hbs +7 -0
- package/site-theme/partials/copy-buttons.hbs +37 -0
- package/site-theme/partials/draft-notice.hbs +5 -0
- package/site-theme/partials/exturl.hbs +2 -0
- package/{hugo-theme/layouts/partials/footer.html → site-theme/partials/footer.hbs} +10 -7
- package/site-theme/partials/header.hbs +44 -0
- package/site-theme/partials/hero-banner.hbs +11 -0
- package/site-theme/partials/infobox-entry.hbs +2 -0
- package/site-theme/partials/infobox-section.hbs +31 -0
- package/site-theme/partials/infobox-value.hbs +2 -0
- package/site-theme/partials/infobox.hbs +18 -0
- package/site-theme/partials/list-members.hbs +9 -0
- package/site-theme/partials/nav-links.hbs +5 -0
- package/site-theme/partials/page-body.hbs +48 -0
- package/site-theme/partials/page-list.hbs +14 -0
- package/site-theme/partials/related.hbs +24 -0
- package/site-theme/partials/search-meta.hbs +6 -0
- package/site-theme/partials/search-names.hbs +2 -0
- package/site-theme/partials/search.hbs +52 -0
- package/site-theme/partials/tags.hbs +4 -0
- package/site-theme/partials/toc.hbs +7 -0
- package/{hugo-theme → site-theme}/static/css/style.css +106 -14
- package/stage.mjs +1 -2
- package/types/config.d.mts +8 -4
- package/types/content-config.d.mts +15 -15
- package/types/engine/asset-shipping.d.mts +5 -2
- package/types/engine/base-compiler.d.mts +20 -2
- package/types/engine/code-fences.d.mts +3 -3
- package/types/engine/content-address.d.mts +1 -1
- package/types/engine/content-alerts.d.mts +8 -2
- package/types/engine/content-blocks.d.mts +11 -4
- package/types/engine/content-expand.d.mts +288 -0
- package/types/engine/content-figures.d.mts +11 -3
- package/types/engine/content-glyphs.d.mts +85 -0
- package/types/engine/content-images.d.mts +6 -7
- package/types/engine/content-links.d.mts +1 -1
- package/types/engine/content-render.d.mts +199 -0
- package/types/engine/content-scripts.d.mts +70 -4
- package/types/engine/content-spans.d.mts +1 -1
- package/types/engine/content-styles.d.mts +33 -0
- package/types/engine/content-symbols.d.mts +35 -0
- package/types/engine/content-verse.d.mts +240 -0
- package/types/engine/core-report.d.mts +45 -0
- package/types/engine/core-vocabulary.d.mts +53 -0
- package/types/engine/dictionary-html.d.mts +8 -0
- package/types/engine/dictionary-rows.d.mts +10 -0
- package/types/engine/field-reference.d.mts +2 -2
- package/types/engine/heading-attributes.d.mts +20 -0
- package/types/engine/helpers.d.mts +3 -10
- package/types/engine/homepage.d.mts +11 -24
- package/types/engine/html-minify.d.mts +9 -0
- package/types/engine/index.d.mts +2 -1
- package/types/engine/key-case-collisions.d.mts +28 -0
- package/types/engine/language-data.d.mts +0 -2
- package/types/engine/language-terms.d.mts +24 -2
- package/types/engine/logographic.d.mts +45 -0
- package/types/engine/map-lexicon.d.mts +66 -13
- package/types/engine/map-places.d.mts +8 -0
- package/types/engine/markdown-expressions.d.mts +0 -2
- package/types/engine/metre-declaration.d.mts +39 -0
- package/types/engine/names.d.mts +34 -3
- package/types/engine/note-matter.d.mts +48 -0
- package/types/engine/pdf-render.d.mts +6 -2
- package/types/engine/post-fields.d.mts +44 -0
- package/types/engine/preview-index.d.mts +72 -0
- package/types/engine/preview-source.d.mts +51 -0
- package/types/engine/read-stdin.d.mts +11 -0
- package/types/engine/run-rows.d.mts +2 -2
- package/types/engine/run-words.d.mts +0 -15
- package/types/engine/script-emphasis-terms.d.mts +21 -0
- package/types/engine/script-emphasis.d.mts +84 -0
- package/types/engine/script-font-delivery.d.mts +4 -4
- package/types/engine/site-build.d.mts +71 -113
- package/types/engine/site-config.d.mts +14 -134
- package/types/engine/site-files.d.mts +73 -0
- package/types/engine/site-index.d.mts +11 -3
- package/types/engine/site-preview.d.mts +15 -19
- package/types/engine/site-render.d.mts +171 -0
- package/types/engine/site-root.d.mts +12 -4
- package/types/engine/site-search.d.mts +54 -0
- package/types/engine/site-serve.d.mts +44 -0
- package/types/engine/site-theme-banner.d.mts +51 -0
- package/types/engine/site-theme-urls.d.mts +60 -0
- package/types/engine/site-theme.d.mts +127 -0
- package/types/engine/sql-tables.d.mts +4 -1
- package/types/engine/text-maps.d.mts +17 -3
- package/types/engine/typographer.d.mts +11 -0
- package/types/engine/verse-metre.d.mts +131 -0
- package/types/engine/wikilink-syntax.d.mts +30 -48
- package/types/engine/wikilinks.d.mts +330 -14
- package/types/engine/word-forms.d.mts +83 -0
- package/types/manifest.d.mts +3 -4
- package/types/stage.d.mts +1 -2
- package/types/vendor/markdownlint.d.mts +8 -0
- package/vendor/markdownlint.mjs +20388 -0
- package/engine/content-poetry.mjs +0 -240
- package/engine/web-wikilinks.mjs +0 -594
- package/hugo-theme/layouts/404.html +0 -85
- package/hugo-theme/layouts/_default/baseof.html +0 -67
- package/hugo-theme/layouts/_default/list.html +0 -139
- package/hugo-theme/layouts/_default/single.html +0 -4
- package/hugo-theme/layouts/blog/list.html +0 -54
- package/hugo-theme/layouts/blog-post/single.html +0 -36
- package/hugo-theme/layouts/index.html +0 -75
- package/hugo-theme/layouts/partials/banner-url.html +0 -141
- package/hugo-theme/layouts/partials/cdn-url.html +0 -36
- package/hugo-theme/layouts/partials/dictionary-lists.html +0 -58
- package/hugo-theme/layouts/partials/draft-notice.html +0 -41
- package/hugo-theme/layouts/partials/draft-state.html +0 -26
- package/hugo-theme/layouts/partials/header.html +0 -35
- package/hugo-theme/layouts/partials/hero-banner.html +0 -85
- package/hugo-theme/layouts/partials/infobox/entry.html +0 -18
- package/hugo-theme/layouts/partials/infobox/items.html +0 -26
- package/hugo-theme/layouts/partials/infobox/section.html +0 -56
- package/hugo-theme/layouts/partials/infobox/value.html +0 -43
- package/hugo-theme/layouts/partials/infobox.html +0 -57
- package/hugo-theme/layouts/partials/page-body.html +0 -152
- package/hugo-theme/layouts/partials/related.html +0 -138
- package/hugo-theme/layouts/partials/search.html +0 -84
- package/hugo-theme/layouts/partials/site-url.html +0 -32
- package/hugo-theme/layouts/partials/tags.html +0 -30
- package/hugo-theme/layouts/partials/toc.html +0 -28
- package/hugo-theme/layouts/partials/warn-missing-banner.html +0 -3
- package/hugo-theme/layouts/shortcodes/cartouche.html +0 -64
- package/hugo-theme/layouts/shortcodes/exturl.html +0 -12
- package/hugo-theme/layouts/shortcodes/glyph.html +0 -43
- package/hugo-theme/layouts/shortcodes/pagelist.html +0 -63
- package/hugo-theme/theme.toml +0 -11
- package/types/engine/content-poetry.d.mts +0 -58
- package/types/engine/web-wikilinks.d.mts +0 -107
- /package/{hugo-theme → site-theme}/data/banner-kinds.yaml +0 -0
- /package/{hugo-theme → site-theme}/static/css/sohl-icons.css +0 -0
- /package/{hugo-theme → site-theme}/static/fonts/game-icons-kb.woff2 +0 -0
- /package/{hugo-theme → site-theme}/static/img/banners/takheperu-hieroglyphs.svg +0 -0
- /package/{hugo-theme → site-theme}/static/img/cartouches/takheperu.svg +0 -0
- /package/{hugo-theme → site-theme}/static/js/inline-spoilers.js +0 -0
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* **Text marked with a language, a font and a mapping.** Three keys on the
|
|
16
|
-
* attribute grammar spans, divs and
|
|
16
|
+
* attribute grammar spans, divs and verse fences already share, each optional
|
|
17
17
|
* and independent of the others:
|
|
18
18
|
*
|
|
19
19
|
* ```markdown
|
|
20
20
|
* [Daimela, helthos.]{lang=vyl}
|
|
21
|
-
* [Zaron tholmeza]{lang=vyl font=semaron-kuriale map=
|
|
21
|
+
* [Zaron tholmeza]{lang=vyl font=semaron-kuriale map=semaron}
|
|
22
22
|
*
|
|
23
|
-
* ::: {lang=vyl font=semaron-kuriale map=
|
|
23
|
+
* ::: {lang=vyl font=semaron-kuriale map=semaron}
|
|
24
24
|
* Zaron tholmeza.
|
|
25
25
|
* :::
|
|
26
26
|
*
|
|
27
|
-
* ```
|
|
27
|
+
* ```verse {form=epic lang=vyl font=semaron-kuriale map=semaron}
|
|
28
28
|
* ```
|
|
29
29
|
*
|
|
30
30
|
* - **`lang=`** gives the language tag and changes nothing else. It is a name
|
|
@@ -39,17 +39,25 @@
|
|
|
39
39
|
* - **`lang=` with `map=` reads the language's lexicon first**: a word whose
|
|
40
40
|
* entry states a native form for the map is written by it, and the rows
|
|
41
41
|
* spell every other word; see {@link module:engine/map-lexicon}.
|
|
42
|
+
* - **A logographic map writes every word from the lexicon**: by the word's
|
|
43
|
+
* own writing or its parts', and a word with neither, or whose form stands
|
|
44
|
+
* for entries written differently, is a finding at the word, once per run;
|
|
45
|
+
* see {@link module:engine/logographic}.
|
|
42
46
|
* - **Every word of a run with `lang=` and `map=` is a word of its language**
|
|
43
47
|
* where the language's `note` names its language note: a lexicon form, a
|
|
44
48
|
* word of a name its own register lists, or a regular form of a lexicon
|
|
45
49
|
* form. Any other
|
|
46
50
|
* word is a finding at its first character, once per run; see
|
|
47
51
|
* {@link module:engine/run-words}.
|
|
48
|
-
* - **A run inside a div or a
|
|
52
|
+
* - **A run inside a div or a verse fence inherits its keys**, and its own
|
|
49
53
|
* keys override them.
|
|
50
54
|
* - **Text with a font, a map or a declared language is plain.** A span or a
|
|
51
55
|
* verse holds no emphasis, link, image, footnote, wikilink, code or HTML,
|
|
52
56
|
* and no span but a marked one; a div holds paragraphs and line breaks only.
|
|
57
|
+
* - **Text set in a face takes `*…*` and `**…**`**, set by the convention its
|
|
58
|
+
* language's note states, else its face's script note, else plainly; see
|
|
59
|
+
* {@link module:engine/script-emphasis}. A language that marks emphasis
|
|
60
|
+
* with a word makes either a finding at the emphasis.
|
|
53
61
|
* The text is read as written, so `[`, `]`, `"`, `<<`, `>>`, `!` and
|
|
54
62
|
* `@#$%^&` are characters, and need no escaping. A real language's tag alone
|
|
55
63
|
* marks text and leaves its Markdown alone.
|
|
@@ -59,7 +67,7 @@
|
|
|
59
67
|
*
|
|
60
68
|
* {@link scanRuns} reads the markup through {@link module:engine/content-spans.scanSpans},
|
|
61
69
|
* {@link module:engine/content-blocks.scanBlocks} and
|
|
62
|
-
* {@link module:engine/content-
|
|
70
|
+
* {@link module:engine/content-verse.scanVerse}, so what counts as a span, a
|
|
63
71
|
* div or a fence is theirs to say.
|
|
64
72
|
*
|
|
65
73
|
* @module
|
|
@@ -68,18 +76,30 @@
|
|
|
68
76
|
import MarkdownIt from "markdown-it";
|
|
69
77
|
|
|
70
78
|
import { scanBlocks, BLOCK_CONTAINERS } from "./content-blocks.mjs";
|
|
71
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
COLUMN_KEYS,
|
|
81
|
+
COLUMNS_CLASS,
|
|
82
|
+
expandVerse,
|
|
83
|
+
expandVerseOrigins,
|
|
84
|
+
scanVerse,
|
|
85
|
+
sourcePosition,
|
|
86
|
+
} from "./content-verse.mjs";
|
|
72
87
|
import { scanSpans } from "./content-spans.mjs";
|
|
73
88
|
import { isLanguageTag } from "./language-tag.mjs";
|
|
74
89
|
import { distance, nearest } from "./near-miss.mjs";
|
|
90
|
+
import { emphasesFromRecords, languageEmphasesOf } from "./script-emphasis.mjs";
|
|
91
|
+
import { EMPHASIS_CLASSES } from "./script-emphasis-terms.mjs";
|
|
75
92
|
import { FONTS_PACKAGE, resolveScriptFonts } from "./script-fonts.mjs";
|
|
76
93
|
import {
|
|
77
94
|
fontProblem,
|
|
95
|
+
languageRecordsByName,
|
|
78
96
|
lexiconsFromRecords,
|
|
79
97
|
mappingFor,
|
|
80
98
|
uncapitalisedMessage,
|
|
99
|
+
unwrittenMessage,
|
|
81
100
|
} from "./map-lexicon.mjs";
|
|
82
101
|
import { unknownWordMessage, unknownWords, wordBooksFromRecords } from "./run-words.mjs";
|
|
102
|
+
import { metreBooksFromRecords, metreFindings, metresFromRecords } from "./verse-metre.mjs";
|
|
83
103
|
import { applyMap, mapsFromRecords } from "./text-maps.mjs";
|
|
84
104
|
|
|
85
105
|
/** The keys this module reads, which no construct passes through as written. */
|
|
@@ -113,15 +133,26 @@ const CONFIG = "package-build.config.yaml";
|
|
|
113
133
|
* @property {ReadonlyMap<string, import("./run-words.mjs").WordBook>} [wordBooks] -
|
|
114
134
|
* What each configured language's mapped runs are checked against, from
|
|
115
135
|
* {@link module:engine/run-words.wordBooksFromRecords}.
|
|
136
|
+
* @property {ReadonlyMap<string, import("./metre-declaration.mjs").Metre>} [metres] -
|
|
137
|
+
* The metres the `doc` notes declare, by name.
|
|
138
|
+
* @property {ReadonlyMap<string, import("./verse-metre.mjs").MetreBook>} [metreBooks] -
|
|
139
|
+
* What each configured language's verse is counted by, from
|
|
140
|
+
* {@link module:engine/verse-metre.metreBooksFromRecords}.
|
|
141
|
+
* @property {ReadonlyMap<string, import("./script-emphasis.mjs").Emphasis>} [emphases] -
|
|
142
|
+
* Each face's emphasis conventions, from the script notes, by
|
|
143
|
+
* {@link module:engine/script-emphasis.emphasesFromRecords}.
|
|
144
|
+
* @property {ReadonlyMap<string, import("./script-emphasis.mjs").LanguageEmphasis>} [languageEmphases] -
|
|
145
|
+
* The convention each configured language's note states, by language name,
|
|
146
|
+
* from {@link module:engine/script-emphasis.languageEmphasesOf}.
|
|
116
147
|
*/
|
|
117
148
|
|
|
118
149
|
/**
|
|
119
|
-
* One span, div or
|
|
150
|
+
* One span, div or verse fence carrying `lang=`, `font=` or `map=`. Its
|
|
120
151
|
* `lang`, `font` and `map` are the ones it is set with: its own, else the
|
|
121
152
|
* innermost div or fence around it that states one.
|
|
122
153
|
*
|
|
123
154
|
* @typedef {object} Run
|
|
124
|
-
* @property {"span"|"block"|"
|
|
155
|
+
* @property {"span"|"block"|"verse"} kind - Which construct.
|
|
125
156
|
* @property {string|undefined} lang - The language as written.
|
|
126
157
|
* @property {string|undefined} tag - The BCP 47 tag it resolves to; `undefined`
|
|
127
158
|
* when it resolves to none.
|
|
@@ -181,6 +212,10 @@ export function runContext(config, records) {
|
|
|
181
212
|
maps: mapsFromRecords(records),
|
|
182
213
|
lexicons: lexiconsFromRecords(records, config),
|
|
183
214
|
wordBooks: wordBooksFromRecords(records, config),
|
|
215
|
+
metres: metresFromRecords(records),
|
|
216
|
+
metreBooks: metreBooksFromRecords(records, config),
|
|
217
|
+
emphases: emphasesFromRecords(records),
|
|
218
|
+
languageEmphases: languageEmphasesOf(languageRecordsByName(records, config)),
|
|
184
219
|
});
|
|
185
220
|
byRecords.set(records, context);
|
|
186
221
|
return context;
|
|
@@ -205,7 +240,7 @@ function withinOneEdit(a, b) {
|
|
|
205
240
|
}
|
|
206
241
|
|
|
207
242
|
/** The word each construct is called in a finding. */
|
|
208
|
-
const CONSTRUCT = Object.freeze({ span: "span", block: "div",
|
|
243
|
+
const CONSTRUCT = Object.freeze({ span: "span", block: "div", verse: "verse fence" });
|
|
209
244
|
|
|
210
245
|
/** Line starts of a source, for offsets to positions. */
|
|
211
246
|
function lineStarts(text) {
|
|
@@ -298,9 +333,11 @@ function closing(text, open) {
|
|
|
298
333
|
* @param {ReadonlyMap<number, number>} [holes] - Marked spans inside the
|
|
299
334
|
* text, each its offset of `[` to the offset past its attributes. They are
|
|
300
335
|
* runs of their own and checked as such.
|
|
336
|
+
* @param {boolean} [stars] - Whether `*…*`, `_…_`, `**…**` and `__…__` are
|
|
337
|
+
* emphasis the text may hold, as in a run in a script face; `~~` is not.
|
|
301
338
|
* @returns {Array<{offset: number, what: string}>} Each fault, in order.
|
|
302
339
|
*/
|
|
303
|
-
function inlineMarkup(text, footnotes, holes = new Map()) {
|
|
340
|
+
function inlineMarkup(text, footnotes, holes = new Map(), stars = false) {
|
|
304
341
|
const faults = [];
|
|
305
342
|
const skip = new Map();
|
|
306
343
|
const runs = [];
|
|
@@ -384,6 +421,10 @@ function inlineMarkup(text, footnotes, holes = new Map()) {
|
|
|
384
421
|
if (char === "*" || char === "_" || (char === "~" && text[i + 1] === "~")) {
|
|
385
422
|
let j = i;
|
|
386
423
|
while (text[j] === char) j++;
|
|
424
|
+
if (stars && char !== "~") {
|
|
425
|
+
i = j - 1;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
387
428
|
const before = text[i - 1] ?? " ",
|
|
388
429
|
after = text[j] ?? " ";
|
|
389
430
|
const left =
|
|
@@ -401,7 +442,7 @@ function inlineMarkup(text, footnotes, holes = new Map()) {
|
|
|
401
442
|
i = j - 1;
|
|
402
443
|
}
|
|
403
444
|
}
|
|
404
|
-
let outside = text;
|
|
445
|
+
let outside = stars ? text.replace(/[*_]/g, " ") : text;
|
|
405
446
|
for (const [from, to] of holes)
|
|
406
447
|
outside = outside.slice(0, from) + " ".repeat(to - from) + outside.slice(to);
|
|
407
448
|
if (runs.length && holdsEmphasis(outside)) {
|
|
@@ -422,6 +463,91 @@ function inlineMarkup(text, footnotes, holes = new Map()) {
|
|
|
422
463
|
return faults.sort((a, b) => a.offset - b.offset);
|
|
423
464
|
}
|
|
424
465
|
|
|
466
|
+
/** A parser that reads emphasis and nothing else in text whose other punctuation is escaped. */
|
|
467
|
+
const starParser = new MarkdownIt({ html: false, linkify: false, typographer: false });
|
|
468
|
+
|
|
469
|
+
/** ASCII punctuation, which a backslash escapes. */
|
|
470
|
+
const ASCII_PUNCTUATION = /[!-/:-@[-`{-~]/;
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* One `*`, `_`, `**` or `__` that Markdown reads as opening or closing emphasis.
|
|
474
|
+
*
|
|
475
|
+
* @typedef {object} Delimiter
|
|
476
|
+
* @property {number} offset - Its first character's offset.
|
|
477
|
+
* @property {number} length - 1 for `*` or `_`, 2 for `**` or `__`.
|
|
478
|
+
* @property {"em"|"strong"} tag - Which level it opens or closes.
|
|
479
|
+
* @property {boolean} open - Whether it opens.
|
|
480
|
+
*/
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The stars and underscores Markdown reads as emphasis in a run's text, as
|
|
484
|
+
* CommonMark pairs them. Every other character is read as itself: an escaped
|
|
485
|
+
* delimiter, or one that pairs with none, is a character of the text. A marked span inside the text
|
|
486
|
+
* reads as one letter, so emphasis may enclose it.
|
|
487
|
+
*
|
|
488
|
+
* @param {string} text - Inline text, as written.
|
|
489
|
+
* @param {ReadonlyMap<number, number>} [holes] - Marked spans inside it, by
|
|
490
|
+
* offset in `text`.
|
|
491
|
+
* @returns {Delimiter[]} Each delimiter, in order, at its offset in `text`.
|
|
492
|
+
*/
|
|
493
|
+
function emphasisDelimiters(text, holes = new Map()) {
|
|
494
|
+
let masked = "";
|
|
495
|
+
const stars = [];
|
|
496
|
+
for (let i = 0; i < text.length; i++) {
|
|
497
|
+
if (holes.has(i)) {
|
|
498
|
+
masked += "x";
|
|
499
|
+
i = /** @type {number} */ (holes.get(i)) - 1;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const char = text[i];
|
|
503
|
+
if (char === "\\" && ASCII_PUNCTUATION.test(text[i + 1] ?? "")) {
|
|
504
|
+
masked += text[i + 1] === "*" || text[i + 1] === "_" ? "\\!" : `\\${text[i + 1]}`;
|
|
505
|
+
i++;
|
|
506
|
+
} else if (char === "*" || char === "_") {
|
|
507
|
+
masked += char;
|
|
508
|
+
stars.push(i);
|
|
509
|
+
} else masked += ASCII_PUNCTUATION.test(char) ? `\\${char}` : char;
|
|
510
|
+
}
|
|
511
|
+
if (!stars.length) return [];
|
|
512
|
+
const delimiters = [];
|
|
513
|
+
let at = 0;
|
|
514
|
+
for (const token of starParser.parseInline(masked, {})[0]?.children ?? []) {
|
|
515
|
+
if (token.type === "text") at += (token.content.match(/[*_]/g) ?? []).length;
|
|
516
|
+
else if (/^(?:em|strong)_(?:open|close)$/.test(token.type)) {
|
|
517
|
+
delimiters.push({
|
|
518
|
+
offset: stars[at],
|
|
519
|
+
length: token.markup.length,
|
|
520
|
+
tag: /** @type {"em"|"strong"} */ (token.tag),
|
|
521
|
+
open: token.nesting === 1,
|
|
522
|
+
});
|
|
523
|
+
at += token.markup.length;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return delimiters;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The parts of ranges of text that no delimiter covers.
|
|
531
|
+
*
|
|
532
|
+
* @param {Array<[number, number]>} parts - The ranges, as offsets.
|
|
533
|
+
* @param {readonly Delimiter[]} delimiters - Delimiters, by absolute offset.
|
|
534
|
+
* @returns {Array<[number, number]>} The ranges, split around each delimiter.
|
|
535
|
+
*/
|
|
536
|
+
function outsideDelimiters(parts, delimiters) {
|
|
537
|
+
if (!delimiters.length) return parts;
|
|
538
|
+
const out = [];
|
|
539
|
+
for (const [from, to] of parts) {
|
|
540
|
+
let at = from;
|
|
541
|
+
for (const delimiter of delimiters) {
|
|
542
|
+
if (delimiter.offset < at || delimiter.offset >= to) continue;
|
|
543
|
+
if (delimiter.offset > at) out.push([at, delimiter.offset]);
|
|
544
|
+
at = delimiter.offset + delimiter.length;
|
|
545
|
+
}
|
|
546
|
+
if (at < to) out.push([at, to]);
|
|
547
|
+
}
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
|
|
425
551
|
/** A line split into its indentation, its text, a trailing break and its trailing space. */
|
|
426
552
|
const LINE_PARTS = /^(\s*)(.*?)(\\?)(\s*)$/;
|
|
427
553
|
|
|
@@ -474,7 +600,7 @@ function unescapeRoman(core) {
|
|
|
474
600
|
* @param {import("./map-lexicon.mjs").Mapping|undefined} mapping - The run's
|
|
475
601
|
* map, face and lexicon.
|
|
476
602
|
* @param {boolean} lineStart - Whether `raw` starts its line.
|
|
477
|
-
* @returns {{text: string, pieces: Array<{offset: number, from: string, to: string, row: boolean, native?: boolean, uncapitalised?: string}>}}
|
|
603
|
+
* @returns {{text: string, pieces: Array<{offset: number, from: string, to: string, row: boolean, native?: boolean, uncapitalised?: string, unwritten?: object}>}}
|
|
478
604
|
* The set text, and each piece of it with the offset in `raw` it came from.
|
|
479
605
|
*/
|
|
480
606
|
function setText(raw, mapping, lineStart) {
|
|
@@ -491,6 +617,7 @@ function setText(raw, mapping, lineStart) {
|
|
|
491
617
|
const mapped = applyMap(mapping?.map, plain.text, {
|
|
492
618
|
font: mapping?.font,
|
|
493
619
|
native: mapping?.native,
|
|
620
|
+
writing: mapping?.writing,
|
|
494
621
|
});
|
|
495
622
|
for (const piece of mapped.pieces)
|
|
496
623
|
pieces.push({
|
|
@@ -502,6 +629,7 @@ function setText(raw, mapping, lineStart) {
|
|
|
502
629
|
...(piece.uncapitalised === undefined ?
|
|
503
630
|
{}
|
|
504
631
|
: { uncapitalised: piece.uncapitalised }),
|
|
632
|
+
...(piece.unwritten === undefined ? {} : { unwritten: piece.unwritten }),
|
|
505
633
|
});
|
|
506
634
|
out.push(
|
|
507
635
|
lead +
|
|
@@ -574,7 +702,40 @@ const BLOCK_WHAT = Object.freeze({
|
|
|
574
702
|
* @property {[number, number]|undefined} body - A div's or fence's body, as
|
|
575
703
|
* offsets: the first body line's start to the end of the last.
|
|
576
704
|
* @property {number|undefined} contentEnd - A span's offset of its closing `]`.
|
|
705
|
+
* @property {import("./script-emphasis.mjs").LanguageEmphasis|undefined} languageEmphasis -
|
|
706
|
+
* The convention its language's note states.
|
|
707
|
+
* @property {import("./script-emphasis.mjs").Emphasis|undefined} conventions -
|
|
708
|
+
* What its emphasis is set by: its language's convention, else its face's
|
|
709
|
+
* script's; `undefined` sets it plain.
|
|
710
|
+
* @property {Delimiter[]} emphasis - The emphasis it holds, by absolute offset.
|
|
711
|
+
*/
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* The finding for emphasis in a language that marks it with a word.
|
|
715
|
+
*
|
|
716
|
+
* @param {string} lang - The language, as `lang=` names it.
|
|
717
|
+
* @returns {string} The message.
|
|
718
|
+
*/
|
|
719
|
+
function wordsMessage(lang) {
|
|
720
|
+
return `\`${lang}\` marks emphasis with a word, not a mark; write the word`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* What a run's emphasis is set by: the convention its language's note states,
|
|
725
|
+
* else the one its face's script note states, else none. A language that
|
|
726
|
+
* marks emphasis with a word sets none, and its emphasis is a finding.
|
|
727
|
+
*
|
|
728
|
+
* @param {{face: import("./script-fonts.mjs").Face|undefined, languageEmphasis: string|undefined}} detail -
|
|
729
|
+
* The run.
|
|
730
|
+
* @param {RunContext} ctx - The conventions.
|
|
731
|
+
* @returns {import("./script-emphasis.mjs").Emphasis|undefined} The conventions.
|
|
577
732
|
*/
|
|
733
|
+
function conventionsOf({ face, languageEmphasis }, ctx) {
|
|
734
|
+
if (!face || languageEmphasis === "words") return undefined;
|
|
735
|
+
if (languageEmphasis === "rubric" || languageEmphasis === "overline")
|
|
736
|
+
return { em: languageEmphasis, strong: languageEmphasis };
|
|
737
|
+
return ctx.emphases?.get(face.slug);
|
|
738
|
+
}
|
|
578
739
|
|
|
579
740
|
/**
|
|
580
741
|
* Read every marked construct in a source, and every fault in it.
|
|
@@ -645,7 +806,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
645
806
|
for (const key of Object.keys(keys))
|
|
646
807
|
report(
|
|
647
808
|
at(key, "key"),
|
|
648
|
-
`\`${key}=\` is read on a span, a div or a
|
|
809
|
+
`\`${key}=\` is read on a span, a div or a verse fence, not on a ${block.name} block`,
|
|
649
810
|
);
|
|
650
811
|
} else {
|
|
651
812
|
divs.set(first, block);
|
|
@@ -679,13 +840,13 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
679
840
|
collect(0, text);
|
|
680
841
|
|
|
681
842
|
const poems = new Map();
|
|
682
|
-
for (const poem of
|
|
843
|
+
for (const poem of scanVerse(text).blocks) {
|
|
683
844
|
poems.set(poem.start, poem);
|
|
684
845
|
const keys = runKeys(poem.attributes);
|
|
685
846
|
if (!Object.keys(keys).length) continue;
|
|
686
847
|
const opening = lines[poem.start];
|
|
687
848
|
constructs.push({
|
|
688
|
-
kind: "
|
|
849
|
+
kind: "verse",
|
|
689
850
|
keys,
|
|
690
851
|
node: poem,
|
|
691
852
|
line0: poem.start,
|
|
@@ -790,25 +951,44 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
790
951
|
);
|
|
791
952
|
|
|
792
953
|
/**
|
|
793
|
-
* Report the markup in inline text that must be plain
|
|
954
|
+
* Report the markup in inline text that must be plain, and collect the
|
|
955
|
+
* emphasis a run in a script face holds.
|
|
794
956
|
*
|
|
795
957
|
* @param {string} inline - The text.
|
|
796
958
|
* @param {number} offset - Its offset in the source.
|
|
797
959
|
* @param {string} marking - How its construct is marked.
|
|
960
|
+
* @param {RunDetail} detail - The run, whose `emphasis` collects each
|
|
961
|
+
* delimiter by absolute offset.
|
|
798
962
|
* @param {ReadonlyMap<number, number>} [holes] - Marked spans in it, by
|
|
799
963
|
* absolute offset.
|
|
800
964
|
*/
|
|
801
|
-
const plainInline = (inline, offset, marking, holes = new Map()) => {
|
|
965
|
+
const plainInline = (inline, offset, marking, detail, holes = new Map()) => {
|
|
802
966
|
const relative = new Map(
|
|
803
967
|
[...holes]
|
|
804
968
|
.filter(([start]) => start >= offset && start < offset + inline.length)
|
|
805
969
|
.map(([start, end]) => [start - offset, end - offset]),
|
|
806
970
|
);
|
|
807
|
-
|
|
971
|
+
const { lang } = detail.run;
|
|
972
|
+
const words = detail.languageEmphasis === "words";
|
|
973
|
+
const stars = words || detail.face !== undefined;
|
|
974
|
+
for (const fault of inlineMarkup(inline, footnotes, relative, stars))
|
|
808
975
|
report(
|
|
809
976
|
positionOf(starts, offset + fault.offset),
|
|
810
|
-
|
|
977
|
+
fault.what !== "emphasis" || !stars ?
|
|
978
|
+
`text marked \`${marking}\` is plain text and holds no ${fault.what}`
|
|
979
|
+
: words ? wordsMessage(/** @type {string} */ (lang))
|
|
980
|
+
: `text marked \`${marking}\` takes emphasis only as \`*…*\`, \`_…_\`, \`**…**\` or \`__…__\``,
|
|
811
981
|
);
|
|
982
|
+
if (!stars) return;
|
|
983
|
+
for (const delimiter of emphasisDelimiters(inline, relative)) {
|
|
984
|
+
if (words) {
|
|
985
|
+
if (delimiter.open)
|
|
986
|
+
report(
|
|
987
|
+
positionOf(starts, offset + delimiter.offset),
|
|
988
|
+
wordsMessage(/** @type {string} */ (lang)),
|
|
989
|
+
);
|
|
990
|
+
} else detail.emphasis.push({ ...delimiter, offset: offset + delimiter.offset });
|
|
991
|
+
}
|
|
812
992
|
};
|
|
813
993
|
|
|
814
994
|
/**
|
|
@@ -825,6 +1005,8 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
825
1005
|
for (const [from, to] of parts) {
|
|
826
1006
|
const lineStart = from === 0 || text[from - 1] === "\n";
|
|
827
1007
|
for (const piece of setText(text.slice(from, to), mapping, lineStart).pieces) {
|
|
1008
|
+
// A word with no writing is its own finding, not a face's.
|
|
1009
|
+
if (piece.unwritten) continue;
|
|
828
1010
|
for (const char of piece.to) {
|
|
829
1011
|
const point = /** @type {number} */ (char.codePointAt(0));
|
|
830
1012
|
if (/\s/u.test(char) || face.draws.has(point)) continue;
|
|
@@ -884,6 +1066,35 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
884
1066
|
}
|
|
885
1067
|
};
|
|
886
1068
|
|
|
1069
|
+
/**
|
|
1070
|
+
* Report each word a logographic map gives no writing, once per run, at
|
|
1071
|
+
* the word.
|
|
1072
|
+
*
|
|
1073
|
+
* @param {Array<[number, number]>} parts - The text to check, as offsets.
|
|
1074
|
+
* @param {RunDetail} detail - The run.
|
|
1075
|
+
*/
|
|
1076
|
+
const writings = (parts, detail) => {
|
|
1077
|
+
const { mapping } = detail;
|
|
1078
|
+
if (!mapping?.map.logographic) return;
|
|
1079
|
+
const said = new Set();
|
|
1080
|
+
for (const [from, to] of parts) {
|
|
1081
|
+
const lineStart = from === 0 || text[from - 1] === "\n";
|
|
1082
|
+
for (const piece of setText(text.slice(from, to), mapping, lineStart).pieces) {
|
|
1083
|
+
if (piece.unwritten === undefined || said.has(piece.from)) continue;
|
|
1084
|
+
said.add(piece.from);
|
|
1085
|
+
report(
|
|
1086
|
+
positionOf(starts, from + piece.offset),
|
|
1087
|
+
unwrittenMessage(
|
|
1088
|
+
piece.from,
|
|
1089
|
+
piece.unwritten,
|
|
1090
|
+
detail.run.lang,
|
|
1091
|
+
mapping.map.name,
|
|
1092
|
+
),
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
|
|
887
1098
|
/**
|
|
888
1099
|
* Report each word of a mapped run that is not a word of its language, once
|
|
889
1100
|
* per run, at its first character.
|
|
@@ -914,7 +1125,11 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
914
1125
|
const runs = [];
|
|
915
1126
|
const details = [];
|
|
916
1127
|
for (const construct of constructs) {
|
|
1128
|
+
const unchecked = errors.length;
|
|
917
1129
|
checkOwn(construct);
|
|
1130
|
+
// A key that names nothing is the finding; the text it would have set
|
|
1131
|
+
// is not then checked against a face or a language it cannot reach.
|
|
1132
|
+
const named = errors.length === unchecked;
|
|
918
1133
|
const keys = Object.assign({}, ...around(construct).map((c) => c.keys), construct.keys);
|
|
919
1134
|
const { lang, font, map } = keys;
|
|
920
1135
|
// A face the run's map is not for, once the face and the map are each
|
|
@@ -959,7 +1174,11 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
959
1174
|
holes: new Map(),
|
|
960
1175
|
body: undefined,
|
|
961
1176
|
contentEnd: construct.kind === "span" ? construct.node.contentEnd : undefined,
|
|
1177
|
+
languageEmphasis: lang === undefined ? undefined : ctx.languageEmphases?.get(lang),
|
|
1178
|
+
conventions: undefined,
|
|
1179
|
+
emphasis: [],
|
|
962
1180
|
};
|
|
1181
|
+
detail.conventions = conventionsOf(detail, ctx);
|
|
963
1182
|
details.push(detail);
|
|
964
1183
|
if (!plain) continue;
|
|
965
1184
|
const marking = [
|
|
@@ -972,11 +1191,16 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
972
1191
|
if (construct.kind === "span") {
|
|
973
1192
|
const span = construct.node;
|
|
974
1193
|
const inline = text.slice(span.start + 1, span.contentEnd);
|
|
975
|
-
plainInline(inline, span.start + 1, marking);
|
|
976
|
-
if (errors.length === before) {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1194
|
+
plainInline(inline, span.start + 1, marking, detail);
|
|
1195
|
+
if (named && errors.length === before) {
|
|
1196
|
+
const parts = outsideDelimiters(
|
|
1197
|
+
[[span.start + 1, span.contentEnd]],
|
|
1198
|
+
detail.emphasis,
|
|
1199
|
+
);
|
|
1200
|
+
lexical(parts, detail);
|
|
1201
|
+
capitals(parts, detail);
|
|
1202
|
+
writings(parts, detail);
|
|
1203
|
+
covered(parts, detail);
|
|
980
1204
|
}
|
|
981
1205
|
continue;
|
|
982
1206
|
}
|
|
@@ -1010,6 +1234,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1010
1234
|
body.slice(top, past).join("\n"),
|
|
1011
1235
|
starts[first + top],
|
|
1012
1236
|
marking,
|
|
1237
|
+
detail,
|
|
1013
1238
|
detail.holes,
|
|
1014
1239
|
);
|
|
1015
1240
|
continue;
|
|
@@ -1017,20 +1242,22 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1017
1242
|
if (token.type === "fence")
|
|
1018
1243
|
said(
|
|
1019
1244
|
top,
|
|
1020
|
-
token.info.trim().split(/\s/)[0] === "
|
|
1021
|
-
"a
|
|
1245
|
+
token.info.trim().split(/\s/)[0] === "verse" ?
|
|
1246
|
+
"a verse fence"
|
|
1022
1247
|
: "a code fence",
|
|
1023
1248
|
);
|
|
1024
1249
|
else if (Object.hasOwn(BLOCK_WHAT, token.type)) said(top, BLOCK_WHAT[token.type]);
|
|
1025
1250
|
}
|
|
1026
1251
|
} else {
|
|
1027
1252
|
for (let line = first; line < last; line++)
|
|
1028
|
-
plainInline(lines[line], starts[line], marking, detail.holes);
|
|
1253
|
+
plainInline(lines[line], starts[line], marking, detail, detail.holes);
|
|
1029
1254
|
}
|
|
1030
|
-
if (errors.length === before) {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1255
|
+
if (named && errors.length === before) {
|
|
1256
|
+
const parts = outsideDelimiters(outsideHoles(from, to, detail.holes), detail.emphasis);
|
|
1257
|
+
lexical(parts, detail);
|
|
1258
|
+
capitals(parts, detail);
|
|
1259
|
+
writings(parts, detail);
|
|
1260
|
+
covered(parts, detail);
|
|
1034
1261
|
}
|
|
1035
1262
|
}
|
|
1036
1263
|
|
|
@@ -1043,7 +1270,7 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1043
1270
|
}
|
|
1044
1271
|
|
|
1045
1272
|
/**
|
|
1046
|
-
* Find the spans, divs and
|
|
1273
|
+
* Find the spans, divs and verse fences marked with `lang=`, `font=` or
|
|
1047
1274
|
* `map=`, and every fault in their markup and their text.
|
|
1048
1275
|
*
|
|
1049
1276
|
* @param {string} source - Authored Markdown: a note body, or a whole file
|
|
@@ -1059,8 +1286,50 @@ function analyseRuns(source, ctx, syntax) {
|
|
|
1059
1286
|
* declared name.
|
|
1060
1287
|
*/
|
|
1061
1288
|
export function scanRuns(source, ctx = {}, syntax) {
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1289
|
+
const text = String(source ?? "");
|
|
1290
|
+
const { markdown, origins } = expandVerseOrigins(text);
|
|
1291
|
+
const authored = analyseRuns(text, ctx, syntax);
|
|
1292
|
+
const metre = metreFindings(scanVerse(text).blocks, text.split("\n"), {
|
|
1293
|
+
metres: ctx.metres,
|
|
1294
|
+
metreBooks: ctx.metreBooks,
|
|
1295
|
+
lexicons: ctx.lexicons,
|
|
1296
|
+
columnsClass: COLUMNS_CLASS,
|
|
1297
|
+
columnKeys: COLUMN_KEYS,
|
|
1298
|
+
attributeColumns,
|
|
1299
|
+
});
|
|
1300
|
+
/** @param {object[]} found @param {object[]} extra */
|
|
1301
|
+
const merged = (found, extra) =>
|
|
1302
|
+
extra.length ?
|
|
1303
|
+
[...found, ...extra].sort((a, b) => a.line - b.line || a.column - b.column)
|
|
1304
|
+
: found;
|
|
1305
|
+
if (markdown === text)
|
|
1306
|
+
return {
|
|
1307
|
+
...authored,
|
|
1308
|
+
errors: merged(authored.errors, metre.errors),
|
|
1309
|
+
warnings: merged(authored.warnings, metre.warnings),
|
|
1310
|
+
};
|
|
1311
|
+
// A verse in columns is checked as the spans each entry sets, which are
|
|
1312
|
+
// written only in the expansion; every finding is placed back where the
|
|
1313
|
+
// note wrote the text or the entry it comes from, once.
|
|
1314
|
+
const expanded = markdown.split("\n");
|
|
1315
|
+
const checked = analyseRuns(markdown, ctx);
|
|
1316
|
+
const placed = (findings) => {
|
|
1317
|
+
const seen = new Set();
|
|
1318
|
+
return findings
|
|
1319
|
+
.map((finding) => ({ ...finding, ...sourcePosition(finding, expanded, origins) }))
|
|
1320
|
+
.filter((finding) => {
|
|
1321
|
+
const key = `${finding.line}:${finding.column}:${finding.message}`;
|
|
1322
|
+
if (seen.has(key)) return false;
|
|
1323
|
+
seen.add(key);
|
|
1324
|
+
return true;
|
|
1325
|
+
})
|
|
1326
|
+
.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
1327
|
+
};
|
|
1328
|
+
return {
|
|
1329
|
+
runs: authored.runs,
|
|
1330
|
+
errors: merged(placed(checked.errors), metre.errors),
|
|
1331
|
+
warnings: merged(placed(checked.warnings), metre.warnings),
|
|
1332
|
+
};
|
|
1064
1333
|
}
|
|
1065
1334
|
|
|
1066
1335
|
/**
|
|
@@ -1113,7 +1382,7 @@ function rewrittenAttributes(detail, search) {
|
|
|
1113
1382
|
const changes =
|
|
1114
1383
|
run.font !== undefined ||
|
|
1115
1384
|
run.map !== undefined ||
|
|
1116
|
-
(run.tag !== undefined && (keys.lang !== run.tag || run.kind === "
|
|
1385
|
+
(run.tag !== undefined && (keys.lang !== run.tag || run.kind === "verse"));
|
|
1117
1386
|
if (!changes) return undefined;
|
|
1118
1387
|
const classes = [...own.classes];
|
|
1119
1388
|
const values = [...own.values];
|
|
@@ -1128,12 +1397,70 @@ function rewrittenAttributes(detail, search) {
|
|
|
1128
1397
|
return attributeText({ lang: run.tag, id: own.id, classes, values });
|
|
1129
1398
|
}
|
|
1130
1399
|
|
|
1400
|
+
/**
|
|
1401
|
+
* The tag a delimiter is written as: an `<em>` or a `<strong>` carrying its
|
|
1402
|
+
* convention's class. The book's parser reads only the classed form, so a
|
|
1403
|
+
* delimiter with no convention writes nothing there, and the text is set in
|
|
1404
|
+
* the face's Regular.
|
|
1405
|
+
*
|
|
1406
|
+
* @param {Delimiter} delimiter - The delimiter.
|
|
1407
|
+
* @param {import("./script-emphasis.mjs").Emphasis|undefined} conventions -
|
|
1408
|
+
* The run's conventions.
|
|
1409
|
+
* @param {boolean} book - Whether the target is the book.
|
|
1410
|
+
* @returns {string} The tag.
|
|
1411
|
+
*/
|
|
1412
|
+
function emphasisTag(delimiter, conventions, book) {
|
|
1413
|
+
const convention = conventions?.[delimiter.tag];
|
|
1414
|
+
if (!convention && book) return "";
|
|
1415
|
+
if (!delimiter.open) return `</${delimiter.tag}>`;
|
|
1416
|
+
return convention ?
|
|
1417
|
+
`<${delimiter.tag} class="${EMPHASIS_CLASSES[convention]}">`
|
|
1418
|
+
: `<${delimiter.tag}>`;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Set a range of a run's text, each piece between its emphasis delimiters by
|
|
1423
|
+
* {@link setText}, and each delimiter as its tag.
|
|
1424
|
+
*
|
|
1425
|
+
* @param {string} text - The source.
|
|
1426
|
+
* @param {number} from - The range's first offset.
|
|
1427
|
+
* @param {number} to - The offset past it.
|
|
1428
|
+
* @param {RunDetail} detail - The run.
|
|
1429
|
+
* @param {boolean} lineStart - Whether `from` starts its line.
|
|
1430
|
+
* @param {boolean} book - Whether the target is the book.
|
|
1431
|
+
* @returns {string} The set text.
|
|
1432
|
+
*/
|
|
1433
|
+
function setRange(text, from, to, detail, lineStart, book) {
|
|
1434
|
+
let out = "";
|
|
1435
|
+
let at = from;
|
|
1436
|
+
for (const delimiter of detail.emphasis) {
|
|
1437
|
+
if (delimiter.offset < from || delimiter.offset >= to) continue;
|
|
1438
|
+
if (delimiter.offset > at)
|
|
1439
|
+
out += setText(
|
|
1440
|
+
text.slice(at, delimiter.offset),
|
|
1441
|
+
detail.mapping,
|
|
1442
|
+
at === from && lineStart,
|
|
1443
|
+
).text;
|
|
1444
|
+
out += emphasisTag(delimiter, detail.conventions, book);
|
|
1445
|
+
at = delimiter.offset + delimiter.length;
|
|
1446
|
+
}
|
|
1447
|
+
if (at < to) out += setText(text.slice(at, to), detail.mapping, at === from && lineStart).text;
|
|
1448
|
+
return out;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1131
1451
|
/**
|
|
1132
1452
|
* Rewrite text marked with `lang=`, `font=` and `map=` into the plain span,
|
|
1133
|
-
* div and
|
|
1453
|
+
* div and verse grammar the rest of each chain renders, one line for one line.
|
|
1134
1454
|
*
|
|
1455
|
+
* - **A verse in columns is expanded first** into one span per column entry
|
|
1456
|
+
* per verse line, by {@link module:engine/content-verse.expandVerse}, so
|
|
1457
|
+
* each entry is mapped and set as a span with its keys is. That is the one
|
|
1458
|
+
* rewrite that changes the number of lines.
|
|
1135
1459
|
* - **The map, if any, is applied first.** Characters Markdown would read are
|
|
1136
1460
|
* then backslash-escaped, so they reach every later pass as characters.
|
|
1461
|
+
* - **Emphasis in a face** becomes an `<em>` or a `<strong>` carrying its
|
|
1462
|
+
* convention's class, or no class where none is stated. The book reads
|
|
1463
|
+
* only the classed form, so a classless one writes nothing there.
|
|
1137
1464
|
* - **The construct carries** the language's tag as `lang`, the face's class
|
|
1138
1465
|
* (with `script`, and `script-block` on a div) or the generic family's
|
|
1139
1466
|
* class. On the site, a face's construct also carries
|
|
@@ -1156,7 +1483,10 @@ function rewrittenAttributes(detail, search) {
|
|
|
1156
1483
|
*/
|
|
1157
1484
|
export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
1158
1485
|
const search = options.target === "site";
|
|
1159
|
-
const
|
|
1486
|
+
const book = options.target === "book";
|
|
1487
|
+
const authored = String(source ?? "");
|
|
1488
|
+
const text = expandVerse(authored);
|
|
1489
|
+
if (text !== authored) syntax = undefined;
|
|
1160
1490
|
if (!/\b(?:lang|font|map)=/.test(text)) return { markdown: text, errors: [] };
|
|
1161
1491
|
const { details, errors } = analyseRuns(text, ctx, syntax);
|
|
1162
1492
|
if (!details.length) return { markdown: text, errors };
|
|
@@ -1185,11 +1515,14 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1185
1515
|
const contentEnd = /** @type {number} */ (detail.contentEnd);
|
|
1186
1516
|
let inner = text.slice(run.start + 1, contentEnd);
|
|
1187
1517
|
if (detail.plain)
|
|
1188
|
-
inner =
|
|
1189
|
-
|
|
1190
|
-
|
|
1518
|
+
inner = setRange(
|
|
1519
|
+
text,
|
|
1520
|
+
run.start + 1,
|
|
1521
|
+
contentEnd,
|
|
1522
|
+
detail,
|
|
1191
1523
|
run.start === 0 || text[run.start - 1] === "\n",
|
|
1192
|
-
|
|
1524
|
+
book,
|
|
1525
|
+
);
|
|
1193
1526
|
else if (attributes === undefined) continue;
|
|
1194
1527
|
const braces = attributes ?? text.slice(contentEnd + 2, run.end - 1);
|
|
1195
1528
|
edits.push({ from: run.start, to: run.end, text: `[${inner}]{${braces}}` });
|
|
@@ -1207,7 +1540,7 @@ export function renderRuns(source, ctx = {}, syntax, options = {}) {
|
|
|
1207
1540
|
if (detail.plain && detail.body)
|
|
1208
1541
|
for (const [from, to] of outsideHoles(detail.body[0], detail.body[1], detail.holes)) {
|
|
1209
1542
|
const lineStart = from === 0 || text[from - 1] === "\n";
|
|
1210
|
-
const set =
|
|
1543
|
+
const set = setRange(text, from, to, detail, lineStart, book);
|
|
1211
1544
|
if (set !== text.slice(from, to)) edits.push({ from, to, text: set });
|
|
1212
1545
|
}
|
|
1213
1546
|
}
|