@stati/core 1.29.2 → 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 -11
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +115 -196
- 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, relative
|
|
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,60 +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), with `/` or `\` separators
|
|
54
|
-
* @param config - Stati configuration
|
|
55
|
-
* @returns Object mapping partial names to their file paths
|
|
56
|
-
*/
|
|
57
|
-
export async function discoverPartials(pagePath, config) {
|
|
58
|
-
const srcDir = resolveSrcDir(config);
|
|
59
|
-
const partials = {};
|
|
60
|
-
// Get the directory of the current page. Windows paths use backslashes, so convert them
|
|
61
|
-
// first, or the folders between the root and the page's own folder are skipped.
|
|
62
|
-
const pageDir = posix.dirname(pagePath.replace(/\\/g, '/'));
|
|
63
|
-
const pathSegments = pageDir === '.' ? [] : pageDir.split('/');
|
|
64
|
-
// Scan from root to current directory (least specific first)
|
|
65
|
-
// This allows more specific partials to override less specific ones
|
|
66
|
-
const dirsToScan = [];
|
|
67
|
-
if (pathSegments.length > 0) {
|
|
68
|
-
// Add root directory first, then parent directories, then current
|
|
69
|
-
dirsToScan.push(''); // Root directory first
|
|
70
|
-
// Add directories from root to current
|
|
71
|
-
for (let i = 1; i <= pathSegments.length; i++) {
|
|
72
|
-
dirsToScan.push(pathSegments.slice(0, i).join('/'));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
dirsToScan.push(''); // Root directory only
|
|
77
|
-
}
|
|
78
|
-
for (const dir of dirsToScan) {
|
|
79
|
-
const searchDir = dir ? join(srcDir, dir) : srcDir;
|
|
80
|
-
// Find all underscore folders in this directory level
|
|
81
|
-
const globPattern = join(searchDir, '_*/').replace(/\\/g, '/');
|
|
82
|
-
const underscoreFolders = await glob(globPattern, {
|
|
83
|
-
absolute: true,
|
|
84
|
-
onlyDirectories: true,
|
|
85
|
-
});
|
|
86
|
-
// Scan each underscore folder for .eta files
|
|
87
|
-
for (const folderPath of underscoreFolders) {
|
|
88
|
-
const etaFiles = await glob('**/*.eta', {
|
|
89
|
-
cwd: folderPath,
|
|
90
|
-
absolute: false,
|
|
91
|
-
});
|
|
92
|
-
for (const etaFile of etaFiles) {
|
|
93
|
-
const partialName = basename(etaFile, TEMPLATE_EXTENSION);
|
|
94
|
-
const fullPath = join(folderPath, etaFile);
|
|
95
|
-
// Get relative path from srcDir to the partial file
|
|
96
|
-
const relativePath = relative(srcDir, fullPath);
|
|
97
|
-
partials[partialName] = posix.normalize(relativePath);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return partials;
|
|
102
|
-
}
|
|
103
49
|
/**
|
|
104
50
|
* Cached template engine for dev mode to avoid re-creating on each rebuild.
|
|
105
51
|
*
|
|
@@ -118,6 +64,12 @@ let cachedTemplateEngine = null;
|
|
|
118
64
|
export function clearTemplateEngineCache() {
|
|
119
65
|
cachedTemplateEngine = null;
|
|
120
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
|
+
*/
|
|
121
73
|
export function createTemplateEngine(config) {
|
|
122
74
|
const templateDir = resolveSrcDir(config);
|
|
123
75
|
// In dev mode, reuse a single cached engine across rebuilds to avoid
|
|
@@ -125,8 +77,8 @@ export function createTemplateEngine(config) {
|
|
|
125
77
|
if (isDevelopment() && cachedTemplateEngine && cachedTemplateEngine.templateDir === templateDir) {
|
|
126
78
|
return cachedTemplateEngine.eta;
|
|
127
79
|
}
|
|
128
|
-
// Enable the template cache in dev too so
|
|
129
|
-
//
|
|
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().
|
|
130
82
|
const useCache = isDevelopment() || isProduction();
|
|
131
83
|
const eta = new Eta({
|
|
132
84
|
views: templateDir,
|
|
@@ -136,33 +88,40 @@ export function createTemplateEngine(config) {
|
|
|
136
88
|
// Debug mode wraps runtime errors with the template path and line number,
|
|
137
89
|
// which the dev error overlay uses to point at the failing line
|
|
138
90
|
debug: isDevelopment(),
|
|
91
|
+
plugins: [createStatiEtaPlugin()],
|
|
139
92
|
});
|
|
93
|
+
installEtaPlugin(eta);
|
|
140
94
|
if (isDevelopment()) {
|
|
141
95
|
cachedTemplateEngine = { templateDir, eta };
|
|
142
96
|
}
|
|
143
97
|
return eta;
|
|
144
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
|
+
*/
|
|
145
105
|
export async function renderPage(page, body, config, eta, navigation, allPages, assets, toc, logger, extras) {
|
|
146
106
|
const log = logger || createFallbackLogger();
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
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.
|
|
150
115
|
const ogState = {
|
|
151
116
|
image: undefined,
|
|
152
117
|
pending: pageImages?.wantsOg() ?? false,
|
|
153
118
|
};
|
|
154
|
-
// Discover partials for this page's directory hierarchy
|
|
155
|
-
const srcDir = resolveSrcDir(config);
|
|
156
|
-
const relativePath = relative(srcDir, page.sourcePath);
|
|
157
|
-
const partialPaths = await discoverPartials(relativePath, config);
|
|
158
|
-
// Count templates: partials + layout (if found)
|
|
159
|
-
const partialsCount = Object.keys(partialPaths).length;
|
|
160
119
|
// What this render reads from shared data, so the cache can rebuild the page when it changes
|
|
161
120
|
const reads = { collection: false, nav: false };
|
|
162
121
|
// The page's collection: its own on index pages, the one it belongs to on other pages, or
|
|
163
122
|
// the one its front matter names. Collections are computed once per page list.
|
|
164
123
|
let collectionData;
|
|
165
|
-
const isIndexPage = allPages && isCollectionIndexPage(page, allPages);
|
|
124
|
+
const isIndexPage = allPages !== undefined && isCollectionIndexPage(page, allPages);
|
|
166
125
|
if (allPages) {
|
|
167
126
|
const collectionPath = resolveCollectionPath(page, allPages);
|
|
168
127
|
const data = collectionPath !== undefined
|
|
@@ -172,11 +131,13 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
|
|
|
172
131
|
collectionData = createCollectionView(data, reads);
|
|
173
132
|
}
|
|
174
133
|
}
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
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);
|
|
180
141
|
// Create navigation helpers
|
|
181
142
|
const navTree = navigation || [];
|
|
182
143
|
const navHelpers = createNavigationHelpers(navTree, page);
|
|
@@ -204,24 +165,22 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
|
|
|
204
165
|
return currentNavNode;
|
|
205
166
|
},
|
|
206
167
|
});
|
|
207
|
-
//
|
|
208
|
-
|
|
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 = {
|
|
209
171
|
site: config.site,
|
|
210
172
|
page: pageContext,
|
|
211
173
|
content: body,
|
|
212
|
-
nav: createNavView(navHelpers, reads),
|
|
213
|
-
collection: collectionData,
|
|
214
|
-
// Set on paginated listings and on pages generated from [param].md templates
|
|
215
|
-
// 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
|
|
216
177
|
pagination: page.pagination ? copyPaginationData(page.pagination) : undefined,
|
|
217
178
|
generated: page.generated ? copyGeneratedData(page.generated) : undefined,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
...(config.eta?.filters || {}),
|
|
179
|
+
// A copy, so a template that replaces a helper does not change it for later pages
|
|
180
|
+
helpers: { ...config.helpers },
|
|
221
181
|
generator: {
|
|
222
182
|
version: getStatiVersion(),
|
|
223
183
|
},
|
|
224
|
-
// Stati utilities object with helper functions
|
|
225
184
|
generateSEO: (tags) => generateSEO({
|
|
226
185
|
page,
|
|
227
186
|
config,
|
|
@@ -230,141 +189,101 @@ export async function renderPage(page, body, config, eta, navigation, allPages,
|
|
|
230
189
|
deferGeneratedImage: ogState.pending,
|
|
231
190
|
}, tags),
|
|
232
191
|
propValue,
|
|
192
|
+
slugify,
|
|
233
193
|
// Image with dimensions, lazy loading and, with images.responsive, srcset variants
|
|
234
194
|
image: (src, options) => pageImages ? pageImages.renderHelperImage(src, options) : renderPlainImage(src, options),
|
|
235
195
|
assets, // TypeScript bundle assets (available when typescript.enabled is true)
|
|
236
|
-
// Set by callable partials invoked with props; always present so that
|
|
237
|
-
// stati.props?.x works under the dev-mode context validation
|
|
238
|
-
props: undefined,
|
|
239
196
|
// Set while the Open Graph template renders: { width, height }
|
|
240
197
|
og: undefined,
|
|
241
198
|
// The page's generated Open Graph image, once rendered: { url, path, width, height, alt }
|
|
242
199
|
ogImage: undefined,
|
|
243
200
|
};
|
|
244
|
-
// Render
|
|
245
|
-
// Use multiple passes to allow partials to reference other partials
|
|
246
|
-
const renderedPartials = {};
|
|
247
|
-
const maxPasses = 3; // Prevent infinite loops
|
|
248
|
-
for (let pass = 0; pass < maxPasses; pass++) {
|
|
249
|
-
// Each pass renders all partials to ensure they have access to all previously rendered ones
|
|
250
|
-
// TODO: Optimize by tracking which partials changed or have dependencies
|
|
251
|
-
const partialsToRender = { ...partialPaths };
|
|
252
|
-
if (Object.keys(partialsToRender).length === 0) {
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
let progressMade = false;
|
|
256
|
-
const passRenderedPartials = {};
|
|
257
|
-
for (const [partialName, partialPath] of Object.entries(partialsToRender)) {
|
|
258
|
-
try {
|
|
259
|
-
// Create context with all previously rendered partials available
|
|
260
|
-
const combinedPartials = { ...renderedPartials, ...passRenderedPartials };
|
|
261
|
-
// Wrap partials as callable before passing to validation proxy
|
|
262
|
-
const callablePartials = wrapPartialsAsCallable(eta, combinedPartials, partialPaths, baseContext);
|
|
263
|
-
const partialContext = createValidatingContextProxy({
|
|
264
|
-
...baseContext,
|
|
265
|
-
partials: createValidatingPartialsProxy(callablePartials), // Include both previous and current pass partials with validation
|
|
266
|
-
});
|
|
267
|
-
const renderedContent = await eta.renderAsync(partialPath, partialContext);
|
|
268
|
-
passRenderedPartials[partialName] = renderedContent;
|
|
269
|
-
progressMade = true;
|
|
270
|
-
}
|
|
271
|
-
catch (error) {
|
|
272
|
-
// If this is the last pass, log the error and create placeholder
|
|
273
|
-
if (pass === maxPasses - 1) {
|
|
274
|
-
log.warning(`Failed to render partial ${partialName} at ${partialPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
275
|
-
// In development mode, throw enhanced template error for partials too
|
|
276
|
-
if (isDevelopment()) {
|
|
277
|
-
const templateError = createTemplateError(error instanceof Error ? error : new Error(String(error)), partialPath);
|
|
278
|
-
throw templateError;
|
|
279
|
-
}
|
|
280
|
-
passRenderedPartials[partialName] = `<!-- Error rendering partial: ${partialName} -->`;
|
|
281
|
-
progressMade = true;
|
|
282
|
-
}
|
|
283
|
-
// Otherwise, use existing content if available, or skip for retry
|
|
284
|
-
else if (renderedPartials[partialName]) {
|
|
285
|
-
passRenderedPartials[partialName] = renderedPartials[partialName];
|
|
286
|
-
}
|
|
287
|
-
// For failed partials on non-last pass, still count as progress to allow retries
|
|
288
|
-
else {
|
|
289
|
-
progressMade = true;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
// Update the rendered partials with this pass's results
|
|
294
|
-
Object.assign(renderedPartials, passRenderedPartials);
|
|
295
|
-
// If no progress was made, break to avoid infinite loop
|
|
296
|
-
if (!progressMade) {
|
|
297
|
-
break;
|
|
298
|
-
}
|
|
299
|
-
// If this is pass 0, always do at least one more pass to allow interdependencies
|
|
300
|
-
if (pass === 0) {
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
// For subsequent passes, only continue if we're not at max passes yet
|
|
304
|
-
if (pass >= maxPasses - 1) {
|
|
305
|
-
break;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// 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
|
|
309
202
|
// generated image is known before the layout renders its SEO tags
|
|
310
203
|
if (pageImages && ogState.pending) {
|
|
311
204
|
// Copy the page's property descriptors so that navNode stays a getter: spreading would
|
|
312
205
|
// read it and count every page with a card as reading the navigation
|
|
313
|
-
const ogPage = Object.defineProperties({}, Object.getOwnPropertyDescriptors(
|
|
206
|
+
const ogPage = Object.defineProperties({}, Object.getOwnPropertyDescriptors(pageContext));
|
|
314
207
|
ogPage.content = '';
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
content: '',
|
|
318
|
-
page: ogPage,
|
|
319
|
-
og: pageImages.ogSize(),
|
|
208
|
+
const ogBase = {
|
|
209
|
+
id: 'og',
|
|
210
|
+
values: { ...values, content: '', page: ogPage, og: pageImages.ogSize() },
|
|
320
211
|
};
|
|
321
|
-
|
|
322
|
-
...ogBaseContext,
|
|
323
|
-
partials: createValidatingPartialsProxy(wrapPartialsAsCallable(eta, renderedPartials, partialPaths, ogBaseContext)),
|
|
324
|
-
});
|
|
325
|
-
ogState.image = (await pageImages.renderOg(ogContext, eta)) ?? undefined;
|
|
212
|
+
ogState.image = (await pageImages.renderOg(createOgRenderer(scope, ogBase))) ?? undefined;
|
|
326
213
|
ogState.pending = false;
|
|
327
|
-
|
|
214
|
+
values.ogImage = ogState.image;
|
|
328
215
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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()],
|
|
334
225
|
});
|
|
226
|
+
if (!layout.file) {
|
|
227
|
+
log.warning('No layout template found, using fallback');
|
|
228
|
+
return result(createFallbackHtml(page, body));
|
|
229
|
+
}
|
|
335
230
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return {
|
|
339
|
-
html: createFallbackHtml(page, body),
|
|
340
|
-
templatesLoaded,
|
|
341
|
-
ogImage: ogState.image,
|
|
342
|
-
reads,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
const html = await eta.renderAsync(layoutPath, context);
|
|
346
|
-
return {
|
|
347
|
-
html: resolveGeneratedImagePlaceholders(html, ogState.image),
|
|
348
|
-
templatesLoaded,
|
|
349
|
-
ogImage: ogState.image,
|
|
350
|
-
reads,
|
|
351
|
-
};
|
|
231
|
+
const html = await scope.renderAsync({ id: 'page', values }, { file: layout.file });
|
|
232
|
+
return result(resolveGeneratedImagePlaceholders(html, ogState.image));
|
|
352
233
|
}
|
|
353
234
|
catch (error) {
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
356
238
|
if (isDevelopment()) {
|
|
357
|
-
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}`);
|
|
358
243
|
throw templateError;
|
|
359
244
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
templatesLoaded,
|
|
363
|
-
ogImage: ogState.image,
|
|
364
|
-
reads,
|
|
365
|
-
};
|
|
245
|
+
log.error(`Error rendering ${scope.display(failed)} for ${page.url}: ${failure.message}`);
|
|
246
|
+
return result(createFallbackHtml(page, body));
|
|
366
247
|
}
|
|
367
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
|
+
}
|
|
368
287
|
function createFallbackHtml(page, body) {
|
|
369
288
|
const title = page.frontMatter.title || 'Untitled';
|
|
370
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"}
|