@jxsuite/parser 0.35.1 → 1.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 +107 -66
- package/dist/content-loader.js +285 -0
- package/dist/content.js +2 -2
- package/dist/markdown.js +19 -5
- package/dist/serialize.js +2 -2
- package/jx-extension.json +17 -0
- package/package.json +19 -16
- package/schemas/document.fragment.schema.json +27 -0
- package/schemas/project.fragment.schema.json +59 -0
- package/src/Content.class.json +117 -0
- package/src/ContentCollection.class.json +3 -3
- package/src/ContentEntry.class.json +1 -1
- package/src/Csv.class.json +1 -1
- package/src/content-loader.ts +499 -0
- package/src/content.ts +6 -4
- package/src/html-to-jx.ts +8 -117
- package/src/markdown.ts +7 -0
- package/src/md-html.ts +5 -42
- package/src/md.ts +28 -2
- package/src/serialize.ts +1 -1
- package/src/transpile.ts +2 -2
- package/src/types.ts +15 -18
package/README.md
CHANGED
|
@@ -1,115 +1,156 @@
|
|
|
1
1
|
# `@jxsuite/parser`
|
|
2
2
|
|
|
3
|
-
> Markdown
|
|
3
|
+
> Content & Markdown extension for Jx — file-based content collections with Markdown and CSV formats.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
`@jxsuite/parser`
|
|
7
|
+
`@jxsuite/parser` is the first-party content extension. Its `jx-extension.json` manifest exposes six classes plus project/document schema fragments:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
| Class | Kind | Purpose |
|
|
10
|
+
| -------------------- | --------------- | -------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| `Markdown` | Format (`.md`) | Parse/serialize Jx Markdown documents, load markdown content entries, resolve a file at runtime |
|
|
12
|
+
| `Csv` | Format (`.csv`) | Parse local CSV files or remote CSV URLs into typed content entries |
|
|
13
|
+
| `Content` | Project section | Loads the project.json `content` section into `_project.content` and expands `$paths` route params |
|
|
14
|
+
| `ContentCollection` | Runtime class | Query a content type with filter, sort, and limit |
|
|
15
|
+
| `ContentEntry` | Runtime class | Fetch a single entry from a content type by ID (or another field) |
|
|
16
|
+
| `MarkdownCollection` | Runtime class | Glob markdown files into a sorted, filterable array |
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
Built on the [`unified`](https://unifiedjs.com/) / remark ecosystem.
|
|
19
|
+
|
|
20
|
+
## Installation & registration
|
|
12
21
|
|
|
13
22
|
```bash
|
|
14
23
|
bun add @jxsuite/parser
|
|
15
24
|
```
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Parses a single Markdown file into structured data.
|
|
26
|
+
Registration is manifest-based — add the package to `project.json`:
|
|
20
27
|
|
|
21
28
|
```json
|
|
22
29
|
{
|
|
23
|
-
"
|
|
24
|
-
"post": {
|
|
25
|
-
"$prototype": "MarkdownFile",
|
|
26
|
-
"$src": "@jxsuite/parser/MarkdownFile.class.json",
|
|
27
|
-
"src": "./content/posts/hello-world.md"
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
+
"extensions": ["@jxsuite/parser"]
|
|
30
31
|
}
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
That one entry registers the `.md`/`.csv` formats, the `content` project section, and the classes: `$prototype: "ContentEntry"` (etc.) resolves by name through the project's extension registry — no `$src` or imports wiring. There are no implicit defaults: a project without the extension supports only `.json` documents.
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
| -------------- | -------- | ----------------------------------- |
|
|
37
|
-
| `slug` | `string` | Filename without extension |
|
|
38
|
-
| `path` | `string` | Full file path |
|
|
39
|
-
| `frontmatter` | `object` | Parsed YAML frontmatter |
|
|
40
|
-
| `$body` | `string` | Rendered HTML body |
|
|
41
|
-
| `$excerpt` | `string` | First paragraph as HTML |
|
|
42
|
-
| `$toc` | `array` | Table of contents (id, text, depth) |
|
|
43
|
-
| `$readingTime` | `number` | Estimated reading time in minutes |
|
|
44
|
-
| `$wordCount` | `number` | Word count |
|
|
36
|
+
## Content collections
|
|
45
37
|
|
|
46
|
-
|
|
38
|
+
Declare content types in the `content` section (validated by this package's project schema fragment):
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"content": {
|
|
43
|
+
"docs": {
|
|
44
|
+
"source": "./content/docs",
|
|
45
|
+
"format": "Markdown",
|
|
46
|
+
"schema": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": { "title": { "type": "string" }, "date": { "type": "string" } }
|
|
49
|
+
},
|
|
50
|
+
"$elements": [{ "$ref": "./components/doc-note.json" }]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
Query them from any page or component:
|
|
49
57
|
|
|
50
58
|
```json
|
|
51
59
|
{
|
|
52
60
|
"state": {
|
|
53
61
|
"posts": {
|
|
54
|
-
"$prototype": "
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"sortBy": "date",
|
|
58
|
-
"sortOrder": "desc",
|
|
62
|
+
"$prototype": "ContentCollection",
|
|
63
|
+
"contentType": "docs",
|
|
64
|
+
"filter": [{ "field": "draft", "op": "!=", "value": true }],
|
|
59
65
|
"limit": 10
|
|
66
|
+
},
|
|
67
|
+
"page": {
|
|
68
|
+
"$prototype": "ContentEntry",
|
|
69
|
+
"contentType": "docs",
|
|
70
|
+
"id": { "$ref": "#/$params/slug" }
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
73
|
}
|
|
63
74
|
```
|
|
64
75
|
|
|
65
|
-
|
|
76
|
+
`ContentEntry` matches on the entry id (slug) by default; set `field` to match a frontmatter field (e.g. `"sku"`) instead.
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
| ----------- | -------- | -------- | ----------------------------------- |
|
|
69
|
-
| `src` | `string` | — | Glob pattern for Markdown files |
|
|
70
|
-
| `sortBy` | `string` | `"date"` | Frontmatter field to sort by |
|
|
71
|
-
| `sortOrder` | `string` | `"desc"` | `"asc"` or `"desc"` |
|
|
72
|
-
| `limit` | `number` | — | Maximum number of results |
|
|
73
|
-
| `filter` | `string` | — | Frontmatter field filter expression |
|
|
78
|
+
## `Markdown`
|
|
74
79
|
|
|
75
|
-
|
|
80
|
+
One class carries every format capability:
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
- **static `parse`** — Jx Markdown source → Jx document (browser-safe; Studio opens `.md` files with it)
|
|
83
|
+
- **static `serialize`** — Jx document → markdown, `roundtrip` (lossless, default) or `export` (lossy clean markdown)
|
|
84
|
+
- **static `discover` / `load`** — compile-time content access: frontmatter → `data`, source preserved as `body`, parsed `$children`, and `_meta` (excerpt, toc, reading time, word count)
|
|
85
|
+
- **instance `resolve`** — runtime on-demand access for a `$prototype: "Markdown"` state entry
|
|
78
86
|
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"state": {
|
|
90
|
+
"post": { "$prototype": "Markdown", "src": "./content/posts/hello-world.md" }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
83
93
|
```
|
|
84
94
|
|
|
85
|
-
|
|
95
|
+
**Resolved value** (`MarkdownFileResult`):
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
| Property | Type | Description |
|
|
98
|
+
| -------------- | -------- | ------------------------------------------- |
|
|
99
|
+
| `slug` | `string` | Entry id (path-based under the source root) |
|
|
100
|
+
| `path` | `string` | Full file path |
|
|
101
|
+
| `frontmatter` | `object` | Parsed YAML frontmatter |
|
|
102
|
+
| `$children` | `array` | Body as Jx element nodes |
|
|
103
|
+
| `$excerpt` | `string` | First paragraph |
|
|
104
|
+
| `$toc` | `array` | Table of contents (id, text, depth) |
|
|
105
|
+
| `$readingTime` | `number` | Estimated reading time in minutes |
|
|
106
|
+
| `$wordCount` | `number` | Word count |
|
|
107
|
+
|
|
108
|
+
### Directives
|
|
109
|
+
|
|
110
|
+
`:::name{attrs}` container syntax converts directly to Jx element nodes — `:::my-card{title="Hello"}` becomes a `my-card` element with its attributes routed to the right Jx locations (props, `$`-annotations, styles). Capitalized directives (e.g. `:::Array`) become tagName-less `$prototype` nodes. A content type's `$elements` list declares which components its entries may use.
|
|
111
|
+
|
|
112
|
+
## `Csv`
|
|
92
113
|
|
|
93
|
-
|
|
114
|
+
CSV content format (`documentKinds: ["content"]`; Studio opens `.csv` in grid/source modes).
|
|
94
115
|
|
|
95
|
-
|
|
116
|
+
| Option | Type | Description |
|
|
117
|
+
| --------- | -------- | ----------------------------------------------------------------- |
|
|
118
|
+
| `src` | `string` | Path or http(s) URL to a CSV file (remote sources supported) |
|
|
119
|
+
| `schema` | `object` | Per-column type schema for coercion (number, boolean, array) |
|
|
120
|
+
| `idField` | `string` | Column used as entry id (default chain: id, sku, slug, row index) |
|
|
96
121
|
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
## `MarkdownCollection`
|
|
123
|
+
|
|
124
|
+
Globs markdown files into a sorted array of `MarkdownFileResult`s (runtime alternative to the content section):
|
|
99
125
|
|
|
100
|
-
|
|
101
|
-
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"state": {
|
|
129
|
+
"posts": {
|
|
130
|
+
"$prototype": "MarkdownCollection",
|
|
131
|
+
"src": "./content/posts/*.md",
|
|
132
|
+
"sortBy": "frontmatter.date",
|
|
133
|
+
"sortOrder": "desc",
|
|
134
|
+
"limit": 10
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
102
138
|
```
|
|
103
139
|
|
|
104
|
-
|
|
140
|
+
Options: `src` (glob), `sortBy` (dot-path, default `"frontmatter.date"`), `sortOrder` (`"asc"`/`"desc"`), `limit`, `filter` (predicate), `basePath`, `directives`.
|
|
141
|
+
|
|
142
|
+
## Module exports
|
|
105
143
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
144
|
+
| Export | Contents |
|
|
145
|
+
| -------------------------------- | ---------------------------------------------------------------------- |
|
|
146
|
+
| `@jxsuite/parser` | `processMarkdown`, `Markdown`, `MarkdownCollection` |
|
|
147
|
+
| `@jxsuite/parser/markdown` | The `Markdown` format class (browser-safe parse/serialize) |
|
|
148
|
+
| `@jxsuite/parser/csv` | `Csv`, `parseCSV`, `coerceCSVRows` |
|
|
149
|
+
| `@jxsuite/parser/content` | `ContentCollection`, `ContentEntry`, pure query functions |
|
|
150
|
+
| `@jxsuite/parser/content-loader` | `Content` project-section loader |
|
|
151
|
+
| `@jxsuite/parser/transpile` | `transpileJxMarkdown` (markdown source → Jx document), `isJxMarkdown` |
|
|
152
|
+
| `@jxsuite/parser/serialize` | `serializeJxMarkdown`, `jxToMdast`, `mdastToJx`, markdown element sets |
|
|
153
|
+
| `@jxsuite/parser/*.class.json` | The class descriptors named by the manifest |
|
|
113
154
|
|
|
114
155
|
## License
|
|
115
156
|
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
function __accessProp(key) {
|
|
8
|
+
return this[key];
|
|
9
|
+
}
|
|
10
|
+
var __toESMCache_node;
|
|
11
|
+
var __toESMCache_esm;
|
|
12
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
+
var canCache = mod != null && typeof mod === "object";
|
|
14
|
+
if (canCache) {
|
|
15
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
+
var cached = cache.get(mod);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
for (let key of __getOwnPropNames(mod))
|
|
23
|
+
if (!__hasOwnProp.call(to, key))
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: __accessProp.bind(mod, key),
|
|
26
|
+
enumerable: true
|
|
27
|
+
});
|
|
28
|
+
if (canCache)
|
|
29
|
+
cache.set(mod, to);
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
33
|
+
var __returnValue = (v) => v;
|
|
34
|
+
function __exportSetter(name, newValue) {
|
|
35
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
36
|
+
}
|
|
37
|
+
var __export = (target, all) => {
|
|
38
|
+
for (var name in all)
|
|
39
|
+
__defProp(target, name, {
|
|
40
|
+
get: all[name],
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
set: __exportSetter.bind(all, name)
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
47
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
48
|
+
|
|
49
|
+
// src/content-loader.ts
|
|
50
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
51
|
+
import { basename, extname, resolve } from "node:path";
|
|
52
|
+
function loadJSONEntries(filePath) {
|
|
53
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
54
|
+
if (Array.isArray(raw)) {
|
|
55
|
+
return raw.map((item, i) => ({
|
|
56
|
+
body: null,
|
|
57
|
+
data: item,
|
|
58
|
+
id: item.id ?? `${basename(filePath, ".json")}-${i}`
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
const rawObj = raw;
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
body: null,
|
|
65
|
+
data: rawObj,
|
|
66
|
+
id: rawObj.id ?? basename(filePath, ".json")
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
function discoverJSONFiles(resolvedSource) {
|
|
71
|
+
if (extname(resolvedSource)) {
|
|
72
|
+
return existsSync(resolvedSource) ? [resolvedSource] : [];
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return readdirSync(resolvedSource, { recursive: true }).filter((f) => String(f).endsWith(".json")).map((f) => resolve(resolvedSource, String(f)));
|
|
76
|
+
} catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function loadContentConfig(projectRoot, projectConfig) {
|
|
81
|
+
const contentDir = resolve(projectRoot, "content");
|
|
82
|
+
const config = { content: projectConfig?.content ?? {} };
|
|
83
|
+
return { config, contentDir };
|
|
84
|
+
}
|
|
85
|
+
async function loadContentSection(section, root, formats) {
|
|
86
|
+
const contentTypes = new Map;
|
|
87
|
+
for (const [name, contentTypeDef] of Object.entries(section)) {
|
|
88
|
+
const entries = await loadContentType(name, contentTypeDef, root, formats);
|
|
89
|
+
contentTypes.set(name, entries);
|
|
90
|
+
}
|
|
91
|
+
return contentTypes;
|
|
92
|
+
}
|
|
93
|
+
function getContentTypeElements(projectRoot, contentTypeName, projectConfig) {
|
|
94
|
+
const { config } = loadContentConfig(projectRoot, projectConfig);
|
|
95
|
+
const def = config.content[contentTypeName];
|
|
96
|
+
return def?.$elements;
|
|
97
|
+
}
|
|
98
|
+
async function loadContentType(name, contentTypeDef, projectRoot, registry) {
|
|
99
|
+
const { source } = contentTypeDef;
|
|
100
|
+
if (!source) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const schema = contentTypeDef.schema;
|
|
104
|
+
const directiveOptions = contentTypeDef.$elements?.length ? {
|
|
105
|
+
allowedNames: contentTypeDef.$elements.filter((e) => typeof e === "string" || typeof e === "object" && e?.$ref).map((e) => typeof e === "string" ? e : e.$ref)
|
|
106
|
+
} : undefined;
|
|
107
|
+
const formatName = contentTypeDef.format;
|
|
108
|
+
let entry;
|
|
109
|
+
if (formatName && formatName !== "json") {
|
|
110
|
+
entry = registry.byName(formatName);
|
|
111
|
+
if (!entry) {
|
|
112
|
+
throw new Error(`Content type "${name}": format "${formatName}" is not a registered format class. ` + `Enable an extension providing it in project.json "extensions", e.g. "@jxsuite/parser".`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
116
|
+
if (!entry) {
|
|
117
|
+
throw new Error(`Content type "${name}": remote sources require an explicit "format" naming a ` + `format class with "remote": true (e.g. "Csv").`);
|
|
118
|
+
}
|
|
119
|
+
if (!entry.remote) {
|
|
120
|
+
throw new Error(`Content type "${name}": format "${entry.name}" does not support remote sources ` + `(its format block lacks "remote": true).`);
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const entries2 = await entry.call("load", source, {
|
|
124
|
+
directiveOptions,
|
|
125
|
+
schema
|
|
126
|
+
});
|
|
127
|
+
if (schema) {
|
|
128
|
+
validateEntries(entries2, schema, name);
|
|
129
|
+
}
|
|
130
|
+
return entries2;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.warn(`Content type "${name}": ${errorMessage(error)}`);
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const resolvedSource = resolve(projectRoot, source).split("\\").join("/");
|
|
137
|
+
const ext = extname(source).toLowerCase();
|
|
138
|
+
if (formatName === "json" || !entry && ext === ".json") {
|
|
139
|
+
const files2 = discoverJSONFiles(resolvedSource);
|
|
140
|
+
const entries2 = [];
|
|
141
|
+
for (const filePath of files2) {
|
|
142
|
+
entries2.push(...loadJSONEntries(filePath));
|
|
143
|
+
}
|
|
144
|
+
if (schema) {
|
|
145
|
+
validateEntries(entries2, schema, name);
|
|
146
|
+
}
|
|
147
|
+
return entries2;
|
|
148
|
+
}
|
|
149
|
+
if (!entry && ext) {
|
|
150
|
+
entry = registry.byExtension(ext, "load");
|
|
151
|
+
}
|
|
152
|
+
if (!entry) {
|
|
153
|
+
throw unknownFormatError(source, ext || `content type "${name}" (directory sources need an explicit "format")`);
|
|
154
|
+
}
|
|
155
|
+
const files = entry.capabilities.discover ? await entry.call("discover", source, {
|
|
156
|
+
baseDir: projectRoot
|
|
157
|
+
}) : [resolvedSource];
|
|
158
|
+
const sourceRoot = extname(source) ? undefined : resolvedSource;
|
|
159
|
+
const entries = [];
|
|
160
|
+
for (const filePath of files) {
|
|
161
|
+
entries.push(...await entry.call("load", filePath, {
|
|
162
|
+
directiveOptions,
|
|
163
|
+
schema,
|
|
164
|
+
...sourceRoot !== undefined && { sourceRoot }
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
if (schema) {
|
|
168
|
+
validateEntries(entries, schema, name);
|
|
169
|
+
}
|
|
170
|
+
return entries;
|
|
171
|
+
}
|
|
172
|
+
function unknownFormatError(path, ext) {
|
|
173
|
+
return new Error(`No format class registered for "${ext}" (${path}). ` + `Enable an extension providing this format in project.json "extensions", ` + `e.g. "@jxsuite/parser".`);
|
|
174
|
+
}
|
|
175
|
+
function errorMessage(error) {
|
|
176
|
+
return error instanceof Error ? error.message : String(error);
|
|
177
|
+
}
|
|
178
|
+
function validateEntries(entries, schema, contentTypeName) {
|
|
179
|
+
const required = schema.required ?? [];
|
|
180
|
+
const properties = schema.properties ?? {};
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
for (const field of required) {
|
|
183
|
+
if (!(field in entry.data) || entry.data[field] == null) {
|
|
184
|
+
console.warn(`Content validation: "${contentTypeName}/${entry.id}" missing required field "${field}"`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const [field, def] of Object.entries(properties)) {
|
|
188
|
+
const value = entry.data[field];
|
|
189
|
+
if (value == null) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (def.type === "string" && typeof value !== "string") {
|
|
193
|
+
console.warn(`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected string, got ${typeof value}`);
|
|
194
|
+
} else if (def.type === "number" && typeof value !== "number") {
|
|
195
|
+
console.warn(`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected number, got ${typeof value}`);
|
|
196
|
+
} else if (def.type === "boolean" && typeof value !== "boolean") {
|
|
197
|
+
console.warn(`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected boolean, got ${typeof value}`);
|
|
198
|
+
} else if (def.type === "array" && !Array.isArray(value)) {
|
|
199
|
+
console.warn(`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected array, got ${typeof value}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
var CONTENT_REF_PREFIX = "#/content/";
|
|
205
|
+
function refTargetName(ref) {
|
|
206
|
+
if (!ref?.startsWith(CONTENT_REF_PREFIX)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
return ref.slice(CONTENT_REF_PREFIX.length);
|
|
210
|
+
}
|
|
211
|
+
function resolveContentTypeRefs(contentTypes, section) {
|
|
212
|
+
for (const [name, contentTypeDef] of Object.entries(section)) {
|
|
213
|
+
const schema = contentTypeDef.schema;
|
|
214
|
+
if (!schema?.properties) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const entries = contentTypes.get(name);
|
|
218
|
+
if (!entries) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
for (const [field, def] of Object.entries(schema.properties)) {
|
|
222
|
+
const toOne = refTargetName(def.$ref);
|
|
223
|
+
const toMany = toOne ? undefined : refTargetName(def.items?.$ref);
|
|
224
|
+
const target = toOne ?? toMany;
|
|
225
|
+
if (!target) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const refEntries = contentTypes.get(target);
|
|
229
|
+
if (!refEntries) {
|
|
230
|
+
console.warn(`Content relationships: "${name}" field "${field}" references unknown content type "${target}"`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
const value = entry.data[field];
|
|
235
|
+
if (toOne && typeof value === "string") {
|
|
236
|
+
const resolved = refEntries.find((e) => e.id === value);
|
|
237
|
+
if (resolved) {
|
|
238
|
+
entry.data[field] = resolved;
|
|
239
|
+
} else {
|
|
240
|
+
console.warn(`Content relationships: "${name}/${entry.id}" field "${field}" references missing "${target}" entry "${value}"`);
|
|
241
|
+
}
|
|
242
|
+
} else if (toMany && Array.isArray(value)) {
|
|
243
|
+
entry.data[field] = value.map((id) => {
|
|
244
|
+
if (typeof id !== "string") {
|
|
245
|
+
return id;
|
|
246
|
+
}
|
|
247
|
+
const resolved = refEntries.find((e) => e.id === id);
|
|
248
|
+
if (!resolved) {
|
|
249
|
+
console.warn(`Content relationships: "${name}/${entry.id}" field "${field}" references missing "${target}" entry "${id}"`);
|
|
250
|
+
}
|
|
251
|
+
return resolved ?? id;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
class Content {
|
|
260
|
+
static async projectData(sectionValue, ctx) {
|
|
261
|
+
const section = sectionValue ?? {};
|
|
262
|
+
const data = await loadContentSection(section, ctx.root, ctx.registry.formats);
|
|
263
|
+
resolveContentTypeRefs(data, section);
|
|
264
|
+
return data;
|
|
265
|
+
}
|
|
266
|
+
static async resolvePaths(pathsDef, ctx) {
|
|
267
|
+
const entries = ctx.data.get(pathsDef.contentType);
|
|
268
|
+
if (!entries || entries.length === 0) {
|
|
269
|
+
console.warn(`Warning: $paths references content type "${pathsDef.contentType}" but it has no entries`);
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
const param = pathsDef.param ?? "slug";
|
|
273
|
+
const field = pathsDef.field ?? "id";
|
|
274
|
+
return entries.map((entry) => ({
|
|
275
|
+
[param]: field === "id" ? entry.id : entry.data[field] ?? entry.id
|
|
276
|
+
})).filter((p) => p[param]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
export {
|
|
280
|
+
resolveContentTypeRefs,
|
|
281
|
+
loadContentSection,
|
|
282
|
+
loadContentConfig,
|
|
283
|
+
getContentTypeElements,
|
|
284
|
+
Content
|
|
285
|
+
};
|
package/dist/content.js
CHANGED
|
@@ -140,7 +140,7 @@ class ContentCollection {
|
|
|
140
140
|
}
|
|
141
141
|
resolve() {
|
|
142
142
|
const { contentType, filter, sort, limit, _project } = this.config;
|
|
143
|
-
const entries = _project?.
|
|
143
|
+
const entries = _project?.content?.get(contentType ?? "");
|
|
144
144
|
if (!entries) {
|
|
145
145
|
return [];
|
|
146
146
|
}
|
|
@@ -163,7 +163,7 @@ class ContentEntry {
|
|
|
163
163
|
}
|
|
164
164
|
resolve() {
|
|
165
165
|
const { contentType, id, field, _project, _document } = this.config;
|
|
166
|
-
const entries = _project?.
|
|
166
|
+
const entries = _project?.content?.get(contentType ?? "");
|
|
167
167
|
if (!entries) {
|
|
168
168
|
return null;
|
|
169
169
|
}
|
package/dist/markdown.js
CHANGED
|
@@ -858,7 +858,7 @@ var init_unified = __esm(() => {
|
|
|
858
858
|
init_lib3();
|
|
859
859
|
});
|
|
860
860
|
|
|
861
|
-
//
|
|
861
|
+
// ../../packages/schema/src/guards.ts
|
|
862
862
|
function isJsonObject(value) {
|
|
863
863
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
864
864
|
}
|
|
@@ -30939,7 +30939,7 @@ var init_property_information3 = __esm(() => {
|
|
|
30939
30939
|
svg6 = merge3([aria3, svg5, xlink3, xmlns3, xml3], "svg");
|
|
30940
30940
|
});
|
|
30941
30941
|
|
|
30942
|
-
// src/html-to-jx.ts
|
|
30942
|
+
// ../../packages/markup/src/html-to-jx.ts
|
|
30943
30943
|
function htmlToJx(htmlString) {
|
|
30944
30944
|
const hast = fromHtml(htmlString, { fragment: true });
|
|
30945
30945
|
return convertHastChildren(hast.children);
|
|
@@ -46180,7 +46180,7 @@ __export(exports_md, {
|
|
|
46180
46180
|
Markdown: () => Markdown
|
|
46181
46181
|
});
|
|
46182
46182
|
import { readFileSync } from "node:fs";
|
|
46183
|
-
import { basename, extname, resolve as resolvePath2 } from "node:path";
|
|
46183
|
+
import { basename, extname, relative, resolve as resolvePath2 } from "node:path";
|
|
46184
46184
|
function visit3(tree, typeOrVisitor, maybeVisitor) {
|
|
46185
46185
|
const type = typeof typeOrVisitor === "string" ? typeOrVisitor : null;
|
|
46186
46186
|
const visitor = type ? maybeVisitor : typeOrVisitor;
|
|
@@ -46239,6 +46239,19 @@ function extractExcerpt(tree) {
|
|
|
46239
46239
|
}
|
|
46240
46240
|
return mdastToString(firstParagraph);
|
|
46241
46241
|
}
|
|
46242
|
+
function deriveSlug(filePath, sourceRoot) {
|
|
46243
|
+
if (sourceRoot) {
|
|
46244
|
+
const rel = relative(sourceRoot, filePath).split("\\").join("/");
|
|
46245
|
+
if (rel && !rel.startsWith("..") && rel.includes("/")) {
|
|
46246
|
+
let slug = rel.slice(0, rel.length - extname(rel).length);
|
|
46247
|
+
if (slug.endsWith("/index")) {
|
|
46248
|
+
slug = slug.slice(0, -"/index".length);
|
|
46249
|
+
}
|
|
46250
|
+
return slug;
|
|
46251
|
+
}
|
|
46252
|
+
}
|
|
46253
|
+
return basename(filePath, extname(filePath));
|
|
46254
|
+
}
|
|
46242
46255
|
function processMarkdown(source, filePath, config = {}) {
|
|
46243
46256
|
let processor = unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(import_remark_parse_frontmatter2.default).use(remarkGfm);
|
|
46244
46257
|
if (config.directives || config.directiveOptions) {
|
|
@@ -46253,7 +46266,7 @@ function processMarkdown(source, filePath, config = {}) {
|
|
|
46253
46266
|
const plainText = mdastToString(mdTree);
|
|
46254
46267
|
const toc = extractToc(tree);
|
|
46255
46268
|
const excerpt = extractExcerpt(tree);
|
|
46256
|
-
const slug =
|
|
46269
|
+
const slug = deriveSlug(filePath, config.sourceRoot);
|
|
46257
46270
|
const bodyNodes = tree.children.filter((n7) => n7.type !== "yaml" && n7.type !== "toml");
|
|
46258
46271
|
const $children = bodyNodes.map((n7) => mdastNodeToJx(n7)).filter(Boolean);
|
|
46259
46272
|
return {
|
|
@@ -46367,7 +46380,8 @@ class Markdown {
|
|
|
46367
46380
|
...options.directives !== undefined && { directives: options.directives },
|
|
46368
46381
|
...options.directiveOptions !== undefined && {
|
|
46369
46382
|
directiveOptions: options.directiveOptions
|
|
46370
|
-
}
|
|
46383
|
+
},
|
|
46384
|
+
...options.sourceRoot !== undefined && { sourceRoot: options.sourceRoot }
|
|
46371
46385
|
});
|
|
46372
46386
|
const _meta = {};
|
|
46373
46387
|
if (result.$excerpt != null) {
|
package/dist/serialize.js
CHANGED
|
@@ -858,7 +858,7 @@ var init_unified = __esm(() => {
|
|
|
858
858
|
init_lib3();
|
|
859
859
|
});
|
|
860
860
|
|
|
861
|
-
//
|
|
861
|
+
// ../../packages/schema/src/guards.ts
|
|
862
862
|
function isJsonObject(value) {
|
|
863
863
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
864
864
|
}
|
|
@@ -30939,7 +30939,7 @@ var init_property_information3 = __esm(() => {
|
|
|
30939
30939
|
svg6 = merge3([aria3, svg5, xlink3, xmlns3, xml3], "svg");
|
|
30940
30940
|
});
|
|
30941
30941
|
|
|
30942
|
-
// src/html-to-jx.ts
|
|
30942
|
+
// ../../packages/markup/src/html-to-jx.ts
|
|
30943
30943
|
function htmlToJx(htmlString) {
|
|
30944
30944
|
const hast = fromHtml(htmlString, { fragment: true });
|
|
30945
30945
|
return convertHastChildren(hast.children);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jxsuite/parser",
|
|
3
|
+
"title": "Content & Markdown",
|
|
4
|
+
"description": "File-based content collections with Markdown and CSV formats",
|
|
5
|
+
"classes": {
|
|
6
|
+
"Markdown": "./src/Markdown.class.json",
|
|
7
|
+
"Csv": "./src/Csv.class.json",
|
|
8
|
+
"MarkdownCollection": "./src/MarkdownCollection.class.json",
|
|
9
|
+
"ContentCollection": "./src/ContentCollection.class.json",
|
|
10
|
+
"ContentEntry": "./src/ContentEntry.class.json",
|
|
11
|
+
"Content": "./src/Content.class.json"
|
|
12
|
+
},
|
|
13
|
+
"schemas": {
|
|
14
|
+
"project": "./schemas/project.fragment.schema.json",
|
|
15
|
+
"document": "./schemas/document.fragment.schema.json"
|
|
16
|
+
}
|
|
17
|
+
}
|