@apleasantview/eleventy-plugin-baseline 0.1.0-next.32 → 0.1.0-next.39

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.
Files changed (65) hide show
  1. package/README.md +48 -23
  2. package/core/content-map-store.js +51 -0
  3. package/core/filters/index.js +4 -0
  4. package/core/filters/isString.js +6 -1
  5. package/core/filters/markdown.js +6 -0
  6. package/core/filters/related-posts.js +7 -1
  7. package/core/global-functions/index.js +6 -0
  8. package/core/logging.js +25 -25
  9. package/core/page-context.js +310 -0
  10. package/core/registry.js +110 -0
  11. package/core/schema.js +37 -0
  12. package/core/shortcodes/image.js +167 -144
  13. package/core/shortcodes/index.js +2 -0
  14. package/core/slug-index.js +61 -0
  15. package/core/translation-map-store.js +46 -0
  16. package/core/types.js +73 -0
  17. package/core/utils/helpers.js +75 -0
  18. package/core/utils/pick.js +7 -0
  19. package/core/virtual-dir.js +111 -0
  20. package/core/wikilinks.js +152 -0
  21. package/index.js +364 -0
  22. package/modules/assets/index.js +162 -0
  23. package/modules/assets/processors/esbuild-process.js +35 -0
  24. package/modules/assets/processors/postcss-process.js +52 -0
  25. package/modules/assets/schema.js +14 -0
  26. package/modules/head/drivers/capo-adapter.js +72 -0
  27. package/modules/head/drivers/posthtml-head-elements.js +140 -0
  28. package/modules/head/index.js +106 -0
  29. package/modules/head/schema.js +42 -0
  30. package/modules/head/utils/alternates.js +11 -0
  31. package/modules/head/utils/dedupe.js +47 -0
  32. package/modules/multilang/index.js +149 -0
  33. package/modules/navigator/index.js +140 -0
  34. package/modules/navigator/schema.js +13 -0
  35. package/modules/{navigator-core → navigator}/templates/navigator-core.html +14 -8
  36. package/modules/navigator/utils/debug.js +41 -0
  37. package/modules/sitemap/index.js +121 -0
  38. package/modules/sitemap/templates/sitemap-core.html +34 -0
  39. package/modules/{sitemap-core → sitemap}/templates/sitemap-index.html +2 -2
  40. package/modules.js +6 -0
  41. package/package.json +15 -6
  42. package/core/debug.js +0 -20
  43. package/core/filters.js +0 -9
  44. package/core/globals.js +0 -6
  45. package/core/helpers.js +0 -127
  46. package/core/modules.js +0 -22
  47. package/core/shortcodes.js +0 -3
  48. package/eleventy.config.js +0 -157
  49. package/modules/assets-core/plugins/assets-core.js +0 -84
  50. package/modules/assets-esbuild/filters/inline-esbuild.js +0 -24
  51. package/modules/assets-esbuild/plugins/assets-esbuild.js +0 -71
  52. package/modules/assets-postcss/filters/inline-postcss.js +0 -38
  53. package/modules/assets-postcss/plugins/assets-postcss.js +0 -75
  54. package/modules/head-core/drivers/posthtml-head-elements.js +0 -132
  55. package/modules/head-core/plugins/head-core.js +0 -57
  56. package/modules/head-core/utils/head-utils.js +0 -183
  57. package/modules/multilang-core/plugins/multilang-core.js +0 -101
  58. package/modules/navigator-core/plugins/navigator-core.js +0 -39
  59. package/modules/sitemap-core/plugins/sitemap-core.js +0 -65
  60. package/modules/sitemap-core/templates/sitemap-core.html +0 -25
  61. /package/core/{globals → global-functions}/date.js +0 -0
  62. /package/modules/{assets-postcss/fallback → assets/configs}/postcss.config.js +0 -0
  63. /package/modules/{multilang-core → multilang}/filters/i18n-default-translation.js +0 -0
  64. /package/modules/{multilang-core → multilang}/filters/i18n-translation-in.js +0 -0
  65. /package/modules/{multilang-core → multilang}/filters/i18n-translations-for.js +0 -0
