@apleasantview/eleventy-plugin-baseline 0.1.0-next.40 → 0.1.0-next.42
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/README.md +30 -32
- package/core/back-compat/options.js +69 -0
- package/core/content-graph/backlinks.js +65 -0
- package/core/content-graph/extractors.js +185 -0
- package/core/content-graph/graph.js +121 -0
- package/core/content-graph/index.js +2 -0
- package/core/content-graph/prepass.js +121 -0
- package/core/dates/git-date.js +71 -0
- package/core/dates/index.js +55 -0
- package/core/locale/derive-lang.js +19 -0
- package/core/locale/index.js +6 -0
- package/core/locale/normalize-lang.js +13 -0
- package/core/locale/normalize-locale.js +20 -0
- package/core/locale/open-graph-locale.js +14 -0
- package/core/locale/resolve-default.js +27 -0
- package/core/locale/resolve-locale.js +16 -0
- package/core/logging/banner.js +49 -0
- package/core/{logging.js → logging/index.js} +19 -2
- package/core/logging/quips.js +30 -0
- package/core/markdown/auto-heading-ids.js +86 -0
- package/core/markdown/index.js +5 -0
- package/core/markdown/safe-use.js +42 -0
- package/core/{wikilinks.js → markdown/wikilinks.js} +4 -4
- package/core/page-context/build.js +336 -0
- package/core/page-context/index.js +1 -0
- package/core/page-context/register.js +73 -0
- package/core/page-context/seo-helpers.js +56 -0
- package/core/schema.js +22 -2
- package/core/seo-graph/adapter.js +246 -0
- package/core/seo-graph/build.js +87 -0
- package/core/seo-graph/index.js +1 -0
- package/core/seo-graph/open-graph.js +130 -0
- package/core/seo-graph/register.js +42 -0
- package/core/seo-graph/schema.js +18 -0
- package/core/slug-index.js +2 -2
- package/core/state.js +75 -0
- package/core/{shortcodes/image.js → surface/image-shortcode.js} +4 -4
- package/core/surface/index.js +22 -0
- package/core/types.js +3 -0
- package/core/utils/add-trailing-slash.js +11 -0
- package/core/utils/ensure-dot-slash-dir.js +13 -0
- package/core/utils/normalize-language-map.js +37 -0
- package/core/utils/resolve-field.js +9 -0
- package/core/utils/resolve-subdir.js +20 -0
- package/core/utils/slugify.js +15 -0
- package/core/utils/title-case-slug.js +15 -0
- package/core/utils/unique-by.js +25 -0
- package/core/virtual-dir.js +11 -10
- package/index.js +161 -118
- package/modules/assets/index.js +4 -2
- package/modules/assets/processors/esbuild-process.js +2 -2
- package/modules/assets/processors/postcss-process.js +2 -2
- package/modules/head/drivers/posthtml-head-elements.js +92 -12
- package/modules/head/index.js +23 -19
- package/modules/head/schema.js +7 -3
- package/modules/multilang/filters/i18n-default-translation.js +2 -4
- package/modules/multilang/filters/i18n-translation-in.js +2 -2
- package/modules/multilang/filters/i18n-translations-for.js +2 -2
- package/modules/multilang/index.js +80 -39
- package/modules/navigator/index.js +39 -25
- package/modules/navigator/templates/navigator-core.html +1 -1
- package/modules/sitemap/index.js +8 -4
- package/modules/sitemap/templates/sitemap-core.html +1 -1
- package/package.json +5 -2
- package/core/filters/index.js +0 -4
- package/core/global-functions/index.js +0 -6
- package/core/page-context.js +0 -310
- package/core/shortcodes/index.js +0 -2
- package/core/utils/helpers.js +0 -75
- /package/core/{global-functions/date.js → dates/date-global.js} +0 -0
- /package/core/{filters/markdown.js → markdown/markdownify.js} +0 -0
- /package/core/{filters → surface/filters}/isString.js +0 -0
- /package/core/{filters → surface/filters}/related-posts.js +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
let cache = null;
|
|
5
|
+
|
|
6
|
+
// Build a { repoRelativePath: ISO date } map from one `git log` walk.
|
|
7
|
+
// Each commit emits its date, then the files it touched; first date wins
|
|
8
|
+
// because git log is newest-first.
|
|
9
|
+
function buildCache() {
|
|
10
|
+
const map = new Map();
|
|
11
|
+
const marker = '__BASELINE_COMMIT__';
|
|
12
|
+
let raw;
|
|
13
|
+
try {
|
|
14
|
+
raw = execFileSync(
|
|
15
|
+
'git',
|
|
16
|
+
['log', '--name-only', '--no-renames', `--pretty=format:${marker}%cI`],
|
|
17
|
+
{ encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }
|
|
18
|
+
);
|
|
19
|
+
} catch {
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let currentDate = null;
|
|
24
|
+
for (const line of raw.split('\n')) {
|
|
25
|
+
if (line.startsWith(marker)) {
|
|
26
|
+
// Normalise to UTC so all outputs (sitemap, JSON-LD, schemamap) match.
|
|
27
|
+
currentDate = new Date(line.slice(marker.length)).toISOString();
|
|
28
|
+
} else if (line && currentDate && !map.has(line)) {
|
|
29
|
+
map.set(line, currentDate);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return map;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalize(inputPath) {
|
|
36
|
+
const abs = path.resolve(inputPath);
|
|
37
|
+
const rel = path.relative(process.cwd(), abs);
|
|
38
|
+
return rel.split(path.sep).join('/');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Last-commit date (UTC ISO) for a file, or `null` when git has no record of
|
|
43
|
+
* it (untracked, or no git history available — e.g. a shallow CI clone).
|
|
44
|
+
*
|
|
45
|
+
* Unlike the docs-site copy this carries no mtime/now fallback: the date floor
|
|
46
|
+
* is `page.date`, applied by `resolveDates`, so this stays a pure "what does
|
|
47
|
+
* git say, or nothing" lookup.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} inputPath
|
|
50
|
+
* @returns {string | null}
|
|
51
|
+
*/
|
|
52
|
+
export function gitModified(inputPath) {
|
|
53
|
+
if (!cache) cache = buildCache();
|
|
54
|
+
return cache.get(normalize(inputPath)) ?? null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The most recent `gitModified` across several paths, or `null` if none resolve.
|
|
59
|
+
*
|
|
60
|
+
* @param {Array<string | undefined | null>} inputPaths
|
|
61
|
+
* @returns {string | null}
|
|
62
|
+
*/
|
|
63
|
+
export function maxGitModified(inputPaths) {
|
|
64
|
+
let max = null;
|
|
65
|
+
for (const p of inputPaths) {
|
|
66
|
+
if (!p) continue;
|
|
67
|
+
const iso = gitModified(p);
|
|
68
|
+
if (iso && (!max || iso > max)) max = iso;
|
|
69
|
+
}
|
|
70
|
+
return max;
|
|
71
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dates substrate
|
|
3
|
+
*
|
|
4
|
+
* One home for date concerns: the Nunjucks `date` global (formatting), the
|
|
5
|
+
* git-backed last-commit lookup, and `resolveDates` — the single source for a
|
|
6
|
+
* page's publish/modified dates that the seo-graph substrate and any other
|
|
7
|
+
* consumer read from instead of re-deriving the chain.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { gitModified, maxGitModified } from './git-date.js';
|
|
11
|
+
|
|
12
|
+
export { registerDateGlobal } from './date-global.js';
|
|
13
|
+
export { gitModified, maxGitModified };
|
|
14
|
+
|
|
15
|
+
/** Coerce a value to a valid `Date`, or `undefined` if it can't be parsed. */
|
|
16
|
+
function toDate(value) {
|
|
17
|
+
if (!value) return undefined;
|
|
18
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
19
|
+
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a page's publish and modified dates from one place. All three author
|
|
24
|
+
* keys are optional; the chain degrades to `page.date`, which Eleventy always
|
|
25
|
+
* backfills (front matter, else file birthtime), so output is never empty.
|
|
26
|
+
*
|
|
27
|
+
* - `datePublished` → front-matter `datePublished` → `page.date` (the floor).
|
|
28
|
+
* - `dateModified` → front-matter `dateModified` → git last-commit → resolved
|
|
29
|
+
* `datePublished`.
|
|
30
|
+
*
|
|
31
|
+
* `git-date` is the middle rung of the modified chain and is allowed to yield
|
|
32
|
+
* nothing; flooring `dateModified` to the *resolved* `datePublished` (not raw
|
|
33
|
+
* `page.date`) keeps the pair coherent when an author overrides `datePublished`.
|
|
34
|
+
* No clamp to `modified >= published`: a scheduled post can legitimately be
|
|
35
|
+
* modified before its publish date.
|
|
36
|
+
*
|
|
37
|
+
* Takes the full cascade `data` bag (matches the substrate convention) and
|
|
38
|
+
* returns normalised `Date` objects, ready for the seo-graph piece builders.
|
|
39
|
+
*
|
|
40
|
+
* `gitLookup` is an injection seam for tests; production calls pass `data` only
|
|
41
|
+
* and get the real git-backed lookup.
|
|
42
|
+
*
|
|
43
|
+
* @param {{ page?: any, datePublished?: unknown, dateModified?: unknown }} data
|
|
44
|
+
* @param {(inputPath: string) => string | null} [gitLookup]
|
|
45
|
+
* @returns {{ datePublished: Date | undefined, dateModified: Date | undefined }}
|
|
46
|
+
*/
|
|
47
|
+
export function resolveDates(data, gitLookup = gitModified) {
|
|
48
|
+
const page = data?.page ?? {};
|
|
49
|
+
const datePublished = toDate(data?.datePublished) ?? toDate(page.date);
|
|
50
|
+
const dateModified =
|
|
51
|
+
toDate(data?.dateModified) ??
|
|
52
|
+
toDate(page.inputPath ? gitLookup(page.inputPath) : null) ??
|
|
53
|
+
datePublished;
|
|
54
|
+
return { datePublished, dateModified };
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract the short language subtag from a BCP 47 tag.
|
|
3
|
+
*
|
|
4
|
+
* `'en-US'` → `'en'`, `'zh-Hant-HK'` → `'zh'`. Normalises via `Intl.Locale`;
|
|
5
|
+
* null for empty input or tags it rejects.
|
|
6
|
+
*
|
|
7
|
+
* @param {unknown} locale
|
|
8
|
+
* @returns {string | null}
|
|
9
|
+
*/
|
|
10
|
+
export function deriveLang(locale) {
|
|
11
|
+
if (locale == null) return null;
|
|
12
|
+
const str = String(locale).trim();
|
|
13
|
+
if (str === '') return null;
|
|
14
|
+
try {
|
|
15
|
+
return new Intl.Locale(str).language;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { normalizeLang } from './normalize-lang.js';
|
|
2
|
+
export { normalizeLocale } from './normalize-locale.js';
|
|
3
|
+
export { deriveLang } from './derive-lang.js';
|
|
4
|
+
export { resolveDefault } from './resolve-default.js';
|
|
5
|
+
export { resolveLocale } from './resolve-locale.js';
|
|
6
|
+
export { toOpenGraphLocale } from './open-graph-locale.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a short language code: lowercase and trim.
|
|
3
|
+
*
|
|
4
|
+
* The substrate's lightest helper. Empty string for null/undefined; coerces
|
|
5
|
+
* non-string input via `String()`.
|
|
6
|
+
*
|
|
7
|
+
* @param {unknown} raw
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeLang(raw) {
|
|
11
|
+
if (raw == null) return '';
|
|
12
|
+
return String(raw).toLowerCase().trim();
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a BCP 47 locale tag to conventional casing using `Intl.Locale`.
|
|
3
|
+
*
|
|
4
|
+
* `Intl.Locale` handles language-script-region casing and subtag rules without
|
|
5
|
+
* reimplementing the spec. Returns null for empty/whitespace input or tags it
|
|
6
|
+
* rejects.
|
|
7
|
+
*
|
|
8
|
+
* @param {unknown} raw
|
|
9
|
+
* @returns {string | null}
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeLocale(raw) {
|
|
12
|
+
if (raw == null) return null;
|
|
13
|
+
const str = String(raw).trim();
|
|
14
|
+
if (str === '') return null;
|
|
15
|
+
try {
|
|
16
|
+
return new Intl.Locale(str).toString();
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { normalizeLocale } from './normalize-locale.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format a BCP 47 tag in Open Graph's `language_TERRITORY` form (`en-US` →
|
|
5
|
+
* `en_US`). Normalises casing first; null for tags `Intl.Locale` rejects.
|
|
6
|
+
* `replaceAll` so script+region tags convert fully (`zh-Hant-HK` → `zh_Hant_HK`).
|
|
7
|
+
*
|
|
8
|
+
* @param {unknown} raw A BCP 47 locale tag.
|
|
9
|
+
* @returns {string | null}
|
|
10
|
+
*/
|
|
11
|
+
export function toOpenGraphLocale(raw) {
|
|
12
|
+
const normalized = normalizeLocale(raw);
|
|
13
|
+
return normalized ? normalized.replaceAll('-', '_') : null;
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { normalizeLang } from './normalize-lang.js';
|
|
2
|
+
import { normalizeLocale } from './normalize-locale.js';
|
|
3
|
+
import { deriveLang } from './derive-lang.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the effective `{ lang, locale }` default from settings.
|
|
7
|
+
*
|
|
8
|
+
* defaultLocale wins; defaultLanguage is a writer-side alias. Given only
|
|
9
|
+
* defaultLanguage, locale is derived via `Intl.Locale`.
|
|
10
|
+
*
|
|
11
|
+
* @param {{ defaultLanguage?: string, defaultLocale?: string }} settings
|
|
12
|
+
* @returns {{ lang: string, locale: string | null }}
|
|
13
|
+
*/
|
|
14
|
+
export function resolveDefault(settings) {
|
|
15
|
+
const explicitLang = normalizeLang(settings?.defaultLanguage);
|
|
16
|
+
const explicitLocale = normalizeLocale(settings?.defaultLocale);
|
|
17
|
+
|
|
18
|
+
if (explicitLocale) {
|
|
19
|
+
return { lang: deriveLang(explicitLocale) ?? explicitLang, locale: explicitLocale };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (explicitLang) {
|
|
23
|
+
return { lang: explicitLang, locale: normalizeLocale(explicitLang) ?? explicitLang };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { lang: '', locale: null };
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick the page's BCP 47 locale out of the cascade.
|
|
3
|
+
*
|
|
4
|
+
* Reads, never normalises — trusts multilang's already-resolved `page.locale`
|
|
5
|
+
* first, then bag-level locale, the language's configured locale, and the bare
|
|
6
|
+
* `lang` tag last.
|
|
7
|
+
*
|
|
8
|
+
* @param {{ locale?: string } | undefined} node The navigator node, if any.
|
|
9
|
+
* @param {Record<string, any>} data The Eleventy cascade data bag.
|
|
10
|
+
* @param {{ languages?: Record<string, { locale?: string }> } | undefined} settings
|
|
11
|
+
* @param {string} lang Resolved language subtag; the final fallback.
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function resolveLocale(node, data, settings, lang) {
|
|
15
|
+
return node?.locale || data?.page?.locale || data?.locale || settings?.languages?.[lang]?.locale || lang;
|
|
16
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import chalk from 'kleur';
|
|
2
|
+
|
|
3
|
+
const BANNER_GLOBAL_KEY = Symbol.for('eleventy:baseline:banner');
|
|
4
|
+
const FALLBACK_NAME = 'Eleventy Baseline';
|
|
5
|
+
const MARGIN = 4;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the label shown inside the banner. Reads `npm_package_name`
|
|
9
|
+
* (set by npm when Baseline runs under `npm run …`) and falls back to
|
|
10
|
+
* the plugin's own name when the env var is absent (raw `npx`, direct
|
|
11
|
+
* node, programmatic use).
|
|
12
|
+
*
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function resolveBannerLabel() {
|
|
16
|
+
return process.env.npm_package_name || FALLBACK_NAME;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render the boxed startup banner string. Pure, label-only.
|
|
21
|
+
* Width scales with the label so any project name fits.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} [label]
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function baselineBanner(label = resolveBannerLabel()) {
|
|
27
|
+
const inner = label.length + MARGIN * 2;
|
|
28
|
+
const top = '╔' + '═'.repeat(inner) + '╗';
|
|
29
|
+
const middle = '║' + ' '.repeat(MARGIN) + chalk.bold().white(label) + ' '.repeat(MARGIN) + '║';
|
|
30
|
+
const bottom = '╚' + '═'.repeat(inner) + '╝';
|
|
31
|
+
|
|
32
|
+
return ['', top, middle, bottom, ''].join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Print the banner and an intro line once per process. Guarded by a global
|
|
37
|
+
* symbol so repeated plugin invocations (inner pre-pass Eleventy,
|
|
38
|
+
* multi-instance setups) don't re-print.
|
|
39
|
+
*
|
|
40
|
+
* @param {import('./index.js').BaselineLogger} log
|
|
41
|
+
* @param {{ version: string, eleventyVersion?: string }} versions
|
|
42
|
+
*/
|
|
43
|
+
export function printBannerOnce(log, { version, eleventyVersion } = {}) {
|
|
44
|
+
if (globalThis[BANNER_GLOBAL_KEY]) return;
|
|
45
|
+
globalThis[BANNER_GLOBAL_KEY] = true;
|
|
46
|
+
log.print(baselineBanner());
|
|
47
|
+
const tail = eleventyVersion ? `, running Eleventy v${eleventyVersion}` : '';
|
|
48
|
+
log.info(`Baseline v${version}${tail}`);
|
|
49
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import chalk from 'kleur';
|
|
2
2
|
|
|
3
|
+
export * from './banner.js';
|
|
4
|
+
export * from './quips.js';
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* Logger (runtime substrate)
|
|
5
8
|
*
|
|
@@ -36,11 +39,12 @@ import chalk from 'kleur';
|
|
|
36
39
|
* @property {(...args: unknown[]) => void} info Verbose-only.
|
|
37
40
|
* @property {(...args: unknown[]) => void} warn Always visible.
|
|
38
41
|
* @property {(...args: unknown[]) => void} error Always visible.
|
|
42
|
+
* @property {(content: string) => void} print Unprefixed pass-through (used by the banner).
|
|
39
43
|
*/
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Create a namespaced logger. Prefix is `[baseline]` at plugin root and
|
|
43
|
-
* `[baseline
|
|
47
|
+
* `[baseline/<namespace>]` inside modules. `info` is gated behind `verbose`;
|
|
44
48
|
* `warn` and `error` always emit.
|
|
45
49
|
*
|
|
46
50
|
* @param {string | null | undefined} namespace
|
|
@@ -49,15 +53,28 @@ import chalk from 'kleur';
|
|
|
49
53
|
*/
|
|
50
54
|
export function createLogger(namespace, { verbose = false } = {}) {
|
|
51
55
|
const label = namespace ? `[baseline/${namespace}]` : '[baseline]';
|
|
56
|
+
|
|
57
|
+
// Pre-pass gate: silence baseline's own info during the inner Eleventy
|
|
58
|
+
// run so modules don't double-log every line they emit again during the
|
|
59
|
+
// real build. Env-var contract scoped to runPrepass's execution (set in
|
|
60
|
+
// try, cleared in finally). Eleventy's own `[11ty]` output is governed
|
|
61
|
+
// by its `quietMode` and stays untouched here.
|
|
62
|
+
const isPrepass = () => process.env.BASELINE_PREPASS_ACTIVE === '1';
|
|
63
|
+
|
|
52
64
|
return {
|
|
53
65
|
info: (...args) => {
|
|
54
|
-
if (verbose)
|
|
66
|
+
if (!verbose) return;
|
|
67
|
+
if (isPrepass()) return;
|
|
68
|
+
console.log(chalk.gray(label), ...args);
|
|
55
69
|
},
|
|
56
70
|
warn: (...args) => {
|
|
57
71
|
console.warn(chalk.yellow().bold(label), ...args);
|
|
58
72
|
},
|
|
59
73
|
error: (...args) => {
|
|
60
74
|
console.error(chalk.red().bold(label), ...args);
|
|
75
|
+
},
|
|
76
|
+
print: (content) => {
|
|
77
|
+
console.log(chalk.gray(content));
|
|
61
78
|
}
|
|
62
79
|
};
|
|
63
80
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging quips (personality)
|
|
3
|
+
*
|
|
4
|
+
* A small set of one-liners used at the pre-pass boundary. The pre-pass
|
|
5
|
+
* runs Eleventy inside Eleventy, so each line is a nod to that idea of
|
|
6
|
+
* repetition or recursion. Picked at random per build.
|
|
7
|
+
*
|
|
8
|
+
* Architecture layer:
|
|
9
|
+
* runtime substrate (logging)
|
|
10
|
+
*
|
|
11
|
+
* Why this exists:
|
|
12
|
+
* Build logs are dry by default. A single playful line at a recurring
|
|
13
|
+
* moment makes the narrative feel like the project, not a config dump.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const REPETITION_QUIPS = [
|
|
17
|
+
'Somewhere, a bowl of petunias is thinking: oh no, not again.',
|
|
18
|
+
'Déjà vu is usually a glitch in the Matrix.',
|
|
19
|
+
'All of this has happened before, and all of it will happen again.',
|
|
20
|
+
'Yo dawg, I heard you like Eleventy, so I put Eleventy in your Eleventy…'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Return a random repetition quip.
|
|
25
|
+
*
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function pickRepetitionQuip() {
|
|
29
|
+
return REPETITION_QUIPS[Math.floor(Math.random() * REPETITION_QUIPS.length)];
|
|
30
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto heading IDs (runtime substrate)
|
|
3
|
+
*
|
|
4
|
+
* Assigns stable id attributes to every heading that doesn't already have
|
|
5
|
+
* one. Manual ids (from markdown-it-attrs or any upstream plugin) win and
|
|
6
|
+
* seed the dedup map, so auto ids skip over names a manual id has claimed.
|
|
7
|
+
* Duplicate slugs get WordPress-style suffixes: foo, foo-2, foo-3, …
|
|
8
|
+
*
|
|
9
|
+
* Architecture layer:
|
|
10
|
+
* runtime substrate
|
|
11
|
+
*
|
|
12
|
+
* System role:
|
|
13
|
+
* Markdown-it plugin registered by the composition root via
|
|
14
|
+
* amendLibrary('md', ...). Mutates heading_open tokens; reads nothing
|
|
15
|
+
* from the runtime.
|
|
16
|
+
*
|
|
17
|
+
* Lifecycle:
|
|
18
|
+
* transform-time → assigns id attrs on heading_open tokens during the
|
|
19
|
+
* core parsing phase
|
|
20
|
+
*
|
|
21
|
+
* Why this exists:
|
|
22
|
+
* The content graph and deep-link consumers all need predictable heading
|
|
23
|
+
* ids. Doing it inside the markdown engine means the rendered HTML and
|
|
24
|
+
* the graph's heading record are guaranteed identical, instead of being
|
|
25
|
+
* "usually equal because the same slugify helper runs on both sides".
|
|
26
|
+
*
|
|
27
|
+
* Scope:
|
|
28
|
+
* Owns id assignment and dedup. Does not render permalink affordances;
|
|
29
|
+
* that's a theme concern.
|
|
30
|
+
*
|
|
31
|
+
* @param {import('markdown-it').default} md
|
|
32
|
+
* @param {Object} deps
|
|
33
|
+
* @param {(text: string) => string | undefined} deps.slugify
|
|
34
|
+
*/
|
|
35
|
+
export function autoHeadingIds(md, { slugify } = {}) {
|
|
36
|
+
if (typeof slugify !== 'function') {
|
|
37
|
+
throw new Error('auto-heading-ids plugin requires { slugify }');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
md.core.ruler.push('baseline_auto_heading_ids', (state) => {
|
|
41
|
+
const tokens = state.tokens;
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const counts = new Map();
|
|
44
|
+
|
|
45
|
+
// Pass 1: seed dedup with manual ids so autos never collide with them.
|
|
46
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
47
|
+
const t = tokens[i];
|
|
48
|
+
if (t.type !== 'heading_open') continue;
|
|
49
|
+
const manual = t.attrGet('id');
|
|
50
|
+
if (manual) seen.add(manual);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Pass 2: assign auto ids to headings that lack one.
|
|
54
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
55
|
+
const t = tokens[i];
|
|
56
|
+
if (t.type !== 'heading_open') continue;
|
|
57
|
+
if (t.attrGet('id')) continue;
|
|
58
|
+
|
|
59
|
+
// Read from children rather than .content: markdown-it-attrs
|
|
60
|
+
// strips the {.class} from children tokens but leaves the parent
|
|
61
|
+
// inline .content untouched, so slugifying .content would fold
|
|
62
|
+
// the attribute text into the id.
|
|
63
|
+
const inline = tokens[i + 1];
|
|
64
|
+
const text = (inline?.children || [])
|
|
65
|
+
.filter((c) => c.type === 'text' || c.type === 'code_inline' || c.type === 'image')
|
|
66
|
+
.map((c) => (c.type === 'image' ? c.attrGet('alt') || c.content || '' : c.content))
|
|
67
|
+
.join('')
|
|
68
|
+
.trim();
|
|
69
|
+
if (!text) continue;
|
|
70
|
+
|
|
71
|
+
const base = slugify(text);
|
|
72
|
+
if (!base) continue;
|
|
73
|
+
|
|
74
|
+
let n = counts.get(base) || 0;
|
|
75
|
+
let id;
|
|
76
|
+
do {
|
|
77
|
+
id = n === 0 ? base : `${base}-${n + 1}`;
|
|
78
|
+
n += 1;
|
|
79
|
+
} while (seen.has(id));
|
|
80
|
+
|
|
81
|
+
counts.set(base, n);
|
|
82
|
+
seen.add(id);
|
|
83
|
+
t.attrSet('id', id);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe markdown-it plugin registration
|
|
3
|
+
*
|
|
4
|
+
* Calls md.use(plugin, options) only when no rule with the given name is
|
|
5
|
+
* already registered on the core, block, or inline ruler. Lets Baseline
|
|
6
|
+
* coexist with user configs that loaded the same plugin themselves.
|
|
7
|
+
*
|
|
8
|
+
* Architecture layer:
|
|
9
|
+
* utility
|
|
10
|
+
*
|
|
11
|
+
* System role:
|
|
12
|
+
* Helper used by the composition root when amending the markdown-it
|
|
13
|
+
* instance.
|
|
14
|
+
*
|
|
15
|
+
* Lifecycle:
|
|
16
|
+
* transform-time (config load)
|
|
17
|
+
*
|
|
18
|
+
* Why this exists:
|
|
19
|
+
* markdown-it has no built-in dedup. Calling .use() twice on a plugin
|
|
20
|
+
* that pushes a parser rule runs it twice per render. Eleventy's
|
|
21
|
+
* amendLibrary callbacks compose with whatever the user already wired,
|
|
22
|
+
* so a guard at the seam keeps both sides intact.
|
|
23
|
+
*
|
|
24
|
+
* @param {import('markdown-it').default} md
|
|
25
|
+
* @param {string} ruleName - canonical rule name the plugin registers
|
|
26
|
+
* @param {(md: any, options?: any) => void} plugin
|
|
27
|
+
* @param {any} [options]
|
|
28
|
+
* @param {{ info?: (msg: string) => void }} [log]
|
|
29
|
+
*/
|
|
30
|
+
export function safeUse(md, ruleName, plugin, options, log) {
|
|
31
|
+
const rulers = [md.core?.ruler, md.block?.ruler, md.inline?.ruler];
|
|
32
|
+
const installed = rulers.some((r) =>
|
|
33
|
+
r?.__rules__?.some((rule) => rule.name === ruleName)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (installed) {
|
|
37
|
+
log?.info?.(`markdown-it rule "${ruleName}" already registered, skipping`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
md.use(plugin, options);
|
|
42
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { slugify } from '
|
|
1
|
+
import { slugify } from '../utils/slugify.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Wikilinks (runtime substrate)
|
|
@@ -38,7 +38,7 @@ import { slugify } from './utils/helpers.js';
|
|
|
38
38
|
*
|
|
39
39
|
* @param {import('markdown-it').default} md
|
|
40
40
|
* @param {Object} deps
|
|
41
|
-
* @param {{getBySlug: (slug: string) => string |
|
|
41
|
+
* @param {{getBySlug: (slug: string) => string | undefined}} deps.slugIndex
|
|
42
42
|
* @param {{getByKey: (url: string) => any}} deps.pageContextRegistry
|
|
43
43
|
* @param {{get: () => Record<string, Record<string, {url: string, title?: string}>> | null}} [deps.translationMapStore]
|
|
44
44
|
*/
|
|
@@ -88,7 +88,7 @@ export function wikilinks(md, { slugIndex, pageContextRegistry, translationMapSt
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const lang = rawLang.toLowerCase();
|
|
91
|
-
const translationKey = ctx?.page?.
|
|
91
|
+
const translationKey = ctx?.page?.translationKey;
|
|
92
92
|
if (!translationKey) return null;
|
|
93
93
|
|
|
94
94
|
const map = translationMapStore?.get?.();
|
|
@@ -148,5 +148,5 @@ export function wikilinks(md, { slugIndex, pageContextRegistry, translationMapSt
|
|
|
148
148
|
return true;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
md.inline.ruler.before('link', '
|
|
151
|
+
md.inline.ruler.before('link', 'baseline_wikilinks', tokenize);
|
|
152
152
|
}
|