coralite-plugin-aggregation 0.7.3 → 0.9.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.
@@ -0,0 +1,104 @@
1
+ <template id="coralite-pagination">
2
+ <nav aria-label="{{ ariaLabel }}">
3
+ <ul class="pagination">
4
+ {{ paginationLinks }}
5
+ </ul>
6
+ </nav>
7
+ </template>
8
+
9
+ <script type="module">
10
+ import { defineComponent } from 'coralite/plugins'
11
+
12
+ export default defineComponent({
13
+ properties: (context) => {
14
+ const props = context.properties
15
+
16
+ const ariaLabel = props['aria-label'] || 'Page navigation'
17
+
18
+ const currentPage = parseInt(props['current-page'] || '1', 10)
19
+ const totalPages = parseInt(props['total-pages'] || '1', 10)
20
+ const maxVisible = parseInt(props['max-visible'] || '5', 10)
21
+
22
+ const baseUrl = props['base-url'] || ''
23
+ const urlPrefix = props['url-prefix'] || ''
24
+ const segment = props['segment'] || 'page'
25
+ const ellipsis = props['ellipsis'] || '...'
26
+
27
+ // Compute pagination synchronously
28
+ const getPageUrl = (page) => {
29
+ if (page === 1) {
30
+ return baseUrl
31
+ }
32
+
33
+ const cleanPrefix = urlPrefix.endsWith('/') ? urlPrefix : `${urlPrefix}/`
34
+ return `${cleanPrefix}${segment}/${page}.html`
35
+ }
36
+
37
+ const createItem = (page, text, isActive, isDisabled) => {
38
+ let className = 'page-item'
39
+ if (isActive) className += ' active'
40
+ if (isDisabled) className += ' disabled'
41
+
42
+ let attr = ''
43
+ if (isActive) attr += ' aria-current="page"'
44
+ if (isDisabled) attr += ' tabindex="-1" aria-disabled="true"'
45
+
46
+ const href = isDisabled ? '#' : getPageUrl(page)
47
+
48
+ return `<li class="${className}"><a class="page-link" href="${href}"${attr}>${text}</a></li>`
49
+ }
50
+
51
+ let links = ''
52
+
53
+ // Previous Link
54
+ links += createItem(currentPage - 1, 'Previous', false, currentPage <= 1)
55
+
56
+ // Calculate Window (Start/End)
57
+ const half = Math.floor(maxVisible / 2)
58
+ let start = currentPage - half
59
+ let end = currentPage + half
60
+
61
+ if (start < 1) {
62
+ start = 1
63
+ end = Math.min(totalPages, maxVisible)
64
+ }
65
+
66
+ if (end > totalPages) {
67
+ end = totalPages
68
+ start = Math.max(1, totalPages - maxVisible + 1)
69
+ }
70
+
71
+ const showFirst = start > 1
72
+ const showLast = end < totalPages
73
+
74
+ // First page + ellipsis
75
+ if (showFirst) {
76
+ links += createItem(1, '1', false, false)
77
+ if (start > 2) {
78
+ links += `<li class="page-item disabled"><span class="page-link">${ellipsis}</span></li>`
79
+ }
80
+ }
81
+
82
+ // Main window loop
83
+ for (let i = start; i <= end; i++) {
84
+ links += createItem(i, i, i === currentPage, false)
85
+ }
86
+
87
+ // Last page + ellipsis
88
+ if (showLast) {
89
+ if (end < totalPages - 1) {
90
+ links += `<li class="page-item disabled"><span class="page-link">${ellipsis}</span></li>`
91
+ }
92
+ links += createItem(totalPages, totalPages, false, false)
93
+ }
94
+
95
+ // Next Link
96
+ links += createItem(currentPage + 1, 'Next', false, currentPage >= totalPages)
97
+
98
+ return {
99
+ ariaLabel,
100
+ paginationLinks: links
101
+ }
102
+ }
103
+ })
104
+ </script>
package/lib/index.js CHANGED
@@ -4,12 +4,12 @@ import path from 'node:path'
4
4
  /**
5
5
  * Aggregates content based on configuration
6
6
  * @param {import('../types/index.js').AggregationOptions} options
7
- * @param {Object} contextInstance
7
+ * @param {Object} context
8
8
  * @returns {Promise<any[]>}
9
9
  */
