@ox-content/vite-plugin 2.8.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/tabs.mjs CHANGED
@@ -1,2 +1,188 @@
1
- import { r as transformTabs } from "./tabs2.mjs";
2
- export { transformTabs };
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/tabs.ts
6
+ /**
7
+ * Tabs Plugin - Pure CSS implementation
8
+ *
9
+ * Transforms <Tabs>/<Tab> components into accessible HTML
10
+ * with CSS :has() based tab switching (no JavaScript required).
11
+ */
12
+ var tabs_exports = /* @__PURE__ */ __exportAll({
13
+ generateTabsCSS: () => generateTabsCSS,
14
+ resetTabGroupCounter: () => resetTabGroupCounter,
15
+ transformTabs: () => transformTabs
16
+ });
17
+ let tabGroupCounter = 0;
18
+ /**
19
+ * Reset tab group counter (for testing).
20
+ */
21
+ function resetTabGroupCounter() {
22
+ tabGroupCounter = 0;
23
+ }
24
+ /**
25
+ * Get element attribute value.
26
+ */
27
+ function getAttribute(el, name) {
28
+ const value = el.properties?.[name];
29
+ if (typeof value === "string") return value;
30
+ if (Array.isArray(value)) return value.join(" ");
31
+ }
32
+ /**
33
+ * Parse Tab elements from Tabs children.
34
+ */
35
+ function parseTabChildren(children) {
36
+ const tabs = [];
37
+ for (const child of children) {
38
+ if (child.type !== "element") continue;
39
+ if (child.tagName.toLowerCase() === "tab") {
40
+ const label = getAttribute(child, "label") || `Tab ${tabs.length + 1}`;
41
+ tabs.push({
42
+ label,
43
+ content: child.children.filter((c) => c.type === "element" || c.type === "text")
44
+ });
45
+ }
46
+ }
47
+ return tabs;
48
+ }
49
+ /**
50
+ * Create the HTML structure for tabs.
51
+ */
52
+ function createTabsElement(tabs, groupId) {
53
+ const children = [];
54
+ const headerChildren = [];
55
+ tabs.forEach((tab, index) => {
56
+ const inputId = `ox-tab-${groupId}-${index}`;
57
+ headerChildren.push({
58
+ type: "element",
59
+ tagName: "input",
60
+ properties: {
61
+ type: "radio",
62
+ name: `ox-tabs-${groupId}`,
63
+ id: inputId,
64
+ checked: index === 0 ? true : void 0
65
+ },
66
+ children: []
67
+ });
68
+ headerChildren.push({
69
+ type: "element",
70
+ tagName: "label",
71
+ properties: { htmlFor: inputId },
72
+ children: [{
73
+ type: "text",
74
+ value: tab.label
75
+ }]
76
+ });
77
+ });
78
+ children.push({
79
+ type: "element",
80
+ tagName: "div",
81
+ properties: { className: ["ox-tabs-header"] },
82
+ children: headerChildren
83
+ });
84
+ tabs.forEach((tab, index) => {
85
+ children.push({
86
+ type: "element",
87
+ tagName: "div",
88
+ properties: {
89
+ className: ["ox-tab-panel"],
90
+ "data-tab": String(index)
91
+ },
92
+ children: tab.content
93
+ });
94
+ });
95
+ return {
96
+ type: "element",
97
+ tagName: "div",
98
+ properties: {
99
+ className: ["ox-tabs"],
100
+ "data-group": groupId
101
+ },
102
+ children
103
+ };
104
+ }
105
+ /**
106
+ * Create fallback HTML using <details> elements.
107
+ */
108
+ function createFallbackElement(tabs) {
109
+ const children = [];
110
+ tabs.forEach((tab, index) => {
111
+ children.push({
112
+ type: "element",
113
+ tagName: "details",
114
+ properties: { open: index === 0 ? true : void 0 },
115
+ children: [{
116
+ type: "element",
117
+ tagName: "summary",
118
+ properties: {},
119
+ children: [{
120
+ type: "text",
121
+ value: tab.label
122
+ }]
123
+ }, {
124
+ type: "element",
125
+ tagName: "div",
126
+ properties: { className: ["ox-tabs-fallback-content"] },
127
+ children: tab.content
128
+ }]
129
+ });
130
+ });
131
+ return {
132
+ type: "element",
133
+ tagName: "noscript",
134
+ properties: {},
135
+ children: [{
136
+ type: "element",
137
+ tagName: "div",
138
+ properties: { className: ["ox-tabs-fallback"] },
139
+ children
140
+ }]
141
+ };
142
+ }
143
+ /**
144
+ * Rehype plugin to transform Tabs components.
145
+ */
146
+ function rehypeTabs() {
147
+ return (tree) => {
148
+ const visit = (node) => {
149
+ if ("children" in node) for (let i = 0; i < node.children.length; i++) {
150
+ const child = node.children[i];
151
+ if (child.type === "element") if (child.tagName.toLowerCase() === "tabs") {
152
+ const tabs = parseTabChildren(child.children);
153
+ if (tabs.length > 0) {
154
+ const wrapper = {
155
+ type: "element",
156
+ tagName: "div",
157
+ properties: { className: ["ox-tabs-container"] },
158
+ children: [createTabsElement(tabs, String(tabGroupCounter++)), createFallbackElement(tabs)]
159
+ };
160
+ node.children[i] = wrapper;
161
+ }
162
+ } else visit(child);
163
+ }
164
+ };
165
+ visit(tree);
166
+ };
167
+ }
168
+ /**
169
+ * Transform Tabs components in HTML.
170
+ */
171
+ async function transformTabs(html) {
172
+ const result = await unified().use(rehypeParse, { fragment: true }).use(rehypeTabs).use(rehypeStringify).process(html);
173
+ return String(result);
174
+ }
175
+ /**
176
+ * Generate dynamic CSS for :has() based tab switching.
177
+ * This is needed because :has() selectors need unique IDs.
178
+ */
179
+ function generateTabsCSS(groupCount) {
180
+ if (groupCount === 0) return "";
181
+ let css = "/* Dynamic Tabs CSS */\n";
182
+ for (let g = 0; g < groupCount; g++) for (let t = 0; t < 8; t++) css += `.ox-tabs[data-group="${g}"]:has(#ox-tab-${g}-${t}:checked) .ox-tab-panel[data-tab="${t}"] { display: block; }\n`;
183
+ return css;
184
+ }
185
+ //#endregion
186
+ export { transformTabs as i, resetTabGroupCounter as n, tabs_exports as r, generateTabsCSS as t };
187
+
188
+ //# sourceMappingURL=tabs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabs.mjs","names":[],"sources":["../src/plugins/tabs.ts"],"sourcesContent":["/**\n * Tabs Plugin - Pure CSS implementation\n *\n * Transforms <Tabs>/<Tab> components into accessible HTML\n * with CSS :has() based tab switching (no JavaScript required).\n */\n\nimport { unified } from \"unified\";\nimport rehypeParse from \"rehype-parse\";\nimport rehypeStringify from \"rehype-stringify\";\nimport type { Root, Element } from \"hast\";\n\nlet tabGroupCounter = 0;\n\n/**\n * Reset tab group counter (for testing).\n */\nexport function resetTabGroupCounter(): void {\n tabGroupCounter = 0;\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\ninterface TabData {\n label: string;\n content: Element[];\n}\n\n/**\n * Parse Tab elements from Tabs children.\n */\nfunction parseTabChildren(children: Element[\"children\"]): TabData[] {\n const tabs: TabData[] = [];\n\n for (const child of children) {\n if (child.type !== \"element\") continue;\n\n // Handle <Tab label=\"...\">\n if (child.tagName.toLowerCase() === \"tab\") {\n const label = getAttribute(child, \"label\") || `Tab ${tabs.length + 1}`;\n tabs.push({\n label,\n content: child.children.filter(\n (c): c is Element => c.type === \"element\" || c.type === \"text\",\n ) as Element[],\n });\n }\n }\n\n return tabs;\n}\n\n/**\n * Create the HTML structure for tabs.\n */\nfunction createTabsElement(tabs: TabData[], groupId: string): Element {\n const children: Element[\"children\"] = [];\n\n // Create header with radio inputs and labels\n const headerChildren: Element[\"children\"] = [];\n\n tabs.forEach((tab, index) => {\n const inputId = `ox-tab-${groupId}-${index}`;\n\n // Radio input\n headerChildren.push({\n type: \"element\",\n tagName: \"input\",\n properties: {\n type: \"radio\",\n name: `ox-tabs-${groupId}`,\n id: inputId,\n checked: index === 0 ? true : undefined,\n },\n children: [],\n });\n\n // Label\n headerChildren.push({\n type: \"element\",\n tagName: \"label\",\n properties: {\n htmlFor: inputId,\n },\n children: [{ type: \"text\", value: tab.label }],\n });\n });\n\n // Tabs header\n children.push({\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-tabs-header\"] },\n children: headerChildren,\n });\n\n // Tab panels\n tabs.forEach((tab, index) => {\n children.push({\n type: \"element\",\n tagName: \"div\",\n properties: {\n className: [\"ox-tab-panel\"],\n \"data-tab\": String(index),\n },\n children: tab.content,\n });\n });\n\n return {\n type: \"element\",\n tagName: \"div\",\n properties: {\n className: [\"ox-tabs\"],\n \"data-group\": groupId,\n },\n children,\n };\n}\n\n/**\n * Create fallback HTML using <details> elements.\n */\nfunction createFallbackElement(tabs: TabData[]): Element {\n const children: Element[\"children\"] = [];\n\n tabs.forEach((tab, index) => {\n children.push({\n type: \"element\",\n tagName: \"details\",\n properties: {\n open: index === 0 ? true : undefined,\n },\n children: [\n {\n type: \"element\",\n tagName: \"summary\",\n properties: {},\n children: [{ type: \"text\", value: tab.label }],\n },\n {\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-tabs-fallback-content\"] },\n children: tab.content,\n },\n ],\n });\n });\n\n return {\n type: \"element\",\n tagName: \"noscript\",\n properties: {},\n children: [\n {\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-tabs-fallback\"] },\n children,\n },\n ],\n };\n}\n\n/**\n * Rehype plugin to transform Tabs components.\n */\nfunction rehypeTabs() {\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 <Tabs> component\n if (child.tagName.toLowerCase() === \"tabs\") {\n const tabs = parseTabChildren(child.children);\n\n if (tabs.length > 0) {\n const groupId = String(tabGroupCounter++);\n const tabsElement = createTabsElement(tabs, groupId);\n const fallbackElement = createFallbackElement(tabs);\n\n // Replace <Tabs> with new structure\n // Keep main tabs and add noscript fallback\n const wrapper: Element = {\n type: \"element\",\n tagName: \"div\",\n properties: { className: [\"ox-tabs-container\"] },\n children: [tabsElement, fallbackElement],\n };\n\n node.children[i] = wrapper;\n }\n } else {\n visit(child);\n }\n }\n }\n }\n };\n\n visit(tree);\n };\n}\n\n/**\n * Transform Tabs components in HTML.\n */\nexport async function transformTabs(html: string): Promise<string> {\n const result = await unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeTabs)\n .use(rehypeStringify)\n .process(html);\n\n return String(result);\n}\n\n/**\n * Generate dynamic CSS for :has() based tab switching.\n * This is needed because :has() selectors need unique IDs.\n */\nexport function generateTabsCSS(groupCount: number): string {\n if (groupCount === 0) return \"\";\n\n let css = \"/* Dynamic Tabs CSS */\\n\";\n\n for (let g = 0; g < groupCount; g++) {\n for (let t = 0; t < 8; t++) {\n css += `.ox-tabs[data-group=\"${g}\"]:has(#ox-tab-${g}-${t}:checked) .ox-tab-panel[data-tab=\"${t}\"] { display: block; }\\n`;\n }\n }\n\n return css;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAYA,IAAI,kBAAkB;;;;AAKtB,SAAgB,uBAA6B;AAC3C,mBAAkB;;;;;AAMpB,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;;;;;AAYlD,SAAS,iBAAiB,UAA0C;CAClE,MAAM,OAAkB,EAAE;AAE1B,MAAK,MAAM,SAAS,UAAU;AAC5B,MAAI,MAAM,SAAS,UAAW;AAG9B,MAAI,MAAM,QAAQ,aAAa,KAAK,OAAO;GACzC,MAAM,QAAQ,aAAa,OAAO,QAAQ,IAAI,OAAO,KAAK,SAAS;AACnE,QAAK,KAAK;IACR;IACA,SAAS,MAAM,SAAS,QACrB,MAAoB,EAAE,SAAS,aAAa,EAAE,SAAS,OACzD;IACF,CAAC;;;AAIN,QAAO;;;;;AAMT,SAAS,kBAAkB,MAAiB,SAA0B;CACpE,MAAM,WAAgC,EAAE;CAGxC,MAAM,iBAAsC,EAAE;AAE9C,MAAK,SAAS,KAAK,UAAU;EAC3B,MAAM,UAAU,UAAU,QAAQ,GAAG;AAGrC,iBAAe,KAAK;GAClB,MAAM;GACN,SAAS;GACT,YAAY;IACV,MAAM;IACN,MAAM,WAAW;IACjB,IAAI;IACJ,SAAS,UAAU,IAAI,OAAO,KAAA;IAC/B;GACD,UAAU,EAAE;GACb,CAAC;AAGF,iBAAe,KAAK;GAClB,MAAM;GACN,SAAS;GACT,YAAY,EACV,SAAS,SACV;GACD,UAAU,CAAC;IAAE,MAAM;IAAQ,OAAO,IAAI;IAAO,CAAC;GAC/C,CAAC;GACF;AAGF,UAAS,KAAK;EACZ,MAAM;EACN,SAAS;EACT,YAAY,EAAE,WAAW,CAAC,iBAAiB,EAAE;EAC7C,UAAU;EACX,CAAC;AAGF,MAAK,SAAS,KAAK,UAAU;AAC3B,WAAS,KAAK;GACZ,MAAM;GACN,SAAS;GACT,YAAY;IACV,WAAW,CAAC,eAAe;IAC3B,YAAY,OAAO,MAAM;IAC1B;GACD,UAAU,IAAI;GACf,CAAC;GACF;AAEF,QAAO;EACL,MAAM;EACN,SAAS;EACT,YAAY;GACV,WAAW,CAAC,UAAU;GACtB,cAAc;GACf;EACD;EACD;;;;;AAMH,SAAS,sBAAsB,MAA0B;CACvD,MAAM,WAAgC,EAAE;AAExC,MAAK,SAAS,KAAK,UAAU;AAC3B,WAAS,KAAK;GACZ,MAAM;GACN,SAAS;GACT,YAAY,EACV,MAAM,UAAU,IAAI,OAAO,KAAA,GAC5B;GACD,UAAU,CACR;IACE,MAAM;IACN,SAAS;IACT,YAAY,EAAE;IACd,UAAU,CAAC;KAAE,MAAM;KAAQ,OAAO,IAAI;KAAO,CAAC;IAC/C,EACD;IACE,MAAM;IACN,SAAS;IACT,YAAY,EAAE,WAAW,CAAC,2BAA2B,EAAE;IACvD,UAAU,IAAI;IACf,CACF;GACF,CAAC;GACF;AAEF,QAAO;EACL,MAAM;EACN,SAAS;EACT,YAAY,EAAE;EACd,UAAU,CACR;GACE,MAAM;GACN,SAAS;GACT,YAAY,EAAE,WAAW,CAAC,mBAAmB,EAAE;GAC/C;GACD,CACF;EACF;;;;;AAMH,SAAS,aAAa;AACpB,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,QAAQ;KAC1C,MAAM,OAAO,iBAAiB,MAAM,SAAS;AAE7C,SAAI,KAAK,SAAS,GAAG;MAOnB,MAAM,UAAmB;OACvB,MAAM;OACN,SAAS;OACT,YAAY,EAAE,WAAW,CAAC,oBAAoB,EAAE;OAChD,UAAU,CATQ,kBAAkB,MADtB,OAAO,kBAAkB,CACW,EAC5B,sBAAsB,KAAK,CAQT;OACzC;AAED,WAAK,SAAS,KAAK;;UAGrB,OAAM,MAAM;;;AAOtB,QAAM,KAAK;;;;;;AAOf,eAAsB,cAAc,MAA+B;CACjE,MAAM,SAAS,MAAM,SAAS,CAC3B,IAAI,aAAa,EAAE,UAAU,MAAM,CAAC,CACpC,IAAI,WAAW,CACf,IAAI,gBAAgB,CACpB,QAAQ,KAAK;AAEhB,QAAO,OAAO,OAAO;;;;;;AAOvB,SAAgB,gBAAgB,YAA4B;AAC1D,KAAI,eAAe,EAAG,QAAO;CAE7B,IAAI,MAAM;AAEV,MAAK,IAAI,IAAI,GAAG,IAAI,YAAY,IAC9B,MAAK,IAAI,IAAI,GAAG,IAAI,GAAG,IACrB,QAAO,wBAAwB,EAAE,iBAAiB,EAAE,GAAG,EAAE,oCAAoC,EAAE;AAInG,QAAO"}
package/dist/youtube.mjs CHANGED
@@ -1,2 +1,117 @@
1
- import { n as transformYouTube } from "./youtube2.mjs";
2
- export { transformYouTube };
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/youtube.ts
6
+ /**
7
+ * YouTube Plugin - Privacy-enhanced iframe embedding
8
+ *
9
+ * Transforms <YouTube> components into responsive iframe embeds
10
+ * using youtube-nocookie.com for enhanced privacy.
11
+ */
12
+ var youtube_exports = /* @__PURE__ */ __exportAll({
13
+ extractVideoId: () => extractVideoId,
14
+ transformYouTube: () => transformYouTube
15
+ });
16
+ const defaultOptions = {
17
+ privacyEnhanced: true,
18
+ aspectRatio: "16/9",
19
+ allowFullscreen: true,
20
+ lazyLoad: true
21
+ };
22
+ /**
23
+ * Get element attribute value.
24
+ */
25
+ function getAttribute(el, name) {
26
+ const value = el.properties?.[name];
27
+ if (typeof value === "string") return value;
28
+ if (Array.isArray(value)) return value.join(" ");
29
+ }
30
+ /**
31
+ * Extract YouTube video ID from various URL formats.
32
+ */
33
+ function extractVideoId(input) {
34
+ if (/^[a-zA-Z0-9_-]{11}$/.test(input)) return input;
35
+ for (const pattern of [/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/v\/)([a-zA-Z0-9_-]{11})/, /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/]) {
36
+ const match = input.match(pattern);
37
+ if (match) return match[1];
38
+ }
39
+ return null;
40
+ }
41
+ /**
42
+ * Build YouTube embed URL with parameters.
43
+ */
44
+ function buildEmbedUrl(videoId, options, params) {
45
+ const domain = options.privacyEnhanced ? "www.youtube-nocookie.com" : "www.youtube.com";
46
+ const url = new URL(`https://${domain}/embed/${videoId}`);
47
+ if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
48
+ return url.toString();
49
+ }
50
+ /**
51
+ * Create YouTube embed element.
52
+ */
53
+ function createYouTubeElement(videoId, options, title, start) {
54
+ const params = {};
55
+ if (start) params.start = start;
56
+ const iframe = {
57
+ type: "element",
58
+ tagName: "iframe",
59
+ properties: {
60
+ src: buildEmbedUrl(videoId, options, params),
61
+ title: title || `YouTube video ${videoId}`,
62
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
63
+ referrerpolicy: "strict-origin-when-cross-origin",
64
+ allowfullscreen: options.allowFullscreen || void 0,
65
+ loading: options.lazyLoad ? "lazy" : void 0
66
+ },
67
+ children: []
68
+ };
69
+ return {
70
+ type: "element",
71
+ tagName: "div",
72
+ properties: {
73
+ className: ["ox-youtube"],
74
+ style: `aspect-ratio: ${options.aspectRatio};`
75
+ },
76
+ children: [iframe]
77
+ };
78
+ }
79
+ /**
80
+ * Rehype plugin to transform YouTube components.
81
+ */
82
+ function rehypeYouTube(options) {
83
+ return (tree) => {
84
+ const visit = (node) => {
85
+ if ("children" in node) for (let i = 0; i < node.children.length; i++) {
86
+ const child = node.children[i];
87
+ if (child.type === "element") if (child.tagName.toLowerCase() === "youtube") {
88
+ const id = getAttribute(child, "id");
89
+ const url = getAttribute(child, "url");
90
+ const title = getAttribute(child, "title");
91
+ const start = getAttribute(child, "start");
92
+ const videoId = id ? extractVideoId(id) : url ? extractVideoId(url) : null;
93
+ if (videoId) {
94
+ const youtubeElement = createYouTubeElement(videoId, options, title, start);
95
+ node.children[i] = youtubeElement;
96
+ }
97
+ } else visit(child);
98
+ }
99
+ };
100
+ visit(tree);
101
+ };
102
+ }
103
+ /**
104
+ * Transform YouTube components in HTML.
105
+ */
106
+ async function transformYouTube(html, options) {
107
+ const mergedOptions = {
108
+ ...defaultOptions,
109
+ ...options
110
+ };
111
+ const result = await unified().use(rehypeParse, { fragment: true }).use(rehypeYouTube, mergedOptions).use(rehypeStringify).process(html);
112
+ return String(result);
113
+ }
114
+ //#endregion
115
+ export { transformYouTube as n, youtube_exports as r, extractVideoId as t };
116
+
117
+ //# sourceMappingURL=youtube.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"youtube.mjs","names":[],"sources":["../src/plugins/youtube.ts"],"sourcesContent":["/**\n * YouTube Plugin - Privacy-enhanced iframe embedding\n *\n * Transforms <YouTube> components into responsive iframe embeds\n * using youtube-nocookie.com for enhanced privacy.\n */\n\nimport { unified } from \"unified\";\nimport rehypeParse from \"rehype-parse\";\nimport rehypeStringify from \"rehype-stringify\";\nimport type { Root, Element, Properties } from \"hast\";\n\nexport interface YouTubeOptions {\n /** Use privacy-enhanced mode (youtube-nocookie.com). Default: true */\n privacyEnhanced?: boolean;\n /** Default aspect ratio. Default: \"16/9\" */\n aspectRatio?: string;\n /** Allow fullscreen. Default: true */\n allowFullscreen?: boolean;\n /** Lazy load iframe. Default: true */\n lazyLoad?: boolean;\n}\n\nconst defaultOptions: Required<YouTubeOptions> = {\n privacyEnhanced: true,\n aspectRatio: \"16/9\",\n allowFullscreen: true,\n lazyLoad: true,\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 YouTube video ID from various URL formats.\n */\nexport function extractVideoId(input: string): string | null {\n // Already a video ID (11 characters, alphanumeric + _ -)\n if (/^[a-zA-Z0-9_-]{11}$/.test(input)) {\n return input;\n }\n\n // Full URL patterns\n const patterns = [\n /(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/|youtube\\.com\\/embed\\/|youtube\\.com\\/v\\/)([a-zA-Z0-9_-]{11})/,\n /youtube\\.com\\/shorts\\/([a-zA-Z0-9_-]{11})/,\n ];\n\n for (const pattern of patterns) {\n const match = input.match(pattern);\n if (match) return match[1];\n }\n\n return null;\n}\n\n/**\n * Build YouTube embed URL with parameters.\n */\nfunction buildEmbedUrl(\n videoId: string,\n options: Required<YouTubeOptions>,\n params?: Record<string, string>,\n): string {\n const domain = options.privacyEnhanced ? \"www.youtube-nocookie.com\" : \"www.youtube.com\";\n const url = new URL(`https://${domain}/embed/${videoId}`);\n\n // Add any custom parameters\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n url.searchParams.set(key, value);\n }\n }\n\n return url.toString();\n}\n\n/**\n * Create YouTube embed element.\n */\nfunction createYouTubeElement(\n videoId: string,\n options: Required<YouTubeOptions>,\n title?: string,\n start?: string,\n): Element {\n const params: Record<string, string> = {};\n if (start) {\n params.start = start;\n }\n\n const embedUrl = buildEmbedUrl(videoId, options, params);\n\n const iframeProps: Properties = {\n src: embedUrl,\n title: title || `YouTube video ${videoId}`,\n allow:\n \"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\",\n referrerpolicy: \"strict-origin-when-cross-origin\",\n allowfullscreen: options.allowFullscreen || undefined,\n loading: options.lazyLoad ? \"lazy\" : undefined,\n };\n\n const iframe: Element = {\n type: \"element\",\n tagName: \"iframe\",\n properties: iframeProps,\n children: [],\n };\n\n return {\n type: \"element\",\n tagName: \"div\",\n properties: {\n className: [\"ox-youtube\"],\n style: `aspect-ratio: ${options.aspectRatio};`,\n },\n children: [iframe],\n };\n}\n\n/**\n * Rehype plugin to transform YouTube components.\n */\nfunction rehypeYouTube(options: Required<YouTubeOptions>) {\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 <YouTube> component\n if (child.tagName.toLowerCase() === \"youtube\") {\n const id = getAttribute(child, \"id\");\n const url = getAttribute(child, \"url\");\n const title = getAttribute(child, \"title\");\n const start = getAttribute(child, \"start\");\n\n // Extract video ID from id or url attribute\n const videoId = id ? extractVideoId(id) : url ? extractVideoId(url) : null;\n\n if (videoId) {\n const youtubeElement = createYouTubeElement(videoId, options, title, start);\n node.children[i] = youtubeElement;\n }\n } else {\n visit(child);\n }\n }\n }\n }\n };\n\n visit(tree);\n };\n}\n\n/**\n * Transform YouTube components in HTML.\n */\nexport async function transformYouTube(html: string, options?: YouTubeOptions): Promise<string> {\n const mergedOptions = { ...defaultOptions, ...options };\n\n const result = await unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeYouTube, mergedOptions)\n .use(rehypeStringify)\n .process(html);\n\n return String(result);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAuBA,MAAM,iBAA2C;CAC/C,iBAAiB;CACjB,aAAa;CACb,iBAAiB;CACjB,UAAU;CACX;;;;AAKD,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,SAAgB,eAAe,OAA8B;AAE3D,KAAI,sBAAsB,KAAK,MAAM,CACnC,QAAO;AAST,MAAK,MAAM,WALM,CACf,sGACA,4CACD,EAE+B;EAC9B,MAAM,QAAQ,MAAM,MAAM,QAAQ;AAClC,MAAI,MAAO,QAAO,MAAM;;AAG1B,QAAO;;;;;AAMT,SAAS,cACP,SACA,SACA,QACQ;CACR,MAAM,SAAS,QAAQ,kBAAkB,6BAA6B;CACtE,MAAM,MAAM,IAAI,IAAI,WAAW,OAAO,SAAS,UAAU;AAGzD,KAAI,OACF,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,CAC/C,KAAI,aAAa,IAAI,KAAK,MAAM;AAIpC,QAAO,IAAI,UAAU;;;;;AAMvB,SAAS,qBACP,SACA,SACA,OACA,OACS;CACT,MAAM,SAAiC,EAAE;AACzC,KAAI,MACF,QAAO,QAAQ;CAejB,MAAM,SAAkB;EACtB,MAAM;EACN,SAAS;EACT,YAb8B;GAC9B,KAHe,cAAc,SAAS,SAAS,OAAO;GAItD,OAAO,SAAS,iBAAiB;GACjC,OACE;GACF,gBAAgB;GAChB,iBAAiB,QAAQ,mBAAmB,KAAA;GAC5C,SAAS,QAAQ,WAAW,SAAS,KAAA;GACtC;EAMC,UAAU,EAAE;EACb;AAED,QAAO;EACL,MAAM;EACN,SAAS;EACT,YAAY;GACV,WAAW,CAAC,aAAa;GACzB,OAAO,iBAAiB,QAAQ,YAAY;GAC7C;EACD,UAAU,CAAC,OAAO;EACnB;;;;;AAMH,SAAS,cAAc,SAAmC;AACxD,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,WAAW;KAC7C,MAAM,KAAK,aAAa,OAAO,KAAK;KACpC,MAAM,MAAM,aAAa,OAAO,MAAM;KACtC,MAAM,QAAQ,aAAa,OAAO,QAAQ;KAC1C,MAAM,QAAQ,aAAa,OAAO,QAAQ;KAG1C,MAAM,UAAU,KAAK,eAAe,GAAG,GAAG,MAAM,eAAe,IAAI,GAAG;AAEtE,SAAI,SAAS;MACX,MAAM,iBAAiB,qBAAqB,SAAS,SAAS,OAAO,MAAM;AAC3E,WAAK,SAAS,KAAK;;UAGrB,OAAM,MAAM;;;AAOtB,QAAM,KAAK;;;;;;AAOf,eAAsB,iBAAiB,MAAc,SAA2C;CAC9F,MAAM,gBAAgB;EAAE,GAAG;EAAgB,GAAG;EAAS;CAEvD,MAAM,SAAS,MAAM,SAAS,CAC3B,IAAI,aAAa,EAAE,UAAU,MAAM,CAAC,CACpC,IAAI,eAAe,cAAc,CACjC,IAAI,gBAAgB,CACpB,QAAQ,KAAK;AAEhB,QAAO,OAAO,OAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ox-content/vite-plugin",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Vite plugin for Ox Content - High-performance Markdown processing with Environment API",
5
5
  "keywords": [
6
6
  "environment-api",
@@ -50,7 +50,7 @@
50
50
  "shiki": "^1.24.0",
51
51
  "typescript": "^5.7.0",
52
52
  "unified": "^11.0.5",
53
- "@ox-content/napi": "2.8.0"
53
+ "@ox-content/napi": "2.9.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@playwright/test": "^1.59.1",
package/dist/github2.mjs DELETED
@@ -1,291 +0,0 @@
1
- import { unified } from "unified";
2
- import rehypeParse from "rehype-parse";
3
- import rehypeStringify from "rehype-stringify";
4
- //#region src/plugins/github.ts
5
- /**
6
- * GitHub Plugin - Repository card embedding
7
- *
8
- * Transforms <GitHub> components into static repository cards
9
- * by fetching data from GitHub API at build time.
10
- */
11
- const defaultOptions = {
12
- token: "",
13
- cache: true,
14
- cacheTTL: 36e5
15
- };
16
- const repoCache = /* @__PURE__ */ new Map();
17
- const GITHUB_REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
18
- function isSafeGitHubRepo(repo) {
19
- return GITHUB_REPO_RE.test(repo) && !repo.split("/").some((part) => part === "." || part === "..");
20
- }
21
- /**
22
- * Get element attribute value.
23
- */
24
- function getAttribute(el, name) {
25
- const value = el.properties?.[name];
26
- if (typeof value === "string") return value;
27
- if (Array.isArray(value)) return value.join(" ");
28
- }
29
- /**
30
- * Format number with K/M suffix.
31
- */
32
- function formatNumber(num) {
33
- if (num >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
34
- if (num >= 1e3) return `${(num / 1e3).toFixed(1)}k`;
35
- return String(num);
36
- }
37
- /**
38
- * Fetch repository data from GitHub API.
39
- */
40
- async function fetchRepoData(repo, options) {
41
- if (!isSafeGitHubRepo(repo)) return null;
42
- if (options.cache) {
43
- const cached = repoCache.get(repo);
44
- if (cached && Date.now() - cached.timestamp < options.cacheTTL) return cached.data;
45
- }
46
- try {
47
- const headers = {
48
- Accept: "application/vnd.github.v3+json",
49
- "User-Agent": "ox-content-github-plugin"
50
- };
51
- if (options.token) headers.Authorization = `Bearer ${options.token}`;
52
- const response = await fetch(`https://api.github.com/repos/${repo}`, { headers });
53
- if (!response.ok) {
54
- console.warn(`Failed to fetch GitHub repo ${repo}: ${response.status}`);
55
- return null;
56
- }
57
- const data = await response.json();
58
- if (options.cache) repoCache.set(repo, {
59
- data,
60
- timestamp: Date.now()
61
- });
62
- return data;
63
- } catch (error) {
64
- console.warn(`Error fetching GitHub repo ${repo}:`, error);
65
- return null;
66
- }
67
- }
68
- /**
69
- * Create GitHub card element from repo data.
70
- */
71
- function createGitHubCard(repoData) {
72
- const statsChildren = [];
73
- if (repoData.language) statsChildren.push({
74
- type: "element",
75
- tagName: "span",
76
- properties: { className: ["ox-github-language"] },
77
- children: [{
78
- type: "element",
79
- tagName: "span",
80
- properties: {
81
- className: ["ox-github-language-color"],
82
- "data-lang": repoData.language.toLowerCase()
83
- },
84
- children: []
85
- }, {
86
- type: "text",
87
- value: repoData.language
88
- }]
89
- });
90
- statsChildren.push({
91
- type: "element",
92
- tagName: "span",
93
- properties: { className: ["ox-github-stat"] },
94
- children: [{
95
- type: "element",
96
- tagName: "svg",
97
- properties: {
98
- viewBox: "0 0 16 16",
99
- fill: "currentColor"
100
- },
101
- children: [{
102
- type: "element",
103
- tagName: "path",
104
- properties: { d: "M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" },
105
- children: []
106
- }]
107
- }, {
108
- type: "text",
109
- value: formatNumber(repoData.stargazers_count)
110
- }]
111
- });
112
- statsChildren.push({
113
- type: "element",
114
- tagName: "span",
115
- properties: { className: ["ox-github-stat"] },
116
- children: [{
117
- type: "element",
118
- tagName: "svg",
119
- properties: {
120
- viewBox: "0 0 16 16",
121
- fill: "currentColor"
122
- },
123
- children: [{
124
- type: "element",
125
- tagName: "path",
126
- properties: { d: "M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z" },
127
- children: []
128
- }]
129
- }, {
130
- type: "text",
131
- value: formatNumber(repoData.forks_count)
132
- }]
133
- });
134
- return {
135
- type: "element",
136
- tagName: "a",
137
- properties: {
138
- className: ["ox-github-card"],
139
- href: repoData.html_url,
140
- target: "_blank",
141
- rel: "noopener noreferrer"
142
- },
143
- children: [
144
- {
145
- type: "element",
146
- tagName: "div",
147
- properties: { className: ["ox-github-header"] },
148
- children: [{
149
- type: "element",
150
- tagName: "svg",
151
- properties: {
152
- className: ["ox-github-icon"],
153
- viewBox: "0 0 16 16",
154
- fill: "currentColor"
155
- },
156
- children: [{
157
- type: "element",
158
- tagName: "path",
159
- properties: { d: "M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z" },
160
- children: []
161
- }]
162
- }, {
163
- type: "element",
164
- tagName: "span",
165
- properties: { className: ["ox-github-repo"] },
166
- children: [{
167
- type: "text",
168
- value: repoData.full_name
169
- }]
170
- }]
171
- },
172
- ...repoData.description ? [{
173
- type: "element",
174
- tagName: "p",
175
- properties: { className: ["ox-github-description"] },
176
- children: [{
177
- type: "text",
178
- value: repoData.description
179
- }]
180
- }] : [],
181
- {
182
- type: "element",
183
- tagName: "div",
184
- properties: { className: ["ox-github-stats"] },
185
- children: statsChildren
186
- }
187
- ]
188
- };
189
- }
190
- /**
191
- * Create fallback element when repo data is unavailable.
192
- */
193
- function createFallbackCard(repo) {
194
- return {
195
- type: "element",
196
- tagName: "a",
197
- properties: {
198
- className: ["ox-github-card", "error"],
199
- href: isSafeGitHubRepo(repo) ? `https://github.com/${repo}` : "#",
200
- target: "_blank",
201
- rel: "noopener noreferrer"
202
- },
203
- children: [{
204
- type: "element",
205
- tagName: "div",
206
- properties: { className: ["ox-github-header"] },
207
- children: [{
208
- type: "element",
209
- tagName: "svg",
210
- properties: {
211
- className: ["ox-github-icon"],
212
- viewBox: "0 0 16 16",
213
- fill: "currentColor"
214
- },
215
- children: [{
216
- type: "element",
217
- tagName: "path",
218
- properties: { d: "M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z" },
219
- children: []
220
- }]
221
- }, {
222
- type: "element",
223
- tagName: "span",
224
- properties: { className: ["ox-github-repo"] },
225
- children: [{
226
- type: "text",
227
- value: repo
228
- }]
229
- }]
230
- }]
231
- };
232
- }
233
- /**
234
- * Collect all GitHub repos from HTML for pre-fetching.
235
- */
236
- async function collectGitHubRepos(html) {
237
- const repos = [];
238
- const repoPattern = /<github[^>]*\s+repo=["']([^"']+)["']/gi;
239
- let match;
240
- while ((match = repoPattern.exec(html)) !== null) if (isSafeGitHubRepo(match[1])) repos.push(match[1]);
241
- return repos;
242
- }
243
- /**
244
- * Pre-fetch all GitHub repos data.
245
- */
246
- async function prefetchGitHubRepos(repos, options) {
247
- const mergedOptions = {
248
- ...defaultOptions,
249
- ...options
250
- };
251
- const results = /* @__PURE__ */ new Map();
252
- await Promise.all(repos.map(async (repo) => {
253
- const data = await fetchRepoData(repo, mergedOptions);
254
- results.set(repo, data);
255
- }));
256
- return results;
257
- }
258
- /**
259
- * Rehype plugin to transform GitHub components.
260
- */
261
- function rehypeGitHub(repoDataMap) {
262
- return (tree) => {
263
- const visit = (node) => {
264
- if ("children" in node) for (let i = 0; i < node.children.length; i++) {
265
- const child = node.children[i];
266
- if (child.type === "element") if (child.tagName.toLowerCase() === "github") {
267
- const repo = getAttribute(child, "repo");
268
- if (repo) {
269
- const repoData = repoDataMap.get(repo);
270
- const cardElement = repoData ? createGitHubCard(repoData) : createFallbackCard(repo);
271
- node.children[i] = cardElement;
272
- }
273
- } else visit(child);
274
- }
275
- };
276
- visit(tree);
277
- };
278
- }
279
- /**
280
- * Transform GitHub components in HTML.
281
- */
282
- async function transformGitHub(html, repoDataMap, options) {
283
- let dataMap = repoDataMap;
284
- if (!dataMap) dataMap = await prefetchGitHubRepos(await collectGitHubRepos(html), options);
285
- const result = await unified().use(rehypeParse, { fragment: true }).use(rehypeGitHub, dataMap).use(rehypeStringify).process(html);
286
- return String(result);
287
- }
288
- //#endregion
289
- export { transformGitHub as a, prefetchGitHubRepos as i, fetchRepoData as n, isSafeGitHubRepo as r, collectGitHubRepos as t };
290
-
291
- //# sourceMappingURL=github2.mjs.map