@mzebley/mark-down 1.0.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 +118 -0
- package/dist/browser.d.ts +60 -0
- package/dist/browser.js +311 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-TQ5Y4RZJ.mjs +19 -0
- package/dist/chunk-TQ5Y4RZJ.mjs.map +1 -0
- package/dist/index.cjs +228 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +60 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.mjs +179 -0
- package/dist/index.mjs.map +1 -0
- package/dist/slug.cjs +43 -0
- package/dist/slug.cjs.map +1 -0
- package/dist/slug.d.cts +3 -0
- package/dist/slug.d.ts +3 -0
- package/dist/slug.mjs +7 -0
- package/dist/slug.mjs.map +1 -0
- package/package.json +36 -0
- package/src/browser.ts +141 -0
- package/src/index.ts +3 -0
- package/src/slug.ts +21 -0
- package/src/snippet-client.ts +215 -0
- package/src/types.ts +46 -0
- package/tsconfig.json +7 -0
- package/tsup.config.ts +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# mark↓ Core Runtime
|
|
2
|
+
*(published as `@mzebley/mark-down`)*
|
|
3
|
+
|
|
4
|
+
This package provides the framework-agnostic `SnippetClient` and supporting types used to fetch, cache, and render Markdown snippets at runtime. For a monorepo overview visit the [root README](../../README.md).
|
|
5
|
+
|
|
6
|
+
## Table of contents
|
|
7
|
+
|
|
8
|
+
1. [Installation](#installation)
|
|
9
|
+
2. [Quick start](#quick-start)
|
|
10
|
+
3. [Client options](#client-options)
|
|
11
|
+
4. [Working with snippets](#working-with-snippets)
|
|
12
|
+
5. [SSR and custom fetchers](#ssr-and-custom-fetchers)
|
|
13
|
+
6. [Static sites / CDN usage](#static-sites--cdn-usage)
|
|
14
|
+
7. [Testing & type safety](#testing--type-safety)
|
|
15
|
+
8. [Related packages](#related-packages)
|
|
16
|
+
9. [Roadmap](#roadmap)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Install the runtime alongside your application code:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @mzebley/mark-down
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
You will also need a manifest file generated by the CLI. See the [CLI documentation](../cli/README.md) for steps to build `snippets-index.json`.
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { SnippetClient } from "@mzebley/mark-down";
|
|
32
|
+
|
|
33
|
+
const client = new SnippetClient({
|
|
34
|
+
manifest: "/snippets-index.json",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const hero = await client.get("getting-started-welcome");
|
|
38
|
+
const components = await client.listByType("component");
|
|
39
|
+
|
|
40
|
+
console.log(hero?.title);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
## Client options
|
|
46
|
+
|
|
47
|
+
`SnippetClient` accepts a single configuration object:
|
|
48
|
+
|
|
49
|
+
- **`manifest`** (`string | ManifestEntry[] | () => Promise<ManifestEntry[]>`) – where to load the manifest. Pass a URL (default), an in-memory array, or an async factory for advanced scenarios.
|
|
50
|
+
- **`fetcher`** (`(input, init) => Promise<Response>`) – provide a custom fetch implementation. Useful for SSR environments or when using libraries like Axios.
|
|
51
|
+
- **`markdownRenderer`** – a `(markdown: string) => string | Promise<string>` function. Override to swap the default `marked` renderer for something like `remark` or a bespoke pipeline.
|
|
52
|
+
- **`resolveSnippetPath`** – map manifest entries to final URLs. Override when static assets live in a CDN or custom folder.
|
|
53
|
+
|
|
54
|
+
All options are optional except `manifest`.
|
|
55
|
+
|
|
56
|
+
## Working with snippets
|
|
57
|
+
|
|
58
|
+
Commonly used APIs:
|
|
59
|
+
|
|
60
|
+
- `client.get(slug)` – fetch a single snippet. Returns `Promise<Snippet | undefined>`.
|
|
61
|
+
- `client.list(filterOrOptions)` – list snippets using predicates, offsets, and limits.
|
|
62
|
+
- `client.listByType(type, options?)` – filter by `type`.
|
|
63
|
+
- `client.listByGroup(group, options?)` – filter based on the folder-derived `group`.
|
|
64
|
+
|
|
65
|
+
Metadata is preserved exactly as declared in front matter. Standard keys (`slug`, `title`, etc.) are copied onto `SnippetMeta` and everything else is available through the `extra` bag so you can access custom fields like `snippet.extra.ctaLabel`.
|
|
66
|
+
|
|
67
|
+
## SSR and custom fetchers
|
|
68
|
+
|
|
69
|
+
The runtime runs in browsers, Node.js, or edge runtimes. For server-side rendering:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import fetch from "node-fetch";
|
|
73
|
+
import { SnippetClient } from "@mzebley/mark-down";
|
|
74
|
+
|
|
75
|
+
const client = new SnippetClient({
|
|
76
|
+
manifest: () => import("./snippets-index.json"),
|
|
77
|
+
fetcher: (input, init) => fetch(input as string, init),
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
You can also pre-seed snippets by passing an array to `manifest` to avoid network requests entirely.
|
|
82
|
+
|
|
83
|
+
## Static sites / CDN usage
|
|
84
|
+
|
|
85
|
+
Need to run mark↓ inside a plain `<script type="module">` context? Use the pre-bundled browser build published at `dist/browser.js`. It automatically polyfills the minimum `Buffer` APIs required by the runtime.
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<script type="module">
|
|
89
|
+
import { SnippetClient } from "https://cdn.jsdelivr.net/npm/@mzebley/mark-down/dist/browser.js";
|
|
90
|
+
|
|
91
|
+
const client = new SnippetClient({ manifest: "./snippets-index.json" });
|
|
92
|
+
const hero = await client.get("marketing-hero");
|
|
93
|
+
document.querySelector("#hero").innerHTML = hero.html;
|
|
94
|
+
</script>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
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
|
+
|
|
99
|
+
## Testing & type safety
|
|
100
|
+
|
|
101
|
+
- 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 `fetcher` function.
|
|
103
|
+
- Run the workspace tests with `npm run test -- core` to exercise Vitest suites that cover caching, filtering, and Markdown conversion.
|
|
104
|
+
|
|
105
|
+
## Related packages
|
|
106
|
+
|
|
107
|
+
- [CLI](../cli/README.md) – generate and watch manifests during development.
|
|
108
|
+
- [Angular adapter](../angular/README.md) – DI-aware provider, service, and component abstractions.
|
|
109
|
+
- [React adapter](../react/README.md) – Hooks and components for React and Next.js applications.
|
|
110
|
+
|
|
111
|
+
Looking for a step-by-step walkthrough? Start from the [Quick start guide in the root README](../../README.md#quick-start).
|
|
112
|
+
|
|
113
|
+
## Roadmap
|
|
114
|
+
|
|
115
|
+
- **Tag-aware helpers** – add `listByTag` and `listByExtra` filters for common metadata structures.
|
|
116
|
+
- **Prefetch & hydration APIs** – expose utilities for bundling snippets with static builds or preloading ahead of navigation.
|
|
117
|
+
- **Search integration** – optional fuzzy search index built from manifest data.
|
|
118
|
+
- **Cache policies** – allow LRU or time-based eviction strategies to better suit long-lived sessions.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
interface SnippetMeta {
|
|
2
|
+
slug: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
order?: number | null;
|
|
5
|
+
type?: string;
|
|
6
|
+
tags?: string[];
|
|
7
|
+
draft?: boolean;
|
|
8
|
+
path: string;
|
|
9
|
+
group: string;
|
|
10
|
+
extra?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
interface Snippet extends SnippetMeta {
|
|
13
|
+
markdown: string;
|
|
14
|
+
html: string;
|
|
15
|
+
}
|
|
16
|
+
type SnippetFilter = (snippet: SnippetMeta) => boolean;
|
|
17
|
+
interface ListOptions {
|
|
18
|
+
filter?: SnippetFilter;
|
|
19
|
+
limit?: number;
|
|
20
|
+
offset?: number;
|
|
21
|
+
}
|
|
22
|
+
type ManifestSource = string | SnippetMeta[] | (() => Promise<SnippetMeta[]> | SnippetMeta[]);
|
|
23
|
+
interface SnippetClientOptions {
|
|
24
|
+
/** Where to load the manifest; defaults to `/snippets-index.json`. */
|
|
25
|
+
manifest: ManifestSource;
|
|
26
|
+
/** Custom fetch to use in Node environments. Falls back to global `fetch`. */
|
|
27
|
+
fetcher?: (input: string) => Promise<ResponseLike>;
|
|
28
|
+
/** Allows overriding markdown → HTML rendering. Defaults to `marked`. */
|
|
29
|
+
markdownRenderer?: (markdown: string) => Promise<string> | string;
|
|
30
|
+
/** Customize how snippet URLs are resolved; defaults to the manifest `path`. */
|
|
31
|
+
resolveSnippetPath?: (meta: SnippetMeta) => string;
|
|
32
|
+
}
|
|
33
|
+
interface ResponseLike {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
status: number;
|
|
36
|
+
text(): Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare class SnippetClient {
|
|
40
|
+
private readonly manifestSource;
|
|
41
|
+
private readonly fetcher;
|
|
42
|
+
private readonly markdownRenderer;
|
|
43
|
+
private readonly resolveSnippetPath;
|
|
44
|
+
private manifestPromise?;
|
|
45
|
+
private snippetCache;
|
|
46
|
+
constructor(options: SnippetClientOptions);
|
|
47
|
+
get(slug: string): Promise<Snippet | undefined>;
|
|
48
|
+
list(filter?: SnippetFilter | ListOptions, options?: ListOptions): Promise<SnippetMeta[]>;
|
|
49
|
+
listByType(type: string, options?: ListOptions): Promise<SnippetMeta[]>;
|
|
50
|
+
listByGroup(group: string, options?: ListOptions): Promise<SnippetMeta[]>;
|
|
51
|
+
private loadSnippet;
|
|
52
|
+
private fetchSnippet;
|
|
53
|
+
private loadManifest;
|
|
54
|
+
private resolveManifest;
|
|
55
|
+
private fetchText;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
declare function normalizeSlug(input: string): string;
|
|
59
|
+
|
|
60
|
+
export { type ListOptions, type ManifestSource, type ResponseLike, type Snippet, SnippetClient, type SnippetClientOptions, type SnippetFilter, type SnippetMeta, normalizeSlug };
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// src/snippet-client.ts
|
|
2
|
+
import matter from "gray-matter";
|
|
3
|
+
import { marked } from "marked";
|
|
4
|
+
var OPTIONAL_FIELDS = [
|
|
5
|
+
"title",
|
|
6
|
+
"order",
|
|
7
|
+
"type",
|
|
8
|
+
"tags",
|
|
9
|
+
"draft"
|
|
10
|
+
];
|
|
11
|
+
var SnippetClient = class {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.snippetCache = /* @__PURE__ */ new Map();
|
|
14
|
+
this.manifestSource = options.manifest;
|
|
15
|
+
this.fetcher = options.fetcher ?? defaultFetch;
|
|
16
|
+
this.markdownRenderer = options.markdownRenderer ?? marked.parse;
|
|
17
|
+
this.resolveSnippetPath = options.resolveSnippetPath ?? ((meta) => meta.path);
|
|
18
|
+
}
|
|
19
|
+
async get(slug) {
|
|
20
|
+
const manifest = await this.loadManifest();
|
|
21
|
+
const entry = manifest.find((snippet) => snippet.slug === slug);
|
|
22
|
+
if (!entry) {
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
return this.loadSnippet(entry);
|
|
26
|
+
}
|
|
27
|
+
async list(filter, options) {
|
|
28
|
+
const manifest = await this.loadManifest();
|
|
29
|
+
let predicate;
|
|
30
|
+
let finalOptions = {};
|
|
31
|
+
if (typeof filter === "function") {
|
|
32
|
+
predicate = filter;
|
|
33
|
+
finalOptions = options ?? {};
|
|
34
|
+
} else if (filter) {
|
|
35
|
+
finalOptions = filter;
|
|
36
|
+
} else if (options) {
|
|
37
|
+
finalOptions = options;
|
|
38
|
+
}
|
|
39
|
+
let items = manifest;
|
|
40
|
+
const filters = [];
|
|
41
|
+
if (predicate) {
|
|
42
|
+
filters.push(predicate);
|
|
43
|
+
}
|
|
44
|
+
if (finalOptions.filter) {
|
|
45
|
+
filters.push(finalOptions.filter);
|
|
46
|
+
}
|
|
47
|
+
if (filters.length) {
|
|
48
|
+
items = items.filter((entry) => filters.every((fn) => fn(entry)));
|
|
49
|
+
}
|
|
50
|
+
const offset = finalOptions.offset ?? 0;
|
|
51
|
+
const limit = finalOptions.limit;
|
|
52
|
+
if (offset > 0) {
|
|
53
|
+
items = items.slice(offset);
|
|
54
|
+
}
|
|
55
|
+
if (typeof limit === "number") {
|
|
56
|
+
items = items.slice(0, limit);
|
|
57
|
+
}
|
|
58
|
+
return [...items];
|
|
59
|
+
}
|
|
60
|
+
listByType(type, options) {
|
|
61
|
+
return this.list((entry) => entry.type === type, options);
|
|
62
|
+
}
|
|
63
|
+
listByGroup(group, options) {
|
|
64
|
+
return this.list((entry) => entry.group === group, options);
|
|
65
|
+
}
|
|
66
|
+
loadSnippet(meta) {
|
|
67
|
+
const cached = this.snippetCache.get(meta.slug);
|
|
68
|
+
if (cached) {
|
|
69
|
+
return cached;
|
|
70
|
+
}
|
|
71
|
+
const promise = this.fetchSnippet(meta);
|
|
72
|
+
this.snippetCache.set(meta.slug, promise);
|
|
73
|
+
return promise;
|
|
74
|
+
}
|
|
75
|
+
async fetchSnippet(meta) {
|
|
76
|
+
const snippetPath = this.resolveSnippetPath(meta);
|
|
77
|
+
const response = await this.fetcher(snippetPath);
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`Failed to fetch snippet '${meta.slug}' (status ${response.status})`);
|
|
80
|
+
}
|
|
81
|
+
const rawContent = await response.text();
|
|
82
|
+
const parsed = matter(rawContent);
|
|
83
|
+
const frontMatter = sanitizeFrontMatter(parsed.data ?? {});
|
|
84
|
+
const mergedMeta = mergeFrontMatter(meta, frontMatter.meta);
|
|
85
|
+
const html = await this.markdownRenderer(parsed.content);
|
|
86
|
+
const baseExtra = mergedMeta.extra ?? {};
|
|
87
|
+
return {
|
|
88
|
+
...mergedMeta,
|
|
89
|
+
extra: {
|
|
90
|
+
...baseExtra,
|
|
91
|
+
...frontMatter.extra
|
|
92
|
+
},
|
|
93
|
+
markdown: parsed.content,
|
|
94
|
+
html
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
loadManifest() {
|
|
98
|
+
if (!this.manifestPromise) {
|
|
99
|
+
this.manifestPromise = this.resolveManifest();
|
|
100
|
+
}
|
|
101
|
+
return this.manifestPromise;
|
|
102
|
+
}
|
|
103
|
+
async resolveManifest() {
|
|
104
|
+
if (Array.isArray(this.manifestSource)) {
|
|
105
|
+
return this.manifestSource;
|
|
106
|
+
}
|
|
107
|
+
if (typeof this.manifestSource === "function") {
|
|
108
|
+
const result = await this.manifestSource();
|
|
109
|
+
return Array.isArray(result) ? result : Promise.reject(new Error("Manifest function must return an array"));
|
|
110
|
+
}
|
|
111
|
+
const raw = await this.fetchText(this.manifestSource);
|
|
112
|
+
const manifest = JSON.parse(raw);
|
|
113
|
+
if (!Array.isArray(manifest)) {
|
|
114
|
+
throw new Error("Manifest must be an array");
|
|
115
|
+
}
|
|
116
|
+
return manifest;
|
|
117
|
+
}
|
|
118
|
+
async fetchText(path) {
|
|
119
|
+
const response = await this.fetcher(path);
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new Error(`Failed to fetch manifest from ${path} (status ${response.status})`);
|
|
122
|
+
}
|
|
123
|
+
return response.text();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
function sanitizeFrontMatter(data) {
|
|
127
|
+
const meta = {};
|
|
128
|
+
const extra = {};
|
|
129
|
+
for (const [key, value] of Object.entries(data)) {
|
|
130
|
+
if (OPTIONAL_FIELDS.includes(key)) {
|
|
131
|
+
if (key === "tags") {
|
|
132
|
+
meta.tags = normalizeTags(value);
|
|
133
|
+
} else {
|
|
134
|
+
meta[key] = value;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
extra[key] = value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { meta, extra };
|
|
141
|
+
}
|
|
142
|
+
function normalizeTags(value) {
|
|
143
|
+
if (!value) {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(value)) {
|
|
147
|
+
return value.map((tag) => String(tag));
|
|
148
|
+
}
|
|
149
|
+
if (typeof value === "string") {
|
|
150
|
+
return value.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
function mergeFrontMatter(base, overrides) {
|
|
155
|
+
const merged = { ...base };
|
|
156
|
+
for (const field of OPTIONAL_FIELDS) {
|
|
157
|
+
const overrideValue = overrides[field];
|
|
158
|
+
if (overrideValue !== void 0 && merged[field] === void 0) {
|
|
159
|
+
merged[field] = overrideValue;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return merged;
|
|
163
|
+
}
|
|
164
|
+
async function defaultFetch(input) {
|
|
165
|
+
const runtimeFetch = globalThis.fetch;
|
|
166
|
+
if (!runtimeFetch) {
|
|
167
|
+
throw new Error("No global fetch implementation found. Provide a custom fetcher.");
|
|
168
|
+
}
|
|
169
|
+
return runtimeFetch(input);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/slug.ts
|
|
173
|
+
var NON_ALPHANUMERIC = /[^a-z0-9]+/gi;
|
|
174
|
+
var LEADING_TRAILING_DASH = /^-+|-+$/g;
|
|
175
|
+
function normalizeSlug(input) {
|
|
176
|
+
const value = input?.trim();
|
|
177
|
+
if (!value) {
|
|
178
|
+
throw new Error("Cannot normalize an empty slug");
|
|
179
|
+
}
|
|
180
|
+
const normalized = value.toLowerCase().replace(NON_ALPHANUMERIC, "-").replace(/-{2,}/g, "-").replace(LEADING_TRAILING_DASH, "");
|
|
181
|
+
if (!normalized) {
|
|
182
|
+
throw new Error(`Slug '${input}' does not contain any alphanumeric characters`);
|
|
183
|
+
}
|
|
184
|
+
return normalized;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/browser.ts
|
|
188
|
+
installBufferShim();
|
|
189
|
+
function installBufferShim() {
|
|
190
|
+
const globalRef = globalThis;
|
|
191
|
+
if (typeof globalRef.Buffer !== "undefined") {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const textEncoder = new TextEncoder();
|
|
195
|
+
const textDecoder = new TextDecoder();
|
|
196
|
+
class BrowserBuffer extends Uint8Array {
|
|
197
|
+
constructor(value) {
|
|
198
|
+
if (typeof value === "number") {
|
|
199
|
+
super(value);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (isArrayBufferLike(value)) {
|
|
203
|
+
super(new Uint8Array(value));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (ArrayBuffer.isView(value)) {
|
|
207
|
+
super(
|
|
208
|
+
new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength))
|
|
209
|
+
);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
super(Array.from(value));
|
|
213
|
+
}
|
|
214
|
+
toString(encoding = "utf-8") {
|
|
215
|
+
if (encoding !== "utf-8" && encoding !== "utf8") {
|
|
216
|
+
throw new Error(`Unsupported encoding '${encoding}' in browser Buffer shim`);
|
|
217
|
+
}
|
|
218
|
+
return textDecoder.decode(this);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const from = (value, encoding = "utf-8") => {
|
|
222
|
+
if (typeof value === "string") {
|
|
223
|
+
if (encoding !== "utf-8" && encoding !== "utf8") {
|
|
224
|
+
throw new Error(`Unsupported encoding '${encoding}' in browser Buffer shim`);
|
|
225
|
+
}
|
|
226
|
+
return new BrowserBuffer(textEncoder.encode(value));
|
|
227
|
+
}
|
|
228
|
+
if (isArrayBufferLike(value)) {
|
|
229
|
+
return new BrowserBuffer(new Uint8Array(value));
|
|
230
|
+
}
|
|
231
|
+
if (ArrayBuffer.isView(value)) {
|
|
232
|
+
return new BrowserBuffer(value);
|
|
233
|
+
}
|
|
234
|
+
if (typeof value.length === "number") {
|
|
235
|
+
return new BrowserBuffer(Array.from(value));
|
|
236
|
+
}
|
|
237
|
+
throw new TypeError("Unsupported input passed to Buffer.from in browser shim");
|
|
238
|
+
};
|
|
239
|
+
const alloc = (size, fill) => {
|
|
240
|
+
if (size < 0) {
|
|
241
|
+
throw new RangeError("Invalid Buffer size");
|
|
242
|
+
}
|
|
243
|
+
const buffer = new BrowserBuffer(size);
|
|
244
|
+
if (typeof fill === "number") {
|
|
245
|
+
buffer.fill(fill);
|
|
246
|
+
} else if (typeof fill === "string") {
|
|
247
|
+
if (!fill.length) {
|
|
248
|
+
buffer.fill(0);
|
|
249
|
+
} else {
|
|
250
|
+
const pattern = textEncoder.encode(fill);
|
|
251
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
252
|
+
buffer[i] = pattern[i % pattern.length];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
buffer.fill(0);
|
|
257
|
+
}
|
|
258
|
+
return buffer;
|
|
259
|
+
};
|
|
260
|
+
const concat = (buffers, totalLength) => {
|
|
261
|
+
const sanitized = Array.from(
|
|
262
|
+
buffers,
|
|
263
|
+
(buffer) => buffer instanceof BrowserBuffer ? buffer : new BrowserBuffer(buffer)
|
|
264
|
+
);
|
|
265
|
+
const length = totalLength ?? sanitized.reduce((acc, current) => acc + current.length, 0);
|
|
266
|
+
const result = new BrowserBuffer(length);
|
|
267
|
+
let offset = 0;
|
|
268
|
+
for (const buffer of sanitized) {
|
|
269
|
+
result.set(buffer, offset);
|
|
270
|
+
offset += buffer.length;
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
};
|
|
274
|
+
const byteLength = (value) => {
|
|
275
|
+
if (typeof value === "string") {
|
|
276
|
+
return textEncoder.encode(value).length;
|
|
277
|
+
}
|
|
278
|
+
if (value instanceof ArrayBuffer) {
|
|
279
|
+
return value.byteLength;
|
|
280
|
+
}
|
|
281
|
+
if (ArrayBuffer.isView(value)) {
|
|
282
|
+
return value.byteLength;
|
|
283
|
+
}
|
|
284
|
+
throw new TypeError("Unable to determine byte length for provided value");
|
|
285
|
+
};
|
|
286
|
+
Object.defineProperties(BrowserBuffer, {
|
|
287
|
+
from: { value: from },
|
|
288
|
+
isBuffer: { value: (candidate) => candidate instanceof BrowserBuffer },
|
|
289
|
+
alloc: { value: alloc },
|
|
290
|
+
concat: { value: concat },
|
|
291
|
+
byteLength: { value: byteLength }
|
|
292
|
+
});
|
|
293
|
+
BrowserBuffer.prototype.valueOf = function valueOf() {
|
|
294
|
+
return this;
|
|
295
|
+
};
|
|
296
|
+
globalRef.Buffer = BrowserBuffer;
|
|
297
|
+
if (typeof window !== "undefined" && typeof window.Buffer === "undefined") {
|
|
298
|
+
window.Buffer = globalRef.Buffer;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function isArrayBufferLike(value) {
|
|
302
|
+
if (value instanceof ArrayBuffer) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
return typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer;
|
|
306
|
+
}
|
|
307
|
+
export {
|
|
308
|
+
SnippetClient,
|
|
309
|
+
normalizeSlug
|
|
310
|
+
};
|
|
311
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/snippet-client.ts","../src/slug.ts","../src/browser.ts"],"sourcesContent":["import matter from \"gray-matter\";\nimport { marked } from \"marked\";\nimport type {\n ListOptions,\n ManifestSource,\n ResponseLike,\n Snippet,\n SnippetClientOptions,\n SnippetFilter,\n SnippetMeta\n} from \"./types\";\n\nconst OPTIONAL_FIELDS: Array<keyof SnippetMeta> = [\n \"title\",\n \"order\",\n \"type\",\n \"tags\",\n \"draft\"\n];\n\nexport class SnippetClient {\n private readonly manifestSource: ManifestSource;\n private readonly fetcher: (input: string) => Promise<ResponseLike>;\n private readonly markdownRenderer: (markdown: string) => Promise<string> | string;\n private readonly resolveSnippetPath: (meta: SnippetMeta) => string;\n private manifestPromise?: Promise<SnippetMeta[]>;\n private snippetCache = new Map<string, Promise<Snippet>>();\n\n constructor(options: SnippetClientOptions) {\n this.manifestSource = options.manifest;\n this.fetcher = options.fetcher ?? defaultFetch;\n this.markdownRenderer = options.markdownRenderer ?? marked.parse;\n this.resolveSnippetPath = options.resolveSnippetPath ?? ((meta) => meta.path);\n }\n\n async get(slug: string): Promise<Snippet | undefined> {\n const manifest = await this.loadManifest();\n const entry = manifest.find((snippet) => snippet.slug === slug);\n if (!entry) {\n return undefined;\n }\n\n return this.loadSnippet(entry);\n }\n\n async list(filter?: SnippetFilter | ListOptions, options?: ListOptions): Promise<SnippetMeta[]> {\n const manifest = await this.loadManifest();\n let predicate: SnippetFilter | undefined;\n let finalOptions: ListOptions = {};\n\n if (typeof filter === \"function\") {\n predicate = filter;\n finalOptions = options ?? {};\n } else if (filter) {\n finalOptions = filter;\n } else if (options) {\n finalOptions = options;\n }\n\n let items = manifest;\n const filters: SnippetFilter[] = [];\n if (predicate) {\n filters.push(predicate);\n }\n if (finalOptions.filter) {\n filters.push(finalOptions.filter);\n }\n if (filters.length) {\n items = items.filter((entry) => filters.every((fn) => fn(entry)));\n }\n\n const offset = finalOptions.offset ?? 0;\n const limit = finalOptions.limit;\n if (offset > 0) {\n items = items.slice(offset);\n }\n if (typeof limit === \"number\") {\n items = items.slice(0, limit);\n }\n\n return [...items];\n }\n\n listByType(type: string, options?: ListOptions): Promise<SnippetMeta[]> {\n return this.list((entry) => entry.type === type, options);\n }\n\n listByGroup(group: string, options?: ListOptions): Promise<SnippetMeta[]> {\n return this.list((entry) => entry.group === group, options);\n }\n\n private loadSnippet(meta: SnippetMeta): Promise<Snippet> {\n const cached = this.snippetCache.get(meta.slug);\n if (cached) {\n return cached;\n }\n\n const promise = this.fetchSnippet(meta);\n this.snippetCache.set(meta.slug, promise);\n return promise;\n }\n\n private async fetchSnippet(meta: SnippetMeta): Promise<Snippet> {\n const snippetPath = this.resolveSnippetPath(meta);\n const response = await this.fetcher(snippetPath);\n if (!response.ok) {\n throw new Error(`Failed to fetch snippet '${meta.slug}' (status ${response.status})`);\n }\n\n const rawContent = await response.text();\n const parsed = matter(rawContent);\n const frontMatter = sanitizeFrontMatter(parsed.data ?? {});\n const mergedMeta = mergeFrontMatter(meta, frontMatter.meta);\n\n const html = await this.markdownRenderer(parsed.content);\n\n const baseExtra = mergedMeta.extra ?? {};\n return {\n ...mergedMeta,\n extra: {\n ...baseExtra,\n ...frontMatter.extra\n },\n markdown: parsed.content,\n html\n };\n }\n\n private loadManifest(): Promise<SnippetMeta[]> {\n if (!this.manifestPromise) {\n this.manifestPromise = this.resolveManifest();\n }\n return this.manifestPromise;\n }\n\n private async resolveManifest(): Promise<SnippetMeta[]> {\n if (Array.isArray(this.manifestSource)) {\n return this.manifestSource;\n }\n\n if (typeof this.manifestSource === \"function\") {\n const result = await this.manifestSource();\n return Array.isArray(result) ? result : Promise.reject(new Error(\"Manifest function must return an array\"));\n }\n\n const raw = await this.fetchText(this.manifestSource);\n const manifest = JSON.parse(raw);\n if (!Array.isArray(manifest)) {\n throw new Error(\"Manifest must be an array\");\n }\n return manifest;\n }\n\n private async fetchText(path: string): Promise<string> {\n const response = await this.fetcher(path);\n if (!response.ok) {\n throw new Error(`Failed to fetch manifest from ${path} (status ${response.status})`);\n }\n return response.text();\n }\n}\n\nfunction sanitizeFrontMatter(data: Record<string, unknown>) {\n const meta: Partial<SnippetMeta> = {};\n const extra: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(data)) {\n if (OPTIONAL_FIELDS.includes(key as keyof SnippetMeta)) {\n if (key === \"tags\") {\n meta.tags = normalizeTags(value);\n } else {\n (meta as Record<string, unknown>)[key] = value;\n }\n } else {\n extra[key] = value;\n }\n }\n\n return { meta, extra };\n}\n\nfunction normalizeTags(value: unknown): string[] | undefined {\n if (!value) {\n return undefined;\n }\n if (Array.isArray(value)) {\n return value.map((tag) => String(tag));\n }\n if (typeof value === \"string\") {\n return value\n .split(\",\")\n .map((tag) => tag.trim())\n .filter(Boolean);\n }\n return undefined;\n}\n\nfunction mergeFrontMatter(base: SnippetMeta, overrides: Partial<SnippetMeta>): SnippetMeta {\n const merged: SnippetMeta = { ...base };\n for (const field of OPTIONAL_FIELDS) {\n const overrideValue = overrides[field];\n if (overrideValue !== undefined && merged[field] === undefined) {\n merged[field] = overrideValue as never;\n }\n }\n return merged;\n}\n\nasync function defaultFetch(input: string): Promise<ResponseLike> {\n const runtimeFetch = (globalThis as typeof globalThis & { fetch?: typeof fetch }).fetch;\n if (!runtimeFetch) {\n throw new Error(\"No global fetch implementation found. Provide a custom fetcher.\");\n }\n return runtimeFetch(input);\n}\n","const NON_ALPHANUMERIC = /[^a-z0-9]+/gi;\nconst LEADING_TRAILING_DASH = /^-+|-+$/g;\n\nexport function normalizeSlug(input: string): string {\n const value = input?.trim();\n if (!value) {\n throw new Error(\"Cannot normalize an empty slug\");\n }\n\n const normalized = value\n .toLowerCase()\n .replace(NON_ALPHANUMERIC, \"-\")\n .replace(/-{2,}/g, \"-\")\n .replace(LEADING_TRAILING_DASH, \"\");\n\n if (!normalized) {\n throw new Error(`Slug '${input}' does not contain any alphanumeric characters`);\n }\n\n return normalized;\n}\n","installBufferShim();\n\nexport * from \"./index\";\n\nfunction installBufferShim() {\n const globalRef = globalThis as typeof globalThis & { Buffer?: BufferConstructor };\n\n if (typeof globalRef.Buffer !== \"undefined\") {\n return;\n }\n\n const textEncoder = new TextEncoder();\n const textDecoder = new TextDecoder();\n\n class BrowserBuffer extends Uint8Array {\n constructor(value: number | ArrayBufferLike | ArrayBufferView | ArrayLike<number>) {\n if (typeof value === \"number\") {\n super(value);\n return;\n }\n\n if (isArrayBufferLike(value)) {\n super(new Uint8Array(value));\n return;\n }\n\n if (ArrayBuffer.isView(value)) {\n super(\n new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength))\n );\n return;\n }\n\n super(Array.from(value));\n }\n\n override toString(encoding: BufferEncoding = \"utf-8\") {\n if (encoding !== \"utf-8\" && encoding !== \"utf8\") {\n throw new Error(`Unsupported encoding '${encoding}' in browser Buffer shim`);\n }\n return textDecoder.decode(this);\n }\n }\n\n const from = (value: string | ArrayLike<number> | BufferSource, encoding: BufferEncoding = \"utf-8\") => {\n if (typeof value === \"string\") {\n if (encoding !== \"utf-8\" && encoding !== \"utf8\") {\n throw new Error(`Unsupported encoding '${encoding}' in browser Buffer shim`);\n }\n return new BrowserBuffer(textEncoder.encode(value));\n }\n\n if (isArrayBufferLike(value)) {\n return new BrowserBuffer(new Uint8Array(value));\n }\n\n if (ArrayBuffer.isView(value)) {\n return new BrowserBuffer(value);\n }\n\n if (typeof (value as ArrayLike<number>).length === \"number\") {\n return new BrowserBuffer(Array.from(value as ArrayLike<number>));\n }\n\n throw new TypeError(\"Unsupported input passed to Buffer.from in browser shim\");\n };\n\n const alloc = (size: number, fill?: number | string) => {\n if (size < 0) {\n throw new RangeError(\"Invalid Buffer size\");\n }\n const buffer = new BrowserBuffer(size);\n if (typeof fill === \"number\") {\n buffer.fill(fill);\n } else if (typeof fill === \"string\") {\n if (!fill.length) {\n buffer.fill(0);\n } else {\n const pattern = textEncoder.encode(fill);\n for (let i = 0; i < buffer.length; i++) {\n buffer[i] = pattern[i % pattern.length];\n }\n }\n } else {\n buffer.fill(0);\n }\n return buffer;\n };\n\n const concat = (buffers: ArrayLike<Uint8Array>, totalLength?: number) => {\n const sanitized = Array.from(buffers, (buffer) =>\n buffer instanceof BrowserBuffer ? buffer : new BrowserBuffer(buffer)\n );\n const length = totalLength ?? sanitized.reduce((acc, current) => acc + current.length, 0);\n const result = new BrowserBuffer(length);\n let offset = 0;\n for (const buffer of sanitized) {\n result.set(buffer, offset);\n offset += buffer.length;\n }\n return result;\n };\n\n const byteLength = (value: string | ArrayBuffer | ArrayBufferView) => {\n if (typeof value === \"string\") {\n return textEncoder.encode(value).length;\n }\n if (value instanceof ArrayBuffer) {\n return value.byteLength;\n }\n if (ArrayBuffer.isView(value)) {\n return value.byteLength;\n }\n throw new TypeError(\"Unable to determine byte length for provided value\");\n };\n\n Object.defineProperties(BrowserBuffer, {\n from: { value: from },\n isBuffer: { value: (candidate: unknown) => candidate instanceof BrowserBuffer },\n alloc: { value: alloc },\n concat: { value: concat },\n byteLength: { value: byteLength }\n });\n\n BrowserBuffer.prototype.valueOf = function valueOf() {\n return this;\n };\n\n globalRef.Buffer = BrowserBuffer as unknown as BufferConstructor;\n\n if (typeof window !== \"undefined\" && typeof (window as typeof globalThis).Buffer === \"undefined\") {\n (window as typeof globalThis & { Buffer?: BufferConstructor }).Buffer = globalRef.Buffer;\n }\n}\n\nfunction isArrayBufferLike(value: unknown): value is ArrayBufferLike {\n if (value instanceof ArrayBuffer) {\n return true;\n }\n return typeof SharedArrayBuffer !== \"undefined\" && value instanceof SharedArrayBuffer;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/slug.ts
|
|
2
|
+
var NON_ALPHANUMERIC = /[^a-z0-9]+/gi;
|
|
3
|
+
var LEADING_TRAILING_DASH = /^-+|-+$/g;
|
|
4
|
+
function normalizeSlug(input) {
|
|
5
|
+
const value = input?.trim();
|
|
6
|
+
if (!value) {
|
|
7
|
+
throw new Error("Cannot normalize an empty slug");
|
|
8
|
+
}
|
|
9
|
+
const normalized = value.toLowerCase().replace(NON_ALPHANUMERIC, "-").replace(/-{2,}/g, "-").replace(LEADING_TRAILING_DASH, "");
|
|
10
|
+
if (!normalized) {
|
|
11
|
+
throw new Error(`Slug '${input}' does not contain any alphanumeric characters`);
|
|
12
|
+
}
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
normalizeSlug
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=chunk-TQ5Y4RZJ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/slug.ts"],"sourcesContent":["const NON_ALPHANUMERIC = /[^a-z0-9]+/gi;\nconst LEADING_TRAILING_DASH = /^-+|-+$/g;\n\nexport function normalizeSlug(input: string): string {\n const value = input?.trim();\n if (!value) {\n throw new Error(\"Cannot normalize an empty slug\");\n }\n\n const normalized = value\n .toLowerCase()\n .replace(NON_ALPHANUMERIC, \"-\")\n .replace(/-{2,}/g, \"-\")\n .replace(LEADING_TRAILING_DASH, \"\");\n\n if (!normalized) {\n throw new Error(`Slug '${input}' does not contain any alphanumeric characters`);\n }\n\n return normalized;\n}\n"],"mappings":";AAAA,IAAM,mBAAmB;AACzB,IAAM,wBAAwB;AAEvB,SAAS,cAAc,OAAuB;AACnD,QAAM,QAAQ,OAAO,KAAK;AAC1B,MAAI,CAAC,OAAO;AACV,UAAM,IAAI,MAAM,gCAAgC;AAAA,EAClD;AAEA,QAAM,aAAa,MAChB,YAAY,EACZ,QAAQ,kBAAkB,GAAG,EAC7B,QAAQ,UAAU,GAAG,EACrB,QAAQ,uBAAuB,EAAE;AAEpC,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,MAAM,SAAS,KAAK,gDAAgD;AAAA,EAChF;AAEA,SAAO;AACT;","names":[]}
|