@jxsuite/parser 1.0.0 → 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
 
@@ -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
  }
package/dist/markdown.js CHANGED
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/parser",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Jx markdown parser and external class integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,8 +48,8 @@
48
48
  "upgrade": "bunx npm-check-updates -u && bun install"
49
49
  },
50
50
  "dependencies": {
51
- "@jxsuite/markup": "^0.2.0",
52
- "@jxsuite/schema": "^1.0.0",
51
+ "@jxsuite/markup": "^0.3.0",
52
+ "@jxsuite/schema": "^1.1.0",
53
53
  "glob": "^13.0.6",
54
54
  "mdast-util-to-string": "^4.0.0",
55
55
  "remark-directive": "^4.0.0",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "devDependencies": {
66
66
  "@happy-dom/global-registrator": "^20.10.6",
67
- "@jxsuite/runtime": "^1.0.0",
67
+ "@jxsuite/runtime": "^1.1.0",
68
68
  "@types/mdast": "^4.0.4",
69
69
  "ajv": "^8.20.0"
70
70
  },
@@ -17,7 +17,7 @@
17
17
 
18
18
  "$studio": {
19
19
  "icon": "table",
20
- "modes": ["source"]
20
+ "modes": ["grid", "source"]
21
21
  },
22
22
 
23
23
  "$defs": {
@@ -269,12 +269,15 @@ async function loadContentType(
269
269
  })) as string[])
270
270
  : [resolvedSource];
271
271
 
272
+ // Directory sources pass their resolved root so formats can derive path-based ids for nested files
273
+ const sourceRoot = extname(source) ? undefined : resolvedSource;
272
274
  const entries: ContentLoaderEntry[] = [];
273
275
  for (const filePath of files) {
274
276
  entries.push(
275
277
  ...((await entry.call("load", filePath, {
276
278
  directiveOptions,
277
279
  schema,
280
+ ...(sourceRoot !== undefined && { sourceRoot }),
278
281
  })) as ContentLoaderEntry[]),
279
282
  );
280
283
  }
@@ -374,7 +377,7 @@ function refTargetName(ref: string | undefined): string | undefined {
374
377
  * field `$ref` is to-one: a blog post's `author: "jane-doe"` with a schema `$ref` of
375
378
  * `#/content/authors` gets replaced by the full author entry. An array field whose `items` carry
376
379
  * the `$ref` is to-many: each id in the stored array is replaced by its entry. Unresolvable ids are
377
- * left untouched.
380
+ * left untouched, with a warning naming the entry and the missing target.
378
381
  *
379
382
  * @param {Map<string, ContentLoaderEntry[]>} contentTypes - All loaded content types
380
383
  * @param {ContentSection} section - The `content` section value the types were loaded from
@@ -403,6 +406,9 @@ export function resolveContentTypeRefs(
403
406
  }
404
407
  const refEntries = contentTypes.get(target);
405
408
  if (!refEntries) {
409
+ console.warn(
410
+ `Content relationships: "${name}" field "${field}" references unknown content type "${target}"`,
411
+ );
406
412
  continue;
407
413
  }
408
414
 
@@ -412,11 +418,24 @@ export function resolveContentTypeRefs(
412
418
  const resolved = refEntries.find((e) => e.id === value);
413
419
  if (resolved) {
414
420
  entry.data[field] = resolved;
421
+ } else {
422
+ console.warn(
423
+ `Content relationships: "${name}/${entry.id}" field "${field}" references missing "${target}" entry "${value}"`,
424
+ );
415
425
  }
416
426
  } else if (toMany && Array.isArray(value)) {
417
- entry.data[field] = value.map((id) =>
418
- typeof id === "string" ? (refEntries.find((e) => e.id === id) ?? id) : id,
419
- );
427
+ entry.data[field] = value.map((id) => {
428
+ if (typeof id !== "string") {
429
+ return id;
430
+ }
431
+ const resolved = refEntries.find((e) => e.id === id);
432
+ if (!resolved) {
433
+ console.warn(
434
+ `Content relationships: "${name}/${entry.id}" field "${field}" references missing "${target}" entry "${id}"`,
435
+ );
436
+ }
437
+ return resolved ?? id;
438
+ });
420
439
  }
421
440
  }
422
441
  }
