@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 +162 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +162 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
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}
|
|
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);
|