@heroiclands/package-build 23.13.0 → 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 +36 -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 +244 -78
- 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 +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 +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 +259 -314
- 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-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/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-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/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/map-lexicon.mjs
CHANGED
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
* over one with none.
|
|
38
38
|
* - **Derived forms take the rows**: a plural or a verb ending of an exception
|
|
39
39
|
* word is spelled by the rows, and only the word as cited is looked up.
|
|
40
|
+
* - **A logographic map** spells no word by its rows: every word is written
|
|
41
|
+
* from the lexicon, by its own writing or its parts' (see
|
|
42
|
+
* {@link module:engine/logographic}), and a word with neither is a finding.
|
|
40
43
|
*
|
|
41
44
|
* {@link mappingFor} is the one path from a map, a face and a language to the
|
|
42
45
|
* function that rewrites text: the build's runs, `package-build transliterate`
|
|
@@ -51,6 +54,8 @@ import { parseAddress, renderAddress } from "./address.mjs";
|
|
|
51
54
|
import { positionOfFrontmatterPath, positionOfYamlPath } from "./diagnostics.mjs";
|
|
52
55
|
import { isNoteRecord } from "./index-records.mjs";
|
|
53
56
|
import { nearest } from "./near-miss.mjs";
|
|
57
|
+
import { WRITTEN_ENDINGS } from "./language-terms.mjs";
|
|
58
|
+
import { writingsOf } from "./logographic.mjs";
|
|
54
59
|
import { FONTS_PACKAGE } from "./script-fonts.mjs";
|
|
55
60
|
import { GENERIC_FAMILIES, applyMap } from "./text-maps.mjs";
|
|
56
61
|
import { isLanguageRecord, noteAddressOf } from "./word-rows.mjs";
|
|
@@ -107,6 +112,8 @@ function said(names) {
|
|
|
107
112
|
* @property {ReadonlyArray<{form: string, from: LexiconWord}>} derived - The
|
|
108
113
|
* forms the language's rules derive from its words, folded, as the mapped-run
|
|
109
114
|
* word check accepts them (see {@link module:engine/word-forms.derivedForms}).
|
|
115
|
+
* @property {Record<string, any>} data - The language note's `data`, which a
|
|
116
|
+
* logographic map writes words from.
|
|
110
117
|
*/
|
|
111
118
|
|
|
112
119
|
/**
|
|
@@ -161,6 +168,7 @@ function lexiconOf(language, data) {
|
|
|
161
168
|
words: Object.freeze(words),
|
|
162
169
|
byForm,
|
|
163
170
|
derived: Object.freeze(derived),
|
|
171
|
+
data: isMap(data) ? data : {},
|
|
164
172
|
});
|
|
165
173
|
}
|
|
166
174
|
|
|
@@ -330,7 +338,10 @@ export function nativeLookup(lexicon, map, font) {
|
|
|
330
338
|
* @property {import("./text-maps.mjs").TextMap} map - The map.
|
|
331
339
|
* @property {string|undefined} font - The face, whose own rows come first.
|
|
332
340
|
* @property {((word: string) => string|undefined)|undefined} native - The
|
|
333
|
-
* language's native forms under the map in the face
|
|
341
|
+
* language's native forms under the map in the face, for a map that spells
|
|
342
|
+
* by its rows.
|
|
343
|
+
* @property {((word: string) => import("./logographic.mjs").Writing)|undefined} writing -
|
|
344
|
+
* Each word's writing, for a logographic map.
|
|
334
345
|
*/
|
|
335
346
|
|
|
336
347
|
/**
|
|
@@ -346,7 +357,20 @@ export function mappingFor(ctx, { map, font, lang }) {
|
|
|
346
357
|
const textMap = ctx?.maps?.get(map);
|
|
347
358
|
if (!textMap) return undefined;
|
|
348
359
|
const lexicon = lang === undefined ? undefined : ctx.lexicons?.get(lang);
|
|
349
|
-
|
|
360
|
+
if (!textMap.logographic)
|
|
361
|
+
return {
|
|
362
|
+
map: textMap,
|
|
363
|
+
font,
|
|
364
|
+
native: nativeLookup(lexicon, textMap.name, font),
|
|
365
|
+
writing: undefined,
|
|
366
|
+
};
|
|
367
|
+
const writings = lexicon ? writingsOf(lexicon.data, textMap.name, font) : undefined;
|
|
368
|
+
return {
|
|
369
|
+
map: textMap,
|
|
370
|
+
font,
|
|
371
|
+
native: undefined,
|
|
372
|
+
writing: writings ? writings.word : () => ({ missing: true, noLexicon: true }),
|
|
373
|
+
};
|
|
350
374
|
}
|
|
351
375
|
|
|
352
376
|
/**
|
|
@@ -424,6 +448,52 @@ export function uncapitalisedMessage(word, lower, lang, map) {
|
|
|
424
448
|
);
|
|
425
449
|
}
|
|
426
450
|
|
|
451
|
+
/**
|
|
452
|
+
* What is wrong with a word a logographic map gives no writing.
|
|
453
|
+
*
|
|
454
|
+
* @param {string} word - The word as written.
|
|
455
|
+
* @param {object} unwritten - Why it has none, as
|
|
456
|
+
* {@link module:engine/logographic.writingsOf} says.
|
|
457
|
+
* @param {string|undefined} lang - The run's language.
|
|
458
|
+
* @param {string} map - The map's name.
|
|
459
|
+
* @returns {string} The message.
|
|
460
|
+
*/
|
|
461
|
+
export function unwrittenMessage(word, unwritten, lang, map) {
|
|
462
|
+
const why = /** @type {{noLexicon?: true, ambiguous?: {form: string, glosses: string[]}}} */ (
|
|
463
|
+
unwritten
|
|
464
|
+
);
|
|
465
|
+
if (why.noLexicon)
|
|
466
|
+
return lang === undefined ?
|
|
467
|
+
`\`${word}\` has no \`${map}\` writing: the map \`${map}\` is logographic, and the run ` +
|
|
468
|
+
"names no `lang=` whose lexicon writes it; mark the run with its language, or " +
|
|
469
|
+
"write the word directly in signs"
|
|
470
|
+
: `\`${word}\` has no \`${map}\` writing: the map \`${map}\` is logographic, and \`${lang}\` ` +
|
|
471
|
+
`has no lexicon (\`languages.${lang}.note\`) to write it from; write the word ` +
|
|
472
|
+
"directly in signs";
|
|
473
|
+
if (why.ambiguous) {
|
|
474
|
+
const { form, glosses } = why.ambiguous;
|
|
475
|
+
const quoted = glosses.map((gloss) => `“${gloss}”`);
|
|
476
|
+
const listed =
|
|
477
|
+
quoted.length < 2 ?
|
|
478
|
+
quoted.join("")
|
|
479
|
+
: `${quoted.slice(0, -1).join(", ")} and ${quoted.at(-1)}`;
|
|
480
|
+
const stands =
|
|
481
|
+
`stands for ${glosses.length} words of the \`${lang}\` lexicon that the map ` +
|
|
482
|
+
`\`${map}\` writes differently, ${listed}`;
|
|
483
|
+
return (
|
|
484
|
+
(form === word ?
|
|
485
|
+
`\`${word}\` ${stands}`
|
|
486
|
+
: `\`${word}\` is written from its part \`${form}\`, which ${stands}`) +
|
|
487
|
+
"; write the word directly in signs, choosing by its sense in " +
|
|
488
|
+
`\`package-build lexicon ${lang} --map ${map}\``
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return (
|
|
492
|
+
`\`${word}\` has no \`${map}\` writing in the \`${lang}\` lexicon, and its parts do not ` +
|
|
493
|
+
"give one; state one, or write the word directly in signs"
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
427
497
|
/**
|
|
428
498
|
* Map a passage exactly as the build maps a run with the same keys.
|
|
429
499
|
*
|
|
@@ -436,8 +506,14 @@ export function uncapitalisedMessage(word, lower, lang, map) {
|
|
|
436
506
|
export function transliterate(ctx, keys, text) {
|
|
437
507
|
const found = commandMapping(ctx, keys);
|
|
438
508
|
if ("problem" in found) return { problem: found.problem, about: found.about };
|
|
439
|
-
const { map, font, native } = found.mapping;
|
|
440
|
-
const mapped = applyMap(map, text, { font, native });
|
|
509
|
+
const { map, font, native, writing } = found.mapping;
|
|
510
|
+
const mapped = applyMap(map, text, { font, native, writing });
|
|
511
|
+
const unwritten = mapped.pieces.find((one) => one.unwritten !== undefined);
|
|
512
|
+
if (unwritten)
|
|
513
|
+
return {
|
|
514
|
+
about: "word",
|
|
515
|
+
problem: unwrittenMessage(unwritten.from, unwritten.unwritten, keys.lang, map.name),
|
|
516
|
+
};
|
|
441
517
|
const piece = mapped.pieces.find((one) => one.uncapitalised !== undefined);
|
|
442
518
|
if (piece)
|
|
443
519
|
return {
|
|
@@ -459,10 +535,17 @@ export function transliterate(ctx, keys, text) {
|
|
|
459
535
|
* each case folded and naming the word it comes from in `from`, so the list
|
|
460
536
|
* is what the mapped-run word check accepts of the lexicon.
|
|
461
537
|
*
|
|
538
|
+
* Under a logographic map an entry carries `source` in place of `exception`:
|
|
539
|
+
* `own` for a writing it states, `parts` for one its parts give, and `none`,
|
|
540
|
+
* with an empty `native`, where it has none. A word whose form a run cannot
|
|
541
|
+
* write, because the form stands for entries written differently, or a part
|
|
542
|
+
* does, also carries `ambiguous: true`; its `native` is still its own
|
|
543
|
+
* writing, the signs a run writes directly for that sense.
|
|
544
|
+
*
|
|
462
545
|
* @param {object} ctx - The run context.
|
|
463
546
|
* @param {{lang: string, map: string, font?: string, derived?: boolean}} keys -
|
|
464
547
|
* The language, the map, the face, and whether to list derived forms.
|
|
465
|
-
* @returns {{entries: Array<{form: string, native: string, exception
|
|
548
|
+
* @returns {{entries: Array<{form: string, native: string, exception?: boolean, source?: "own"|"parts"|"none", ambiguous?: true, class: string|null, gloss: string|null, from?: string}>}|{problem: string, about: string}}
|
|
466
549
|
* The words in `data.words` order, then any derived forms, or the problem.
|
|
467
550
|
*/
|
|
468
551
|
export function lexiconEntries(ctx, { lang, map, font, derived = false }) {
|
|
@@ -477,6 +560,7 @@ export function lexiconEntries(ctx, { lang, map, font, derived = false }) {
|
|
|
477
560
|
"in its `note`",
|
|
478
561
|
};
|
|
479
562
|
const { map: textMap, font: face, native } = found.mapping;
|
|
563
|
+
if (textMap.logographic) return { entries: writtenEntries(lexicon, textMap, face, derived) };
|
|
480
564
|
const listed = [
|
|
481
565
|
...lexicon.words.map((word) => ({ word, form: word.form, from: undefined })),
|
|
482
566
|
...(derived ?
|
|
@@ -509,6 +593,51 @@ export function lexiconEntries(ctx, { lang, map, font, derived = false }) {
|
|
|
509
593
|
};
|
|
510
594
|
}
|
|
511
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Every word of a lexicon with its writing under a logographic map, as
|
|
598
|
+
* {@link lexiconEntries} lists them.
|
|
599
|
+
*
|
|
600
|
+
* @param {Lexicon} lexicon - The lexicon.
|
|
601
|
+
* @param {import("./text-maps.mjs").TextMap} map - The map.
|
|
602
|
+
* @param {string|undefined} font - The face.
|
|
603
|
+
* @param {boolean} derived - Whether to list derived forms.
|
|
604
|
+
* @returns {Array<Record<string, any>>} The entries.
|
|
605
|
+
*/
|
|
606
|
+
function writtenEntries(lexicon, map, font, derived) {
|
|
607
|
+
const writings = writingsOf(lexicon.data, map.name, font);
|
|
608
|
+
// The `data.words` index of each lexicon word, which reads the same entries.
|
|
609
|
+
const indexes = [];
|
|
610
|
+
listOf(lexicon.data.words).forEach((word, index) => {
|
|
611
|
+
if (isMap(word) && typeof word.form === "string") indexes.push(index);
|
|
612
|
+
});
|
|
613
|
+
const shaped = (form, writing, word, from) => {
|
|
614
|
+
// Whether a run of the form, written as the build writes it, meets an
|
|
615
|
+
// ambiguity: another entry of the form, or of a part, written otherwise.
|
|
616
|
+
const run = applyMap(map, form, { font, writing: writings.word }).pieces.some(
|
|
617
|
+
(piece) => piece.unwritten?.ambiguous !== undefined,
|
|
618
|
+
);
|
|
619
|
+
return {
|
|
620
|
+
form,
|
|
621
|
+
native: "text" in writing ? writing.text : "",
|
|
622
|
+
source:
|
|
623
|
+
"text" in writing ? writing.source
|
|
624
|
+
: "ambiguous" in writing ? "parts"
|
|
625
|
+
: "none",
|
|
626
|
+
...("ambiguous" in writing || run ? { ambiguous: true } : {}),
|
|
627
|
+
class: word.class,
|
|
628
|
+
gloss: word.gloss,
|
|
629
|
+
...(from === undefined ? {} : { from }),
|
|
630
|
+
};
|
|
631
|
+
};
|
|
632
|
+
const entries = lexicon.words.map((word, at) =>
|
|
633
|
+
shaped(word.form, writings.entry(indexes[at]), word, undefined),
|
|
634
|
+
);
|
|
635
|
+
if (derived)
|
|
636
|
+
for (const one of lexicon.derived)
|
|
637
|
+
entries.push(shaped(one.form, writings.word(one.form), one.from, one.from.form));
|
|
638
|
+
return entries;
|
|
639
|
+
}
|
|
640
|
+
|
|
512
641
|
/**
|
|
513
642
|
* Whether a shared row can be reached in a face: no plain row of the face's
|
|
514
643
|
* own matches at the start of its `from`, which would win there first.
|
|
@@ -607,10 +736,16 @@ export function checkMapFonts(note, ctx) {
|
|
|
607
736
|
}
|
|
608
737
|
|
|
609
738
|
/**
|
|
610
|
-
* Check a language note's native forms against the maps and faces:
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
739
|
+
* Check a language note's native forms against the maps and faces: a word's
|
|
740
|
+
* `native`, an affix's, and the endings' in `data.grammar.native`. Each is
|
|
741
|
+
* checked for an unknown map, a face the map is not for, a second form for one
|
|
742
|
+
* map and face (and one ending), and a code point a face it applies in does
|
|
743
|
+
* not draw. A word's form under a map that spells by its rows is also checked
|
|
744
|
+
* for stating what the rows already give; under a logographic map the rows
|
|
745
|
+
* spell no word, so that check does not apply. An affix's and an ending's
|
|
746
|
+
* writings mean something only under a logographic map, and an ending of a
|
|
747
|
+
* person is written only where `data.grammar.verbs` states `persons`. The
|
|
748
|
+
* shape of each entry is the data-key check's.
|
|
614
749
|
*
|
|
615
750
|
* @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
|
|
616
751
|
* @param {{faces?: ReadonlyMap<string, import("./script-fonts.mjs").Face>, maps?: ReadonlyMap<string, import("./text-maps.mjs").TextMap>}} ctx -
|
|
@@ -623,35 +758,47 @@ export function checkLexiconNatives(note, ctx) {
|
|
|
623
758
|
const raw = note.raw ?? "";
|
|
624
759
|
const finding = (keyPath, message, key = false) => ({
|
|
625
760
|
file: note.file,
|
|
626
|
-
...positionOfFrontmatterPath(raw, ["data",
|
|
761
|
+
...positionOfFrontmatterPath(raw, ["data", ...keyPath], { key }),
|
|
627
762
|
severity: "error",
|
|
628
763
|
message,
|
|
629
764
|
});
|
|
630
765
|
const maps = ctx?.maps ?? new Map();
|
|
631
766
|
const faces = ctx?.faces ?? new Map();
|
|
632
767
|
const findings = [];
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Check one `native` list.
|
|
771
|
+
*
|
|
772
|
+
* @param {unknown[]} list - The list as authored.
|
|
773
|
+
* @param {Array<string|number>} keyPath - Its path under `data`.
|
|
774
|
+
* @param {object} as - What it belongs to.
|
|
775
|
+
* @param {"word"|"affix"|"ending"} as.kind - A word's, an affix's or the
|
|
776
|
+
* endings' list.
|
|
777
|
+
* @param {string} [as.form] - The word's form, for the rows' check.
|
|
778
|
+
*/
|
|
779
|
+
const checkList = (list, keyPath, { kind, form = "" }) => {
|
|
780
|
+
const base = `data.${keyPath.join(".").replace(/\.(\d+)/g, "[$1]")}`;
|
|
636
781
|
const stated = new Map();
|
|
637
|
-
const entries =
|
|
782
|
+
const entries = list.map((entry) =>
|
|
638
783
|
isMap(entry) && typeof entry.map === "string" && typeof entry.text === "string" ?
|
|
639
784
|
{
|
|
640
785
|
map: entry.map,
|
|
641
786
|
font: typeof entry.font === "string" ? entry.font : undefined,
|
|
642
787
|
text: entry.text,
|
|
788
|
+
ending: typeof entry.ending === "string" ? entry.ending : undefined,
|
|
643
789
|
}
|
|
644
790
|
: undefined,
|
|
645
791
|
);
|
|
792
|
+
const persons = isMap(fm.data.grammar?.verbs?.persons);
|
|
646
793
|
entries.forEach((entry, j) => {
|
|
647
794
|
if (!entry) return;
|
|
648
|
-
const label =
|
|
795
|
+
const label = `${base}[${j}]`;
|
|
649
796
|
const map = maps.get(entry.map);
|
|
650
797
|
if (!map) {
|
|
651
798
|
const near = nearest(entry.map, [...maps.keys()]);
|
|
652
799
|
findings.push(
|
|
653
800
|
finding(
|
|
654
|
-
[
|
|
801
|
+
[...keyPath, j, "map"],
|
|
655
802
|
`\`${label}.map\` is \`${entry.map}\`, which is not a map: a native form names ` +
|
|
656
803
|
"the `data.map.name` of a mapping note" +
|
|
657
804
|
(near ? `; did you mean \`${near}\`?` : ""),
|
|
@@ -659,24 +806,51 @@ export function checkLexiconNatives(note, ctx) {
|
|
|
659
806
|
);
|
|
660
807
|
return;
|
|
661
808
|
}
|
|
809
|
+
if (kind !== "word" && !map.logographic) {
|
|
810
|
+
findings.push(
|
|
811
|
+
finding(
|
|
812
|
+
[...keyPath, j],
|
|
813
|
+
`\`${label}\` names the map \`${map.name}\`, which is not logographic; ` +
|
|
814
|
+
`${kind === "affix" ? "an affix" : "an ending"} is written by a native ` +
|
|
815
|
+
"form only under a logographic map",
|
|
816
|
+
),
|
|
817
|
+
);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
662
820
|
if (entry.font !== undefined && map.fonts.size && !map.fonts.has(entry.font)) {
|
|
663
821
|
findings.push(
|
|
664
822
|
finding(
|
|
665
|
-
[
|
|
823
|
+
[...keyPath, j, "font"],
|
|
666
824
|
`\`${label}.font\` is \`${entry.font}\`, which is not a font of the map ` +
|
|
667
825
|
`\`${map.name}\`; it is for ${said([...map.fonts.keys()])}`,
|
|
668
826
|
),
|
|
669
827
|
);
|
|
670
828
|
return;
|
|
671
829
|
}
|
|
672
|
-
|
|
830
|
+
if (
|
|
831
|
+
kind === "ending" &&
|
|
832
|
+
entry.ending !== undefined &&
|
|
833
|
+
entry.ending !== WRITTEN_ENDINGS[0] &&
|
|
834
|
+
WRITTEN_ENDINGS.includes(entry.ending) &&
|
|
835
|
+
!persons
|
|
836
|
+
)
|
|
837
|
+
findings.push(
|
|
838
|
+
finding(
|
|
839
|
+
[...keyPath, j],
|
|
840
|
+
`\`${label}\` writes the ending \`${entry.ending}\`, and \`data.grammar.verbs\` ` +
|
|
841
|
+
"states no `persons`",
|
|
842
|
+
),
|
|
843
|
+
);
|
|
844
|
+
const slot = `${entry.ending ?? ""}\u0000${entry.map}\u0000${entry.font ?? ""}`;
|
|
673
845
|
if (stated.has(slot))
|
|
674
846
|
findings.push(
|
|
675
847
|
finding(
|
|
676
|
-
[
|
|
677
|
-
`\`${label}\` states a second native form
|
|
848
|
+
[...keyPath, j],
|
|
849
|
+
`\`${label}\` states a second native form` +
|
|
850
|
+
(kind === "ending" ? ` of the ending \`${entry.ending}\`` : "") +
|
|
851
|
+
` for the map \`${map.name}\`` +
|
|
678
852
|
(entry.font === undefined ? "" : ` in the font \`${entry.font}\``) +
|
|
679
|
-
`; the first is
|
|
853
|
+
`; the first is \`${base}[${stated.get(slot)}]\``,
|
|
680
854
|
),
|
|
681
855
|
);
|
|
682
856
|
else stated.set(slot, j);
|
|
@@ -684,10 +858,14 @@ export function checkLexiconNatives(note, ctx) {
|
|
|
684
858
|
entry.font !== undefined ? [entry.font]
|
|
685
859
|
: map.fonts.size ? [...map.fonts.keys()]
|
|
686
860
|
: [undefined];
|
|
687
|
-
if (
|
|
861
|
+
if (
|
|
862
|
+
kind === "word" &&
|
|
863
|
+
!map.logographic &&
|
|
864
|
+
fonts.every((font) => applyMap(map, form, { font }).text === entry.text)
|
|
865
|
+
)
|
|
688
866
|
findings.push(
|
|
689
867
|
finding(
|
|
690
|
-
[
|
|
868
|
+
[...keyPath, j],
|
|
691
869
|
`\`${label}\` states what the rows already give: the map \`${map.name}\` ` +
|
|
692
870
|
`spells \`${form}\` so ` +
|
|
693
871
|
(entry.font !== undefined ? `in the font \`${entry.font}\``
|
|
@@ -705,7 +883,11 @@ export function checkLexiconNatives(note, ctx) {
|
|
|
705
883
|
: [...map.fonts.keys()].filter(
|
|
706
884
|
(font) =>
|
|
707
885
|
!entries.some(
|
|
708
|
-
(other) =>
|
|
886
|
+
(other) =>
|
|
887
|
+
other &&
|
|
888
|
+
other.map === entry.map &&
|
|
889
|
+
other.ending === entry.ending &&
|
|
890
|
+
other.font === font,
|
|
709
891
|
),
|
|
710
892
|
);
|
|
711
893
|
for (const slug of applies) {
|
|
@@ -718,7 +900,7 @@ export function checkLexiconNatives(note, ctx) {
|
|
|
718
900
|
seen.add(point);
|
|
719
901
|
findings.push(
|
|
720
902
|
finding(
|
|
721
|
-
[
|
|
903
|
+
[...keyPath, j],
|
|
722
904
|
`\`${label}\` holds ${codeName(point)}, which the font \`${slug}\`, ` +
|
|
723
905
|
`listed in the map \`${map.name}\`, does not draw`,
|
|
724
906
|
),
|
|
@@ -726,7 +908,21 @@ export function checkLexiconNatives(note, ctx) {
|
|
|
726
908
|
}
|
|
727
909
|
}
|
|
728
910
|
});
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
listOf(fm.data.words).forEach((word, i) => {
|
|
914
|
+
if (!isMap(word) || !Array.isArray(word.native)) return;
|
|
915
|
+
checkList(word.native, ["words", i, "native"], {
|
|
916
|
+
kind: "word",
|
|
917
|
+
form: typeof word.form === "string" ? word.form : "",
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
listOf(fm.data.affixes).forEach((affix, i) => {
|
|
921
|
+
if (isMap(affix) && Array.isArray(affix.native))
|
|
922
|
+
checkList(affix.native, ["affixes", i, "native"], { kind: "affix" });
|
|
729
923
|
});
|
|
924
|
+
if (isMap(fm.data.grammar) && Array.isArray(fm.data.grammar.native))
|
|
925
|
+
checkList(fm.data.grammar.native, ["grammar", "native"], { kind: "ending" });
|
|
730
926
|
return findings;
|
|
731
927
|
}
|
|
732
928
|
|
package/engine/map-places.mjs
CHANGED
|
@@ -131,12 +131,13 @@ function parentShortcode(parent) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* A `parents
|
|
134
|
+
* A list of places — `parents`, a being's `homes`, a scenario's `locations` —
|
|
135
|
+
* as their shortcodes.
|
|
135
136
|
*
|
|
136
137
|
* @param {unknown} value - The authored value, a list or a lone entry.
|
|
137
138
|
* @returns {string[]} Shortcodes, lower case, empties dropped.
|
|
138
139
|
*/
|
|
139
|
-
function
|
|
140
|
+
export function placeShortcodes(value) {
|
|
140
141
|
const list =
|
|
141
142
|
Array.isArray(value) ? value
|
|
142
143
|
: value ? [value]
|
|
@@ -198,7 +199,7 @@ export function placesFromRecords(records, { contentBase, base }) {
|
|
|
198
199
|
name,
|
|
199
200
|
type: "place",
|
|
200
201
|
subType: record.subType === undefined ? undefined : String(record.subType),
|
|
201
|
-
parents:
|
|
202
|
+
parents: placeShortcodes(data.parents),
|
|
202
203
|
borders: relationList(data.borders),
|
|
203
204
|
routes: relationList(data.routes),
|
|
204
205
|
address: record.address?.canonical,
|
|
@@ -251,7 +252,7 @@ function addForeignPlaces(world, foreignIndex) {
|
|
|
251
252
|
name: String(entry.name ?? shortcode),
|
|
252
253
|
type: "place",
|
|
253
254
|
subType: entry.subType === undefined ? undefined : String(entry.subType),
|
|
254
|
-
parents:
|
|
255
|
+
parents: placeShortcodes(entry.parents),
|
|
255
256
|
borders: relationList(entry.borders),
|
|
256
257
|
routes: relationList(entry.routes),
|
|
257
258
|
address: canonical,
|
|
@@ -25,8 +25,6 @@ import { noteCalendar } from "./note-calendars.mjs";
|
|
|
25
25
|
/**
|
|
26
26
|
* An inline expression, as an author writes one.
|
|
27
27
|
*
|
|
28
|
-
* Hugo shortcodes begin `{{<` or `{{%` and stay in the source for Hugo.
|
|
29
|
-
*
|
|
30
28
|
* Exported because a renderer that meets one has met a body this pass never ran
|
|
31
29
|
* over, and has to be able to say so.
|
|
32
30
|
*
|
|
@@ -51,11 +51,16 @@ import {
|
|
|
51
51
|
printParseErrorCode,
|
|
52
52
|
} from "jsonc-parser";
|
|
53
53
|
import markdownIt from "markdown-it";
|
|
54
|
-
import { applyFixes, resolveModule } from "markdownlint";
|
|
55
|
-
import { extendConfig, lint, readConfig } from "markdownlint/promise";
|
|
56
54
|
import { minimatch } from "minimatch";
|
|
57
55
|
import { LineCounter, parse as parseYaml, parseDocument } from "yaml";
|
|
58
56
|
|
|
57
|
+
import {
|
|
58
|
+
applyFixes,
|
|
59
|
+
extendConfig,
|
|
60
|
+
lint,
|
|
61
|
+
readConfig,
|
|
62
|
+
resolveModule,
|
|
63
|
+
} from "../vendor/markdownlint.mjs";
|
|
59
64
|
import { walkFiles } from "./walk-files.mjs";
|
|
60
65
|
|
|
61
66
|
/** Option-file names, in the order the first existing one is taken. */
|
|
@@ -313,7 +318,8 @@ async function validateOptions(file, text, raw, isRoot) {
|
|
|
313
318
|
*/
|
|
314
319
|
async function extendRules(file, config) {
|
|
315
320
|
try {
|
|
316
|
-
|
|
321
|
+
// `extendConfig`, unlike `readConfig`, has no default file system.
|
|
322
|
+
return await extendConfig(config, file, [...CONFIG_PARSERS], fs);
|
|
317
323
|
} catch (err) {
|
|
318
324
|
throw new MarkdownConfigError(file, `cannot resolve \`extends\`: ${err.message}`);
|
|
319
325
|
}
|
|
@@ -14,6 +14,26 @@
|
|
|
14
14
|
import { scanBlocks } from "./content-blocks.mjs";
|
|
15
15
|
import { scanAlerts } from "./content-alerts.mjs";
|
|
16
16
|
import { scanRuns } from "./content-scripts.mjs";
|
|
17
|
+
import { matchAllOutsideCode } from "./code-fences.mjs";
|
|
18
|
+
|
|
19
|
+
/** The opening of a shortcode: `{{<` or `{{%`. */
|
|
20
|
+
const SHORTCODE = /\{\{[<%]/g;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Locate every shortcode outside code.
|
|
24
|
+
* @param {string} body - Markdown with its frontmatter masked.
|
|
25
|
+
* @returns {Array<{line: number, column: number, message: string}>} One error per shortcode.
|
|
26
|
+
*/
|
|
27
|
+
function shortcodeErrors(body) {
|
|
28
|
+
return matchAllOutsideCode(body, SHORTCODE).map((match) => {
|
|
29
|
+
const before = body.slice(0, match.index);
|
|
30
|
+
return {
|
|
31
|
+
line: before.split("\n").length,
|
|
32
|
+
column: match.index - before.lastIndexOf("\n"),
|
|
33
|
+
message: "A shortcode is not supported; write `[x]{.glyph}` or `[signs]{.cartouche}`",
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
17
37
|
|
|
18
38
|
/**
|
|
19
39
|
* Validate authored block syntax with positions in the original file.
|
|
@@ -39,6 +59,7 @@ export function markupFindings(source, file, syntax, runs) {
|
|
|
39
59
|
...(scanned?.blocks ?? scanBlocks(body)).errors,
|
|
40
60
|
...(scanned?.alerts ?? scanAlerts(body)).errors,
|
|
41
61
|
...(marked?.errors ?? []),
|
|
62
|
+
...shortcodeErrors(body),
|
|
42
63
|
].map((error) => ({
|
|
43
64
|
file,
|
|
44
65
|
...error,
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
* **A metre is declared once, as an entry of a `doc` note's `data.metres`**:
|
|
16
|
+
* its `name`, the `beats` of a line and the `kind` of count. This module holds
|
|
17
|
+
* the declaration and its field check, and imports nothing that reads the note
|
|
18
|
+
* vocabulary, which declares the field with them; reading the metres of an
|
|
19
|
+
* index and counting a verse against them is {@link module:engine/verse-metre}.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { positionOfFrontmatterPath } from "./diagnostics.mjs";
|
|
25
|
+
|
|
26
|
+
/** Whether a value is a plain map. */
|
|
27
|
+
const isMap = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
28
|
+
|
|
29
|
+
/** A list value, or an empty one. */
|
|
30
|
+
const listOf = (value) => (Array.isArray(value) ? value : []);
|
|
31
|
+
|
|
32
|
+
/** What a metre's `kind` may be. */
|
|
33
|
+
export const METRE_KINDS = Object.freeze(["accentual"]);
|
|
34
|
+
|
|
35
|
+
/** What a metre's `name` may be. */
|
|
36
|
+
export const METRE_NAME = /^[a-z0-9-]+$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One declared metre.
|
|
40
|
+
*
|
|
41
|
+
* @typedef {object} Metre
|
|
42
|
+
* @property {string} name - What `scan=` names it by.
|
|
43
|
+
* @property {number} beats - The beats of a line.
|
|
44
|
+
* @property {string} kind - How they are counted: `accentual`.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/** The notes declaring each metre name, read once per lint index. */
|
|
48
|
+
const declaredNames = new WeakMap();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Each metre name the index's notes declare, and the notes declaring it.
|
|
52
|
+
*
|
|
53
|
+
* @param {{notes?: Iterable<{file: string, fm?: Record<string, any>}>}} index - The lint index.
|
|
54
|
+
* @returns {Map<string, string[]>} Name → the files that declare it.
|
|
55
|
+
*/
|
|
56
|
+
function namesIn(index) {
|
|
57
|
+
const known = declaredNames.get(index);
|
|
58
|
+
if (known) return known;
|
|
59
|
+
const names = new Map();
|
|
60
|
+
for (const note of index.notes ?? [])
|
|
61
|
+
for (const entry of listOf(note?.fm?.data?.metres)) {
|
|
62
|
+
if (!isMap(entry) || typeof entry.name !== "string") continue;
|
|
63
|
+
if (!names.has(entry.name)) names.set(entry.name, new Set());
|
|
64
|
+
names.get(entry.name).add(note.file);
|
|
65
|
+
}
|
|
66
|
+
const out = new Map([...names].map(([name, files]) => [name, [...files]]));
|
|
67
|
+
declaredNames.set(index, out);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check a note's `data.metres`: each name's form, that no other entry declares
|
|
73
|
+
* it, and that a beat count is a whole number above zero. The data-key check
|
|
74
|
+
* owns every key, its kind and the closed `kind` list.
|
|
75
|
+
*
|
|
76
|
+
* @param {{file: string, raw?: string, fm?: Record<string, any>}} note - The note.
|
|
77
|
+
* @param {{index?: {notes?: Iterable<object>}}} [context] - The lint index.
|
|
78
|
+
* @returns {object[]} The findings, each at its key.
|
|
79
|
+
*/
|
|
80
|
+
export function checkMetresField(note, { index } = {}) {
|
|
81
|
+
const list = note?.fm?.data?.metres;
|
|
82
|
+
if (!Array.isArray(list)) return [];
|
|
83
|
+
const raw = note.raw ?? "";
|
|
84
|
+
const finding = (keyPath, message) => ({
|
|
85
|
+
file: note.file,
|
|
86
|
+
...positionOfFrontmatterPath(raw, ["data", "metres", ...keyPath]),
|
|
87
|
+
severity: "error",
|
|
88
|
+
message,
|
|
89
|
+
});
|
|
90
|
+
const findings = [];
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
list.forEach((entry, at) => {
|
|
93
|
+
if (!isMap(entry)) return;
|
|
94
|
+
const { name, beats } = entry;
|
|
95
|
+
if (typeof name === "string") {
|
|
96
|
+
const label = `data.metres[${at}].name`;
|
|
97
|
+
if (!METRE_NAME.test(name))
|
|
98
|
+
findings.push(
|
|
99
|
+
finding(
|
|
100
|
+
[at, "name"],
|
|
101
|
+
`\`${label}\` is \`${name}\`; a metre's name is lowercase letters, digits and \`-\``,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
else if (seen.has(name))
|
|
105
|
+
findings.push(
|
|
106
|
+
finding(
|
|
107
|
+
[at, "name"],
|
|
108
|
+
`\`${label}\` is \`${name}\`, which this note declares twice; a metre's name is unique`,
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
else {
|
|
112
|
+
const others = (index ? (namesIn(index).get(name) ?? []) : []).filter(
|
|
113
|
+
(file) => file !== note.file,
|
|
114
|
+
);
|
|
115
|
+
if (others.length)
|
|
116
|
+
findings.push(
|
|
117
|
+
finding(
|
|
118
|
+
[at, "name"],
|
|
119
|
+
`\`${label}\` is \`${name}\`, which ${others.join(", ")} also declares; a metre's name is unique`,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
seen.add(name);
|
|
124
|
+
}
|
|
125
|
+
if (beats !== undefined && Number.isFinite(Number(beats)) && !(Number(beats) >= 1))
|
|
126
|
+
findings.push(
|
|
127
|
+
finding(
|
|
128
|
+
[at, "beats"],
|
|
129
|
+
`\`data.metres[${at}].beats\` is \`${String(beats)}\`; a metre has one or more beats`,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
return findings;
|
|
134
|
+
}
|
package/engine/note-matter.mjs
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* exactly as the content index does ({@link splitFrontmatter} and `yaml`), so a
|
|
17
17
|
* value reads the same on the site and in the preview as in a compendium pack.
|
|
18
18
|
* Writing serializes with `js-yaml`, which quotes every string a YAML 1.1
|
|
19
|
-
* reader would take for a boolean, a number or a timestamp, so
|
|
20
|
-
* value back as the string or number it was.
|
|
19
|
+
* reader would take for a boolean, a number or a timestamp, so each
|
|
20
|
+
* value reads back as the string or number it was.
|
|
21
21
|
*
|
|
22
22
|
* @module
|
|
23
23
|
*/
|