@ox-content/vite-plugin 0.3.0-alpha.21 → 0.6.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/github2.cjs.map +1 -1
- package/dist/github2.js.map +1 -1
- package/dist/index.cjs +17 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -5
- package/dist/index.js.map +1 -1
- package/dist/mermaid2.cjs.map +1 -1
- package/dist/mermaid2.js.map +1 -1
- package/dist/ogp2.cjs.map +1 -1
- package/dist/ogp2.js.map +1 -1
- package/dist/tabs2.cjs.map +1 -1
- package/dist/tabs2.js.map +1 -1
- package/dist/youtube2.cjs.map +1 -1
- package/dist/youtube2.js.map +1 -1
- package/package.json +2 -2
package/dist/mermaid2.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mermaid2.cjs","names":[],"sources":["../src/plugins/mermaid.ts"],"sourcesContent":["/**\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\";\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: (
|
|
1
|
+
{"version":3,"file":"mermaid2.cjs","names":[],"sources":["../src/plugins/mermaid.ts"],"sourcesContent":["/**\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\";\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 mod = await import(\"@ox-content/napi\");\n // CJS-to-ESM interop: native functions are on mod.default\n const binding = (mod.default ?? mod) 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":";;;;;;;;;;;;;;AAkBA,IAAI,eAEO;AAEX,IAAI,oBAAoB;AAExB,eAAe,WAAW;AACxB,KAAI,kBAAmB,QAAO;AAC9B,qBAAoB;AACpB,KAAI;EACF,MAAM,MAAM,MAAM,OAAO;EAEzB,MAAM,UAAW,IAAI,WAAW;AAChC,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,OAAW,QAAO;AAIzC,KAAI;EACF,MAAM,WAAoB,QAAQ,0BAA0B;EAC5D,MAAM,sCAAwB,IAAI,IAAI,YAAY,MAAM,CAAC;AACzD,8BAAe,QAAQ,EAAE;AACvB,oBAAiB;AACjB,UAAO;;SAEH;CAKR,MAAM,8BAAe,QAAQ,KAAK,EAAE,gBAAgB,QAAQ,OAAO;AACnE,6BAAe,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/mermaid2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mermaid2.js","names":[],"sources":["../src/plugins/mermaid.ts"],"sourcesContent":["/**\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\";\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: (
|
|
1
|
+
{"version":3,"file":"mermaid2.js","names":[],"sources":["../src/plugins/mermaid.ts"],"sourcesContent":["/**\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\";\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 mod = await import(\"@ox-content/napi\");\n // CJS-to-ESM interop: native functions are on mod.default\n const binding = (mod.default ?? mod) 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":";;;;;;;;;;;;;AAkBA,IAAI,eAEO;AAEX,IAAI,oBAAoB;AAExB,eAAe,WAAW;AACxB,KAAI,kBAAmB,QAAO;AAC9B,qBAAoB;AACpB,KAAI;EACF,MAAM,MAAM,MAAM,OAAO;EAEzB,MAAM,UAAW,IAAI,WAAW;AAChC,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,OAAW,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/ogp2.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ogp2.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 = 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 = 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 = 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 = 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(url: string, options: Required<OgpOptions>): 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(urls: string[], options?: OgpOptions): 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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"}
|
|
1
|
+
{"version":3,"file":"ogp2.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":"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"}
|
package/dist/ogp2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ogp2.js","names":[],"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 = 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 = 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 = 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 = 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(url: string, options: Required<OgpOptions>): 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(urls: string[], options?: OgpOptions): 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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"}
|
|
1
|
+
{"version":3,"file":"ogp2.js","names":[],"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":"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"}
|
package/dist/tabs2.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs2.cjs","names":["rehypeParse","rehypeStringify"],"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, Text } 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 * Extract text content from a hast node.\n */\nfunction getTextContent(node: Element | Root): string {\n let text = \"\";\n if (\"children\" in node) {\n for (const child of node.children) {\n if (child.type === \"text\") {\n text += (child as Text).value;\n } else if (child.type === \"element\") {\n text += getTextContent(child as Element);\n }\n }\n }\n return text;\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((c): c is Element => c.type === \"element\" || c.type === \"text\") 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;;;;AA6BtB,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,
|
|
1
|
+
{"version":3,"file":"tabs2.cjs","names":["rehypeParse","rehypeStringify"],"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, Text } 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 * Extract text content from a hast node.\n */\nfunction getTextContent(node: Element | Root): string {\n let text = \"\";\n if (\"children\" in node) {\n for (const child of node.children) {\n if (child.type === \"text\") {\n text += (child as Text).value;\n } else if (child.type === \"element\") {\n text += getTextContent(child as Element);\n }\n }\n }\n return text;\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":"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"}
|
package/dist/tabs2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs2.js","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, Text } 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 * Extract text content from a hast node.\n */\nfunction getTextContent(node: Element | Root): string {\n let text = \"\";\n if (\"children\" in node) {\n for (const child of node.children) {\n if (child.type === \"text\") {\n text += (child as Text).value;\n } else if (child.type === \"element\") {\n text += getTextContent(child as Element);\n }\n }\n }\n return text;\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((c): c is Element => c.type === \"element\" || c.type === \"text\") 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;;;;AA6BtB,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,
|
|
1
|
+
{"version":3,"file":"tabs2.js","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, Text } 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 * Extract text content from a hast node.\n */\nfunction getTextContent(node: Element | Root): string {\n let text = \"\";\n if (\"children\" in node) {\n for (const child of node.children) {\n if (child.type === \"text\") {\n text += (child as Text).value;\n } else if (child.type === \"element\") {\n text += getTextContent(child as Element);\n }\n }\n }\n return text;\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":"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"}
|
package/dist/youtube2.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"youtube2.cjs","names":["rehypeParse","rehypeStringify"],"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(videoId: string
|
|
1
|
+
{"version":3,"file":"youtube2.cjs","names":["rehypeParse","rehypeStringify"],"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;GAC5C,SAAS,QAAQ,WAAW,SAAS;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,4BAAe,CAC3B,IAAIA,sBAAa,EAAE,UAAU,MAAM,CAAC,CACpC,IAAI,eAAe,cAAc,CACjC,IAAIC,yBAAgB,CACpB,QAAQ,KAAK;AAEhB,QAAO,OAAO,OAAO"}
|
package/dist/youtube2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"youtube2.js","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(videoId: string
|
|
1
|
+
{"version":3,"file":"youtube2.js","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;GAC5C,SAAS,QAAQ,WAAW,SAAS;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": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Vite plugin for Ox Content - High-performance Markdown processing with Environment API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"rehype-stringify": "^10.0.1",
|
|
49
49
|
"shiki": "^1.24.0",
|
|
50
50
|
"unified": "^11.0.5",
|
|
51
|
-
"@ox-content/napi": "0.
|
|
51
|
+
"@ox-content/napi": "0.6.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/hast": "^3.0.4",
|