@heroiclands/package-build 25.6.1 → 25.6.3
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 +12 -0
- package/bin/content-commands.mjs +51 -10
- package/content-config.mjs +90 -8
- package/docs/api.md +17 -16
- package/docs/commands.md +29 -2
- package/docs/configuration.md +5 -0
- package/docs/encoding-core.md +19 -4
- package/docs/guides/site.md +30 -0
- package/docs/guides/writing-in-invented-scripts.md +8 -2
- package/docs/reference/format-details.md +2 -1
- package/docs/reference/note-types.md +2 -1
- package/engine/encoding-integrity.mjs +3 -3
- package/engine/encoding-template.mjs +24 -4
- package/engine/encoding-terms.mjs +8 -1
- package/engine/encoding.mjs +27 -6
- package/package.json +1 -1
- package/types/content-config.d.mts +14 -0
- package/types/engine/encoding-integrity.d.mts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 25.6.3
|
|
4
|
+
|
|
5
|
+
**Rendering and sites**
|
|
6
|
+
|
|
7
|
+
- Build a preview at another address with `site build --base-url` and serve its images from another asset root with `--assets`. Pages, links, script fonts and search use the preview prefix, while the package's authored configuration stays unchanged.
|
|
8
|
+
|
|
9
|
+
## 25.6.2
|
|
10
|
+
|
|
11
|
+
**Languages and scripts**
|
|
12
|
+
|
|
13
|
+
- Encoding documents can choose a mapping according to the letter before it, as well as the following letter and word position.
|
|
14
|
+
|
|
3
15
|
## 25.6.1
|
|
4
16
|
|
|
5
17
|
**Languages and scripts**
|
package/bin/content-commands.mjs
CHANGED
|
@@ -46,7 +46,12 @@ import log from "loglevel";
|
|
|
46
46
|
import prefix from "loglevel-plugin-prefix";
|
|
47
47
|
import { readStdin } from "../engine/read-stdin.mjs";
|
|
48
48
|
import { compilePacks, cleanPacks, unpackPacks } from "../engine/compendiums.mjs";
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
compilesFoundryDocuments,
|
|
51
|
+
publishesContentPages,
|
|
52
|
+
siteBase,
|
|
53
|
+
siteBuildConfig,
|
|
54
|
+
} from "../content-config.mjs";
|
|
50
55
|
import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
|
|
51
56
|
import { buildPdf, listBookAssets } from "../engine/pdf-build.mjs";
|
|
52
57
|
import { summaryLines } from "../engine/pdf-coverage.mjs";
|
|
@@ -115,7 +120,7 @@ import {
|
|
|
115
120
|
} from "../engine/encoding-lexicon.mjs";
|
|
116
121
|
import { checkFormatting, checkPrettierConventions, lintMarkdown } from "../engine/prose-lint.mjs";
|
|
117
122
|
import { lintProse, scoreProseTree } from "../engine/readability-lint.mjs";
|
|
118
|
-
import { loadPackageBuildConfig } from "../config.mjs";
|
|
123
|
+
import { checkHomepage, loadPackageBuildConfig } from "../config.mjs";
|
|
119
124
|
import {
|
|
120
125
|
emitContentIndex,
|
|
121
126
|
prepareContentIndex,
|
|
@@ -520,7 +525,7 @@ function withIndexPreflight(command, shouldCheck = () => true) {
|
|
|
520
525
|
handler: async (argv) => {
|
|
521
526
|
let config;
|
|
522
527
|
try {
|
|
523
|
-
config = loadPackConfig();
|
|
528
|
+
config = argv[CONTENT_CONFIG] ?? loadPackConfig();
|
|
524
529
|
if (shouldCheck(config, argv)) cachedMetadataIndexes(config);
|
|
525
530
|
} catch (err) {
|
|
526
531
|
if (config) {
|
|
@@ -2279,9 +2284,11 @@ function siteCommand() {
|
|
|
2279
2284
|
builder: (yargs) =>
|
|
2280
2285
|
yargs
|
|
2281
2286
|
.command(
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2287
|
+
withSiteBuildConfig(
|
|
2288
|
+
withIndexPreflight(
|
|
2289
|
+
withBodyPreflight(siteBuildCommand()),
|
|
2290
|
+
publishesContentPages,
|
|
2291
|
+
),
|
|
2285
2292
|
),
|
|
2286
2293
|
)
|
|
2287
2294
|
.command(
|
|
@@ -2294,6 +2301,29 @@ function siteCommand() {
|
|
|
2294
2301
|
};
|
|
2295
2302
|
}
|
|
2296
2303
|
|
|
2304
|
+
/** Apply runtime site settings before the index and body preflights. */
|
|
2305
|
+
function withSiteBuildConfig(command) {
|
|
2306
|
+
const handler = command.handler;
|
|
2307
|
+
return {
|
|
2308
|
+
...command,
|
|
2309
|
+
handler: async (argv) => {
|
|
2310
|
+
if (argv.baseUrl === undefined && argv.assets === undefined) return handler(argv);
|
|
2311
|
+
try {
|
|
2312
|
+
const authored = argv[CONTENT_CONFIG] ?? loadPackConfig();
|
|
2313
|
+
checkHomepage(authored.homepage, authored.contentPackage, authored.site.base);
|
|
2314
|
+
const config = siteBuildConfig(authored, {
|
|
2315
|
+
baseUrl: argv.baseUrl,
|
|
2316
|
+
assets: argv.assets,
|
|
2317
|
+
});
|
|
2318
|
+
return await handler({ ...argv, [CONTENT_CONFIG]: config });
|
|
2319
|
+
} catch (err) {
|
|
2320
|
+
reportFailure(err);
|
|
2321
|
+
process.exitCode = 1;
|
|
2322
|
+
}
|
|
2323
|
+
},
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2297
2327
|
/**
|
|
2298
2328
|
* Report what the layouts, or the build's section gate, found about a site.
|
|
2299
2329
|
*
|
|
@@ -2342,10 +2372,21 @@ function siteBuildCommand() {
|
|
|
2342
2372
|
command: "build",
|
|
2343
2373
|
describe: "Render the content tree as a website and index it for search",
|
|
2344
2374
|
builder: (yargs) =>
|
|
2345
|
-
yargs
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2375
|
+
yargs
|
|
2376
|
+
.option("out", {
|
|
2377
|
+
type: "string",
|
|
2378
|
+
describe: `The directory that is deployed (default: ${DEPLOY_ROOT})`,
|
|
2379
|
+
})
|
|
2380
|
+
.option("base-url", {
|
|
2381
|
+
type: "string",
|
|
2382
|
+
requiresArg: true,
|
|
2383
|
+
describe: "Absolute HTTP(S) site URL for this build, ending in /",
|
|
2384
|
+
})
|
|
2385
|
+
.option("assets", {
|
|
2386
|
+
type: "string",
|
|
2387
|
+
requiresArg: true,
|
|
2388
|
+
describe: "Absolute HTTP(S) asset root for this build",
|
|
2389
|
+
}),
|
|
2349
2390
|
handler: async (argv) => {
|
|
2350
2391
|
try {
|
|
2351
2392
|
const config = argv[CONTENT_CONFIG] ?? loadPackConfig();
|
package/content-config.mjs
CHANGED
|
@@ -228,6 +228,78 @@ export function siteBase(config) {
|
|
|
228
228
|
return config.site.base || `/${config.contentPackage}/`;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Apply validated runtime site URLs to a resolved configuration, without changing it.
|
|
233
|
+
* The caller checks the authored homepage before applying these build options.
|
|
234
|
+
*
|
|
235
|
+
* @param {ContentBuildConfig} config - The resolved authored configuration.
|
|
236
|
+
* @param {object} options - Runtime options.
|
|
237
|
+
* @param {string} [options.baseUrl] - Absolute HTTP(S) site URL ending in `/`.
|
|
238
|
+
* @param {string} [options.assets] - Absolute HTTP(S) asset root.
|
|
239
|
+
* @returns {ContentBuildConfig} The effective configuration, frozen.
|
|
240
|
+
*/
|
|
241
|
+
export function siteBuildConfig(config, { baseUrl, assets }) {
|
|
242
|
+
if (baseUrl === undefined && assets === undefined) return config;
|
|
243
|
+
const homepage = baseUrl === undefined ? config.homepage : buildUrl(baseUrl, "--base-url").href;
|
|
244
|
+
const base =
|
|
245
|
+
baseUrl === undefined ? config.site.base : normalizeSiteBase(new URL(homepage).pathname);
|
|
246
|
+
const assetRoot =
|
|
247
|
+
assets === undefined ?
|
|
248
|
+
config.site.assets
|
|
249
|
+
: normalizeSiteAssets(buildUrl(assets, "--assets").href);
|
|
250
|
+
checkSiteSitemaps(config.site.sitemaps, base, config.contentPackage);
|
|
251
|
+
return Object.freeze({
|
|
252
|
+
...config,
|
|
253
|
+
homepage,
|
|
254
|
+
site: Object.freeze({ ...config.site, base, assets: assetRoot }),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A runtime web root must survive URL joining and mapping to a local directory.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} value - The command-line value.
|
|
262
|
+
* @param {string} option - The option naming it.
|
|
263
|
+
* @returns {URL} The checked URL.
|
|
264
|
+
*/
|
|
265
|
+
function buildUrl(value, option) {
|
|
266
|
+
// URL parsing consumes both literal and escaped dot segments.
|
|
267
|
+
const rawPath = /^https?:\/\/[^/?#]+([^?#]*)/i.exec(value)?.[1] ?? "";
|
|
268
|
+
if (
|
|
269
|
+
rawPath.includes("%") ||
|
|
270
|
+
rawPath.split("/").some((segment) => segment === "." || segment === "..")
|
|
271
|
+
) {
|
|
272
|
+
throw new TypeError(
|
|
273
|
+
`${option} must use a plain path without percent escapes or dot segments`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
let url;
|
|
277
|
+
try {
|
|
278
|
+
url = new URL(value);
|
|
279
|
+
} catch {
|
|
280
|
+
throw new TypeError(`${option} must be an absolute HTTP(S) URL`);
|
|
281
|
+
}
|
|
282
|
+
if (
|
|
283
|
+
!/^https?:\/\/[^/]+/i.test(value) ||
|
|
284
|
+
/[\s\\]/.test(value) ||
|
|
285
|
+
!["http:", "https:"].includes(url.protocol) ||
|
|
286
|
+
url.username ||
|
|
287
|
+
url.password ||
|
|
288
|
+
value.includes("?") ||
|
|
289
|
+
value.includes("#") ||
|
|
290
|
+
url.pathname.includes("//") ||
|
|
291
|
+
url.pathname.includes("%")
|
|
292
|
+
) {
|
|
293
|
+
throw new TypeError(
|
|
294
|
+
`${option} must be an absolute HTTP(S) URL with a plain path and no credentials, query or fragment`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (option === "--base-url" && !url.pathname.endsWith("/")) {
|
|
298
|
+
throw new TypeError(`${option} must end with /`);
|
|
299
|
+
}
|
|
300
|
+
return url;
|
|
301
|
+
}
|
|
302
|
+
|
|
231
303
|
/**
|
|
232
304
|
* Whether this package publishes the pages its content tree compiles to.
|
|
233
305
|
*
|
|
@@ -1874,6 +1946,23 @@ function packageSiteList(value, field, contentPackage) {
|
|
|
1874
1946
|
);
|
|
1875
1947
|
}
|
|
1876
1948
|
|
|
1949
|
+
/**
|
|
1950
|
+
* @param {readonly string[]} sitemaps - The configured package sitemaps.
|
|
1951
|
+
* @param {string} base - The site path.
|
|
1952
|
+
* @param {string} contentPackage - The package id.
|
|
1953
|
+
* @returns {void}
|
|
1954
|
+
*/
|
|
1955
|
+
function checkSiteSitemaps(sitemaps, base, contentPackage) {
|
|
1956
|
+
if (sitemaps.length && base !== "/") {
|
|
1957
|
+
fail(
|
|
1958
|
+
"site.sitemaps",
|
|
1959
|
+
"names the package sites a sitemap index lists, and a sitemap index and " +
|
|
1960
|
+
"`robots.txt` belong at the root of an origin; this site is served at " +
|
|
1961
|
+
`\`${base || `/${contentPackage}/`}\`, so declare \`site.base: /\` or drop the key`,
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1877
1966
|
/**
|
|
1878
1967
|
* The `site` section — how this repository frames the website it publishes.
|
|
1879
1968
|
*
|
|
@@ -1939,14 +2028,7 @@ function normalizeSite(value, contentPackage) {
|
|
|
1939
2028
|
const base = normalizeSiteBase(input.base);
|
|
1940
2029
|
const search = optionalBoolean(input.search, "site.search", true);
|
|
1941
2030
|
const sitemaps = packageSiteList(input.sitemaps, "site.sitemaps", contentPackage);
|
|
1942
|
-
|
|
1943
|
-
fail(
|
|
1944
|
-
"site.sitemaps",
|
|
1945
|
-
"names the package sites a sitemap index lists, and a sitemap index and " +
|
|
1946
|
-
"`robots.txt` belong at the root of an origin; this site is served at " +
|
|
1947
|
-
`\`${base || `/${contentPackage}/`}\`, so declare \`site.base: /\` or drop the key`,
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
2031
|
+
checkSiteSitemaps(sitemaps, base, contentPackage);
|
|
1950
2032
|
const mergeIndexes = packageSiteList(input.mergeIndexes, "site.mergeIndexes", contentPackage);
|
|
1951
2033
|
if (mergeIndexes.length && !search) {
|
|
1952
2034
|
fail(
|
package/docs/api.md
CHANGED
|
@@ -2193,20 +2193,21 @@ const config = defineConfig({
|
|
|
2193
2193
|
});
|
|
2194
2194
|
```
|
|
2195
2195
|
|
|
2196
|
-
| Export | Signature
|
|
2197
|
-
| -------------------------- |
|
|
2198
|
-
| `defineConfig` | `defineConfig(config)`
|
|
2199
|
-
| `PACKAGE_KINDS` | `const PACKAGE_KINDS`
|
|
2200
|
-
| `DOCUMENTATION_KIND` | `const DOCUMENTATION_KIND`
|
|
2201
|
-
| `AUTHOR_ROLES` | `const AUTHOR_ROLES`
|
|
2202
|
-
| `RELATIONSHIP_KEYS` | `const RELATIONSHIP_KEYS`
|
|
2203
|
-
| `compilesFoundryDocuments` | `compilesFoundryDocuments(config)`
|
|
2204
|
-
| `PACK_DOCUMENT_TYPES` | `const PACK_DOCUMENT_TYPES`
|
|
2205
|
-
| `DEFAULT_PATHS` | `const DEFAULT_PATHS`
|
|
2206
|
-
| `DEFAULT_ADDRESS_SCHEME` | `const DEFAULT_ADDRESS_SCHEME`
|
|
2207
|
-
| `DERIVED_SYSTEM_VERSION` | `const DERIVED_SYSTEM_VERSION`
|
|
2208
|
-
| `publishesContentPages` | `publishesContentPages(config)`
|
|
2209
|
-
| `siteBase` | `siteBase(config)`
|
|
2196
|
+
| Export | Signature | Returns | Use it when |
|
|
2197
|
+
| -------------------------- | ---------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2198
|
+
| `defineConfig` | `defineConfig(config)` | `ContentBuildConfig` — the frozen, defaulted configuration | validating and normalizing a content configuration; every configuration reaches this function whether authored as YAML or as `.mjs` |
|
|
2199
|
+
| `PACKAGE_KINDS` | `const PACKAGE_KINDS` | — | reading the three kinds of Foundry package a content module can be built into — `systems`, `modules`, and `documentation`, the kind that compiles no Foundry document at all (also the Foundry install directory name for the first two) |
|
|
2200
|
+
| `DOCUMENTATION_KIND` | `const DOCUMENTATION_KIND` | — | naming the `packageKind` value that compiles no Foundry documents, spelled once so the validator, the CLI and the compile passes cannot disagree about what it means |
|
|
2201
|
+
| `AUTHOR_ROLES` | `const AUTHOR_ROLES` | `readonly string[]` | reading the closed list an `authors` entry's `role` is drawn from — the configuration check refuses any other value |
|
|
2202
|
+
| `RELATIONSHIP_KEYS` | `const RELATIONSHIP_KEYS` | `readonly string[]` | reading every key a declared relationship may carry — the configuration check refuses anything else, and the manifest writer drops the ones Foundry's own schema does not name |
|
|
2203
|
+
| `compilesFoundryDocuments` | `compilesFoundryDocuments(config)` | `boolean` | checking whether a resolved configuration compiles Foundry documents at all — false only for a `documentation` package |
|
|
2204
|
+
| `PACK_DOCUMENT_TYPES` | `const PACK_DOCUMENT_TYPES` | — | reading the Foundry document types this toolchain is able to compile a compendium pack of |
|
|
2205
|
+
| `DEFAULT_PATHS` | `const DEFAULT_PATHS` | — | reading the conventional directory layout a build reads from and writes to, relative to `rootDir` |
|
|
2206
|
+
| `DEFAULT_ADDRESS_SCHEME` | `const DEFAULT_ADDRESS_SCHEME` | — | reading an unconfigured repository's address-scheme defaults (`prefix`, where the content tree mounts inside the package) |
|
|
2207
|
+
| `DERIVED_SYSTEM_VERSION` | `const DERIVED_SYSTEM_VERSION` | — | the loader-only symbol key `defineConfig` uses internally to receive a resolved system version; not something a configuration author writes |
|
|
2208
|
+
| `publishesContentPages` | `publishesContentPages(config)` | `boolean` | checking whether the authored content tree contains notes beyond its homepage, shared by site, PDF, index, and Foundry address emission |
|
|
2209
|
+
| `siteBase` | `siteBase(config)` | `string` | reading the path a package's site is served at — `site.base` where the configuration states one, `/<contentPackage>/` otherwise |
|
|
2210
|
+
| `siteBuildConfig` | `siteBuildConfig(config, { baseUrl, assets })` | `ContentBuildConfig` | applying optional runtime site URLs to a frozen effective configuration; the caller first validates the authored homepage with `checkHomepage`; the CLI uses it before index/body preflight |
|
|
2210
2211
|
|
|
2211
2212
|
## `./config`
|
|
2212
2213
|
|
|
@@ -2891,8 +2892,8 @@ expose their schema `key`.
|
|
|
2891
2892
|
Encoding rules default to exact, uniquely decodable outputs. A note may state
|
|
2892
2893
|
`rules.roundTrip: canonical` to permit aliases and empty code sequences;
|
|
2893
2894
|
`decodeText` then returns the authored conventional reading without restoring
|
|
2894
|
-
erased Roman characters. Contextual rules accept `
|
|
2895
|
-
and `at: start|end|line-start`; supplied conditions combine with AND. Accent
|
|
2895
|
+
erased Roman characters. Contextual rules accept `after` (one exact NFC Unicode letter, `vowel` or
|
|
2896
|
+
`consonant`), `before: vowel|consonant` and `at: start|end|line-start`; supplied conditions combine with AND. `after` governs forward encoding only. Accent
|
|
2896
2897
|
permission blocks accept explicit Boolean values. See [Encoding text](encoding-core.md).
|
|
2897
2898
|
|
|
2898
2899
|
### `engine.encodingLanguage`
|
package/docs/commands.md
CHANGED
|
@@ -2685,7 +2685,7 @@ it for search.
|
|
|
2685
2685
|
**SYNOPSIS**
|
|
2686
2686
|
|
|
2687
2687
|
```
|
|
2688
|
-
package-build site build [--out <dir>]
|
|
2688
|
+
package-build site build [--out <dir>] [--base-url <url>] [--assets <url>]
|
|
2689
2689
|
```
|
|
2690
2690
|
|
|
2691
2691
|
**DESCRIPTION**
|
|
@@ -2760,9 +2760,27 @@ run fails before the search index is built.
|
|
|
2760
2760
|
|
|
2761
2761
|
`--out <dir>` — the directory that is deployed. Defaults to `build/site`.
|
|
2762
2762
|
|
|
2763
|
+
`--base-url <url>` — the absolute HTTP(S) address of this build. Its pathname
|
|
2764
|
+
becomes the effective `site.base`, and must end with `/`; `/` serves the site
|
|
2765
|
+
at the origin's root. Pages, local links, theme files, script fonts and search
|
|
2766
|
+
all use that prefix. Output lands under `<out>/<pathname>/`.
|
|
2767
|
+
|
|
2768
|
+
`--assets <url>` — the absolute HTTP(S) asset root for this build. It may
|
|
2769
|
+
include a path; trailing slashes are trimmed.
|
|
2770
|
+
|
|
2771
|
+
Both options are optional and independent. Without them, the build uses the
|
|
2772
|
+
authored `homepage`, `site.base` and `site.assets`. Overrides apply in memory
|
|
2773
|
+
before preflight and rendering; they never rewrite `package.json` or
|
|
2774
|
+
`package-build.config.yaml`. The authored homepage must still pass its usual
|
|
2775
|
+
validation. Both URLs require plain paths without percent escapes, dot
|
|
2776
|
+
segments (`.` or `..`), or doubled slashes, and reject credentials, queries,
|
|
2777
|
+
fragments, whitespace and backslashes.
|
|
2778
|
+
Invalid overrides fail before output is replaced. A site declaring
|
|
2779
|
+
`site.sitemaps` must still be served at `/`.
|
|
2780
|
+
|
|
2763
2781
|
**EXIT STATUS**
|
|
2764
2782
|
|
|
2765
|
-
1 if `package.json` declares no `homepage`, or one that does not end
|
|
2783
|
+
1 if a runtime URL override is invalid. 1 if `package.json` declares no `homepage`, or one that does not end
|
|
2766
2784
|
`/<contentPackage>/`; if the site declares no title, description or `assets`
|
|
2767
2785
|
host; or if the navigation has not been fetched (`package-build deps fetch`
|
|
2768
2786
|
fills the cache and is named in the message). 1 if any gate fires — no
|
|
@@ -2774,6 +2792,15 @@ findings of severity `warning` included.
|
|
|
2774
2792
|
|
|
2775
2793
|
**EXAMPLES**
|
|
2776
2794
|
|
|
2795
|
+
```bash
|
|
2796
|
+
package-build site build \
|
|
2797
|
+
--base-url https://www.internal.heroiclands.org/thalorna/ \
|
|
2798
|
+
--assets https://cdn.internal.heroiclands.org
|
|
2799
|
+
package-build site build \
|
|
2800
|
+
--base-url https://www.internal.heroiclands.org/pr/thalorna/123/thalorna/ \
|
|
2801
|
+
--assets https://cdn.internal.heroiclands.org
|
|
2802
|
+
```
|
|
2803
|
+
|
|
2777
2804
|
```
|
|
2778
2805
|
$ package-build site build
|
|
2779
2806
|
[…] indexing content
|
package/docs/configuration.md
CHANGED
|
@@ -1284,6 +1284,11 @@ site:
|
|
|
1284
1284
|
mergeIndexes: [sohl, thalorna, kethira, harnensemble]
|
|
1285
1285
|
```
|
|
1286
1286
|
|
|
1287
|
+
The optional [`site build --base-url` and `--assets` flags](commands.md#package-build-site-build)
|
|
1288
|
+
apply runtime overrides to these resolved settings. The base URL supplies the
|
|
1289
|
+
effective homepage and `site.base`; the asset URL supplies effective
|
|
1290
|
+
`site.assets`. The authored configuration stays unchanged and must be valid.
|
|
1291
|
+
|
|
1287
1292
|
### What the site build reads
|
|
1288
1293
|
|
|
1289
1294
|
`package-build site build` composes the site's settings from sources the
|
package/docs/encoding-core.md
CHANGED
|
@@ -55,7 +55,7 @@ second behaviour.
|
|
|
55
55
|
| `vowel-groups` | `{reading: established, allow-splitting: false, unrecognized-adjacent-vowels: separate, groups: [...]}`; the explicit group list determines where an acute belongs on its first vowel. |
|
|
56
56
|
| `diaeresis` | `{allowed: boolean}`; false forbids diaeresis characters; true derives plain-vowel mappings and restores splits of distinctly mapped declared groups. |
|
|
57
57
|
| `grave-accent` | `{allowed: boolean}` permits or forbids explicitly mapped grave-accent characters. |
|
|
58
|
-
| `contextual-mappings` | A list of `{match, emit, before?, at?}`; following-letter or source-position conditions precede unconditional mappings.
|
|
58
|
+
| `contextual-mappings` | A list of `{match, emit, after?, before?, at?}`; preceding-letter, following-letter or source-position conditions precede unconditional mappings. |
|
|
59
59
|
| `y` | `{kind: consonant, sound: "y in yes", vowel-reading: false}`; y cannot also appear in `data.vowels`. |
|
|
60
60
|
|
|
61
61
|
The accent blocks accept an explicit Boolean `allowed`. False forbids that
|
|
@@ -80,13 +80,28 @@ empty outputs or store invisible metadata to restore the original spelling.
|
|
|
80
80
|
A language's authoritative vocabulary can recover fuller word spellings.
|
|
81
81
|
Shared word spaces remain spaces in both modes.
|
|
82
82
|
|
|
83
|
-
Contextual entries use `{match, emit, before?, at?}` and require at least one
|
|
84
|
-
condition. `
|
|
83
|
+
Contextual entries use `{match, emit, after?, before?, at?}` and require at least one
|
|
84
|
+
condition. `after` tests the immediately preceding Unicode scalar in the NFC
|
|
85
|
+
matching stream (including derived diaeresis-to-plain vowel projection). It is
|
|
86
|
+
`vowel`, `consonant`, or one literal NFC Unicode letter, compared exactly and
|
|
87
|
+
case-sensitively; astral letters are permitted. Category conditions require
|
|
88
|
+
`data.vowels`; a literal does not. Neither category matches a missing predecessor, punctuation or space. A literal
|
|
89
|
+
matches only that letter. `after` is a
|
|
90
|
+
positive condition, so refusing an unlisted predecessor requires authored
|
|
91
|
+
allowed alternatives with no unconditional fallback. For example,
|
|
92
|
+
`{match: k, after: a, emit: K_AFTER_A}` selects that font code after `a`.
|
|
93
|
+
|
|
94
|
+
`after` governs forward encoding only, like `at`; decoding does not validate a
|
|
95
|
+
preceding-source condition. Existing inverse `before` checks remain strict in
|
|
96
|
+
exact mode. Source validation permits spellings without mapping coverage for
|
|
97
|
+
authoritative native entries; `encodeText` refuses uncovered source text.
|
|
98
|
+
|
|
99
|
+
`before` is `vowel` or `consonant`; `at` is `start`, `end` or
|
|
85
100
|
`line-start`. `start` and `end` refer to word boundaries. A consonant is a
|
|
86
101
|
letter outside the declared `data.vowels`. Line start follows a newline or
|
|
87
102
|
verse slash and permits preceding indentation carets. Multiple conditions
|
|
88
103
|
combine with AND. Entries for the same source may use different conditions;
|
|
89
|
-
repeating the same
|
|
104
|
+
repeating the same `(match, after, before, at)` tuple is an error. Equal-length
|
|
90
105
|
contextual matches retain authored order.
|
|
91
106
|
|
|
92
107
|
```yaml
|
package/docs/guides/site.md
CHANGED
|
@@ -306,6 +306,36 @@ The terminal reports indexing, rendering, writing and search indexing as they
|
|
|
306
306
|
start, including page counts where available. Progress is on stdout;
|
|
307
307
|
diagnostics retain their file, line and column on stderr.
|
|
308
308
|
|
|
309
|
+
For an internal main preview, supply the build's address and asset root:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
package-build site build \
|
|
313
|
+
--base-url https://www.internal.heroiclands.org/thalorna/ \
|
|
314
|
+
--assets https://cdn.internal.heroiclands.org
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
For a pull request preview, include the whole prefix:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
package-build site build \
|
|
321
|
+
--base-url https://www.internal.heroiclands.org/pr/thalorna/123/thalorna/ \
|
|
322
|
+
--assets https://cdn.internal.heroiclands.org
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The second build writes to `build/site/pr/thalorna/123/thalorna/`. Its local
|
|
326
|
+
links, theme files, script fonts and search stay under that prefix; its images
|
|
327
|
+
use the supplied asset root. `--out` changes the deployment directory above the
|
|
328
|
+
prefix. Both URL options are optional and independent, and require absolute
|
|
329
|
+
HTTP(S) URLs. The base URL's path ends with `/`; the asset root may include a
|
|
330
|
+
path and has trailing slashes trimmed. Use plain paths without percent escapes,
|
|
331
|
+
dot segments (`.` or `..`), or doubled slashes, credentials, queries or fragments.
|
|
332
|
+
|
|
333
|
+
The options apply to this build only. Authored `package.json` and YAML stay
|
|
334
|
+
unchanged, and the authored homepage still has to match the configured site.
|
|
335
|
+
Invalid overrides fail before replacing output. A site with `site.sitemaps`
|
|
336
|
+
requires a base URL at the origin's root. See the
|
|
337
|
+
[command reference](../commands.md#package-build-site-build) for URL validation.
|
|
338
|
+
|
|
309
339
|
```bash
|
|
310
340
|
package-build site serve
|
|
311
341
|
```
|
|
@@ -153,8 +153,14 @@ An encoding's output must decode unambiguously, including across adjacent
|
|
|
153
153
|
mappings and spaces. The compiler refuses indistinguishable spellings rather
|
|
154
154
|
than choosing a guessed reading. See [the executable rules](../encoding-core.md).
|
|
155
155
|
|
|
156
|
-
Mappings match longest first; contextual
|
|
157
|
-
|
|
156
|
+
Mappings match longest first; contextual rules precede their unconditional
|
|
157
|
+
alternatives. A contextual mapping may use `after: vowel`, `after: consonant`
|
|
158
|
+
or one exact NFC letter such as `after: a` to test the preceding source letter.
|
|
159
|
+
It may combine this with `before` and `at`; all supplied conditions must hold.
|
|
160
|
+
The categories use `data.vowels`. These positive conditions need explicit
|
|
161
|
+
allowed alternatives to refuse a predecessor, with no unconditional fallback.
|
|
162
|
+
`after` governs encoding only; see [the executable rules](../encoding-core.md)
|
|
163
|
+
for normalization and decoding details. A case rule may allow lowercase and initial-capital
|
|
158
164
|
groups while refusing other mixed or uppercase group spellings. Encoding keeps
|
|
159
165
|
authored case and never rewrites a word's Roman spelling.
|
|
160
166
|
|
|
@@ -8352,8 +8352,9 @@ did not foresee. Holes found in play are fixed in the scenario itself.
|
|
|
8352
8352
|
| `rules.diaeresis.allowed` | true or false | False forbids the diaeresis; true derives plain-vowel mappings and restores splits where a declared group's native mapping distinguishes joined and separate vowels. Exact conversion refuses an unrecoverable derived group split; canonical conversion keeps its conventional reading. |
|
|
8353
8353
|
| `rules.grave-accent` | a map | Whether grave-accent mappings are allowed. |
|
|
8354
8354
|
| `rules.grave-accent.allowed` | true or false | False forbids the accent; true permits explicitly mapped and accepted characters. |
|
|
8355
|
-
| `rules.contextual-mappings` | a list of conditional mappings | Alternatives conditioned on the following letter or source position, applied before unconditional mappings.
|
|
8355
|
+
| `rules.contextual-mappings` | a list of conditional mappings | Alternatives conditioned on the preceding letter, following letter or source position, applied before unconditional mappings. |
|
|
8356
8356
|
| `rules.contextual-mappings[].match` | a string | The source sequence. |
|
|
8357
|
+
| `rules.contextual-mappings[].after` | one NFC Unicode letter, vowel or consonant | Match after one exact NFC Unicode letter, a declared vowel or another letter. |
|
|
8357
8358
|
| `rules.contextual-mappings[].before` | a string | Match before a declared vowel or another letter. |
|
|
8358
8359
|
| `rules.contextual-mappings[].at` | a string | Match at word start, word end or line start. |
|
|
8359
8360
|
| `rules.contextual-mappings[].emit` | a glyph code or list of codes (empty only with canonical round trips) | The font codes written by this alternative. |
|
|
@@ -1054,8 +1054,9 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
|
|
|
1054
1054
|
| `data.rules.diaeresis.allowed` | true or false | yes | False forbids the diaeresis; true derives plain-vowel mappings and restores splits where a declared group's native mapping distinguishes joined and separate vowels. Exact conversion refuses an unrecoverable derived group split; canonical conversion keeps its conventional reading. |
|
|
1055
1055
|
| `data.rules.grave-accent` | a map | no | Whether grave-accent mappings are allowed. |
|
|
1056
1056
|
| `data.rules.grave-accent.allowed` | true or false | yes | False forbids the accent; true permits explicitly mapped and accepted characters. |
|
|
1057
|
-
| `data.rules.contextual-mappings` | a list of conditional mappings | no | Alternatives conditioned on the following letter or source position, applied before unconditional mappings.
|
|
1057
|
+
| `data.rules.contextual-mappings` | a list of conditional mappings | no | Alternatives conditioned on the preceding letter, following letter or source position, applied before unconditional mappings. |
|
|
1058
1058
|
| `data.rules.contextual-mappings[].match` | a string | yes | The source sequence. |
|
|
1059
|
+
| `data.rules.contextual-mappings[].after` | one NFC Unicode letter, vowel or consonant | no | Match after one exact NFC Unicode letter, a declared vowel or another letter. |
|
|
1059
1060
|
| `data.rules.contextual-mappings[].before` | a string | no | Match before a declared vowel or another letter. |
|
|
1060
1061
|
| `data.rules.contextual-mappings[].at` | a string | no | Match at word start, word end or line start. |
|
|
1061
1062
|
| `data.rules.contextual-mappings[].emit` | a glyph code or list of codes (empty only with canonical round trips) | yes | The font codes written by this alternative. |
|
|
@@ -16,20 +16,20 @@
|
|
|
16
16
|
/**
|
|
17
17
|
* Check codeword integrity with a trie and finite Sardinas–Patterson residuals.
|
|
18
18
|
* Source-equivalent compositions are redundant rules, not distinct spellings.
|
|
19
|
-
* @param {Array<{from:string,to:string,key:string,before?:string,at?:string}>} rows - Validated ordinary rows.
|
|
19
|
+
* @param {Array<{from:string,to:string,key:string,after?:string,before?:string,at?:string}>} rows - Validated ordinary rows.
|
|
20
20
|
* @param {(key:string,message:string)=>never} fault - Positioned schema-error constructor.
|
|
21
21
|
*/
|
|
22
22
|
export function assertUniqueOutputs(rows, fault) {
|
|
23
23
|
const bySource = new Map();
|
|
24
24
|
for (const row of rows) {
|
|
25
|
-
if (row.before || row.at) continue;
|
|
25
|
+
if (row.after || row.before || row.at) continue;
|
|
26
26
|
const first = [...row.from][0];
|
|
27
27
|
if (!bySource.has(first)) bySource.set(first, []);
|
|
28
28
|
bySource.get(first).push(row);
|
|
29
29
|
}
|
|
30
30
|
for (const group of bySource.values()) group.sort((a, b) => b.from.length - a.from.length);
|
|
31
31
|
const redundant = (row) => {
|
|
32
|
-
if (row.before || row.at || [...row.from].length < 2) return false;
|
|
32
|
+
if (row.after || row.before || row.at || [...row.from].length < 2) return false;
|
|
33
33
|
let at = 0,
|
|
34
34
|
output = "";
|
|
35
35
|
while (at < row.from.length) {
|
|
@@ -271,11 +271,25 @@ export function templateProgram(data, options, fault) {
|
|
|
271
271
|
const seen = new Set();
|
|
272
272
|
for (const [i, row] of rules["contextual-mappings"].entries()) {
|
|
273
273
|
const key = `rules.contextual-mappings.${i}`;
|
|
274
|
-
closed(row, ["match", "before", "at", "emit"], key);
|
|
274
|
+
closed(row, ["match", "after", "before", "at", "emit"], key);
|
|
275
275
|
if (!scalar(row.match) || !row.match || row.match !== row.match.normalize("NFC"))
|
|
276
276
|
fault(`${key}.match`, "A contextual match must be nonempty NFC text");
|
|
277
|
-
if (row.before === undefined && row.at === undefined)
|
|
278
|
-
fault(key, "A contextual mapping requires before or at");
|
|
277
|
+
if (row.after === undefined && row.before === undefined && row.at === undefined)
|
|
278
|
+
fault(key, "A contextual mapping requires after, before or at");
|
|
279
|
+
if (row.after !== undefined) {
|
|
280
|
+
if (
|
|
281
|
+
!["vowel", "consonant"].includes(row.after) &&
|
|
282
|
+
(typeof row.after !== "string" ||
|
|
283
|
+
!/^\p{L}$/u.test(row.after) ||
|
|
284
|
+
row.after !== row.after.normalize("NFC"))
|
|
285
|
+
)
|
|
286
|
+
fault(`${key}.after`, "after is one NFC Unicode letter, vowel or consonant");
|
|
287
|
+
if (["vowel", "consonant"].includes(row.after) && !vowels)
|
|
288
|
+
fault(
|
|
289
|
+
`${key}.after`,
|
|
290
|
+
"After-vowel or after-consonant mappings require declared vowels",
|
|
291
|
+
);
|
|
292
|
+
}
|
|
279
293
|
if (row.before !== undefined && !["vowel", "consonant"].includes(row.before))
|
|
280
294
|
fault(`${key}.before`, "before is vowel or consonant");
|
|
281
295
|
if (row.at !== undefined && !["start", "end", "line-start"].includes(row.at))
|
|
@@ -285,7 +299,12 @@ export function templateProgram(data, options, fault) {
|
|
|
285
299
|
`${key}.before`,
|
|
286
300
|
"Before-vowel or before-consonant mappings require declared vowels",
|
|
287
301
|
);
|
|
288
|
-
const condition = JSON.stringify([
|
|
302
|
+
const condition = JSON.stringify([
|
|
303
|
+
row.match,
|
|
304
|
+
row.after ?? null,
|
|
305
|
+
row.before ?? null,
|
|
306
|
+
row.at ?? null,
|
|
307
|
+
]);
|
|
289
308
|
if (seen.has(condition))
|
|
290
309
|
fault(
|
|
291
310
|
`${key}.match`,
|
|
@@ -298,6 +317,7 @@ export function templateProgram(data, options, fault) {
|
|
|
298
317
|
contextual.push({
|
|
299
318
|
from: row.match,
|
|
300
319
|
to,
|
|
320
|
+
...(row.after !== undefined ? { after: row.after } : {}),
|
|
301
321
|
...(row.before !== undefined ? { before: row.before } : {}),
|
|
302
322
|
...(row.at !== undefined ? { at: row.at } : {}),
|
|
303
323
|
});
|
|
@@ -114,12 +114,19 @@ export const ENCODING_RULE_FIELDS = Object.freeze([
|
|
|
114
114
|
kind: "list",
|
|
115
115
|
shape: "a list of conditional mappings",
|
|
116
116
|
describe:
|
|
117
|
-
"Alternatives conditioned on the following letter or source position, applied before unconditional mappings.",
|
|
117
|
+
"Alternatives conditioned on the preceding letter, following letter or source position, applied before unconditional mappings.",
|
|
118
118
|
entries: {
|
|
119
119
|
kind: "map",
|
|
120
120
|
shape: "a contextual mapping",
|
|
121
121
|
fields: [
|
|
122
122
|
text("match", "The source sequence.", true),
|
|
123
|
+
{
|
|
124
|
+
...text(
|
|
125
|
+
"after",
|
|
126
|
+
"Match after one exact NFC Unicode letter, a declared vowel or another letter.",
|
|
127
|
+
),
|
|
128
|
+
shape: "one NFC Unicode letter, vowel or consonant",
|
|
129
|
+
},
|
|
123
130
|
{
|
|
124
131
|
...text("before", "Match before a declared vowel or another letter."),
|
|
125
132
|
allowed: ["vowel", "consonant"],
|
package/engine/encoding.mjs
CHANGED
|
@@ -114,14 +114,14 @@ function rows(value, key, vowels, allowEmpty) {
|
|
|
114
114
|
if (!Array.isArray(value)) fault(key, `\`${key}\` must be a list of rows`);
|
|
115
115
|
return value.map((value, order) => {
|
|
116
116
|
const at = `${key}.${order}`;
|
|
117
|
-
let from, to, before, position;
|
|
117
|
+
let from, to, after, before, position;
|
|
118
118
|
if (Array.isArray(value)) {
|
|
119
119
|
if (value.length !== 2) fault(at, "A row is two strings, [from, to]");
|
|
120
120
|
[from, to] = value;
|
|
121
121
|
} else if (object(value)) {
|
|
122
|
-
keys(value, new Set(["from", "to", "before", "at"]), at);
|
|
123
|
-
({ from, to, before, at: position } = value);
|
|
124
|
-
} else fault(at, "A row is [from, to] or {from, to, before?, at?}");
|
|
122
|
+
keys(value, new Set(["from", "to", "after", "before", "at"]), at);
|
|
123
|
+
({ from, to, after, before, at: position } = value);
|
|
124
|
+
} else fault(at, "A row is [from, to] or {from, to, after?, before?, at?}");
|
|
125
125
|
string(from, `${at}.from`, true);
|
|
126
126
|
string(to, `${at}.to`);
|
|
127
127
|
if (from.includes(OPEN) || from.includes(CLOSE))
|
|
@@ -130,12 +130,23 @@ function rows(value, key, vowels, allowEmpty) {
|
|
|
130
130
|
fault(`${at}.from`, "Whitespace is stated by separator");
|
|
131
131
|
if (!to && !allowEmpty && from !== " ")
|
|
132
132
|
fault(`${at}.to`, "An exact row must write a nonempty output");
|
|
133
|
+
if (after !== undefined) {
|
|
134
|
+
if (
|
|
135
|
+
!["vowel", "consonant"].includes(after) &&
|
|
136
|
+
(typeof after !== "string" ||
|
|
137
|
+
!LETTER.test(after) ||
|
|
138
|
+
after !== after.normalize("NFC"))
|
|
139
|
+
)
|
|
140
|
+
fault(`${at}.after`, "after is one NFC Unicode letter, vowel or consonant");
|
|
141
|
+
if (["vowel", "consonant"].includes(after) && !vowels)
|
|
142
|
+
fault(`${at}.after`, "after categories need vowels");
|
|
143
|
+
}
|
|
133
144
|
if (before !== undefined && !["vowel", "consonant"].includes(before))
|
|
134
145
|
fault(`${at}.before`, "before is vowel or consonant");
|
|
135
146
|
if (before && !vowels) fault(`${at}.before`, "before needs vowels");
|
|
136
147
|
if (position !== undefined && !["start", "end", "line-start"].includes(position))
|
|
137
148
|
fault(`${at}.at`, "at is start, end or line-start");
|
|
138
|
-
return Object.freeze({ from, to, before, at: position, order, key: at });
|
|
149
|
+
return Object.freeze({ from, to, after, before, at: position, order, key: at });
|
|
139
150
|
});
|
|
140
151
|
}
|
|
141
152
|
function indexRows(rows) {
|
|
@@ -149,7 +160,8 @@ function indexRows(rows) {
|
|
|
149
160
|
group.sort(
|
|
150
161
|
(a, b) =>
|
|
151
162
|
length(b.from) - length(a.from) ||
|
|
152
|
-
Number(Boolean(b.
|
|
163
|
+
Number(Boolean(b.after || b.before || b.at)) -
|
|
164
|
+
Number(Boolean(a.after || a.before || a.at)) ||
|
|
153
165
|
a.order - b.order,
|
|
154
166
|
);
|
|
155
167
|
return index;
|
|
@@ -328,6 +340,15 @@ function beforeHolds(condition, char, vowels) {
|
|
|
328
340
|
);
|
|
329
341
|
}
|
|
330
342
|
function holds(row, text, start, end, vowels) {
|
|
343
|
+
if (row.after) {
|
|
344
|
+
const preceding = start > 0 ? previousAt(text, start) : undefined;
|
|
345
|
+
if (
|
|
346
|
+
["vowel", "consonant"].includes(row.after) ?
|
|
347
|
+
!beforeHolds(row.after, preceding, vowels)
|
|
348
|
+
: preceding !== row.after
|
|
349
|
+
)
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
331
352
|
if (row.before && !beforeHolds(row.before, firstAt(text, end), vowels)) return false;
|
|
332
353
|
if (row.at === "line-start") {
|
|
333
354
|
let slash = text.lastIndexOf("/", start - 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "25.6.
|
|
3
|
+
"version": "25.6.3",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package \u2014 content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -25,6 +25,20 @@ export function siteBase(config: {
|
|
|
25
25
|
base: string;
|
|
26
26
|
};
|
|
27
27
|
}): string;
|
|
28
|
+
/**
|
|
29
|
+
* Apply validated runtime site URLs to a resolved configuration, without changing it.
|
|
30
|
+
* The caller checks the authored homepage before applying these build options.
|
|
31
|
+
*
|
|
32
|
+
* @param {ContentBuildConfig} config - The resolved authored configuration.
|
|
33
|
+
* @param {object} options - Runtime options.
|
|
34
|
+
* @param {string} [options.baseUrl] - Absolute HTTP(S) site URL ending in `/`.
|
|
35
|
+
* @param {string} [options.assets] - Absolute HTTP(S) asset root.
|
|
36
|
+
* @returns {ContentBuildConfig} The effective configuration, frozen.
|
|
37
|
+
*/
|
|
38
|
+
export function siteBuildConfig(config: ContentBuildConfig, { baseUrl, assets }: {
|
|
39
|
+
baseUrl?: string | undefined;
|
|
40
|
+
assets?: string | undefined;
|
|
41
|
+
}): ContentBuildConfig;
|
|
28
42
|
/**
|
|
29
43
|
* Whether this package publishes the pages its content tree compiles to.
|
|
30
44
|
*
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Check codeword integrity with a trie and finite Sardinas–Patterson residuals.
|
|
4
4
|
* Source-equivalent compositions are redundant rules, not distinct spellings.
|
|
5
|
-
* @param {Array<{from:string,to:string,key:string,before?:string,at?:string}>} rows - Validated ordinary rows.
|
|
5
|
+
* @param {Array<{from:string,to:string,key:string,after?:string,before?:string,at?:string}>} rows - Validated ordinary rows.
|
|
6
6
|
* @param {(key:string,message:string)=>never} fault - Positioned schema-error constructor.
|
|
7
7
|
*/
|
|
8
8
|
export function assertUniqueOutputs(rows: Array<{
|
|
9
9
|
from: string;
|
|
10
10
|
to: string;
|
|
11
11
|
key: string;
|
|
12
|
+
after?: string;
|
|
12
13
|
before?: string;
|
|
13
14
|
at?: string;
|
|
14
15
|
}>, fault: (key: string, message: string) => never): void;
|