@bbki.ng/site 5.5.14 → 5.5.15

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @bbki.ng/site
2
2
 
3
+ ## 5.5.15
4
+
5
+ ### Patch Changes
6
+
7
+ - b377c66: add new plugin
8
+ - Updated dependencies [b377c66]
9
+ - @bbki.ng/ui@0.2.11
10
+
3
11
  ## 5.5.14
4
12
 
5
13
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/site",
3
- "version": "5.5.14",
3
+ "version": "5.5.15",
4
4
  "description": "code behind bbki.ng",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -14,7 +14,7 @@
14
14
  "react-dom": "^18.0.0",
15
15
  "react-router-dom": "6",
16
16
  "swr": "^2.2.5",
17
- "@bbki.ng/ui": "0.2.10"
17
+ "@bbki.ng/ui": "0.2.11"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@eslint/compat": "^1.0.0",
Binary file
package/src/blog/app.tsx CHANGED
@@ -13,8 +13,7 @@ import { BotRedirect } from '@/pages/bot';
13
13
  import { BBContext } from '@/context/bbcontext';
14
14
  import { useDynamicLogo } from './hooks/use_dynamic_logo';
15
15
  import { Slot } from '../core/components/SlotComp';
16
- import { pluginManager } from '#/core/pluginManager';
17
- import { useSticker } from './hooks/use_sticker';
16
+ import { usePlugins } from './hooks/use_plugins';
18
17
 
