@jxsuite/parser 1.0.0 → 1.2.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 CHANGED
@@ -1,115 +1,156 @@
1
1
  # `@jxsuite/parser`
2
2
 
3
- > Markdown parser and external class integration for Jx.
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` provides the content layer for Jx applications. It exports `MarkdownFile` and `MarkdownCollection` — external classes that satisfy the Jx `$prototype` + `$src` contract — and a `transpile` utility for converting Jx-flavored Markdown directly into Jx component objects.
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
- Built on the [`unified`](https://unifiedjs.com/) / remark / rehype pipeline.
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
- ## Installation
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
- ## `MarkdownFile`
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
- "state": {
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
- **Resolved value:**
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
- | Property | Type | Description |
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
- ## `MarkdownCollection`
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
- Globs Markdown files into a sorted, filterable collection.
56
+ Query them from any page or component:
49
57
 
50
58
  ```json
51
59
  {
52
60
  "state": {
53
61
  "posts": {
54
- "$prototype": "MarkdownCollection",
55
- "$src": "@jxsuite/parser/MarkdownCollection.class.json",
56
- "src": "./content/posts/*.md",
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
- Returns an array of `MarkdownFile` resolved objects.
76
+ `ContentEntry` matches on the entry id (slug) by default; set `field` to match a frontmatter field (e.g. `"sku"`) instead.
66
77
 
67
- | Option | Type | Default | Description |
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
- ## `MarkdownDirective` (remark plugin)
80
+ One class carries every format capability:
76
81
 
77
- Maps `::directive{attrs}` syntax to custom element tags in HTML output. Parameters are encoded as a `data-jx-props` JSON attribute so Jx components can receive them via reactive state.
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
- ```markdown
80
- :::my-card{title="Hello" image="/img/hero.png"}
81
- Card body content here.
82
- :::
87
+ ```json
88
+ {
89
+ "state": {
90
+ "post": { "$prototype": "Markdown", "src": "./content/posts/hello-world.md" }
91
+ }
92
+ }
83
93
  ```
84
94
 
85
- Produces:
95
+ **Resolved value** (`MarkdownFileResult`):
86
96
 
87
- ```html
88
- <my-card data-jx-props='{"title":"Hello","image":"/img/hero.png"}'>
89
- <p>Card body content here.</p>
90
- </my-card>
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
- ## `transpileJxMarkdown`
114
+ CSV content format (`documentKinds: ["content"]`; Studio opens `.csv` in grid/source modes).
94
115
 
95
- Converts a Jx-flavored Markdown source string directly into a Jx component object (no file I/O).
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
- ```js
98
- import { transpileJxMarkdown } from "@jxsuite/parser/transpile";
122
+ ## `MarkdownCollection`
123
+
124
+ Globs markdown files into a sorted array of `MarkdownFileResult`s (runtime alternative to the content section):
99
125
 
100
- const doc = transpileJxMarkdown(markdownString);
101
- // { tagName: "article", children: [...] }
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
- ## Parsing pipeline
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
- 1. `remark-parse` — Markdown → MDAST
107
- 2. `remark-frontmatter` + `remark-parse-frontmatter` — YAML extraction
108
- 3. `remark-gfm` — GitHub Flavored Markdown
109
- 4. `remark-directive` — directive syntax
110
- 5. `MarkdownDirective` — directive nodes → custom element tags
111
- 6. `remark-rehype` — MDAST → HAST
112
- 7. `rehype-stringify` — HAST → HTML
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
 
@@ -155,11 +155,13 @@ async function loadContentType(name, contentTypeDef, projectRoot, registry) {
155
155
  const files = entry.capabilities.discover ? await entry.call("discover", source, {
156
156
  baseDir: projectRoot
157
157
  }) : [resolvedSource];
158
+ const sourceRoot = extname(source) ? undefined : resolvedSource;
158
159
  const entries = [];
159
160
  for (const filePath of files) {
160
161
  entries.push(...await entry.call("load", filePath, {
161
162
  directiveOptions,
162
- schema
163
+ schema,
164
+ ...sourceRoot !== undefined && { sourceRoot }
163
165
  }));
164
166
  }
165
167
  if (schema) {
@@ -225,6 +227,7 @@ function resolveContentTypeRefs(contentTypes, section) {
225
227
  }
226
228
  const refEntries = contentTypes.get(target);
227
229
  if (!refEntries) {
230
+ console.warn(`Content relationships: "${name}" field "${field}" references unknown content type "${target}"`);
228
231
  continue;
229
232
  }
230
233
  for (const entry of entries) {
@@ -233,9 +236,20 @@ function resolveContentTypeRefs(contentTypes, section) {
233
236
  const resolved = refEntries.find((e) => e.id === value);
234
237
  if (resolved) {
235
238
  entry.data[field] = resolved;
239
+ } else {
240
+ console.warn(`Content relationships: "${name}/${entry.id}" field "${field}" references missing "${target}" entry "${value}"`);
236
241
  }
237
242
  } else if (toMany && Array.isArray(value)) {
238
- entry.data[field] = value.map((id) => typeof id === "string" ? refEntries.find((e) => e.id === id) ?? id : id);
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
+ });
239
253
  }
240
254
  }
241
255
  }