@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slybridges/kiss",
3
- "version": "0.9.2",
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.3",
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": "^3.6.0",
20
- "fast-glob": "^3.3.2",
19
+ "date-fns": "^4.1.0",
20
+ "fast-glob": "^3.3.3",
21
21
  "front-matter": "^4.0.2",
22
- "fs-extra": "^11.2.0",
22
+ "fs-extra": "^11.3.0",
23
23
  "lodash": "^4.17.21",
24
- "marked": "^12.0.1",
24
+ "marked": "^12.0.2",
25
25
  "nunjucks": "^3.2.4",
26
- "sharp": "^0.33.5",
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.11.1",
33
- "eslint": "^9.11.1",
34
- "eslint-config-prettier": "^9.1.0",
35
- "globals": "^15.9.0",
36
- "prettier": "^3.3.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
- target: "feed.xml",
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 = [["_meta.descendants", "modified"]]
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
- // TODO: try with const AT_FILE_ATTRIBUTE_REGEX = /@file:([^\s,\\\]'"<>)}#]+)(?=[\s,\\\]'"<>)}#]|$)/g;
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
- return flatCollections.find((collection) => collection._id === id)
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
- { _meta: { baseTitle: options.name } },
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
- // we need to go though all the keys in the object
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
- // we cannot search and replace things as we go, as some attributes may overlap others (e.g. @id:home and @id:home:fr)
67
- // so first, we are going to find all the attributes
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
- while ((match = AT_GENERIC_ATTRIBUTE_REGEX.exec(objValue))) {
70
- let [fullMatch, attribute, value] = match
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
- // we sort attributes by their fullMatch length to make sure we replace the longest first
75
- // this is important to make sure we don't replace a shorter attribute that is part of a longer one
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
- // now we can go through all the attributes and resolve them
136
+
137
+ // Process the attributes
80
138
  for (const { attribute, fullMatch, value } of sortedAttributes) {
81
- // find the resolver
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
- const [result, error] = resolver.handler(value, page, config, context)
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
- let templateContext = {}
5
- if (page._meta.isCollection) {
6
- templateContext = {
7
- ...context,
8
- collection: findCollectionById(context.collections, page._meta.id),
9
- ...page,
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
- templateContext = {
13
- ...context,
14
- ...page,
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 pageObjects = _.map(
194
- sortPages(
195
- _.filter(context.pages, options.pageFilter),
196
- config.defaults.sortCollectionBy,
197
- ),
198
- (page) => pageObject(page, options, config),
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)