@ox-content/vite-plugin 0.17.0 → 1.0.0-alpha.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/index.cjs CHANGED
@@ -15,6 +15,7 @@ let shiki = require("shiki");
15
15
  let node_path = require("node:path");
16
16
  let fs = require("fs");
17
17
  fs = require_chunk.__toESM(fs);
18
+ let node_crypto = require("node:crypto");
18
19
  let fs_promises = require("fs/promises");
19
20
  fs_promises = require_chunk.__toESM(fs_promises);
20
21
  let glob = require("glob");
@@ -9880,6 +9881,153 @@ async function generateHtmlPage(pageData, navGroups, siteName, base, ogImage, th
9880
9881
  theme: themeForRust
9881
9882
  });
9882
9883
  }
9884
+ const SSG_STYLE_BLOCK_RE = /[ \t]*<!-- ox-content:styles:start -->\s*<style>([\s\S]*?)<\/style>\s*<!-- ox-content:styles:end -->/;
9885
+ const SSG_SCRIPT_BLOCK_RE = /[ \t]*<!-- ox-content:scripts:start -->\s*<script>([\s\S]*?)<\/script>\s*<!-- ox-content:scripts:end -->/;
9886
+ const FIRST_INLINE_STYLE_RE = /[ \t]*<style>([\s\S]*?)<\/style>/;
9887
+ const LAST_INLINE_BODY_SCRIPT_RE = /[ \t]*<script>([\s\S]*?)<\/script>\s*<\/body>/;
9888
+ const CSS_SECTION_RE = /\/\* ox-content:css:([a-z0-9-]+):start \*\/\s*([\s\S]*?)\s*\/\* ox-content:css:\1:end \*\//g;
9889
+ const SEARCH_CHUNK_RE = /\/\/ ox-content:search:start\s*([\s\S]*?)\s*\/\/ ox-content:search:end/;
9890
+ const SEARCH_CHUNK_PLACEHOLDER = "__OX_CONTENT_SEARCH_CHUNK__";
9891
+ const CORE_CSS_SECTION_NAMES = new Set(["base", "footer"]);
9892
+ const THEME_INLINE_CSS_MAX_BYTES = 2048;
9893
+ function createContentHash(content) {
9894
+ return (0, node_crypto.createHash)("sha256").update(content).digest("hex").slice(0, 10);
9895
+ }
9896
+ function sanitizeChunkLabel(label) {
9897
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "asset";
9898
+ }
9899
+ function toPublicAssetPath(base, fileName) {
9900
+ return `${base.endsWith("/") ? base : `${base}/`}assets/${fileName}`;
9901
+ }
9902
+ function hasRelativeCssUrls(css) {
9903
+ let cursor = 0;
9904
+ while (cursor < css.length) {
9905
+ const urlIndex = css.indexOf("url(", cursor);
9906
+ if (urlIndex === -1) return false;
9907
+ let valueStart = urlIndex + 4;
9908
+ while (valueStart < css.length && /\s/.test(css[valueStart])) valueStart++;
9909
+ const quote = css[valueStart] === "\"" || css[valueStart] === "'" ? css[valueStart] : "";
9910
+ if (quote) valueStart++;
9911
+ let valueEnd = valueStart;
9912
+ while (valueEnd < css.length) {
9913
+ const char = css[valueEnd];
9914
+ if (quote) {
9915
+ if (char === "\\") {
9916
+ valueEnd += 2;
9917
+ continue;
9918
+ }
9919
+ if (char === quote) break;
9920
+ } else if (char === ")") break;
9921
+ valueEnd++;
9922
+ }
9923
+ const value = css.slice(valueStart, valueEnd).trim();
9924
+ if (value && !value.startsWith("data:") && !value.startsWith("http:") && !value.startsWith("https:") && !value.startsWith("//") && !value.startsWith("/") && !value.startsWith("#") && !value.startsWith("blob:") && !value.startsWith("var(")) return true;
9925
+ cursor = valueEnd + 1;
9926
+ }
9927
+ return false;
9928
+ }
9929
+ function createSharedAssetChunk(type, label, content, outDir, base) {
9930
+ const hash = createContentHash(content);
9931
+ const fileName = `ox-content-${sanitizeChunkLabel(label)}-${hash}.${type}`;
9932
+ return {
9933
+ outputPath: path$1.join(outDir, "assets", fileName),
9934
+ publicPath: toPublicAssetPath(base, fileName),
9935
+ content
9936
+ };
9937
+ }
9938
+ function extractCssSections(cssContent) {
9939
+ return Array.from(cssContent.matchAll(CSS_SECTION_RE)).map(([, name, content]) => ({
9940
+ name,
9941
+ content: content.trim()
9942
+ })).filter((section) => section.content.length > 0);
9943
+ }
9944
+ function getOrCreateSharedChunk(chunks, type, label, content, outDir, base) {
9945
+ let chunk = chunks.get(content);
9946
+ if (!chunk) {
9947
+ chunk = createSharedAssetChunk(type, label, content, outDir, base);
9948
+ chunks.set(content, chunk);
9949
+ }
9950
+ return chunk;
9951
+ }
9952
+ function buildStyleReplacement(cssContent, cssChunks, outDir, base) {
9953
+ const sections = extractCssSections(cssContent);
9954
+ const effectiveSections = sections.length > 0 ? sections : [{
9955
+ name: "css",
9956
+ content: cssContent.trim()
9957
+ }];
9958
+ const coreContent = effectiveSections.filter((section) => CORE_CSS_SECTION_NAMES.has(section.name)).map((section) => section.content).join("\n").trim();
9959
+ const fragments = [];
9960
+ if (coreContent) {
9961
+ const coreChunk = getOrCreateSharedChunk(cssChunks, "css", "core", coreContent, outDir, base);
9962
+ fragments.push(` <link rel="stylesheet" href="${coreChunk.publicPath}">`);
9963
+ }
9964
+ for (const section of effectiveSections) {
9965
+ if (CORE_CSS_SECTION_NAMES.has(section.name)) continue;
9966
+ if (section.name === "theme" && (hasRelativeCssUrls(section.content) || section.content.length <= THEME_INLINE_CSS_MAX_BYTES) || hasRelativeCssUrls(section.content)) {
9967
+ fragments.push(` <style>${section.content}</style>`);
9968
+ continue;
9969
+ }
9970
+ const chunk = getOrCreateSharedChunk(cssChunks, "css", section.name, section.content, outDir, base);
9971
+ fragments.push(` <link rel="stylesheet" href="${chunk.publicPath}">`);
9972
+ }
9973
+ return fragments.join("\n");
9974
+ }
9975
+ function buildScriptReplacement(jsContent, jsChunks, outDir, base) {
9976
+ const searchMatch = jsContent.match(SEARCH_CHUNK_RE);
9977
+ if (searchMatch && jsContent.includes(SEARCH_CHUNK_PLACEHOLDER)) {
9978
+ const searchContent = searchMatch[1].trim();
9979
+ if (searchContent) {
9980
+ const searchChunk = getOrCreateSharedChunk(jsChunks, "js", "search", searchContent, outDir, base);
9981
+ const coreContent = jsContent.replace(SEARCH_CHUNK_RE, "").replaceAll(SEARCH_CHUNK_PLACEHOLDER, searchChunk.publicPath).trim();
9982
+ if (coreContent) return ` <script defer src="${getOrCreateSharedChunk(jsChunks, "js", "core", coreContent, outDir, base).publicPath}"><\/script>`;
9983
+ }
9984
+ }
9985
+ const fallbackContent = jsContent.trim();
9986
+ if (!fallbackContent) return "";
9987
+ return ` <script defer src="${getOrCreateSharedChunk(jsChunks, "js", "js", fallbackContent, outDir, base).publicPath}"><\/script>`;
9988
+ }
9989
+ async function externalizeSharedPageAssets(pages, outDir, base) {
9990
+ const cssChunks = /* @__PURE__ */ new Map();
9991
+ const jsChunks = /* @__PURE__ */ new Map();
9992
+ const optimizedPages = pages.map((page) => {
9993
+ let html = page.html;
9994
+ const styleMatch = html.match(SSG_STYLE_BLOCK_RE);
9995
+ if (styleMatch) {
9996
+ const replacement = buildStyleReplacement(styleMatch[1], cssChunks, outDir, base);
9997
+ html = html.replace(SSG_STYLE_BLOCK_RE, replacement);
9998
+ } else {
9999
+ const inlineStyleMatch = html.match(FIRST_INLINE_STYLE_RE);
10000
+ if (inlineStyleMatch) {
10001
+ const replacement = buildStyleReplacement(inlineStyleMatch[1], cssChunks, outDir, base);
10002
+ html = html.replace(FIRST_INLINE_STYLE_RE, replacement);
10003
+ }
10004
+ }
10005
+ const scriptMatch = html.match(SSG_SCRIPT_BLOCK_RE);
10006
+ if (scriptMatch) {
10007
+ const replacement = buildScriptReplacement(scriptMatch[1], jsChunks, outDir, base);
10008
+ html = html.replace(SSG_SCRIPT_BLOCK_RE, replacement);
10009
+ } else {
10010
+ const inlineScriptMatch = html.match(LAST_INLINE_BODY_SCRIPT_RE);
10011
+ if (inlineScriptMatch) {
10012
+ const replacement = buildScriptReplacement(inlineScriptMatch[1], jsChunks, outDir, base);
10013
+ html = html.replace(LAST_INLINE_BODY_SCRIPT_RE, replacement ? `${replacement}\n</body>` : "</body>");
10014
+ }
10015
+ }
10016
+ return {
10017
+ ...page,
10018
+ html
10019
+ };
10020
+ });
10021
+ const chunks = [...cssChunks.values(), ...jsChunks.values()];
10022
+ await Promise.all(chunks.map(async (chunk) => {
10023
+ await fs_promises.mkdir(path$1.dirname(chunk.outputPath), { recursive: true });
10024
+ await fs_promises.writeFile(chunk.outputPath, chunk.content, "utf-8");
10025
+ }));
10026
+ return {
10027
+ pages: optimizedPages,
10028
+ assets: chunks.map((chunk) => chunk.outputPath)
10029
+ };
10030
+ }
9883
10031
  /**
9884
10032
  * Converts a markdown file path to its corresponding HTML output path.
9885
10033
  */
