@fraqjs/plugin-takumi 0.2.1 → 0.3.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 CHANGED
@@ -1,12 +1,10 @@
1
1
  import { Context, Disposable } from "@fraqjs/fraq";
2
- import { ConstructRendererOptions, Font, RenderOptions, Renderer } from "@takumi-rs/core";
2
+ import { FontLoader, RenderOptions, Renderer } from "@takumi-rs/core";
3
3
  import { ReactElementLike } from "@takumi-rs/helpers";
4
4
  import { EmojiType } from "@takumi-rs/helpers/emoji";
5
5
  import { ReactNode } from "react";
6
-
7
6
  //#region src/service.d.ts
8
7
  interface TakumiServiceOptions {
9
- renderer?: ConstructRendererOptions;
10
8
  renderDefaults?: RenderOptions;
11
9
  onFontRegisterConflict?: 'error' | 'warn-and-ignore' | 'warn-and-replace';
12
10
  }
@@ -21,16 +19,19 @@ declare class TakumiService implements Disposable {
21
19
  private readonly ctx?;
22
20
  readonly renderer: Renderer;
23
21
  private readonly abortController;
22
+ private readonly imageFetchCache;
24
23
  private readonly registeredFontFamilies;
25
24
  private onFontRegisterConflict;
26
25
  constructor(options?: TakumiServiceOptions | undefined, ctx?: Context | undefined);
27
- registerFontFamily(family: string, fonts: (string | PathBasedFontDetails | Font)[], signal?: AbortSignal): Promise<void>;
28
- renderJsx(jsx: ReactNode | ReactElementLike, renderOptions?: RenderOptions, signal?: AbortSignal): Promise<Buffer>;
29
- renderJsxWithEmoji(jsx: ReactNode | ReactElementLike, renderOptions?: RenderOptions, signal?: AbortSignal, emojiType?: EmojiType): Promise<Buffer>;
30
- renderHtml(html: string, renderOptions?: RenderOptions, signal?: AbortSignal): Promise<Buffer>;
31
- renderHtmlWithEmoji(html: string, renderOptions?: RenderOptions, signal?: AbortSignal, emojiType?: EmojiType): Promise<Buffer>;
26
+ registerFontFamily(family: string, fonts: (string | PathBasedFontDetails | FontLoader)[], signal?: AbortSignal): Promise<void>;
27
+ renderJsx(jsx: ReactNode | ReactElementLike, renderOptions?: RenderOptions, signal?: AbortSignal, emojiType?: EmojiType): Promise<Buffer>;
28
+ renderHtml(html: string, renderOptions?: RenderOptions, signal?: AbortSignal, emojiType?: EmojiType): Promise<Buffer>;
29
+ private renderNode;
32
30
  private mergeRenderOptions;
33
- private processEmoji;
31
+ private mergeFonts;
32
+ private mergeImages;
33
+ private toImagesInput;
34
+ private toFontLoader;
34
35
  dispose(): void;
35
36
  }
36
37
  //#endregion
package/dist/index.mjs CHANGED
@@ -1,26 +1,42 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { definePlugin } from "@fraqjs/fraq";
3
- import { Renderer, extractResourceUrls } from "@takumi-rs/core";
4
- import { fetchResources } from "@takumi-rs/helpers";
3
+ import { Renderer } from "@takumi-rs/core";
4
+ import { fontFromUrl, prepareImages } from "@takumi-rs/helpers";
5
5
  import { extractEmojis } from "@takumi-rs/helpers/emoji";
6
6
  import { fromHtml } from "@takumi-rs/helpers/html";
7
7
  import { fromJsx } from "@takumi-rs/helpers/jsx";
8
8
  import fs from "node:fs/promises";
9
9
  //#region src/service.ts
10
10
  function combineAbortSignals(...signals) {
11
- return AbortSignal.any(signals.filter((signal) => signal !== void 0));
11
+ const filteredSignals = signals.filter((signal) => signal !== void 0);
12
+ if (filteredSignals.length === 0) return;
13
+ if (filteredSignals.length === 1) return filteredSignals[0];
14
+ return AbortSignal.any(filteredSignals);
15
+ }
16
+ function isRemoteUrl(value) {
17
+ return value.startsWith("https://") || value.startsWith("http://");
18
+ }
19
+ function getImageSources(images) {
20
+ if (!images) return [];
21
+ if (Array.isArray(images)) return images;
22
+ return images.sources ?? [];
23
+ }
24
+ function getImageCache(images) {
25
+ if (!images || Array.isArray(images)) return;
26
+ return images.cache;
12
27
  }
