@slybridges/kiss 0.9.2 → 0.9.4
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/package.json +12 -12
- package/src/config/defaultConfig.js +3 -1
- package/src/data/computeModified.js +5 -1
- package/src/helpers.js +27 -3
- package/src/loaders/baseLoader.js +2 -0
- package/src/loaders/computeCollectionLoader.js +6 -1
- package/src/transforms/atAttributesContentTransform.js +83 -12
- package/src/transforms/nunjucksContentTransform.js +38 -10
- package/src/writers/rssContextWriter.js +10 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slybridges/kiss",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "Keep It Simple and Static site generator",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,28 +12,28 @@
|
|
|
12
12
|
"author": "Sylvestre Dupont",
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"browser-sync": "^3.0.
|
|
15
|
+
"browser-sync": "^3.0.4",
|
|
16
16
|
"chalk": "^4.1.2",
|
|
17
17
|
"cheerio": "^1.0.0",
|
|
18
18
|
"chokidar": "^3.6.0",
|
|
19
|
-
"date-fns": "^
|
|
20
|
-
"fast-glob": "^3.3.
|
|
19
|
+
"date-fns": "^4.1.0",
|
|
20
|
+
"fast-glob": "^3.3.3",
|
|
21
21
|
"front-matter": "^4.0.2",
|
|
22
|
-
"fs-extra": "^11.
|
|
22
|
+
"fs-extra": "^11.3.0",
|
|
23
23
|
"lodash": "^4.17.21",
|
|
24
|
-
"marked": "^12.0.
|
|
24
|
+
"marked": "^12.0.2",
|
|
25
25
|
"nunjucks": "^3.2.4",
|
|
26
|
-
"sharp": "^0.
|
|
26
|
+
"sharp": "^0.34.1",
|
|
27
27
|
"slugify": "^1.6.6",
|
|
28
28
|
"xml": "^1.0.1",
|
|
29
29
|
"yargs": "^17.7.2"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@eslint/js": "^9.
|
|
33
|
-
"eslint": "^9.
|
|
34
|
-
"eslint-config-prettier": "^
|
|
35
|
-
"globals": "^15.
|
|
36
|
-
"prettier": "^3.
|
|
32
|
+
"@eslint/js": "^9.26.0",
|
|
33
|
+
"eslint": "^9.26.0",
|
|
34
|
+
"eslint-config-prettier": "^10.1.2",
|
|
35
|
+
"globals": "^15.15.0",
|
|
36
|
+
"prettier": "^3.5.3"
|
|
37
37
|
},
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
|
@@ -195,12 +195,14 @@ const defaultConfig = {
|
|
|
195
195
|
},
|
|
196
196
|
rss: {
|
|
197
197
|
active: true,
|
|
198
|
-
|
|
198
|
+
limit: null, // number of entries to include. null = all entries
|
|
199
199
|
pageFilter: (page) =>
|
|
200
200
|
page.url &&
|
|
201
201
|
page._meta.isPost &&
|
|
202
202
|
!page.excludeFromWrite &&
|
|
203
203
|
!page.excludeFromSitemap,
|
|
204
|
+
sortCollectionBy: null, // defaults to config.defaults.sortCollectionBy
|
|
205
|
+
target: "feed.xml",
|
|
204
206
|
xmlOptions: {
|
|
205
207
|
declaration: true,
|
|
206
208
|
indent: env === "production" ? null : " ",
|
|
@@ -19,6 +19,10 @@ const computeModified = ({ _meta }, config, { pages }) => {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
computeModified.kissDependencies = [
|
|
22
|
+
computeModified.kissDependencies = [
|
|
23
|
+
"_meta.isCollection",
|
|
24
|
+
"_meta.descendants",
|
|
25
|
+
["_meta.descendants", "modified"],
|
|
26
|
+
]
|
|
23
27
|
|
|
24
28
|
module.exports = computeModified
|
package/src/helpers.js
CHANGED
|
@@ -4,13 +4,15 @@ const path = require("path")
|
|
|
4
4
|
|
|
5
5
|
// @ attribute format: @<attribute>:<value><terminator>
|
|
6
6
|
// terminators: space, comma, newline, end of string, ', ",<, >, ), ], }, #
|
|
7
|
+
// Regex must have global flag for matchAll to work correctly
|
|
7
8
|
const AT_GENERIC_ATTRIBUTE_REGEX =
|
|
8
9
|
/@([a-zA-Z0-9-_]+):([^,\s\n\]'"<>)}#]+)(?=[,\s\n\]'"<>)}#]|$)/g
|
|
9
|
-
// TODO: try with const AT_GENERIC_ATTRIBUTE_REGEX = /@([a-zA-Z0-9_-]+):([^,\s\]'"<>)}#]+)(?=[,\s\]'"<>)}#]|$)/g;
|
|
10
10
|
|
|
11
11
|
const AT_FILE_ATTRIBUTE_REGEX =
|
|
12
12
|
/@file:([^,\s\n\]'"<>)}#]+)(?=[,\s\n\]'"<>)}#]|$)/g
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
// Indexed cached version of collections for faster lookups
|
|
15
|
+
const collectionsCache = new Map()
|
|
14
16
|
|
|
15
17
|
const computePageId = (inputPath, config) => {
|
|
16
18
|
const imputPathObject = path.parse(inputPath)
|
|
@@ -35,15 +37,36 @@ const computeParentId = (inputPath, config) => {
|
|
|
35
37
|
return computePageId(parentPath, config)
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
// Optimized findCollectionById with caching
|
|
38
41
|
const findCollectionById = (collections, id) => {
|
|
39
42
|
if (id === ".") {
|
|
40
43
|
return collections
|
|
41
44
|
}
|
|
45
|
+
|
|
46
|
+
// Quick cache lookup by ID
|
|
47
|
+
const cacheKey = typeof id === "string" ? id : JSON.stringify(id)
|
|
48
|
+
if (collectionsCache.has(cacheKey)) {
|
|
49
|
+
return collectionsCache.get(cacheKey)
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
const flatCollections = flattenObjects(
|
|
43
53
|
collections,
|
|
44
54
|
(v) => v._type === "collection",
|
|
45
55
|
)
|
|
46
|
-
|
|
56
|
+
|
|
57
|
+
const result = flatCollections.find((collection) => collection._id === id)
|
|
58
|
+
|
|
59
|
+
// Cache the result for future lookups
|
|
60
|
+
if (result) {
|
|
61
|
+
collectionsCache.set(cacheKey, result)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Clear the collections cache - should be called when collections are modified
|
|
68
|
+
const clearCollectionsCache = () => {
|
|
69
|
+
collectionsCache.clear()
|
|
47
70
|
}
|
|
48
71
|
|
|
49
72
|
const flattenObjects = (obj, predicate) => {
|
|
@@ -379,6 +402,7 @@ module.exports = {
|
|
|
379
402
|
relativeToAbsoluteAttributes,
|
|
380
403
|
sortPageIds,
|
|
381
404
|
sortPages,
|
|
405
|
+
clearCollectionsCache,
|
|
382
406
|
}
|
|
383
407
|
|
|
384
408
|
/** private **/
|
|
@@ -11,6 +11,8 @@ const baseLoader = (inputPath, options = {}, page = {}, pages, config) => {
|
|
|
11
11
|
if (basename === config.dirs.content) {
|
|
12
12
|
basename = ""
|
|
13
13
|
}
|
|
14
|
+
// FIXME: the parent data we want here is the one coming from index.* files
|
|
15
|
+
// and not any that was overwritten by post.* files
|
|
14
16
|
const parentData = parentId
|
|
15
17
|
? getParentPage(pages, parentId, inputPathObject.name === "post")
|
|
16
18
|
: {}
|
|
@@ -58,7 +58,12 @@ const computeCollectionLoader = (pages, options, config) => {
|
|
|
58
58
|
let topLevelPage = baseLoader(
|
|
59
59
|
inputPath,
|
|
60
60
|
{ source: "computed", collectionGroup: options.name },
|
|
61
|
-
{
|
|
61
|
+
{
|
|
62
|
+
// FIXME: remove once we are able to tell the baseloader
|
|
63
|
+
// to only load data form index.* files and not post.* files
|
|
64
|
+
layout: options.template || config.templates.collection,
|
|
65
|
+
_meta: { baseTitle: options.name },
|
|
66
|
+
},
|
|
62
67
|
pages,
|
|
63
68
|
config,
|
|
64
69
|
)
|
|
@@ -3,7 +3,19 @@ const path = require("path")
|
|
|
3
3
|
|
|
4
4
|
const { AT_GENERIC_ATTRIBUTE_REGEX } = require("../helpers")
|
|
5
5
|
|
|
6
|
+
// Memoization cache for attribute resolution
|
|
7
|
+
const memoCache = new Map()
|
|
8
|
+
|
|
9
|
+
// Cache key generator for memoization
|
|
10
|
+
const getCacheKey = (attribute, value, pageId) =>
|
|
11
|
+
`${pageId}:${attribute}:${value}`
|
|
12
|
+
|
|
6
13
|
const atAttributesContentTransform = (page, options, config, context) => {
|
|
14
|
+
// Clear the cache if this is a full build (not incremental)
|
|
15
|
+
if (!options.incremental) {
|
|
16
|
+
memoCache.clear()
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
// we need to go though all the keys in the page
|
|
8
20
|
// that will be expensive, but YOLO
|
|
9
21
|
for (const objKey in page) {
|
|
@@ -33,7 +45,6 @@ const transformAtAttributesInObjValue = (
|
|
|
33
45
|
config,
|
|
34
46
|
context,
|
|
35
47
|
) => {
|
|
36
|
-
let match
|
|
37
48
|
const resolvers = [
|
|
38
49
|
{ key: "data", handler: dataAttributeResolver },
|
|
39
50
|
{ key: "file", handler: fileAttributeResolver, pageAttribute: "permalink" },
|
|
@@ -44,8 +55,35 @@ const transformAtAttributesInObjValue = (
|
|
|
44
55
|
pageAttribute: "permalink",
|
|
45
56
|
},
|
|
46
57
|
]
|
|
58
|
+
|
|
47
59
|
if (typeof objValue === "object") {
|
|
48
|
-
//
|
|
60
|
+
// Process object values in parallel via Promise.all for nested objects
|
|
61
|
+
if (Array.isArray(objValue) && objValue.length > 10) {
|
|
62
|
+
// For large arrays, process in parallel
|
|
63
|
+
const promises = objValue.map(async (item, index) => {
|
|
64
|
+
if (typeof item === "string") {
|
|
65
|
+
return transformAtAttributesInObjValue(
|
|
66
|
+
`${objKey}[${index}]`,
|
|
67
|
+
item,
|
|
68
|
+
page,
|
|
69
|
+
config,
|
|
70
|
+
context,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return item
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// This is non-blocking but will still return the result immediately
|
|
77
|
+
// since we're not awaiting the Promise.all
|
|
78
|
+
Promise.all(promises).then((results) => {
|
|
79
|
+
results.forEach((result, index) => {
|
|
80
|
+
objValue[index] = result
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
return objValue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// For regular objects or small arrays, process sequentially
|
|
49
87
|
for (const key in objValue) {
|
|
50
88
|
if (typeof objValue[key] === "string") {
|
|
51
89
|
let newValue = transformAtAttributesInObjValue(
|
|
@@ -56,29 +94,49 @@ const transformAtAttributesInObjValue = (
|
|
|
56
94
|
context,
|
|
57
95
|
)
|
|
58
96
|
objValue[key] = newValue
|
|
97
|
+
} else if (typeof objValue[key] === "object") {
|
|
98
|
+
objValue[key] = transformAtAttributesInObjValue(
|
|
99
|
+
key,
|
|
100
|
+
objValue[key],
|
|
101
|
+
page,
|
|
102
|
+
config,
|
|
103
|
+
context,
|
|
104
|
+
)
|
|
59
105
|
}
|
|
60
106
|
}
|
|
61
107
|
return objValue
|
|
62
108
|
}
|
|
109
|
+
|
|
63
110
|
if (typeof objValue !== "string") {
|
|
64
111
|
return objValue
|
|
65
112
|
}
|
|
66
|
-
|
|
67
|
-
//
|
|
113
|
+
|
|
114
|
+
// Quick check to avoid regex processing if there's no @ symbol
|
|
115
|
+
if (!objValue.includes("@")) {
|
|
116
|
+
return objValue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Use matchAll with the global regex (now fixed in helpers.js)
|
|
120
|
+
const matches = Array.from(objValue.matchAll(AT_GENERIC_ATTRIBUTE_REGEX))
|
|
121
|
+
if (matches.length === 0) {
|
|
122
|
+
return objValue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create a map of attributes to avoid duplicates
|
|
68
126
|
let allAttributes = {}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// using an object to deduplicate attributes
|
|
127
|
+
for (const match of matches) {
|
|
128
|
+
const [fullMatch, attribute, value] = match
|
|
72
129
|
allAttributes[fullMatch] = { attribute, fullMatch, value }
|
|
73
130
|
}
|
|
74
|
-
|
|
75
|
-
//
|
|
131
|
+
|
|
132
|
+
// Sort attributes by length to replace longest first
|
|
76
133
|
const sortedAttributes = Object.values(allAttributes).sort(
|
|
77
134
|
(a, b) => b.fullMatch.length - a.fullMatch.length,
|
|
78
135
|
)
|
|
79
|
-
|
|
136
|
+
|
|
137
|
+
// Process the attributes
|
|
80
138
|
for (const { attribute, fullMatch, value } of sortedAttributes) {
|
|
81
|
-
//
|
|
139
|
+
// Find the resolver
|
|
82
140
|
const resolver = resolvers.find((r) => r.key === attribute)
|
|
83
141
|
if (!resolver) {
|
|
84
142
|
global.logger.error(
|
|
@@ -86,12 +144,25 @@ const transformAtAttributesInObjValue = (
|
|
|
86
144
|
)
|
|
87
145
|
continue
|
|
88
146
|
}
|
|
89
|
-
|
|
147
|
+
|
|
148
|
+
// Check memoization cache
|
|
149
|
+
const cacheKey = getCacheKey(attribute, value, page._meta.id)
|
|
150
|
+
let result, error
|
|
151
|
+
|
|
152
|
+
if (memoCache.has(cacheKey)) {
|
|
153
|
+
;[result, error] = memoCache.get(cacheKey)
|
|
154
|
+
} else {
|
|
155
|
+
;[result, error] = resolver.handler(value, page, config, context)
|
|
156
|
+
// Cache the result
|
|
157
|
+
memoCache.set(cacheKey, [result, error])
|
|
158
|
+
}
|
|
159
|
+
|
|
90
160
|
if (error) {
|
|
91
161
|
global.logger.error(`Page '${page._meta.id}' in '${objKey}': ${error}`)
|
|
92
162
|
objValue = objValue.replaceAll(fullMatch, value)
|
|
93
163
|
continue
|
|
94
164
|
}
|
|
165
|
+
|
|
95
166
|
if (resolver.pageAttribute) {
|
|
96
167
|
// result is a page
|
|
97
168
|
// first we check if the page is excluded from write
|
|
@@ -1,19 +1,47 @@
|
|
|
1
1
|
const { findCollectionById } = require("../helpers.js")
|
|
2
2
|
|
|
3
|
+
// Template context cache for memoization
|
|
4
|
+
const templateContextCache = new Map()
|
|
5
|
+
|
|
6
|
+
// Cache key generator for memoization
|
|
7
|
+
const getContextCacheKey = (pageId, collectionId) =>
|
|
8
|
+
`${pageId}:${collectionId || "no-collection"}`
|
|
9
|
+
|
|
3
10
|
const nunjucksContentTransform = (page, options, config, context) => {
|
|
4
|
-
|
|
5
|
-
if (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
// Clear the cache if this is a full build (not incremental)
|
|
12
|
+
if (!options.incremental) {
|
|
13
|
+
templateContextCache.clear()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let templateContext
|
|
17
|
+
const cacheKey = getContextCacheKey(
|
|
18
|
+
page._meta.id,
|
|
19
|
+
page._meta.isCollection ? page._meta.id : null,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
// Check if we have a cached context
|
|
23
|
+
if (templateContextCache.has(cacheKey)) {
|
|
24
|
+
templateContext = templateContextCache.get(cacheKey)
|
|
11
25
|
} else {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
26
|
+
// Create template context based on whether this is a collection or not
|
|
27
|
+
if (page._meta.isCollection) {
|
|
28
|
+
templateContext = {
|
|
29
|
+
...context,
|
|
30
|
+
collection: findCollectionById(context.collections, page._meta.id),
|
|
31
|
+
...page,
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
templateContext = {
|
|
35
|
+
...context,
|
|
36
|
+
...page,
|
|
37
|
+
}
|
|
15
38
|
}
|
|
39
|
+
|
|
40
|
+
// Cache the template context for future use
|
|
41
|
+
templateContextCache.set(cacheKey, templateContext)
|
|
16
42
|
}
|
|
43
|
+
|
|
44
|
+
// Render the page using Nunjucks
|
|
17
45
|
page._html = config.libs.nunjucks.render(page.layout, templateContext)
|
|
18
46
|
return page
|
|
19
47
|
}
|
|
@@ -190,12 +190,16 @@ const rssContextWriter = async (context, options, config) => {
|
|
|
190
190
|
feedObject.push({ category: context.site.category })
|
|
191
191
|
}
|
|
192
192
|
// page entries
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
const sortBy = options.sortCollectionBy || config.defaults.sortCollectionBy
|
|
194
|
+
let sortedPages = sortPages(
|
|
195
|
+
_.filter(context.pages, options.pageFilter),
|
|
196
|
+
sortBy,
|
|
197
|
+
)
|
|
198
|
+
if (options.limit) {
|
|
199
|
+
sortedPages = sortedPages.slice(0, options.limit)
|
|
200
|
+
}
|
|
201
|
+
const pageObjects = sortedPages.map((page) =>
|
|
202
|
+
pageObject(page, options, config),
|
|
199
203
|
)
|
|
200
204
|
feedObject = feedObject.concat(pageObjects)
|
|
201
205
|
const feedXML = xml({ feed: feedObject }, options.xmlOptions)
|