@heroiclands/package-build 23.13.0 → 24.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +2 -1
- package/assets/preview/gitea.ini +12 -7
- package/assets/styles/content-base.css +150 -1
- package/bin/content-commands.mjs +356 -210
- 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 +93 -72
- package/docs/authoring/links-and-markup.md +316 -78
- package/docs/commands.md +248 -80
- package/docs/configuration.md +210 -102
- package/docs/content-format.md +2 -2
- package/docs/diagnostics.md +2 -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 +155 -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 +589 -308
- package/docs/reference/note-types.md +40 -10
- package/docs/reference/theme-pages.md +127 -108
- package/docs/source-preview.md +15 -1
- package/engine/asset-shipping.mjs +22 -10
- package/engine/base-compiler.mjs +62 -104
- package/engine/changelog-lint.mjs +1 -1
- package/engine/content-address.mjs +1 -1
- package/engine/content-alerts.mjs +8 -4
- package/engine/content-blocks.mjs +26 -11
- package/engine/content-embeds.mjs +1 -1
- package/engine/content-expand.mjs +376 -0
- package/engine/content-figures.mjs +13 -6
- package/engine/content-format.yaml +8 -0
- package/engine/content-glyphs.mjs +213 -0
- package/engine/content-images.mjs +6 -7
- 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 +326 -27
- package/engine/content-spans.mjs +4 -1
- package/engine/content-styles.mjs +91 -0
- package/engine/content-symbols.mjs +186 -0
- package/engine/content-verse.mjs +6 -18
- package/engine/core-report.mjs +169 -0
- package/engine/core-vocabulary.mjs +208 -0
- package/engine/dictionary-html.mjs +119 -0
- package/engine/field-reference.mjs +1 -1
- 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/language-rows.mjs +8 -2
- package/engine/language-terms.mjs +115 -14
- package/engine/language-words.mjs +12 -0
- package/engine/logographic.mjs +255 -0
- package/engine/map-lexicon.mjs +221 -25
- 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/note-matter.mjs +2 -2
- package/engine/note-vocabulary.mjs +187 -3
- package/engine/pdf-build.mjs +68 -133
- package/engine/pdf-render.mjs +102 -44
- 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/run-words.mjs +3 -2
- 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 +265 -313
- 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 +50 -523
- package/engine/site-reach.mjs +213 -0
- package/engine/site-render.mjs +668 -0
- package/engine/site-root.mjs +90 -39
- package/engine/site-search.mjs +161 -0
- package/engine/site-serve.mjs +274 -0
- package/engine/site-theme-banner.mjs +154 -0
- package/engine/site-theme-urls.mjs +125 -0
- package/engine/site-theme.mjs +939 -0
- package/engine/sql-tables.mjs +9 -6
- 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 +39 -24
- package/engine/word-rows.mjs +12 -2
- package/engine/word-terms.mjs +29 -4
- package/manifest.mjs +3 -4
- package/package.json +10 -7
- 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 +14 -0
- 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/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 +62 -0
- 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/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/field-reference.d.mts +1 -1
- 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/language-terms.d.mts +24 -2
- package/types/engine/logographic.d.mts +45 -0
- package/types/engine/map-lexicon.d.mts +48 -8
- 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/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/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-reach.d.mts +76 -0
- package/types/engine/site-render.d.mts +173 -0
- package/types/engine/site-root.d.mts +14 -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/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 +3 -2
- 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/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/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
package/engine/sql-tables.mjs
CHANGED
|
@@ -603,11 +603,7 @@ async function createEventRelation(connection, file, records, prefix, audience)
|
|
|
603
603
|
fs.writeFileSync(file, rows.map((row) => JSON.stringify(row)).join("\n"));
|
|
604
604
|
// `whenIn` and `untilIn` are keyed by calendar Address, and read as structs
|
|
605
605
|
// whatever their number of keys, so `whenIn['<calendar>']` always binds.
|
|
606
|
-
await createTableFromFile(
|
|
607
|
-
connection,
|
|
608
|
-
`${prefix}events`,
|
|
609
|
-
readJsonAuto(file).replace(/\)$/, ", map_inference_threshold=-1)"),
|
|
610
|
-
);
|
|
606
|
+
await createTableFromFile(connection, `${prefix}events`, readJsonAuto(file));
|
|
611
607
|
}
|
|
612
608
|
|
|
613
609
|
/**
|
|
@@ -941,13 +937,20 @@ async function createRowRelation(connection, file, rows, view, columns, types) {
|
|
|
941
937
|
* type's fields with `NULL` where a record lacks one. A dependency's index has
|
|
942
938
|
* exactly the same shape and needs exactly the same reading.
|
|
943
939
|
*
|
|
940
|
+
* `map_inference_threshold=-1` keeps every object a struct. Left at its default,
|
|
941
|
+
* DuckDB reads an object whose keys are sparsely spread across rows (the mean
|
|
942
|
+
* share of rows carrying a key falls under `field_appearance_threshold`) as a
|
|
943
|
+
* map, so `data.<key>` changes meaning with the corpus: one more field on one
|
|
944
|
+
* note can turn a plain comparison into a JSON conversion error.
|
|
945
|
+
*
|
|
944
946
|
* @param {string} file - The JSONL to read.
|
|
945
947
|
* @returns {string} The `SELECT … FROM read_json_auto(…)` clause.
|
|
946
948
|
*/
|
|
947
949
|
function readJsonAuto(file) {
|
|
948
950
|
return (
|
|
949
951
|
`SELECT * FROM read_json_auto('${file.replace(/'/g, "''")}', ` +
|
|
950
|
-
`format='newline_delimited', union_by_name=true, maximum_object_size=20000000
|
|
952
|
+
`format='newline_delimited', union_by_name=true, maximum_object_size=20000000, ` +
|
|
953
|
+
`map_inference_threshold=-1)`
|
|
951
954
|
);
|
|
952
955
|
}
|
|
953
956
|
|
package/engine/text-maps.mjs
CHANGED
|
@@ -51,6 +51,14 @@
|
|
|
51
51
|
* `uncapitalised`, which a run reports; the capitalised word is then stated
|
|
52
52
|
* as a lexicon word of its own.
|
|
53
53
|
*
|
|
54
|
+
* - **A logographic map** (`logographic: true`) writes every word, a word
|
|
55
|
+
* being a match of the word pattern that holds a letter, by the writing a
|
|
56
|
+
* lookup gives it (see {@link module:engine/logographic}), and its rows
|
|
57
|
+
* write only the text between words: spaces, points, and anything else
|
|
58
|
+
* with no letter. A word the lookup gives no writing is written as typed
|
|
59
|
+
* and marked `unwritten`, which a run reports. A row of such a map whose
|
|
60
|
+
* `from` holds a letter could never apply, and is refused.
|
|
61
|
+
*
|
|
54
62
|
* `rows` is a list, never a keyed map, so `A` and `a` are two rows: the index
|
|
55
63
|
* carries them as rows of the `maps` relation, never as struct keys.
|
|
56
64
|
*
|
|
@@ -97,6 +105,8 @@ const BOUNDARY = /[\s\p{P}\p{S}]/u;
|
|
|
97
105
|
*
|
|
98
106
|
* @typedef {object} TextMap
|
|
99
107
|
* @property {string} name - Its name.
|
|
108
|
+
* @property {boolean} logographic - Whether it writes words from the lexicon
|
|
109
|
+
* alone, its rows only between them.
|
|
100
110
|
* @property {ReadonlySet<string>} vowels - The characters `before: vowel` reads.
|
|
101
111
|
* @property {readonly MapRow[]} rows - Its well-formed shared rows, in authored order.
|
|
102
112
|
* @property {ReadonlyMap<string, readonly MapRow[]>} byFirst - Its shared rows
|
|
@@ -113,6 +123,8 @@ const BOUNDARY = /[\s\p{P}\p{S}]/u;
|
|
|
113
123
|
* tried first.
|
|
114
124
|
* @property {(word: string) => string|undefined} [native] - A word's native
|
|
115
125
|
* form, for the word exactly as given, or `undefined` where it states none.
|
|
126
|
+
* @property {(word: string) => import("./logographic.mjs").Writing} [writing] -
|
|
127
|
+
* A word's writing under a logographic map; without one, no word has one.
|
|
116
128
|
*/
|
|
117
129
|
|
|
118
130
|
/**
|
|
@@ -236,6 +248,7 @@ export function compileMap(value) {
|
|
|
236
248
|
}
|
|
237
249
|
return Object.freeze({
|
|
238
250
|
name: map.name,
|
|
251
|
+
logographic: map.logographic === true,
|
|
239
252
|
vowels,
|
|
240
253
|
rows,
|
|
241
254
|
byFirst: indexRows(rows),
|
|
@@ -300,9 +313,9 @@ function rowAt(map, font, text, at, limit) {
|
|
|
300
313
|
* @param {TextMap|undefined} map - The map.
|
|
301
314
|
* @param {string} text - The text.
|
|
302
315
|
* @param {string|undefined} font - The face.
|
|
303
|
-
* @param {ReadonlyMap<number, {end: number, to: string, uncapitalised?: string}>} words -
|
|
304
|
-
* Words written
|
|
305
|
-
* @returns {{text: string, pieces: Array<{at: number, from: string, to: string, row: boolean, native?: boolean, uncapitalised?: string}>}}
|
|
316
|
+
* @param {ReadonlyMap<number, {end: number, to: string, uncapitalised?: string, unwritten?: object}>} words -
|
|
317
|
+
* Words written whole, by offset.
|
|
318
|
+
* @returns {{text: string, pieces: Array<{at: number, from: string, to: string, row: boolean, native?: boolean, uncapitalised?: string, unwritten?: object}>}}
|
|
306
319
|
* As {@link applyMap}.
|
|
307
320
|
*/
|
|
308
321
|
function applyRows(map, text, font, words) {
|
|
@@ -317,8 +330,9 @@ function applyRows(map, text, font, words) {
|
|
|
317
330
|
at,
|
|
318
331
|
from: text.slice(at, word.end),
|
|
319
332
|
to: word.to,
|
|
320
|
-
|
|
321
|
-
|
|
333
|
+
...(word.unwritten === undefined ?
|
|
334
|
+
{ row: true, native: true }
|
|
335
|
+
: { row: false, unwritten: word.unwritten }),
|
|
322
336
|
...(word.uncapitalised === undefined ? {} : { uncapitalised: word.uncapitalised }),
|
|
323
337
|
});
|
|
324
338
|
out += word.to;
|
|
@@ -371,15 +385,31 @@ export function capitalNative(map, font, word, native) {
|
|
|
371
385
|
* as it is.
|
|
372
386
|
* @param {string} text - The text, with no escapes in it.
|
|
373
387
|
* @param {MapOptions} [options] - The face, and the native forms of words.
|
|
374
|
-
* @returns {{text: string, pieces: Array<{at: number, from: string, to: string, row: boolean, native?: boolean}>}}
|
|
388
|
+
* @returns {{text: string, pieces: Array<{at: number, from: string, to: string, row: boolean, native?: boolean, unwritten?: object}>}}
|
|
375
389
|
* The rewritten text, and each piece of it with the UTF-16 offset in `text`
|
|
376
390
|
* it came from: a matched row's `from` and what it wrote, a word and its
|
|
377
391
|
* native form (`native: true`), or one character that passed through. A
|
|
378
392
|
* capitalised word whose native form the rows cannot capitalise is written
|
|
379
393
|
* as stated and carries `uncapitalised`, the lowercase form it was found by.
|
|
394
|
+
* Under a logographic map, a word with no writing is written as typed and
|
|
395
|
+
* carries `unwritten`, why it has none.
|
|
380
396
|
*/
|
|
381
|
-
export function applyMap(map, text, { font, native } = {}) {
|
|
397
|
+
export function applyMap(map, text, { font, native, writing } = {}) {
|
|
382
398
|
const words = new Map();
|
|
399
|
+
if (map?.logographic) {
|
|
400
|
+
for (const match of text.matchAll(WORD)) {
|
|
401
|
+
const word = match[0];
|
|
402
|
+
if (!LETTER.test(word)) continue;
|
|
403
|
+
const found = writing ? writing(word) : { missing: true, noLexicon: true };
|
|
404
|
+
words.set(
|
|
405
|
+
match.index,
|
|
406
|
+
"text" in found ?
|
|
407
|
+
{ end: match.index + word.length, to: found.text }
|
|
408
|
+
: { end: match.index + word.length, to: word, unwritten: found },
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
return applyRows(map, text, font, words);
|
|
412
|
+
}
|
|
383
413
|
if (map && native)
|
|
384
414
|
for (const match of text.matchAll(WORD)) {
|
|
385
415
|
const word = match[0];
|
|
@@ -502,7 +532,8 @@ function namesIn(index) {
|
|
|
502
532
|
/**
|
|
503
533
|
* Check a note's `data.map`: its name, that no other note declares it, each
|
|
504
534
|
* row written as a list, a `before` condition on a map with no `vowels`, an
|
|
505
|
-
* empty `fonts`,
|
|
535
|
+
* empty `fonts`, a generic family in it, and a row of a logographic map that
|
|
536
|
+
* matches a letter. The data-key check owns every
|
|
506
537
|
* key, its kind and the words a condition takes; whether a listed face is
|
|
507
538
|
* installed, and draws what the map writes, is
|
|
508
539
|
* {@link module:engine/map-lexicon.checkMapFonts}'s.
|
|
@@ -527,7 +558,9 @@ export function checkMapField(note, { index } = {}) {
|
|
|
527
558
|
message,
|
|
528
559
|
});
|
|
529
560
|
const findings = [];
|
|
530
|
-
const { name, vowels, rows, fonts } = /** @type {Record<string, unknown>} */ (
|
|
561
|
+
const { name, vowels, rows, fonts, logographic } = /** @type {Record<string, unknown>} */ (
|
|
562
|
+
value
|
|
563
|
+
);
|
|
531
564
|
if (typeof name === "string" && !MAP_NAME.test(name))
|
|
532
565
|
findings.push(
|
|
533
566
|
finding(
|
|
@@ -565,12 +598,26 @@ export function checkMapField(note, { index } = {}) {
|
|
|
565
598
|
}
|
|
566
599
|
// A row's keys, their kinds and the words a condition takes are the
|
|
567
600
|
// data-key check's; what it cannot say is a list row's arity, an empty
|
|
568
|
-
// `from` in one,
|
|
601
|
+
// `from` in one, a condition the map has no vowels for, and a letter a
|
|
602
|
+
// logographic map's row could never meet.
|
|
569
603
|
const hasVowels = typeof vowels === "string" && vowels !== "";
|
|
570
604
|
const rowFindings = (list, keyPath) => {
|
|
571
605
|
if (!Array.isArray(list)) return;
|
|
572
606
|
const label = `data.map.${keyPath.join(".").replace(/\.(\d+)/g, "[$1]")}`;
|
|
573
607
|
list.forEach((row, at) => {
|
|
608
|
+
const from =
|
|
609
|
+
Array.isArray(row) ? row[0]
|
|
610
|
+
: isMap(row) ? row.from
|
|
611
|
+
: undefined;
|
|
612
|
+
const letter = typeof from === "string" ? from.match(LETTER)?.[0] : undefined;
|
|
613
|
+
if (logographic === true && letter !== undefined)
|
|
614
|
+
findings.push(
|
|
615
|
+
finding(
|
|
616
|
+
[...keyPath, at],
|
|
617
|
+
`\`${label}[${at}]\` matches \`${letter}\`, a letter, and the rows of a ` +
|
|
618
|
+
"logographic map write only the text between words",
|
|
619
|
+
),
|
|
620
|
+
);
|
|
574
621
|
if (Array.isArray(row)) {
|
|
575
622
|
if (row.length !== 2)
|
|
576
623
|
findings.push(
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* **Typographic punctuation on the web.** A web page sets straight quotes as
|
|
16
|
+
* curly ones and a run of hyphens or full stops as the dash or ellipsis it
|
|
17
|
+
* stands for, as a printed page does:
|
|
18
|
+
*
|
|
19
|
+
* | Written | Set as |
|
|
20
|
+
* | ------- | ------ |
|
|
21
|
+
* | `"word"` | “word” |
|
|
22
|
+
* | `'word'` | ‘word’ |
|
|
23
|
+
* | `it's`, `'90s`, `'twas` | it’s, ’90s, ’twas |
|
|
24
|
+
* | `--` | – |
|
|
25
|
+
* | `---` | — |
|
|
26
|
+
* | `...` | … |
|
|
27
|
+
* | `<<`, `>>` | «, » |
|
|
28
|
+
*
|
|
29
|
+
* A quote is read by the characters either side of it, as emphasis is: one
|
|
30
|
+
* that can only open a quotation opens one, one that can only close it closes
|
|
31
|
+
* a quotation already open earlier on the page, and one that could do either
|
|
32
|
+
* is left straight. Code, link destinations, raw HTML and a heading's
|
|
33
|
+
* attribute block are never touched, and neither is a backslash-escaped
|
|
34
|
+
* character.
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** The characters markdown-it's text rule stops at, and the ones this rule reads. */
|
|
40
|
+
const STOPS = new Set([..."\n!#$%&*+-:<=@[\\]^_`{}~", '"', "'", ".", ">"]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The token a typographic character is written as. It is a token of its own,
|
|
44
|
+
* not text, so a reader of a heading's text can tell where it stands.
|
|
45
|
+
*/
|
|
46
|
+
export const TYPOGRAPHIC = "typographic";
|
|
47
|
+
|
|
48
|
+
/** The characters this rule reads. */
|
|
49
|
+
const TRIGGERS = new Set(['"', "'", "-", ".", "<", ">"]);
|
|
50
|
+
|
|
51
|
+
const LEFT_SINGLE = "‘";
|
|
52
|
+
const RIGHT_SINGLE = "’";
|
|
53
|
+
const LEFT_DOUBLE = "“";
|
|
54
|
+
const RIGHT_DOUBLE = "”";
|
|
55
|
+
|
|
56
|
+
/** ASCII punctuation, as a byte table reads it. */
|
|
57
|
+
const isAsciiPunct = (char) => char !== undefined && /^[!-/:-@[-`{-~]$/.test(char);
|
|
58
|
+
|
|
59
|
+
/** ASCII white space, as a byte table reads it. */
|
|
60
|
+
const isAsciiSpace = (char) => char === " " || char === "\t" || char === "\n" || char === "\r";
|
|
61
|
+
|
|
62
|
+
/** Unicode punctuation. */
|
|
63
|
+
const isPunct = (char) => char !== undefined && /^\p{P}$/u.test(char);
|
|
64
|
+
|
|
65
|
+
/** Unicode punctuation or a symbol. */
|
|
66
|
+
const isPunctOrSymbol = (char) => char !== undefined && /^[\p{P}\p{S}]$/u.test(char);
|
|
67
|
+
|
|
68
|
+
/** Unicode white space. */
|
|
69
|
+
const isSpace = (char) => char !== undefined && (isAsciiSpace(char) || /^\s$/u.test(char));
|
|
70
|
+
|
|
71
|
+
const isLetter = (char) => char !== undefined && /^\p{L}$/u.test(char);
|
|
72
|
+
const isDigit = (char) => char !== undefined && /^\p{Nd}$/u.test(char);
|
|
73
|
+
const isAsciiDigit = (char) => char !== undefined && char >= "0" && char <= "9";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether a run of quote characters can open and can close, by the flanking
|
|
77
|
+
* rules emphasis uses.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} before - The character before the run, or a newline.
|
|
80
|
+
* @param {string} after - The character after it, or a space.
|
|
81
|
+
* @returns {{canOpen: boolean, canClose: boolean}} What the run can do.
|
|
82
|
+
*/
|
|
83
|
+
function flanking(before, after) {
|
|
84
|
+
const beforePunct = isPunctOrSymbol(before);
|
|
85
|
+
const beforeSpace = isSpace(before);
|
|
86
|
+
const afterPunct = isPunctOrSymbol(after);
|
|
87
|
+
const afterSpace = isSpace(after);
|
|
88
|
+
return {
|
|
89
|
+
canOpen: !afterSpace && (!afterPunct || beforeSpace || beforePunct),
|
|
90
|
+
canClose: !beforeSpace && (!beforePunct || afterSpace || afterPunct),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Where a trailing attribute block starts in an inline run, so a heading's
|
|
96
|
+
* `{#id key="value"}` keeps its straight quotes and hyphens.
|
|
97
|
+
*
|
|
98
|
+
* @param {string} src - The inline run.
|
|
99
|
+
* @returns {number} The index of its opening brace, or the run's length.
|
|
100
|
+
*/
|
|
101
|
+
function attributeStart(src) {
|
|
102
|
+
const match = /\{[^{}\n]*\}\s*$/.exec(src);
|
|
103
|
+
return match ? match.index : src.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The punctuation one position sets, and how many characters it consumes.
|
|
108
|
+
*
|
|
109
|
+
* @param {object} state - markdown-it's inline state.
|
|
110
|
+
* @returns {{text: string, length: number}|null} The replacement, or `null`
|
|
111
|
+
* where the character stays as written.
|
|
112
|
+
*/
|
|
113
|
+
function substitution(state) {
|
|
114
|
+
const { src, pos } = state;
|
|
115
|
+
const end = src.indexOf("\n", pos);
|
|
116
|
+
const line = [...src.slice(pos, end < 0 ? src.length : end + 1)];
|
|
117
|
+
const c = line[0];
|
|
118
|
+
if (line.length > 2) {
|
|
119
|
+
if (c === "-" && line[1] === "-" && line[2] === "-") return { text: "—", length: 3 };
|
|
120
|
+
if (c === "." && line[1] === "." && line[2] === ".") return { text: "…", length: 3 };
|
|
121
|
+
}
|
|
122
|
+
if (c === ".") return null;
|
|
123
|
+
if (line.length > 1) {
|
|
124
|
+
if (c === "<") return line[1] === "<" ? { text: "«", length: 2 } : null;
|
|
125
|
+
if (c === ">") return line[1] === ">" ? { text: "»", length: 2 } : null;
|
|
126
|
+
if (c === "-" && line[1] === "-") return { text: "–", length: 2 };
|
|
127
|
+
}
|
|
128
|
+
if (c !== "'" && c !== '"') return null;
|
|
129
|
+
|
|
130
|
+
const before = pos > 0 ? [...src.slice(0, pos)].at(-1) : "\n";
|
|
131
|
+
let run = 0;
|
|
132
|
+
while (run < line.length && line[run] === c) run++;
|
|
133
|
+
const { canOpen, canClose } = flanking(before, run < line.length ? line[run] : " ");
|
|
134
|
+
const counter = (state.env.typographerQuotes ??= { single: 0, double: 0 });
|
|
135
|
+
const one = (text) => ({ text, length: 1 });
|
|
136
|
+
|
|
137
|
+
if (c === "'") {
|
|
138
|
+
// A decade: '90s.
|
|
139
|
+
if (
|
|
140
|
+
canOpen &&
|
|
141
|
+
!canClose &&
|
|
142
|
+
line.length > 3 &&
|
|
143
|
+
isAsciiDigit(line[1]) &&
|
|
144
|
+
isAsciiDigit(line[2]) &&
|
|
145
|
+
line[3] === "s"
|
|
146
|
+
) {
|
|
147
|
+
const after = line.length > 4 ? line[4] : " ";
|
|
148
|
+
if (isSpace(after) || isPunctOrSymbol(after)) return one(RIGHT_SINGLE);
|
|
149
|
+
}
|
|
150
|
+
// An elision: 'twas, 'em, 'net.
|
|
151
|
+
if (
|
|
152
|
+
line.length > 1 &&
|
|
153
|
+
(isPunct(before) || isSpace(before)) &&
|
|
154
|
+
["t", "e", "n", "l"].includes(line[1])
|
|
155
|
+
)
|
|
156
|
+
return one(RIGHT_SINGLE);
|
|
157
|
+
// An apostrophe between a letter or digit and a letter.
|
|
158
|
+
if (line.length > 1 && (isDigit(before) || isLetter(before)) && isLetter(line[1]))
|
|
159
|
+
return one(RIGHT_SINGLE);
|
|
160
|
+
if (canOpen && !canClose) {
|
|
161
|
+
const contracted =
|
|
162
|
+
(line.length > 1 &&
|
|
163
|
+
["s", "m", "t", "d"].includes(line[1]) &&
|
|
164
|
+
(line.length < 3 || isAsciiPunct(line[2]) || isAsciiSpace(line[2]))) ||
|
|
165
|
+
(line.length > 2 &&
|
|
166
|
+
["ve", "ll", "re"].includes(`${line[1]}${line[2]}`) &&
|
|
167
|
+
(line.length < 4 || isAsciiPunct(line[3]) || isAsciiSpace(line[3])));
|
|
168
|
+
if (contracted) return one(RIGHT_SINGLE);
|
|
169
|
+
counter.single++;
|
|
170
|
+
return one(LEFT_SINGLE);
|
|
171
|
+
}
|
|
172
|
+
// A plural possessive or a dropped letter: Smiths', doin'.
|
|
173
|
+
if (line.length > 2 && !isDigit(line[1])) return one(RIGHT_SINGLE);
|
|
174
|
+
if (counter.single > 0 && closes(canOpen, canClose, line)) {
|
|
175
|
+
counter.single--;
|
|
176
|
+
return one(RIGHT_SINGLE);
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (canOpen && !canClose) {
|
|
182
|
+
counter.double++;
|
|
183
|
+
return one(LEFT_DOUBLE);
|
|
184
|
+
}
|
|
185
|
+
if (counter.double > 0 && closes(canOpen, canClose, line)) {
|
|
186
|
+
// A measurement in inches: "Monitor 21"".
|
|
187
|
+
if (line.length > 1 && line[1] === '"' && isDigit(before)) return null;
|
|
188
|
+
counter.double--;
|
|
189
|
+
return one(RIGHT_DOUBLE);
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Whether a quote closes: it can only close, or it could do either and stands
|
|
196
|
+
* before punctuation that ends the line or is followed by white space or more
|
|
197
|
+
* punctuation.
|
|
198
|
+
*
|
|
199
|
+
* @param {boolean} canOpen - The run can open.
|
|
200
|
+
* @param {boolean} canClose - The run can close.
|
|
201
|
+
* @param {string[]} line - The rest of the line, from the quote.
|
|
202
|
+
* @returns {boolean} Whether it closes.
|
|
203
|
+
*/
|
|
204
|
+
function closes(canOpen, canClose, line) {
|
|
205
|
+
if (canClose && !canOpen) return true;
|
|
206
|
+
return (
|
|
207
|
+
canClose &&
|
|
208
|
+
canOpen &&
|
|
209
|
+
line.length > 1 &&
|
|
210
|
+
isPunct(line[1]) &&
|
|
211
|
+
(line.length === 2 || isAsciiPunct(line[2]) || isAsciiSpace(line[2]))
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Set typographic punctuation in every inline run.
|
|
217
|
+
*
|
|
218
|
+
* @param {import("markdown-it")} md - Markdown parser to extend.
|
|
219
|
+
*/
|
|
220
|
+
export function typographerPlugin(md) {
|
|
221
|
+
md.inline.ruler.at("text", (state, silent) => {
|
|
222
|
+
let pos = state.pos;
|
|
223
|
+
while (pos < state.posMax && !STOPS.has(state.src[pos])) pos++;
|
|
224
|
+
if (pos === state.pos) return false;
|
|
225
|
+
if (!silent) state.pending += state.src.slice(state.pos, pos);
|
|
226
|
+
state.pos = pos;
|
|
227
|
+
return true;
|
|
228
|
+
});
|
|
229
|
+
md.inline.ruler.push("typographer", (state, silent) => {
|
|
230
|
+
if (!TRIGGERS.has(state.src[state.pos])) return false;
|
|
231
|
+
state.typographerEnd ??= attributeStart(state.src);
|
|
232
|
+
if (state.pos >= state.typographerEnd) return false;
|
|
233
|
+
const found = substitution(state);
|
|
234
|
+
if (!found) return false;
|
|
235
|
+
if (!silent) state.push(TYPOGRAPHIC, "", 0).content = found.text;
|
|
236
|
+
state.pos += found.length;
|
|
237
|
+
return true;
|
|
238
|
+
});
|
|
239
|
+
md.renderer.rules[TYPOGRAPHIC] = (tokens, idx) => md.utils.escapeHtml(tokens[idx].content);
|
|
240
|
+
// An image's description is its text, its punctuation included.
|
|
241
|
+
const asText = md.renderer.renderInlineAsText.bind(md.renderer);
|
|
242
|
+
md.renderer.renderInlineAsText = (tokens, options, env) =>
|
|
243
|
+
tokens
|
|
244
|
+
.map((token) =>
|
|
245
|
+
token.type === TYPOGRAPHIC ? token.content : asText([token], options, env),
|
|
246
|
+
)
|
|
247
|
+
.join("");
|
|
248
|
+
}
|