@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 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
 
@@ -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?.contentTypes?.get(contentType ?? "");
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?.contentTypes?.get(contentType ?? "");
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
- // ../schema/src/guards.ts
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 = basename(filePath, extname(filePath));
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
- // ../schema/src/guards.ts
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
+ }