@mzebley/mark-down 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 +132 -19
- package/package.json +15 -10
- package/src/angular/index.ts +47 -0
- package/src/errors.ts +19 -0
- package/src/front-matter.ts +111 -0
- package/src/inline.ts +80 -0
- package/src/markdown.ts +5 -0
- package/src/snippet-client.ts +337 -140
- package/src/types.ts +24 -21
- package/tsup.config.ts +26 -4
- package/dist/browser.d.ts +0 -60
- package/dist/browser.js +0 -311
- package/dist/browser.js.map +0 -1
- package/dist/chunk-TQ5Y4RZJ.mjs +0 -19
- package/dist/chunk-TQ5Y4RZJ.mjs.map +0 -1
- package/dist/index.cjs +0 -228
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -60
- package/dist/index.d.ts +0 -60
- package/dist/index.mjs +0 -179
- package/dist/index.mjs.map +0 -1
- package/dist/slug.cjs +0 -43
- package/dist/slug.cjs.map +0 -1
- package/dist/slug.d.cts +0 -3
- package/dist/slug.d.ts +0 -3
- package/dist/slug.mjs +0 -7
- package/dist/slug.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -9,11 +9,12 @@ This package provides the framework-agnostic `SnippetClient` and supporting type
|
|
|
9
9
|
2. [Quick start](#quick-start)
|
|
10
10
|
3. [Client options](#client-options)
|
|
11
11
|
4. [Working with snippets](#working-with-snippets)
|
|
12
|
-
5. [SSR and custom
|
|
12
|
+
5. [SSR and custom fetch functions](#ssr-and-custom-fetch-functions)
|
|
13
13
|
6. [Static sites / CDN usage](#static-sites--cdn-usage)
|
|
14
|
-
7. [
|
|
15
|
-
8. [
|
|
16
|
-
9. [
|
|
14
|
+
7. [Zero-build inline mode](#zero-build-inline-mode)
|
|
15
|
+
8. [Testing & type safety](#testing--type-safety)
|
|
16
|
+
9. [Related packages](#related-packages)
|
|
17
|
+
10. [Roadmap](#roadmap)
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
@@ -37,7 +38,7 @@ const client = new SnippetClient({
|
|
|
37
38
|
const hero = await client.get("getting-started-welcome");
|
|
38
39
|
const components = await client.listByType("component");
|
|
39
40
|
|
|
40
|
-
console.log(hero
|
|
41
|
+
console.log(hero.title);
|
|
41
42
|
```
|
|
42
43
|
|
|
43
44
|
The client lazily loads the manifest when first needed, then fetches Markdown files on demand. Results are cached for the lifetime of the client instance.
|
|
@@ -46,25 +47,30 @@ The client lazily loads the manifest when first needed, then fetches Markdown fi
|
|
|
46
47
|
|
|
47
48
|
`SnippetClient` accepts a single configuration object:
|
|
48
49
|
|
|
49
|
-
- **`manifest`** (`string |
|
|
50
|
-
- **`
|
|
51
|
-
- **`
|
|
52
|
-
- **`
|
|
50
|
+
- **`manifest`** (`string | SnippetMeta[] | () => Promise<SnippetMeta[]>`) – where to load the manifest. Provide a URL, an in-memory array, or an async factory.
|
|
51
|
+
- **`base`** (`string`) – optional base path prepended to relative snippet paths. The client infers the directory from the manifest URL when omitted.
|
|
52
|
+
- **`fetch`** (`(url: string) => Promise<Response | string>`) – inject a custom fetch implementation. Use this for SSR, testing, or advanced caching.
|
|
53
|
+
- **`frontMatter`** (`boolean`, default `true`) – toggle YAML front-matter parsing.
|
|
54
|
+
- **`cache`** (`boolean`, default `true`) – enable or disable per-snippet and manifest memoisation.
|
|
55
|
+
- **`verbose`** (`boolean`) – log helpful warnings (for example, slug mismatches) during development.
|
|
56
|
+
- **`render`** (`(markdown: string) => string | Promise<string>`) – override the default `marked` renderer when you need custom HTML output.
|
|
53
57
|
|
|
54
|
-
All options are optional except `manifest`.
|
|
58
|
+
All options are optional except `manifest`. Results are rendered with `marked` by default; override at the application level if you need a different Markdown pipeline.
|
|
55
59
|
|
|
56
60
|
## Working with snippets
|
|
57
61
|
|
|
58
62
|
Commonly used APIs:
|
|
59
63
|
|
|
60
|
-
- `client.get(slug)` – fetch a single snippet.
|
|
61
|
-
- `client.
|
|
62
|
-
- `client.listByType(type
|
|
63
|
-
- `client.
|
|
64
|
+
- `client.get(slug)` – fetch a single snippet. Throws `SnippetNotFoundError` if the manifest does not include the slug.
|
|
65
|
+
- `client.listAll()` – return a copy of every manifest entry.
|
|
66
|
+
- `client.listByType(type)` / `client.listByGroup(group)` – targeted manifest filters.
|
|
67
|
+
- `client.search({ type, group, tags, tagsMode })` – multi-field search helper with tag matching.
|
|
68
|
+
- `client.getHtml(slug)` – convenience wrapper that resolves directly to HTML.
|
|
69
|
+
- `client.invalidate()` / `client.invalidateSlug(slug)` – clear caches to force refetching.
|
|
64
70
|
|
|
65
|
-
Metadata is preserved exactly as declared in front matter. Standard keys (`slug`, `title`, etc.) are copied onto `SnippetMeta` and
|
|
71
|
+
Metadata is preserved exactly as declared in front matter. Standard keys (`slug`, `title`, etc.) are copied onto `SnippetMeta`, additional properties live inside `extra`, and the resolved `Snippet` includes both rendered HTML and an optional `raw` Markdown string (without front matter) for advanced use cases.
|
|
66
72
|
|
|
67
|
-
## SSR and custom
|
|
73
|
+
## SSR and custom fetch functions
|
|
68
74
|
|
|
69
75
|
The runtime runs in browsers, Node.js, or edge runtimes. For server-side rendering:
|
|
70
76
|
|
|
@@ -74,11 +80,16 @@ import { SnippetClient } from "@mzebley/mark-down";
|
|
|
74
80
|
|
|
75
81
|
const client = new SnippetClient({
|
|
76
82
|
manifest: () => import("./snippets-index.json"),
|
|
77
|
-
|
|
83
|
+
fetch: (url) => fetch(url).then((response) => {
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`Request failed with status ${response.status}`);
|
|
86
|
+
}
|
|
87
|
+
return response;
|
|
88
|
+
}),
|
|
78
89
|
});
|
|
79
90
|
```
|
|
80
91
|
|
|
81
|
-
You can also pre-seed snippets by passing an array to `manifest` to avoid network requests entirely.
|
|
92
|
+
You can also pre-seed snippets by passing an array to `manifest` to avoid network requests entirely. When `cache` is disabled the client re-fetches both manifest and snippet payloads on every request.
|
|
82
93
|
|
|
83
94
|
## Static sites / CDN usage
|
|
84
95
|
|
|
@@ -96,10 +107,112 @@ Need to run mark↓ inside a plain `<script type="module">` context? Use the pre
|
|
|
96
107
|
|
|
97
108
|
As long as you host `snippets-index.json` (generated by the CLI) alongside your static assets, this bundle works without any additional tooling or manual Buffer shims.
|
|
98
109
|
|
|
110
|
+
## Zero-build inline mode
|
|
111
|
+
|
|
112
|
+
### Introduction
|
|
113
|
+
|
|
114
|
+
Need Markdown inside plain HTML without a manifest, build step, or Node runtime? mark↓ ships a lightweight inline helper that scans the DOM for raw Markdown, renders it with the same `marked` pipeline used by `SnippetClient`, and progressively enhances the page. Content stays inside the HTML you ship, so crawlers and no-JS users can still read the raw Markdown even before JavaScript runs.
|
|
115
|
+
|
|
116
|
+
### Installation
|
|
117
|
+
|
|
118
|
+
Install the core package as usual:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm install @mzebley/mark-down
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- **Bundlers / ESM:** import the inline helper directly.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { enhanceInlineMarkdown } from "@mzebley/mark-down/inline";
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **Static `<script>` usage:** include the prebuilt UMD bundle that exposes `window.markDownInline`.
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<script src="/node_modules/@mzebley/mark-down/dist/mark-down-inline.umd.js"></script>
|
|
134
|
+
<script>
|
|
135
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
136
|
+
window.markDownInline.enhanceInlineMarkdown();
|
|
137
|
+
});
|
|
138
|
+
</script>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Host the UMD file yourself or load it from a CDN—no build tooling required.
|
|
142
|
+
|
|
143
|
+
### Basic usage (happy path)
|
|
144
|
+
|
|
145
|
+
Write plain Markdown directly in your HTML. The helper finds `[data-markdown]` blocks by default.
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<div data-markdown>
|
|
149
|
+
# Hello
|
|
150
|
+
|
|
151
|
+
This is inline markdown with no build step.
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<script src="path/to/mark-down-inline.umd.js"></script>
|
|
155
|
+
<script>
|
|
156
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
157
|
+
window.markDownInline.enhanceInlineMarkdown();
|
|
158
|
+
});
|
|
159
|
+
</script>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The Markdown text remains in the DOM for SEO while JavaScript enhances it to semantic HTML for users.
|
|
163
|
+
|
|
164
|
+
### Advanced usage (power path with front matter)
|
|
165
|
+
|
|
166
|
+
You can optionally prepend YAML front matter to provide metadata for each block. Fields are parsed with the same utilities used by the manifest flow.
|
|
167
|
+
|
|
168
|
+
```html
|
|
169
|
+
<div data-markdown>
|
|
170
|
+
---
|
|
171
|
+
slug: intro
|
|
172
|
+
title: Introduction
|
|
173
|
+
tags: [hero]
|
|
174
|
+
variant: lead
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
# Introduction
|
|
178
|
+
|
|
179
|
+
This block has metadata.
|
|
180
|
+
</div>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- `slug` → sets `id="intro"` if no ID exists and `data-slug="intro"`, allowing deep links.
|
|
184
|
+
- `title` → stored in `data-title` for TOC/index generation.
|
|
185
|
+
- `tags` → serialized to `data-tags="hero"` (comma-separated when multiple) for light-weight client-side filtering.
|
|
186
|
+
- `variant` → adds a class like `md-block--lead` so you can theme specific sections.
|
|
187
|
+
|
|
188
|
+
You still get fully rendered HTML, but additional attributes unlock progressive enhancement such as TOCs or custom styling.
|
|
189
|
+
|
|
190
|
+
### Options
|
|
191
|
+
|
|
192
|
+
Configure the helper per page:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
enhanceInlineMarkdown({
|
|
196
|
+
selector?: string; // defaults to "[data-markdown]"
|
|
197
|
+
processFrontMatter?: boolean; // defaults to true
|
|
198
|
+
applyMetaToDom?: boolean; // defaults to true
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
- Use `selector` to target different attributes or classes.
|
|
203
|
+
- Disable `processFrontMatter` when the block contains literal `---` fences that should stay visible.
|
|
204
|
+
- Set `applyMetaToDom` to `false` if you only care about rendered HTML and not DOM metadata.
|
|
205
|
+
|
|
206
|
+
### SEO & progressive enhancement
|
|
207
|
+
|
|
208
|
+
- Raw Markdown lives directly in your HTML, so crawlers can index the text before scripts execute.
|
|
209
|
+
- Users without JavaScript still see readable Markdown; JavaScript simply upgrades it to semantic HTML, adds IDs, and decorates DOM attributes for richer UX.
|
|
210
|
+
- This inline mode is ideal for small/medium static pages or marketing sites. Larger doc sites should continue using the manifest-driven snippet system for better caching and composition.
|
|
211
|
+
|
|
99
212
|
## Testing & type safety
|
|
100
213
|
|
|
101
214
|
- Use `SnippetMeta` and `Snippet` TypeScript types to describe props and state in your application.
|
|
102
|
-
- Mock the client by providing a manifest array or by stubbing the `
|
|
215
|
+
- Mock the client by providing a manifest array or by stubbing the `fetch` option.
|
|
103
216
|
- Run the workspace tests with `npm run test -- core` to exercise Vitest suites that cover caching, filtering, and Markdown conversion.
|
|
104
217
|
|
|
105
218
|
## Related packages
|
package/package.json
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mzebley/mark-down",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "mark↓ core runtime and shared utilities",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.cjs",
|
|
7
|
-
"module": "dist/index.mjs",
|
|
8
6
|
"types": "dist/index.d.ts",
|
|
9
7
|
"publishConfig": {
|
|
10
8
|
"access": "public"
|
|
@@ -12,17 +10,23 @@
|
|
|
12
10
|
"exports": {
|
|
13
11
|
".": {
|
|
14
12
|
"types": "./dist/index.d.ts",
|
|
15
|
-
"import": "./dist/index.
|
|
16
|
-
"require": "./dist/index.cjs"
|
|
13
|
+
"import": "./dist/index.js"
|
|
17
14
|
},
|
|
18
15
|
"./slug": {
|
|
19
16
|
"types": "./dist/slug.d.ts",
|
|
20
|
-
"import": "./dist/slug.
|
|
21
|
-
"require": "./dist/slug.cjs"
|
|
17
|
+
"import": "./dist/slug.js"
|
|
22
18
|
},
|
|
23
19
|
"./browser": {
|
|
24
20
|
"types": "./dist/browser.d.ts",
|
|
25
21
|
"import": "./dist/browser.js"
|
|
22
|
+
},
|
|
23
|
+
"./inline": {
|
|
24
|
+
"types": "./dist/inline.d.ts",
|
|
25
|
+
"import": "./dist/inline.js"
|
|
26
|
+
},
|
|
27
|
+
"./angular": {
|
|
28
|
+
"types": "./dist/angular/index.d.ts",
|
|
29
|
+
"import": "./dist/angular/index.js"
|
|
26
30
|
}
|
|
27
31
|
},
|
|
28
32
|
"scripts": {
|
|
@@ -30,7 +34,8 @@
|
|
|
30
34
|
"dev": "tsup --config tsup.config.ts --watch"
|
|
31
35
|
},
|
|
32
36
|
"dependencies": {
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
}
|
|
37
|
+
"marked": "^11.1.0",
|
|
38
|
+
"yaml": "^2.4.1"
|
|
39
|
+
},
|
|
40
|
+
"sideEffects": false
|
|
36
41
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Inject, Injectable, InjectionToken, Provider } from "@angular/core";
|
|
2
|
+
import { from, map, Observable, shareReplay } from "rxjs";
|
|
3
|
+
import { SnippetClient } from "../snippet-client";
|
|
4
|
+
import type { Snippet, SnippetClientOptions, SnippetMeta } from "../types";
|
|
5
|
+
|
|
6
|
+
export const SNIPPET_CLIENT = new InjectionToken<SnippetClient>("@mzebley/mark-down/SNIPPET_CLIENT");
|
|
7
|
+
export const SNIPPET_CLIENT_OPTIONS = new InjectionToken<SnippetClientOptions>(
|
|
8
|
+
"@mzebley/mark-down/SNIPPET_CLIENT_OPTIONS"
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export function provideSnippetClient(options: SnippetClientOptions): Provider[] {
|
|
12
|
+
return [
|
|
13
|
+
{ provide: SNIPPET_CLIENT_OPTIONS, useValue: options },
|
|
14
|
+
{
|
|
15
|
+
provide: SNIPPET_CLIENT,
|
|
16
|
+
useFactory: (opts: SnippetClientOptions) => new SnippetClient(opts),
|
|
17
|
+
deps: [SNIPPET_CLIENT_OPTIONS]
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Injectable({ providedIn: "root" })
|
|
23
|
+
export class MarkdownSnippetService {
|
|
24
|
+
constructor(@Inject(SNIPPET_CLIENT) private readonly client: SnippetClient) {}
|
|
25
|
+
|
|
26
|
+
get(slug: string): Observable<Snippet> {
|
|
27
|
+
return from(this.client.get(slug)).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
listAll(): Observable<SnippetMeta[]> {
|
|
31
|
+
return from(this.client.listAll()).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
listByGroup(group: string): Observable<SnippetMeta[]> {
|
|
35
|
+
return from(this.client.listByGroup(group)).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
listByType(type: string): Observable<SnippetMeta[]> {
|
|
39
|
+
return from(this.client.listByType(type)).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
html(slug: string): Observable<string> {
|
|
43
|
+
return this.get(slug).pipe(map((snippet) => snippet.html));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type { Snippet, SnippetClientOptions, SnippetMeta } from "../types";
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class SnippetNotFoundError extends Error {
|
|
2
|
+
readonly slug: string;
|
|
3
|
+
|
|
4
|
+
constructor(slug: string) {
|
|
5
|
+
super(`Snippet with slug '${slug}' was not found in the manifest.`);
|
|
6
|
+
this.name = "SnippetNotFoundError";
|
|
7
|
+
this.slug = slug;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ManifestLoadError extends Error {
|
|
12
|
+
readonly cause?: unknown;
|
|
13
|
+
|
|
14
|
+
constructor(message: string, cause?: unknown) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "ManifestLoadError";
|
|
17
|
+
this.cause = cause instanceof Error ? cause : cause ? new Error(String(cause)) : undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
import { ManifestLoadError } from "./errors";
|
|
3
|
+
import type { SnippetMeta } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface FrontMatterResult {
|
|
6
|
+
content: string;
|
|
7
|
+
meta: Partial<SnippetMeta>;
|
|
8
|
+
extra: Record<string, unknown>;
|
|
9
|
+
slug?: string;
|
|
10
|
+
hasFrontMatter: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const FRONT_MATTER_PATTERN = /^(?:\uFEFF)?[ \t\r\n]*---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/;
|
|
14
|
+
|
|
15
|
+
export function parseFrontMatter(raw: string): FrontMatterResult {
|
|
16
|
+
const match = FRONT_MATTER_PATTERN.exec(raw);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return { content: raw, meta: {}, extra: {}, hasFrontMatter: false };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const yamlSection = match[1];
|
|
22
|
+
let data: unknown;
|
|
23
|
+
try {
|
|
24
|
+
data = parseYaml(yamlSection) ?? {};
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new ManifestLoadError("Failed to parse snippet front-matter.", error);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!isRecord(data)) {
|
|
30
|
+
return { content: raw.slice(match[0].length), meta: {}, extra: {}, hasFrontMatter: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { known, extra } = splitFrontMatter(data);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
content: raw.slice(match[0].length),
|
|
37
|
+
meta: known.meta,
|
|
38
|
+
extra,
|
|
39
|
+
slug: known.slug,
|
|
40
|
+
hasFrontMatter: true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function splitFrontMatter(
|
|
45
|
+
data: Record<string, unknown>
|
|
46
|
+
): { known: { meta: Partial<SnippetMeta>; slug?: string }; extra: Record<string, unknown> } {
|
|
47
|
+
const meta: Partial<SnippetMeta> = {};
|
|
48
|
+
const extra: Record<string, unknown> = {};
|
|
49
|
+
let slug: string | undefined;
|
|
50
|
+
|
|
51
|
+
for (const [key, value] of Object.entries(data)) {
|
|
52
|
+
switch (key) {
|
|
53
|
+
case "slug":
|
|
54
|
+
slug = typeof value === "string" ? value : undefined;
|
|
55
|
+
break;
|
|
56
|
+
case "title":
|
|
57
|
+
if (typeof value === "string") {
|
|
58
|
+
meta.title = value;
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
case "type":
|
|
62
|
+
if (typeof value === "string") {
|
|
63
|
+
meta.type = value;
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case "order":
|
|
67
|
+
if (typeof value === "number") {
|
|
68
|
+
meta.order = value;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
case "tags":
|
|
72
|
+
meta.tags = normalizeTags(value);
|
|
73
|
+
break;
|
|
74
|
+
case "group":
|
|
75
|
+
if (typeof value === "string" || value === null) {
|
|
76
|
+
meta.group = value;
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case "draft":
|
|
80
|
+
if (typeof value === "boolean") {
|
|
81
|
+
meta.draft = value;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
extra[key] = value;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { known: { meta, slug }, extra };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeTags(value: unknown): string[] | undefined {
|
|
94
|
+
if (!value) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
return value.map((item) => String(item));
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === "string") {
|
|
101
|
+
return value
|
|
102
|
+
.split(",")
|
|
103
|
+
.map((tag) => tag.trim())
|
|
104
|
+
.filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isRecord(candidate: unknown): candidate is Record<string, unknown> {
|
|
110
|
+
return Boolean(candidate) && typeof candidate === "object" && !Array.isArray(candidate);
|
|
111
|
+
}
|
package/src/inline.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { parseFrontMatter, type FrontMatterResult } from "./front-matter";
|
|
2
|
+
import { renderMarkdown } from "./markdown";
|
|
3
|
+
|
|
4
|
+
export interface InlineMarkdownOptions {
|
|
5
|
+
selector?: string;
|
|
6
|
+
processFrontMatter?: boolean;
|
|
7
|
+
applyMetaToDom?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DEFAULT_SELECTOR = "[data-markdown]";
|
|
11
|
+
|
|
12
|
+
export function enhanceInlineMarkdown(options: InlineMarkdownOptions = {}): void {
|
|
13
|
+
if (typeof document === "undefined") {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const selector = options.selector ?? DEFAULT_SELECTOR;
|
|
18
|
+
const processFrontMatter = options.processFrontMatter !== false;
|
|
19
|
+
const applyMetaToDom = options.applyMetaToDom !== false;
|
|
20
|
+
|
|
21
|
+
const elements = Array.from(document.querySelectorAll<HTMLElement>(selector));
|
|
22
|
+
for (const element of elements) {
|
|
23
|
+
if (element.dataset.markdownProcessed === "true") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
processElement(element, { processFrontMatter, applyMetaToDom });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function processElement(
|
|
31
|
+
element: HTMLElement,
|
|
32
|
+
options: { processFrontMatter: boolean; applyMetaToDom: boolean }
|
|
33
|
+
): void {
|
|
34
|
+
const raw = element.textContent ?? "";
|
|
35
|
+
let frontMatter: FrontMatterResult | undefined;
|
|
36
|
+
|
|
37
|
+
if (options.processFrontMatter) {
|
|
38
|
+
try {
|
|
39
|
+
frontMatter = parseFrontMatter(raw);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.warn("[mark↓ inline] Failed to parse front matter for element:", error);
|
|
42
|
+
frontMatter = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const body = frontMatter?.content ?? raw;
|
|
47
|
+
const html = renderMarkdown(body);
|
|
48
|
+
|
|
49
|
+
element.innerHTML = html;
|
|
50
|
+
element.dataset.markdownProcessed = "true";
|
|
51
|
+
|
|
52
|
+
if (options.applyMetaToDom && frontMatter?.hasFrontMatter) {
|
|
53
|
+
applyMetaAttributes(element, frontMatter);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function applyMetaAttributes(element: HTMLElement, frontMatter: FrontMatterResult): void {
|
|
58
|
+
const { slug, meta, extra } = frontMatter;
|
|
59
|
+
|
|
60
|
+
if (slug) {
|
|
61
|
+
if (!element.id) {
|
|
62
|
+
element.id = slug;
|
|
63
|
+
}
|
|
64
|
+
element.dataset.slug = slug;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (meta.title) {
|
|
68
|
+
element.dataset.title = meta.title;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (meta.tags?.length) {
|
|
72
|
+
element.dataset.tags = meta.tags.join(",");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const variant = typeof extra.variant === "string" ? extra.variant.trim() : "";
|
|
76
|
+
if (variant) {
|
|
77
|
+
element.classList.add(`md-block--${variant}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
}
|