@ox-content/vite-plugin 2.7.0 → 2.9.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/dist/github.cjs +8 -2
- package/dist/github.cjs.map +1 -1
- package/dist/github.mjs +299 -2
- package/dist/github.mjs.map +1 -0
- package/dist/index.cjs +99 -1088
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +5 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +180 -1206
- package/dist/index.mjs.map +1 -1
- package/dist/mermaid.cjs +22 -0
- package/dist/mermaid.cjs.map +1 -1
- package/dist/mermaid.mjs +57 -1
- package/dist/mermaid.mjs.map +1 -1
- package/dist/ogp.cjs +24 -3
- package/dist/ogp.cjs.map +1 -1
- package/dist/ogp.mjs +307 -2
- package/dist/ogp.mjs.map +1 -0
- package/dist/tabs.mjs +188 -2
- package/dist/tabs.mjs.map +1 -0
- package/dist/youtube.mjs +117 -2
- package/dist/youtube.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/github2.mjs +0 -286
- package/dist/github2.mjs.map +0 -1
- package/dist/mermaid2.mjs +0 -2
- package/dist/ogp2.mjs +0 -279
- package/dist/ogp2.mjs.map +0 -1
- package/dist/tabs2.mjs +0 -182
- package/dist/tabs2.mjs.map +0 -1
- package/dist/youtube2.mjs +0 -112
- package/dist/youtube2.mjs.map +0 -1
package/dist/mermaid.cjs
CHANGED
|
@@ -11,6 +11,22 @@ async function importNapiModule() {
|
|
|
11
11
|
};
|
|
12
12
|
return mod;
|
|
13
13
|
}
|
|
14
|
+
let syncNapiModule;
|
|
15
|
+
function importNapiModuleSync() {
|
|
16
|
+
if (syncNapiModule) return syncNapiModule;
|
|
17
|
+
if (syncNapiModule === null) throw new Error("[ox-content] @ox-content/napi is required. Please ensure the NAPI module is built.");
|
|
18
|
+
try {
|
|
19
|
+
const mod = require("@ox-content/napi");
|
|
20
|
+
syncNapiModule = mod.default && typeof mod.default === "object" ? {
|
|
21
|
+
...mod.default,
|
|
22
|
+
...mod
|
|
23
|
+
} : mod;
|
|
24
|
+
return syncNapiModule;
|
|
25
|
+
} catch {
|
|
26
|
+
syncNapiModule = null;
|
|
27
|
+
throw new Error("[ox-content] @ox-content/napi is required. Please ensure the NAPI module is built.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
14
30
|
//#endregion
|
|
15
31
|
//#region src/plugins/mermaid.ts
|
|
16
32
|
/**
|
|
@@ -94,6 +110,12 @@ Object.defineProperty(exports, "importNapiModule", {
|
|
|
94
110
|
return importNapiModule;
|
|
95
111
|
}
|
|
96
112
|
});
|
|
113
|
+
Object.defineProperty(exports, "importNapiModuleSync", {
|
|
114
|
+
enumerable: true,
|
|
115
|
+
get: function() {
|
|
116
|
+
return importNapiModuleSync;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
97
119
|
Object.defineProperty(exports, "mermaidClientScript", {
|
|
98
120
|
enumerable: true,
|
|
99
121
|
get: function() {
|
package/dist/mermaid.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mermaid.cjs","names":[],"sources":["../src/napi.ts","../src/plugins/mermaid.ts"],"sourcesContent":["export async function importNapiModule(): Promise<typeof import(\"@ox-content/napi\")> {\n const mod = (await import(\"@ox-content/napi\")) as typeof import(\"@ox-content/napi\") & {\n default?: Partial<typeof import(\"@ox-content/napi\")>;\n };\n\n if (mod.default && typeof mod.default === \"object\") {\n return {\n ...mod.default,\n ...mod,\n };\n }\n\n return mod;\n}\n","/**\n * Mermaid Plugin - Native Rust renderer via NAPI\n *\n * Renders mermaid code blocks to SVG using the native Rust renderer\n * via NAPI. Delegates to the NAPI `transformMermaid` function which\n * extracts mermaid code blocks from HTML and renders them using mmdc.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { importNapiModule } from \"../napi\";\n\nexport interface MermaidOptions {\n /** Mermaid theme. Default: \"neutral\" */\n theme?: \"default\" | \"dark\" | \"forest\" | \"neutral\" | \"base\";\n}\n\n/** Cached NAPI bindings */\nlet napiBindings: {\n transformMermaid: (html: string, mmdcPath: string) => { html: string; errors: string[] };\n} | null = null;\n\nlet napiLoadAttempted = false;\n\nasync function loadNapi() {\n if (napiLoadAttempted) return napiBindings;\n napiLoadAttempted = true;\n try {\n const binding = (await importNapiModule()) as unknown as NonNullable<typeof napiBindings>;\n if (typeof binding.transformMermaid !== \"function\") {\n napiBindings = null;\n return null;\n }\n napiBindings = binding;\n return binding;\n } catch {\n napiBindings = null;\n return null;\n }\n}\n\nlet cachedMmdcPath: string | null | undefined;\n\nfunction resolveMmdcPath(): string | null {\n if (cachedMmdcPath !== undefined) return cachedMmdcPath;\n\n // 1. Resolve via import.meta.resolve (works in pnpm strict mode)\n // @mermaid-js/mermaid-cli exports ./src/index.js; cli.js is in the same dir\n try {\n const entry = import.meta.resolve(\"@mermaid-js/mermaid-cli\");\n const cliPath = fileURLToPath(new URL(\"./cli.js\", entry));\n if (existsSync(cliPath)) {\n cachedMmdcPath = cliPath;\n return cachedMmdcPath;\n }\n } catch {\n // not resolvable\n }\n\n // 2. Fallback: node_modules/.bin/mmdc relative to cwd\n const binPath = join(process.cwd(), \"node_modules\", \".bin\", \"mmdc\");\n if (existsSync(binPath)) {\n cachedMmdcPath = binPath;\n return cachedMmdcPath;\n }\n\n cachedMmdcPath = null;\n return null;\n}\n\n/**\n * Transforms mermaid code blocks in HTML to rendered SVG diagrams.\n * Uses the native Rust NAPI transformMermaid function.\n */\nexport async function transformMermaidStatic(\n html: string,\n _options?: MermaidOptions,\n): Promise<string> {\n const napi = await loadNapi();\n if (!napi) {\n return html;\n }\n\n const mmdcPath = resolveMmdcPath();\n if (!mmdcPath) {\n console.warn(\"[ox-content] mmdc not found, skipping mermaid rendering\");\n return html;\n }\n\n try {\n const result = napi.transformMermaid(html, mmdcPath);\n for (const error of result.errors) {\n console.warn(\"[ox-content] Mermaid render error:\", error);\n }\n return result.html;\n } catch (err) {\n console.warn(\"[ox-content] Mermaid transform error:\", err);\n return html;\n }\n}\n\n/**\n * @deprecated No longer used. Mermaid rendering is now done at build time via NAPI.\n */\nexport const mermaidClientScript = \"\";\n"],"mappings":";;;;;AAAA,eAAsB,mBAA+D;CACnF,MAAM,MAAO,MAAM,OAAO;AAI1B,KAAI,IAAI,WAAW,OAAO,IAAI,YAAY,SACxC,QAAO;EACL,GAAG,IAAI;EACP,GAAG;EACJ;AAGH,QAAO
|
|
1
|
+
{"version":3,"file":"mermaid.cjs","names":[],"sources":["../src/napi.ts","../src/plugins/mermaid.ts"],"sourcesContent":["export async function importNapiModule(): Promise<typeof import(\"@ox-content/napi\")> {\n const mod = (await import(\"@ox-content/napi\")) as typeof import(\"@ox-content/napi\") & {\n default?: Partial<typeof import(\"@ox-content/napi\")>;\n };\n\n if (mod.default && typeof mod.default === \"object\") {\n return {\n ...mod.default,\n ...mod,\n };\n }\n\n return mod;\n}\n\nlet syncNapiModule: typeof import(\"@ox-content/napi\") | null | undefined;\n\nexport function importNapiModuleSync(): typeof import(\"@ox-content/napi\") {\n if (syncNapiModule) {\n return syncNapiModule;\n }\n\n if (syncNapiModule === null) {\n throw new Error(\n \"[ox-content] @ox-content/napi is required. Please ensure the NAPI module is built.\",\n );\n }\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require(\"@ox-content/napi\") as typeof import(\"@ox-content/napi\") & {\n default?: Partial<typeof import(\"@ox-content/napi\")>;\n };\n syncNapiModule =\n mod.default && typeof mod.default === \"object\"\n ? ({\n ...mod.default,\n ...mod,\n } as typeof import(\"@ox-content/napi\"))\n : mod;\n return syncNapiModule;\n } catch {\n syncNapiModule = null;\n throw new Error(\n \"[ox-content] @ox-content/napi is required. Please ensure the NAPI module is built.\",\n );\n }\n}\n","/**\n * Mermaid Plugin - Native Rust renderer via NAPI\n *\n * Renders mermaid code blocks to SVG using the native Rust renderer\n * via NAPI. Delegates to the NAPI `transformMermaid` function which\n * extracts mermaid code blocks from HTML and renders them using mmdc.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { importNapiModule } from \"../napi\";\n\nexport interface MermaidOptions {\n /** Mermaid theme. Default: \"neutral\" */\n theme?: \"default\" | \"dark\" | \"forest\" | \"neutral\" | \"base\";\n}\n\n/** Cached NAPI bindings */\nlet napiBindings: {\n transformMermaid: (html: string, mmdcPath: string) => { html: string; errors: string[] };\n} | null = null;\n\nlet napiLoadAttempted = false;\n\nasync function loadNapi() {\n if (napiLoadAttempted) return napiBindings;\n napiLoadAttempted = true;\n try {\n const binding = (await importNapiModule()) as unknown as NonNullable<typeof napiBindings>;\n if (typeof binding.transformMermaid !== \"function\") {\n napiBindings = null;\n return null;\n }\n napiBindings = binding;\n return binding;\n } catch {\n napiBindings = null;\n return null;\n }\n}\n\nlet cachedMmdcPath: string | null | undefined;\n\nfunction resolveMmdcPath(): string | null {\n if (cachedMmdcPath !== undefined) return cachedMmdcPath;\n\n // 1. Resolve via import.meta.resolve (works in pnpm strict mode)\n // @mermaid-js/mermaid-cli exports ./src/index.js; cli.js is in the same dir\n try {\n const entry = import.meta.resolve(\"@mermaid-js/mermaid-cli\");\n const cliPath = fileURLToPath(new URL(\"./cli.js\", entry));\n if (existsSync(cliPath)) {\n cachedMmdcPath = cliPath;\n return cachedMmdcPath;\n }\n } catch {\n // not resolvable\n }\n\n // 2. Fallback: node_modules/.bin/mmdc relative to cwd\n const binPath = join(process.cwd(), \"node_modules\", \".bin\", \"mmdc\");\n if (existsSync(binPath)) {\n cachedMmdcPath = binPath;\n return cachedMmdcPath;\n }\n\n cachedMmdcPath = null;\n return null;\n}\n\n/**\n * Transforms mermaid code blocks in HTML to rendered SVG diagrams.\n * Uses the native Rust NAPI transformMermaid function.\n */\nexport async function transformMermaidStatic(\n html: string,\n _options?: MermaidOptions,\n): Promise<string> {\n const napi = await loadNapi();\n if (!napi) {\n return html;\n }\n\n const mmdcPath = resolveMmdcPath();\n if (!mmdcPath) {\n console.warn(\"[ox-content] mmdc not found, skipping mermaid rendering\");\n return html;\n }\n\n try {\n const result = napi.transformMermaid(html, mmdcPath);\n for (const error of result.errors) {\n console.warn(\"[ox-content] Mermaid render error:\", error);\n }\n return result.html;\n } catch (err) {\n console.warn(\"[ox-content] Mermaid transform error:\", err);\n return html;\n }\n}\n\n/**\n * @deprecated No longer used. Mermaid rendering is now done at build time via NAPI.\n */\nexport const mermaidClientScript = \"\";\n"],"mappings":";;;;;AAAA,eAAsB,mBAA+D;CACnF,MAAM,MAAO,MAAM,OAAO;AAI1B,KAAI,IAAI,WAAW,OAAO,IAAI,YAAY,SACxC,QAAO;EACL,GAAG,IAAI;EACP,GAAG;EACJ;AAGH,QAAO;;AAGT,IAAI;AAEJ,SAAgB,uBAA0D;AACxE,KAAI,eACF,QAAO;AAGT,KAAI,mBAAmB,KACrB,OAAM,IAAI,MACR,qFACD;AAGH,KAAI;EAEF,MAAM,MAAM,QAAQ,mBAAmB;AAGvC,mBACE,IAAI,WAAW,OAAO,IAAI,YAAY,WACjC;GACC,GAAG,IAAI;GACP,GAAG;GACJ,GACD;AACN,SAAO;SACD;AACN,mBAAiB;AACjB,QAAM,IAAI,MACR,qFACD;;;;;;;;;;;;;;;;;AC1BL,IAAI,eAEO;AAEX,IAAI,oBAAoB;AAExB,eAAe,WAAW;AACxB,KAAI,kBAAmB,QAAO;AAC9B,qBAAoB;AACpB,KAAI;EACF,MAAM,UAAW,MAAM,kBAAkB;AACzC,MAAI,OAAO,QAAQ,qBAAqB,YAAY;AAClD,kBAAe;AACf,UAAO;;AAET,iBAAe;AACf,SAAO;SACD;AACN,iBAAe;AACf,SAAO;;;AAIX,IAAI;AAEJ,SAAS,kBAAiC;AACxC,KAAI,mBAAmB,KAAA,EAAW,QAAO;AAIzC,KAAI;EACF,MAAM,QAAA,EAAA,CAAoB,QAAQ,0BAA0B;EAC5D,MAAM,WAAA,GAAA,SAAA,eAAwB,IAAI,IAAI,YAAY,MAAM,CAAC;AACzD,OAAA,GAAA,QAAA,YAAe,QAAQ,EAAE;AACvB,oBAAiB;AACjB,UAAO;;SAEH;CAKR,MAAM,WAAA,GAAA,UAAA,MAAe,QAAQ,KAAK,EAAE,gBAAgB,QAAQ,OAAO;AACnE,MAAA,GAAA,QAAA,YAAe,QAAQ,EAAE;AACvB,mBAAiB;AACjB,SAAO;;AAGT,kBAAiB;AACjB,QAAO;;;;;;AAOT,eAAsB,uBACpB,MACA,UACiB;CACjB,MAAM,OAAO,MAAM,UAAU;AAC7B,KAAI,CAAC,KACH,QAAO;CAGT,MAAM,WAAW,iBAAiB;AAClC,KAAI,CAAC,UAAU;AACb,UAAQ,KAAK,0DAA0D;AACvE,SAAO;;AAGT,KAAI;EACF,MAAM,SAAS,KAAK,iBAAiB,MAAM,SAAS;AACpD,OAAK,MAAM,SAAS,OAAO,OACzB,SAAQ,KAAK,sCAAsC,MAAM;AAE3D,SAAO,OAAO;UACP,KAAK;AACZ,UAAQ,KAAK,yCAAyC,IAAI;AAC1D,SAAO;;;;;;AAOX,MAAa,sBAAsB"}
|
package/dist/mermaid.mjs
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
13
|
+
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
14
|
+
var __exportAll = (all, no_symbols) => {
|
|
15
|
+
let target = {};
|
|
16
|
+
for (var name in all) __defProp(target, name, {
|
|
17
|
+
get: all[name],
|
|
18
|
+
enumerable: true
|
|
19
|
+
});
|
|
20
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
21
|
+
return target;
|
|
22
|
+
};
|
|
23
|
+
var __copyProps = (to, from, except, desc) => {
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
25
|
+
key = keys[i];
|
|
26
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
27
|
+
get: ((k) => from[k]).bind(null, key),
|
|
28
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return to;
|
|
32
|
+
};
|
|
33
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
34
|
+
value: mod,
|
|
35
|
+
enumerable: true
|
|
36
|
+
}) : target, mod));
|
|
37
|
+
var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
38
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
39
|
+
//#endregion
|
|
4
40
|
//#region src/napi.ts
|
|
5
41
|
async function importNapiModule() {
|
|
6
42
|
const mod = await import("@ox-content/napi");
|
|
@@ -10,6 +46,22 @@ async function importNapiModule() {
|
|
|
10
46
|
};
|
|
11
47
|
return mod;
|
|
12
48
|
}
|
|
49
|
+
let syncNapiModule;
|
|
50
|
+
function importNapiModuleSync() {
|
|
51
|
+
if (syncNapiModule) return syncNapiModule;
|
|
52
|
+
if (syncNapiModule === null) throw new Error("[ox-content] @ox-content/napi is required. Please ensure the NAPI module is built.");
|
|
53
|
+
try {
|
|
54
|
+
const mod = __require("@ox-content/napi");
|
|
55
|
+
syncNapiModule = mod.default && typeof mod.default === "object" ? {
|
|
56
|
+
...mod.default,
|
|
57
|
+
...mod
|
|
58
|
+
} : mod;
|
|
59
|
+
return syncNapiModule;
|
|
60
|
+
} catch {
|
|
61
|
+
syncNapiModule = null;
|
|
62
|
+
throw new Error("[ox-content] @ox-content/napi is required. Please ensure the NAPI module is built.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
13
65
|
//#endregion
|
|
14
66
|
//#region src/plugins/mermaid.ts
|
|
15
67
|
/**
|
|
@@ -19,6 +71,10 @@ async function importNapiModule() {
|
|
|
19
71
|
* via NAPI. Delegates to the NAPI `transformMermaid` function which
|
|
20
72
|
* extracts mermaid code blocks from HTML and renders them using mmdc.
|
|
21
73
|
*/
|
|
74
|
+
var mermaid_exports = /* @__PURE__ */ __exportAll({
|
|
75
|
+
mermaidClientScript: () => "",
|
|
76
|
+
transformMermaidStatic: () => transformMermaidStatic
|
|
77
|
+
});
|
|
22
78
|
/** Cached NAPI bindings */
|
|
23
79
|
let napiBindings = null;
|
|
24
80
|
let napiLoadAttempted = false;
|
|
@@ -83,6 +139,6 @@ async function transformMermaidStatic(html, _options) {
|
|
|
83
139
|
*/
|
|
84
140
|
const mermaidClientScript = "";
|
|
85
141
|
//#endregion
|
|
86
|
-
export {
|
|
142
|
+
export { importNapiModuleSync as a, __exportAll as c, __toESM as d, importNapiModule as i, __require as l, mermaid_exports as n, __commonJSMin as o, transformMermaidStatic as r, __esmMin as s, mermaidClientScript as t, __toCommonJS as u };
|
|
87
143
|
|
|
88
144
|
//# sourceMappingURL=mermaid.mjs.map
|
package/dist/mermaid.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mermaid.mjs","names":[],"sources":["../src/napi.ts","../src/plugins/mermaid.ts"],"sourcesContent":["export async function importNapiModule(): Promise<typeof import(\"@ox-content/napi\")> {\n const mod = (await import(\"@ox-content/napi\")) as typeof import(\"@ox-content/napi\") & {\n default?: Partial<typeof import(\"@ox-content/napi\")>;\n };\n\n if (mod.default && typeof mod.default === \"object\") {\n return {\n ...mod.default,\n ...mod,\n };\n }\n\n return mod;\n}\n","/**\n * Mermaid Plugin - Native Rust renderer via NAPI\n *\n * Renders mermaid code blocks to SVG using the native Rust renderer\n * via NAPI. Delegates to the NAPI `transformMermaid` function which\n * extracts mermaid code blocks from HTML and renders them using mmdc.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { importNapiModule } from \"../napi\";\n\nexport interface MermaidOptions {\n /** Mermaid theme. Default: \"neutral\" */\n theme?: \"default\" | \"dark\" | \"forest\" | \"neutral\" | \"base\";\n}\n\n/** Cached NAPI bindings */\nlet napiBindings: {\n transformMermaid: (html: string, mmdcPath: string) => { html: string; errors: string[] };\n} | null = null;\n\nlet napiLoadAttempted = false;\n\nasync function loadNapi() {\n if (napiLoadAttempted) return napiBindings;\n napiLoadAttempted = true;\n try {\n const binding = (await importNapiModule()) as unknown as NonNullable<typeof napiBindings>;\n if (typeof binding.transformMermaid !== \"function\") {\n napiBindings = null;\n return null;\n }\n napiBindings = binding;\n return binding;\n } catch {\n napiBindings = null;\n return null;\n }\n}\n\nlet cachedMmdcPath: string | null | undefined;\n\nfunction resolveMmdcPath(): string | null {\n if (cachedMmdcPath !== undefined) return cachedMmdcPath;\n\n // 1. Resolve via import.meta.resolve (works in pnpm strict mode)\n // @mermaid-js/mermaid-cli exports ./src/index.js; cli.js is in the same dir\n try {\n const entry = import.meta.resolve(\"@mermaid-js/mermaid-cli\");\n const cliPath = fileURLToPath(new URL(\"./cli.js\", entry));\n if (existsSync(cliPath)) {\n cachedMmdcPath = cliPath;\n return cachedMmdcPath;\n }\n } catch {\n // not resolvable\n }\n\n // 2. Fallback: node_modules/.bin/mmdc relative to cwd\n const binPath = join(process.cwd(), \"node_modules\", \".bin\", \"mmdc\");\n if (existsSync(binPath)) {\n cachedMmdcPath = binPath;\n return cachedMmdcPath;\n }\n\n cachedMmdcPath = null;\n return null;\n}\n\n/**\n * Transforms mermaid code blocks in HTML to rendered SVG diagrams.\n * Uses the native Rust NAPI transformMermaid function.\n */\nexport async function transformMermaidStatic(\n html: string,\n _options?: MermaidOptions,\n): Promise<string> {\n const napi = await loadNapi();\n if (!napi) {\n return html;\n }\n\n const mmdcPath = resolveMmdcPath();\n if (!mmdcPath) {\n console.warn(\"[ox-content] mmdc not found, skipping mermaid rendering\");\n return html;\n }\n\n try {\n const result = napi.transformMermaid(html, mmdcPath);\n for (const error of result.errors) {\n console.warn(\"[ox-content] Mermaid render error:\", error);\n }\n return result.html;\n } catch (err) {\n console.warn(\"[ox-content] Mermaid transform error:\", err);\n return html;\n }\n}\n\n/**\n * @deprecated No longer used. Mermaid rendering is now done at build time via NAPI.\n */\nexport const mermaidClientScript = \"\";\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"mermaid.mjs","names":[],"sources":["../src/napi.ts","../src/plugins/mermaid.ts"],"sourcesContent":["export async function importNapiModule(): Promise<typeof import(\"@ox-content/napi\")> {\n const mod = (await import(\"@ox-content/napi\")) as typeof import(\"@ox-content/napi\") & {\n default?: Partial<typeof import(\"@ox-content/napi\")>;\n };\n\n if (mod.default && typeof mod.default === \"object\") {\n return {\n ...mod.default,\n ...mod,\n };\n }\n\n return mod;\n}\n\nlet syncNapiModule: typeof import(\"@ox-content/napi\") | null | undefined;\n\nexport function importNapiModuleSync(): typeof import(\"@ox-content/napi\") {\n if (syncNapiModule) {\n return syncNapiModule;\n }\n\n if (syncNapiModule === null) {\n throw new Error(\n \"[ox-content] @ox-content/napi is required. Please ensure the NAPI module is built.\",\n );\n }\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require(\"@ox-content/napi\") as typeof import(\"@ox-content/napi\") & {\n default?: Partial<typeof import(\"@ox-content/napi\")>;\n };\n syncNapiModule =\n mod.default && typeof mod.default === \"object\"\n ? ({\n ...mod.default,\n ...mod,\n } as typeof import(\"@ox-content/napi\"))\n : mod;\n return syncNapiModule;\n } catch {\n syncNapiModule = null;\n throw new Error(\n \"[ox-content] @ox-content/napi is required. Please ensure the NAPI module is built.\",\n );\n }\n}\n","/**\n * Mermaid Plugin - Native Rust renderer via NAPI\n *\n * Renders mermaid code blocks to SVG using the native Rust renderer\n * via NAPI. Delegates to the NAPI `transformMermaid` function which\n * extracts mermaid code blocks from HTML and renders them using mmdc.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { importNapiModule } from \"../napi\";\n\nexport interface MermaidOptions {\n /** Mermaid theme. Default: \"neutral\" */\n theme?: \"default\" | \"dark\" | \"forest\" | \"neutral\" | \"base\";\n}\n\n/** Cached NAPI bindings */\nlet napiBindings: {\n transformMermaid: (html: string, mmdcPath: string) => { html: string; errors: string[] };\n} | null = null;\n\nlet napiLoadAttempted = false;\n\nasync function loadNapi() {\n if (napiLoadAttempted) return napiBindings;\n napiLoadAttempted = true;\n try {\n const binding = (await importNapiModule()) as unknown as NonNullable<typeof napiBindings>;\n if (typeof binding.transformMermaid !== \"function\") {\n napiBindings = null;\n return null;\n }\n napiBindings = binding;\n return binding;\n } catch {\n napiBindings = null;\n return null;\n }\n}\n\nlet cachedMmdcPath: string | null | undefined;\n\nfunction resolveMmdcPath(): string | null {\n if (cachedMmdcPath !== undefined) return cachedMmdcPath;\n\n // 1. Resolve via import.meta.resolve (works in pnpm strict mode)\n // @mermaid-js/mermaid-cli exports ./src/index.js; cli.js is in the same dir\n try {\n const entry = import.meta.resolve(\"@mermaid-js/mermaid-cli\");\n const cliPath = fileURLToPath(new URL(\"./cli.js\", entry));\n if (existsSync(cliPath)) {\n cachedMmdcPath = cliPath;\n return cachedMmdcPath;\n }\n } catch {\n // not resolvable\n }\n\n // 2. Fallback: node_modules/.bin/mmdc relative to cwd\n const binPath = join(process.cwd(), \"node_modules\", \".bin\", \"mmdc\");\n if (existsSync(binPath)) {\n cachedMmdcPath = binPath;\n return cachedMmdcPath;\n }\n\n cachedMmdcPath = null;\n return null;\n}\n\n/**\n * Transforms mermaid code blocks in HTML to rendered SVG diagrams.\n * Uses the native Rust NAPI transformMermaid function.\n */\nexport async function transformMermaidStatic(\n html: string,\n _options?: MermaidOptions,\n): Promise<string> {\n const napi = await loadNapi();\n if (!napi) {\n return html;\n }\n\n const mmdcPath = resolveMmdcPath();\n if (!mmdcPath) {\n console.warn(\"[ox-content] mmdc not found, skipping mermaid rendering\");\n return html;\n }\n\n try {\n const result = napi.transformMermaid(html, mmdcPath);\n for (const error of result.errors) {\n console.warn(\"[ox-content] Mermaid render error:\", error);\n }\n return result.html;\n } catch (err) {\n console.warn(\"[ox-content] Mermaid transform error:\", err);\n return html;\n }\n}\n\n/**\n * @deprecated No longer used. Mermaid rendering is now done at build time via NAPI.\n */\nexport const mermaidClientScript = \"\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,eAAsB,mBAA+D;CACnF,MAAM,MAAO,MAAM,OAAO;AAI1B,KAAI,IAAI,WAAW,OAAO,IAAI,YAAY,SACxC,QAAO;EACL,GAAG,IAAI;EACP,GAAG;EACJ;AAGH,QAAO;;AAGT,IAAI;AAEJ,SAAgB,uBAA0D;AACxE,KAAI,eACF,QAAO;AAGT,KAAI,mBAAmB,KACrB,OAAM,IAAI,MACR,qFACD;AAGH,KAAI;EAEF,MAAM,MAAA,UAAc,mBAAmB;AAGvC,mBACE,IAAI,WAAW,OAAO,IAAI,YAAY,WACjC;GACC,GAAG,IAAI;GACP,GAAG;GACJ,GACD;AACN,SAAO;SACD;AACN,mBAAiB;AACjB,QAAM,IAAI,MACR,qFACD;;;;;;;;;;;;;;;;;AC1BL,IAAI,eAEO;AAEX,IAAI,oBAAoB;AAExB,eAAe,WAAW;AACxB,KAAI,kBAAmB,QAAO;AAC9B,qBAAoB;AACpB,KAAI;EACF,MAAM,UAAW,MAAM,kBAAkB;AACzC,MAAI,OAAO,QAAQ,qBAAqB,YAAY;AAClD,kBAAe;AACf,UAAO;;AAET,iBAAe;AACf,SAAO;SACD;AACN,iBAAe;AACf,SAAO;;;AAIX,IAAI;AAEJ,SAAS,kBAAiC;AACxC,KAAI,mBAAmB,KAAA,EAAW,QAAO;AAIzC,KAAI;EACF,MAAM,QAAQ,OAAO,KAAK,QAAQ,0BAA0B;EAC5D,MAAM,UAAU,cAAc,IAAI,IAAI,YAAY,MAAM,CAAC;AACzD,MAAI,WAAW,QAAQ,EAAE;AACvB,oBAAiB;AACjB,UAAO;;SAEH;CAKR,MAAM,UAAU,KAAK,QAAQ,KAAK,EAAE,gBAAgB,QAAQ,OAAO;AACnE,KAAI,WAAW,QAAQ,EAAE;AACvB,mBAAiB;AACjB,SAAO;;AAGT,kBAAiB;AACjB,QAAO;;;;;;AAOT,eAAsB,uBACpB,MACA,UACiB;CACjB,MAAM,OAAO,MAAM,UAAU;AAC7B,KAAI,CAAC,KACH,QAAO;CAGT,MAAM,WAAW,iBAAiB;AAClC,KAAI,CAAC,UAAU;AACb,UAAQ,KAAK,0DAA0D;AACvE,SAAO;;AAGT,KAAI;EACF,MAAM,SAAS,KAAK,iBAAiB,MAAM,SAAS;AACpD,OAAK,MAAM,SAAS,OAAO,OACzB,SAAQ,KAAK,sCAAsC,MAAM;AAE3D,SAAO,OAAO;UACP,KAAK;AACZ,UAAQ,KAAK,yCAAyC,IAAI;AAC1D,SAAO;;;;;;AAOX,MAAa,sBAAsB"}
|
package/dist/ogp.cjs
CHANGED
|
@@ -14,6 +14,7 @@ rehype_stringify = require_chunk.__toESM(rehype_stringify);
|
|
|
14
14
|
var ogp_exports = /* @__PURE__ */ require_chunk.__exportAll({
|
|
15
15
|
collectOgpUrls: () => collectOgpUrls,
|
|
16
16
|
fetchOgpData: () => fetchOgpData,
|
|
17
|
+
isSafeOgpUrl: () => isSafeOgpUrl,
|
|
17
18
|
prefetchOgpData: () => prefetchOgpData,
|
|
18
19
|
transformOgp: () => transformOgp
|
|
19
20
|
});
|
|
@@ -24,6 +25,25 @@ const defaultOptions = {
|
|
|
24
25
|
userAgent: "ox-content-ogp-bot/1.0 (compatible; +https://github.com/ubugeeei/ox-content)"
|
|
25
26
|
};
|
|
26
27
|
const ogpCache = /* @__PURE__ */ new Map();
|
|
28
|
+
function isPrivateIPv4(hostname) {
|
|
29
|
+
const parts = hostname.split(".").map(Number);
|
|
30
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
|
|
31
|
+
const [a, b] = parts;
|
|
32
|
+
return a === 10 || a === 127 || a === 0 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
|
|
33
|
+
}
|
|
34
|
+
function isSafeOgpUrl(value) {
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(value);
|
|
37
|
+
const host = url.hostname.toLowerCase();
|
|
38
|
+
const ipv6 = host.replace(/^\[|\]$/g, "");
|
|
39
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
40
|
+
if (host === "localhost" || host.endsWith(".localhost")) return false;
|
|
41
|
+
if (ipv6.includes(":") && (ipv6 === "::1" || ipv6.startsWith("fc") || ipv6.startsWith("fd") || ipv6.startsWith("fe80"))) return false;
|
|
42
|
+
return !isPrivateIPv4(host);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
27
47
|
/**
|
|
28
48
|
* Get element attribute value.
|
|
29
49
|
*/
|
|
@@ -82,6 +102,7 @@ function parseOgpFromHtml(html, url) {
|
|
|
82
102
|
* Fetch OGP data for a URL.
|
|
83
103
|
*/
|
|
84
104
|
async function fetchOgpData(url, options) {
|
|
105
|
+
if (!isSafeOgpUrl(url)) return null;
|
|
85
106
|
if (options.cache) {
|
|
86
107
|
const cached = ogpCache.get(url);
|
|
87
108
|
if (cached && Date.now() - cached.timestamp < options.cacheTTL) return cached.data;
|
|
@@ -186,7 +207,7 @@ function createOgpCard(data) {
|
|
|
186
207
|
tagName: "a",
|
|
187
208
|
properties: {
|
|
188
209
|
className: ["ox-ogp-card"],
|
|
189
|
-
href: data.url,
|
|
210
|
+
href: isSafeOgpUrl(data.url) ? data.url : "#",
|
|
190
211
|
target: "_blank",
|
|
191
212
|
rel: "noopener noreferrer"
|
|
192
213
|
},
|
|
@@ -202,7 +223,7 @@ function createFallbackCard(url) {
|
|
|
202
223
|
tagName: "a",
|
|
203
224
|
properties: {
|
|
204
225
|
className: ["ox-ogp-simple"],
|
|
205
|
-
href: url,
|
|
226
|
+
href: isSafeOgpUrl(url) ? url : "#",
|
|
206
227
|
target: "_blank",
|
|
207
228
|
rel: "noopener noreferrer"
|
|
208
229
|
},
|
|
@@ -234,7 +255,7 @@ async function collectOgpUrls(html) {
|
|
|
234
255
|
const urls = [];
|
|
235
256
|
const urlPattern = /<ogcard[^>]*\s+url=["']([^"']+)["']/gi;
|
|
236
257
|
let match;
|
|
237
|
-
while ((match = urlPattern.exec(html)) !== null) urls.push(match[1]);
|
|
258
|
+
while ((match = urlPattern.exec(html)) !== null) if (isSafeOgpUrl(match[1])) urls.push(match[1]);
|
|
238
259
|
return urls;
|
|
239
260
|
}
|
|
240
261
|
/**
|
package/dist/ogp.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ogp.cjs","names":["rehypeParse","rehypeStringify"],"sources":["../src/plugins/ogp.ts"],"sourcesContent":["/**\n * OGP Card Plugin - Link card embedding\n *\n * Transforms <OgCard> components into static link preview cards\n * by fetching OGP metadata at build time.\n */\n\nimport { unified } from \"unified\";\nimport rehypeParse from \"rehype-parse\";\nimport rehypeStringify from \"rehype-stringify\";\nimport type { Root, Element } from \"hast\";\n\nexport interface OgpData {\n url: string;\n title: string;\n description?: string;\n image?: string;\n siteName?: string;\n favicon?: string;\n}\n\nexport interface OgpOptions {\n /** Request timeout in milliseconds. Default: 10000 */\n timeout?: number;\n /** Cache fetched data. Default: true */\n cache?: boolean;\n /** Cache TTL in milliseconds. Default: 3600000 (1 hour) */\n cacheTTL?: number;\n /** User agent for requests */\n userAgent?: string;\n}\n\nconst defaultOptions: Required<OgpOptions> = {\n timeout: 10000,\n cache: true,\n cacheTTL: 3600000,\n userAgent: \"ox-content-ogp-bot/1.0 (compatible; +https://github.com/ubugeeei/ox-content)\",\n};\n\n// Simple in-memory cache\nconst ogpCache = new Map<string, { data: OgpData; timestamp: number }>();\n\n/**\n * Get element attribute value.\n */\nfunction getAttribute(el: Element, name: string): string | undefined {\n const value = el.properties?.[name];\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) return value.join(\" \");\n return undefined;\n}\n\n/**\n * Extract domain from URL.\n */\nfunction extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n return url;\n }\n}\n\n/**\n * Get favicon URL for a domain.\n */\nfunction getFaviconUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n // Use Google's favicon service as fallback\n return `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=32`;\n } catch {\n return \"\";\n }\n}\n\n/**\n * Parse OGP metadata from HTML.\n */\nfunction parseOgpFromHtml(html: string, url: string): OgpData {\n const result: OgpData = {\n url,\n title: \"\",\n };\n\n // Extract title\n const titleMatch = html.match(/<title[^>]*>([^<]+)<\\/title>/i);\n const ogTitleMatch =\n html.match(/<meta[^>]*property=[\"']og:title[\"'][^>]*content=[\"']([^\"']+)[\"']/i) ||\n html.match(/<meta[^>]*content=[\"']([^\"']+)[\"'][^>]*property=[\"']og:title[\"']/i);\n\n result.title = ogTitleMatch?.[1] || titleMatch?.[1] || extractDomain(url);\n\n // Extract description\n const descMatch =\n html.match(/<meta[^>]*property=[\"']og:description[\"'][^>]*content=[\"']([^\"']+)[\"']/i) ||\n html.match(/<meta[^>]*content=[\"']([^\"']+)[\"'][^>]*property=[\"']og:description[\"']/i) ||\n html.match(/<meta[^>]*name=[\"']description[\"'][^>]*content=[\"']([^\"']+)[\"']/i) ||\n html.match(/<meta[^>]*content=[\"']([^\"']+)[\"'][^>]*name=[\"']description[\"']/i);\n\n if (descMatch) {\n result.description = descMatch[1];\n }\n\n // Extract image\n const imageMatch =\n html.match(/<meta[^>]*property=[\"']og:image[\"'][^>]*content=[\"']([^\"']+)[\"']/i) ||\n html.match(/<meta[^>]*content=[\"']([^\"']+)[\"'][^>]*property=[\"']og:image[\"']/i);\n\n if (imageMatch) {\n let imageUrl = imageMatch[1];\n // Handle relative URLs\n if (imageUrl.startsWith(\"/\")) {\n try {\n const urlObj = new URL(url);\n imageUrl = `${urlObj.protocol}//${urlObj.host}${imageUrl}`;\n } catch {\n // Keep as is\n }\n }\n result.image = imageUrl;\n }\n\n // Extract site name\n const siteNameMatch =\n html.match(/<meta[^>]*property=[\"']og:site_name[\"'][^>]*content=[\"']([^\"']+)[\"']/i) ||\n html.match(/<meta[^>]*content=[\"']([^\"']+)[\"'][^>]*property=[\"']og:site_name[\"']/i);\n\n if (siteNameMatch) {\n result.siteName = siteNameMatch[1];\n }\n\n // Get favicon\n result.favicon = getFaviconUrl(url);\n\n return result;\n}\n\n/**\n * Fetch OGP data for a URL.\n */\nexport async function fetchOgpData(\n url: string,\n options: Required<OgpOptions>,\n): Promise<OgpData | null> {\n // Check cache\n if (options.cache) {\n const cached = ogpCache.get(url);\n if (cached && Date.now() - cached.timestamp < options.cacheTTL) {\n return cached.data;\n }\n }\n\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), options.timeout);\n\n const response = await fetch(url, {\n headers: {\n \"User-Agent\": options.userAgent,\n Accept: \"text/html,application/xhtml+xml\",\n },\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n console.warn(`Failed to fetch OGP for ${url}: ${response.status}`);\n return null;\n }\n\n const html = await response.text();\n const data = parseOgpFromHtml(html, url);\n\n // Cache the result\n if (options.cache) {\n ogpCache.set(url, { data, timestamp: Date.now() });\n }\n\n return data;\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n console.warn(`Timeout fetching OGP for ${url}`);\n } else {\n console.warn(`Error fetching OGP for ${url}:`, error);\n }\n return null;\n }\n}\n\n/**\n * Create OGP card element.\n */\nfunction createOgpCard(data: OgpData): Element {\n const children: Element[\"children\"] = [];\n\n // Content section\n const contentChildren: Element[\"children\"] = [];\n\n // Title\n contentChildren.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-ogp-title\"] },\n children: [{ type: \"text\", value: data.title }],\n });\n\n // Description\n if (data.description) {\n contentChildren.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-ogp-description\"] },\n children: [{ type: \"text\", value: data.description }],\n });\n }\n\n // Meta (favicon + domain)\n const metaChildren: Element[\"children\"] = [];\n\n if (data.favicon) {\n metaChildren.push({\n type: \"element\",\n tagName: \"img\",\n properties: {\n className: [\"ox-ogp-favicon\"],\n src: data.favicon,\n alt: \"\",\n loading: \"lazy\",\n },\n children: [],\n });\n }\n\n metaChildren.push({\n type: \"element\",\n tagName: \"span\",\n properties: { className: [\"ox-ogp-domain\"] },\n children: [{ type: \"text\", value: data.siteName || extractDomain(data.url) }],\n });\n\n contentChildren.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-ogp-meta\"] },\n children: metaChildren,\n });\n\n children.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-ogp-content\"] },\n children: contentChildren,\n });\n\n // Image\n if (data.image) {\n children.push({\n type: \"element\",\n tagName: \"img\",\n properties: {\n className: [\"ox-ogp-image\"],\n src: data.image,\n alt: \"\",\n loading: \"lazy\",\n },\n children: [],\n });\n }\n\n return {\n type: \"element\",\n tagName: \"a\",\n properties: {\n className: [\"ox-ogp-card\"],\n href: data.url,\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n children,\n };\n}\n\n/**\n * Create fallback element when OGP data is unavailable.\n */\nfunction createFallbackCard(url: string): Element {\n return {\n type: \"element\",\n tagName: \"a\",\n properties: {\n className: [\"ox-ogp-simple\"],\n href: url,\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n children: [\n {\n type: \"element\",\n tagName: \"svg\",\n properties: {\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"2\",\n },\n children: [\n {\n type: \"element\",\n tagName: \"path\",\n properties: {\n d: \"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3\",\n },\n children: [],\n },\n ],\n },\n { type: \"text\", value: extractDomain(url) },\n ],\n };\n}\n\n/**\n * Collect all OGP URLs from HTML for pre-fetching.\n */\nexport async function collectOgpUrls(html: string): Promise<string[]> {\n const urls: string[] = [];\n const urlPattern = /<ogcard[^>]*\\s+url=[\"']([^\"']+)[\"']/gi;\n\n let match;\n while ((match = urlPattern.exec(html)) !== null) {\n urls.push(match[1]);\n }\n\n return urls;\n}\n\n/**\n * Pre-fetch all OGP data.\n */\nexport async function prefetchOgpData(\n urls: string[],\n options?: OgpOptions,\n): Promise<Map<string, OgpData | null>> {\n const mergedOptions = { ...defaultOptions, ...options };\n const results = new Map<string, OgpData | null>();\n\n await Promise.all(\n urls.map(async (url) => {\n const data = await fetchOgpData(url, mergedOptions);\n results.set(url, data);\n }),\n );\n\n return results;\n}\n\n/**\n * Rehype plugin to transform OgCard components.\n */\nfunction rehypeOgp(ogpDataMap: Map<string, OgpData | null>) {\n return (tree: Root) => {\n const visit = (node: Root | Element) => {\n if (\"children\" in node) {\n for (let i = 0; i < node.children.length; i++) {\n const child = node.children[i];\n\n if (child.type === \"element\") {\n // Check for <OgCard> component\n if (child.tagName.toLowerCase() === \"ogcard\") {\n const url = getAttribute(child, \"url\");\n\n if (url) {\n const ogpData = ogpDataMap.get(url);\n const cardElement = ogpData ? createOgpCard(ogpData) : createFallbackCard(url);\n node.children[i] = cardElement;\n }\n } else {\n visit(child);\n }\n }\n }\n }\n };\n\n visit(tree);\n };\n}\n\n/**\n * Transform OgCard components in HTML.\n */\nexport async function transformOgp(\n html: string,\n ogpDataMap?: Map<string, OgpData | null>,\n options?: OgpOptions,\n): Promise<string> {\n // If no pre-fetched data, collect and fetch\n let dataMap = ogpDataMap;\n if (!dataMap) {\n const urls = await collectOgpUrls(html);\n dataMap = await prefetchOgpData(urls, options);\n }\n\n const result = await unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeOgp, dataMap)\n .use(rehypeStringify)\n .process(html);\n\n return String(result);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgCA,MAAM,iBAAuC;CAC3C,SAAS;CACT,OAAO;CACP,UAAU;CACV,WAAW;CACZ;AAGD,MAAM,2BAAW,IAAI,KAAmD;;;;AAKxE,SAAS,aAAa,IAAa,MAAkC;CACnE,MAAM,QAAQ,GAAG,aAAa;AAC9B,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,MAAM,KAAK,IAAI;;;;;AAOlD,SAAS,cAAc,KAAqB;AAC1C,KAAI;AAEF,SADe,IAAI,IAAI,IAAI,CACb;SACR;AACN,SAAO;;;;;;AAOX,SAAS,cAAc,KAAqB;AAC1C,KAAI;AAGF,SAAO,6CAFQ,IAAI,IAAI,IAAI,CAEgC,SAAS;SAC9D;AACN,SAAO;;;;;;AAOX,SAAS,iBAAiB,MAAc,KAAsB;CAC5D,MAAM,SAAkB;EACtB;EACA,OAAO;EACR;CAGD,MAAM,aAAa,KAAK,MAAM,gCAAgC;AAK9D,QAAO,SAHL,KAAK,MAAM,oEAAoE,IAC/E,KAAK,MAAM,oEAAoE,IAEnD,MAAM,aAAa,MAAM,cAAc,IAAI;CAGzE,MAAM,YACJ,KAAK,MAAM,0EAA0E,IACrF,KAAK,MAAM,0EAA0E,IACrF,KAAK,MAAM,mEAAmE,IAC9E,KAAK,MAAM,mEAAmE;AAEhF,KAAI,UACF,QAAO,cAAc,UAAU;CAIjC,MAAM,aACJ,KAAK,MAAM,oEAAoE,IAC/E,KAAK,MAAM,oEAAoE;AAEjF,KAAI,YAAY;EACd,IAAI,WAAW,WAAW;AAE1B,MAAI,SAAS,WAAW,IAAI,CAC1B,KAAI;GACF,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,cAAW,GAAG,OAAO,SAAS,IAAI,OAAO,OAAO;UAC1C;AAIV,SAAO,QAAQ;;CAIjB,MAAM,gBACJ,KAAK,MAAM,wEAAwE,IACnF,KAAK,MAAM,wEAAwE;AAErF,KAAI,cACF,QAAO,WAAW,cAAc;AAIlC,QAAO,UAAU,cAAc,IAAI;AAEnC,QAAO;;;;;AAMT,eAAsB,aACpB,KACA,SACyB;AAEzB,KAAI,QAAQ,OAAO;EACjB,MAAM,SAAS,SAAS,IAAI,IAAI;AAChC,MAAI,UAAU,KAAK,KAAK,GAAG,OAAO,YAAY,QAAQ,SACpD,QAAO,OAAO;;AAIlB,KAAI;EACF,MAAM,aAAa,IAAI,iBAAiB;EACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,QAAQ,QAAQ;EAEvE,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,SAAS;IACP,cAAc,QAAQ;IACtB,QAAQ;IACT;GACD,QAAQ,WAAW;GACpB,CAAC;AAEF,eAAa,UAAU;AAEvB,MAAI,CAAC,SAAS,IAAI;AAChB,WAAQ,KAAK,2BAA2B,IAAI,IAAI,SAAS,SAAS;AAClE,UAAO;;EAIT,MAAM,OAAO,iBADA,MAAM,SAAS,MAAM,EACE,IAAI;AAGxC,MAAI,QAAQ,MACV,UAAS,IAAI,KAAK;GAAE;GAAM,WAAW,KAAK,KAAK;GAAE,CAAC;AAGpD,SAAO;UACA,OAAO;AACd,MAAI,iBAAiB,SAAS,MAAM,SAAS,aAC3C,SAAQ,KAAK,4BAA4B,MAAM;MAE/C,SAAQ,KAAK,0BAA0B,IAAI,IAAI,MAAM;AAEvD,SAAO;;;;;;AAOX,SAAS,cAAc,MAAwB;CAC7C,MAAM,WAAgC,EAAE;CAGxC,MAAM,kBAAuC,EAAE;AAG/C,iBAAgB,KAAK;EACnB,MAAM;EACN,SAAS;EACT,YAAY,EAAE,WAAW,CAAC,eAAe,EAAE;EAC3C,UAAU,CAAC;GAAE,MAAM;GAAQ,OAAO,KAAK;GAAO,CAAC;EAChD,CAAC;AAGF,KAAI,KAAK,YACP,iBAAgB,KAAK;EACnB,MAAM;EACN,SAAS;EACT,YAAY,EAAE,WAAW,CAAC,qBAAqB,EAAE;EACjD,UAAU,CAAC;GAAE,MAAM;GAAQ,OAAO,KAAK;GAAa,CAAC;EACtD,CAAC;CAIJ,MAAM,eAAoC,EAAE;AAE5C,KAAI,KAAK,QACP,cAAa,KAAK;EAChB,MAAM;EACN,SAAS;EACT,YAAY;GACV,WAAW,CAAC,iBAAiB;GAC7B,KAAK,KAAK;GACV,KAAK;GACL,SAAS;GACV;EACD,UAAU,EAAE;EACb,CAAC;AAGJ,cAAa,KAAK;EAChB,MAAM;EACN,SAAS;EACT,YAAY,EAAE,WAAW,CAAC,gBAAgB,EAAE;EAC5C,UAAU,CAAC;GAAE,MAAM;GAAQ,OAAO,KAAK,YAAY,cAAc,KAAK,IAAI;GAAE,CAAC;EAC9E,CAAC;AAEF,iBAAgB,KAAK;EACnB,MAAM;EACN,SAAS;EACT,YAAY,EAAE,WAAW,CAAC,cAAc,EAAE;EAC1C,UAAU;EACX,CAAC;AAEF,UAAS,KAAK;EACZ,MAAM;EACN,SAAS;EACT,YAAY,EAAE,WAAW,CAAC,iBAAiB,EAAE;EAC7C,UAAU;EACX,CAAC;AAGF,KAAI,KAAK,MACP,UAAS,KAAK;EACZ,MAAM;EACN,SAAS;EACT,YAAY;GACV,WAAW,CAAC,eAAe;GAC3B,KAAK,KAAK;GACV,KAAK;GACL,SAAS;GACV;EACD,UAAU,EAAE;EACb,CAAC;AAGJ,QAAO;EACL,MAAM;EACN,SAAS;EACT,YAAY;GACV,WAAW,CAAC,cAAc;GAC1B,MAAM,KAAK;GACX,QAAQ;GACR,KAAK;GACN;EACD;EACD;;;;;AAMH,SAAS,mBAAmB,KAAsB;AAChD,QAAO;EACL,MAAM;EACN,SAAS;EACT,YAAY;GACV,WAAW,CAAC,gBAAgB;GAC5B,MAAM;GACN,QAAQ;GACR,KAAK;GACN;EACD,UAAU,CACR;GACE,MAAM;GACN,SAAS;GACT,YAAY;IACV,SAAS;IACT,MAAM;IACN,QAAQ;IACR,gBAAgB;IACjB;GACD,UAAU,CACR;IACE,MAAM;IACN,SAAS;IACT,YAAY,EACV,GAAG,gFACJ;IACD,UAAU,EAAE;IACb,CACF;GACF,EACD;GAAE,MAAM;GAAQ,OAAO,cAAc,IAAI;GAAE,CAC5C;EACF;;;;;AAMH,eAAsB,eAAe,MAAiC;CACpE,MAAM,OAAiB,EAAE;CACzB,MAAM,aAAa;CAEnB,IAAI;AACJ,SAAQ,QAAQ,WAAW,KAAK,KAAK,MAAM,KACzC,MAAK,KAAK,MAAM,GAAG;AAGrB,QAAO;;;;;AAMT,eAAsB,gBACpB,MACA,SACsC;CACtC,MAAM,gBAAgB;EAAE,GAAG;EAAgB,GAAG;EAAS;CACvD,MAAM,0BAAU,IAAI,KAA6B;AAEjD,OAAM,QAAQ,IACZ,KAAK,IAAI,OAAO,QAAQ;EACtB,MAAM,OAAO,MAAM,aAAa,KAAK,cAAc;AACnD,UAAQ,IAAI,KAAK,KAAK;GACtB,CACH;AAED,QAAO;;;;;AAMT,SAAS,UAAU,YAAyC;AAC1D,SAAQ,SAAe;EACrB,MAAM,SAAS,SAAyB;AACtC,OAAI,cAAc,KAChB,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,SAAS,QAAQ,KAAK;IAC7C,MAAM,QAAQ,KAAK,SAAS;AAE5B,QAAI,MAAM,SAAS,UAEjB,KAAI,MAAM,QAAQ,aAAa,KAAK,UAAU;KAC5C,MAAM,MAAM,aAAa,OAAO,MAAM;AAEtC,SAAI,KAAK;MACP,MAAM,UAAU,WAAW,IAAI,IAAI;MACnC,MAAM,cAAc,UAAU,cAAc,QAAQ,GAAG,mBAAmB,IAAI;AAC9E,WAAK,SAAS,KAAK;;UAGrB,OAAM,MAAM;;;AAOtB,QAAM,KAAK;;;;;;AAOf,eAAsB,aACpB,MACA,YACA,SACiB;CAEjB,IAAI,UAAU;AACd,KAAI,CAAC,QAEH,WAAU,MAAM,gBADH,MAAM,eAAe,KAAK,EACD,QAAQ;CAGhD,MAAM,SAAS,OAAA,GAAA,QAAA,UAAe,CAC3B,IAAIA,aAAAA,SAAa,EAAE,UAAU,MAAM,CAAC,CACpC,IAAI,WAAW,QAAQ,CACvB,IAAIC,iBAAAA,QAAgB,CACpB,QAAQ,KAAK;AAEhB,QAAO,OAAO,OAAO"}
|
|
1
|
+
{"version":3,"file":"ogp.cjs","names":["rehypeParse","rehypeStringify"],"sources":["../src/plugins/ogp.ts"],"sourcesContent":["/**\n * OGP Card Plugin - Link card embedding\n *\n * Transforms <OgCard> components into static link preview cards\n * by fetching OGP metadata at build time.\n */\n\nimport { unified } from \"unified\";\nimport rehypeParse from \"rehype-parse\";\nimport rehypeStringify from \"rehype-stringify\";\nimport type { Root, Element } from \"hast\";\n\nexport interface OgpData {\n url: string;\n title: string;\n description?: string;\n image?: string;\n siteName?: string;\n favicon?: string;\n}\n\nexport interface OgpOptions {\n /** Request timeout in milliseconds. Default: 10000 */\n timeout?: number;\n /** Cache fetched data. Default: true */\n cache?: boolean;\n /** Cache TTL in milliseconds. Default: 3600000 (1 hour) */\n cacheTTL?: number;\n /** User agent for requests */\n userAgent?: string;\n}\n\nconst defaultOptions: Required<OgpOptions> = {\n timeout: 10000,\n cache: true,\n cacheTTL: 3600000,\n userAgent: \"ox-content-ogp-bot/1.0 (compatible; +https://github.com/ubugeeei/ox-content)\",\n};\n\n// Simple in-memory cache\nconst ogpCache = new Map<string, { data: OgpData; timestamp: number }>();\n\nfunction isPrivateIPv4(hostname: string): boolean {\n const parts = hostname.split(\".\").map(Number);\n if (\n parts.length !== 4 ||\n parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)\n ) {\n return false;\n }\n const [a, b] = parts;\n return (\n a === 10 ||\n a === 127 ||\n a === 0 ||\n (a === 172 && b >= 16 && b <= 31) ||\n (a === 192 && b === 168) ||\n (a === 169 && b === 254)\n );\n}\n\nexport function isSafeOgpUrl(value: string): boolean {\n try {\n const url = new URL(value);\n const host = url.hostname.toLowerCase();\n const ipv6 = host.replace(/^\\[|\\]$/g, \"\");\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return false;\n if (host === \"localhost\" || host.endsWith(\".localhost\")) return false;\n if (\n ipv6.includes(\":\") &&\n (ipv6 === \"::1\" || ipv6.startsWith(\"fc\") || ipv6.startsWith(\"fd\") || ipv6.startsWith(\"fe80\"))\n )\n return false;\n return !isPrivateIPv4(host);\n } catch {\n return false;\n }\n}\n\n/**\n * Get element attribute value.\n */\nfunction getAttribute(el: Element, name: string): string | undefined {\n const value = el.properties?.[name];\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) return value.join(\" \");\n return undefined;\n}\n\n/**\n * Extract domain from URL.\n */\nfunction extractDomain(url: string): string {\n try {\n const urlObj = new URL(url);\n return urlObj.hostname;\n } catch {\n return url;\n }\n}\n\n/**\n * Get favicon URL for a domain.\n */\nfunction getFaviconUrl(url: string): string {\n try {\n const urlObj = new URL(url);\n // Use Google's favicon service as fallback\n return `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=32`;\n } catch {\n return \"\";\n }\n}\n\n/**\n * Parse OGP metadata from HTML.\n */\nfunction parseOgpFromHtml(html: string, url: string): OgpData {\n const result: OgpData = {\n url,\n title: \"\",\n };\n\n // Extract title\n const titleMatch = html.match(/<title[^>]*>([^<]+)<\\/title>/i);\n const ogTitleMatch =\n html.match(/<meta[^>]*property=[\"']og:title[\"'][^>]*content=[\"']([^\"']+)[\"']/i) ||\n html.match(/<meta[^>]*content=[\"']([^\"']+)[\"'][^>]*property=[\"']og:title[\"']/i);\n\n result.title = ogTitleMatch?.[1] || titleMatch?.[1] || extractDomain(url);\n\n // Extract description\n const descMatch =\n html.match(/<meta[^>]*property=[\"']og:description[\"'][^>]*content=[\"']([^\"']+)[\"']/i) ||\n html.match(/<meta[^>]*content=[\"']([^\"']+)[\"'][^>]*property=[\"']og:description[\"']/i) ||\n html.match(/<meta[^>]*name=[\"']description[\"'][^>]*content=[\"']([^\"']+)[\"']/i) ||\n html.match(/<meta[^>]*content=[\"']([^\"']+)[\"'][^>]*name=[\"']description[\"']/i);\n\n if (descMatch) {\n result.description = descMatch[1];\n }\n\n // Extract image\n const imageMatch =\n html.match(/<meta[^>]*property=[\"']og:image[\"'][^>]*content=[\"']([^\"']+)[\"']/i) ||\n html.match(/<meta[^>]*content=[\"']([^\"']+)[\"'][^>]*property=[\"']og:image[\"']/i);\n\n if (imageMatch) {\n let imageUrl = imageMatch[1];\n // Handle relative URLs\n if (imageUrl.startsWith(\"/\")) {\n try {\n const urlObj = new URL(url);\n imageUrl = `${urlObj.protocol}//${urlObj.host}${imageUrl}`;\n } catch {\n // Keep as is\n }\n }\n result.image = imageUrl;\n }\n\n // Extract site name\n const siteNameMatch =\n html.match(/<meta[^>]*property=[\"']og:site_name[\"'][^>]*content=[\"']([^\"']+)[\"']/i) ||\n html.match(/<meta[^>]*content=[\"']([^\"']+)[\"'][^>]*property=[\"']og:site_name[\"']/i);\n\n if (siteNameMatch) {\n result.siteName = siteNameMatch[1];\n }\n\n // Get favicon\n result.favicon = getFaviconUrl(url);\n\n return result;\n}\n\n/**\n * Fetch OGP data for a URL.\n */\nexport async function fetchOgpData(\n url: string,\n options: Required<OgpOptions>,\n): Promise<OgpData | null> {\n if (!isSafeOgpUrl(url)) {\n return null;\n }\n\n // Check cache\n if (options.cache) {\n const cached = ogpCache.get(url);\n if (cached && Date.now() - cached.timestamp < options.cacheTTL) {\n return cached.data;\n }\n }\n\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), options.timeout);\n\n const response = await fetch(url, {\n headers: {\n \"User-Agent\": options.userAgent,\n Accept: \"text/html,application/xhtml+xml\",\n },\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n console.warn(`Failed to fetch OGP for ${url}: ${response.status}`);\n return null;\n }\n\n const html = await response.text();\n const data = parseOgpFromHtml(html, url);\n\n // Cache the result\n if (options.cache) {\n ogpCache.set(url, { data, timestamp: Date.now() });\n }\n\n return data;\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n console.warn(`Timeout fetching OGP for ${url}`);\n } else {\n console.warn(`Error fetching OGP for ${url}:`, error);\n }\n return null;\n }\n}\n\n/**\n * Create OGP card element.\n */\nfunction createOgpCard(data: OgpData): Element {\n const children: Element[\"children\"] = [];\n\n // Content section\n const contentChildren: Element[\"children\"] = [];\n\n // Title\n contentChildren.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-ogp-title\"] },\n children: [{ type: \"text\", value: data.title }],\n });\n\n // Description\n if (data.description) {\n contentChildren.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-ogp-description\"] },\n children: [{ type: \"text\", value: data.description }],\n });\n }\n\n // Meta (favicon + domain)\n const metaChildren: Element[\"children\"] = [];\n\n if (data.favicon) {\n metaChildren.push({\n type: \"element\",\n tagName: \"img\",\n properties: {\n className: [\"ox-ogp-favicon\"],\n src: data.favicon,\n alt: \"\",\n loading: \"lazy\",\n },\n children: [],\n });\n }\n\n metaChildren.push({\n type: \"element\",\n tagName: \"span\",\n properties: { className: [\"ox-ogp-domain\"] },\n children: [{ type: \"text\", value: data.siteName || extractDomain(data.url) }],\n });\n\n contentChildren.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-ogp-meta\"] },\n children: metaChildren,\n });\n\n children.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-ogp-content\"] },\n children: contentChildren,\n });\n\n // Image\n if (data.image) {\n children.push({\n type: \"element\",\n tagName: \"img\",\n properties: {\n className: [\"ox-ogp-image\"],\n src: data.image,\n alt: \"\",\n loading: \"lazy\",\n },\n children: [],\n });\n }\n\n return {\n type: \"element\",\n tagName: \"a\",\n properties: {\n className: [\"ox-ogp-card\"],\n href: isSafeOgpUrl(data.url) ? data.url : \"#\",\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n children,\n };\n}\n\n/**\n * Create fallback element when OGP data is unavailable.\n */\nfunction createFallbackCard(url: string): Element {\n return {\n type: \"element\",\n tagName: \"a\",\n properties: {\n className: [\"ox-ogp-simple\"],\n href: isSafeOgpUrl(url) ? url : \"#\",\n target: \"_blank\",\n rel: \"noopener noreferrer\",\n },\n children: [\n {\n type: \"element\",\n tagName: \"svg\",\n properties: {\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n \"stroke-width\": \"2\",\n },\n children: [\n {\n type: \"element\",\n tagName: \"path\",\n properties: {\n d: \"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3\",\n },\n children: [],\n },\n ],\n },\n { type: \"text\", value: extractDomain(url) },\n ],\n };\n}\n\n/**\n * Collect all OGP URLs from HTML for pre-fetching.\n */\nexport async function collectOgpUrls(html: string): Promise<string[]> {\n const urls: string[] = [];\n const urlPattern = /<ogcard[^>]*\\s+url=[\"']([^\"']+)[\"']/gi;\n\n let match;\n while ((match = urlPattern.exec(html)) !== null) {\n if (isSafeOgpUrl(match[1])) {\n urls.push(match[1]);\n }\n }\n\n return urls;\n}\n\n/**\n * Pre-fetch all OGP data.\n */\nexport async function prefetchOgpData(\n urls: string[],\n options?: OgpOptions,\n): Promise<Map<string, OgpData | null>> {\n const mergedOptions = { ...defaultOptions, ...options };\n const results = new Map<string, OgpData | null>();\n\n await Promise.all(\n urls.map(async (url) => {\n const data = await fetchOgpData(url, mergedOptions);\n results.set(url, data);\n }),\n );\n\n return results;\n}\n\n/**\n * Rehype plugin to transform OgCard components.\n */\nfunction rehypeOgp(ogpDataMap: Map<string, OgpData | null>) {\n return (tree: Root) => {\n const visit = (node: Root | Element) => {\n if (\"children\" in node) {\n for (let i = 0; i < node.children.length; i++) {\n const child = node.children[i];\n\n if (child.type === \"element\") {\n // Check for <OgCard> component\n if (child.tagName.toLowerCase() === \"ogcard\") {\n const url = getAttribute(child, \"url\");\n\n if (url) {\n const ogpData = ogpDataMap.get(url);\n const cardElement = ogpData ? createOgpCard(ogpData) : createFallbackCard(url);\n node.children[i] = cardElement;\n }\n } else {\n visit(child);\n }\n }\n }\n }\n };\n\n visit(tree);\n };\n}\n\n/**\n * Transform OgCard components in HTML.\n */\nexport async function transformOgp(\n html: string,\n ogpDataMap?: Map<string, OgpData | null>,\n options?: OgpOptions,\n): Promise<string> {\n // If no pre-fetched data, collect and fetch\n let dataMap = ogpDataMap;\n if (!dataMap) {\n const urls = await collectOgpUrls(html);\n dataMap = await prefetchOgpData(urls, options);\n }\n\n const result = await unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeOgp, dataMap)\n .use(rehypeStringify)\n .process(html);\n\n return String(result);\n}\n"],"mappings":"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"}
|
package/dist/ogp.mjs
CHANGED
|
@@ -1,2 +1,307 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { c as __exportAll } from "./mermaid.mjs";
|
|
2
|
+
import { unified } from "unified";
|
|
3
|
+
import rehypeParse from "rehype-parse";
|
|
4
|
+
import rehypeStringify from "rehype-stringify";
|
|
5
|
+
//#region src/plugins/ogp.ts
|
|
6
|
+
/**
|
|
7
|
+
* OGP Card Plugin - Link card embedding
|
|
8
|
+
*
|
|
9
|
+
* Transforms <OgCard> components into static link preview cards
|
|
10
|
+
* by fetching OGP metadata at build time.
|
|
11
|
+
*/
|
|
12
|
+
var ogp_exports = /* @__PURE__ */ __exportAll({
|
|
13
|
+
collectOgpUrls: () => collectOgpUrls,
|
|
14
|
+
fetchOgpData: () => fetchOgpData,
|
|
15
|
+
isSafeOgpUrl: () => isSafeOgpUrl,
|
|
16
|
+
prefetchOgpData: () => prefetchOgpData,
|
|
17
|
+
transformOgp: () => transformOgp
|
|
18
|
+
});
|
|
19
|
+
const defaultOptions = {
|
|
20
|
+
timeout: 1e4,
|
|
21
|
+
cache: true,
|
|
22
|
+
cacheTTL: 36e5,
|
|
23
|
+
userAgent: "ox-content-ogp-bot/1.0 (compatible; +https://github.com/ubugeeei/ox-content)"
|
|
24
|
+
};
|
|
25
|
+
const ogpCache = /* @__PURE__ */ new Map();
|
|
26
|
+
function isPrivateIPv4(hostname) {
|
|
27
|
+
const parts = hostname.split(".").map(Number);
|
|
28
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
|
|
29
|
+
const [a, b] = parts;
|
|
30
|
+
return a === 10 || a === 127 || a === 0 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
|
|
31
|
+
}
|
|
32
|
+
function isSafeOgpUrl(value) {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(value);
|
|
35
|
+
const host = url.hostname.toLowerCase();
|
|
36
|
+
const ipv6 = host.replace(/^\[|\]$/g, "");
|
|
37
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
38
|
+
if (host === "localhost" || host.endsWith(".localhost")) return false;
|
|
39
|
+
if (ipv6.includes(":") && (ipv6 === "::1" || ipv6.startsWith("fc") || ipv6.startsWith("fd") || ipv6.startsWith("fe80"))) return false;
|
|
40
|
+
return !isPrivateIPv4(host);
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get element attribute value.
|
|
47
|
+
*/
|
|
48
|
+
function getAttribute(el, name) {
|
|
49
|
+
const value = el.properties?.[name];
|
|
50
|
+
if (typeof value === "string") return value;
|
|
51
|
+
if (Array.isArray(value)) return value.join(" ");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract domain from URL.
|
|
55
|
+
*/
|
|
56
|
+
function extractDomain(url) {
|
|
57
|
+
try {
|
|
58
|
+
return new URL(url).hostname;
|
|
59
|
+
} catch {
|
|
60
|
+
return url;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get favicon URL for a domain.
|
|
65
|
+
*/
|
|
66
|
+
function getFaviconUrl(url) {
|
|
67
|
+
try {
|
|
68
|
+
return `https://www.google.com/s2/favicons?domain=${new URL(url).hostname}&sz=32`;
|
|
69
|
+
} catch {
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse OGP metadata from HTML.
|
|
75
|
+
*/
|
|
76
|
+
function parseOgpFromHtml(html, url) {
|
|
77
|
+
const result = {
|
|
78
|
+
url,
|
|
79
|
+
title: ""
|
|
80
|
+
};
|
|
81
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
82
|
+
result.title = (html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:title["']/i))?.[1] || titleMatch?.[1] || extractDomain(url);
|
|
83
|
+
const descMatch = html.match(/<meta[^>]*property=["']og:description["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:description["']/i) || html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*name=["']description["']/i);
|
|
84
|
+
if (descMatch) result.description = descMatch[1];
|
|
85
|
+
const imageMatch = html.match(/<meta[^>]*property=["']og:image["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:image["']/i);
|
|
86
|
+
if (imageMatch) {
|
|
87
|
+
let imageUrl = imageMatch[1];
|
|
88
|
+
if (imageUrl.startsWith("/")) try {
|
|
89
|
+
const urlObj = new URL(url);
|
|
90
|
+
imageUrl = `${urlObj.protocol}//${urlObj.host}${imageUrl}`;
|
|
91
|
+
} catch {}
|
|
92
|
+
result.image = imageUrl;
|
|
93
|
+
}
|
|
94
|
+
const siteNameMatch = html.match(/<meta[^>]*property=["']og:site_name["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*property=["']og:site_name["']/i);
|
|
95
|
+
if (siteNameMatch) result.siteName = siteNameMatch[1];
|
|
96
|
+
result.favicon = getFaviconUrl(url);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Fetch OGP data for a URL.
|
|
101
|
+
*/
|
|
102
|
+
async function fetchOgpData(url, options) {
|
|
103
|
+
if (!isSafeOgpUrl(url)) return null;
|
|
104
|
+
if (options.cache) {
|
|
105
|
+
const cached = ogpCache.get(url);
|
|
106
|
+
if (cached && Date.now() - cached.timestamp < options.cacheTTL) return cached.data;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
111
|
+
const response = await fetch(url, {
|
|
112
|
+
headers: {
|
|
113
|
+
"User-Agent": options.userAgent,
|
|
114
|
+
Accept: "text/html,application/xhtml+xml"
|
|
115
|
+
},
|
|
116
|
+
signal: controller.signal
|
|
117
|
+
});
|
|
118
|
+
clearTimeout(timeoutId);
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
console.warn(`Failed to fetch OGP for ${url}: ${response.status}`);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const data = parseOgpFromHtml(await response.text(), url);
|
|
124
|
+
if (options.cache) ogpCache.set(url, {
|
|
125
|
+
data,
|
|
126
|
+
timestamp: Date.now()
|
|
127
|
+
});
|
|
128
|
+
return data;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error instanceof Error && error.name === "AbortError") console.warn(`Timeout fetching OGP for ${url}`);
|
|
131
|
+
else console.warn(`Error fetching OGP for ${url}:`, error);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Create OGP card element.
|
|
137
|
+
*/
|
|
138
|
+
function createOgpCard(data) {
|
|
139
|
+
const children = [];
|
|
140
|
+
const contentChildren = [];
|
|
141
|
+
contentChildren.push({
|
|
142
|
+
type: "element",
|
|
143
|
+
tagName: "div",
|
|
144
|
+
properties: { className: ["ox-ogp-title"] },
|
|
145
|
+
children: [{
|
|
146
|
+
type: "text",
|
|
147
|
+
value: data.title
|
|
148
|
+
}]
|
|
149
|
+
});
|
|
150
|
+
if (data.description) contentChildren.push({
|
|
151
|
+
type: "element",
|
|
152
|
+
tagName: "div",
|
|
153
|
+
properties: { className: ["ox-ogp-description"] },
|
|
154
|
+
children: [{
|
|
155
|
+
type: "text",
|
|
156
|
+
value: data.description
|
|
157
|
+
}]
|
|
158
|
+
});
|
|
159
|
+
const metaChildren = [];
|
|
160
|
+
if (data.favicon) metaChildren.push({
|
|
161
|
+
type: "element",
|
|
162
|
+
tagName: "img",
|
|
163
|
+
properties: {
|
|
164
|
+
className: ["ox-ogp-favicon"],
|
|
165
|
+
src: data.favicon,
|
|
166
|
+
alt: "",
|
|
167
|
+
loading: "lazy"
|
|
168
|
+
},
|
|
169
|
+
children: []
|
|
170
|
+
});
|
|
171
|
+
metaChildren.push({
|
|
172
|
+
type: "element",
|
|
173
|
+
tagName: "span",
|
|
174
|
+
properties: { className: ["ox-ogp-domain"] },
|
|
175
|
+
children: [{
|
|
176
|
+
type: "text",
|
|
177
|
+
value: data.siteName || extractDomain(data.url)
|
|
178
|
+
}]
|
|
179
|
+
});
|
|
180
|
+
contentChildren.push({
|
|
181
|
+
type: "element",
|
|
182
|
+
tagName: "div",
|
|
183
|
+
properties: { className: ["ox-ogp-meta"] },
|
|
184
|
+
children: metaChildren
|
|
185
|
+
});
|
|
186
|
+
children.push({
|
|
187
|
+
type: "element",
|
|
188
|
+
tagName: "div",
|
|
189
|
+
properties: { className: ["ox-ogp-content"] },
|
|
190
|
+
children: contentChildren
|
|
191
|
+
});
|
|
192
|
+
if (data.image) children.push({
|
|
193
|
+
type: "element",
|
|
194
|
+
tagName: "img",
|
|
195
|
+
properties: {
|
|
196
|
+
className: ["ox-ogp-image"],
|
|
197
|
+
src: data.image,
|
|
198
|
+
alt: "",
|
|
199
|
+
loading: "lazy"
|
|
200
|
+
},
|
|
201
|
+
children: []
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
type: "element",
|
|
205
|
+
tagName: "a",
|
|
206
|
+
properties: {
|
|
207
|
+
className: ["ox-ogp-card"],
|
|
208
|
+
href: isSafeOgpUrl(data.url) ? data.url : "#",
|
|
209
|
+
target: "_blank",
|
|
210
|
+
rel: "noopener noreferrer"
|
|
211
|
+
},
|
|
212
|
+
children
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Create fallback element when OGP data is unavailable.
|
|
217
|
+
*/
|
|
218
|
+
function createFallbackCard(url) {
|
|
219
|
+
return {
|
|
220
|
+
type: "element",
|
|
221
|
+
tagName: "a",
|
|
222
|
+
properties: {
|
|
223
|
+
className: ["ox-ogp-simple"],
|
|
224
|
+
href: isSafeOgpUrl(url) ? url : "#",
|
|
225
|
+
target: "_blank",
|
|
226
|
+
rel: "noopener noreferrer"
|
|
227
|
+
},
|
|
228
|
+
children: [{
|
|
229
|
+
type: "element",
|
|
230
|
+
tagName: "svg",
|
|
231
|
+
properties: {
|
|
232
|
+
viewBox: "0 0 24 24",
|
|
233
|
+
fill: "none",
|
|
234
|
+
stroke: "currentColor",
|
|
235
|
+
"stroke-width": "2"
|
|
236
|
+
},
|
|
237
|
+
children: [{
|
|
238
|
+
type: "element",
|
|
239
|
+
tagName: "path",
|
|
240
|
+
properties: { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3" },
|
|
241
|
+
children: []
|
|
242
|
+
}]
|
|
243
|
+
}, {
|
|
244
|
+
type: "text",
|
|
245
|
+
value: extractDomain(url)
|
|
246
|
+
}]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Collect all OGP URLs from HTML for pre-fetching.
|
|
251
|
+
*/
|
|
252
|
+
async function collectOgpUrls(html) {
|
|
253
|
+
const urls = [];
|
|
254
|
+
const urlPattern = /<ogcard[^>]*\s+url=["']([^"']+)["']/gi;
|
|
255
|
+
let match;
|
|
256
|
+
while ((match = urlPattern.exec(html)) !== null) if (isSafeOgpUrl(match[1])) urls.push(match[1]);
|
|
257
|
+
return urls;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Pre-fetch all OGP data.
|
|
261
|
+
*/
|
|
262
|
+
async function prefetchOgpData(urls, options) {
|
|
263
|
+
const mergedOptions = {
|
|
264
|
+
...defaultOptions,
|
|
265
|
+
...options
|
|
266
|
+
};
|
|
267
|
+
const results = /* @__PURE__ */ new Map();
|
|
268
|
+
await Promise.all(urls.map(async (url) => {
|
|
269
|
+
const data = await fetchOgpData(url, mergedOptions);
|
|
270
|
+
results.set(url, data);
|
|
271
|
+
}));
|
|
272
|
+
return results;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Rehype plugin to transform OgCard components.
|
|
276
|
+
*/
|
|
277
|
+
function rehypeOgp(ogpDataMap) {
|
|
278
|
+
return (tree) => {
|
|
279
|
+
const visit = (node) => {
|
|
280
|
+
if ("children" in node) for (let i = 0; i < node.children.length; i++) {
|
|
281
|
+
const child = node.children[i];
|
|
282
|
+
if (child.type === "element") if (child.tagName.toLowerCase() === "ogcard") {
|
|
283
|
+
const url = getAttribute(child, "url");
|
|
284
|
+
if (url) {
|
|
285
|
+
const ogpData = ogpDataMap.get(url);
|
|
286
|
+
const cardElement = ogpData ? createOgpCard(ogpData) : createFallbackCard(url);
|
|
287
|
+
node.children[i] = cardElement;
|
|
288
|
+
}
|
|
289
|
+
} else visit(child);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
visit(tree);
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Transform OgCard components in HTML.
|
|
297
|
+
*/
|
|
298
|
+
async function transformOgp(html, ogpDataMap, options) {
|
|
299
|
+
let dataMap = ogpDataMap;
|
|
300
|
+
if (!dataMap) dataMap = await prefetchOgpData(await collectOgpUrls(html), options);
|
|
301
|
+
const result = await unified().use(rehypeParse, { fragment: true }).use(rehypeOgp, dataMap).use(rehypeStringify).process(html);
|
|
302
|
+
return String(result);
|
|
303
|
+
}
|
|
304
|
+
//#endregion
|
|
305
|
+
export { transformOgp as a, prefetchOgpData as i, fetchOgpData as n, ogp_exports as r, collectOgpUrls as t };
|
|
306
|
+
|
|
307
|
+
//# sourceMappingURL=ogp.mjs.map
|