10
- async function aggregationMethod (options, contextInstance) {
10
+ async function aggregationMethod (options, context) {
11
11
  const {
12
- path: paths = [],
12
+ path: paths = [],
13
13
  template,
14
14
  pagination,
15
15
  filter,
@@ -17,10 +17,10 @@ async function aggregationMethod (options, contextInstance) {
17
17
  limit,
18
18
  offset = 0,
19
19
  recursive = false,
20
- tokens
20
+ transformProperties
21
21
  } = options
22
22
 
23
- const contextValues = contextInstance.values
23
+ const contextProperties = context.properties || {}
24
24
  const pagesRoot = this.options.pages
25
25
 
26
26
  // Collect pages
@@ -30,13 +30,13 @@ async function aggregationMethod (options, contextInstance) {
30
30
  for (const relativePath of paths) {
31
31
  const targetPath = path.join(pagesRoot, relativePath)
32
32
 
33
- // Check direct path match in listByPath (non-recursive)
34
33
  if (!recursive) {
35
34
  const pagesInDir = this.pages.getListByPath(targetPath)
36
35
  if (pagesInDir) {
37
36
  for (const page of pagesInDir) {
38
- if (!uniquePaths.has(page.path.pathname)) {
39
- uniquePaths.add(page.path.pathname)
37
+ const pagePath = page.url ? page.url.pathname : page.path.pathname
38
+ if (!uniquePaths.has(pagePath)) {
39
+ uniquePaths.add(pagePath)
40
40
  allPages.push(page)
41
41
  }
42
42
  }
@@ -44,11 +44,11 @@ async function aggregationMethod (options, contextInstance) {
44
44
  } else {
45
45
  // Recursive search
46
46
  for (const page of this.pages.list) {
47
- const dirname = page.path.dirname
48
- // Check if dirname is targetPath or a subdirectory of targetPath
47
+ const dirname = page.file ? page.file.dirname : page.path.dirname
49
48
  if (dirname === targetPath || dirname.startsWith(targetPath + path.sep)) {
50
- if (!uniquePaths.has(page.path.pathname)) {
51
- uniquePaths.add(page.path.pathname)
49
+ const pagePath = page.url ? page.url.pathname : page.path.pathname
50
+ if (!uniquePaths.has(pagePath)) {
51
+ uniquePaths.add(pagePath)
52
52
  allPages.push(page)
53
53
  }
54
54
  }
@@ -59,18 +59,17 @@ async function aggregationMethod (options, contextInstance) {
59
59
  // Filter
60
60
  if (typeof filter === 'function') {
61
61
  allPages = allPages.filter(page => {
62
- // Access values from result property
63
- const values = page.result && page.result.values ? page.result.values : page.values
64
- return filter(values)
62
+ const pageProps = (page.result && page.result.properties) ? page.result.properties : page.properties
63
+ return filter(pageProps)
65
64
  })
66
65
  }
67
66
 
68
67
  // Sort
69
68
  if (typeof sort === 'function') {
70
69
  allPages.sort((a, b) => {
71
- const valA = a.result && a.result.values ? a.result.values : a.values
72
- const valB = b.result && b.result.values ? b.result.values : b.values
73
- return sort(valA, valB)
70
+ const propsA = (a.result && a.result.properties) ? a.result.properties : a.properties
71
+ const propsB = (b.result && b.result.properties) ? b.result.properties : b.properties
72
+ return sort(propsA, propsB)
74
73
  })
75
74
  }
76
75
 
@@ -81,18 +80,15 @@ async function aggregationMethod (options, contextInstance) {
81
80
  let currentPage = 1
82
81
  let totalPages = 1
83
82
 
84
- // Ensure renderContext is available
85
- const currentRenderContext = contextInstance.renderContext
83
+ const currentRenderContext = context.renderContext
86
84
  const buildId = currentRenderContext && currentRenderContext.buildId
87
85
 
88
86
  if (limit) {
89
87
  if (pagination) {
90
88
  const segment = pagination.segment || 'page'
91
- const urlPathname = contextValues.$urlPathname || ''
89
+ const urlPathname = context.page.url.pathname
92
90
 
93
91
  const escapedSegment = segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
94
-
95
- // Try to match segment in URL: /.../segment/Number
96
92
  const segmentRegex = new RegExp(`/${escapedSegment}/(\\d+)`)
97
93
  const match = urlPathname.match(segmentRegex)
98
94
 
@@ -102,81 +98,55 @@ async function aggregationMethod (options, contextInstance) {
102
98
 
103
99
  startIndex = offset + (currentPage - 1) * limit
104
100
  endIndex = startIndex + limit
105
-
106
101
  totalPages = Math.ceil(allPages.length / limit)
107
102
 
108
- // Automatically generate subsequent pagination pages if we are on the "root" page
109
- // e.g., if we are on /blog/index.html (page 1), queue up /blog/page/2, /blog/page/3...
110
103
  if (!match && currentPage === 1 && totalPages > 1 && buildId) {
111
- const currentDocument = contextInstance.component
112
- const currentPathname = currentDocument.path.pathname
113
- const currentFilename = currentDocument.path.filename
114
- const currentDirname = currentDocument.path.dirname
104
+ const currentPathname = context.page.file.pathname
105
+ const currentFilename = context.page.file.filename
106
+ const currentDirname = context.page.file.dirname
115
107
 
116
- // Determine the output path structure based on rules
117
108
  let targetDir = currentDirname
118
109
  let urlPrefixBase = ''
119
110
 
120
- // Rules:
121
- // /index.html -> /page/1.html (relative to currentDirname)
122
- // /blog.html -> /blog/page/1.html (creates subdirectory)
123
- // /blog/index.html -> /blog/page/1.html (relative to currentDirname)
124
- // /blog/today.html -> /blog/today/page/1.html (creates subdirectory)
125
-
126
111
  if (currentFilename === 'index.html') {
127
- // Standard case: keep in same directory
128
112
  targetDir = currentDirname
129
- // Prefix for URL generation in child
130
- urlPrefixBase = path.dirname(urlPathname) // e.g. /blog/
113
+ urlPrefixBase = path.dirname(urlPathname)
131
114
  } else {
132
- // Named file: create subdirectory with same name (minus extension)
133
115
  const basename = path.basename(currentFilename, path.extname(currentFilename))
134
116
  targetDir = path.join(currentDirname, basename)
135
-
136
- // e.g. /blog.html -> /blog/
137
117
  urlPrefixBase = urlPathname.replace(path.extname(currentFilename), '')
138
118
  }
139
119
 
140
- // Ensure trailing slash for URL prefix
141
120
  if (!urlPrefixBase.endsWith('/')) urlPrefixBase += '/'
142
- // path.dirname returns / for /index.html, but /blog for /blog/index.html.
143
- // If /blog/index.html, urlPathname is /blog/index.html. dirname is /blog.
144
121
 
145
122
  if (currentFilename === 'index.html') {
146
- // If /index.html, urlPathname /index.html. dirname /. prefix /.
147
- // If /blog/index.html, urlPathname /blog/index.html. dirname /blog. prefix /blog/.
148
123
  urlPrefixBase = path.dirname(urlPathname)
149
124
  if (!urlPrefixBase.endsWith('/')) urlPrefixBase += '/'
150
125
  }
151
126
 
152
- // Retrieve the original item to get content
153
- const currentItem = this.pages.getItem(currentDocument.path.pathname)
127
+ const currentItem = this.pages.getItem(currentPathname)
154
128
 
155
129
  for (let i = 2; i <= totalPages; i++) {
156
130
  const newPathname = path.join(targetDir, segment, `${i}.html`)
157
131
 
158
132
  const virtualItem = {
159
- content: currentItem ? currentItem.content : '', // Use original content
133
+ content: currentItem ? currentItem.content : '',
160
134
  path: {
161
135
  pathname: newPathname,
162
136
  dirname: path.dirname(newPathname),
163
137
  filename: path.basename(newPathname)
164
138
  },
165
- values: {
166
- // Pass metadata to help resolve paths in children
167
- meta_pagination_base_url: urlPathname,
168
- // Calculate prefix once and pass it down
169
- meta_pagination_url_prefix: urlPrefixBase
139
+ properties: {
140
+ paginationBaseUrl: urlPathname,
141
+ paginationUrlPrefix: urlPrefixBase
170
142
  },
171
143
  type: 'page'
172
144
  }
173
145
 
174
- // Add to queue
175
146
  await this.addRenderQueue(virtualItem, buildId)
176
147
  }
177
148
  }
178
149
  } else {
179
- // Simple limit/offset without pagination logic
180
150
  endIndex = Math.min(startIndex + limit, allPages.length)
181
151
  }
182
152
  }
@@ -185,29 +155,28 @@ async function aggregationMethod (options, contextInstance) {
185
155
  const resultNodes = []
186
156
 
187
157
  for (const page of paginatedPages) {
188
- const pageValues = page.result && page.result.values ? page.result.values : page.values
189
- let itemValues = { ...pageValues }
190
-
191
- // Apply token transformations
192
- if (tokens && typeof tokens === 'object') {
193
- for (const key in tokens) {
194
- if (Object.prototype.hasOwnProperty.call(tokens, key)) {
195
- const transform = tokens[key]
158
+ const pageProps = (page.result && page.result.properties) ? page.result.properties : page.properties
159
+ let itemProps = { ...pageProps }
160
+
161
+ // Apply properties transformations
162
+ if (transformProperties && typeof transformProperties === 'object') {
163
+ for (const key in transformProperties) {
164
+ if (Object.prototype.hasOwnProperty.call(transformProperties, key)) {
165
+ const transform = transformProperties[key]
196
166
  if (typeof transform === 'string') {
197
- itemValues[key] = pageValues[transform]
167
+ itemProps[key] = pageProps[transform]
198
168
  } else if (typeof transform === 'function') {
199
- itemValues[key] = transform(pageValues)
169
+ itemProps[key] = transform(pageProps)
200
170
  }
201
171
  }
202
172
  }
203
173
  }
204
174
 
205
- // Render the item template
206
175
  if (template) {
207
176
  const component = await this.createComponentElement({
208
177
  id: template,
209
- values: itemValues,
210
- component: contextInstance.component,
178
+ properties: itemProps,
179
+ page: context.page,
211
180
  renderContext: currentRenderContext
212
181
  })
213
182
 
@@ -217,39 +186,31 @@ async function aggregationMethod (options, contextInstance) {
217
186
  }
218
187
  }
219
188
 
220
- // Render pagination controls
221
189
  if (pagination) {
222
- const segment = pagination.segment || 'page'
223
- const escapedSegment = segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
224
-
225
190
  const paginationTemplateId = pagination.template || 'coralite-pagination'
226
-
227
- // Construct baseUrl and urlPrefix for template
228
- const urlPathname = contextValues.$urlPathname
191
+ const urlPathname = context.page.url.pathname
192
+
229
193
  let baseUrl = urlPathname
230
194
  let urlPrefix = ''
231
195
 
232
- if (contextValues.meta_pagination_base_url) {
233
- baseUrl = contextValues.meta_pagination_base_url
196
+ if (contextProperties.paginationBaseUrl) {
197
+ baseUrl = contextProperties.paginationBaseUrl
234
198
  }
235
199
 
236
- if (contextValues.meta_pagination_url_prefix) {
237
- urlPrefix = contextValues.meta_pagination_url_prefix
200
+ if (contextProperties.paginationUrlPrefix) {
201
+ urlPrefix = contextProperties.paginationUrlPrefix
238
202
  } else {
239
- // Page 1 logic calculation if not passed
240
203
  if (baseUrl.endsWith('/index.html') || baseUrl.endsWith('/')) {
241
204
  urlPrefix = path.dirname(baseUrl)
242
205
  } else {
243
- // Named file
244
206
  const basename = path.basename(baseUrl, '.html')
245
207
  urlPrefix = path.join(path.dirname(baseUrl), basename)
246
208
  }
247
209
  }
248
210
 
249
- // Normalize urlPrefix to ensure trailing slash
250
211
  if (!urlPrefix.endsWith('/')) urlPrefix += '/'
251
212
 
252
- const paginationValues = {
213
+ const paginationProps = {
253
214
  'current-page': String(currentPage),
254
215
  'total-pages': String(totalPages),
255
216
  'base-url': baseUrl,
@@ -262,8 +223,8 @@ async function aggregationMethod (options, contextInstance) {
262
223
 
263
224
  const component = await this.createComponentElement({
264
225
  id: paginationTemplateId,
265
- values: paginationValues,
266
- component: contextInstance.component,
226
+ properties: paginationProps,
227
+ page: context.page,
267
228
  renderContext: currentRenderContext
268
229
  })
269
230
 
@@ -279,8 +240,9 @@ export const aggregation = definePlugin({
279
240
  name: 'aggregation',
280
241
  method: aggregationMethod,
281
242
  components: [
282
- path.join(import.meta.dirname, 'templates/coralite-pagination.html')
243
+ // @ts-ignore
244
+ path.join(import.meta.dirname, 'components/coralite-pagination.html')
283
245
  ]
284
246
  })
285
247
 
286
- export default aggregation
248
+ export default aggregation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coralite-plugin-aggregation",
3
- "version": "0.7.3",
3
+ "version": "0.9.0",
4
4
  "description": "A Coralite plugin for dynamically collecting, filtering, sorting, and displaying content across multiple sources. Build database-free Coralite websites with automated content aggregation.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -39,14 +39,11 @@
39
39
  "lib",
40
40
  "types"
41
41
  ],
42
- "peerDependencies": {
43
- "coralite": "^0.31.7"
44
- },
45
- "license": "AGPL-3.0-only",
42
+ "license": "MPL-2.0",
46
43
  "devDependencies": {
47
44
  "@stylistic/eslint-plugin-js": "^4.2.0",
48
45
  "@stylistic/eslint-plugin-plus": "^4.2.0",
49
- "coralite": "^0.31.7"
46
+ "coralite": "^0.33.1"
50
47
  },
51
48
  "scripts": {
52
49
  "test": "node --test ./tests/index.spec.js"
package/types/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  * @property {number} [limit] - The maximum number of items to return (or items per page if pagination is used).
18
18
  * @property {number} [offset=0] - The starting index for fetching items.
19
19
  * @property {boolean} [recursive=false] - If true, searches subdirectories of the specified paths.
20
- * @property {Object.<string, (string|function(Object): *)>} [tokens] - A map of key transformations. Keys are the new property names. Values can be a string (source property name) or a function (receiving page values and returning the new value).
20
+ * @property {Object.<string, (string|function(Object): *)>} [transformProperties] - A map of key transformations. Keys are the new property names. Values can be a string (source property name) or a function (receiving page values and returning the new value).
21
21
  */
22
22
 
23
23
  export default {}
@@ -1,123 +0,0 @@
1
- <template id="coralite-pagination">
2
- <nav aria-label="{{ ariaLabel }}">
3
- <ul class="pagination">
4
- {{ paginationLinks }}
5
- </ul>
6
- </nav>
7
- </template>
8
-
9
- <script type="module">
10
- import { defineComponent } from 'coralite'
11
-
12
- export default defineComponent({
13
- tokens: {
14
- ariaLabel(context) {
15
- return context['aria-label'] || 'Page navigation'
16
- },
17
- /**
18
- * Generates HTML pagination links based on the provided context.
19
- *
20
- * @param {Object} context - The context object containing pagination details.
21
- * @param {string} context['current-page'] - The current page number (1-based).
22
- * @param {string} context['total-pages'] - The total number of pages.
23
- * @param {string} context['base-url'] - The explicit URL for Page 1 (e.g., '/blog.html').
24
- * @param {string} context['url-prefix'] - The directory prefix for paginated segments (e.g., '/blog').
25
- * @param {string} [context.segment='page'] - The URL segment for pagination (default: 'page').
26
- * @param {string} [context.max-visible='5'] - Max number of direct page links to show.
27
- * @param {string} [context.ellipsis='...'] - Text to display for skipped pages.
28
- * @returns {string} - A string containing the HTML <li> elements.
29
- */
30
- paginationLinks(context) {
31
- // Parse inputs with defaults
32
- const currentPage = parseInt(context['current-page'] || '1', 10)
33
- const totalPages = parseInt(context['total-pages'] || '1', 10)
34
- const maxVisible = parseInt(context['max-visible'] || '5', 10)
35
-
36
- const baseUrl = context['base-url'] || ''
37
- const urlPrefix = context['url-prefix'] || ''
38
- const segment = context['segment'] || 'page'
39
- const ellipsis = context['ellipsis'] || '...'
40
-
41
- /**
42
- * Constructs the URL for a specific page.
43
- * 1. Page 1 links strictly to `base-url` (canonical root).
44
- * 2. Pages > 1 follow the pattern: `{url-prefix}/{segment}/{page}.html`.
45
- */
46
- const getPageUrl = (page) => {
47
- if (page === 1) {
48
- return baseUrl
49
- }
50
-
51
- // Ensure prefix has a trailing slash for concatenation
52
- const cleanPrefix = urlPrefix.endsWith('/') ? urlPrefix : `${urlPrefix}/`
53
- return `${cleanPrefix}${segment}/${page}.html`
54
- }
55
-
56
- // Helper to generate list items
57
- const createItem = (page, text, isActive, isDisabled) => {
58
- let className = 'page-item'
59
- if (isActive) className += ' active'
60
- if (isDisabled) className += ' disabled'
61
-
62
- let attr = ''
63
- if (isActive) attr += ' aria-current="page"'
64
- if (isDisabled) attr += ' tabindex="-1" aria-disabled="true"'
65
-
66
- const href = isDisabled ? '#' : getPageUrl(page)
67
-
68
- return `<li class="${className}"><a class="page-link" href="${href}"${attr}>${text}</a></li>`
69
- }
70
-
71
- let links = ''
72
-
73
- // Previous Link
74
- links += createItem(currentPage - 1, 'Previous', false, currentPage <= 1)
75
-
76
- // Calculate Window (Start/End)
77
- const half = Math.floor(maxVisible / 2)
78
- let start = currentPage - half
79
- let end = currentPage + half
80
-
81
- if (start < 1) {
82
- start = 1
83
- end = Math.min(totalPages, maxVisible)
84
- }
85
-
86
- if (end > totalPages) {
87
- end = totalPages
88
- start = Math.max(1, totalPages - maxVisible + 1)
89
- }
90
-
91
- // Render Pages with ellipsis
92
- const showFirst = start > 1
93
- const showLast = end < totalPages
94
-
95
- // First page + ellipsis if window doesn't touch start
96
- if (showFirst) {
97
- links += createItem(1, '1', false, false)
98
- if (start > 2) {
99
- links += `<li class="page-item disabled"><span class="page-link">${ellipsis}</span></li>`
100
- }
101
- }
102
-
103
- // Main window loop
104
- for (let i = start; i <= end; i++) {
105
- links += createItem(i, i, i === currentPage, false)
106
- }
107
-
108
- // Last page + ellipsis if window doesn't touch end
109
- if (showLast) {
110
- if (end < totalPages - 1) {
111
- links += `<li class="page-item disabled"><span class="page-link">${ellipsis}</span></li>`
112
- }
113
- links += createItem(totalPages, totalPages, false, false)
114
- }
115
-
116
- // Next Link
117
- links += createItem(currentPage + 1, 'Next', false, currentPage >= totalPages)
118
-
119
- return links
120
- }
121
- }
122
- })
123
- </script>