@soonit/rspress-plugin-og 0.0.2 → 0.1.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.d.mts +3 -9
- package/dist/index.mjs +40 -68
- package/dist/types-7FdncbbI.d.mts +94 -0
- package/dist/types.d.mts +1 -1
- package/package.json +2 -4
- package/dist/types-ChngGzQc.d.mts +0 -41
package/dist/index.d.mts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import { t as Options } from "./types-
|
|
2
|
-
import
|
|
3
|
-
import { PageIndexInfo } from "@rspress/core";
|
|
1
|
+
import { t as Options } from "./types-7FdncbbI.mjs";
|
|
2
|
+
import { RspressPlugin } from "@rspress/core";
|
|
4
3
|
|
|
5
4
|
//#region src/index.d.ts
|
|
6
|
-
declare function export_default(userOptions: Options):
|
|
7
|
-
name: string;
|
|
8
|
-
config(config: _rspress_core0.UserConfig): _rspress_core0.UserConfig;
|
|
9
|
-
extendPageData: (pageData: PageIndexInfo) => void;
|
|
10
|
-
afterBuild(config: _rspress_core0.UserConfig): Promise<void>;
|
|
11
|
-
};
|
|
5
|
+
declare function export_default(userOptions: Options): RspressPlugin;
|
|
12
6
|
//#endregion
|
|
13
7
|
export { export_default as default };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { dirname, join, relative } from "node:path";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
2
3
|
import node_process, { cwd } from "node:process";
|
|
3
4
|
import { Buffer } from "node:buffer";
|
|
4
5
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
6
|
+
import { Resvg } from "@resvg/resvg-js";
|
|
5
7
|
import sharp from "sharp";
|
|
6
8
|
import node_os from "node:os";
|
|
7
9
|
import node_tty from "node:tty";
|
|
@@ -48,79 +50,45 @@ function createOgImageTypeHead() {
|
|
|
48
50
|
//#endregion
|
|
49
51
|
//#region ../core/src/og.ts
|
|
50
52
|
const templates = /* @__PURE__ */ new Map();
|
|
53
|
+
const baseImages = /* @__PURE__ */ new Map();
|
|
51
54
|
function escapeHtml(unsafe) {
|
|
52
55
|
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
53
56
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Wrap a title string into at most three lines with a best-effort “do not break words” strategy.
|
|
56
|
-
*
|
|
57
|
-
* The input is first split by explicit newlines into paragraphs (empty lines are ignored). Then each
|
|
58
|
-
* paragraph is wrapped into lines whose length does not exceed {@link maxSizePerLine} when possible:
|
|
59
|
-
*
|
|
60
|
-
* - If the paragraph contains whitespace, it is tokenized by whitespace and tokens are joined with a
|
|
61
|
-
* single space.
|
|
62
|
-
* - If the paragraph contains no whitespace (e.g. CJK text), it is tokenized by individual characters.
|
|
63
|
-
* - If a single token is longer than the line limit, it is hard-split into fixed-size chunks.
|
|
64
|
-
*
|
|
65
|
-
* Wrapping stops once three lines have been produced in total.
|
|
66
|
-
*
|
|
67
|
-
* @param input - The title text to wrap. `null`/`undefined`/empty (after trimming) results in `[]`.
|
|
68
|
-
* @param maxSizePerLine - Maximum allowed character count per line. Values `<= 0` are treated as `1`.
|
|
69
|
-
* @returns An array of wrapped lines, with a maximum length of 3.
|
|
70
|
-
*/
|
|
71
|
-
function wrapTitleToLines(input, maxSizePerLine) {
|
|
72
|
-
const text = (input ?? "").trim();
|
|
73
|
-
if (!text) return [];
|
|
74
|
-
const max = Math.max(1, maxSizePerLine || 1);
|
|
75
|
-
const paragraphs = text.split(/\r?\n+/).map((s) => s.trim()).filter(Boolean);
|
|
76
|
-
const out = [];
|
|
77
|
-
for (const para of paragraphs) {
|
|
78
|
-
if (out.length >= 3) break;
|
|
79
|
-
const tokens = /\s/.test(para) ? para.split(/\s+/).filter(Boolean) : [...para];
|
|
80
|
-
let line = "";
|
|
81
|
-
for (const token of tokens) {
|
|
82
|
-
if (out.length >= 3) break;
|
|
83
|
-
const next = line ? /\s/.test(para) ? `${line} ${token}` : `${line}${token}` : token;
|
|
84
|
-
if (next.length <= max) {
|
|
85
|
-
line = next;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (line) {
|
|
89
|
-
out.push(line);
|
|
90
|
-
line = "";
|
|
91
|
-
if (out.length >= 3) break;
|
|
92
|
-
}
|
|
93
|
-
if (token.length > max) {
|
|
94
|
-
const chunks = token.match(new RegExp(`.{1,${max}}`, "g")) ?? [];
|
|
95
|
-
for (const chunk of chunks) {
|
|
96
|
-
if (out.length >= 3) break;
|
|
97
|
-
if (chunk.length === max) out.push(chunk);
|
|
98
|
-
else line = chunk;
|
|
99
|
-
}
|
|
100
|
-
} else line = token;
|
|
101
|
-
}
|
|
102
|
-
if (out.length >= 3) break;
|
|
103
|
-
if (line) out.push(line);
|
|
104
|
-
}
|
|
105
|
-
return out.slice(0, 3);
|
|
106
|
-
}
|
|
107
57
|
async function generateOgImage({ title }, output, options) {
|
|
108
58
|
if (existsSync(output)) return;
|
|
109
59
|
if (!templates.has(options.ogTemplate)) templates.set(options.ogTemplate, readFileSync(options.ogTemplate, "utf-8"));
|
|
110
60
|
const ogTemplate = templates.get(options.ogTemplate);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return;
|
|
61
|
+
if (!baseImages.has(options.ogTemplate)) {
|
|
62
|
+
const baseSvg = ogTemplate.replace(/\{\{[^}]+\}\}/g, "").replace(/<text[^>]*>[\s\S]*?<\/text>/g, "");
|
|
63
|
+
const baseImageBuffer = await sharp(Buffer.from(baseSvg)).resize(1200, 630).png().toBuffer();
|
|
64
|
+
baseImages.set(options.ogTemplate, baseImageBuffer);
|
|
116
65
|
}
|
|
117
|
-
|
|
66
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
67
|
+
const lines = title.trim().split(new RegExp(`(.{0,${options.maxTitleSizePerLine}})(?:\\s|$)`, "g")).filter(Boolean);
|
|
68
|
+
const textLayerBuffer = new Resvg(createTextLayerSvg(ogTemplate, {
|
|
118
69
|
line1: lines[0] ? escapeHtml(lines[0]) : "",
|
|
119
70
|
line2: lines[1] ? escapeHtml(lines[1]) : "",
|
|
120
71
|
line3: lines[2] ? escapeHtml(lines[2]) : ""
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
72
|
+
}), {
|
|
73
|
+
fitTo: {
|
|
74
|
+
mode: "width",
|
|
75
|
+
value: 1200
|
|
76
|
+
},
|
|
77
|
+
font: { loadSystemFonts: false },
|
|
78
|
+
shapeRendering: 0,
|
|
79
|
+
textRendering: 2,
|
|
80
|
+
imageRendering: 1,
|
|
81
|
+
dpi: 96,
|
|
82
|
+
...options.resvgOptions
|
|
83
|
+
}).render().asPng();
|
|
84
|
+
await sharp(baseImages.get(options.ogTemplate)).composite([{
|
|
85
|
+
input: textLayerBuffer,
|
|
86
|
+
blend: "over"
|
|
87
|
+
}]).png().toFile(output);
|
|
88
|
+
}
|
|
89
|
+
function createTextLayerSvg(template, data) {
|
|
90
|
+
const processedText = (template.match(/<text[^>]*>[\s\S]*?<\/text>/g) || []).map((element) => element.replace(/\{\{([^}]+)\}\}/g, (_, name) => data[name] || ""));
|
|
91
|
+
return `${template.match(/<svg[^>]*>/)?.[0] || "<svg>"}${processedText.join("")}</svg>`;
|
|
124
92
|
}
|
|
125
93
|
|
|
126
94
|
//#endregion
|
|
@@ -377,15 +345,16 @@ let src_logger = createLogger();
|
|
|
377
345
|
|
|
378
346
|
//#endregion
|
|
379
347
|
//#region src/options.ts
|
|
380
|
-
function resolveOptions(userOptions) {
|
|
381
|
-
|
|
348
|
+
async function resolveOptions(userOptions) {
|
|
349
|
+
const options = {
|
|
382
350
|
domain: "",
|
|
383
351
|
outDir: "og",
|
|
384
352
|
ogTemplate: "og-template.svg",
|
|
385
353
|
maxTitleSizePerLine: 30,
|
|
386
|
-
sharpOptions: {},
|
|
387
354
|
...userOptions
|
|
388
355
|
};
|
|
356
|
+
if (typeof options.resvgOptions === "function") options.resvgOptions = await options.resvgOptions();
|
|
357
|
+
return options;
|
|
389
358
|
}
|
|
390
359
|
|
|
391
360
|
//#endregion
|
|
@@ -393,7 +362,7 @@ function resolveOptions(userOptions) {
|
|
|
393
362
|
const NAME = "rspress-plugin-og";
|
|
394
363
|
const LOG_PREFIX = `[${NAME}]`;
|
|
395
364
|
function src_default(userOptions) {
|
|
396
|
-
|
|
365
|
+
let options;
|
|
397
366
|
const images = /* @__PURE__ */ new Map();
|
|
398
367
|
const headCreators = [
|
|
399
368
|
(url) => createTwitterImageHead(url),
|
|
@@ -405,7 +374,8 @@ function src_default(userOptions) {
|
|
|
405
374
|
];
|
|
406
375
|
return {
|
|
407
376
|
name: NAME,
|
|
408
|
-
config(config) {
|
|
377
|
+
async config(config) {
|
|
378
|
+
options = await resolveOptions(userOptions);
|
|
409
379
|
config.head = [...config.head || [], ...headCreators.map((creator) => (route) => {
|
|
410
380
|
const imageInfo = images.get(route.routePath);
|
|
411
381
|
if (!imageInfo) return;
|
|
@@ -428,10 +398,12 @@ function src_default(userOptions) {
|
|
|
428
398
|
async afterBuild(config) {
|
|
429
399
|
const outputFolder = join(cwd(), config.outDir ?? "doc_build", options.outDir);
|
|
430
400
|
src_logger.info(`${LOG_PREFIX} Generating OG images to ${relative(cwd(), outputFolder)} ...`);
|
|
401
|
+
const start = performance.now();
|
|
431
402
|
await Promise.all(Array.from(images.entries()).map(([_, { title, imageName }]) => {
|
|
432
403
|
return generateOgImage({ title }, join(outputFolder, imageName), options);
|
|
433
404
|
}));
|
|
434
|
-
|
|
405
|
+
const duration = (performance.now() - start) / 1e3;
|
|
406
|
+
src_logger.success(`${LOG_PREFIX} ${images.size} OG images generated in ${duration.toFixed(2)}s.`);
|
|
435
407
|
}
|
|
436
408
|
};
|
|
437
409
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/types.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* resvg render options
|
|
7
|
+
* @see https://github.com/yisibl/resvg-js
|
|
8
|
+
*/
|
|
9
|
+
interface ResvgRenderOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Custom font data
|
|
12
|
+
*/
|
|
13
|
+
font?: {
|
|
14
|
+
fontBuffers?: Buffer[];
|
|
15
|
+
loadSystemFonts?: boolean;
|
|
16
|
+
defaultFontFamily?: string;
|
|
17
|
+
defaultFontSize?: number;
|
|
18
|
+
serifFamily?: string;
|
|
19
|
+
sansSerifFamily?: string;
|
|
20
|
+
cursiveFamily?: string;
|
|
21
|
+
fantasyFamily?: string;
|
|
22
|
+
monospaceFamily?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* DPI for rendering
|
|
26
|
+
* @default 96
|
|
27
|
+
*/
|
|
28
|
+
dpi?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Shape rendering mode
|
|
31
|
+
*/
|
|
32
|
+
shapeRendering?: 0 | 1 | 2;
|
|
33
|
+
/**
|
|
34
|
+
* Text rendering mode
|
|
35
|
+
*/
|
|
36
|
+
textRendering?: 0 | 1 | 2;
|
|
37
|
+
/**
|
|
38
|
+
* Image rendering mode
|
|
39
|
+
*/
|
|
40
|
+
imageRendering?: 0 | 1;
|
|
41
|
+
/**
|
|
42
|
+
* Fit to dimensions
|
|
43
|
+
*/
|
|
44
|
+
fitTo?: {
|
|
45
|
+
mode: 'original' | 'width' | 'height' | 'zoom';
|
|
46
|
+
value?: number;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Background color
|
|
50
|
+
*/
|
|
51
|
+
background?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Log level for resvg
|
|
54
|
+
*/
|
|
55
|
+
logLevel?: 'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
|
|
56
|
+
}
|
|
57
|
+
interface Options {
|
|
58
|
+
/**
|
|
59
|
+
* The domain to use for the generated OG image URLs.
|
|
60
|
+
*/
|
|
61
|
+
domain: string;
|
|
62
|
+
/**
|
|
63
|
+
* Output directory for the generated OG images.
|
|
64
|
+
*
|
|
65
|
+
* @default 'og'
|
|
66
|
+
*/
|
|
67
|
+
outDir?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Maximum number of characters per line in the title.
|
|
70
|
+
*
|
|
71
|
+
* @default 30
|
|
72
|
+
*/
|
|
73
|
+
maxTitleSizePerLine?: number;
|
|
74
|
+
/**
|
|
75
|
+
* The path to the OG image template file.
|
|
76
|
+
*
|
|
77
|
+
* @default '.vitepress/og-template.svg' for VitePress
|
|
78
|
+
* @default 'og-template.svg' for Rspress
|
|
79
|
+
*/
|
|
80
|
+
ogTemplate?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Options for resvg rendering
|
|
83
|
+
*/
|
|
84
|
+
resvgOptions?: ResvgRenderOptions | (() => Promise<ResvgRenderOptions> | ResvgRenderOptions);
|
|
85
|
+
}
|
|
86
|
+
interface ResolvedOptions extends Required<Omit<Options, 'resvgOptions'>> {
|
|
87
|
+
resvgOptions?: ResvgRenderOptions;
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/types.d.ts
|
|
91
|
+
interface Options$1 extends Options {}
|
|
92
|
+
interface ResolvedOptions$1 extends ResolvedOptions {}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { ResolvedOptions$1 as n, Options$1 as t };
|
package/dist/types.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as ResolvedOptions, t as Options } from "./types-
|
|
1
|
+
import { n as ResolvedOptions, t as Options } from "./types-7FdncbbI.mjs";
|
|
2
2
|
export { Options, ResolvedOptions };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soonit/rspress-plugin-og",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"description": "Automatically generate Open Graph images for your Rspress pages.",
|
|
6
6
|
"author": "Estéban Soubiran <esteban@soubiran.dev>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "https://github.com/Barbapapazes/vitepress-plugin-og"
|
|
13
13
|
},
|
|
14
|
-
"publishConfig": {
|
|
15
|
-
"access": "public"
|
|
16
|
-
},
|
|
17
14
|
"bugs": "https://github.com/Barbapapazes/vitepress-plugin-og/issues",
|
|
18
15
|
"exports": {
|
|
19
16
|
".": {
|
|
@@ -38,6 +35,7 @@
|
|
|
38
35
|
"@rspress/core": "^2.0.0-rc.1 || ^2.0.0"
|
|
39
36
|
},
|
|
40
37
|
"dependencies": {
|
|
38
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
41
39
|
"sharp": "^0.34.5",
|
|
42
40
|
"ufo": "^1.6.1"
|
|
43
41
|
},
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import sharp from "sharp";
|
|
2
|
-
|
|
3
|
-
//#region ../core/src/types.d.ts
|
|
4
|
-
interface Options {
|
|
5
|
-
/**
|
|
6
|
-
* The domain to use for the generated OG image URLs.
|
|
7
|
-
*/
|
|
8
|
-
domain: string;
|
|
9
|
-
/**
|
|
10
|
-
* Output directory for the generated OG images.
|
|
11
|
-
*
|
|
12
|
-
* @default 'og'
|
|
13
|
-
*/
|
|
14
|
-
outDir?: string;
|
|
15
|
-
/**
|
|
16
|
-
* Maximum number of characters per line in the title.
|
|
17
|
-
*
|
|
18
|
-
* @default 30
|
|
19
|
-
*/
|
|
20
|
-
maxTitleSizePerLine?: number;
|
|
21
|
-
/**
|
|
22
|
-
* The path to the OG image template file.
|
|
23
|
-
*
|
|
24
|
-
* @default '.vitepress/og-template.svg' for VitePress
|
|
25
|
-
* @default 'og-template.svg' for Rspress
|
|
26
|
-
*/
|
|
27
|
-
ogTemplate?: string;
|
|
28
|
-
/**
|
|
29
|
-
* Options to pass to the `sharp` image processing library.
|
|
30
|
-
*
|
|
31
|
-
* @default {}
|
|
32
|
-
*/
|
|
33
|
-
sharpOptions?: sharp.SharpOptions;
|
|
34
|
-
}
|
|
35
|
-
interface ResolvedOptions extends Required<Options> {}
|
|
36
|
-
//#endregion
|
|
37
|
-
//#region src/types.d.ts
|
|
38
|
-
interface Options$1 extends Options {}
|
|
39
|
-
interface ResolvedOptions$1 extends ResolvedOptions {}
|
|
40
|
-
//#endregion
|
|
41
|
-
export { ResolvedOptions$1 as n, Options$1 as t };
|