package/src/markdown.ts CHANGED
@@ -26,6 +26,12 @@ export interface MarkdownLoadOptions {
26
26
  directiveOptions?: unknown;
27
27
  /** Enable remark-directive parsing. Implied by directiveOptions. */
28
28
  directives?: boolean;
29
+ /**
30
+ * Resolved content-source root directory. When set, entries in subdirectories get path-based ids
31
+ * (`studio/canvas` for `studio/canvas.md`, `/index` stripped); files directly at the root keep
32
+ * basename ids.
33
+ */
34
+ sourceRoot?: string;
29
35
  }
30
36
 
31
37
  /**
@@ -83,6 +89,7 @@ export class Markdown {
83
89
  ...(options.directiveOptions !== undefined && {
84
90
  directiveOptions: options.directiveOptions,
85
91
  }),
92
+ ...(options.sourceRoot !== undefined && { sourceRoot: options.sourceRoot }),
86
93
  });
87
94
  const _meta: ContentLoaderEntry["_meta"] = {};
88
95
  if (result.$excerpt != null) {
package/src/md.ts CHANGED
@@ -18,7 +18,7 @@ import remarkParseFrontmatter from "remark-parse-frontmatter";
18
18
  import remarkGfm from "remark-gfm";
19
19
  import remarkDirective from "remark-directive";
20
20
  import { readFileSync } from "node:fs";
21
- import { basename, extname, resolve as resolvePath } from "node:path";
21
+ import { basename, extname, relative, resolve as resolvePath } from "node:path";
22
22
  import { globSync } from "glob";
23
23
  import { mdastNodeToJx } from "./transpile.ts";
24
24
  import type { MarkdownFileResult, MdastNode, TocEntry, UnifiedProcessor } from "./types.ts";
@@ -133,6 +133,30 @@ function extractExcerpt(tree: MdastNode) {
133
133
  return mdastToString(firstParagraph);
134
134
  }
135
135
 
136
+ /**
137
+ * Derive an entry slug from a file path. Without a source root (or for files directly at the root),
138
+ * the slug is the basename — the historical behavior every flat collection relies on. Files in
139
+ * subdirectories of the root get path-based slugs with POSIX separators and a trailing `/index`
140
+ * stripped, so `studio/canvas.md` and `studio/canvas/index.md` both yield `studio/canvas`.
141
+ *
142
+ * @param {string} filePath - Absolute path to the markdown file
143
+ * @param {string} [sourceRoot] - Resolved content-source root directory
144
+ * @returns {string} The entry slug
145
+ */
146
+ function deriveSlug(filePath: string, sourceRoot?: string): string {
147
+ if (sourceRoot) {
148
+ const rel = relative(sourceRoot, filePath).split("\\").join("/");
149
+ if (rel && !rel.startsWith("..") && rel.includes("/")) {
150
+ let slug = rel.slice(0, rel.length - extname(rel).length);
151
+ if (slug.endsWith("/index")) {
152
+ slug = slug.slice(0, -"/index".length);
153
+ }
154
+ return slug;
155
+ }
156
+ }
157
+ return basename(filePath, extname(filePath));
158
+ }
159
+
136
160
  /**
137
161
  * Process a single markdown source string into a MarkdownFileResult.
138
162
  *
@@ -143,6 +167,7 @@ function extractExcerpt(tree: MdastNode) {
143
167
  * @param {object} config - Processing options
144
168
  * @param {boolean} [config.directives] - Enable directive support
145
169
  * @param {unknown} [config.directiveOptions] - Directive plugin options
170
+ * @param {string} [config.sourceRoot] - Content-source root; files below it get path-based slugs
146
171
  * @returns {MarkdownFileResult}
147
172
  */
148
173
  export function processMarkdown(
@@ -151,6 +176,7 @@ export function processMarkdown(
151
176
  config: {
152
177
  directives?: boolean;
153
178
  directiveOptions?: unknown;
179
+ sourceRoot?: string;
154
180
  } = {},
155
181
  ) {
156
182
  let processor = (unified as unknown as () => UnifiedProcessor)()
@@ -173,7 +199,7 @@ export function processMarkdown(
173
199
  const plainText = mdastToString(mdTree);
174
200
  const toc = extractToc(tree);
175
201
  const excerpt = extractExcerpt(tree);
176
- const slug = basename(filePath, extname(filePath));
202
+ const slug = deriveSlug(filePath, config.sourceRoot);
177
203
 
178
204
  const bodyNodes = tree.children!.filter(
179
205
  (n: MdastNode) => n.type !== "yaml" && n.type !== "toml",