13
28
  var TakumiService = class {
14
29
  options;
15
30
  ctx;
16
31
  renderer;
17
32
  abortController = new AbortController();
18
- registeredFontFamilies = /* @__PURE__ */ new Set();
33
+ imageFetchCache = /* @__PURE__ */ new Map();
34
+ registeredFontFamilies = /* @__PURE__ */ new Map();
19
35
  onFontRegisterConflict;
20
36
  constructor(options, ctx) {
21
37
  this.options = options;
22
38
  this.ctx = ctx;
23
- this.renderer = new Renderer(options?.renderer);
39
+ this.renderer = new Renderer();
24
40
  this.onFontRegisterConflict = options?.onFontRegisterConflict ?? "warn-and-ignore";
25
41
  }
26
42
  async registerFontFamily(family, fonts, signal) {
@@ -34,56 +50,50 @@ var TakumiService = class {
34
50
  } else if (this.onFontRegisterConflict === "warn-and-replace") if (this.ctx) this.ctx.logger.warn(`${message} Replacing previous registration.`);
35
51
  else console.warn(`${message} Replacing previous registration.`);
36
52
  }
37
- await this.renderer.loadFonts(await Promise.all(fonts.map(async (font) => {
38
- if (typeof font === "string") return {
39
- name: family,
40
- data: await fs.readFile(font)
41
- };
42
- else if ("path" in font) {
43
- const { path, name = family, weight, style } = font;
44
- return {
45
- name,
46
- data: await fs.readFile(path),
47
- weight,
48
- style
49
- };
50
- } else return font;
51
- })), combineAbortSignals(signal, this.abortController.signal));
52
- this.registeredFontFamilies.add(family);
53
+ signal?.throwIfAborted();
54
+ this.registeredFontFamilies.set(family, fonts.map((font) => this.toFontLoader(family, font)));
53
55
  }
