@stati/core 1.27.2 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/errors.d.ts +10 -0
- package/dist/config/errors.d.ts.map +1 -0
- package/dist/config/errors.js +10 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +15 -2
- package/dist/constants.d.ts +26 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +23 -0
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +96 -24
- package/dist/core/isg/builder.d.ts +14 -2
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +41 -7
- package/dist/core/isg/deps.d.ts +9 -0
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +30 -1
- package/dist/core/isg/hash.d.ts +10 -0
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +14 -0
- package/dist/core/isg/index.d.ts +2 -1
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +1 -1
- package/dist/core/isg/manifest.js +8 -0
- package/dist/core/markdown.d.ts +10 -1
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +13 -3
- package/dist/core/templates.d.ts +12 -1
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +61 -6
- package/dist/core/utils/fs.utils.d.ts +21 -1
- package/dist/core/utils/fs.utils.d.ts.map +1 -1
- package/dist/core/utils/fs.utils.js +61 -0
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +1 -1
- package/dist/images/config.d.ts +52 -0
- package/dist/images/config.d.ts.map +1 -0
- package/dist/images/config.js +200 -0
- package/dist/images/index.d.ts +15 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +11 -0
- package/dist/images/manifest.d.ts +36 -0
- package/dist/images/manifest.d.ts.map +1 -0
- package/dist/images/manifest.js +69 -0
- package/dist/images/markdown-rule.d.ts +26 -0
- package/dist/images/markdown-rule.d.ts.map +1 -0
- package/dist/images/markdown-rule.js +60 -0
- package/dist/images/markup.d.ts +39 -0
- package/dist/images/markup.d.ts.map +1 -0
- package/dist/images/markup.js +72 -0
- package/dist/images/og.d.ts +52 -0
- package/dist/images/og.d.ts.map +1 -0
- package/dist/images/og.js +165 -0
- package/dist/images/plan.d.ts +101 -0
- package/dist/images/plan.d.ts.map +1 -0
- package/dist/images/plan.js +128 -0
- package/dist/images/provider.d.ts +35 -0
- package/dist/images/provider.d.ts.map +1 -0
- package/dist/images/provider.js +70 -0
- package/dist/images/resolve.d.ts +45 -0
- package/dist/images/resolve.d.ts.map +1 -0
- package/dist/images/resolve.js +51 -0
- package/dist/images/runtime.d.ts +258 -0
- package/dist/images/runtime.d.ts.map +1 -0
- package/dist/images/runtime.js +659 -0
- package/dist/images/sources.d.ts +47 -0
- package/dist/images/sources.d.ts.map +1 -0
- package/dist/images/sources.js +97 -0
- package/dist/images/utils/image-probe.utils.d.ts +30 -0
- package/dist/images/utils/image-probe.utils.d.ts.map +1 -0
- package/dist/images/utils/image-probe.utils.js +307 -0
- package/dist/images/utils/index.d.ts +7 -0
- package/dist/images/utils/index.d.ts.map +1 -0
- package/dist/images/utils/index.js +5 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/metrics/noop.d.ts.map +1 -1
- package/dist/metrics/noop.js +3 -0
- package/dist/metrics/recorder.d.ts.map +1 -1
- package/dist/metrics/recorder.js +3 -0
- package/dist/metrics/types.d.ts +8 -0
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/seo/auto-inject.d.ts +3 -0
- package/dist/seo/auto-inject.d.ts.map +1 -1
- package/dist/seo/auto-inject.js +1 -0
- package/dist/seo/generator.d.ts +24 -0
- package/dist/seo/generator.d.ts.map +1 -1
- package/dist/seo/generator.js +65 -1
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/index.d.ts.map +1 -1
- package/dist/seo/index.js +1 -1
- package/dist/types/config.d.ts +5 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +21 -2
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/images.d.ts +185 -0
- package/dist/types/images.d.ts.map +1 -0
- package/dist/types/images.js +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +7 -0
- package/dist/types/isg.d.ts.map +1 -1
- package/dist/types/seo.d.ts +9 -0
- package/dist/types/seo.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation and defaults for the `images` configuration block.
|
|
3
|
+
* @module images/config
|
|
4
|
+
*/
|
|
5
|
+
import { ConfigValidationError } from '../config/errors.js';
|
|
6
|
+
import { DEFAULT_IMAGE_FORMATS, DEFAULT_IMAGE_QUALITY, DEFAULT_IMAGE_SIZES, DEFAULT_IMAGE_WIDTHS, DEFAULT_OG_HEIGHT, DEFAULT_OG_TEMPLATE, DEFAULT_OG_WIDTH, } from '../constants.js';
|
|
7
|
+
const IMAGES_KEYS = ['eagerFirst', 'responsive', 'og', 'dev'];
|
|
8
|
+
const RESPONSIVE_KEYS = ['widths', 'formats', 'quality', 'sizes'];
|
|
9
|
+
const QUALITY_KEYS = ['avif', 'webp', 'jpeg', 'png'];
|
|
10
|
+
const OG_KEYS = ['template', 'width', 'height', 'format', 'fonts', 'exclude'];
|
|
11
|
+
const FONT_KEYS = ['name', 'path', 'weight', 'style'];
|
|
12
|
+
const VARIANT_FORMATS = ['avif', 'webp'];
|
|
13
|
+
const MAX_IMAGE_WIDTH = 16384;
|
|
14
|
+
const MAX_OG_SIZE = 4096;
|
|
15
|
+
function fail(message) {
|
|
16
|
+
throw new ConfigValidationError(message);
|
|
17
|
+
}
|
|
18
|
+
function describe(value) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(value) ?? String(value);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return String(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isPlainObject(value) {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function checkKeys(value, allowed, path) {
|
|
30
|
+
for (const key of Object.keys(value)) {
|
|
31
|
+
if (!allowed.includes(key)) {
|
|
32
|
+
fail(`Unknown option ${path}.${key}. Allowed options: ${allowed.join(', ')}.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function isIntegerInRange(value, min, max) {
|
|
37
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max;
|
|
38
|
+
}
|
|
39
|
+
function validateResponsive(value) {
|
|
40
|
+
if (typeof value === 'boolean')
|
|
41
|
+
return;
|
|
42
|
+
if (!isPlainObject(value)) {
|
|
43
|
+
fail(`images.responsive must be true, false or an object (got ${describe(value)}).`);
|
|
44
|
+
}
|
|
45
|
+
checkKeys(value, RESPONSIVE_KEYS, 'images.responsive');
|
|
46
|
+
if (value.widths !== undefined) {
|
|
47
|
+
if (!Array.isArray(value.widths) ||
|
|
48
|
+
value.widths.length === 0 ||
|
|
49
|
+
!value.widths.every((w) => isIntegerInRange(w, 1, MAX_IMAGE_WIDTH))) {
|
|
50
|
+
fail(`images.responsive.widths must be a non-empty array of whole pixel widths between 1 and ${MAX_IMAGE_WIDTH} (got ${describe(value.widths)}).`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (value.formats !== undefined) {
|
|
54
|
+
if (!Array.isArray(value.formats) ||
|
|
55
|
+
!value.formats.every((f) => typeof f === 'string' && VARIANT_FORMATS.includes(f))) {
|
|
56
|
+
fail(`images.responsive.formats must be an array of 'avif' and 'webp' (got ${describe(value.formats)}).`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (value.quality !== undefined) {
|
|
60
|
+
if (!isPlainObject(value.quality)) {
|
|
61
|
+
fail(`images.responsive.quality must be an object (got ${describe(value.quality)}).`);
|
|
62
|
+
}
|
|
63
|
+
checkKeys(value.quality, QUALITY_KEYS, 'images.responsive.quality');
|
|
64
|
+
for (const [format, quality] of Object.entries(value.quality)) {
|
|
65
|
+
if (quality !== undefined && !isIntegerInRange(quality, 1, 100)) {
|
|
66
|
+
fail(`images.responsive.quality.${format} must be a whole number between 1 and 100 (got ${describe(quality)}).`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (value.sizes !== undefined && (typeof value.sizes !== 'string' || !value.sizes.trim())) {
|
|
71
|
+
fail(`images.responsive.sizes must be a non-empty string (got ${describe(value.sizes)}).`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function validateFont(font, index) {
|
|
75
|
+
const path = `images.og.fonts[${index}]`;
|
|
76
|
+
if (!isPlainObject(font)) {
|
|
77
|
+
fail(`${path} must be an object with name and path (got ${describe(font)}).`);
|
|
78
|
+
}
|
|
79
|
+
checkKeys(font, FONT_KEYS, path);
|
|
80
|
+
if (typeof font.name !== 'string' || !font.name.trim()) {
|
|
81
|
+
fail(`${path}.name must be a non-empty string.`);
|
|
82
|
+
}
|
|
83
|
+
if (typeof font.path !== 'string' || !font.path.trim()) {
|
|
84
|
+
fail(`${path}.path must be a non-empty string.`);
|
|
85
|
+
}
|
|
86
|
+
if (font.weight !== undefined && !isIntegerInRange(font.weight, 100, 900)) {
|
|
87
|
+
fail(`${path}.weight must be a whole number between 100 and 900 (got ${describe(font.weight)}).`);
|
|
88
|
+
}
|
|
89
|
+
if (font.style !== undefined && font.style !== 'normal' && font.style !== 'italic') {
|
|
90
|
+
fail(`${path}.style must be 'normal' or 'italic' (got ${describe(font.style)}).`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function validateOg(value) {
|
|
94
|
+
if (typeof value === 'boolean')
|
|
95
|
+
return;
|
|
96
|
+
if (!isPlainObject(value)) {
|
|
97
|
+
fail(`images.og must be true, false or an object (got ${describe(value)}).`);
|
|
98
|
+
}
|
|
99
|
+
checkKeys(value, OG_KEYS, 'images.og');
|
|
100
|
+
if (value.template !== undefined) {
|
|
101
|
+
if (typeof value.template !== 'string' ||
|
|
102
|
+
!value.template.endsWith('.eta') ||
|
|
103
|
+
/[/\\]/.test(value.template)) {
|
|
104
|
+
fail(`images.og.template must be a file name ending in .eta, such as 'og.eta' (got ${describe(value.template)}).`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const dimension of ['width', 'height']) {
|
|
108
|
+
if (value[dimension] !== undefined && !isIntegerInRange(value[dimension], 1, MAX_OG_SIZE)) {
|
|
109
|
+
fail(`images.og.${dimension} must be a whole number of pixels between 1 and ${MAX_OG_SIZE} (got ${describe(value[dimension])}).`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (value.format !== undefined && value.format !== 'png' && value.format !== 'jpeg') {
|
|
113
|
+
fail(`images.og.format must be 'png' or 'jpeg' (got ${describe(value.format)}).`);
|
|
114
|
+
}
|
|
115
|
+
if (value.fonts !== undefined) {
|
|
116
|
+
if (!Array.isArray(value.fonts)) {
|
|
117
|
+
fail(`images.og.fonts must be an array (got ${describe(value.fonts)}).`);
|
|
118
|
+
}
|
|
119
|
+
value.fonts.forEach(validateFont);
|
|
120
|
+
}
|
|
121
|
+
if (value.exclude !== undefined) {
|
|
122
|
+
if (!Array.isArray(value.exclude) || !value.exclude.every((p) => typeof p === 'string')) {
|
|
123
|
+
fail(`images.og.exclude must be an array of URL glob patterns (got ${describe(value.exclude)}).`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Validates the `images` configuration block.
|
|
129
|
+
*
|
|
130
|
+
* @param images - The `images` value from the config
|
|
131
|
+
* @throws {ConfigValidationError} When an option has the wrong type or value
|
|
132
|
+
*/
|
|
133
|
+
export function validateImagesConfig(images) {
|
|
134
|
+
if (images === undefined || images === false) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!isPlainObject(images)) {
|
|
138
|
+
fail(`images must be an object or false (got ${describe(images)}).`);
|
|
139
|
+
}
|
|
140
|
+
checkKeys(images, IMAGES_KEYS, 'images');
|
|
141
|
+
if (images.eagerFirst !== undefined && typeof images.eagerFirst !== 'boolean') {
|
|
142
|
+
fail(`images.eagerFirst must be true or false (got ${describe(images.eagerFirst)}).`);
|
|
143
|
+
}
|
|
144
|
+
if (images.dev !== undefined && images.dev !== 'fast' && images.dev !== 'full') {
|
|
145
|
+
fail(`images.dev must be 'fast' or 'full' (got ${describe(images.dev)}).`);
|
|
146
|
+
}
|
|
147
|
+
if (images.responsive !== undefined) {
|
|
148
|
+
validateResponsive(images.responsive);
|
|
149
|
+
}
|
|
150
|
+
if (images.og !== undefined) {
|
|
151
|
+
validateOg(images.og);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Applies defaults to the `images` configuration block.
|
|
156
|
+
* Expects a block that passed {@link validateImagesConfig}.
|
|
157
|
+
*
|
|
158
|
+
* @param images - The `images` value from the config
|
|
159
|
+
* @returns The resolved options, or `null` when images are turned off with `images: false`
|
|
160
|
+
*/
|
|
161
|
+
export function resolveImagesConfig(images) {
|
|
162
|
+
if (images === false) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const config = images ?? {};
|
|
166
|
+
let responsive = null;
|
|
167
|
+
if (config.responsive) {
|
|
168
|
+
const options = config.responsive === true ? {} : config.responsive;
|
|
169
|
+
const quality = options.quality ?? {};
|
|
170
|
+
responsive = {
|
|
171
|
+
widths: [...new Set(options.widths ?? DEFAULT_IMAGE_WIDTHS)].sort((a, b) => a - b),
|
|
172
|
+
formats: [...new Set(options.formats ?? DEFAULT_IMAGE_FORMATS)],
|
|
173
|
+
quality: {
|
|
174
|
+
avif: quality.avif ?? DEFAULT_IMAGE_QUALITY.avif,
|
|
175
|
+
webp: quality.webp ?? DEFAULT_IMAGE_QUALITY.webp,
|
|
176
|
+
jpeg: quality.jpeg ?? DEFAULT_IMAGE_QUALITY.jpeg,
|
|
177
|
+
...(quality.png !== undefined && { png: quality.png }),
|
|
178
|
+
},
|
|
179
|
+
sizes: options.sizes ?? DEFAULT_IMAGE_SIZES,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
let og = null;
|
|
183
|
+
if (config.og) {
|
|
184
|
+
const options = config.og === true ? {} : config.og;
|
|
185
|
+
og = {
|
|
186
|
+
template: options.template ?? DEFAULT_OG_TEMPLATE,
|
|
187
|
+
width: options.width ?? DEFAULT_OG_WIDTH,
|
|
188
|
+
height: options.height ?? DEFAULT_OG_HEIGHT,
|
|
189
|
+
format: options.format ?? 'png',
|
|
190
|
+
fonts: options.fonts ?? [],
|
|
191
|
+
exclude: options.exclude ?? [],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
eagerFirst: config.eagerFirst ?? true,
|
|
196
|
+
responsive,
|
|
197
|
+
og,
|
|
198
|
+
dev: config.dev ?? 'fast',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Image pipeline exports
|
|
3
|
+
* Intrinsic sizes, lazy loading and co-located images in core, plus the plumbing for
|
|
4
|
+
* responsive variants and Open Graph images rendered by `@stati/images`.
|
|
5
|
+
*/
|
|
6
|
+
export { resolveImagesConfig, validateImagesConfig } from './config.js';
|
|
7
|
+
export type { ResolvedImagesConfig, ResolvedResponsiveConfig, ResolvedOgConfig } from './config.js';
|
|
8
|
+
export { ImageRuntime, PageImages, validateOgBaseUrl } from './runtime.js';
|
|
9
|
+
export type { StatiImageOptions, SettleResult, ImageRuntimeOptions } from './runtime.js';
|
|
10
|
+
export { installImageRule, selectEagerImage } from './markdown-rule.js';
|
|
11
|
+
export { findOgTemplatePath, clearOgTemplateCache } from './og.js';
|
|
12
|
+
export { loadImageProvider, resolveImagesPackage, missingPackageMessage } from './provider.js';
|
|
13
|
+
export { probeImage } from './utils/index.js';
|
|
14
|
+
export type { ImageProbeResult } from './utils/index.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/images/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACxE,YAAY,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAC3E,YAAY,EAAE,iBAAiB,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACzF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC/F,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Image pipeline exports
|
|
3
|
+
* Intrinsic sizes, lazy loading and co-located images in core, plus the plumbing for
|
|
4
|
+
* responsive variants and Open Graph images rendered by `@stati/images`.
|
|
5
|
+
*/
|
|
6
|
+
export { resolveImagesConfig, validateImagesConfig } from './config.js';
|
|
7
|
+
export { ImageRuntime, PageImages, validateOgBaseUrl } from './runtime.js';
|
|
8
|
+
export { installImageRule, selectEagerImage } from './markdown-rule.js';
|
|
9
|
+
export { findOgTemplatePath, clearOgTemplateCache } from './og.js';
|
|
10
|
+
export { loadImageProvider, resolveImagesPackage, missingPackageMessage } from './provider.js';
|
|
11
|
+
export { probeImage } from './utils/index.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The images manifest, `.stati/images/manifest.json`: which files each image job produced,
|
|
3
|
+
* and cached hashes and probes of source images.
|
|
4
|
+
* @module images/manifest
|
|
5
|
+
*/
|
|
6
|
+
import type { SourceInfo } from './sources.js';
|
|
7
|
+
/**
|
|
8
|
+
* Persisted state of the image pipeline.
|
|
9
|
+
*/
|
|
10
|
+
export interface ImagesManifest {
|
|
11
|
+
version: number;
|
|
12
|
+
/** Cached facts about source images, keyed by absolute path */
|
|
13
|
+
sources: Record<string, SourceInfo>;
|
|
14
|
+
/** Files each job wrote, relative to the output directory, keyed by job key */
|
|
15
|
+
outputs: Record<string, {
|
|
16
|
+
files: string[];
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates an empty images manifest.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createEmptyImagesManifest(): ImagesManifest;
|
|
23
|
+
/**
|
|
24
|
+
* Path of the images manifest inside the cache directory.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getImagesManifestPath(cacheDir: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Loads the images manifest. A missing, unreadable or outdated manifest yields an empty one,
|
|
29
|
+
* which only means images are checked again.
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadImagesManifest(cacheDir: string): Promise<ImagesManifest>;
|
|
32
|
+
/**
|
|
33
|
+
* Saves the images manifest.
|
|
34
|
+
*/
|
|
35
|
+
export declare function saveImagesManifest(cacheDir: string, manifest: ImagesManifest): Promise<void>;
|
|
36
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/images/manifest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAK/C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpC,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC9C;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,cAAc,CAE1D;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE9D;AAMD;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAqClF;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The images manifest, `.stati/images/manifest.json`: which files each image job produced,
|
|
3
|
+
* and cached hashes and probes of source images.
|
|
4
|
+
* @module images/manifest
|
|
5
|
+
*/
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { ensureDir, readFile, writeFile } from '../core/utils/index.js';
|
|
8
|
+
import { IMAGES_CACHE_SUBDIR, MANIFEST_FILENAME } from '../constants.js';
|
|
9
|
+
/** Bumped when the manifest layout changes; older manifests are discarded. */
|
|
10
|
+
const IMAGES_MANIFEST_VERSION = 1;
|
|
11
|
+
/**
|
|
12
|
+
* Creates an empty images manifest.
|
|
13
|
+
*/
|
|
14
|
+
export function createEmptyImagesManifest() {
|
|
15
|
+
return { version: IMAGES_MANIFEST_VERSION, sources: {}, outputs: {} };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Path of the images manifest inside the cache directory.
|
|
19
|
+
*/
|
|
20
|
+
export function getImagesManifestPath(cacheDir) {
|
|
21
|
+
return join(cacheDir, IMAGES_CACHE_SUBDIR, MANIFEST_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
function isStringArray(value) {
|
|
24
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Loads the images manifest. A missing, unreadable or outdated manifest yields an empty one,
|
|
28
|
+
* which only means images are checked again.
|
|
29
|
+
*/
|
|
30
|
+
export async function loadImagesManifest(cacheDir) {
|
|
31
|
+
try {
|
|
32
|
+
const content = await readFile(getImagesManifestPath(cacheDir));
|
|
33
|
+
if (!content)
|
|
34
|
+
return createEmptyImagesManifest();
|
|
35
|
+
const parsed = JSON.parse(content);
|
|
36
|
+
if (parsed?.version !== IMAGES_MANIFEST_VERSION ||
|
|
37
|
+
typeof parsed.sources !== 'object' ||
|
|
38
|
+
typeof parsed.outputs !== 'object') {
|
|
39
|
+
return createEmptyImagesManifest();
|
|
40
|
+
}
|
|
41
|
+
const outputs = {};
|
|
42
|
+
for (const [key, record] of Object.entries(parsed.outputs ?? {})) {
|
|
43
|
+
if (record && isStringArray(record.files)) {
|
|
44
|
+
outputs[key] = { files: record.files };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const sources = {};
|
|
48
|
+
for (const [path, info] of Object.entries(parsed.sources ?? {})) {
|
|
49
|
+
if (info &&
|
|
50
|
+
typeof info.size === 'number' &&
|
|
51
|
+
typeof info.mtimeMs === 'number' &&
|
|
52
|
+
typeof info.hash === 'string') {
|
|
53
|
+
sources[path] = info;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { version: IMAGES_MANIFEST_VERSION, sources, outputs };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return createEmptyImagesManifest();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Saves the images manifest.
|
|
64
|
+
*/
|
|
65
|
+
export async function saveImagesManifest(cacheDir, manifest) {
|
|
66
|
+
const path = getImagesManifestPath(cacheDir);
|
|
67
|
+
await ensureDir(join(cacheDir, IMAGES_CACHE_SUBDIR));
|
|
68
|
+
await writeFile(path, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
69
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markdown-it `image` rule that adds dimensions, lazy loading and responsive variants.
|
|
3
|
+
* @module images/markdown-rule
|
|
4
|
+
*/
|
|
5
|
+
import type MarkdownIt from 'markdown-it';
|
|
6
|
+
import type Token from 'markdown-it/lib/token.mjs';
|
|
7
|
+
/** Key of the page's image state in the markdown-it `env`. */
|
|
8
|
+
export declare const IMAGES_ENV_KEY = "statiImages";
|
|
9
|
+
/**
|
|
10
|
+
* Installs the image rule. It holds no per-page state: each render passes its page's image
|
|
11
|
+
* state in `env`, and renders without it fall through to the previous rule. Installed before
|
|
12
|
+
* plugins and `markdown.configure`, so a custom image rule there replaces it.
|
|
13
|
+
*
|
|
14
|
+
* @param md - The markdown-it instance
|
|
15
|
+
*/
|
|
16
|
+
export declare function installImageRule(md: MarkdownIt): void;
|
|
17
|
+
/**
|
|
18
|
+
* Picks the image `eagerFirst` loads eagerly: the document's first image, when it sits in
|
|
19
|
+
* one of the first top-level blocks. A first image further down is not the page's largest
|
|
20
|
+
* contentful paint, so it stays lazy.
|
|
21
|
+
*
|
|
22
|
+
* @param tokens - Parsed block tokens
|
|
23
|
+
* @returns The image token, or undefined
|
|
24
|
+
*/
|
|
25
|
+
export declare function selectEagerImage(tokens: Token[]): Token | undefined;
|
|
26
|
+
//# sourceMappingURL=markdown-rule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-rule.d.ts","sourceRoot":"","sources":["../../src/images/markdown-rule.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,UAAU,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,KAAK,MAAM,2BAA2B,CAAC;AAGnD,8DAA8D;AAC9D,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAO5C;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,UAAU,GAAG,IAAI,CAwBrD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,GAAG,SAAS,CAanE"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markdown-it `image` rule that adds dimensions, lazy loading and responsive variants.
|
|
3
|
+
* @module images/markdown-rule
|
|
4
|
+
*/
|
|
5
|
+
/** Key of the page's image state in the markdown-it `env`. */
|
|
6
|
+
export const IMAGES_ENV_KEY = 'statiImages';
|
|
7
|
+
/** An image counts as near the top of a page when it sits in one of the first blocks. */
|
|
8
|
+
const EAGER_BLOCK_LIMIT = 3;
|
|
9
|
+
/**
|
|
10
|
+
* Installs the image rule. It holds no per-page state: each render passes its page's image
|
|
11
|
+
* state in `env`, and renders without it fall through to the previous rule. Installed before
|
|
12
|
+
* plugins and `markdown.configure`, so a custom image rule there replaces it.
|
|
13
|
+
*
|
|
14
|
+
* @param md - The markdown-it instance
|
|
15
|
+
*/
|
|
16
|
+
export function installImageRule(md) {
|
|
17
|
+
const previous = md.renderer.rules.image ??
|
|
18
|
+
((tokens, idx, options, _env, self) => self.renderToken(tokens, idx, options));
|
|
19
|
+
md.renderer.rules.image = (tokens, idx, options, env, self) => {
|
|
20
|
+
const images = env?.[IMAGES_ENV_KEY];
|
|
21
|
+
const token = tokens[idx];
|
|
22
|
+
if (!images || !token) {
|
|
23
|
+
return previous(tokens, idx, options, env, self);
|
|
24
|
+
}
|
|
25
|
+
const alt = self.renderInlineAsText(token.children ?? [], options, env);
|
|
26
|
+
const result = images.renderMarkdownImage(token, alt);
|
|
27
|
+
if ('html' in result) {
|
|
28
|
+
return result.html;
|
|
29
|
+
}
|
|
30
|
+
// Left as authored, apart from a stripped `?raw` suffix
|
|
31
|
+
if (result.src !== token.attrGet('src')) {
|
|
32
|
+
token.attrSet('src', result.src);
|
|
33
|
+
}
|
|
34
|
+
return previous(tokens, idx, options, env, self);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Picks the image `eagerFirst` loads eagerly: the document's first image, when it sits in
|
|
39
|
+
* one of the first top-level blocks. A first image further down is not the page's largest
|
|
40
|
+
* contentful paint, so it stays lazy.
|
|
41
|
+
*
|
|
42
|
+
* @param tokens - Parsed block tokens
|
|
43
|
+
* @returns The image token, or undefined
|
|
44
|
+
*/
|
|
45
|
+
export function selectEagerImage(tokens) {
|
|
46
|
+
let blocks = 0;
|
|
47
|
+
for (const token of tokens) {
|
|
48
|
+
if (token.level === 0 && token.nesting !== -1) {
|
|
49
|
+
blocks++;
|
|
50
|
+
if (blocks > EAGER_BLOCK_LIMIT)
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (token.type === 'inline') {
|
|
54
|
+
const image = token.children?.find((child) => child.type === 'image');
|
|
55
|
+
if (image)
|
|
56
|
+
return image;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML for processed images.
|
|
3
|
+
* @module images/markup
|
|
4
|
+
*/
|
|
5
|
+
import type { VariantPlan } from './plan.js';
|
|
6
|
+
/**
|
|
7
|
+
* Attributes for an `<img>` element.
|
|
8
|
+
*/
|
|
9
|
+
export interface ImgMarkupOptions {
|
|
10
|
+
alt: string;
|
|
11
|
+
title?: string | undefined;
|
|
12
|
+
/** Other authored attributes, such as `class` or `id`, in order */
|
|
13
|
+
attributes?: ReadonlyArray<readonly [string, string]> | undefined;
|
|
14
|
+
width?: number | string | undefined;
|
|
15
|
+
height?: number | string | undefined;
|
|
16
|
+
/** Load eagerly with high fetch priority instead of lazily */
|
|
17
|
+
eager?: boolean | undefined;
|
|
18
|
+
srcset?: string | undefined;
|
|
19
|
+
sizes?: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Renders an `<img>` with dimensions, loading hints and authored attributes.
|
|
23
|
+
*
|
|
24
|
+
* @param src - Image URL
|
|
25
|
+
* @param options - Attributes
|
|
26
|
+
* @returns The `<img>` element
|
|
27
|
+
*/
|
|
28
|
+
export declare function renderImg(src: string, options: ImgMarkupOptions): string;
|
|
29
|
+
/**
|
|
30
|
+
* Renders a responsive image: a `<picture>` with one `<source>` per modern format, or a
|
|
31
|
+
* plain `<img srcset>` when there are none.
|
|
32
|
+
*
|
|
33
|
+
* @param plan - The planned variants
|
|
34
|
+
* @param options - Attributes for the `<img>` fallback
|
|
35
|
+
* @param sizes - The `sizes` attribute
|
|
36
|
+
* @returns The `<picture>` or `<img>` element
|
|
37
|
+
*/
|
|
38
|
+
export declare function renderResponsiveImage(plan: VariantPlan, options: Omit<ImgMarkupOptions, 'srcset' | 'sizes'>, sizes: string): string;
|
|
39
|
+
//# sourceMappingURL=markup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markup.d.ts","sourceRoot":"","sources":["../../src/images/markup.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAgB7C;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,mEAAmE;IACnE,UAAU,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAClE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IACrC,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAMD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAexE;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,GAAG,OAAO,CAAC,EACnD,KAAK,EAAE,MAAM,GACZ,MAAM,CAgBR"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML for processed images.
|
|
3
|
+
* @module images/markup
|
|
4
|
+
*/
|
|
5
|
+
import { escapeHtml } from '../seo/utils/index.js';
|
|
6
|
+
/** Attributes Stati sets itself; authored values for these are ignored or handled separately. */
|
|
7
|
+
const MANAGED_ATTRIBUTES = new Set([
|
|
8
|
+
'src',
|
|
9
|
+
'srcset',
|
|
10
|
+
'sizes',
|
|
11
|
+
'alt',
|
|
12
|
+
'title',
|
|
13
|
+
'width',
|
|
14
|
+
'height',
|
|
15
|
+
'loading',
|
|
16
|
+
'decoding',
|
|
17
|
+
'fetchpriority',
|
|
18
|
+
]);
|
|
19
|
+
function attribute(name, value) {
|
|
20
|
+
return ` ${name}="${escapeHtml(String(value))}"`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Renders an `<img>` with dimensions, loading hints and authored attributes.
|
|
24
|
+
*
|
|
25
|
+
* @param src - Image URL
|
|
26
|
+
* @param options - Attributes
|
|
27
|
+
* @returns The `<img>` element
|
|
28
|
+
*/
|
|
29
|
+
export function renderImg(src, options) {
|
|
30
|
+
let html = `<img${attribute('src', src)}`;
|
|
31
|
+
if (options.srcset)
|
|
32
|
+
html += attribute('srcset', options.srcset);
|
|
33
|
+
if (options.sizes)
|
|
34
|
+
html += attribute('sizes', options.sizes);
|
|
35
|
+
html += attribute('alt', options.alt);
|
|
36
|
+
if (options.title)
|
|
37
|
+
html += attribute('title', options.title);
|
|
38
|
+
for (const [name, value] of options.attributes ?? []) {
|
|
39
|
+
if (!MANAGED_ATTRIBUTES.has(name.toLowerCase())) {
|
|
40
|
+
html += attribute(name, value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (options.width !== undefined)
|
|
44
|
+
html += attribute('width', options.width);
|
|
45
|
+
if (options.height !== undefined)
|
|
46
|
+
html += attribute('height', options.height);
|
|
47
|
+
html += options.eager ? ' loading="eager" fetchpriority="high"' : ' loading="lazy"';
|
|
48
|
+
return `${html} decoding="async">`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Renders a responsive image: a `<picture>` with one `<source>` per modern format, or a
|
|
52
|
+
* plain `<img srcset>` when there are none.
|
|
53
|
+
*
|
|
54
|
+
* @param plan - The planned variants
|
|
55
|
+
* @param options - Attributes for the `<img>` fallback
|
|
56
|
+
* @param sizes - The `sizes` attribute
|
|
57
|
+
* @returns The `<picture>` or `<img>` element
|
|
58
|
+
*/
|
|
59
|
+
export function renderResponsiveImage(plan, options, sizes) {
|
|
60
|
+
const img = renderImg(plan.fallback.src, {
|
|
61
|
+
...options,
|
|
62
|
+
srcset: plan.fallback.srcset,
|
|
63
|
+
sizes,
|
|
64
|
+
});
|
|
65
|
+
if (plan.sources.length === 0) {
|
|
66
|
+
return img;
|
|
67
|
+
}
|
|
68
|
+
const sources = plan.sources
|
|
69
|
+
.map((source) => `<source${attribute('type', source.type)}${attribute('srcset', source.srcset)}${attribute('sizes', sizes)}>`)
|
|
70
|
+
.join('');
|
|
71
|
+
return `<picture>${sources}${img}</picture>`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for Open Graph image generation: template lookup, exclusions and inlining.
|
|
3
|
+
* @module images/og
|
|
4
|
+
*/
|
|
5
|
+
import type { PageModel } from '../types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Clears the Open Graph template lookup cache.
|
|
8
|
+
*/
|
|
9
|
+
export declare function clearOgTemplateCache(): void;
|
|
10
|
+
/**
|
|
11
|
+
* Finds the Open Graph template for a page: the nearest file named `templateName` in the
|
|
12
|
+
* page's directory or one of its parents, up to the source directory.
|
|
13
|
+
*
|
|
14
|
+
* @param pageSourcePath - Absolute path of the page's Markdown file
|
|
15
|
+
* @param srcDir - Absolute source directory
|
|
16
|
+
* @param templateName - Template file name, such as `og.eta`
|
|
17
|
+
* @returns Absolute path of the template, or null when there is none
|
|
18
|
+
*/
|
|
19
|
+
export declare function findOgTemplatePath(pageSourcePath: string, srcDir: string, templateName: string): Promise<string | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Whether a page gets no generated Open Graph image: it sets its own image in front matter
|
|
22
|
+
* (`seo.openGraph.image`, or `false` to have none) or matches an `exclude` pattern.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isOgImageExcluded(page: PageModel, exclude: readonly string[]): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* File name stem for a page's Open Graph image, from its URL.
|
|
27
|
+
* The root page is `index`; `/blog/hello/` becomes `blog-hello`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function ogImageSlug(url: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Replaces local `<img src>` values in Open Graph HTML with data URIs, because the renderer
|
|
32
|
+
* does not fetch files. Root-relative paths resolve from the static directory, relative ones
|
|
33
|
+
* from `baseDir`. Remote URLs and data URIs are left alone.
|
|
34
|
+
*
|
|
35
|
+
* @param generatedSource - Finds the source of an image Stati generates (`/_img/...`), such as
|
|
36
|
+
* a responsive variant from `stati.image()`. Cards inline the source, because generated
|
|
37
|
+
* files may not exist yet.
|
|
38
|
+
* @returns The HTML and the absolute paths of the files that were inlined
|
|
39
|
+
*/
|
|
40
|
+
export declare function inlineLocalImages(html: string, dirs: {
|
|
41
|
+
staticDir: string;
|
|
42
|
+
baseDir: string;
|
|
43
|
+
srcDir: string;
|
|
44
|
+
}, generatedSource?: (path: string) => string | undefined): Promise<{
|
|
45
|
+
html: string;
|
|
46
|
+
files: string[];
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* The template path to show in messages, relative to the project.
|
|
50
|
+
*/
|
|
51
|
+
export declare function displayTemplatePath(templatePath: string): string;
|
|
52
|
+
//# sourceMappingURL=og.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"og.d.ts","sourceRoot":"","sources":["../../src/images/og.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAKnD;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAyBxB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAOtF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAU/C;AAsCD;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC5D,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACrD,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAwC5C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhE"}
|