@soonit/rspress-plugin-og 0.0.5 → 0.0.6

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 CHANGED
@@ -1,4 +1,4 @@
1
- import { t as Options } from "./types-R_GC9oNb.mjs";
1
+ import { t as Options } from "./types-d5VtTQ1o.mjs";
2
2
  import { RspressPlugin } from "@rspress/core";
3
3
 
4
4
  //#region src/index.d.ts
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import { dirname, join, relative } from "node:path";
2
2
  import { performance } from "node:perf_hooks";
3
3
  import node_process, { cwd } from "node:process";
4
+ import { Buffer } from "node:buffer";
4
5
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
5
- import { writeFile } from "node:fs/promises";
6
- import { Resvg } from "@resvg/resvg-js";
6
+ import sharp from "sharp";
7
7
  import node_os from "node:os";
8
8
  import node_tty from "node:tty";
9
9
  import { joinURL } from "ufo";
@@ -49,84 +49,35 @@ function createOgImageTypeHead() {
49
49
  //#endregion
50
50
  //#region ../core/src/og.ts
51
51
  const templates = /* @__PURE__ */ new Map();
52
+ const baseImages = /* @__PURE__ */ new Map();
52
53
  function escapeHtml(unsafe) {
53
54
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
54
55
  }
55
- /**
56
- * Wrap a title string into at most three lines with a best-effort “do not break words” strategy.
57
- *
58
- * The input is first split by explicit newlines into paragraphs (empty lines are ignored). Then each
59
- * paragraph is wrapped into lines whose length does not exceed {@link maxSizePerLine} when possible:
60
- *
61
- * - If the paragraph contains whitespace, it is tokenized by whitespace and tokens are joined with a
62
- * single space.
63
- * - If the paragraph contains no whitespace (e.g. CJK text), it is tokenized by individual characters.
64
- * - If a single token is longer than the line limit, it is hard-split into fixed-size chunks.
65
- *
66
- * Wrapping stops once three lines have been produced in total.
67
- *
68
- * @param input - The title text to wrap. `null`/`undefined`/empty (after trimming) results in `[]`.
69
- * @param maxSizePerLine - Maximum allowed character count per line. Values `<= 0` are treated as `1`.
70
- * @returns An array of wrapped lines, with a maximum length of 3.
71
- */
72
- function wrapTitleToLines(input, maxSizePerLine) {
73
- const text = (input ?? "").trim();
74
- if (!text) return [];
75
- const max = Math.max(1, maxSizePerLine || 1);
76
- const paragraphs = text.split(/\r?\n+/).map((s) => s.trim()).filter(Boolean);
77
- const out = [];
78
- for (const para of paragraphs) {
79
- if (out.length >= 3) break;
80
- const tokens = /\s/.test(para) ? para.split(/\s+/).filter(Boolean) : [...para];
81
- let line = "";
82
- for (const token of tokens) {
83
- if (out.length >= 3) break;
84
- const next = line ? /\s/.test(para) ? `${line} ${token}` : `${line}${token}` : token;
85
- if (next.length <= max) {
86
- line = next;
87
- continue;
88
- }
89
- if (line) {
90
- out.push(line);
91
- line = "";
92
- if (out.length >= 3) break;
93
- }
94
- if (token.length > max) {
95
- const chunks = token.match(new RegExp(`.{1,${max}}`, "g")) ?? [];
96
- for (const chunk of chunks) {
97
- if (out.length >= 3) break;
98
- if (chunk.length === max) out.push(chunk);
99
- else line = chunk;
100
- }
101
- } else line = token;
102
- }
103
- if (out.length >= 3) break;
104
- if (line) out.push(line);
105
- }
106
- return out.slice(0, 3);
107
- }
108
56
  async function generateOgImage({ title }, output, options) {
109
57
  if (existsSync(output)) return;
110
58
  if (!templates.has(options.ogTemplate)) templates.set(options.ogTemplate, readFileSync(options.ogTemplate, "utf-8"));
111
59
  const ogTemplate = templates.get(options.ogTemplate);
112
- mkdirSync(dirname(output), { recursive: true });
113
- const lines = wrapTitleToLines(title, options.maxTitleSizePerLine);
114
- if (lines.length === 0) {
115
- console.warn("[vitepress-plugin-og] Missing page title, skip og image generation.");
116
- return;
60
+ if (!baseImages.has(options.ogTemplate)) {
61
+ const baseSvg = ogTemplate.replace(/\{\{([^}]+)\}\}/g, "");
62
+ const baseImageBuffer = sharp(Buffer.from(baseSvg)).resize(1200, 630).png().toBuffer();
63
+ baseImages.set(options.ogTemplate, baseImageBuffer);
117
64
  }
118
- const data = {
65
+ mkdirSync(dirname(output), { recursive: true });
66
+ const lines = title.trim().split(new RegExp(`(.{0,${options.maxTitleSizePerLine}})(?:\\s|$)`, "g")).filter(Boolean);
67
+ const textOnlySvg = createTextLayerSvg(ogTemplate, {
119
68
  line1: lines[0] ? escapeHtml(lines[0]) : "",
120
69
  line2: lines[1] ? escapeHtml(lines[1]) : "",
121
70
  line3: lines[2] ? escapeHtml(lines[2]) : ""
122
- };
123
- await writeFile(output, new Resvg(ogTemplate.replace(/\{\{([^}]+)\}\}/g, (_, name) => data[name] || ""), {
124
- fitTo: {
125
- mode: "width",
126
- value: 1200
127
- },
128
- ...options.resvgOptions
129
- }).render().asPng());
71
+ });
72
+ const textLayerBuffer = await sharp(Buffer.from(textOnlySvg)).resize(1200, 630).png().toBuffer();
73
+ await sharp(await baseImages.get(options.ogTemplate)).composite([{
74
+ input: textLayerBuffer,
75
+ blend: "over"
76
+ }]).png().toFile(output);
77
+ }
78
+ function createTextLayerSvg(template, data) {
79
+ const processedText = (template.match(/<text[^>]*>[\s\S]*?<\/text>/g) || []).filter((element) => /\{\{[^}]+\}\}/.test(element)).map((element) => element.replace(/\{\{([^}]+)\}\}/g, (_, name) => data[name] || ""));
80
+ return `${template.match(/<svg[^>]*>/)?.[0] || "<svg>"}${processedText.join("")}</svg>`;
130
81
  }