54
- async renderJsx(jsx, renderOptions, signal) {
56
+ async renderJsx(jsx, renderOptions, signal, emojiType) {
55
57
  const { node, stylesheets } = await fromJsx(jsx);
56
- return this.renderer.render(node, this.mergeRenderOptions({
58
+ return await this.renderNode({
59
+ node,
57
60
  stylesheets,
58
- userOptions: renderOptions
59
- }), combineAbortSignals(signal, this.abortController.signal));
61
+ renderOptions,
62
+ signal,
63
+ emojiType
64
+ });
60
65
  }
61
- async renderJsxWithEmoji(jsx, renderOptions, signal, emojiType = "noto") {
62
- const { node, stylesheets } = await fromJsx(jsx);
63
- const { node: processedNode, fetchedResources } = await this.processEmoji(node, emojiType);
64
- return this.renderer.render(processedNode, this.mergeRenderOptions({
65
- stylesheets,
66
- fetchedResources,
67
- userOptions: renderOptions
68
- }), combineAbortSignals(signal, this.abortController.signal));
69
- }
70
- async renderHtml(html, renderOptions, signal) {
66
+ async renderHtml(html, renderOptions, signal, emojiType) {
71
67
  const { node, stylesheets } = fromHtml(html);
72
- return this.renderer.render(node, this.mergeRenderOptions({
68
+ return await this.renderNode({
69
+ node,
73
70
  stylesheets,
74
- userOptions: renderOptions
75
- }), combineAbortSignals(signal, this.abortController.signal));
71
+ renderOptions,
72
+ signal,
73
+ emojiType
74
+ });
76
75
  }
77
- async renderHtmlWithEmoji(html, renderOptions, signal, emojiType = "noto") {
78
- const { node, stylesheets } = fromHtml(html);
79
- const { node: processedNode, fetchedResources } = await this.processEmoji(node, emojiType);
80
- return this.renderer.render(processedNode, this.mergeRenderOptions({
81
- stylesheets,
82
- fetchedResources,
83
- userOptions: renderOptions
84
- }), combineAbortSignals(signal, this.abortController.signal));
76
+ async renderNode(components) {
77
+ const node = components.emojiType === void 0 ? components.node : extractEmojis(components.node, components.emojiType);
78
+ return await this.renderer.render(node, await this.mergeRenderOptions({
79
+ node,
80
+ stylesheets: components.stylesheets,
81
+ userOptions: components.renderOptions,
82
+ signal: components.signal,
83
+ images: components.emojiType !== void 0
84
+ }));
85
85
  }
86
- mergeRenderOptions(components) {
86
+ async mergeRenderOptions(components) {
87
+ const renderDefaults = this.options?.renderDefaults;
88
+ const signal = combineAbortSignals(components.signal, components.userOptions?.signal, this.abortController.signal);
89
+ const images = await this.mergeImages({
90
+ node: components.node,
91
+ prepareImages: components.images ?? false,
92
+ signal,
93
+ defaultImages: renderDefaults?.images,
94
+ userImages: components.userOptions?.images
95
+ });
96
+ const fonts = this.mergeFonts(renderDefaults?.fonts, components.userOptions?.fonts);
87
97
  return {
88
98
  ...this.options?.renderDefaults,
89
99
  ...components.userOptions,
@@ -92,20 +102,63 @@ var TakumiService = class {
92
102
  ...components.stylesheets ?? [],
93
103
  ...components.userOptions?.stylesheets ?? []
94
104
  ],
95
- fetchedResources: [
96
- ...this.options?.renderDefaults?.fetchedResources ?? [],
97
- ...components.fetchedResources ?? [],
98
- ...components.userOptions?.fetchedResources ?? []
99
- ]
105
+ ...fonts ? { fonts } : {},
106
+ ...images ? { images } : {},
107
+ ...signal ? { signal } : {}
100
108
  };
101
109
  }
102
- async processEmoji(node, emojiType = "twemoji") {
103
- const processedNode = extractEmojis(node, emojiType);
104
- return {
105
- node: processedNode,
106
- fetchedResources: await fetchResources(extractResourceUrls(processedNode), { throwOnError: false })
110
+ mergeFonts(defaultFonts, userFonts) {
111
+ const fonts = [...this.registeredFontFamilies.values()].flat();
112
+ if (defaultFonts) fonts.push(...defaultFonts);
113
+ if (userFonts) fonts.push(...userFonts);
114
+ return fonts.length > 0 ? fonts : void 0;
115
+ }
116
+ async mergeImages(components) {
117
+ const sources = [...getImageSources(components.defaultImages), ...getImageSources(components.userImages)];
118
+ const cache = getImageCache(components.userImages) ?? getImageCache(components.defaultImages);
119
+ if (components.prepareImages) {
120
+ const preparedSources = await prepareImages({
121
+ node: components.node,
122
+ sources,
123
+ fetchCache: this.imageFetchCache,
124
+ signal: components.signal,
125
+ throwOnError: false
126
+ });
127
+ return this.toImagesInput(preparedSources, cache);
128
+ }
129
+ return this.toImagesInput(sources, cache);
130
+ }
131
+ toImagesInput(sources, cache) {
132
+ if (sources.length === 0) return cache === void 0 ? void 0 : { cache };
133
+ return cache === void 0 ? sources : {
134
+ sources,
135
+ cache
107
136
  };
108
137
  }
138
+ toFontLoader(family, font) {
139
+ if (typeof font === "string") {
140
+ if (isRemoteUrl(font)) return {
141
+ ...fontFromUrl(font),
142
+ name: family
143
+ };
144
+ return {
145
+ name: family,
146
+ key: `${family}:${font}`,
147
+ data: () => fs.readFile(font)
148
+ };
149
+ }
150
+ if ("path" in font) {
151
+ const { path, name = family, weight, style } = font;
152
+ return {
153
+ name,
154
+ key: `${name}:${path}:${weight ?? ""}:${style ?? ""}`,
155
+ weight,
156
+ style,
157
+ data: () => fs.readFile(path)
158
+ };
159
+ }
160
+ return font;
161
+ }
109
162
  dispose() {
110
163
  this.abortController.abort();
111
164
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fraqjs/plugin-takumi",
3
3
  "type": "module",
4
- "version": "0.2.1",
4
+ "version": "0.3.0",
5
5
  "description": "Takumi integration plugin for Fraq",
6
6
  "files": [
7
7
  "dist",
@@ -19,12 +19,12 @@
19
19
  "license": "MIT",
20
20
  "peerDependencies": {
21
21
  "react": "^19.0.0",
22
- "@fraqjs/fraq": "^0.11.0"
22
+ "@fraqjs/fraq": "^0.13.0"
23
23
  },
24
24
  "dependencies": {
25
25
  "@fraqjs/takumi-builtin-fonts": "^1.0.0",
26
- "@takumi-rs/core": "^1.7.0",
27
- "@takumi-rs/helpers": "^1.7.0"
26
+ "@takumi-rs/core": "^2.0.1",
27
+ "@takumi-rs/helpers": "^2.0.1"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/react": "^19.2.15",