package/core/schema.js ADDED
@@ -0,0 +1,37 @@
1
+ import * as z from 'zod';
2
+
3
+ export const configSchema = z.object({
4
+ dir: z.object({
5
+ input: z.string().min(1),
6
+ output: z.string().min(1),
7
+ data: z.string().min(1),
8
+ includes: z.string().min(1),
9
+ assets: z.string().min(1),
10
+ public: z.string().min(1)
11
+ }),
12
+ htmlTemplateEngine: z.string().min(1),
13
+ markdownTemplateEngine: z.string().min(1),
14
+ templateFormats: z
15
+ .array(z.string().min(1))
16
+ .min(1)
17
+ .refine((templateFormats) => templateFormats.includes('njk'), {
18
+ error: 'Baseline requires njk in templateFormats'
19
+ })
20
+ });
21
+
22
+ export const settingsSchema = z.object({
23
+ title: z.string().optional(),
24
+ tagline: z.string().optional(),
25
+ url: z.string().optional(),
26
+ noindex: z.boolean().optional(),
27
+ defaultLanguage: z.string().optional(),
28
+ languages: z.record(z.string(), z.looseObject({})).optional(),
29
+ head: z
30
+ .object({
31
+ link: z.array(z.looseObject({})).optional(),
32
+ script: z.array(z.looseObject({})).optional(),
33
+ meta: z.array(z.looseObject({})).optional(),
34
+ style: z.array(z.looseObject({})).optional()
35
+ })
36
+ .optional()
37
+ });
@@ -1,144 +1,167 @@
1
- import path from 'node:path';
2
- import Image from '@11ty/eleventy-img';
3
-
4
- const DEFAULT_WIDTHS = [320, 640, 960, 1280, 1920, 'auto'];
5
- const DEFAULT_FORMATS = ['avif', 'webp'];
6
- const DEFAULT_SIZES = '(max-width: 768px) 100vw, 768px';
7
-
8
- function pickRenditions(metadata) {
9
- // Use the first available format; first entry is smallest, last is largest.
10
- const firstFormat = Object.values(metadata)[0];
11
- const lowsrc = firstFormat?.[0];
12
- const highsrc = firstFormat?.[firstFormat.length - 1];
13
- return { lowsrc, highsrc };
14
- }
15
-
16
- /**
17
- * Responsive image shortcode using @11ty/eleventy-img.
18
- *
19
- * @param {Object} options
20
- * @param {string} options.src Required image source (local or remote).
21
- * @param {string} options.alt Required alt text (empty string allowed for decorative).
22
- * @param {string} [options.caption=""] Optional caption; enables figure wrapper when non-empty.
23
- * @param {("lazy"|"eager")} [options.loading="lazy"] Loading behavior.
24
- * @param {string} [options.containerClass=""] Class applied to <picture>.
25
- * @param {string} [options.imageClass=""] Class applied to <img>.
26
- * @param {Array<number|string>} [options.widths=DEFAULT_WIDTHS] Widths passed to eleventy-img.
27
- * @param {string} [options.sizes=DEFAULT_SIZES] Sizes attribute used on sources.
28
- * @param {string[]} [options.formats=DEFAULT_FORMATS] Output formats (order matters).
29
- * @param {string} [options.outputDir] Output directory for generated assets.
30
- * @param {string} [options.urlPath="/media/"] Public URL base for generated assets.
31
- * @param {Object} [options.attrs={}] Extra attributes applied to <img>; `class` merges with imageClass.
32
- * @param {string} [options.style] Inline style applied to <img> (alias for attrs.style).
33
- * @param {boolean} [options.figure=true] Wrap in <figure> when caption is provided.
34
- * @param {boolean} [options.setDimensions=true] When false, omit width/height on <img>.
35
- */
36
- export async function imageShortcode(options = {}) {
37
- const outputBase = this?.eleventy?.directories?.output || 'dist';
38
- const {
39
- src,
40
- alt,
41
- caption = '',
42
- loading = 'lazy',
43
- containerClass = '',
44
- imageClass = '',
45
- style,
46
- widths = DEFAULT_WIDTHS,
47
- sizes = DEFAULT_SIZES,
48
- formats = DEFAULT_FORMATS,
49
- outputDir = path.join('.', outputBase, 'media'),
50
- urlPath = '/media/',
51
- attrs = {},
52
- figure = true,
53
- setDimensions = true
54
- } = options;
55
- const hasImageTransformPlugin = this.ctx._baseline.hasImageTransformPlugin;
56
-
57
- if (!src) throw new Error('imageShortcode: src is required');
58
- if (alt === undefined) {
59
- throw new Error('imageShortcode: alt is required (use empty string for decorative images)');
60
- }
61
-
62
- const normalizedCaption = caption == null ? '' : String(caption);
63
- const normalizedAlt = alt == null ? '' : String(alt);
64
-
65
- const inputDir = this?.eleventy?.directories?.input;
66
- const isRemote = /^https?:\/\//i.test(src);
67
- const resolvedSrc = !isRemote && inputDir ? path.join(inputDir, src.replace(/^\//, '')) : src;
68
-
69
- let metadata;
70
- try {
71
- metadata = await Image(resolvedSrc, {
72
- transformOnRequest: process.env.ELEVENTY_RUN_MODE === 'serve',
73
- widths: [...widths],
74
- formats: [...formats],
75
- outputDir,
76
- urlPath,
77
- filenameFormat(id, srcPath, width, format) {
78
- const extension = path.extname(srcPath);
79
- const name = path.basename(srcPath, extension);
80
- return `${name}-${width}w.${format}`;
81
- }
82
- });
83
- } catch (error) {
84
- console.warn(`imageShortcode: transformOnRequest failed for ${src}.\n > ${error?.message || error}`);
85
- metadata = await Image(resolvedSrc, {
86
- widths: [...widths],
87
- formats: [...formats],
88
- outputDir,
89
- urlPath,
90
- filenameFormat(id, srcPath, width, format) {
91
- const extension = path.extname(srcPath);
92
- const name = path.basename(srcPath, extension);
93
- return `${name}-${width}w.${format}`;
94
- }
95
- });
96
- }
97
-
98
- const { lowsrc, highsrc } = pickRenditions(metadata);
99
- if (!lowsrc || !highsrc) {
100
- throw new Error(`imageShortcode: no renditions produced for ${src}`);
101
- }
102
-
103
- const sourceTags = Object.values(metadata)
104
- .map((formatEntries) => {
105
- const type = formatEntries[0].sourceType;
106
- const srcset = formatEntries.map((entry) => entry.srcset).join(', ');
107
- return `<source type="${type}" srcset="${srcset}" sizes="${sizes}">`;
108
- })
109
- .join('\n');
110
-
111
- const { class: attrClass, ...restAttrs } = attrs;
112
- const combinedClass = [imageClass, attrClass].filter(Boolean).join(' ').trim() || undefined;
113
-
114
- const imageAttributes = {
115
- src: lowsrc.url,
116
- alt: normalizedAlt,
117
- loading,
118
- decoding: loading === 'eager' ? 'sync' : 'async',
119
- class: combinedClass,
120
- style,
121
- ...(setDimensions ? { width: highsrc.width, height: highsrc.height } : {}),
122
- ...restAttrs,
123
- ...(hasImageTransformPlugin ? { 'eleventy:ignore': true } : {})
124
- };
125
-
126
- const imgAttrString = Object.entries(imageAttributes)
127
- .filter(([, value]) => value !== undefined && value !== null && value !== '')
128
- .map(([key, value]) => (value === true ? key : `${key}="${value}"`))
129
- .join(' ');
130
-
131
- const pictureClass = containerClass && containerClass.trim() ? ` class="${containerClass.trim()}"` : '';
132
-
133
- const picture = `<picture${pictureClass}>
134
- ${sourceTags}
135
- <img ${imgAttrString}>
136
- </picture>`;
137
-
138
- if (!figure || !normalizedCaption) return picture;
139
-
140
- return `<figure>
141
- ${picture}
142
- <figcaption>${normalizedCaption}</figcaption>
143
- </figure>`;
144
- }
1
+ import path from 'node:path';
2
+ import Image from '@11ty/eleventy-img';
3
+ import { createLogger } from '../logging.js';
4
+
5
+ // Module-level logger. Image shortcode only uses `.warn`, which emits regardless
6
+ // of verbose, so we don't thread verbose through the shortcode signature.
7
+ const log = createLogger('image');
8
+
9
+ const DEFAULT_WIDTHS = [320, 640, 960, 1280, 1920, 'auto'];
10
+ const DEFAULT_FORMATS = ['avif', 'webp'];
11
+ const DEFAULT_SIZES = '(max-width: 768px) 100vw, 768px';
12
+
13
+ /**
14
+ * Pick the smallest and largest rendition from eleventy-img metadata.
15
+ * Uses the first available format; entries are ordered smallest → largest.
16
+ * @param {Object} metadata - eleventy-img metadata keyed by format.
17
+ * @returns {{lowsrc: Object, highsrc: Object}} Smallest and largest rendition.
18
+ */
19
+ function pickRenditions(metadata) {
20
+ const firstFormat = Object.values(metadata)[0];
21
+ const lowsrc = firstFormat?.[0];
22
+ const highsrc = firstFormat?.[firstFormat.length - 1];
23
+ return { lowsrc, highsrc };
24
+ }
25
+
26
+ /**
27
+ * Responsive image shortcode using @11ty/eleventy-img.
28
+ *
29
+ * @param {Object} options
30
+ * @param {string} options.src Required image source (local or remote).
31
+ * @param {string} options.alt Required alt text (empty string allowed for decorative).
32
+ * @param {string} [options.caption=""] Optional caption; enables figure wrapper when non-empty.
33
+ * @param {("lazy"|"eager")} [options.loading="lazy"] Loading behavior.
34
+ * @param {string} [options.containerClass=""] Class applied to <picture>.
35
+ * @param {string} [options.imageClass=""] Class applied to <img>.
36
+ * @param {Array<number|string>} [options.widths=DEFAULT_WIDTHS] Widths passed to eleventy-img.
37
+ * @param {string} [options.sizes=DEFAULT_SIZES] Sizes attribute used on sources.
38
+ * @param {string[]} [options.formats=DEFAULT_FORMATS] Output formats (order matters).
39
+ * @param {string} [options.outputDir] Output directory for generated assets.
40
+ * @param {string} [options.urlPath="/media/"] Public URL base for generated assets.
41
+ * @param {Object} [options.attrs={}] Extra attributes applied to <img>; `class` merges with imageClass.
42
+ * @param {string} [options.style] Inline style applied to <img>. Separate from attrs.style — if both are passed, attrs.style takes precedence via restAttrs spread.
43
+ * @param {boolean} [options.figure=true] Wrap in <figure> when caption is provided.
44
+ * @param {boolean} [options.setDimensions=true] When false, omit width/height on <img>.
45
+ */
46
+ export async function imageShortcode(options = {}) {
47
+ const outputBase = this?.eleventy?.directories?.output || 'dist';
48
+ const {
49
+ src,
50
+ alt,
51
+ caption = '',
52
+ loading = 'lazy',
53
+ containerClass = '',
54
+ imageClass = '',
55
+ style,
56
+ widths = DEFAULT_WIDTHS,
57
+ sizes = DEFAULT_SIZES,
58
+ formats = DEFAULT_FORMATS,
59
+ outputDir = path.join('.', outputBase, 'media'),
60
+ urlPath = '/media/',
61
+ attrs = {},
62
+ figure = true,
63
+ setDimensions = true
64
+ } = options;
65
+ // Read from global data set during plugin init. When true, `eleventy:ignore`
66
+ // is added to the <img> (line 140) to prevent double-processing.
67
+ const hasImageTransformPlugin = this.ctx._baseline.features.hasImageTransformPlugin;
68
+
69
+ // --- Validation and normalization ---
70
+
71
+ if (!src) throw new Error(`imageShortcode: src is required (received ${JSON.stringify(src)})`);
72
+ if (alt == null) {
73
+ log.warn('alt is required (use empty string for decorative images)');
74
+ }
75
+
76
+ const normalizedCaption = String(caption);
77
+ const normalizedAlt = alt == null ? '' : String(alt);
78
+
79
+ const inputDir = this?.eleventy?.directories?.input;
80
+ const isRemote = /^https?:\/\//i.test(src);
81
+ // Note: remote URLs rely on eleventy-img's built-in fetch — no timeout/retry control at shortcode level.
82
+ const resolvedSrc = !isRemote && inputDir ? path.join(inputDir, src.replace(/^\//, '')) : src;
83
+
84
+ const imageOptions = {
85
+ widths: [...widths],
86
+ formats: [...formats],
87
+ outputDir,
88
+ urlPath,
89
+ filenameFormat(id, srcPath, width, format) {
90
+ const extension = path.extname(srcPath);
91
+ const name = path.basename(srcPath, extension);
92
+ return `${name}-${id.slice(0, 6)}-${width}w.${format}`;
93
+ }
94
+ };
95
+
96
+ // --- Image processing ---
97
+ // In serve mode, `transformOnRequest` defers processing to first browser request
98
+ // for faster dev startup. If it fails, retry without it — this is an edge case
99
+ // but one that has bitten in practice. In build mode, errors surface immediately.
100
+
101
+ let metadata;
102
+ try {
103
+ metadata = await Image(resolvedSrc, {
104
+ transformOnRequest: process.env.ELEVENTY_RUN_MODE === 'serve',
105
+ ...imageOptions
106
+ });
107
+ } catch (error) {
108
+ if (process.env.ELEVENTY_RUN_MODE === 'serve') {
109
+ log.warn(`transformOnRequest failed for ${src}, retrying.\n > ${error?.message || error}`);
110
+ metadata = await Image(resolvedSrc, imageOptions);
111
+ } else {
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ const { lowsrc, highsrc } = pickRenditions(metadata);
117
+ if (!lowsrc || !highsrc) {
118
+ throw new Error(`imageShortcode: no renditions produced for ${src}`);
119
+ }
120
+
121
+ // --- HTML assembly ---
122
+ // One <source> per format, each carrying the full srcset for that format.
123
+ const sourceTags = Object.values(metadata)
124
+ .map((formatEntries) => {
125
+ const type = formatEntries[0].sourceType;
126
+ const srcset = formatEntries.map((entry) => entry.srcset).join(', ');
127
+ return `<source type="${type}" srcset="${srcset}" sizes="${sizes}">`;
128
+ })
129
+ .join('\n');
130
+
131
+ // Pull `class` out of attrs so it can merge with imageClass. Remaining attrs
132
+ // and `eleventy:ignore` (if needed) are spread onto imageAttributes below.
133
+ const { class: attrClass, ...restAttrs } = attrs;
134
+ const combinedClass = [imageClass, attrClass].filter(Boolean).join(' ').trim() || undefined;
135
+
136
+ const imageAttributes = {
137
+ src: lowsrc.url,
138
+ alt: normalizedAlt,
139
+ loading,
140
+ decoding: loading === 'eager' ? 'sync' : 'async',
141
+ class: combinedClass,
142
+ style,
143
+ ...(setDimensions ? { width: highsrc.width, height: highsrc.height } : {}),
144
+ ...restAttrs,
145
+ ...(hasImageTransformPlugin ? { 'eleventy:ignore': true } : {})
146
+ };
147
+
148
+ // Build the attribute string, dropping any empty/null values to keep output clean.
149
+ const imgAttrString = Object.entries(imageAttributes)
150
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
151
+ .map(([key, value]) => (value === true ? key : `${key}="${value}"`))
152
+ .join(' ');
153
+
154
+ const pictureClass = containerClass && containerClass.trim() ? ` class="${containerClass.trim()}"` : '';
155
+
156
+ const picture = `<picture${pictureClass}>
157
+ ${sourceTags}
158
+ <img ${imgAttrString}>
159
+ </picture>`;
160
+
161
+ if (!figure || !normalizedCaption) return picture;
162
+
163
+ return `<figure>
164
+ ${picture}
165
+ <figcaption>${normalizedCaption}</figcaption>
166
+ </figure>`;
167
+ }
@@ -0,0 +1,2 @@
1
+ // Shortcodes barrel.
2
+ export { imageShortcode } from './image.js';
@@ -0,0 +1,61 @@
1
+ import { getScope, setEntry, getEntry } from './registry.js';
2
+
3
+ const SCOPE_NAME = 'core:slug-index';
4
+
5
+ /**
6
+ * Slug index (runtime substrate)
7
+ *
8
+ * Maps wikilink-friendly slugs to canonical page URLs. Populated by the
9
+ * page-context builder as each page resolves; read by the wikilinks
10
+ * markdown-it plugin during body render.
11
+ *
12
+ * Architecture layer:
13
+ * runtime substrate
14
+ *
15
+ * System role:
16
+ * Forward-link index for the wikilinks plugin. In multilingual sites
17
+ * only defaultLanguage pages register; the wikilinks plugin uses the
18
+ * translation map to hop to other languages.
19
+ *
20
+ * Lifecycle:
21
+ * cascade-time → page-context registers slugs as templates compute
22
+ * transform-time → wikilinks plugin resolves slugs during body render
23
+ *
24
+ * Why this exists:
25
+ * Eleventy's data cascade resolves all eleventyComputed values before any
26
+ * body renders, so the index is complete by the time wikilinks need it.
27
+ * A dedicated scope keeps slug→url out of the page-context values map
28
+ * (which holds url→pageContext).
29
+ *
30
+ * Scope:
31
+ * Owns slug registration with collision detection and slug-keyed lookup.
32
+ * Does not own slug derivation (page-context) or link rendering (wikilinks).
33
+ *
34
+ * Data flow:
35
+ * page-context.buildPageContext → set() → registry scope → wikilinks getBySlug()
36
+ *
37
+ * @param {import('@11ty/eleventy').UserConfig} eleventyConfig
38
+ * @returns {{set: (slug: string, url: string, inputPath?: string) => void, getBySlug: (slug: string) => string | null, snapshot: () => Record<string, {url: string, inputPath?: string}>}}
39
+ */
40
+ export function createSlugIndex(eleventyConfig) {
41
+ const scope = getScope(eleventyConfig, SCOPE_NAME);
42
+
43
+ return {
44
+ set(slug, url, inputPath) {
45
+ if (!slug || !url) return;
46
+ const existing = getEntry(scope, slug);
47
+ if (existing && existing.url !== url) {
48
+ throw new Error(
49
+ `Wikilink slug collision: "${slug}" used by both ${existing.inputPath ?? existing.url} and ${inputPath ?? url}`
50
+ );
51
+ }
52
+ setEntry(scope, slug, { url, inputPath });
53
+ },
54
+ getBySlug(slug) {
55
+ return getEntry(scope, slug)?.url ?? null;
56
+ },
57
+ snapshot() {
58
+ return Object.fromEntries(scope.values);
59
+ }
60
+ };
61
+ }
@@ -0,0 +1,46 @@
1
+ import { getScope, setEntry, getEntry } from './registry.js';
2
+
3
+ const SCOPE_NAME = 'core:translation-map-store';
4
+ const KEY = 'translationMap';
5
+
6
+ /**
7
+ * Translation map store (runtime substrate)
8
+ *
9
+ * Hand-off point for the translations map: written by multilang at
10
+ * cascade-time, read by head at transform-time.
11
+ *
12
+ * Architecture layer:
13
+ * runtime substrate
14
+ *
15
+ * System role:
16
+ * Bridge between the multilang module (writer) and the head module (reader).
17
+ * The set/get pair lives in a registry scope rather than the data cascade
18
+ * because head's PostHTML plugin runs outside the cascade.
19
+ *
20
+ * Lifecycle:
21
+ * cascade-time → multilang's translationsMap collection writes via set()
22
+ * transform-time → head reads via get() to build hreflang alternates
23
+ *
24
+ * Why this exists:
25
+ * The translations map is built inside an Eleventy collection, but head
26
+ * needs it inside an htmlTransformer plugin where collections aren't
27
+ * available.
28
+ *
29
+ * Scope:
30
+ * Owns set/get on a per-config scope.
31
+ * Does not own the map's shape, how it's built, or how head uses it.
32
+ *
33
+ * Data flow:
34
+ * multilang translationsMap collection → set() → registry scope → head get()
35
+ *
36
+ * @param {import('@11ty/eleventy').UserConfig} eleventyConfig
37
+ * @returns {{set: (map: object) => void, get: () => object | null}}
38
+ */
39
+ export function createTranslationMapStore(eleventyConfig) {
40
+ const scope = getScope(eleventyConfig, SCOPE_NAME);
41
+
42
+ return {
43
+ set: (map) => setEntry(scope, KEY, map),
44
+ get: () => getEntry(scope, KEY) ?? null
45
+ };
46
+ }
package/core/types.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Baseline shared typedefs.
3
+ *
4
+ * Extracted from the composition root so file-level headers stay scannable.
5
+ * No runtime exports; this file exists for IDE and JSDoc tooling only.
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} BaselineSettings
10
+ * Site identity and SEO configuration.
11
+ *
12
+ * @property {string} [title]
13
+ * @property {string} [tagline]
14
+ * @property {string} [url]
15
+ * @property {boolean} [noindex]
16
+ * @property {string} [defaultLanguage]
17
+ * @property {Record<string, unknown>} [languages]
18
+ * @property {Object} [head]
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} BaselineOptions
23
+ * Runtime feature flags and behaviour configuration.
24
+ *
25
+ * User-facing input. Each module reads its own slice from `state.options.<module>`.
26
+ *
27
+ * @property {boolean} [verbose]
28
+ * Enables structured debug logging across modules.
29
+ *
30
+ * @property {boolean} [multilingual]
31
+ * Enables multilingual mode. Requires settings.defaultLanguage and
32
+ * settings.languages; the multilang module bails with a log otherwise.
33
+ *
34
+ * @property {boolean} [sitemap]
35
+ * Enables sitemap generation module (default: true).
36
+ *
37
+ * @property {boolean | { template?: boolean, inspectorDepth?: number }} [navigator]
38
+ * Controls navigator tooling. Boolean shorthand activates the module and
39
+ * the virtual debug page. Object form lets the page render flag and the
40
+ * inspector depth be tuned independently. Defaults to true in dev mode.
41
+ *
42
+ * @property {{ titleSeparator?: string, showGenerator?: boolean }} [head]
43
+ * Head module options.
44
+ *
45
+ * @property {{ esbuild?: { minify?: boolean, target?: string } }} [assets]
46
+ * Assets module options. The esbuild slice is permissive — any esbuild
47
+ * option is accepted; only `minify` and `target` are typed.
48
+ */
49
+
50
+ /**
51
+ * @typedef {Object} BaselineState
52
+ * Fully resolved internal plugin state.
53
+ *
54
+ * @property {Object} settings
55
+ * @property {Object} options
56
+ */
57
+
58
+ /**
59
+ * @typedef {Object} BaselineContext
60
+ * Shared module boundary contract.
61
+ *
62
+ * This context is the only supported interface between:
63
+ * - Eleventy configuration runtime
64
+ * - baseline core
65
+ * - feature modules
66
+ *
67
+ * @property {BaselineState} state
68
+ * @property {Object} runtime
69
+ * @property {Object} runtime.contentMap
70
+ * @property {Object} runtime.site
71
+ */
72
+
73
+ export {};
@@ -0,0 +1,75 @@
1
+ import { TemplatePath } from '@11ty/eleventy-utils';
2
+ import slugifyLib from 'slugify';
3
+
4
+ /**
5
+ * Helper function to add trailing slash to a path
6
+ * @param {string} path
7
+ * @returns {string}
8
+ */
9
+ export function addTrailingSlash(path) {
10
+ if (path.slice(-1) === '/') {
11
+ return path;
12
+ }
13
+ return path + '/';
14
+ }
15
+
16
+ /**
17
+ * Resolve a subdirectory under input and output.
18
+ * Joins inputDir/outputDir with rawDir, normalises, and adds trailing slashes.
19
+ * @param {string} inputDir - The input directory (e.g., "./src/").
20
+ * @param {string} outputDir - The output directory (e.g., "./dist/").
21
+ * @param {string} rawDir - Raw subdirectory value (e.g., "assets", "static").
22
+ * @returns {{input: string, output: string}}
23
+ */
24
+ export function resolveSubdir(inputDir, outputDir, rawDir) {
25
+ const joinedInput = TemplatePath.join(inputDir, rawDir || '');
26
+ const joinedOutput = TemplatePath.join(outputDir, rawDir || '');
27
+
28
+ return {
29
+ input: addTrailingSlash(TemplatePath.standardizeFilePath(joinedInput)),
30
+ output: addTrailingSlash(TemplatePath.standardizeFilePath(joinedOutput))
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Slugify a string into a wikilink-friendly key.
36
+ * Lowercases, strips diacritics, replaces non-alphanumerics with hyphens,
37
+ * trims leading/trailing hyphens. Returns null for empty input.
38
+ *
39
+ * @param {string|null|undefined} input
40
+ * @returns {string|null}
41
+ */
42
+ export function slugify(input) {
43
+ if (input == null) return null;
44
+ const slug = slugifyLib(String(input), { lower: true, strict: true, trim: true });
45
+ return slug || null;
46
+ }
47
+
48
+ /**
49
+ * Normalize language input to an object map.
50
+ * Accepts an array of language codes or an object keyed by language code.
51
+ * Returns null if input is invalid or empty.
52
+ *
53
+ * @param {Object} settings - Options object containing languages.
54
+ * @param {import('../logging.js').BaselineLogger} [logger] - Logger for dropped-entry notice.
55
+ * @returns {Record<string, Object>|null} Normalized language map, or null.
56
+ */
57
+ export function normalizeLanguages(settings, logger) {
58
+ const normalizedLanguages = Array.isArray(settings.languages)
59
+ ? Object.fromEntries(
60
+ settings.languages
61
+ .filter((lang) => typeof lang === 'string' && lang.trim())
62
+ .map((lang) => [lang.toLowerCase().trim(), {}])
63
+ )
64
+ : settings.languages && typeof settings.languages === 'object'
65
+ ? settings.languages
66
+ : null;
67
+
68
+ if (logger && Array.isArray(settings.languages)) {
69
+ const normalizedCount = normalizedLanguages ? Object.keys(normalizedLanguages).length : 0;
70
+ if (normalizedCount !== settings.languages.length) {
71
+ logger.info('Some languages entries were invalid and were dropped.');
72
+ }
73
+ }
74
+ return normalizedLanguages;
75
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Return the first value that is neither undefined nor null.
3
+ * @param {...*} values
4
+ * @returns {*}
5
+ */
6
+ const pick = (...values) => values.find((v) => v !== undefined && v !== null);
7
+ export default pick;