131
82
 
132
83
  //#endregion
@@ -383,17 +334,14 @@ let src_logger = createLogger();
383
334
 
384
335
  //#endregion
385
336
  //#region src/options.ts
386
- async function resolveOptions(userOptions) {
387
- const options = {
337
+ function resolveOptions(userOptions) {
338
+ return {
388
339
  domain: "",
389
340
  outDir: "og",
390
341
  ogTemplate: "og-template.svg",
391
342
  maxTitleSizePerLine: 30,
392
- resvgOptions: void 0,
393
343
  ...userOptions
394
344
  };
395
- if (typeof options.resvgOptions === "function") options.resvgOptions = await options.resvgOptions();
396
- return options;
397
345
  }
398
346
 
399
347
  //#endregion
@@ -1,5 +1,3 @@
1
- import { ResvgRenderOptions } from "@resvg/resvg-js";
2
-
3
1
  //#region ../core/src/types.d.ts
4
2
  interface Options {
5
3
  /**
@@ -25,14 +23,8 @@ interface Options {
25
23
  * @default 'og-template.svg' for Rspress
26
24
  */
27
25
  ogTemplate?: string;
28
- /**
29
- * Options for resvg rendering
30
- */
31
- resvgOptions?: ResvgRenderOptions | (() => Promise<ResvgRenderOptions>);
32
- }
33
- interface ResolvedOptions extends Required<Omit<Options, 'resvgOptions'>> {
34
- resvgOptions?: ResvgRenderOptions;
35
26
  }
27
+ interface ResolvedOptions extends Required<Options> {}
36
28
  //#endregion
37
29
  //#region src/types.d.ts
38
30
  interface Options$1 extends Options {}
package/dist/types.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { n as ResolvedOptions, t as Options } from "./types-R_GC9oNb.mjs";
1
+ import { n as ResolvedOptions, t as Options } from "./types-d5VtTQ1o.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.5",
4
+ "version": "0.0.6",
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",
@@ -35,7 +35,7 @@
35
35
  "@rspress/core": "^2.0.0-rc.1 || ^2.0.0"
36
36
  },
37
37
  "dependencies": {
38
- "@resvg/resvg-js": "^2.6.2",
38
+ "sharp": "^0.34.5",
39
39
  "ufo": "^1.6.1"
40
40
  },
41
41
  "devDependencies": {