@jxsuite/parser 4.1.2 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/content-loader.js +944 -721
- package/dist/content.js +82 -21
- package/dist/markdown.js +3245 -2485
- package/dist/serialize.js +760 -604
- package/package.json +5 -5
- package/schemas/project.fragment.schema.json +223 -4
- package/src/Content.class.json +4 -0
- package/src/ContentCollection.class.json +7 -5
- package/src/content-loader.ts +64 -17
- package/src/content-rules.ts +8 -220
- package/src/content.ts +138 -32
- package/src/serialize.ts +10 -1
- package/src/types.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/parser",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "Jx markdown parser and external class integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"upgrade": "bun update --latest && bun install"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@jxsuite/markup": "^1.0.
|
|
52
|
-
"@jxsuite/schema": "^5.
|
|
51
|
+
"@jxsuite/markup": "^1.0.4",
|
|
52
|
+
"@jxsuite/schema": "^5.2.0",
|
|
53
53
|
"@shikijs/langs": "^4.5.0",
|
|
54
54
|
"@shikijs/themes": "^4.5.0",
|
|
55
|
-
"glob": "^
|
|
55
|
+
"glob": "^14.0.3",
|
|
56
56
|
"mdast-util-to-string": "^4.0.0",
|
|
57
57
|
"remark-directive": "^4.0.0",
|
|
58
58
|
"remark-frontmatter": "^5.0.0",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@happy-dom/global-registrator": "^21.0.4",
|
|
70
|
-
"@jxsuite/runtime": "^6.0
|
|
70
|
+
"@jxsuite/runtime": "^6.1.0",
|
|
71
71
|
"@jxsuite/test-support": "^0.0.0",
|
|
72
72
|
"@types/mdast": "^4.0.4",
|
|
73
73
|
"ajv": "^8.20.0"
|
|
@@ -100,16 +100,76 @@
|
|
|
100
100
|
"type": "boolean"
|
|
101
101
|
},
|
|
102
102
|
"$gt": {
|
|
103
|
-
"
|
|
103
|
+
"anyOf": [
|
|
104
|
+
{
|
|
105
|
+
"type": ["string", "number"]
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"type": "object",
|
|
109
|
+
"description": "The moment the rule is evaluated: the build machine's real clock.",
|
|
110
|
+
"properties": {
|
|
111
|
+
"$now": {
|
|
112
|
+
"const": true
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"required": ["$now"],
|
|
116
|
+
"additionalProperties": false
|
|
117
|
+
}
|
|
118
|
+
]
|
|
104
119
|
},
|
|
105
120
|
"$gte": {
|
|
106
|
-
"
|
|
121
|
+
"anyOf": [
|
|
122
|
+
{
|
|
123
|
+
"type": ["string", "number"]
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"type": "object",
|
|
127
|
+
"description": "The moment the rule is evaluated: the build machine's real clock.",
|
|
128
|
+
"properties": {
|
|
129
|
+
"$now": {
|
|
130
|
+
"const": true
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"required": ["$now"],
|
|
134
|
+
"additionalProperties": false
|
|
135
|
+
}
|
|
136
|
+
]
|
|
107
137
|
},
|
|
108
138
|
"$lt": {
|
|
109
|
-
"
|
|
139
|
+
"anyOf": [
|
|
140
|
+
{
|
|
141
|
+
"type": ["string", "number"]
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"type": "object",
|
|
145
|
+
"description": "The moment the rule is evaluated: the build machine's real clock.",
|
|
146
|
+
"properties": {
|
|
147
|
+
"$now": {
|
|
148
|
+
"const": true
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"required": ["$now"],
|
|
152
|
+
"additionalProperties": false
|
|
153
|
+
}
|
|
154
|
+
]
|
|
110
155
|
},
|
|
111
156
|
"$lte": {
|
|
112
|
-
"
|
|
157
|
+
"anyOf": [
|
|
158
|
+
{
|
|
159
|
+
"type": ["string", "number"]
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"type": "object",
|
|
163
|
+
"description": "The moment the rule is evaluated: the build machine's real clock.",
|
|
164
|
+
"properties": {
|
|
165
|
+
"$now": {
|
|
166
|
+
"const": true
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"required": ["$now"],
|
|
170
|
+
"additionalProperties": false
|
|
171
|
+
}
|
|
172
|
+
]
|
|
113
173
|
}
|
|
114
174
|
}
|
|
115
175
|
},
|
|
@@ -133,6 +193,165 @@
|
|
|
133
193
|
}
|
|
134
194
|
]
|
|
135
195
|
},
|
|
196
|
+
"hold": {
|
|
197
|
+
"description": "What keeps an entry out of a production build while authors still see it. Any rule matching holds the entry, and the first names why. `jx build` leaves held entries out, `jx build --include-held`, `jx dev` and Studio keep them. Jx gives no field a meaning of its own: a project names its fields and says here what they imply.",
|
|
198
|
+
"type": "array",
|
|
199
|
+
"items": {
|
|
200
|
+
"type": "object",
|
|
201
|
+
"additionalProperties": false,
|
|
202
|
+
"required": ["label", "when"],
|
|
203
|
+
"properties": {
|
|
204
|
+
"label": {
|
|
205
|
+
"description": "The words every surface shows for an entry this rule holds.",
|
|
206
|
+
"type": "string",
|
|
207
|
+
"minLength": 1
|
|
208
|
+
},
|
|
209
|
+
"when": {
|
|
210
|
+
"type": "object",
|
|
211
|
+
"propertyNames": {
|
|
212
|
+
"pattern": "^[^$]"
|
|
213
|
+
},
|
|
214
|
+
"additionalProperties": {
|
|
215
|
+
"anyOf": [
|
|
216
|
+
{
|
|
217
|
+
"type": "object",
|
|
218
|
+
"propertyNames": {
|
|
219
|
+
"enum": [
|
|
220
|
+
"$eq",
|
|
221
|
+
"$ne",
|
|
222
|
+
"$in",
|
|
223
|
+
"$nin",
|
|
224
|
+
"$exists",
|
|
225
|
+
"$gt",
|
|
226
|
+
"$gte",
|
|
227
|
+
"$lt",
|
|
228
|
+
"$lte"
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
"minProperties": 1,
|
|
232
|
+
"properties": {
|
|
233
|
+
"$eq": {},
|
|
234
|
+
"$ne": {},
|
|
235
|
+
"$in": {
|
|
236
|
+
"type": "array"
|
|
237
|
+
},
|
|
238
|
+
"$nin": {
|
|
239
|
+
"type": "array"
|
|
240
|
+
},
|
|
241
|
+
"$exists": {
|
|
242
|
+
"type": "boolean"
|
|
243
|
+
},
|
|
244
|
+
"$gt": {
|
|
245
|
+
"anyOf": [
|
|
246
|
+
{
|
|
247
|
+
"type": ["string", "number"]
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"type": "object",
|
|
251
|
+
"description": "The moment the rule is evaluated: the build machine's real clock.",
|
|
252
|
+
"properties": {
|
|
253
|
+
"$now": {
|
|
254
|
+
"const": true
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
"required": ["$now"],
|
|
258
|
+
"additionalProperties": false
|
|
259
|
+
}
|
|
260
|
+
]
|
|
261
|
+
},
|
|
262
|
+
"$gte": {
|
|
263
|
+
"anyOf": [
|
|
264
|
+
{
|
|
265
|
+
"type": ["string", "number"]
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"type": "object",
|
|
269
|
+
"description": "The moment the rule is evaluated: the build machine's real clock.",
|
|
270
|
+
"properties": {
|
|
271
|
+
"$now": {
|
|
272
|
+
"const": true
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
"required": ["$now"],
|
|
276
|
+
"additionalProperties": false
|
|
277
|
+
}
|
|
278
|
+
]
|
|
279
|
+
},
|
|
280
|
+
"$lt": {
|
|
281
|
+
"anyOf": [
|
|
282
|
+
{
|
|
283
|
+
"type": ["string", "number"]
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"type": "object",
|
|
287
|
+
"description": "The moment the rule is evaluated: the build machine's real clock.",
|
|
288
|
+
"properties": {
|
|
289
|
+
"$now": {
|
|
290
|
+
"const": true
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
"required": ["$now"],
|
|
294
|
+
"additionalProperties": false
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
},
|
|
298
|
+
"$lte": {
|
|
299
|
+
"anyOf": [
|
|
300
|
+
{
|
|
301
|
+
"type": ["string", "number"]
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"type": "object",
|
|
305
|
+
"description": "The moment the rule is evaluated: the build machine's real clock.",
|
|
306
|
+
"properties": {
|
|
307
|
+
"$now": {
|
|
308
|
+
"const": true
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
"required": ["$now"],
|
|
312
|
+
"additionalProperties": false
|
|
313
|
+
}
|
|
314
|
+
]
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"type": ["string", "number", "boolean", "null", "array"]
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"type": "object",
|
|
323
|
+
"propertyNames": {
|
|
324
|
+
"pattern": "^[^$]"
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
"description": "The frontmatter that matches this rule, with the grammar of `where`.",
|
|
330
|
+
"minProperties": 1
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
"examples": [
|
|
335
|
+
[
|
|
336
|
+
{
|
|
337
|
+
"label": "Draft",
|
|
338
|
+
"when": {
|
|
339
|
+
"draft": true
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"label": "Scheduled",
|
|
344
|
+
"when": {
|
|
345
|
+
"publishDate": {
|
|
346
|
+
"$gt": {
|
|
347
|
+
"$now": true
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
]
|
|
354
|
+
},
|
|
136
355
|
"idField": {
|
|
137
356
|
"description": "Frontmatter field whose value is the entry id, instead of the path-derived id. An entry without the field keeps its path id and is reported.",
|
|
138
357
|
"type": "string",
|
package/src/Content.class.json
CHANGED
|
@@ -78,6 +78,10 @@
|
|
|
78
78
|
"properties": {
|
|
79
79
|
"projectConfig": { "type": "object" },
|
|
80
80
|
"root": { "type": "string" },
|
|
81
|
+
"includeHeld": {
|
|
82
|
+
"type": "boolean",
|
|
83
|
+
"description": "Keep entries a type's `hold` rules match, labelled, rather than leaving them out"
|
|
84
|
+
},
|
|
81
85
|
"registry": { "type": "object" },
|
|
82
86
|
"io": { "type": "object" }
|
|
83
87
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"title": "ContentCollection",
|
|
4
|
-
"description": "Query
|
|
4
|
+
"description": "Query one or more project content types with filter, sort, and limit",
|
|
5
5
|
"$prototype": "Class",
|
|
6
6
|
"extends": "Object",
|
|
7
7
|
"$implementation": "../dist/content.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"contentType": {
|
|
12
12
|
"identifier": "contentType",
|
|
13
13
|
"type": { "type": "string", "enum": { "$ref": "#/$context/content" } },
|
|
14
|
-
"description": "Content type to query"
|
|
14
|
+
"description": "Content type to query. A list of types (written in JSON) is queried as one list, filtered, sorted and cut together"
|
|
15
15
|
},
|
|
16
16
|
"filter": {
|
|
17
17
|
"identifier": "filter",
|
|
@@ -40,12 +40,14 @@
|
|
|
40
40
|
"not empty"
|
|
41
41
|
]
|
|
42
42
|
},
|
|
43
|
-
"value": {
|
|
43
|
+
"value": {
|
|
44
|
+
"description": "The value to compare with, or a reference resolved for the page: { \"$ref\": \"#/$params/<name>\" } is a route parameter and { \"$ref\": \"#/state/<pointer>\" } a value in an entry declared earlier in state"
|
|
45
|
+
}
|
|
44
46
|
},
|
|
45
47
|
"required": ["field", "op"]
|
|
46
48
|
}
|
|
47
49
|
},
|
|
48
|
-
"description": "Filter rules"
|
|
50
|
+
"description": "Filter rules. A field may be a dotted path through the entry's data (terms.category.slug); a path that crosses an array collects every item's value"
|
|
49
51
|
},
|
|
50
52
|
"sort": {
|
|
51
53
|
"identifier": "sort",
|
|
@@ -64,7 +66,7 @@
|
|
|
64
66
|
"required": ["field"]
|
|
65
67
|
}
|
|
66
68
|
},
|
|
67
|
-
"description": "Sort rules"
|
|
69
|
+
"description": "Sort rules. A field may be a dotted path, as in a filter rule"
|
|
68
70
|
},
|
|
69
71
|
"limit": {
|
|
70
72
|
"identifier": "limit",
|
package/src/content-loader.ts
CHANGED
|
@@ -55,8 +55,8 @@ import {
|
|
|
55
55
|
traversalReason,
|
|
56
56
|
} from "./content-routes.ts";
|
|
57
57
|
import type { RouteSpec } from "./content-routes.ts";
|
|
58
|
-
import { compileExclude, compileWhere } from "./content-rules.ts";
|
|
59
|
-
import type { ExcludeMatcher, WherePredicate } from "./content-rules.ts";
|
|
58
|
+
import { compileExclude, compileHold, compileWhere } from "./content-rules.ts";
|
|
59
|
+
import type { ExcludeMatcher, HoldPredicate, WherePredicate } from "./content-rules.ts";
|
|
60
60
|
import { coerceEntryDates, isCoercedDate, isDateFormat } from "./dates.ts";
|
|
61
61
|
import type { ContentNodes } from "./inert.ts";
|
|
62
62
|
import type { ContentLoaderEntry, ContentTypeDef } from "./types.ts";
|
|
@@ -260,16 +260,22 @@ const DOCUMENT_EXTENSIONS = new Set([".md", ".markdown", ".mdx"]);
|
|
|
260
260
|
* What a content type's asset mount may serve, or `undefined` for "everything under the directory"
|
|
261
261
|
* (today's behavior, which every type that declares none of the vault options keeps).
|
|
262
262
|
*
|
|
263
|
-
* A type that declares `exclude`, `where` or `route` is publishing a folder of notes, and
|
|
264
|
-
* mount is the collection's front door, so it must not be a way around the collection's
|
|
265
|
-
* a file `exclude` names is not served however a page refers to it, nothing whose path
|
|
266
|
-
* segment is (`.git`, `.env` and `.obsidian` are what a source set to the repository root
|
|
267
|
-
* contains), and a document is not (a note `where` left out would otherwise leave as a raw
|
|
263
|
+
* A type that declares `exclude`, `where`, `hold` or `route` is publishing a folder of notes, and
|
|
264
|
+
* there the mount is the collection's front door, so it must not be a way around the collection's
|
|
265
|
+
* own rules: a file `exclude` names is not served however a page refers to it, nothing whose path
|
|
266
|
+
* has a dot segment is (`.git`, `.env` and `.obsidian` are what a source set to the repository root
|
|
267
|
+
* also contains), and a document is not (a note `where` left out would otherwise leave as a raw
|
|
268
268
|
* download, frontmatter and all). The paths it is asked about are relative to the mount directory,
|
|
269
269
|
* which is the source directory.
|
|
270
270
|
*/
|
|
271
271
|
function mountFilterFor(def: ContentTypeDef | undefined): ((path: string) => boolean) | undefined {
|
|
272
|
-
if (
|
|
272
|
+
if (
|
|
273
|
+
!def ||
|
|
274
|
+
(def.exclude === undefined &&
|
|
275
|
+
def.where === undefined &&
|
|
276
|
+
def.hold === undefined &&
|
|
277
|
+
def.route === undefined)
|
|
278
|
+
) {
|
|
273
279
|
return undefined;
|
|
274
280
|
}
|
|
275
281
|
let exclude: ExcludeMatcher;
|
|
@@ -551,6 +557,8 @@ export function loadContentConfig(projectRoot: string, projectConfig?: ProjectCo
|
|
|
551
557
|
* @param {ContentSection} section - The project.json `content` section value
|
|
552
558
|
* @param {string} root - Project root directory
|
|
553
559
|
* @param {FormatRegistry} formats - Format-dispatch view of the extension registry
|
|
560
|
+
* @param {ProjectConfig} [projectConfig]
|
|
561
|
+
* @param {boolean} [includeHeld] - Keep entries the `hold` rules match, labelled
|
|
554
562
|
* @returns {Promise<Map<string, ContentLoaderEntry[]>>} Map of content type name → entries
|
|
555
563
|
*/
|
|
556
564
|
export async function loadContentSection(
|
|
@@ -558,13 +566,14 @@ export async function loadContentSection(
|
|
|
558
566
|
root: string,
|
|
559
567
|
formats: FormatRegistry,
|
|
560
568
|
projectConfig?: ProjectConfig,
|
|
569
|
+
includeHeld?: boolean,
|
|
561
570
|
): Promise<Map<string, ContentLoaderEntry[]>> {
|
|
562
571
|
const contentTypes = new Map<string, ContentLoaderEntry[]>();
|
|
563
572
|
const mounts = sectionMounts(section, root, projectConfig);
|
|
564
573
|
for (const name of Object.keys(section)) {
|
|
565
574
|
contentTypes.set(
|
|
566
575
|
name,
|
|
567
|
-
await loadSectionType(name, { formats, mounts, projectConfig, root, section }),
|
|
576
|
+
await loadSectionType(name, { formats, includeHeld, mounts, projectConfig, root, section }),
|
|
568
577
|
);
|
|
569
578
|
}
|
|
570
579
|
return contentTypes;
|
|
@@ -601,6 +610,8 @@ export interface SectionTypeSource {
|
|
|
601
610
|
mounts: Map<string, AssetMount>;
|
|
602
611
|
/** Leave entry bodies for later, reporting each deferred entry here. */
|
|
603
612
|
onDeferred?: DeferredBodyHandler | undefined;
|
|
613
|
+
/** Keep entries the type's `hold` rules match, labelled, rather than leaving them out. */
|
|
614
|
+
includeHeld?: boolean | undefined;
|
|
604
615
|
}
|
|
605
616
|
|
|
606
617
|
/**
|
|
@@ -614,7 +625,7 @@ export async function loadSectionType(
|
|
|
614
625
|
name: string,
|
|
615
626
|
source: SectionTypeSource,
|
|
616
627
|
): Promise<ContentLoaderEntry[]> {
|
|
617
|
-
const { section, root, formats, projectConfig, mounts, onDeferred } = source;
|
|
628
|
+
const { section, root, formats, projectConfig, mounts, onDeferred, includeHeld } = source;
|
|
618
629
|
const contentTypeDef = section[name];
|
|
619
630
|
if (!contentTypeDef) {
|
|
620
631
|
return [];
|
|
@@ -623,6 +634,7 @@ export async function loadSectionType(
|
|
|
623
634
|
const typeSource = contentTypeDef.source;
|
|
624
635
|
if (!isString(typeSource) || !typeSource.includes(LOCALE_PLACEHOLDER)) {
|
|
625
636
|
return await loadContentType(name, contentTypeDef, root, formats, {
|
|
637
|
+
includeHeld,
|
|
626
638
|
mount: mounts.get(name),
|
|
627
639
|
projectConfig,
|
|
628
640
|
...deferral,
|
|
@@ -645,6 +657,7 @@ export async function loadSectionType(
|
|
|
645
657
|
for (const locale of locales) {
|
|
646
658
|
const localized = { ...contentTypeDef, source: localeSource(root, typeSource, locale) };
|
|
647
659
|
const entries = await loadContentType(name, localized, root, formats, {
|
|
660
|
+
includeHeld,
|
|
648
661
|
locale,
|
|
649
662
|
mount: mounts.get(`${name}/${locale}`),
|
|
650
663
|
projectConfig,
|
|
@@ -686,6 +699,7 @@ type LinkMode = "warn" | "error" | "ignore";
|
|
|
686
699
|
interface TypeRules {
|
|
687
700
|
exclude: ExcludeMatcher;
|
|
688
701
|
where: WherePredicate | null;
|
|
702
|
+
hold: HoldPredicate | null;
|
|
689
703
|
idField: string | undefined;
|
|
690
704
|
route: RouteSpec | null;
|
|
691
705
|
links: LinkMode;
|
|
@@ -706,6 +720,7 @@ const KNOWN_TYPE_KEYS = [
|
|
|
706
720
|
"alerts",
|
|
707
721
|
"exclude",
|
|
708
722
|
"format",
|
|
723
|
+
"hold",
|
|
709
724
|
"idField",
|
|
710
725
|
"indexRoute",
|
|
711
726
|
"links",
|
|
@@ -792,6 +807,7 @@ function compileTypeRules(name: string, def: ContentTypeDef): TypeRules {
|
|
|
792
807
|
links,
|
|
793
808
|
route: parseRouteConfig(def.route, def.indexRoute, name),
|
|
794
809
|
where: def.where === undefined ? null : compileWhere(def.where),
|
|
810
|
+
hold: def.hold === undefined ? null : compileHold(def.hold),
|
|
795
811
|
};
|
|
796
812
|
} catch (error) {
|
|
797
813
|
const message = errorMessage(error);
|
|
@@ -831,6 +847,8 @@ interface FinishContext {
|
|
|
831
847
|
reload?: ((filePath: string) => Promise<ContentLoaderEntry[]>) | undefined;
|
|
832
848
|
/** Told of each entry whose body was deferred, with what completes it. */
|
|
833
849
|
onDeferred?: DeferredBodyHandler | undefined;
|
|
850
|
+
/** Keep entries `hold` matches, labelled, rather than leaving them out. */
|
|
851
|
+
includeHeld?: boolean | undefined;
|
|
834
852
|
}
|
|
835
853
|
|
|
836
854
|
/**
|
|
@@ -845,10 +863,11 @@ export type DeferredBodyHandler = (
|
|
|
845
863
|
/**
|
|
846
864
|
* The collection-wide pass every source kind goes through once its files are loaded.
|
|
847
865
|
*
|
|
848
|
-
* Order matters and is the whole design. `where`
|
|
849
|
-
* never validated, coerced, given a route or counted as a duplicate. Ids are settled
|
|
850
|
-
* anything keyed by id (relationships, duplicate checks, routes that read `{id}`). Routes
|
|
851
|
-
* computed before links, because a link can only be resolved once the page it points at has a
|
|
866
|
+
* Order matters and is the whole design. `where` and `hold` run first, so an entry that is not
|
|
867
|
+
* published is never validated, coerced, given a route or counted as a duplicate. Ids are settled
|
|
868
|
+
* before anything keyed by id (relationships, duplicate checks, routes that read `{id}`). Routes
|
|
869
|
+
* are computed before links, because a link can only be resolved once the page it points at has a
|
|
870
|
+
* URL.
|
|
852
871
|
*
|
|
853
872
|
* @returns {ContentLoaderEntry[]} The kept entries, in load order
|
|
854
873
|
*/
|
|
@@ -858,6 +877,7 @@ function finishEntries(files: LoadedFile[], ctx: FinishContext): ContentLoaderEn
|
|
|
858
877
|
// 1. where: split every file's entries into kept and left out.
|
|
859
878
|
const kept: LoadedFile[] = [];
|
|
860
879
|
const unpublished = new Map<string, string>();
|
|
880
|
+
const held: string[] = [];
|
|
861
881
|
let loadedCount = 0;
|
|
862
882
|
for (const file of files) {
|
|
863
883
|
const keep: ContentLoaderEntry[] = [];
|
|
@@ -867,8 +887,19 @@ function finishEntries(files: LoadedFile[], ctx: FinishContext): ContentLoaderEn
|
|
|
867
887
|
if (file.rel !== null && file.entries.length === 1) {
|
|
868
888
|
unpublished.set(file.rel, `left out by where.${rules.where.why(entry.data) ?? "filter"}`);
|
|
869
889
|
}
|
|
870
|
-
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
const hold = rules.hold?.(entry.data);
|
|
893
|
+
if (hold === undefined) {
|
|
871
894
|
keep.push(entry);
|
|
895
|
+
} else if (ctx.includeHeld === true) {
|
|
896
|
+
entry._meta = { ...entry._meta, hold };
|
|
897
|
+
keep.push(entry);
|
|
898
|
+
} else {
|
|
899
|
+
held.push(`${file.rel ?? entry.id} (${hold})`);
|
|
900
|
+
if (file.rel !== null && file.entries.length === 1) {
|
|
901
|
+
unpublished.set(file.rel, `held: ${hold}`);
|
|
902
|
+
}
|
|
872
903
|
}
|
|
873
904
|
}
|
|
874
905
|
if (keep.length > 0) {
|
|
@@ -886,6 +917,14 @@ function finishEntries(files: LoadedFile[], ctx: FinishContext): ContentLoaderEn
|
|
|
886
917
|
);
|
|
887
918
|
}
|
|
888
919
|
|
|
920
|
+
/* Said, not warned: holding an entry is the project's own rule working. It is still said, because
|
|
921
|
+
an entry that disappears from a site with no message reads as a bug. */
|
|
922
|
+
if (held.length > 0) {
|
|
923
|
+
console.info(
|
|
924
|
+
`Content type "${name}": holding ${held.length} from this build: ${held.join(", ")}`,
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
889
928
|
// 2. Per kept entry: where it came from, and content-relative assets onto the mount.
|
|
890
929
|
for (const file of kept) {
|
|
891
930
|
for (const entry of file.entries) {
|
|
@@ -1302,6 +1341,8 @@ interface LoadOptions {
|
|
|
1302
1341
|
*/
|
|
1303
1342
|
deferBody?: boolean | undefined;
|
|
1304
1343
|
onDeferred?: DeferredBodyHandler | undefined;
|
|
1344
|
+
/** Keep entries the type's `hold` rules match, labelled, rather than leaving them out. */
|
|
1345
|
+
includeHeld?: boolean | undefined;
|
|
1305
1346
|
}
|
|
1306
1347
|
|
|
1307
1348
|
/**
|
|
@@ -1325,12 +1366,13 @@ async function loadContentType(
|
|
|
1325
1366
|
if (!source) {
|
|
1326
1367
|
return [];
|
|
1327
1368
|
}
|
|
1328
|
-
const { mount, projectConfig, locale, deferBody, onDeferred } = options;
|
|
1369
|
+
const { mount, projectConfig, locale, deferBody, onDeferred, includeHeld } = options;
|
|
1329
1370
|
const { schema } = contentTypeDef;
|
|
1330
1371
|
const rules = compileTypeRules(name, contentTypeDef);
|
|
1331
1372
|
const finishContext = (extra: Partial<FinishContext>): FinishContext => ({
|
|
1332
1373
|
coerceDates: false,
|
|
1333
1374
|
entryExtensions: new Set<string>(),
|
|
1375
|
+
includeHeld,
|
|
1334
1376
|
locale,
|
|
1335
1377
|
mount,
|
|
1336
1378
|
name,
|
|
@@ -1822,6 +1864,8 @@ export interface ContentStoreSource {
|
|
|
1822
1864
|
root: string;
|
|
1823
1865
|
formats: FormatRegistry;
|
|
1824
1866
|
projectConfig: ProjectConfig | undefined;
|
|
1867
|
+
/** Keep entries the type's `hold` rules match, labelled, rather than leaving them out. */
|
|
1868
|
+
includeHeld?: boolean | undefined;
|
|
1825
1869
|
}
|
|
1826
1870
|
|
|
1827
1871
|
export class ContentStore extends Map<string, ContentLoaderEntry[]> implements ContentSectionData {
|
|
@@ -1852,6 +1896,7 @@ export class ContentStore extends Map<string, ContentLoaderEntry[]> implements C
|
|
|
1852
1896
|
source.root,
|
|
1853
1897
|
source.formats,
|
|
1854
1898
|
source.projectConfig,
|
|
1899
|
+
source.includeHeld,
|
|
1855
1900
|
);
|
|
1856
1901
|
for (const [name, entries] of loaded) {
|
|
1857
1902
|
store.#adopt(name, entries);
|
|
@@ -1964,8 +2009,9 @@ export class ContentStore extends Map<string, ContentLoaderEntry[]> implements C
|
|
|
1964
2009
|
#loadType(name: string): Promise<void> {
|
|
1965
2010
|
let load = this.#loads.get(name);
|
|
1966
2011
|
if (!load) {
|
|
1967
|
-
const { section, root, formats, projectConfig } = this.#source;
|
|
2012
|
+
const { section, root, formats, projectConfig, includeHeld } = this.#source;
|
|
1968
2013
|
load = loadSectionType(name, {
|
|
2014
|
+
includeHeld,
|
|
1969
2015
|
formats,
|
|
1970
2016
|
mounts: this.#mounts,
|
|
1971
2017
|
onDeferred: (entry, completeBody) => {
|
|
@@ -2045,6 +2091,7 @@ export class Content {
|
|
|
2045
2091
|
): Promise<ContentStore> {
|
|
2046
2092
|
const source = {
|
|
2047
2093
|
formats: ctx.registry.formats,
|
|
2094
|
+
includeHeld: ctx.includeHeld,
|
|
2048
2095
|
projectConfig: ctx.projectConfig,
|
|
2049
2096
|
root: ctx.root,
|
|
2050
2097
|
section: sectionValue ?? {},
|