coralite-plugin-aggregation 0.1.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 +306 -0
- package/changelog.md +88 -0
- package/coralite.config.js +5 -0
- package/eslint.config.js +117 -0
- package/jsconfig.json +8 -0
- package/package.json +51 -0
- package/playwright.config.js +44 -0
- package/src/index.js +373 -0
- package/src/templates/coralite-pagination.html +123 -0
- package/types/index.js +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Coralite Aggregation Plugin
|
|
2
|
+
|
|
3
|
+
The **Coralite Aggregation Plugin** is a powerful tool designed to help developers dynamically collect, filter, sort, and display content across multiple sources within a Coralite project.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
- [Installation](#installation)
|
|
7
|
+
- [Example](#example)
|
|
8
|
+
- [Type definition](#types)
|
|
9
|
+
- [Custom pager](#custom-pager)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Content Aggregation**: Gather content from multiple files or directories using path patterns (e.g., `blog/`, `['products/all','blog/']`).
|
|
16
|
+
- **Filtering & Sorting**: Use metadata-based filters and custom sort functions to refine results based on attributes like tags, categories, dates, or tokens.
|
|
17
|
+
- **Pagination Support**: Easily create paginated views with customizable templates for navigation controls and visible page links.
|
|
18
|
+
- **Token Handling**: Configure token aliases, defaults, and metadata mapping.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Coralite Aggregation Plugin Guide
|
|
23
|
+
|
|
24
|
+
### Installation {#installation}
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install coralite-plugin-aggregation
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Setup Configuration
|
|
31
|
+
First, enable the plugin in your `coralite.config.js`:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
// coralite.config.js
|
|
35
|
+
import aggregation from 'coralite-plugin-aggregation'
|
|
36
|
+
|
|
37
|
+
export default {
|
|
38
|
+
plugins: [aggregation]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> **Note**: The plugin must be imported as `'coralite-plugin-aggregation'` for compatibility with the core Coralite framework.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### Example Implementation {#example}
|
|
47
|
+
|
|
48
|
+
#### Entry Point: Displaying Aggregated Results
|
|
49
|
+
Create a file like `coralite-posts.html` to define your aggregation logic and rendering template:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<!-- templates/coralite-posts.html -->
|
|
53
|
+
<template id="coralite-posts">
|
|
54
|
+
<div>
|
|
55
|
+
{{ posts }}
|
|
56
|
+
<!-- Pagination controls will be injected automatically if enabled. -->
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<script type="module">
|
|
61
|
+
import { defineComponent, aggregation } from 'coralite'
|
|
62
|
+
|
|
63
|
+
export default defineComponent({
|
|
64
|
+
tokens: {
|
|
65
|
+
// Aggregation function returns an array of content items.
|
|
66
|
+
posts() {
|
|
67
|
+
return aggregation({
|
|
68
|
+
path: ['products'], // Source directory.
|
|
69
|
+
template: 'coralite-post', // Template ID for individual items.
|
|
70
|
+
limit: 20, // Maximum number of results per page.
|
|
71
|
+
recursive: true, // Include subdirectories.
|
|
72
|
+
pagination: {
|
|
73
|
+
token: 'post_count', // Page size control token.
|
|
74
|
+
template: 'coralite-pagination', // Template for pagination controls.
|
|
75
|
+
path: 'page', // Infix for paginated URLs (e.g., `page/1`).
|
|
76
|
+
visible: 5 // Max number of visible page links.
|
|
77
|
+
},
|
|
78
|
+
filter(meta) {
|
|
79
|
+
return meta.name === 'category' && meta.content === 'tech'
|
|
80
|
+
},
|
|
81
|
+
sort(a, b) {
|
|
82
|
+
return new Date(b.date) - new Date(a.date)
|
|
83
|
+
},
|
|
84
|
+
tokens: {
|
|
85
|
+
default: {
|
|
86
|
+
author: 'Anonymous',
|
|
87
|
+
category: 'uncategorized'
|
|
88
|
+
},
|
|
89
|
+
aliases: {
|
|
90
|
+
tags: ['tech', 'news', 'tutorial']
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
</script>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Aggregated Content Files
|
|
101
|
+
Each file to be aggregated must include metadata via `<meta>` elements. For example:
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
<!-- pages/products/product-1.html -->
|
|
105
|
+
<!DOCTYPE html>
|
|
106
|
+
<html lang="en">
|
|
107
|
+
<head>
|
|
108
|
+
<meta charset="UTF-8" />
|
|
109
|
+
<meta name="title" content="Great Barrier Reef" />
|
|
110
|
+
<meta name="description" content="The Great Barrier Reef—largest, comprising over 2,900 individual reefs and 900 islands stretching for over 2,600 kilometers." />
|
|
111
|
+
<meta name="price" content="1000" />
|
|
112
|
+
<meta name="published_time" content="2025-01-08T20:23:07.645Z" />
|
|
113
|
+
</head>
|
|
114
|
+
<body>
|
|
115
|
+
<coralite-header>
|
|
116
|
+
<h1>Great Barrier Reef</h1>
|
|
117
|
+
</coralite-header>
|
|
118
|
+
<coralite-author name="Nemo" datetime="2025-01-08T20:23:07.645Z"></coralite-author>
|
|
119
|
+
</body>
|
|
120
|
+
</html>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Single Result Template
|
|
124
|
+
Define a `<template>` element for rendering individual items:
|
|
125
|
+
|
|
126
|
+
```html
|
|
127
|
+
<!-- templates/coralite-post.html -->
|
|
128
|
+
<template id="coralite-post">
|
|
129
|
+
<div class="post-item">
|
|
130
|
+
<h2>{{ $title }}</h2>
|
|
131
|
+
<p>{{ $description }} - {{ formattedPrice }}</p>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
|
|
135
|
+
<script type="module">
|
|
136
|
+
import { defineComponent } from 'coralite'
|
|
137
|
+
|
|
138
|
+
export default defineComponent({
|
|
139
|
+
tokens: {
|
|
140
|
+
// Custom token to format prices using Intl.NumberFormat.
|
|
141
|
+
formattedPrice(values) {
|
|
142
|
+
return new Intl.NumberFormat("en-AU", { style: "currency", currency: "AUD" }).format(
|
|
143
|
+
values.$price
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
</script>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
> **Token Syntax**: Metadata attributes are accessed in templates as `$<meta name>`. For example, the `<meta name="title">` element is referenced using `$title`.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
### Configuration Options {#types}
|
|
155
|
+
|
|
156
|
+
#### `CoraliteAggregate` {#coralite-aggregate}
|
|
157
|
+
Configuration object for content aggregation processes.
|
|
158
|
+
|
|
159
|
+
| Property | Type | Description | Reference |
|
|
160
|
+
|-----------------|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|-----------|
|
|
161
|
+
| `path` | `string[]` | Array of paths relative to the pages directory (e.g., `['products', 'blog/']`). | - |
|
|
162
|
+
| `template` | `CoraliteAggregateTemplate` or `string` | Templates used to display the result. Must match an existing `<template>` element by ID. | [CoraliteAggregateTemplate](#coralite-aggregate-template) |
|
|
163
|
+
| `pagination` | `Object` | Pagination settings (optional). | - |
|
|
164
|
+
| `filter` | [`CoraliteAggregateFilter`](#coralite-aggregate-filter) | Callback to filter out unwanted elements from the aggregated content. | [CoraliteAggregateFilter](#coralite-aggregate-filter) |
|
|
165
|
+
| `recursive` | `boolean` | Whether to recursively search subdirectories. | - |
|
|
166
|
+
| `tokens` | [`CoraliteTokenOptions`](#coralite-token-options) | Token configuration options (optional). | [CoraliteTokenOptions](#coralite-token-options) |
|
|
167
|
+
| `sort` | [`CoraliteAggregateSort`](#coralite-aggregate-sort) | Sort aggregated pages. | [CoraliteAggregateSort](#coralite-aggregate-sort) |
|
|
168
|
+
| `limit` | `number` | Maximum number of results to retrieve (used with pagination). | - |
|
|
169
|
+
| `offset` | `number` | Starting index for the results list (used with pagination). | - |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
#### `CoraliteAggregateTemplate` {#coralite-aggregate-template}
|
|
174
|
+
Configuration for templates used to render aggregated results.
|
|
175
|
+
|
|
176
|
+
| Property | Type | Description | Reference |
|
|
177
|
+
|----------|-----------|-----------------------------------------------------------------------------|-----------|
|
|
178
|
+
| `item` | `string` | Unique identifier for the component used for each document (e.g., `'coralite-post'`). | - |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
#### `CoraliteTokenOptions` {#coralite-token-options}
|
|
183
|
+
Configuration options for token handling during processing.
|
|
184
|
+
|
|
185
|
+
| Property | Type | Description |
|
|
186
|
+
|--------------|-------------------------------------------------------|-----------------------------------------------------------------------------|
|
|
187
|
+
| `default` | `Object.<string, string>` | Default token values for properties not explicitly set (e.g., `{ author: 'Anonymous' }`). |
|
|
188
|
+
| `aliases` | `Object.<string, string[]>` | Token aliases and their possible values (e.g., `{ tags: ['tech', 'news'] }`). |
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
#### `CoraliteAggregateFilter` {#coralite-aggregate-filter}
|
|
193
|
+
Callback function for filtering aggregated content based on metadata.
|
|
194
|
+
|
|
195
|
+
| Parameter | Type | Description |
|
|
196
|
+
|---------------|-------------------------------------|-----------------------------------------------------------------------------|
|
|
197
|
+
| `metadata` | [`CoraliteToken`](#coralite-token) | Aggregated HTML page metadata (e.g., `{ name: 'category', content: 'tech' }`). |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
#### `CoraliteAggregateSort` {#coralite-aggregate-sort}
|
|
202
|
+
Callback function for sorting aggregated results based on metadata.
|
|
203
|
+
|
|
204
|
+
| Parameter | Type | Description |
|
|
205
|
+
|-----------|-------------------------------------|-----------------------------------------------------------------------------|
|
|
206
|
+
| `a` | `Object.<string, string>` | Metadata of the first item being compared (e.g., `{ date: '2025-01-08' }`). |
|
|
207
|
+
| `b` | `Object.<string, string>` | Metadata of the second item being compared. |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
#### `CoraliteToken` {#coralite-token}
|
|
212
|
+
A representation of a token with name and value.
|
|
213
|
+
|
|
214
|
+
| Property | Type | Description |
|
|
215
|
+
|----------|--------|-----------------------------------------------------------------------------|
|
|
216
|
+
| `name` | `string` | Token identifier (e.g., `'title'`, `'category'`). |
|
|
217
|
+
| `content`| `string` | Token value or content (e.g., `'Great Barrier Reef'`, `'tech'`). |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Custom Pager Template User Guide for Coralite Pagination Component {#custom-pager}
|
|
222
|
+
|
|
223
|
+
This guide explains how to create a custom pagination template using the existing `coralite-pagination` component as a reference. The goal is to define a new pager layout below the default implementation, preserving compatibility with the core logic while enabling customization.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
### Create a New Template Element
|
|
228
|
+
Define a unique `<template>` element for your custom pager. Use an ID distinct from the default (`coralite-pagination`) to avoid conflicts:
|
|
229
|
+
|
|
230
|
+
```html
|
|
231
|
+
<template id="coralite-pagination-custom">
|
|
232
|
+
{{ pagination_list }}
|
|
233
|
+
</template>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### Implement Custom Logic in `<script type="module">`
|
|
239
|
+
Replace or extend the `pagination_list` token function with your custom logic. The core structure remains compatible with Coralite’s API, but you can modify rendering rules (e.g., ellipsis behavior, link formatting).
|
|
240
|
+
|
|
241
|
+
#### Example: Basic Custom Token Function
|
|
242
|
+
```javascript
|
|
243
|
+
<script type="module">
|
|
244
|
+
import { defineComponent } from 'coralite'
|
|
245
|
+
|
|
246
|
+
export default defineComponent({
|
|
247
|
+
tokens: {
|
|
248
|
+
/**
|
|
249
|
+
* @param {Object} values
|
|
250
|
+
* @param {string} values.pagination_visible - Max number of visible pages items.
|
|
251
|
+
* @param {string} values.pagination_offset - Number of items to skip from the beginning (offset for pagination)
|
|
252
|
+
* @param {string} values.pagination_index - Base URL path for generating pagination links
|
|
253
|
+
* @param {string} values.pagination_dirname - Directory path component for routing context
|
|
254
|
+
* @param {string} values.pagination_length - Total number of items across all pages
|
|
255
|
+
* @param {string} values.pagination_current - Currently active page number or identifier
|
|
256
|
+
*/
|
|
257
|
+
pagination_list (values) {
|
|
258
|
+
const length = parseInt(values.pagination_length)
|
|
259
|
+
if (!length) return ''
|
|
260
|
+
|
|
261
|
+
// Custom logic: render a simplified pager with only previous/next and current page
|
|
262
|
+
const currentPage = parseInt(values.pagination_current)
|
|
263
|
+
const dirname = values.pagination_dirname[0] === '/' ? values.pagination_dirname : '/' + values.pagination_dirname
|
|
264
|
+
|
|
265
|
+
let html = '<ul class="pagination">'
|
|
266
|
+
|
|
267
|
+
// Previous link
|
|
268
|
+
if (currentPage > 1) {
|
|
269
|
+
html += `<li class="page-item"><a class="page-link" href="${dirname}/${currentPage - 1}.html">Previous</a></li>`
|
|
270
|
+
} else {
|
|
271
|
+
html += '<li class="page-item disabled"><span class="page-link">Previous</span></li>'
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Current page
|
|
275
|
+
html += `<li class="page-item active"><span class="page-link">${currentPage}</span></li>`
|
|
276
|
+
|
|
277
|
+
// Next link
|
|
278
|
+
if (currentPage < length) {
|
|
279
|
+
html += `<li class="page-item"><a class="page-link" href="${dirname}/${currentPage + 1}.html">Next</a></li>`
|
|
280
|
+
} else {
|
|
281
|
+
html += '<li class="page-item disabled"><span class="page-link">Next</span></li>'
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
html += '</ul>'
|
|
285
|
+
return html
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
</script>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### Key Parameters
|
|
295
|
+
The `pagination_list` token function receives these critical parameters from Coralite:
|
|
296
|
+
|
|
297
|
+
| Parameter | Description |
|
|
298
|
+
|-----------------------|-----------------------------------------------------------------------------|
|
|
299
|
+
| `values.pagination_length` | Total number of items across all pages (used to determine page count). |
|
|
300
|
+
| `values.pagination_current` | Currently active page number. |
|
|
301
|
+
| `values.pagination_dirname` | Base directory path for routing context (e.g., `/blog`). |
|
|
302
|
+
| `values.pagination_index` | Base URL path for pagination links (e.g., `/blog/index.html`). |
|
|
303
|
+
|
|
304
|
+
> **Note:** Avoid hardcoding values like `length`, `currentPage`, or `dirname`. Use the parameters provided by Coralite to ensure compatibility.
|
|
305
|
+
|
|
306
|
+
---
|
package/changelog.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 🎁 Complete Release History
|
|
2
|
+
|
|
3
|
+
## Initial Release: `v0.1.0`
|
|
4
|
+
|
|
5
|
+
### Initial Commits
|
|
6
|
+
|
|
7
|
+
- 2b0f1dc (HEAD -> main, tag: v0.1.0, origin/main) feat: Implement dynamic pagination with visible items and ellipsis - ([Thomas David](https://codeberg.org/tjdavid))
|
|
8
|
+
- 6b4d75f test: add fixtures to cover visibility feature - ([Thomas David](https://codeberg.org/tjdavid))
|
|
9
|
+
- 70f5a4b add npm ignore list - ([Thomas David](https://codeberg.org/tjdavid))
|
|
10
|
+
- 48cf1e0 docs: Add custom pager template user guide - ([Thomas David](https://codeberg.org/tjdavid))
|
|
11
|
+
- 065ff1a feat(pagination): improve pagination handling with dynamic path and adjusted length calculation - ([Thomas David](https://codeberg.org/tjdavid))
|
|
12
|
+
- ea200fe types: add visible property to pagination configuration - ([Thomas David](https://codeberg.org/tjdavid))
|
|
13
|
+
- 4f68032 chore: update pnpm, coralite peer dep and rename build script - ([Thomas David](https://codeberg.org/tjdavid))
|
|
14
|
+
- 1a4ebc2 docs: add readme - ([Thomas David](https://codeberg.org/tjdavid))
|
|
15
|
+
- 762a507 fix: correct pagination length calculation and offset generation - ([Thomas David](https://codeberg.org/tjdavid))
|
|
16
|
+
Adjust pagination logic to use calculated `paginationLength` for determining
|
|
17
|
+
page counts and offsets, ensuring accurate rendering of paginated content.
|
|
18
|
+
|
|
19
|
+
- 74ba4a0 fix: simplify page limit handling by removing array check and using offset correctly - ([Thomas David](https://codeberg.org/tjdavid))
|
|
20
|
+
- 2cf82e6 refactor: Use tokens instead of values in component definitions - ([Thomas David](https://codeberg.org/tjdavid))
|
|
21
|
+
- b5fde9d fix: fix previous navigation link generation in pagination - ([Thomas David](https://codeberg.org/tjdavid))
|
|
22
|
+
Correctly adjust the "Previous" link href based on current index to ensure proper
|
|
23
|
+
navigation when on pages beyond the second one. The fix updates the logic to
|
|
24
|
+
reference the correct page number for the previous item, resolving invalid
|
|
25
|
+
navigation links in multi-page scenarios.
|
|
26
|
+
|
|
27
|
+
- Removed hardcoded index increments
|
|
28
|
+
- Added dynamic calculation based on current page position
|
|
29
|
+
|
|
30
|
+
- 8b50d9d fix(pagination): use correct length value and adjust skip condition - ([Thomas David](https://codeberg.org/tjdavid))
|
|
31
|
+
- 4879d44 refactor: rename values to tokens in pagination_list - ([Thomas David](https://codeberg.org/tjdavid))
|
|
32
|
+
- 3b06c3c chore: upgrade coralite to 0.11.1 - ([Thomas David](https://codeberg.org/tjdavid))
|
|
33
|
+
- cfa07cb chore: update coralite to 0.10.0 - ([Thomas David](https://codeberg.org/tjdavid))
|
|
34
|
+
- 320f665 chore: add "type": "module" to package.json - ([Thomas David](https://codeberg.org/tjdavid))
|
|
35
|
+
- 0ad0f60 chore: update repository and issues URLs in package.json - ([Thomas David](https://codeberg.org/tjdavid))
|
|
36
|
+
- 679b3cf fix: use import.meta.dirname for template path - ([Thomas David](https://codeberg.org/tjdavid))
|
|
37
|
+
- 093b1ea fix: ensure context scope for new components - ([Thomas David](https://codeberg.org/tjdavid))
|
|
38
|
+
- f118211 refactor: update metadata processing to use name and content in filter - ([Thomas David](https://codeberg.org/tjdavid))
|
|
39
|
+
- 8d2a0e4 refactor: ensure consistent path formatting for pagination links - ([Thomas David](https://codeberg.org/tjdavid))
|
|
40
|
+
- 58bd08f fix: update pagination logic to skip when no directory name - ([Thomas David](https://codeberg.org/tjdavid))
|
|
41
|
+
- 7af1188 fix: token name spacing conflict - ([Thomas David](https://codeberg.org/tjdavid))
|
|
42
|
+
- 27a448a types: update CoraliteAggregate type to use string[] for path and add pagination.id - ([Thomas David](https://codeberg.org/tjdavid))
|
|
43
|
+
- 26b8b9a refactor: remove webServer config and update type imports in index.js - ([Thomas David](https://codeberg.org/tjdavid))
|
|
44
|
+
- 5fdc416 refactor(test): update posts aggregation parameters and add pagination support - ([Thomas David](https://codeberg.org/tjdavid))
|
|
45
|
+
Refactor the posts aggregation logic to use `limit` instead of `pages`, split path into array,
|
|
46
|
+
and add optional pagination configuration. This improves consistency and enables pagination.
|
|
47
|
+
|
|
48
|
+
BREAKING CHANGE: The `pages` parameter has been replaced with `limit`. Existing code using `pages`
|
|
49
|
+
may need adjustment to work with this change.
|
|
50
|
+
|
|
51
|
+
- 1a1d9af chore: remove unused blog index.html test fixture - ([Thomas David](https://codeberg.org/tjdavid))
|
|
52
|
+
- 398b775 refactor: wrap post title in link and remove coralite-header - ([Thomas David](https://codeberg.org/tjdavid))
|
|
53
|
+
- 12f9e54 tests: add blog posts fixtures - ([Thomas David](https://codeberg.org/tjdavid))
|
|
54
|
+
- 314b7e6 test: add index.html test fixture for paginated blog posts - ([Thomas David](https://codeberg.org/tjdavid))
|
|
55
|
+
- 19eb540 feat: add default coralite-pagination template - ([Thomas David](https://codeberg.org/tjdavid))
|
|
56
|
+
- 165ec10 fix(pagination): correct path handling and context value processing - ([Thomas David](https://codeberg.org/tjdavid))
|
|
57
|
+
Use context.document.path instead of document.path to ensure accurate directory resolution.
|
|
58
|
+
Introduce processed flag to prevent redundant pagination value computation.
|
|
59
|
+
Update pagination template rendering to use context-aware values.
|
|
60
|
+
|
|
61
|
+
BREAKING CHANGE: Pagination template behavior may change due to updated value context
|
|
62
|
+
|
|
63
|
+
- 8c0ea5a refactor: use context values and document in createComponent - ([Thomas David](https://codeberg.org/tjdavid))
|
|
64
|
+
- 59bd962 chore: remove html meta data processing logic - ([Thomas David](https://codeberg.org/tjdavid))
|
|
65
|
+
- 78b3bb0 fix: use context values - ([Thomas David](https://codeberg.org/tjdavid))
|
|
66
|
+
- f2c32d1 feat: support multiple aggregate paths in plugin method - ([Thomas David](https://codeberg.org/tjdavid))
|
|
67
|
+
- 5703d63 refactor: use page.result.meta instead of parsing HTML meta tags - ([Thomas David](https://codeberg.org/tjdavid))
|
|
68
|
+
- 7dbcc2c fix: handle excluding self reference in aggregation - ([Thomas David](https://codeberg.org/tjdavid))
|
|
69
|
+
- ca2f228 feat(test): Update blog posts test to exclude current page and adjust component structure - ([Thomas David](https://codeberg.org/tjdavid))
|
|
70
|
+
- 487a0b0 chore: Update script paths, pnpm version, and move coralite to peerDependencies - ([Thomas David](https://codeberg.org/tjdavid))
|
|
71
|
+
- 86af8d5 chore: update package metadata - ([Thomas David](https://codeberg.org/tjdavid))
|
|
72
|
+
- 6e9d733 chore: add jsconfig.json for TypeScript configuration - ([Thomas David](https://codeberg.org/tjdavid))
|
|
73
|
+
- 4b1fd18 build: add Playwright E2E tests and update build tools - ([Thomas David](https://codeberg.org/tjdavid))
|
|
74
|
+
- 1a4e8a0 chore: add coralite config to enable aggregation plugin - ([Thomas David](https://codeberg.org/tjdavid))
|
|
75
|
+
- 85be5fd fix: Update coralite import path to utils module - ([Thomas David](https://codeberg.org/tjdavid))
|
|
76
|
+
- 1a46a51 chore: update .gitignore with additional patterns - ([Thomas David](https://codeberg.org/tjdavid))
|
|
77
|
+
|
|
78
|
+
### Metadata
|
|
79
|
+
```
|
|
80
|
+
First version ------- v0.1.0
|
|
81
|
+
Total commits ------- 47
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Summary
|
|
85
|
+
```
|
|
86
|
+
Total releases ------ 1
|
|
87
|
+
Total commits ------- 47
|
|
88
|
+
```
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import stylisticJs from '@stylistic/eslint-plugin-js'
|
|
2
|
+
import stylisticPlus from '@stylistic/eslint-plugin-plus'
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
{
|
|
6
|
+
plugins: {
|
|
7
|
+
'@stylistic/js': stylisticJs,
|
|
8
|
+
'@stylistic/plus': stylisticPlus
|
|
9
|
+
},
|
|
10
|
+
rules: {
|
|
11
|
+
'@stylistic/plus/curly-newline': ['error', 'always'],
|
|
12
|
+
'@stylistic/js/indent': [
|
|
13
|
+
'error', 2, {
|
|
14
|
+
SwitchCase: 1,
|
|
15
|
+
VariableDeclarator: 1,
|
|
16
|
+
outerIIFEBody: 1,
|
|
17
|
+
MemberExpression: 1,
|
|
18
|
+
FunctionExpression: {
|
|
19
|
+
body: 1,
|
|
20
|
+
parameters: 1
|
|
21
|
+
},
|
|
22
|
+
CallExpression: {
|
|
23
|
+
arguments: 1
|
|
24
|
+
},
|
|
25
|
+
ArrayExpression: 1,
|
|
26
|
+
ObjectExpression: 1,
|
|
27
|
+
ImportDeclaration: 1,
|
|
28
|
+
flatTernaryExpressions: true,
|
|
29
|
+
ignoreComments: false
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
'@stylistic/js/object-curly-spacing': ['error', 'always'],
|
|
33
|
+
'@stylistic/js/object-property-newline': 'error',
|
|
34
|
+
'@stylistic/js/object-curly-newline': ['error', {
|
|
35
|
+
ObjectExpression: {
|
|
36
|
+
multiline: true,
|
|
37
|
+
consistent: true
|
|
38
|
+
},
|
|
39
|
+
ObjectPattern: {
|
|
40
|
+
multiline: true,
|
|
41
|
+
consistent: true
|
|
42
|
+
},
|
|
43
|
+
ImportDeclaration: {
|
|
44
|
+
multiline: true,
|
|
45
|
+
consistent: true
|
|
46
|
+
},
|
|
47
|
+
ExportDeclaration: {
|
|
48
|
+
multiline: true,
|
|
49
|
+
consistent: true
|
|
50
|
+
}
|
|
51
|
+
}],
|
|
52
|
+
'@stylistic/js/quote-props': ['error', 'as-needed'],
|
|
53
|
+
'@stylistic/js/space-before-function-paren': ['error', 'always'],
|
|
54
|
+
'@stylistic/js/function-call-spacing': ['error', 'never'],
|
|
55
|
+
'@stylistic/js/implicit-arrow-linebreak': ['error', 'beside'],
|
|
56
|
+
'@stylistic/js/eol-last': ['error', 'always'],
|
|
57
|
+
'@stylistic/js/brace-style': [
|
|
58
|
+
'error', '1tbs', {
|
|
59
|
+
allowSingleLine: false
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
'@stylistic/js/semi': ['error', 'never'],
|
|
63
|
+
'@stylistic/js/quotes': [
|
|
64
|
+
'error', 'single', {
|
|
65
|
+
avoidEscape: true,
|
|
66
|
+
allowTemplateLiterals: true
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
'@stylistic/js/comma-dangle': ['error', 'never'],
|
|
70
|
+
'@stylistic/js/comma-spacing': [
|
|
71
|
+
'error', {
|
|
72
|
+
before: false,
|
|
73
|
+
after: true
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
'@stylistic/js/comma-style': ['error', 'last'],
|
|
77
|
+
'@stylistic/js/array-bracket-spacing': ['error', 'never'],
|
|
78
|
+
'@stylistic/js/array-bracket-newline': ['error', 'consistent'],
|
|
79
|
+
'@stylistic/js/array-element-newline': ['error', 'consistent'],
|
|
80
|
+
'@stylistic/js/computed-property-spacing': ['error', 'never'],
|
|
81
|
+
'@stylistic/js/no-mixed-operators': [
|
|
82
|
+
'error',
|
|
83
|
+
{
|
|
84
|
+
groups: [
|
|
85
|
+
[
|
|
86
|
+
'+', '-', '*', '/', '%', '**'
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
'&', '|', '^', '~', '<<', '>>', '>>>'
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
'==', '!=', '===', '!==', '>', '>=', '<', '<='
|
|
93
|
+
],
|
|
94
|
+
['&&', '||'],
|
|
95
|
+
['in', 'instanceof']
|
|
96
|
+
],
|
|
97
|
+
allowSamePrecedence: false
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
'@stylistic/js/key-spacing': [
|
|
101
|
+
'error', {
|
|
102
|
+
mode: 'strict'
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
'@stylistic/js/no-trailing-spaces': 'error',
|
|
106
|
+
'@stylistic/js/no-multi-spaces': 'error',
|
|
107
|
+
'@stylistic/js/no-confusing-arrow': 'error'
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
ignores: [
|
|
112
|
+
'**/dist/',
|
|
113
|
+
'**/.history/',
|
|
114
|
+
'**/playwright-report/'
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
]
|
package/jsconfig.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coralite-plugin-aggregation",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Build database free coralite websites",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "coralite -t tests/fixtures/templates -p tests/fixtures/pages -o dist",
|
|
7
|
+
"test-e2e": "playwright test",
|
|
8
|
+
"test-e2e-report": "playwright show-report",
|
|
9
|
+
"test-e2e-ui": "playwright test --ui",
|
|
10
|
+
"server": "sirv dist --dev --port 3000"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"keywords": [],
|
|
14
|
+
"homepage": "https://coralite.io",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Thomas David",
|
|
17
|
+
"url": "https://thomasjackdavid.com"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://codeberg.org/tjdavid/coralite-plugin-aggregation.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://codeberg.org/tjdavid/coralite-plugin-aggregation/issues"
|
|
25
|
+
},
|
|
26
|
+
"imports": {
|
|
27
|
+
"#types": "./types/index.js"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"default": "./src/index.js",
|
|
32
|
+
"types": "./types/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"license": "AGPL-3.0-only",
|
|
36
|
+
"packageManager": "pnpm@10.12.1",
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@playwright/test": "^1.52.0",
|
|
39
|
+
"@stylistic/eslint-plugin-js": "^4.2.0",
|
|
40
|
+
"@stylistic/eslint-plugin-plus": "^4.2.0",
|
|
41
|
+
"coralite": "^0.11.1",
|
|
42
|
+
"sirv-cli": "^3.0.1"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"dom-serializer": "^2.0.0",
|
|
46
|
+
"htmlparser2": "^10.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"coralite": "^0.11.2"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read environment variables from file.
|
|
6
|
+
* https://github.com/motdotla/dotenv
|
|
7
|
+
*/
|
|
8
|
+
// import dotenv from 'dotenv';
|
|
9
|
+
// import path from 'path';
|
|
10
|
+
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @see https://playwright.dev/docs/test-configuration
|
|
14
|
+
*/
|
|
15
|
+
export default defineConfig({
|
|
16
|
+
testDir: './tests/e2e',
|
|
17
|
+
/* Run tests in files in parallel */
|
|
18
|
+
fullyParallel: true,
|
|
19
|
+
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
20
|
+
forbidOnly: !!process.env.CI,
|
|
21
|
+
/* Retry on CI only */
|
|
22
|
+
retries: process.env.CI ? 2 : 0,
|
|
23
|
+
/* Opt out of parallel tests on CI. */
|
|
24
|
+
workers: process.env.CI ? 1 : undefined,
|
|
25
|
+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
26
|
+
reporter: 'html',
|
|
27
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
28
|
+
use: {
|
|
29
|
+
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
30
|
+
baseURL: 'http://localhost:3000',
|
|
31
|
+
|
|
32
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
33
|
+
trace: 'on-first-retry'
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/* Configure projects for major browsers */
|
|
37
|
+
projects: [
|
|
38
|
+
{
|
|
39
|
+
name: 'firefox',
|
|
40
|
+
use: { ...devices['Desktop Firefox'] }
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
})
|
|
44
|
+
|
package/src/index.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { Parser } from 'htmlparser2'
|
|
4
|
+
import {
|
|
5
|
+
getHtmlFiles,
|
|
6
|
+
createElement,
|
|
7
|
+
createTextNode,
|
|
8
|
+
createPlugin,
|
|
9
|
+
} from 'coralite/utils'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @import {CoraliteAnyNode, CoraliteCollectionItem, CoraliteContentNode, CoraliteDocumentRoot} from 'coralite/types'
|
|
13
|
+
* @import {CoraliteAggregate} from '#types'
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export default createPlugin({
|
|
17
|
+
name: 'aggregation',
|
|
18
|
+
/**
|
|
19
|
+
* Aggregates HTML content from specified paths into a single collection of components.
|
|
20
|
+
*
|
|
21
|
+
* @param {CoraliteAggregate} options - Configuration object defining the aggregation behavior
|
|
22
|
+
*
|
|
23
|
+
* @returns {Promise<CoraliteAnyNode[]>} Array of processed content nodes from aggregated documents
|
|
24
|
+
* @throws {Error} If pages directory path is undefined or aggregate path doesn't exist
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
async method (options, context) {
|
|
28
|
+
let templateId
|
|
29
|
+
|
|
30
|
+
// Determine template component ID from configuration
|
|
31
|
+
if (typeof options.template === 'string') {
|
|
32
|
+
templateId = options.template
|
|
33
|
+
} else if (typeof options.template === 'object') {
|
|
34
|
+
templateId = options.template.item
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!templateId) {
|
|
38
|
+
/** @TODO Refer to documentation */
|
|
39
|
+
throw new Error('Aggregate template was undefined')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @type {CoraliteCollectionItem[]} */
|
|
43
|
+
let pages = []
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < options.path.length; i++) {
|
|
46
|
+
let path = options.path[i];
|
|
47
|
+
const dirname = join(context.path.pages, path)
|
|
48
|
+
|
|
49
|
+
if (!existsSync(dirname)) {
|
|
50
|
+
/** @TODO Refer to documentation */
|
|
51
|
+
throw new Error('Aggregate path does not exist: "' + dirname + '"')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (path[0] !== '/') {
|
|
55
|
+
path = '/' + path
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cachePages = this.pages.getListByPath(path)
|
|
59
|
+
|
|
60
|
+
if (!cachePages || !cachePages.length) {
|
|
61
|
+
// Retrieve HTML pages from specified path
|
|
62
|
+
const collection = await getHtmlFiles({
|
|
63
|
+
type: 'page',
|
|
64
|
+
path: dirname
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (!collection.list.length) {
|
|
68
|
+
throw new Error('Aggregation found no documents in "' + dirname + '"')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pages = pages.concat(collection.list)
|
|
72
|
+
} else {
|
|
73
|
+
pages = pages.concat(cachePages)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let result = []
|
|
78
|
+
let startIndex = 0
|
|
79
|
+
let endIndex = pages.length
|
|
80
|
+
let paginationOffset = context.values.pagination_offset
|
|
81
|
+
|
|
82
|
+
// Sort results based on custom sort function
|
|
83
|
+
if (typeof options.sort === 'function') {
|
|
84
|
+
pages.sort((a, b) => {
|
|
85
|
+
const metaA = a.result.meta
|
|
86
|
+
const metaB = b.result.meta
|
|
87
|
+
|
|
88
|
+
return options.sort(metaA, metaB)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof options.filter === 'function') {
|
|
93
|
+
const filteredPages = []
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < pages.length; i++) {
|
|
96
|
+
const page = pages[i]
|
|
97
|
+
const metadata = page.result.values
|
|
98
|
+
let keepItem = false
|
|
99
|
+
|
|
100
|
+
// Process metadata and populate token values for rendering
|
|
101
|
+
for (const key in metadata) {
|
|
102
|
+
if (Object.prototype.hasOwnProperty.call(metadata, key)) {
|
|
103
|
+
const data = metadata[key]
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(data)) {
|
|
106
|
+
for (let i = 0; i < data.length; i++) {
|
|
107
|
+
|
|
108
|
+
if (!keepItem) {
|
|
109
|
+
keepItem = options.filter({ name: key, content: data[i] })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Handle single metadata item
|
|
114
|
+
if (!keepItem) {
|
|
115
|
+
keepItem = options.filter({
|
|
116
|
+
name: key,
|
|
117
|
+
content: data
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (keepItem) {
|
|
125
|
+
filteredPages.push(page)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pages = filteredPages
|
|
130
|
+
endIndex = pages.length
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Apply page offset
|
|
134
|
+
if (Object.prototype.hasOwnProperty.call(options, 'offset') || paginationOffset != null) {
|
|
135
|
+
let offset = paginationOffset || options.offset
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(offset)) {
|
|
138
|
+
if (typeof offset === 'string') {
|
|
139
|
+
offset = parseInt(offset)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (offset > endIndex) {
|
|
143
|
+
startIndex = endIndex
|
|
144
|
+
} else {
|
|
145
|
+
startIndex = offset
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// apply page limit
|
|
151
|
+
let limit
|
|
152
|
+
if (options.limit) {
|
|
153
|
+
limit = options.limit
|
|
154
|
+
|
|
155
|
+
if (typeof limit === 'string') {
|
|
156
|
+
limit = parseInt(limit)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const limitOffset = limit + startIndex
|
|
160
|
+
|
|
161
|
+
if (limitOffset < endIndex) {
|
|
162
|
+
endIndex = limitOffset
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let pageLength = pages.length
|
|
167
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
168
|
+
let page = pages[i]
|
|
169
|
+
|
|
170
|
+
if (page.path.filename === context.document.path.filename) {
|
|
171
|
+
// skip to next page
|
|
172
|
+
page = pages[++i]
|
|
173
|
+
pageLength--
|
|
174
|
+
|
|
175
|
+
if (!page) {
|
|
176
|
+
// exit loop
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// render component with current values and add to results
|
|
182
|
+
const component = await this.createComponent({
|
|
183
|
+
id: templateId,
|
|
184
|
+
values: { ...context.values, ...page.result.values },
|
|
185
|
+
document: context.document,
|
|
186
|
+
contextId: context.id + i + templateId
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
if (typeof component === 'object') {
|
|
190
|
+
// concat rendered components
|
|
191
|
+
result = result.concat(component.children)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (options.pagination) {
|
|
196
|
+
const pagination = options.pagination
|
|
197
|
+
const paginationPath = context.values.pagination_path || pagination.path || 'page'
|
|
198
|
+
const paginationLength = Math.floor(pageLength / limit)
|
|
199
|
+
let processed = context.values.pagination_processed
|
|
200
|
+
|
|
201
|
+
if (!processed && paginationLength) {
|
|
202
|
+
const path = context.document.path
|
|
203
|
+
const nameSplit = context.document.path.filename.split('.')
|
|
204
|
+
const length = nameSplit.length - 1
|
|
205
|
+
let name = ''
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < length; i++) {
|
|
208
|
+
name += nameSplit[i]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (name === 'index') {
|
|
212
|
+
name = ''
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// @ts-ignore
|
|
216
|
+
let dirname = join(path.dirname, name, paginationPath)
|
|
217
|
+
let pageIndex = path.pathname
|
|
218
|
+
const indexPage = this.pages.getItem(path.pathname)
|
|
219
|
+
const maxVisiblePages = (pagination.visible || paginationLength).toString()
|
|
220
|
+
|
|
221
|
+
// check if we are currently not on a pager page
|
|
222
|
+
if (context.values.pagination_pager_dirname == null) {
|
|
223
|
+
for (let i = 1; i < paginationLength; i++) {
|
|
224
|
+
const currentPageIndex = i + 1
|
|
225
|
+
const filename = currentPageIndex + '.html'
|
|
226
|
+
const pathname = join(dirname, filename)
|
|
227
|
+
const contextId = pathname + context.id.substring(path.pathname.length)
|
|
228
|
+
|
|
229
|
+
// store global values for pagination page
|
|
230
|
+
this.values[contextId] = {
|
|
231
|
+
pagination_path: paginationPath,
|
|
232
|
+
pagination_visible: maxVisiblePages,
|
|
233
|
+
pagination_processed: 'true',
|
|
234
|
+
pagination_offset: (endIndex * i).toString(),
|
|
235
|
+
pagination_index: path.pathname,
|
|
236
|
+
pagination_dirname: dirname,
|
|
237
|
+
pagination_length: paginationLength.toString(),
|
|
238
|
+
pagination_current: currentPageIndex.toString(),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// add pagination page to render queue
|
|
242
|
+
this.addRenderQueue({
|
|
243
|
+
values: {
|
|
244
|
+
pagination_pager_dirname: path.dirname,
|
|
245
|
+
pagination_pager_index: pageIndex,
|
|
246
|
+
},
|
|
247
|
+
path: {
|
|
248
|
+
dirname,
|
|
249
|
+
pathname,
|
|
250
|
+
filename
|
|
251
|
+
},
|
|
252
|
+
content: indexPage.content
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
// @ts-ignore
|
|
257
|
+
dirname = join(context.values.pagination_pager_dirname, paginationPath)
|
|
258
|
+
// @ts-ignore
|
|
259
|
+
pageIndex = context.values.pagination_pager_index
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// store pagination values for current page
|
|
263
|
+
context.values = {
|
|
264
|
+
...context.values,
|
|
265
|
+
pagination_path: paginationPath,
|
|
266
|
+
pagination_visible: maxVisiblePages,
|
|
267
|
+
pagination_processed: 'true',
|
|
268
|
+
pagination_offset: endIndex.toString(),
|
|
269
|
+
pagination_index: pageIndex,
|
|
270
|
+
pagination_dirname: dirname,
|
|
271
|
+
pagination_length: paginationLength.toString(),
|
|
272
|
+
pagination_current: '1'
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const contextId = context.id
|
|
277
|
+
let values = this.values[contextId]
|
|
278
|
+
|
|
279
|
+
if (!values || !processed) {
|
|
280
|
+
values = context.values
|
|
281
|
+
this.values[contextId] = values
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const templateId = pagination.template || 'coralite-pagination'
|
|
285
|
+
|
|
286
|
+
const component = await this.createComponent({
|
|
287
|
+
id: templateId,
|
|
288
|
+
values,
|
|
289
|
+
document: context.document,
|
|
290
|
+
contextId: contextId + templateId
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
if (typeof component === 'object') {
|
|
294
|
+
result = result.concat(component.children)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return result
|
|
299
|
+
},
|
|
300
|
+
templates: [join(import.meta.dirname, 'templates/coralite-pagination.html')]
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Parse HTML content and return a CoraliteDocument object representing the parsed document structure
|
|
305
|
+
*
|
|
306
|
+
* @param {string} string - The HTML content to parse. This should be a valid HTML string input.
|
|
307
|
+
* @param {Object} pagination - Configuration object for pagination templates
|
|
308
|
+
* @param {string} pagination.template - The template tag name used to identify pagination elements
|
|
309
|
+
* @param {Object.<string, string>} pagination.attributes - Additional attributes to merge into the template element
|
|
310
|
+
* @example parsePagination('<pagination-element></pagination-element>', {
|
|
311
|
+
* template: 'pagination-element',
|
|
312
|
+
* attributes: { class: 'custom-pagination' }
|
|
313
|
+
* })
|
|
314
|
+
*/
|
|
315
|
+
export function parsePagination (string, meta) {
|
|
316
|
+
// root element reference
|
|
317
|
+
/** @type {CoraliteDocumentRoot} */
|
|
318
|
+
const root = {
|
|
319
|
+
type: 'root',
|
|
320
|
+
children: []
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// stack to keep track of current element hierarchy
|
|
324
|
+
/** @type {CoraliteContentNode[]} */
|
|
325
|
+
const stack = [root]
|
|
326
|
+
const parser = new Parser({
|
|
327
|
+
onprocessinginstruction (name, data) {
|
|
328
|
+
root.children.push({
|
|
329
|
+
type: 'directive',
|
|
330
|
+
name,
|
|
331
|
+
data
|
|
332
|
+
})
|
|
333
|
+
},
|
|
334
|
+
onopentag (originalName, attributes) {
|
|
335
|
+
const parent = stack[stack.length - 1]
|
|
336
|
+
const element = createElement({
|
|
337
|
+
name: originalName,
|
|
338
|
+
attributes,
|
|
339
|
+
parent
|
|
340
|
+
})
|
|
341
|
+
// push element to stack as it may have children
|
|
342
|
+
stack.push(element)
|
|
343
|
+
},
|
|
344
|
+
ontext (text) {
|
|
345
|
+
const parent = stack[stack.length - 1]
|
|
346
|
+
|
|
347
|
+
createTextNode(text, parent)
|
|
348
|
+
},
|
|
349
|
+
onclosetag () {
|
|
350
|
+
const parent = stack[stack.length - 1]
|
|
351
|
+
|
|
352
|
+
if (parent.type === 'tag' && parent.name === 'head') {
|
|
353
|
+
|
|
354
|
+
}
|
|
355
|
+
// remove current element from stack as we're done with its children
|
|
356
|
+
stack.pop()
|
|
357
|
+
},
|
|
358
|
+
oncomment (data) {
|
|
359
|
+
const parent = stack[stack.length - 1]
|
|
360
|
+
|
|
361
|
+
parent.children.push({
|
|
362
|
+
type: 'comment',
|
|
363
|
+
data,
|
|
364
|
+
parent
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
parser.write(string)
|
|
370
|
+
parser.end()
|
|
371
|
+
|
|
372
|
+
return root
|
|
373
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<template id="coralite-pagination">
|
|
2
|
+
{{ pagination_list }}
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script type="module">
|
|
6
|
+
import { defineComponent, document } from 'coralite'
|
|
7
|
+
|
|
8
|
+
export default defineComponent({
|
|
9
|
+
tokens: {
|
|
10
|
+
/**
|
|
11
|
+
* @param {Object} values
|
|
12
|
+
* @param {string} values.pagination_visible- Max number of visible pages items.
|
|
13
|
+
* @param {string} values.pagination_offset - Number of items to skip from the beginning (offset for pagination)
|
|
14
|
+
* @param {string} values.pagination_index - Base URL path for generating pagination links
|
|
15
|
+
* @param {string} values.pagination_dirname - Directory path component for routing context
|
|
16
|
+
* @param {string} values.pagination_length - Total number of items across all pages
|
|
17
|
+
* @param {string} values.pagination_current - Currently active page number or identifier
|
|
18
|
+
*/
|
|
19
|
+
pagination_list (values) {
|
|
20
|
+
const length = parseInt(values.pagination_length)
|
|
21
|
+
|
|
22
|
+
// skip pagination if not needed
|
|
23
|
+
if (!length) {
|
|
24
|
+
return ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const maxVisible = parseInt(values.pagination_visible)
|
|
28
|
+
const currentPage = parseInt(values.pagination_current)
|
|
29
|
+
const dirname = values.pagination_dirname[0] === '/' ? values.pagination_dirname : '/' + values.pagination_dirname
|
|
30
|
+
const indexPathname = values.pagination_index[0] === '/' ? values.pagination_index : '/' + values.pagination_index
|
|
31
|
+
const pages = []
|
|
32
|
+
|
|
33
|
+
if (maxVisible >= length) {
|
|
34
|
+
// show all pages when max visible is greater than or equal to total page count
|
|
35
|
+
for (let i = 1; i <= length; i++) {
|
|
36
|
+
pages.push(i)
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
// calculate start and end of visible range based on current page position
|
|
40
|
+
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2));
|
|
41
|
+
let end = Math.min(length, start + maxVisible - 1);
|
|
42
|
+
|
|
43
|
+
if (start > 1) {
|
|
44
|
+
pages.push(1)
|
|
45
|
+
|
|
46
|
+
if (start > 2) {
|
|
47
|
+
// add ellipsis to indicate omitted pages before visible range
|
|
48
|
+
pages.push('...')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (let i = start; i <= end; i++) {
|
|
53
|
+
pages.push(i)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (end < length) {
|
|
57
|
+
if (length - end > 1) {
|
|
58
|
+
// add ellipsis to indicate omitted pages after visible range
|
|
59
|
+
pages.push('...')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pages.push(length)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let attributes = 'class="page-link"'
|
|
67
|
+
|
|
68
|
+
// highlight current page if it matches the document path
|
|
69
|
+
if (values.pagination_index === document.path.pathname) {
|
|
70
|
+
attributes = 'class="page-link active" aria-current="page"'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// add first page link
|
|
74
|
+
let items = `<li class="page-item"><a ${attributes} href="${indexPathname}">1</a></li>`
|
|
75
|
+
|
|
76
|
+
// generate page items
|
|
77
|
+
for (let i = 1; i < pages.length; i++) {
|
|
78
|
+
const pageNum = pages[i]
|
|
79
|
+
|
|
80
|
+
// check if current item an ellipsis
|
|
81
|
+
if (typeof pageNum === 'string') {
|
|
82
|
+
items += `<li class="page-item disabled"><span class="page-link">${pageNum}</span></li>`
|
|
83
|
+
} else {
|
|
84
|
+
// determine if this page number matches the current page
|
|
85
|
+
const isCurrent = currentPage === pageNum
|
|
86
|
+
|
|
87
|
+
let attributes = 'class="page-link"'
|
|
88
|
+
|
|
89
|
+
if (isCurrent) {
|
|
90
|
+
// add active state for current page
|
|
91
|
+
attributes = 'class="page-link active" aria-current="page"'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// construct link with dynamic page number and base directory path
|
|
95
|
+
items += `<li class="page-item"><a ${attributes} href="${dirname}/${pageNum}.html">${pageNum}</a></li>`
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let nextItem = '<li class="page-item disabled"><span class="page-link">Next</span></li>'
|
|
100
|
+
let prevItem = '<li class="page-item disabled"><span class="page-link">Previous</span></li>'
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
// initialize previous/next items as disabled links
|
|
104
|
+
if (currentPage > 1) {
|
|
105
|
+
let href = `${dirname}/${currentPage - 1}.html`
|
|
106
|
+
|
|
107
|
+
if (currentPage === 2) {
|
|
108
|
+
// set index path
|
|
109
|
+
href = indexPathname
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
prevItem = `<li class="page-item"><a class="page-link" href="${href}">Previous</a></li>`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (currentPage < length) {
|
|
116
|
+
nextItem = `<li class="page-item"><a class="page-link" href="${dirname}/${currentPage + 1}.html">Next</a></li>`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return '<ul class="pagination">' + prevItem + items + nextItem + '</ul>'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
</script>
|
package/types/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import {CoraliteToken, CoraliteTokenOptions, CoraliteDocument } from 'coralite/types'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for templates used to render aggregated results.
|
|
7
|
+
* @typedef {Object} CoraliteAggregateTemplate - Templates used to display the result
|
|
8
|
+
* @property {string} item - Unique identifier for the component used for each document
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Callback function for filtering aggregated content based on metadata.
|
|
13
|
+
* @callback CoraliteAggregateFilter
|
|
14
|
+
* @param {CoraliteToken} metadata - Aggregated HTML page metadata
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Callback function for sorting aggregated results based on metadata.
|
|
19
|
+
* @callback CoraliteAggregateSort
|
|
20
|
+
* @param {Object.<string, (string | CoraliteToken[])>} a - Aggregated HTML page metadata
|
|
21
|
+
* @param {Object.<string, (string | CoraliteToken[])>} b - Aggregated HTML page metadata
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Configuration object for content aggregation processes.
|
|
26
|
+
* @typedef {Object} CoraliteAggregate – Configuration object for the aggregation process
|
|
27
|
+
* @property {string[]} path - The path to aggregate, relative to pages directory
|
|
28
|
+
* @property {CoraliteAggregateTemplate | string} template - Templates used to display the result
|
|
29
|
+
* @property {Object} [pagination]
|
|
30
|
+
* @property {CoraliteAggregateTemplate | string} pagination.template - Pagination template ID
|
|
31
|
+
* @property {string} pagination.path - Pagination page infix (e.g. 'page' will result in 'page/1')
|
|
32
|
+
* @property {number} pagination.visible - Maximum visible number of pages.
|
|
33
|
+
* @property {CoraliteAggregateFilter} [filter] - Callback to filter out unwanted elements from the aggregated content.
|
|
34
|
+
* @property {boolean} [recursive] - Whether to recursively search subdirectories
|
|
35
|
+
* @property {CoraliteTokenOptions} [tokens] - Token configuration options
|
|
36
|
+
* @property {CoraliteAggregateSort} [sort] - Sort aggregated pages
|
|
37
|
+
* @property {number} [limit] - Specifies the maximum number of results to retrieve.
|
|
38
|
+
* @property {number} [offset] - Specifies the starting index for the results list.
|
|
39
|
+
*/
|