@stati/core 1.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/dist/config/loader.d.ts +23 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +73 -0
- package/dist/core/build.d.ts +51 -0
- package/dist/core/build.d.ts.map +1 -0
- package/dist/core/build.js +191 -0
- package/dist/core/content.d.ts +19 -0
- package/dist/core/content.d.ts.map +1 -0
- package/dist/core/content.js +68 -0
- package/dist/core/invalidate.d.ts +2 -0
- package/dist/core/invalidate.d.ts.map +1 -0
- package/dist/core/invalidate.js +7 -0
- package/dist/core/markdown.d.ts +9 -0
- package/dist/core/markdown.d.ts.map +1 -0
- package/dist/core/markdown.js +48 -0
- package/dist/core/navigation.d.ts +21 -0
- package/dist/core/navigation.d.ts.map +1 -0
- package/dist/core/navigation.js +181 -0
- package/dist/core/templates.d.ts +5 -0
- package/dist/core/templates.d.ts.map +1 -0
- package/dist/core/templates.js +307 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/types.d.ts +371 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a hierarchical navigation structure from pages.
|
|
4
|
+
* Groups pages by directory structure and sorts them appropriately.
|
|
5
|
+
*
|
|
6
|
+
* @param pages - Array of page models to build navigation from
|
|
7
|
+
* @returns Array of top-level navigation nodes
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const pages = [
|
|
12
|
+
* { url: '/blog/post-1', frontMatter: { title: 'Post 1', order: 2 } },
|
|
13
|
+
* { url: '/blog/post-2', frontMatter: { title: 'Post 2', order: 1 } },
|
|
14
|
+
* { url: '/about', frontMatter: { title: 'About' } }
|
|
15
|
+
* ];
|
|
16
|
+
* const nav = buildNavigation(pages);
|
|
17
|
+
* // Results in hierarchical structure with sorted blog posts
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function buildNavigation(pages) {
|
|
21
|
+
// Group pages by their collection (directory)
|
|
22
|
+
const collections = new Map();
|
|
23
|
+
for (const page of pages) {
|
|
24
|
+
const collectionPath = getCollectionPath(page.url);
|
|
25
|
+
if (!collections.has(collectionPath)) {
|
|
26
|
+
collections.set(collectionPath, []);
|
|
27
|
+
}
|
|
28
|
+
collections.get(collectionPath).push(page);
|
|
29
|
+
}
|
|
30
|
+
const navNodes = [];
|
|
31
|
+
// Process root-level pages
|
|
32
|
+
if (collections.has('/')) {
|
|
33
|
+
const rootPages = collections.get('/').filter((page) => {
|
|
34
|
+
// Don't include pages that are index pages for other collections
|
|
35
|
+
const potentialCollectionPath = page.url;
|
|
36
|
+
return !collections.has(potentialCollectionPath) || page.url === '/';
|
|
37
|
+
});
|
|
38
|
+
for (const page of rootPages) {
|
|
39
|
+
navNodes.push(createNavNodeFromPage(page));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Process collection directories
|
|
43
|
+
for (const [collectionPath, collectionPages] of collections) {
|
|
44
|
+
if (collectionPath !== '/') {
|
|
45
|
+
// Find the index page for this collection (if it exists)
|
|
46
|
+
const indexPage = pages.find((p) => p.url === collectionPath);
|
|
47
|
+
// Create collection node
|
|
48
|
+
const collectionNode = createCollectionNode(collectionPath, collectionPages, indexPage);
|
|
49
|
+
navNodes.push(collectionNode);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return sortNavigationNodes(navNodes);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Determines which collection a page belongs to based on its URL
|
|
56
|
+
*/
|
|
57
|
+
function getCollectionPath(url) {
|
|
58
|
+
const pathParts = url.split('/').filter(Boolean);
|
|
59
|
+
if (pathParts.length <= 1) {
|
|
60
|
+
// Root level: / or /about
|
|
61
|
+
return '/';
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Collection level: /blog/post-1 -> belongs to /blog
|
|
65
|
+
return '/' + pathParts.slice(0, -1).join('/');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Sorts pages within a single collection.
|
|
70
|
+
* Primary sort: order field (ascending)
|
|
71
|
+
* Secondary sort: publishedAt date (descending, newest first)
|
|
72
|
+
* Tertiary sort: title (ascending)
|
|
73
|
+
*
|
|
74
|
+
* @param pages - Pages to sort
|
|
75
|
+
* @returns Sorted array of pages
|
|
76
|
+
*/
|
|
77
|
+
function sortPagesInCollection(pages) {
|
|
78
|
+
return pages.sort((a, b) => {
|
|
79
|
+
// Primary sort by order field
|
|
80
|
+
const orderA = typeof a.frontMatter.order === 'number' ? a.frontMatter.order : Number.MAX_SAFE_INTEGER;
|
|
81
|
+
const orderB = typeof b.frontMatter.order === 'number' ? b.frontMatter.order : Number.MAX_SAFE_INTEGER;
|
|
82
|
+
if (orderA !== orderB) {
|
|
83
|
+
return orderA - orderB;
|
|
84
|
+
}
|
|
85
|
+
// Secondary sort by publishedAt (newest first)
|
|
86
|
+
if (a.publishedAt && b.publishedAt) {
|
|
87
|
+
return b.publishedAt.getTime() - a.publishedAt.getTime();
|
|
88
|
+
}
|
|
89
|
+
if (a.publishedAt && !b.publishedAt)
|
|
90
|
+
return -1;
|
|
91
|
+
if (!a.publishedAt && b.publishedAt)
|
|
92
|
+
return 1;
|
|
93
|
+
// Tertiary sort by title
|
|
94
|
+
const titleA = a.frontMatter.title || basename(a.url);
|
|
95
|
+
const titleB = b.frontMatter.title || basename(b.url);
|
|
96
|
+
return titleA.localeCompare(titleB);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Sorts navigation nodes by order, then by title
|
|
101
|
+
*/
|
|
102
|
+
function sortNavigationNodes(nodes) {
|
|
103
|
+
return nodes.sort((a, b) => {
|
|
104
|
+
// Primary sort by order
|
|
105
|
+
const orderA = typeof a.order === 'number' ? a.order : Number.MAX_SAFE_INTEGER;
|
|
106
|
+
const orderB = typeof b.order === 'number' ? b.order : Number.MAX_SAFE_INTEGER;
|
|
107
|
+
if (orderA !== orderB) {
|
|
108
|
+
return orderA - orderB;
|
|
109
|
+
}
|
|
110
|
+
// Secondary sort by title
|
|
111
|
+
return a.title.localeCompare(b.title);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Creates a navigation node from a single page.
|
|
116
|
+
*
|
|
117
|
+
* @param page - Page model to convert
|
|
118
|
+
* @returns Navigation node
|
|
119
|
+
*/
|
|
120
|
+
function createNavNodeFromPage(page) {
|
|
121
|
+
const navNode = {
|
|
122
|
+
title: page.frontMatter.title || basename(page.url) || 'Untitled',
|
|
123
|
+
url: page.url,
|
|
124
|
+
path: page.url,
|
|
125
|
+
isCollection: false,
|
|
126
|
+
};
|
|
127
|
+
// Only add order if it's a number
|
|
128
|
+
if (typeof page.frontMatter.order === 'number') {
|
|
129
|
+
navNode.order = page.frontMatter.order;
|
|
130
|
+
}
|
|
131
|
+
// Only add publishedAt if it exists
|
|
132
|
+
if (page.publishedAt) {
|
|
133
|
+
navNode.publishedAt = page.publishedAt;
|
|
134
|
+
}
|
|
135
|
+
return navNode;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Creates a collection navigation node with child pages.
|
|
139
|
+
*
|
|
140
|
+
* @param collectionPath - Path of the collection
|
|
141
|
+
* @param pages - Pages in the collection
|
|
142
|
+
* @param indexPage - Optional index page for the collection
|
|
143
|
+
* @returns Navigation node representing the collection
|
|
144
|
+
*/
|
|
145
|
+
function createCollectionNode(collectionPath, pages, indexPage) {
|
|
146
|
+
const collectionName = basename(collectionPath);
|
|
147
|
+
// Use index page data if available, otherwise derive from collection
|
|
148
|
+
const title = indexPage?.frontMatter.title || capitalizeFirst(collectionName);
|
|
149
|
+
const url = indexPage?.url || collectionPath;
|
|
150
|
+
const order = indexPage?.frontMatter.order;
|
|
151
|
+
const publishedAt = indexPage?.publishedAt;
|
|
152
|
+
const children = sortPagesInCollection(pages).map((page) => createNavNodeFromPage(page));
|
|
153
|
+
const navNode = {
|
|
154
|
+
title,
|
|
155
|
+
url,
|
|
156
|
+
path: collectionPath,
|
|
157
|
+
isCollection: true,
|
|
158
|
+
};
|
|
159
|
+
// Only add order if it's a number
|
|
160
|
+
if (typeof order === 'number') {
|
|
161
|
+
navNode.order = order;
|
|
162
|
+
}
|
|
163
|
+
// Only add publishedAt if it exists
|
|
164
|
+
if (publishedAt) {
|
|
165
|
+
navNode.publishedAt = publishedAt;
|
|
166
|
+
}
|
|
167
|
+
// Only add children if there are any
|
|
168
|
+
if (children.length > 0) {
|
|
169
|
+
navNode.children = children;
|
|
170
|
+
}
|
|
171
|
+
return navNode;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Capitalizes the first letter of a string.
|
|
175
|
+
*
|
|
176
|
+
* @param str - String to capitalize
|
|
177
|
+
* @returns Capitalized string
|
|
178
|
+
*/
|
|
179
|
+
function capitalizeFirst(str) {
|
|
180
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
181
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Eta } from 'eta';
|
|
2
|
+
import type { StatiConfig, PageModel, NavNode } from '../types.js';
|
|
3
|
+
export declare function createTemplateEngine(config: StatiConfig): Eta;
|
|
4
|
+
export declare function renderPage(page: PageModel, body: string, config: StatiConfig, eta: Eta, navigation?: NavNode[], allPages?: PageModel[]): Promise<string>;
|
|
5
|
+
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/core/templates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAK1B,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAkB,MAAM,aAAa,CAAC;AAgRnF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,GAAG,CAS7D;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,EACf,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,GAAG,EAAE,GAAG,EACR,UAAU,CAAC,EAAE,OAAO,EAAE,EACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,GACrB,OAAO,CAAC,MAAM,CAAC,CAgDjB"}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { Eta } from 'eta';
|
|
2
|
+
import { join, dirname, relative, basename } from 'path';
|
|
3
|
+
import fse from 'fs-extra';
|
|
4
|
+
const { pathExists } = fse;
|
|
5
|
+
import glob from 'fast-glob';
|
|
6
|
+
/**
|
|
7
|
+
* Determines if the given page is an index page for a collection.
|
|
8
|
+
* An index page is one whose URL matches a directory path that contains other pages.
|
|
9
|
+
*
|
|
10
|
+
* @param page - The page to check
|
|
11
|
+
* @param allPages - All pages in the site
|
|
12
|
+
* @returns True if the page is a collection index page
|
|
13
|
+
*/
|
|
14
|
+
function isCollectionIndexPage(page, allPages) {
|
|
15
|
+
// Root index page is always a collection index
|
|
16
|
+
if (page.url === '/') {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
// Check if this page's URL is a directory path that contains other pages
|
|
20
|
+
const pageUrlAsDir = page.url.endsWith('/') ? page.url : page.url + '/';
|
|
21
|
+
return allPages.some((otherPage) => otherPage.url !== page.url && otherPage.url.startsWith(pageUrlAsDir));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Gets the collection path for a given page URL.
|
|
25
|
+
* For index pages, returns the page's URL. For child pages, returns the parent directory.
|
|
26
|
+
*
|
|
27
|
+
* @param pageUrl - The page URL
|
|
28
|
+
* @returns The collection path
|
|
29
|
+
*/
|
|
30
|
+
function getCollectionPathForPage(pageUrl) {
|
|
31
|
+
if (pageUrl === '/') {
|
|
32
|
+
return '/';
|
|
33
|
+
}
|
|
34
|
+
const pathParts = pageUrl.split('/').filter(Boolean);
|
|
35
|
+
if (pathParts.length <= 1) {
|
|
36
|
+
return '/';
|
|
37
|
+
}
|
|
38
|
+
return '/' + pathParts.slice(0, -1).join('/');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Groups pages by their tags for aggregation purposes.
|
|
42
|
+
*
|
|
43
|
+
* @param pages - Pages to group
|
|
44
|
+
* @returns Object mapping tag names to arrays of pages
|
|
45
|
+
*/
|
|
46
|
+
function groupPagesByTags(pages) {
|
|
47
|
+
const pagesByTag = {};
|
|
48
|
+
for (const page of pages) {
|
|
49
|
+
const tags = page.frontMatter.tags;
|
|
50
|
+
if (Array.isArray(tags)) {
|
|
51
|
+
for (const tag of tags) {
|
|
52
|
+
if (typeof tag === 'string') {
|
|
53
|
+
if (!pagesByTag[tag]) {
|
|
54
|
+
pagesByTag[tag] = [];
|
|
55
|
+
}
|
|
56
|
+
pagesByTag[tag].push(page);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return pagesByTag;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sorts pages by publishedAt date (most recent first), falling back to alphabetical by title.
|
|
65
|
+
*
|
|
66
|
+
* @param pages - Pages to sort
|
|
67
|
+
* @returns Sorted array of pages
|
|
68
|
+
*/
|
|
69
|
+
function sortPagesByDate(pages) {
|
|
70
|
+
return pages.sort((a, b) => {
|
|
71
|
+
// Sort by publishedAt (newest first)
|
|
72
|
+
if (a.publishedAt && b.publishedAt) {
|
|
73
|
+
return b.publishedAt.getTime() - a.publishedAt.getTime();
|
|
74
|
+
}
|
|
75
|
+
if (a.publishedAt && !b.publishedAt)
|
|
76
|
+
return -1;
|
|
77
|
+
if (!a.publishedAt && b.publishedAt)
|
|
78
|
+
return 1;
|
|
79
|
+
// Fallback to title
|
|
80
|
+
const titleA = a.frontMatter.title || basename(a.url) || 'Untitled';
|
|
81
|
+
const titleB = b.frontMatter.title || basename(b.url) || 'Untitled';
|
|
82
|
+
return titleA.localeCompare(titleB);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Builds collection aggregation data for index pages.
|
|
87
|
+
* Provides all the data needed for index pages to list and organize their collection's content.
|
|
88
|
+
*
|
|
89
|
+
* @param currentPage - The current page being rendered (must be an index page)
|
|
90
|
+
* @param allPages - All pages in the site
|
|
91
|
+
* @returns Collection data for template rendering
|
|
92
|
+
*/
|
|
93
|
+
function buildCollectionData(currentPage, allPages) {
|
|
94
|
+
const collectionPath = currentPage.url === '/' ? '/' : currentPage.url;
|
|
95
|
+
const collectionName = collectionPath === '/' ? 'Home' : basename(collectionPath);
|
|
96
|
+
// Find all pages that belong to this collection
|
|
97
|
+
const collectionPages = allPages.filter((page) => {
|
|
98
|
+
if (page.url === currentPage.url) {
|
|
99
|
+
return false; // Don't include the index page itself
|
|
100
|
+
}
|
|
101
|
+
if (collectionPath === '/') {
|
|
102
|
+
// For root collection, include pages that are direct children of root
|
|
103
|
+
const pageCollectionPath = getCollectionPathForPage(page.url);
|
|
104
|
+
return pageCollectionPath === '/';
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// For other collections, include pages that start with the collection path
|
|
108
|
+
const pageUrlAsDir = collectionPath.endsWith('/') ? collectionPath : collectionPath + '/';
|
|
109
|
+
return page.url.startsWith(pageUrlAsDir);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
// Find direct children (one level down)
|
|
113
|
+
const children = collectionPages.filter((page) => {
|
|
114
|
+
const relativePath = page.url.substring(collectionPath.length);
|
|
115
|
+
const cleanPath = relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
|
|
116
|
+
return !cleanPath.includes('/'); // No further slashes means direct child
|
|
117
|
+
});
|
|
118
|
+
// Sort pages by date for recent posts
|
|
119
|
+
const recentPages = sortPagesByDate([...collectionPages]);
|
|
120
|
+
// Group by tags
|
|
121
|
+
const pagesByTag = groupPagesByTags(collectionPages);
|
|
122
|
+
return {
|
|
123
|
+
pages: collectionPages,
|
|
124
|
+
children,
|
|
125
|
+
recentPages,
|
|
126
|
+
pagesByTag,
|
|
127
|
+
metadata: {
|
|
128
|
+
totalPages: collectionPages.length,
|
|
129
|
+
hasChildren: children.length > 0,
|
|
130
|
+
collectionPath,
|
|
131
|
+
collectionName,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Discovers partials in the hierarchy for a given page path.
|
|
137
|
+
* Scans all parent directories for folders starting with underscore.
|
|
138
|
+
*
|
|
139
|
+
* @param pagePath - The path to the page (relative to srcDir)
|
|
140
|
+
* @param config - Stati configuration
|
|
141
|
+
* @returns Object mapping partial names to their file paths
|
|
142
|
+
*/
|
|
143
|
+
async function discoverPartials(pagePath, config) {
|
|
144
|
+
const srcDir = join(process.cwd(), config.srcDir);
|
|
145
|
+
const partials = {};
|
|
146
|
+
// Get the directory of the current page
|
|
147
|
+
const pageDir = dirname(pagePath);
|
|
148
|
+
const pathSegments = pageDir === '.' ? [] : pageDir.split('/');
|
|
149
|
+
// Scan from root to current directory
|
|
150
|
+
const dirsToScan = [''];
|
|
151
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
|
152
|
+
dirsToScan.push(pathSegments.slice(0, i + 1).join('/'));
|
|
153
|
+
}
|
|
154
|
+
for (const dir of dirsToScan) {
|
|
155
|
+
const searchDir = dir ? join(srcDir, dir) : srcDir;
|
|
156
|
+
// Find all underscore folders in this directory level
|
|
157
|
+
const underscoreFolders = await glob('_*/', {
|
|
158
|
+
cwd: searchDir,
|
|
159
|
+
onlyDirectories: true,
|
|
160
|
+
});
|
|
161
|
+
// Scan each underscore folder for .eta files
|
|
162
|
+
for (const folder of underscoreFolders) {
|
|
163
|
+
const folderPath = join(searchDir, folder);
|
|
164
|
+
const etaFiles = await glob('**/*.eta', {
|
|
165
|
+
cwd: folderPath,
|
|
166
|
+
absolute: false,
|
|
167
|
+
});
|
|
168
|
+
for (const etaFile of etaFiles) {
|
|
169
|
+
const partialName = basename(etaFile, '.eta');
|
|
170
|
+
const fullPath = join(folderPath, etaFile);
|
|
171
|
+
const relativePath = relative(srcDir, fullPath);
|
|
172
|
+
// Store the relative path from srcDir for Eta to find it
|
|
173
|
+
partials[partialName] = relativePath;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return partials;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Discovers the appropriate layout file for a given page path.
|
|
181
|
+
* Implements the hierarchical layout.eta convention by searching
|
|
182
|
+
* from the page's directory up to the root.
|
|
183
|
+
*
|
|
184
|
+
* @param pagePath - The path to the page (relative to srcDir)
|
|
185
|
+
* @param config - Stati configuration
|
|
186
|
+
* @param explicitLayout - Layout specified in front matter (takes precedence)
|
|
187
|
+
* @param isIndexPage - Whether this is an aggregation/index page (enables index.eta lookup)
|
|
188
|
+
* @returns The layout file path or null if none found
|
|
189
|
+
*/
|
|
190
|
+
async function discoverLayout(pagePath, config, explicitLayout, isIndexPage) {
|
|
191
|
+
const srcDir = join(process.cwd(), config.srcDir);
|
|
192
|
+
// If explicit layout is specified, use it
|
|
193
|
+
if (explicitLayout) {
|
|
194
|
+
const layoutPath = join(srcDir, `${explicitLayout}.eta`);
|
|
195
|
+
if (await pathExists(layoutPath)) {
|
|
196
|
+
return `${explicitLayout}.eta`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Get the directory of the current page
|
|
200
|
+
const pageDir = dirname(pagePath);
|
|
201
|
+
const pathSegments = pageDir === '.' ? [] : pageDir.split(/[/\\]/); // Handle both separators
|
|
202
|
+
// Search for layout.eta from current directory up to root
|
|
203
|
+
const dirsToSearch = [];
|
|
204
|
+
// Add current directory if not root
|
|
205
|
+
if (pathSegments.length > 0) {
|
|
206
|
+
for (let i = pathSegments.length; i > 0; i--) {
|
|
207
|
+
dirsToSearch.push(pathSegments.slice(0, i).join('/'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Add root directory
|
|
211
|
+
dirsToSearch.push('');
|
|
212
|
+
for (const dir of dirsToSearch) {
|
|
213
|
+
// For index pages, first check for index.eta in each directory
|
|
214
|
+
if (isIndexPage) {
|
|
215
|
+
const indexLayoutPath = dir ? join(srcDir, dir, 'index.eta') : join(srcDir, 'index.eta');
|
|
216
|
+
if (await pathExists(indexLayoutPath)) {
|
|
217
|
+
// Return relative path with forward slashes for Eta
|
|
218
|
+
const relativePath = dir ? `${dir}/index.eta` : 'index.eta';
|
|
219
|
+
return relativePath.replace(/\\/g, '/'); // Normalize to forward slashes
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Then check for layout.eta as fallback
|
|
223
|
+
const layoutPath = dir ? join(srcDir, dir, 'layout.eta') : join(srcDir, 'layout.eta');
|
|
224
|
+
if (await pathExists(layoutPath)) {
|
|
225
|
+
// Return relative path with forward slashes for Eta
|
|
226
|
+
const relativePath = dir ? `${dir}/layout.eta` : 'layout.eta';
|
|
227
|
+
return relativePath.replace(/\\/g, '/'); // Normalize to forward slashes
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
export function createTemplateEngine(config) {
|
|
233
|
+
const templateDir = join(process.cwd(), config.srcDir);
|
|
234
|
+
const eta = new Eta({
|
|
235
|
+
views: templateDir,
|
|
236
|
+
cache: process.env.NODE_ENV === 'production',
|
|
237
|
+
});
|
|
238
|
+
return eta;
|
|
239
|
+
}
|
|
240
|
+
export async function renderPage(page, body, config, eta, navigation, allPages) {
|
|
241
|
+
// Discover partials for this page's directory hierarchy
|
|
242
|
+
const srcDir = join(process.cwd(), config.srcDir);
|
|
243
|
+
const relativePath = relative(srcDir, page.sourcePath);
|
|
244
|
+
const partials = await discoverPartials(relativePath, config);
|
|
245
|
+
// Build collection data if this is an index page and all pages are provided
|
|
246
|
+
let collectionData;
|
|
247
|
+
const isIndexPage = allPages && isCollectionIndexPage(page, allPages);
|
|
248
|
+
if (isIndexPage) {
|
|
249
|
+
collectionData = buildCollectionData(page, allPages);
|
|
250
|
+
}
|
|
251
|
+
// Discover the appropriate layout using hierarchical layout.eta convention
|
|
252
|
+
// Pass isIndexPage flag to enable index.eta lookup for aggregation pages
|
|
253
|
+
const layoutPath = await discoverLayout(relativePath, config, page.frontMatter.layout, isIndexPage);
|
|
254
|
+
const context = {
|
|
255
|
+
site: config.site,
|
|
256
|
+
page: {
|
|
257
|
+
...page.frontMatter,
|
|
258
|
+
path: page.url,
|
|
259
|
+
content: body,
|
|
260
|
+
},
|
|
261
|
+
content: body,
|
|
262
|
+
navigation: navigation || [],
|
|
263
|
+
partials, // Add discovered partials to template context
|
|
264
|
+
collection: collectionData, // Add collection data for index pages
|
|
265
|
+
// Add custom filters to context
|
|
266
|
+
...(config.eta?.filters || {}),
|
|
267
|
+
};
|
|
268
|
+
try {
|
|
269
|
+
if (!layoutPath) {
|
|
270
|
+
console.warn('No layout template found, using fallback');
|
|
271
|
+
return createFallbackHtml(page, body);
|
|
272
|
+
}
|
|
273
|
+
return await eta.renderAsync(layoutPath, context);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
console.error(`Error rendering layout ${layoutPath || 'unknown'}:`, error);
|
|
277
|
+
return createFallbackHtml(page, body);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function createFallbackHtml(page, body) {
|
|
281
|
+
const title = page.frontMatter.title || 'Untitled';
|
|
282
|
+
const description = page.frontMatter.description || '';
|
|
283
|
+
return `<!DOCTYPE html>
|
|
284
|
+
<html lang="en">
|
|
285
|
+
<head>
|
|
286
|
+
<meta charset="UTF-8">
|
|
287
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
288
|
+
<title>${escapeHtml(title)}</title>
|
|
289
|
+
${description ? `<meta name="description" content="${escapeHtml(description)}">` : ''}
|
|
290
|
+
</head>
|
|
291
|
+
<body>
|
|
292
|
+
<main>
|
|
293
|
+
${body}
|
|
294
|
+
</main>
|
|
295
|
+
</body>
|
|
296
|
+
</html>`;
|
|
297
|
+
}
|
|
298
|
+
function escapeHtml(text) {
|
|
299
|
+
const map = {
|
|
300
|
+
'&': '&',
|
|
301
|
+
'<': '<',
|
|
302
|
+
'>': '>',
|
|
303
|
+
'"': '"',
|
|
304
|
+
"'": ''',
|
|
305
|
+
};
|
|
306
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
307
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview @stati/core - Core engine for Stati static site generator
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { build, loadConfig, defineConfig } from '@stati/core';
|
|
7
|
+
*
|
|
8
|
+
* // Define configuration with TypeScript support
|
|
9
|
+
* export default defineConfig({
|
|
10
|
+
* site: {
|
|
11
|
+
* title: 'My Site',
|
|
12
|
+
* baseUrl: 'https://example.com',
|
|
13
|
+
* },
|
|
14
|
+
* // ... other config options
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // Load configuration and build site
|
|
18
|
+
* const config = await loadConfig();
|
|
19
|
+
* await build({ clean: true });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export type { StatiConfig, PageModel, FrontMatter, BuildContext, PageContext, BuildHooks, NavNode, ISGConfig, AgingRule, BuildStats, } from './types.js';
|
|
23
|
+
export type { BuildOptions } from './core/build.js';
|
|
24
|
+
export { build } from './core/build.js';
|
|
25
|
+
export { loadConfig } from './config/loader.js';
|
|
26
|
+
export { invalidate } from './core/invalidate.js';
|
|
27
|
+
import type { StatiConfig } from './types.js';
|
|
28
|
+
/**
|
|
29
|
+
* Helper function for defining Stati configuration with TypeScript IntelliSense.
|
|
30
|
+
* Provides type checking and autocompletion for configuration options.
|
|
31
|
+
*
|
|
32
|
+
* @param config - The Stati configuration object
|
|
33
|
+
* @returns The same configuration object with proper typing
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* import { defineConfig } from '@stati/core';
|
|
38
|
+
*
|
|
39
|
+
* export default defineConfig({
|
|
40
|
+
* site: {
|
|
41
|
+
* title: 'My Blog',
|
|
42
|
+
* baseUrl: 'https://myblog.com',
|
|
43
|
+
* },
|
|
44
|
+
* srcDir: 'content',
|
|
45
|
+
* outDir: 'public',
|
|
46
|
+
* isg: {
|
|
47
|
+
* enabled: true,
|
|
48
|
+
* ttlSeconds: 3600,
|
|
49
|
+
* },
|
|
50
|
+
* hooks: {
|
|
51
|
+
* beforeAll: async (ctx) => {
|
|
52
|
+
* console.log(`Building ${ctx.pages.length} pages`);
|
|
53
|
+
* },
|
|
54
|
+
* },
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function defineConfig(config: StatiConfig): StatiConfig;
|
|
59
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,YAAY,EACV,WAAW,EACX,SAAS,EACT,WAAW,EACX,YAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,SAAS,EACT,SAAS,EACT,UAAU,GACX,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGlD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAE7D"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview @stati/core - Core engine for Stati static site generator
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import { build, loadConfig, defineConfig } from '@stati/core';
|
|
7
|
+
*
|
|
8
|
+
* // Define configuration with TypeScript support
|
|
9
|
+
* export default defineConfig({
|
|
10
|
+
* site: {
|
|
11
|
+
* title: 'My Site',
|
|
12
|
+
* baseUrl: 'https://example.com',
|
|
13
|
+
* },
|
|
14
|
+
* // ... other config options
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* // Load configuration and build site
|
|
18
|
+
* const config = await loadConfig();
|
|
19
|
+
* await build({ clean: true });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export { build } from './core/build.js';
|
|
23
|
+
export { loadConfig } from './config/loader.js';
|
|
24
|
+
export { invalidate } from './core/invalidate.js';
|
|
25
|
+
/**
|
|
26
|
+
* Helper function for defining Stati configuration with TypeScript IntelliSense.
|
|
27
|
+
* Provides type checking and autocompletion for configuration options.
|
|
28
|
+
*
|
|
29
|
+
* @param config - The Stati configuration object
|
|
30
|
+
* @returns The same configuration object with proper typing
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { defineConfig } from '@stati/core';
|
|
35
|
+
*
|
|
36
|
+
* export default defineConfig({
|
|
37
|
+
* site: {
|
|
38
|
+
* title: 'My Blog',
|
|
39
|
+
* baseUrl: 'https://myblog.com',
|
|
40
|
+
* },
|
|
41
|
+
* srcDir: 'content',
|
|
42
|
+
* outDir: 'public',
|
|
43
|
+
* isg: {
|
|
44
|
+
* enabled: true,
|
|
45
|
+
* ttlSeconds: 3600,
|
|
46
|
+
* },
|
|
47
|
+
* hooks: {
|
|
48
|
+
* beforeAll: async (ctx) => {
|
|
49
|
+
* console.log(`Building ${ctx.pages.length} pages`);
|
|
50
|
+
* },
|
|
51
|
+
* },
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function defineConfig(config) {
|
|
56
|
+
return config;
|
|
57
|
+
}
|