19
18
  const Layout = () => {
20
19
  const paths = usePaths();
@@ -60,7 +59,7 @@ const Layout = () => {
60
59
  };
61
60
 
62
61
  export const App = () => {
63
- useSticker();
62
+ usePlugins(['sticker', 'fontstyler']);
64
63
 
65
64
  return (
66
65
  <SWR>
@@ -0,0 +1,27 @@
1
+ import { useEffect } from 'react';
2
+ import { pluginManager } from '#/core/pluginManager';
3
+
4
+ /**
5
+ * 通用插件管理 hook
6
+ * 根据传入的插件ID列表加载和卸载插件
7
+ *
8
+ * @param pluginIds - 要加载的插件ID列表
9
+ *
10
+ * @example
11
+ * usePlugins(['sticker', 'fontstyler']);
12
+ */
13
+ export const usePlugins = (pluginIds: string[]) => {
14
+ useEffect(() => {
15
+ // 加载指定的插件
16
+ pluginIds.forEach(id => {
17
+ pluginManager.loadPlugin(id);
18
+ });
19
+
20
+ return () => {
21
+ // 卸载所有指定的插件
22
+ pluginIds.forEach(id => {
23
+ pluginManager.disablePlugin(id);
24
+ });
25
+ };
26
+ }, [pluginIds]);
27
+ };
@@ -1,8 +1,9 @@
1
1
  import useSWR from 'swr';
2
- import { useContext, useEffect } from 'react';
2
+ import { useContext, useEffect, useState } from 'react';
3
3
  import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
4
4
  import { baseFetcher } from '@/utils';
5
5
  import { API_ENDPOINT } from '@/constants/routes';
6
+ import { registry } from '#/core/registry';
6
7
 
7
8
  // In dev, use /api prefix to leverage Vite proxy to localhost:8787
8
9
  const isProd = true;
@@ -12,6 +13,13 @@ const POSTS_API = !isProd ? '/api/posts' : `${API_ENDPOINT}/posts`;
12
13
  // Use baseFetcher for full URLs, cfApiFetcher is for relative paths
13
14
  const postsFetcher = (resource: string) => baseFetcher(resource);
14
15
 
16
+ export interface TitleListItem {
17
+ name: string;
18
+ to: string;
19
+ children: string;
20
+ className?: string;
21
+ }
22
+
15
23
  export const usePosts = (name: string = '', suspense?: boolean) => {
16
24
  const { data: response, error } = useSWR(POSTS_API, postsFetcher, {
17
25
  revalidateOnFocus: false,
@@ -21,27 +29,42 @@ export const usePosts = (name: string = '', suspense?: boolean) => {
21
29
  // Extract posts array from API response { status: "success", data: [...] }
22
30
  const data = response?.data;
23
31
 
32
+ const [titleList, setTitleList] = useState<TitleListItem[]>([]);
24
33
  let isLoading = !data && !error;
25
34
  const { setIsLoading } = useContext(GlobalLoadingContext);
26
- const titleList =
27
- isLoading || error || !data
28
- ? []
29
- : [
30
- ...data.map((p: any) => ({
31
- name: p.title,
32
- to: p.title,
33
- children: p.title,
34
- })),
35
- {
36
- name: 'cd ~',
37
- to: '/',
38
- children: 'cd ~',
39
- },
40
- ];
41
35
 
42
36
  useEffect(() => {
43
37
  setIsLoading(isLoading);
44
- }, [isLoading]);
38
+ }, [isLoading, setIsLoading]);
39
+
40
+ // Apply middleware to transform titleList
41
+ useEffect(() => {
42
+ const transformTitleList = async () => {
43
+ if (isLoading || error || !data) {
44
+ setTitleList([]);
45
+ return;
46
+ }
47
+
48
+ const baseTitleList: TitleListItem[] = [
49
+ ...data.map((p: any) => ({
50
+ name: p.title,
51
+ to: p.title,
52
+ children: p.title,
53
+ })),
54
+ {
55
+ name: 'cd ~',
56
+ to: '/',
57
+ children: 'cd ~',
58
+ },
59
+ ];
60
+
61
+ // Run middleware to allow plugins to transform the title list
62
+ const transformedList = await registry.runMiddleware('transformTitleList', baseTitleList);
63
+ setTitleList(transformedList);
64
+ };
65
+
66
+ transformTitleList();
67
+ }, [data, isLoading, error]);
45
68
 
46
69
  const posts =
47
70
  isLoading || name == '' || error || !data ? data : data.find((p: any) => p.title == name);
@@ -0,0 +1,132 @@
1
+ import { IHostContext } from '#/types/hostApi';
2
+ import { IPlugin } from '#/types/plugin';
3
+
4
+ export interface TitleListItem {
5
+ name: string;
6
+ to: string;
7
+ children: string;
8
+ className?: string;
9
+ }
10
+
11
+ export interface FontConfig {
12
+ name: string;
13
+ src: string;
14
+ format: 'woff2' | 'woff' | 'ttf' | 'otf';
15
+ fontWeight?: string;
16
+ fontStyle?: string;
17
+ }
18
+
19
+ export interface FontRule {
20
+ match: string | RegExp;
21
+ fontFamily: string;
22
+ fontConfig?: FontConfig;
23
+ extraCls?: string;
24
+ }
25
+
26
+ class FontStylerPlugin implements IPlugin {
27
+ id = 'fontstyler';
28
+ name = 'Font Styler';
29
+ description = 'Apply custom fonts to specific post titles';
30
+ version = '1.0.0';
31
+ author = 'bbki.ng';
32
+
33
+ /**
34
+ * 配置:标题匹配规则 → 字体配置
35
+ * 支持精确字符串匹配或正则表达式匹配
36
+ *
37
+ * 示例:
38
+ * - 精确匹配: { match: '某篇文章标题', fontFamily: 'CustomFont1', fontConfig: {...} }
39
+ * - 正则匹配: { match: /^示例/, fontFamily: 'CustomFont2', fontConfig: {...} }
40
+ */
41
+ private fontRules: FontRule[] = [
42
+ {
43
+ match: '小乌鸦合集',
44
+ fontFamily: 'xwy',
45
+ fontConfig: {
46
+ name: 'xwy',
47
+ src: '/fonts/xwy.woff2',
48
+ format: 'woff2',
49
+ },
50
+ extraCls: 'text-2xl',
51
+ },
52
+ ];
53
+
54
+ private loadedFonts = new Set<string>();
55
+
56
+ onInstall = (ctx: IHostContext) => {
57
+ // 加载所有需要的字体文件
58
+ this.fontRules.forEach(rule => {
59
+ if (rule.fontConfig && !this.loadedFonts.has(rule.fontConfig.name)) {
60
+ this.loadFont(rule.fontConfig);
61
+ this.loadedFonts.add(rule.fontConfig.name);
62
+ }
63
+ });
64
+
65
+ ctx.api.registerMiddleware(
66
+ 'transformTitleList',
67
+ this.transformTitleList,
68
+ this.id,
69
+ 10 // weight
70
+ );
71
+ };
72
+
73
+ /**
74
+ * 动态加载字体文件
75
+ * 通过创建 style 标签添加 @font-face 规则
76
+ */
77
+ private loadFont = (config: FontConfig) => {
78
+ const styleId = `font-${config.name}`;
79
+ if (document.getElementById(styleId)) return;
80
+
81
+ const style = document.createElement('style');
82
+ style.id = styleId;
83
+ style.textContent = `
84
+ @font-face {
85
+ font-family: '${config.name}';
86
+ src: url('${config.src}') format('${config.format}');
87
+ font-weight: ${config.fontWeight || 'normal'};
88
+ font-style: ${config.fontStyle || 'normal'};
89
+ font-display: swap;
90
+ }
91
+ .font-${config.name} {
92
+ font-family: '${config.name}', monospace;
93
+ }
94
+ `;
95
+ document.head.appendChild(style);
96
+ };
97
+
98
+ /**
99
+ * 转换标题列表,为匹配的标题添加字体类名
100
+ */
101
+ private transformTitleList = (titleList: TitleListItem[]): TitleListItem[] => {
102
+ return titleList.map(item => {
103
+ for (const rule of this.fontRules) {
104
+ const match =
105
+ typeof rule.match === 'string' ? item.name === rule.match : rule.match.test(item.name);
106
+
107
+ if (match) {
108
+ return {
109
+ ...item,
110
+ className: `font-${rule.fontFamily} ${rule.extraCls || ''}`.trim(),
111
+ };
112
+ }
113
+ }
114
+ return item;
115
+ });
116
+ };
117
+
118
+ onDisable = async () => {
119
+ // 清理加载的字体样式
120
+ this.loadedFonts.forEach(fontName => {
121
+ const styleEl = document.getElementById(`font-${fontName}`);
122
+ if (styleEl) {
123
+ styleEl.remove();
124
+ }
125
+ });
126
+ this.loadedFonts.clear();
127
+ };
128
+
129
+ onDestroy = () => {};
130
+ }
131
+
132
+ export default new FontStylerPlugin();
@@ -11,4 +11,10 @@ export const PLUGIN_MANIFEST = [
11
11
  version: '0.1.0',
12
12
  description: 'A sticker plugin',
13
13
  },
14
+ {
15
+ name: 'fontstyler',
16
+ id: 'fontstyler',
17
+ version: '0.1.0',
18
+ description: 'Apply custom fonts to specific post',
19
+ },
14
20
  ];
@@ -1,2 +1,2 @@
1
1
  export type SlotName = 'leftCol' | 'rightCol' | 'articleActionRow';
2
- export type HookPoint = 'filterPosts' | 'transformPostContent';
2
+ export type HookPoint = 'filterPosts' | 'transformPostContent' | 'transformTitleList';