@stati/core 1.29.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -13
- package/dist/config/loader.d.ts +9 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +77 -1
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +39 -29
- package/dist/core/content.d.ts.map +1 -1
- package/dist/core/content.js +9 -7
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +156 -197
- package/dist/core/eta-plugin.d.ts +60 -0
- package/dist/core/eta-plugin.d.ts.map +1 -0
- package/dist/core/eta-plugin.js +143 -0
- package/dist/core/isg/builder.d.ts +37 -19
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +70 -153
- package/dist/core/isg/deps.d.ts +8 -70
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +10 -401
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +2 -1
- package/dist/core/isg/index.d.ts +2 -2
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +3 -3
- package/dist/core/isg/manifest.d.ts.map +1 -1
- package/dist/core/isg/manifest.js +6 -1
- package/dist/core/markdown.d.ts +1 -1
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +3 -2
- package/dist/core/template-index.d.ts +209 -0
- package/dist/core/template-index.d.ts.map +1 -0
- package/dist/core/template-index.js +484 -0
- package/dist/core/template-render.d.ts +137 -0
- package/dist/core/template-render.d.ts.map +1 -0
- package/dist/core/template-render.js +444 -0
- package/dist/core/templates.d.ts +29 -2
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +115 -195
- package/dist/core/utils/context-validation.utils.d.ts +10 -6
- package/dist/core/utils/context-validation.utils.d.ts.map +1 -1
- package/dist/core/utils/context-validation.utils.js +39 -16
- package/dist/core/utils/date.utils.d.ts +1 -1
- package/dist/core/utils/date.utils.js +1 -1
- package/dist/core/utils/dev-css-watcher.utils.d.ts.map +1 -1
- package/dist/core/utils/dev-css-watcher.utils.js +3 -2
- package/dist/core/utils/error-overlay.utils.d.ts.map +1 -1
- package/dist/core/utils/error-overlay.utils.js +7 -45
- package/dist/core/utils/front-matter.utils.d.ts +50 -0
- package/dist/core/utils/front-matter.utils.d.ts.map +1 -0
- package/dist/core/utils/front-matter.utils.js +157 -0
- package/dist/core/utils/index.d.ts +4 -5
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +5 -7
- package/dist/core/utils/paths.utils.d.ts +3 -3
- package/dist/core/utils/paths.utils.js +6 -6
- package/dist/core/utils/tailwind-inventory.utils.d.ts +2 -2
- package/dist/core/utils/tailwind-inventory.utils.d.ts.map +1 -1
- package/dist/core/utils/tailwind-inventory.utils.js +15 -6
- package/dist/core/utils/template-discovery.utils.d.ts +3 -14
- package/dist/core/utils/template-discovery.utils.d.ts.map +1 -1
- package/dist/core/utils/template-discovery.utils.js +2 -62
- package/dist/core/utils/template-errors.utils.d.ts +14 -1
- package/dist/core/utils/template-errors.utils.d.ts.map +1 -1
- package/dist/core/utils/template-errors.utils.js +45 -4
- package/dist/core/utils/theme.utils.d.ts +17 -0
- package/dist/core/utils/theme.utils.d.ts.map +1 -0
- package/dist/core/utils/theme.utils.js +59 -0
- package/dist/images/index.d.ts +1 -2
- package/dist/images/index.d.ts.map +1 -1
- package/dist/images/index.js +0 -1
- package/dist/images/markdown-rule.d.ts +1 -2
- package/dist/images/markdown-rule.d.ts.map +1 -1
- package/dist/images/og.d.ts +2 -15
- package/dist/images/og.d.ts.map +1 -1
- package/dist/images/og.js +4 -47
- package/dist/images/runtime.d.ts +24 -5
- package/dist/images/runtime.d.ts.map +1 -1
- package/dist/images/runtime.js +14 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/metrics/noop.d.ts.map +1 -1
- package/dist/metrics/noop.js +4 -1
- package/dist/metrics/recorder.d.ts.map +1 -1
- package/dist/metrics/recorder.js +11 -8
- package/dist/metrics/types.d.ts +31 -5
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/metrics/utils/html-report.utils.d.ts +2 -1
- package/dist/metrics/utils/html-report.utils.d.ts.map +1 -1
- package/dist/metrics/utils/html-report.utils.js +1115 -1136
- package/dist/types/config.d.ts +18 -6
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +83 -16
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/isg.d.ts +48 -2
- package/dist/types/isg.d.ts.map +1 -1
- package/package.json +6 -6
- package/dist/core/utils/callable-partials.utils.d.ts +0 -60
- package/dist/core/utils/callable-partials.utils.d.ts.map +0 -1
- package/dist/core/utils/callable-partials.utils.js +0 -127
- package/dist/core/utils/partial-validation.utils.d.ts +0 -9
- package/dist/core/utils/partial-validation.utils.d.ts.map +0 -1
- package/dist/core/utils/partial-validation.utils.js +0 -107
package/dist/core/templates.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Eta } from 'eta';
|
|
2
|
-
import { join,
|
|
3
|
-
import
|
|
4
|
-
import { createFallbackLogger } from './utils/index.js';
|
|
5
|
-
import { TEMPLATE_EXTENSION } from '../constants.js';
|
|
6
|
-
import { getStatiVersion, isCollectionIndexPage, discoverLayout, resolveSrcDir, createTemplateError, createValidatingPartialsProxy, createValidatingContextProxy, propValue, slugify, wrapPartialsAsCallable, createNavigationHelpers, } from './utils/index.js';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { createFallbackLogger, getStatiVersion, isCollectionIndexPage, resolveSrcDir, createTemplateError, propValue, slugify, createNavigationHelpers, } from './utils/index.js';
|
|
7
4
|
import { getCollections, resolveCollectionPath, createCollectionView, copyPaginationData, copyGeneratedData, } from './collections.js';
|
|
8
5
|
import { isDevelopment, isProduction } from '../env.js';
|
|
9
6
|
import { generateSEO, resolveGeneratedImagePlaceholders } from '../seo/index.js';
|
|
7
|
+
import { TemplateIndex } from './template-index.js';
|
|
8
|
+
import { RenderScope, failingTemplateOf, runInRenderScope, } from './template-render.js';
|
|
9
|
+
import { createStatiEtaPlugin, installEtaPlugin } from './eta-plugin.js';
|
|
10
10
|
/**
|
|
11
11
|
* `stati.image()` when images are turned off: a plain `<img>`, attributes escaped.
|
|
12
12
|
*/
|
|
@@ -46,59 +46,6 @@ function createNavView(helpers, reads) {
|
|
|
46
46
|
}
|
|
47
47
|
return view;
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Discovers partials in the hierarchy for a given page path.
|
|
51
|
-
* Scans all parent directories for folders starting with underscore.
|
|
52
|
-
*
|
|
53
|
-
* @param pagePath - The path to the page (relative to srcDir)
|
|
54
|
-
* @param config - Stati configuration
|
|
55
|
-
* @returns Object mapping partial names to their file paths
|
|
56
|
-
*/
|
|
57
|
-
async function discoverPartials(pagePath, config) {
|
|
58
|
-
const srcDir = resolveSrcDir(config);
|
|
59
|
-
const partials = {};
|
|
60
|
-
// Get the directory of the current page
|
|
61
|
-
const pageDir = dirname(pagePath);
|
|
62
|
-
const pathSegments = pageDir === '.' ? [] : pageDir.split('/');
|
|
63
|
-
// Scan from root to current directory (least specific first)
|
|
64
|
-
// This allows more specific partials to override less specific ones
|
|
65
|
-
const dirsToScan = [];
|
|
66
|
-
if (pathSegments.length > 0) {
|
|
67
|
-
// Add root directory first, then parent directories, then current
|
|
68
|
-
dirsToScan.push(''); // Root directory first
|
|
69
|
-
// Add directories from root to current
|
|
70
|
-
for (let i = 1; i <= pathSegments.length; i++) {
|
|
71
|
-
dirsToScan.push(pathSegments.slice(0, i).join('/'));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
dirsToScan.push(''); // Root directory only
|
|
76
|
-
}
|
|
77
|
-
for (const dir of dirsToScan) {
|
|
78
|
-
const searchDir = dir ? join(srcDir, dir) : srcDir;
|
|
79
|
-
// Find all underscore folders in this directory level
|
|
80
|
-
const globPattern = join(searchDir, '_*/').replace(/\\/g, '/');
|
|
81
|
-
const underscoreFolders = await glob(globPattern, {
|
|
82
|
-
absolute: true,
|
|
83
|
-
onlyDirectories: true,
|
|
84
|
-
});
|
|
85
|
-
// Scan each underscore folder for .eta files
|
|
86
|
-
for (const folderPath of underscoreFolders) {
|
|
87
|
-
const etaFiles = await glob('**/*.eta', {
|
|
88
|
-
cwd: folderPath,
|
|
89
|
-
absolute: false,
|
|
90
|
-
});
|
|
91
|
-
for (const etaFile of etaFiles) {
|
|
92
|
-
const partialName = basename(etaFile, TEMPLATE_EXTENSION);
|
|
93
|
-
const fullPath = join(folderPath, etaFile);
|
|
94
|
-
// Get relative path from srcDir to the partial file
|
|
95
|
-
const relativePath = relative(srcDir, fullPath);
|
|
96
|
-
partials[partialName] = posix.normalize(relativePath);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return partials;
|
|
101
|
-
}
|
|
102
49
|
/**
|
|
103
50
|
* Cached template engine for dev mode to avoid re-creating on each rebuild.
|
|
104
51
|
*
|
|
@@ -117,6 +64,12 @@ let cachedTemplateEngine = null;
|
|
|
117
64
|
export function clearTemplateEngineCache() {
|
|
118
65
|
cachedTemplateEngine = null;
|
|
119
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Creates the template engine: Eta with `stati` as its data variable and Stati's plugin,
|
|
69
|
+
* which replaces Eta's own `include()`, `layout()` and `block()` in templates.
|
|
70
|
+
*
|
|
71
|
+
* @throws {Error} When the installed Eta generates code the plugin does not recognize
|
|
72
|
+
*/
|
|
120
73
|
export function createTemplateEngine(config) {
|
|
121
74
|
const templateDir = resolveSrcDir(config);
|
|
122
75
|
// In dev mode, reuse a single cached engine across rebuilds to avoid
|
|
@@ -124,8 +77,8 @@ export function createTemplateEngine(config) {
|
|
|
124
77
|
if (isDevelopment() && cachedTemplateEngine && cachedTemplateEngine.templateDir === templateDir) {
|
|
125
78
|
return cachedTemplateEngine.eta;
|
|
126
79
|
}
|
|
127
|
-
// Enable the template cache in dev too so
|
|
128
|
-
//
|
|
80
|
+
// Enable the template cache in dev too so templates compile once and are reused
|
|
81
|
+
// across rebuilds; template edits clear this cache via clearTemplateEngineCache().
|
|
129
82
|
const useCache = isDevelopment() || isProduction();
|
|
130
83
|
const eta = new Eta({
|
|
131
84
|
views: templateDir,
|
|
@@ -135,33 +88,40 @@ export function createTemplateEngine(config) {
|
|
|
135
88
|
// Debug mode wraps runtime errors with the template path and line number,
|
|
136
89
|
// which the dev error overlay uses to point at the failing line
|
|
137
90
|
debug: isDevelopment(),
|
|
91
|
+
plugins: [createStatiEtaPlugin()],
|
|
138
92
|
});
|
|
93
|
+
installEtaPlugin(eta);
|
|
139
94
|
if (isDevelopment()) {
|
|
140
95
|
cachedTemplateEngine = { templateDir, eta };
|
|
141
96
|
}
|
|
142
97
|
return eta;
|
|
143
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Renders a page: its layout (chosen by front matter `layout:` or discovery), the parents the
|
|
101
|
+
* layout names with `stati.layout()`, and every template they include, plus the page's Open
|
|
102
|
+
* Graph template when it gets a generated image. Records every template and name lookup for
|
|
103
|
+
* the ISG cache.
|
|
104
|
+
*/
|
|
144
105
|
export async function renderPage(page, body, config, eta, navigation, allPages, assets, toc, logger, extras) {
|
|
145
106
|
const log = logger || createFallbackLogger();
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
107
|
+
const index = extras?.templateIndex ?? (await TemplateIndex.load(resolveSrcDir(config)));
|
|
108
|
+
const scope = new RenderScope({ page, eta, index, logger: log, development: isDevelopment() });
|
|
109
|
+
return runInRenderScope(scope, () => renderPageInScope(scope, body, config, navigation, allPages, assets, toc, extras?.images));
|
|
110
|
+
}
|
|
111
|
+
async function renderPageInScope(scope, body, config, navigation, allPages, assets, toc, pageImages) {
|
|
112
|
+
const { page, logger: log, index } = scope;
|
|
113
|
+
// The generated Open Graph image is known once the og template has rendered, before the
|
|
114
|
+
// layout. Until then stati.generateSEO() emits placeholders for its tags.
|
|
149
115
|
const ogState = {
|
|
150
116
|
image: undefined,
|
|
151
117
|
pending: pageImages?.wantsOg() ?? false,
|
|
152
118
|
};
|
|
153
|
-
// Discover partials for this page's directory hierarchy
|
|
154
|
-
const srcDir = resolveSrcDir(config);
|
|
155
|
-
const relativePath = relative(srcDir, page.sourcePath);
|
|
156
|
-
const partialPaths = await discoverPartials(relativePath, config);
|
|
157
|
-
// Count templates: partials + layout (if found)
|
|
158
|
-
const partialsCount = Object.keys(partialPaths).length;
|
|
159
119
|
// What this render reads from shared data, so the cache can rebuild the page when it changes
|
|
160
120
|
const reads = { collection: false, nav: false };
|
|
161
121
|
// The page's collection: its own on index pages, the one it belongs to on other pages, or
|
|
162
122
|
// the one its front matter names. Collections are computed once per page list.
|
|
163
123
|
let collectionData;
|
|
164
|
-
const isIndexPage = allPages && isCollectionIndexPage(page, allPages);
|
|
124
|
+
const isIndexPage = allPages !== undefined && isCollectionIndexPage(page, allPages);
|
|
165
125
|
if (allPages) {
|
|
166
126
|
const collectionPath = resolveCollectionPath(page, allPages);
|
|
167
127
|
const data = collectionPath !== undefined
|
|
@@ -171,11 +131,13 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
|
|
|
171
131
|
collectionData = createCollectionView(data, reads);
|
|
172
132
|
}
|
|
173
133
|
}
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
134
|
+
// The layout: front matter `layout:` by the name lookup, otherwise layout.eta discovery
|
|
135
|
+
// (index.eta first on index pages)
|
|
136
|
+
const layout = index.resolvePageLayout(scope.pageFile, page.frontMatter.layout, isIndexPage);
|
|
137
|
+
for (const lookup of layout.lookups) {
|
|
138
|
+
scope.record(lookup);
|
|
139
|
+
}
|
|
140
|
+
reportFrontMatterLayout(scope, layout);
|
|
179
141
|
// Create navigation helpers
|
|
180
142
|
const navTree = navigation || [];
|
|
181
143
|
const navHelpers = createNavigationHelpers(navTree, page);
|
|
@@ -203,24 +165,22 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
|
|
|
203
165
|
return currentNavNode;
|
|
204
166
|
},
|
|
205
167
|
});
|
|
206
|
-
//
|
|
207
|
-
|
|
168
|
+
// Values every template of the page shares. Each render adds props, body, include and layout.
|
|
169
|
+
// The site's helpers live under `helpers`; the root holds only Stati's own API.
|
|
170
|
+
const values = {
|
|
208
171
|
site: config.site,
|
|
209
172
|
page: pageContext,
|
|
210
173
|
content: body,
|
|
211
|
-
nav: createNavView(navHelpers, reads),
|
|
212
|
-
collection: collectionData,
|
|
213
|
-
// Set on paginated listings and on pages generated from [param].md templates
|
|
214
|
-
// the filters, like slugify, so that an existing filter with the same name keeps working.
|
|
174
|
+
nav: createNavView(navHelpers, reads),
|
|
175
|
+
collection: collectionData,
|
|
176
|
+
// Set on paginated listings and on pages generated from [param].md templates
|
|
215
177
|
pagination: page.pagination ? copyPaginationData(page.pagination) : undefined,
|
|
216
178
|
generated: page.generated ? copyGeneratedData(page.generated) : undefined,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
...(config.eta?.filters || {}),
|
|
179
|
+
// A copy, so a template that replaces a helper does not change it for later pages
|
|
180
|
+
helpers: { ...config.helpers },
|
|
220
181
|
generator: {
|
|
221
182
|
version: getStatiVersion(),
|
|
222
183
|
},
|
|
223
|
-
// Stati utilities object with helper functions
|
|
224
184
|
generateSEO: (tags) => generateSEO({
|
|
225
185
|
page,
|
|
226
186
|
config,
|
|
@@ -229,141 +189,101 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
|
|
|
229
189
|
deferGeneratedImage: ogState.pending,
|
|
230
190
|
}, tags),
|
|
231
191
|
propValue,
|
|
192
|
+
slugify,
|
|
232
193
|
// Image with dimensions, lazy loading and, with images.responsive, srcset variants
|
|
233
194
|
image: (src, options) => pageImages ? pageImages.renderHelperImage(src, options) : renderPlainImage(src, options),
|
|
234
195
|
assets, // TypeScript bundle assets (available when typescript.enabled is true)
|
|
235
|
-
// Set by callable partials invoked with props; always present so that
|
|
236
|
-
// stati.props?.x works under the dev-mode context validation
|
|
237
|
-
props: undefined,
|
|
238
196
|
// Set while the Open Graph template renders: { width, height }
|
|
239
197
|
og: undefined,
|
|
240
198
|
// The page's generated Open Graph image, once rendered: { url, path, width, height, alt }
|
|
241
199
|
ogImage: undefined,
|
|
242
200
|
};
|
|
243
|
-
// Render
|
|
244
|
-
// Use multiple passes to allow partials to reference other partials
|
|
245
|
-
const renderedPartials = {};
|
|
246
|
-
const maxPasses = 3; // Prevent infinite loops
|
|
247
|
-
for (let pass = 0; pass < maxPasses; pass++) {
|
|
248
|
-
// Each pass renders all partials to ensure they have access to all previously rendered ones
|
|
249
|
-
// TODO: Optimize by tracking which partials changed or have dependencies
|
|
250
|
-
const partialsToRender = { ...partialPaths };
|
|
251
|
-
if (Object.keys(partialsToRender).length === 0) {
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
let progressMade = false;
|
|
255
|
-
const passRenderedPartials = {};
|
|
256
|
-
for (const [partialName, partialPath] of Object.entries(partialsToRender)) {
|
|
257
|
-
try {
|
|
258
|
-
// Create context with all previously rendered partials available
|
|
259
|
-
const combinedPartials = { ...renderedPartials, ...passRenderedPartials };
|
|
260
|
-
// Wrap partials as callable before passing to validation proxy
|
|
261
|
-
const callablePartials = wrapPartialsAsCallable(eta, combinedPartials, partialPaths, baseContext);
|
|
262
|
-
const partialContext = createValidatingContextProxy({
|
|
263
|
-
...baseContext,
|
|
264
|
-
partials: createValidatingPartialsProxy(callablePartials), // Include both previous and current pass partials with validation
|
|
265
|
-
});
|
|
266
|
-
const renderedContent = await eta.renderAsync(partialPath, partialContext);
|
|
267
|
-
passRenderedPartials[partialName] = renderedContent;
|
|
268
|
-
progressMade = true;
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
// If this is the last pass, log the error and create placeholder
|
|
272
|
-
if (pass === maxPasses - 1) {
|
|
273
|
-
log.warning(`Failed to render partial ${partialName} at ${partialPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
274
|
-
// In development mode, throw enhanced template error for partials too
|
|
275
|
-
if (isDevelopment()) {
|
|
276
|
-
const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), partialPath);
|
|
277
|
-
throw templateError;
|
|
278
|
-
}
|
|
279
|
-
passRenderedPartials[partialName] = `<!-- Error rendering partial: ${partialName} -->`;
|
|
280
|
-
progressMade = true;
|
|
281
|
-
}
|
|
282
|
-
// Otherwise, use existing content if available, or skip for retry
|
|
283
|
-
else if (renderedPartials[partialName]) {
|
|
284
|
-
passRenderedPartials[partialName] = renderedPartials[partialName];
|
|
285
|
-
}
|
|
286
|
-
// For failed partials on non-last pass, still count as progress to allow retries
|
|
287
|
-
else {
|
|
288
|
-
progressMade = true;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
// Update the rendered partials with this pass's results
|
|
293
|
-
Object.assign(renderedPartials, passRenderedPartials);
|
|
294
|
-
// If no progress was made, break to avoid infinite loop
|
|
295
|
-
if (!progressMade) {
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
// If this is pass 0, always do at least one more pass to allow interdependencies
|
|
299
|
-
if (pass === 0) {
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
// For subsequent passes, only continue if we're not at max passes yet
|
|
303
|
-
if (pass >= maxPasses - 1) {
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// Render the Open Graph template with the layout's context, minus the page body, so the
|
|
201
|
+
// Render the Open Graph template with the layout's values, minus the page body, so the
|
|
308
202
|
// generated image is known before the layout renders its SEO tags
|
|
309
203
|
if (pageImages && ogState.pending) {
|
|
310
204
|
// Copy the page's property descriptors so that navNode stays a getter: spreading would
|
|
311
205
|
// read it and count every page with a card as reading the navigation
|
|
312
|
-
const ogPage = Object.defineProperties({}, Object.getOwnPropertyDescriptors(
|
|
206
|
+
const ogPage = Object.defineProperties({}, Object.getOwnPropertyDescriptors(pageContext));
|
|
313
207
|
ogPage.content = '';
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
content: '',
|
|
317
|
-
page: ogPage,
|
|
318
|
-
og: pageImages.ogSize(),
|
|
208
|
+
const ogBase = {
|
|
209
|
+
id: 'og',
|
|
210
|
+
values: { ...values, content: '', page: ogPage, og: pageImages.ogSize() },
|
|
319
211
|
};
|
|
320
|
-
|
|
321
|
-
...ogBaseContext,
|
|
322
|
-
partials: createValidatingPartialsProxy(wrapPartialsAsCallable(eta, renderedPartials, partialPaths, ogBaseContext)),
|
|
323
|
-
});
|
|
324
|
-
ogState.image = (await pageImages.renderOg(ogContext, eta)) ?? undefined;
|
|
212
|
+
ogState.image = (await pageImages.renderOg(createOgRenderer(scope, ogBase))) ?? undefined;
|
|
325
213
|
ogState.pending = false;
|
|
326
|
-
|
|
214
|
+
values.ogImage = ogState.image;
|
|
327
215
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
216
|
+
const result = (html) => ({
|
|
217
|
+
html,
|
|
218
|
+
templatesLoaded: scope.templates.size,
|
|
219
|
+
templateRenders: scope.renders,
|
|
220
|
+
includeCacheHits: scope.includeCacheHits,
|
|
221
|
+
ogImage: ogState.image,
|
|
222
|
+
reads,
|
|
223
|
+
templates: scope.templateFiles(layout.file),
|
|
224
|
+
templateLookups: [...scope.lookups.values()],
|
|
333
225
|
});
|
|
226
|
+
if (!layout.file) {
|
|
227
|
+
log.warning('No layout template found, using fallback');
|
|
228
|
+
return result(createFallbackHtml(page, body));
|
|
229
|
+
}
|
|
334
230
|
try {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
html: createFallbackHtml(page, body),
|
|
339
|
-
templatesLoaded,
|
|
340
|
-
ogImage: ogState.image,
|
|
341
|
-
reads,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
const html = await eta.renderAsync(layoutPath, context);
|
|
345
|
-
return {
|
|
346
|
-
html: resolveGeneratedImagePlaceholders(html, ogState.image),
|
|
347
|
-
templatesLoaded,
|
|
348
|
-
ogImage: ogState.image,
|
|
349
|
-
reads,
|
|
350
|
-
};
|
|
231
|
+
const html = await scope.renderAsync({ id: 'page', values }, { file: layout.file });
|
|
232
|
+
return result(resolveGeneratedImagePlaceholders(html, ogState.image));
|
|
351
233
|
}
|
|
352
234
|
catch (error) {
|
|
353
|
-
|
|
354
|
-
|
|
235
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
236
|
+
const failed = failingTemplateOf(failure) ?? layout.file;
|
|
237
|
+
// In development, throw the error for the overlay; it points at the innermost template
|
|
355
238
|
if (isDevelopment()) {
|
|
356
|
-
const
|
|
239
|
+
const failedPath = join(index.srcDir, failed);
|
|
240
|
+
const templateError = createTemplateError(failure, failedPath);
|
|
241
|
+
const where = scope.displayPath(templateError.filePath ?? failedPath);
|
|
242
|
+
log.error(`Error rendering ${where} for ${page.url}: ${templateError.message}`);
|
|
357
243
|
throw templateError;
|
|
358
244
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
templatesLoaded,
|
|
362
|
-
ogImage: ogState.image,
|
|
363
|
-
reads,
|
|
364
|
-
};
|
|
245
|
+
log.error(`Error rendering ${scope.display(failed)} for ${page.url}: ${failure.message}`);
|
|
246
|
+
return result(createFallbackHtml(page, body));
|
|
365
247
|
}
|
|
366
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Warns about a front matter `layout:` value that names no template, with the templates it
|
|
251
|
+
* could have meant. Layout discovery picks the page's layout instead.
|
|
252
|
+
*/
|
|
253
|
+
function reportFrontMatterLayout(scope, layout) {
|
|
254
|
+
const unresolved = layout.unresolvedFrontMatter;
|
|
255
|
+
if (!unresolved) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const page = scope.displayPath(scope.page.sourcePath);
|
|
259
|
+
const used = layout.file === null ? 'the fallback HTML' : scope.display(layout.file);
|
|
260
|
+
const didYouMean = unresolved.suggestions.length > 0
|
|
261
|
+
? ` Did you mean ${unresolved.suggestions.map((name) => `"${name}"`).join(', ')}?`
|
|
262
|
+
: '';
|
|
263
|
+
scope.logger.warning(`${page}: front matter layout ${JSON.stringify(unresolved.value)} ${unresolved.problem}; using ${used}.${didYouMean}`);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Lets the image pipeline find the page's Open Graph template through the template index,
|
|
267
|
+
* recording the lookup, and render it through the render scope, so it can use
|
|
268
|
+
* `stati.include()` and `stati.layout()` like any template.
|
|
269
|
+
*/
|
|
270
|
+
function createOgRenderer(scope, base) {
|
|
271
|
+
return {
|
|
272
|
+
find(templateName) {
|
|
273
|
+
const name = templateName.replace(/\.eta$/, '');
|
|
274
|
+
const file = scope.index.lookup(name, scope.pageFolder);
|
|
275
|
+
scope.record({ name, from: scope.pageFolder, file });
|
|
276
|
+
return file === null ? null : join(scope.index.srcDir, file);
|
|
277
|
+
},
|
|
278
|
+
render(template) {
|
|
279
|
+
return 'path' in template
|
|
280
|
+
? scope.renderAsync(base, {
|
|
281
|
+
file: relative(scope.index.srcDir, template.path).replace(/\\/g, '/'),
|
|
282
|
+
})
|
|
283
|
+
: scope.renderAsync(base, { source: template.source });
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
367
287
|
function createFallbackHtml(page, body) {
|
|
368
288
|
const title = page.frontMatter.title || 'Untitled';
|
|
369
289
|
const description = page.frontMatter.description || '';
|
|
@@ -6,14 +6,18 @@ export declare function isPassthroughProperty(prop: string | symbol): boolean;
|
|
|
6
6
|
* Thrown in development when a template reads a property that does not exist
|
|
7
7
|
* on the template context.
|
|
8
8
|
*
|
|
9
|
-
* The suggestions are encoded in the message
|
|
10
|
-
*
|
|
9
|
+
* The suggestions are encoded in the message, which Eta's debug mode copies into
|
|
10
|
+
* the error it rethrows for each template the error passes through.
|
|
11
11
|
* Use {@link parseUnknownPropertyMessage} to read them back.
|
|
12
12
|
*/
|
|
13
13
|
export declare class UnknownTemplatePropertyError extends Error {
|
|
14
14
|
readonly propertyPath: string;
|
|
15
15
|
readonly suggestions: string[];
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* @param hint - Explanation shown instead of the suggestions, for a property that only some
|
|
18
|
+
* templates have
|
|
19
|
+
*/
|
|
20
|
+
constructor(propertyPath: string, suggestions: string[], availableKeys: string[], hint?: string);
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* Reads an {@link UnknownTemplatePropertyError} message back into a short message
|
|
@@ -35,9 +39,9 @@ export declare function parseUnknownPropertyMessage(message: string): {
|
|
|
35
39
|
* instead of silently rendering `undefined`. The error reaches the dev server's
|
|
36
40
|
* error overlay.
|
|
37
41
|
*
|
|
38
|
-
* Validates the top-level `stati.*` keys
|
|
39
|
-
*
|
|
40
|
-
*
|
|
42
|
+
* Validates the top-level `stati.*` keys and one level down for `site`, `nav`,
|
|
43
|
+
* `collection`, `generator` and `helpers`, so a misspelt helper is caught too.
|
|
44
|
+
* Frontmatter under `stati.page` is not validated.
|
|
41
45
|
*
|
|
42
46
|
* Only active in development; other environments get the context back unchanged.
|
|
43
47
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-validation.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/context-validation.utils.ts"],"names":[],"mappings":"AAkBA;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAEpE;
|
|
1
|
+
{"version":3,"file":"context-validation.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/context-validation.utils.ts"],"names":[],"mappings":"AAkBA;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAEpE;AA2CD;;;;;;;GAOG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,SAAgB,YAAY,EAAE,MAAM,CAAC;IACrC,SAAgB,WAAW,EAAE,MAAM,EAAE,CAAC;IAEtC;;;OAGG;gBACS,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM;CAMhG;AAqBD;;;;;;;;;GASG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,GACd;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CAsBxD;AAyBD;;;;;;;;;;;GAWG;AACH,wBAAgB,4BAA4B,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,CAAC,CA4D5E"}
|
|
@@ -34,41 +34,55 @@ const SITE_CONFIG_KEYS = {
|
|
|
34
34
|
/**
|
|
35
35
|
* Context properties whose own keys form a closed set and are validated one level
|
|
36
36
|
* down, with keys that may be legitimately absent at runtime.
|
|
37
|
-
* `page` is left out because frontmatter keys differ from page to page
|
|
38
|
-
* `partials` already has its own validating proxy.
|
|
37
|
+
* `page` is left out because frontmatter keys differ from page to page.
|
|
39
38
|
*/
|
|
40
39
|
const VALIDATED_NESTED_PROPERTIES = {
|
|
41
40
|
site: Object.keys(SITE_CONFIG_KEYS),
|
|
42
41
|
nav: [],
|
|
43
42
|
collection: [],
|
|
44
43
|
generator: [],
|
|
44
|
+
helpers: [],
|
|
45
45
|
};
|
|
46
46
|
/**
|
|
47
47
|
* Context properties searched for an exact match when a key is missing from the
|
|
48
|
-
* root, so that `stati.title` can suggest `stati.page.title` and `stati.
|
|
49
|
-
* can suggest `stati.
|
|
48
|
+
* root, so that `stati.title` can suggest `stati.page.title` and `stati.formatDate`
|
|
49
|
+
* can suggest `stati.helpers.formatDate`.
|
|
50
50
|
*/
|
|
51
|
-
const CROSS_LEVEL_PROPERTIES = ['page', 'site', 'nav', '
|
|
51
|
+
const CROSS_LEVEL_PROPERTIES = ['page', 'site', 'nav', 'helpers'];
|
|
52
|
+
/**
|
|
53
|
+
* Explanations for root properties that exist only in some templates, shown instead of
|
|
54
|
+
* spelling suggestions when a template reads one where it is absent.
|
|
55
|
+
*/
|
|
56
|
+
const ABSENT_PROPERTY_HINTS = {
|
|
57
|
+
body: "stati.body is set only in templates rendered by stati.layout() or stati.include(); the page's Markdown is stati.content",
|
|
58
|
+
};
|
|
52
59
|
/**
|
|
53
60
|
* Thrown in development when a template reads a property that does not exist
|
|
54
61
|
* on the template context.
|
|
55
62
|
*
|
|
56
|
-
* The suggestions are encoded in the message
|
|
57
|
-
*
|
|
63
|
+
* The suggestions are encoded in the message, which Eta's debug mode copies into
|
|
64
|
+
* the error it rethrows for each template the error passes through.
|
|
58
65
|
* Use {@link parseUnknownPropertyMessage} to read them back.
|
|
59
66
|
*/
|
|
60
67
|
export class UnknownTemplatePropertyError extends Error {
|
|
61
68
|
propertyPath;
|
|
62
69
|
suggestions;
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
/**
|
|
71
|
+
* @param hint - Explanation shown instead of the suggestions, for a property that only some
|
|
72
|
+
* templates have
|
|
73
|
+
*/
|
|
74
|
+
constructor(propertyPath, suggestions, availableKeys, hint) {
|
|
75
|
+
super(formatUnknownPropertyMessage(propertyPath, suggestions, availableKeys, hint));
|
|
65
76
|
this.name = 'UnknownTemplatePropertyError';
|
|
66
77
|
this.propertyPath = propertyPath;
|
|
67
|
-
this.suggestions = suggestions;
|
|
78
|
+
this.suggestions = hint ? [hint] : suggestions;
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
|
-
function formatUnknownPropertyMessage(propertyPath, suggestions, availableKeys) {
|
|
81
|
+
function formatUnknownPropertyMessage(propertyPath, suggestions, availableKeys, hint) {
|
|
71
82
|
const base = `Unknown property "${propertyPath}".`;
|
|
83
|
+
if (hint) {
|
|
84
|
+
return `${base} ${hint}.`;
|
|
85
|
+
}
|
|
72
86
|
if (suggestions.length > 0) {
|
|
73
87
|
const quoted = suggestions.map((s) => `"${s}"`);
|
|
74
88
|
const list = quoted.length === 1 ? quoted[0] : `${quoted.slice(0, -1).join(', ')} or ${quoted.at(-1)}`;
|
|
@@ -87,11 +101,11 @@ function formatUnknownPropertyMessage(propertyPath, suggestions, availableKeys)
|
|
|
87
101
|
* ```
|
|
88
102
|
*/
|
|
89
103
|
export function parseUnknownPropertyMessage(message) {
|
|
90
|
-
const match = message.match(/^Unknown property "([^"]+)"\.(?: Did you mean (.+)\?| Available properties: (.*)\.)?$/);
|
|
104
|
+
const match = message.match(/^Unknown property "([^"]+)"\.(?: Did you mean (.+)\?| Available properties: (.*)\.| (.+)\.)?$/);
|
|
91
105
|
if (!match) {
|
|
92
106
|
return undefined;
|
|
93
107
|
}
|
|
94
|
-
const [, propertyPath, didYouMean, available] = match;
|
|
108
|
+
const [, propertyPath, didYouMean, available, hint] = match;
|
|
95
109
|
const suggestions = [];
|
|
96
110
|
if (didYouMean) {
|
|
97
111
|
for (const [, name] of didYouMean.matchAll(/"([^"]+)"/g)) {
|
|
@@ -102,6 +116,9 @@ export function parseUnknownPropertyMessage(message) {
|
|
|
102
116
|
const parentPath = propertyPath.slice(0, propertyPath.lastIndexOf('.'));
|
|
103
117
|
suggestions.push(`Available properties on ${parentPath}: ${available || '(none)'}`);
|
|
104
118
|
}
|
|
119
|
+
else if (hint) {
|
|
120
|
+
suggestions.push(hint);
|
|
121
|
+
}
|
|
105
122
|
return { message: `Unknown property "${propertyPath}"`, suggestions };
|
|
106
123
|
}
|
|
107
124
|
function listKeys(target, extraKeys) {
|
|
@@ -126,9 +143,9 @@ function createValidatingObjectProxy(target, path, extraKeys) {
|
|
|
126
143
|
* instead of silently rendering `undefined`. The error reaches the dev server's
|
|
127
144
|
* error overlay.
|
|
128
145
|
*
|
|
129
|
-
* Validates the top-level `stati.*` keys
|
|
130
|
-
*
|
|
131
|
-
*
|
|
146
|
+
* Validates the top-level `stati.*` keys and one level down for `site`, `nav`,
|
|
147
|
+
* `collection`, `generator` and `helpers`, so a misspelt helper is caught too.
|
|
148
|
+
* Frontmatter under `stati.page` is not validated.
|
|
132
149
|
*
|
|
133
150
|
* Only active in development; other environments get the context back unchanged.
|
|
134
151
|
*/
|
|
@@ -157,6 +174,12 @@ export function createValidatingContextProxy(context) {
|
|
|
157
174
|
}
|
|
158
175
|
const name = String(prop);
|
|
159
176
|
const availableKeys = listKeys(obj, []);
|
|
177
|
+
const hint = Object.hasOwn(ABSENT_PROPERTY_HINTS, name)
|
|
178
|
+
? ABSENT_PROPERTY_HINTS[name]
|
|
179
|
+
: undefined;
|
|
180
|
+
if (hint) {
|
|
181
|
+
throw new UnknownTemplatePropertyError(`stati.${name}`, [], availableKeys, hint);
|
|
182
|
+
}
|
|
160
183
|
// An exact match one level down (stati.title -> stati.page.title) is a
|
|
161
184
|
// stronger signal than a spelling match, so it goes first
|
|
162
185
|
const suggestions = [];
|
|
@@ -12,7 +12,7 @@ export declare const PUBLISHED_DATE_FIELDS: readonly ["publishedAt", "published"
|
|
|
12
12
|
*
|
|
13
13
|
* Checks {@link PUBLISHED_DATE_FIELDS} in order and returns the first valid value.
|
|
14
14
|
* Accepts strings (quoted YAML dates) and `Date` objects (unquoted YAML dates,
|
|
15
|
-
* which
|
|
15
|
+
* which YAML parses into `Date`). Missing, `null` and empty values are skipped;
|
|
16
16
|
* other invalid values are reported through `onInvalid` and skipped, so a bad
|
|
17
17
|
* `publishedAt` does not hide a valid `date`.
|
|
18
18
|
*
|
|
@@ -12,7 +12,7 @@ export const PUBLISHED_DATE_FIELDS = ['publishedAt', 'published', 'date', 'creat
|
|
|
12
12
|
*
|
|
13
13
|
* Checks {@link PUBLISHED_DATE_FIELDS} in order and returns the first valid value.
|
|
14
14
|
* Accepts strings (quoted YAML dates) and `Date` objects (unquoted YAML dates,
|
|
15
|
-
* which
|
|
15
|
+
* which YAML parses into `Date`). Missing, `null` and empty values are skipped;
|
|
16
16
|
* other invalid values are reported through `onInvalid` and skipped, so a bad
|
|
17
17
|
* `publishedAt` does not hide a valid `date`.
|
|
18
18
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-css-watcher.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/dev-css-watcher.utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAGnD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"dev-css-watcher.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/dev-css-watcher.utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAGnD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAc9E;AA0BD,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,eAAe,EAAE,OAAO,CAAC;IACzB,UAAU,EAAE,SAAS,GAAG,IAAI,CAAC;IAC7B,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAgC3B"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chokidar from 'chokidar';
|
|
2
|
-
import
|
|
2
|
+
import { glob } from 'tinyglobby';
|
|
3
3
|
import { pathExists } from './fs.utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Returns concrete CSS files currently present in outDir.
|
|
@@ -11,11 +11,12 @@ export async function discoverOutputCssFiles(outDir) {
|
|
|
11
11
|
if (!(await pathExists(outDir))) {
|
|
12
12
|
return [];
|
|
13
13
|
}
|
|
14
|
-
const cssFiles = await
|
|
14
|
+
const cssFiles = await glob('**/*.css', {
|
|
15
15
|
cwd: outDir,
|
|
16
16
|
absolute: true,
|
|
17
17
|
onlyFiles: true,
|
|
18
18
|
dot: false,
|
|
19
|
+
expandDirectories: false,
|
|
19
20
|
});
|
|
20
21
|
return cssFiles;
|
|
21
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-overlay.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/error-overlay.utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"error-overlay.utils.d.ts","sourceRoot":"","sources":["../../../src/core/utils/error-overlay.utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;KAClB,CAAC;IACF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AA6pBD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAiJnF;AAsFD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,EAC1B,QAAQ,CAAC,EAAE,MAAM,GAChB,YAAY,CAkCd"}
|