release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
export type MdxComponent = (props: Record<string, string>, children: string) => string;
|
||||
|
||||
const escapeHtml = (value: string) =>
|
||||
value.replace(
|
||||
/[&<>"']/g,
|
||||
(character) =>
|
||||
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]!,
|
||||
);
|
||||
|
||||
/** Execute explicitly registered MDX components without evaluating arbitrary JavaScript. */
|
||||
export function renderMdxComponents(
|
||||
source: string,
|
||||
components: Record<string, MdxComponent>,
|
||||
): string {
|
||||
let output = source;
|
||||
const parseProps = (raw: string) =>
|
||||
Object.fromEntries(
|
||||
[...raw.matchAll(/([A-Za-z_$][\w$-]*)\s*=\s*["']([^"']*)["']/g)].map((match) => [
|
||||
match[1]!,
|
||||
match[2]!,
|
||||
]),
|
||||
);
|
||||
for (let pass = 0; pass < 20; pass++) {
|
||||
let changed = false;
|
||||
output = output.replace(
|
||||
/<([A-Z][A-Za-z0-9_$]*)\b([^>]*)>([\s\S]*?)<\/\1>|<([A-Z][A-Za-z0-9_$]*)\b([^>]*)\/>/g,
|
||||
(whole, pairedName, pairedProps, children, singleName, singleProps) => {
|
||||
const name = pairedName ?? singleName;
|
||||
const component = components[name];
|
||||
if (!component) throw new Error(`WRN-CONTENT-MDX-COMPONENT: '${name}' is not registered.`);
|
||||
changed = true;
|
||||
return component(parseProps(pairedProps ?? singleProps ?? ""), children ?? "");
|
||||
},
|
||||
);
|
||||
if (!changed) break;
|
||||
}
|
||||
if (/<[A-Z][A-Za-z0-9_$]*\b/.test(output))
|
||||
throw new Error("WRN-CONTENT-MDX-DEPTH: component expansion exceeded its bound.");
|
||||
return output;
|
||||
}
|
||||
|
||||
export interface SyntaxLanguageBundle {
|
||||
highlight(source: string): string;
|
||||
}
|
||||
export function createIncrementalHighlighter(
|
||||
loaders: Record<string, () => SyntaxLanguageBundle | Promise<SyntaxLanguageBundle>>,
|
||||
) {
|
||||
const loaded = new Map<string, Promise<SyntaxLanguageBundle>>();
|
||||
return {
|
||||
languages: () => [...loaded.keys()],
|
||||
async highlight(language: string, source: string) {
|
||||
const load = loaders[language];
|
||||
if (!load) return escapeHtml(source);
|
||||
let bundle = loaded.get(language);
|
||||
if (!bundle) {
|
||||
bundle = Promise.resolve(load());
|
||||
loaded.set(language, bundle);
|
||||
}
|
||||
return (await bundle).highlight(source);
|
||||
},
|
||||
async render(html: string) {
|
||||
const matches = [
|
||||
...html.matchAll(/<pre><code class="language-([\w-]+)">([\s\S]*?)<\/code><\/pre>/g),
|
||||
];
|
||||
let output = html;
|
||||
for (const match of matches)
|
||||
output = output.replace(
|
||||
match[0],
|
||||
`<pre><code class="language-${match[1]}">${await this.highlight(match[1]!, match[2]!)}</code></pre>`,
|
||||
);
|
||||
return output;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface VendorAdapterOptions {
|
||||
fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
||||
token?: string;
|
||||
map?: (entry: any) => { id: string; content: string; source?: string };
|
||||
}
|
||||
const requestJson = async (
|
||||
url: string,
|
||||
options: VendorAdapterOptions,
|
||||
headers: HeadersInit = {},
|
||||
) => {
|
||||
const response = await (options.fetch ?? globalThis.fetch)(url, { headers });
|
||||
if (!response.ok) throw new Error(`WRN-CONTENT-CMS: ${response.status}`);
|
||||
return response.json();
|
||||
};
|
||||
const normalize = (entry: any, source: string, map?: VendorAdapterOptions["map"]) =>
|
||||
map?.(entry) ?? {
|
||||
id: String(entry.id ?? entry.sys?.id ?? entry._id),
|
||||
content: String(entry.content ?? entry.body ?? entry.fields?.body ?? ""),
|
||||
source,
|
||||
};
|
||||
|
||||
export function contentfulAdapter(
|
||||
space: string,
|
||||
environment = "master",
|
||||
options: VendorAdapterOptions = {},
|
||||
) {
|
||||
return {
|
||||
async list(collection: string) {
|
||||
const url = `https://cdn.contentful.com/spaces/${encodeURIComponent(space)}/environments/${encodeURIComponent(environment)}/entries?content_type=${encodeURIComponent(collection)}`;
|
||||
const value = await requestJson(
|
||||
url,
|
||||
options,
|
||||
options.token ? { authorization: `Bearer ${options.token}` } : {},
|
||||
);
|
||||
return (value.items ?? []).map((entry: any) =>
|
||||
normalize(entry, `contentful:${space}:${entry.sys?.id}`, options.map),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
export function sanityAdapter(
|
||||
project: string,
|
||||
dataset: string,
|
||||
options: VendorAdapterOptions & { apiVersion?: string } = {},
|
||||
) {
|
||||
return {
|
||||
async list(collection: string) {
|
||||
const query = encodeURIComponent(`*[_type == $type]`);
|
||||
const url = `https://${encodeURIComponent(project)}.api.sanity.io/v${options.apiVersion ?? "2024-01-01"}/data/query/${encodeURIComponent(dataset)}?query=${query}&$type=${encodeURIComponent(JSON.stringify(collection))}`;
|
||||
const value = await requestJson(
|
||||
url,
|
||||
options,
|
||||
options.token ? { authorization: `Bearer ${options.token}` } : {},
|
||||
);
|
||||
return (value.result ?? []).map((entry: any) =>
|
||||
normalize(entry, `sanity:${project}:${entry._id}`, options.map),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
export function strapiAdapter(baseUrl: string, options: VendorAdapterOptions = {}) {
|
||||
const base = new URL(baseUrl);
|
||||
if (!/^https?:$/.test(base.protocol)) throw new Error("Strapi URL must use HTTP(S)");
|
||||
return {
|
||||
async list(collection: string) {
|
||||
if (!/^[A-Za-z0-9_-]+$/.test(collection)) throw new Error("Invalid Strapi collection");
|
||||
const value = await requestJson(
|
||||
new URL(`/api/${collection}`, base).href,
|
||||
options,
|
||||
options.token ? { authorization: `Bearer ${options.token}` } : {},
|
||||
);
|
||||
return (value.data ?? []).map((entry: any) =>
|
||||
normalize(entry, `strapi:${base.host}:${entry.id}`, options.map),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user