@@ -10024,6 +10172,7 @@ async function buildSsg(options, root) {
10024
10172
  const outDir = path$1.resolve(root, options.outDir);
10025
10173
  const base = options.base.endsWith("/") ? options.base : options.base + "/";
10026
10174
  const generatedFiles = [];
10175
+ const generatedPages = [];
10027
10176
  const errors = [];
10028
10177
  if (ssgOptions.clean) try {
10029
10178
  await fs_promises.rm(outDir, {
@@ -10137,13 +10286,22 @@ async function buildSsg(options, root) {
10137
10286
  entryPage
10138
10287
  }, navItems, siteName, base, pageOgImage, ssgOptions.theme);
10139
10288
  const outputPath = getOutputPath(inputPath, srcDir, outDir, ssgOptions.extension);
10140
- await fs_promises.mkdir(path$1.dirname(outputPath), { recursive: true });
10141
- await fs_promises.writeFile(outputPath, html, "utf-8");
10142
- generatedFiles.push(outputPath);
10289
+ generatedPages.push({
10290
+ inputPath,
10291
+ outputPath,
10292
+ html
10293
+ });
10143
10294
  } catch (err) {
10144
10295
  const errorMessage = err instanceof Error ? err.message : String(err);
10145
10296
  errors.push(`Failed to generate HTML for ${pageResult.inputPath}: ${errorMessage}`);
10146
10297
  }
10298
+ const optimizedOutput = await externalizeSharedPageAssets(generatedPages, outDir, base);
10299
+ generatedFiles.push(...optimizedOutput.assets);
10300
+ for (const page of optimizedOutput.pages) {
10301
+ await fs_promises.mkdir(path$1.dirname(page.outputPath), { recursive: true });
10302
+ await fs_promises.writeFile(page.outputPath, page.html, "utf-8");
10303
+ generatedFiles.push(page.outputPath);
10304
+ }
10147
10305
  return {
10148
10306
  files: generatedFiles,
10149
10307
  errors
@@ -11920,7 +12078,7 @@ function oxContent(options = {}) {
11920
12078
  const root = config?.root || process.cwd();
11921
12079
  try {
11922
12080
  const result = await buildSsg(resolvedOptions, root);
11923
- if (result.files.length > 0) console.log(`[ox-content] Generated ${result.files.length} HTML files`);
12081
+ if (result.files.length > 0) console.log(`[ox-content] Generated ${result.files.length} output files`);
11924
12082
  if (result.errors.length > 0) for (const error of result.errors) console.warn(`[ox-content] ${error}`);
11925
12083
  } catch (err) {
11926
12084
  console.error("[ox-content] SSG build failed:", err);