@soonit/rspress-plugin-og 0.0.1 → 0.0.5

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-d5VtTQ1o.mjs";
2
- import * as _rspress_core0 from "@rspress/core";
3
- import { PageIndexInfo } from "@rspress/core";
1
+ import { t as Options } from "./types-R_GC9oNb.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,8 +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
- import { Buffer } from "node:buffer";
4
4
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
5
- import sharp from "sharp";
5
+ import { writeFile } from "node:fs/promises";
6
+ import { Resvg } from "@resvg/resvg-js";
6
7
  import node_os from "node:os";
7
8
  import node_tty from "node:tty";
8
9
  import { joinURL } from "ufo";
@@ -51,19 +52,81 @@ const templates = /* @__PURE__ */ new Map();
51
52
  function escapeHtml(unsafe) {
52
53
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
53
54
  }
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
+ }
54
108
  async function generateOgImage({ title }, output, options) {
55
109
  if (existsSync(output)) return;
56
110
  if (!templates.has(options.ogTemplate)) templates.set(options.ogTemplate, readFileSync(options.ogTemplate, "utf-8"));
57
111
  const ogTemplate = templates.get(options.ogTemplate);
58
112
  mkdirSync(dirname(output), { recursive: true });
59
- const lines = title.trim().split(new RegExp(`(.{0,${options.maxTitleSizePerLine}})(?:\\s|$)`, "g")).filter(Boolean);
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;
117
+ }
60
118
  const data = {
61
119
  line1: lines[0] ? escapeHtml(lines[0]) : "",
62
120
  line2: lines[1] ? escapeHtml(lines[1]) : "",
63
121
  line3: lines[2] ? escapeHtml(lines[2]) : ""
64
122
  };
65
- const svg = ogTemplate.replace(/\{\{([^}]+)\}\}/g, (_, name) => data[name] || "");
66
- await sharp(Buffer.from(svg)).resize(1200, 630).png().toFile(output);
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());
67
130
  }
68
131
 
69
132
  //#endregion
@@ -320,14 +383,17 @@ let src_logger = createLogger();
320
383
 
321
384
  //#endregion
322
385
  //#region src/options.ts
323
- function resolveOptions(userOptions) {
324
- return {
386
+ async function resolveOptions(userOptions) {
387
+ const options = {
325
388
  domain: "",
326
389
  outDir: "og",
327
390
  ogTemplate: "og-template.svg",
328
391
  maxTitleSizePerLine: 30,
392
+ resvgOptions: void 0,
329
393
  ...userOptions
330
394
  };
395
+ if (typeof options.resvgOptions === "function") options.resvgOptions = await options.resvgOptions();
396
+ return options;
331
397
  }
332
398
 
333
399
  //#endregion
@@ -335,7 +401,7 @@ function resolveOptions(userOptions) {
335
401
  const NAME = "rspress-plugin-og";
336
402
  const LOG_PREFIX = `[${NAME}]`;
337
403
  function src_default(userOptions) {
338
- const options = resolveOptions(userOptions);
404
+ let options;
339
405
  const images = /* @__PURE__ */ new Map();
340
406
  const headCreators = [
341
407
  (url) => createTwitterImageHead(url),
@@ -347,7 +413,8 @@ function src_default(userOptions) {
347
413
  ];
348
414
  return {
349
415
  name: NAME,
350
- config(config) {
416
+ async config(config) {
417
+ options = await resolveOptions(userOptions);
351
418
  config.head = [...config.head || [], ...headCreators.map((creator) => (route) => {
352
419
  const imageInfo = images.get(route.routePath);
353
420
  if (!imageInfo) return;
@@ -370,10 +437,12 @@ function src_default(userOptions) {
370
437
  async afterBuild(config) {
371
438
  const outputFolder = join(cwd(), config.outDir ?? "doc_build", options.outDir);
372
439
  src_logger.info(`${LOG_PREFIX} Generating OG images to ${relative(cwd(), outputFolder)} ...`);
440
+ const start = performance.now();
373
441
  await Promise.all(Array.from(images.entries()).map(([_, { title, imageName }]) => {
374
442
  return generateOgImage({ title }, join(outputFolder, imageName), options);
375
443
  }));
376
- src_logger.success(`${LOG_PREFIX} ${images.size} OG images generated.`);
444
+ const duration = (performance.now() - start) / 1e3;
445
+ src_logger.success(`${LOG_PREFIX} ${images.size} OG images generated in ${duration.toFixed(2)}s.`);
377
446
  }
378
447
  };
379
448
  }
@@ -1,3 +1,5 @@
1
+ import { ResvgRenderOptions } from "@resvg/resvg-js";
2
+
1
3
  //#region ../core/src/types.d.ts
2
4
  interface Options {
3
5
  /**
@@ -23,8 +25,14 @@ interface Options {
23
25
  * @default 'og-template.svg' for Rspress
24
26
  */
25
27
  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;
26
35
  }
27
- interface ResolvedOptions extends Required<Options> {}
28
36
  //#endregion
29
37
  //#region src/types.d.ts
30
38
  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-d5VtTQ1o.mjs";
1
+ import { n as ResolvedOptions, t as Options } from "./types-R_GC9oNb.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.1",
4
+ "version": "0.0.5",
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",
@@ -12,10 +12,6 @@
12
12
  "url": "https://github.com/Barbapapazes/vitepress-plugin-og"
13
13
  },
14
14
  "bugs": "https://github.com/Barbapapazes/vitepress-plugin-og/issues",
15
- "publishConfig": {
16
- "access": "public",
17
- "registry": "https://registry.npmjs.org/"
18
- },
19
15
  "exports": {
20
16
  ".": {
21
17
  "types": "./dist/index.d.mts",
@@ -39,7 +35,7 @@
39
35
  "@rspress/core": "^2.0.0-rc.1 || ^2.0.0"
40
36
  },
41
37
  "dependencies": {
42
- "sharp": "^0.34.5",
38
+ "@resvg/resvg-js": "^2.6.2",
43
39
  "ufo": "^1.6.1"
44
40
  },
45
41
  "devDependencies": {