@soonit/rspress-plugin-og 0.0.2 → 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,13 +1,7 @@
1
- import { t as Options } from "./types-ChngGzQc.mjs";
2
- import * as _rspress_core0 from "@rspress/core";
3
- import { PageIndexInfo } from "@rspress/core";
1
+ import { t as Options } from "./types-d5VtTQ1o.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,4 +1,5 @@
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";
@@ -48,79 +49,35 @@ function createOgImageTypeHead() {
48
49
  //#endregion
49
50
  //#region ../core/src/og.ts
50
51
  const templates = /* @__PURE__ */ new Map();
52
+ const baseImages = /* @__PURE__ */ new Map();
51
53
  function escapeHtml(unsafe) {
52
54
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
53
55
  }
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
56
  async function generateOgImage({ title }, output, options) {
108
57
  if (existsSync(output)) return;
109
58
  if (!templates.has(options.ogTemplate)) templates.set(options.ogTemplate, readFileSync(options.ogTemplate, "utf-8"));
110
59
  const ogTemplate = templates.get(options.ogTemplate);
111
- mkdirSync(dirname(output), { recursive: true });
112
- const lines = wrapTitleToLines(title, options.maxTitleSizePerLine);
113
- if (lines.length === 0) {
114
- console.warn("[vitepress-plugin-og] Missing page title, skip og image generation.");
115
- 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);
116
64
  }
117
- 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, {
118
68
  line1: lines[0] ? escapeHtml(lines[0]) : "",
119
69
  line2: lines[1] ? escapeHtml(lines[1]) : "",
120
70
  line3: lines[2] ? escapeHtml(lines[2]) : ""
121
- };
122
- const svg = ogTemplate.replace(/\{\{([^}]+)\}\}/g, (_, name) => data[name] || "");
123
- await sharp(Buffer.from(svg), options.sharpOptions).resize(1200, 630).png().toFile(output);
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>`;
124
81
  }
125
82
 
126
83
  //#endregion
@@ -383,7 +340,6 @@ function resolveOptions(userOptions) {
383
340
  outDir: "og",
384
341
  ogTemplate: "og-template.svg",
385
342
  maxTitleSizePerLine: 30,
386
- sharpOptions: {},
387
343
  ...userOptions
388
344
  };
389
345
  }
@@ -393,7 +349,7 @@ function resolveOptions(userOptions) {
393
349
  const NAME = "rspress-plugin-og";
394
350
  const LOG_PREFIX = `[${NAME}]`;
395
351
  function src_default(userOptions) {
396
- const options = resolveOptions(userOptions);
352
+ let options;
397
353
  const images = /* @__PURE__ */ new Map();
398
354
  const headCreators = [
399
355
  (url) => createTwitterImageHead(url),
@@ -405,7 +361,8 @@ function src_default(userOptions) {
405
361
  ];
406
362
  return {
407
363
  name: NAME,
408
- config(config) {
364
+ async config(config) {
365
+ options = await resolveOptions(userOptions);
409
366
  config.head = [...config.head || [], ...headCreators.map((creator) => (route) => {
410
367
  const imageInfo = images.get(route.routePath);
411
368
  if (!imageInfo) return;
@@ -428,10 +385,12 @@ function src_default(userOptions) {
428
385
  async afterBuild(config) {
429
386
  const outputFolder = join(cwd(), config.outDir ?? "doc_build", options.outDir);
430
387
  src_logger.info(`${LOG_PREFIX} Generating OG images to ${relative(cwd(), outputFolder)} ...`);
388
+ const start = performance.now();
431
389
  await Promise.all(Array.from(images.entries()).map(([_, { title, imageName }]) => {
432
390
  return generateOgImage({ title }, join(outputFolder, imageName), options);
433
391
  }));
434
- src_logger.success(`${LOG_PREFIX} ${images.size} OG images generated.`);
392
+ const duration = (performance.now() - start) / 1e3;
393
+ src_logger.success(`${LOG_PREFIX} ${images.size} OG images generated in ${duration.toFixed(2)}s.`);
435
394
  }
436
395
  };
437
396
  }
@@ -1,5 +1,3 @@
1
- import sharp from "sharp";
2
-
3
1
  //#region ../core/src/types.d.ts
4
2
  interface Options {
5
3
  /**
@@ -25,12 +23,6 @@ interface Options {
25
23
  * @default 'og-template.svg' for Rspress
26
24
  */
27
25
  ogTemplate?: string;
28
- /**
29
- * Options to pass to the `sharp` image processing library.
30
- *
31
- * @default {}
32
- */
33
- sharpOptions?: sharp.SharpOptions;
34
26
  }
35
27
  interface ResolvedOptions extends Required<Options> {}
36
28
  //#endregion
package/dist/types.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { n as ResolvedOptions, t as Options } from "./types-ChngGzQc.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.2",
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",
@@ -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
  ".": {