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