@blocklet/editor 2.4.75 → 2.4.76

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.
@@ -1,6 +1,8 @@
1
+ import { type EditorThemeClasses } from 'lexical';
1
2
  interface CustomComponentProps {
2
3
  component: string;
3
4
  properties?: any;
5
+ editorTheme?: EditorThemeClasses;
4
6
  }
5
- export declare function CustomComponent({ component, properties }: CustomComponentProps): import("react/jsx-runtime").JSX.Element | null;
7
+ export declare function CustomComponent({ component, properties, editorTheme }: CustomComponentProps): import("react/jsx-runtime").JSX.Element | null;
6
8
  export {};
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box } from '@mui/material';
3
3
  import { ErrorBoundary } from 'react-error-boundary';
4
4
  import components from './components';
5
- export function CustomComponent({ component, properties }) {
5
+ export function CustomComponent({ component, properties, editorTheme }) {
6
6
  const Component = components[component];
7
7
  if (!Component) {
8
8
  return null;
@@ -11,5 +11,5 @@ export function CustomComponent({ component, properties }) {
11
11
  console.error('CustomComponent error', component, properties, error);
12
12
  return null;
13
13
  };
14
- return (_jsx(Box, { sx: { py: 1 }, children: _jsx(ErrorBoundary, { fallbackRender: fallbackRender, children: _jsx(Component, { ...properties }) }) }));
14
+ return (_jsx(Box, { sx: { py: 1 }, children: _jsx(ErrorBoundary, { fallbackRender: fallbackRender, children: _jsx(Component, { ...properties, editorTheme: editorTheme }) }) }));
15
15
  }
@@ -65,6 +65,7 @@ export class CustomComponentNode extends DecoratorBlockNode {
65
65
  }
66
66
  static importDOM() {
67
67
  return {
68
+ 'x-code': () => ({ conversion: convertCustomComponentElement, priority: 1 }),
68
69
  'x-card': () => ({ conversion: convertCustomComponentElement, priority: 1 }),
69
70
  'x-cards': () => ({ conversion: convertCustomComponentElement, priority: 1 }),
70
71
  'x-code-group': () => ({ conversion: convertCustomComponentElement, priority: 1 }),
@@ -99,7 +100,7 @@ export class CustomComponentNode extends DecoratorBlockNode {
99
100
  base: customComponentTheme.base || '',
100
101
  focus: customComponentTheme.focus || '',
101
102
  };
102
- return (_jsx(BlockWithAlignableContents, { className: className, format: this.__format, nodeKey: this.__key, children: _jsx(CustomComponent, { component: this.__data.component, properties: this.__data.properties }) }));
103
+ return (_jsx(BlockWithAlignableContents, { className: className, format: this.__format, nodeKey: this.__key, children: _jsx(CustomComponent, { component: this.__data.component, properties: this.__data.properties, editorTheme: config.theme }) }));
103
104
  }
104
105
  }
105
106
  export function $createCustomComponentNode(data) {
@@ -0,0 +1,11 @@
1
+ import { type EditorThemeClasses } from 'lexical';
2
+ export interface CodeProps {
3
+ editorTheme: EditorThemeClasses;
4
+ language: string;
5
+ title: string;
6
+ body?: string;
7
+ icon?: string;
8
+ foldable?: string;
9
+ foldthreshold?: string;
10
+ }
11
+ export default function Code({ editorTheme, language, title, body, icon, foldable: _foldable, foldthreshold: _foldThreshold, }: CodeProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,171 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { codeToHtml } from 'shiki';
3
+ import { useEffect, useState } from 'react';
4
+ import { Box, IconButton, styled, useTheme, Typography, Stack } from '@mui/material';
5
+ import { Icon } from '@iconify/react';
6
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
7
+ import { translate } from '@arcblock/ux/lib/Locale/util';
8
+ import { useMemoizedFn } from 'ahooks';
9
+ const translations = {
10
+ en: {
11
+ seeAllLines: 'See all {lines} lines',
12
+ collapseCode: 'Collapse code',
13
+ },
14
+ zh: {
15
+ seeAllLines: '查看所有 {lines} 行',
16
+ collapseCode: '折叠代码',
17
+ },
18
+ };
19
+ const XCode = styled('div')(({ theme }) => ({
20
+ position: 'relative',
21
+ borderRadius: theme.shape.borderRadius,
22
+ border: '1px solid',
23
+ borderColor: theme.palette.divider,
24
+ overflow: 'hidden',
25
+ pre: {
26
+ margin: 0,
27
+ borderRadius: 0,
28
+ code: {
29
+ margin: 0,
30
+ border: 0,
31
+ borderRadius: 0,
32
+ background: 'transparent',
33
+ color: 'inherit',
34
+ overflow: 'hidden',
35
+ },
36
+ },
37
+ }));
38
+ /** Header 组件 */
39
+ function CodeHeader({ title, icon, actions }) {
40
+ if (!title && !icon)
41
+ return null;
42
+ return (_jsxs(Box, { sx: {
43
+ display: 'flex',
44
+ alignItems: 'center',
45
+ justifyContent: 'space-between',
46
+ p: 1.5,
47
+ borderBottom: 1,
48
+ borderColor: 'divider',
49
+ bgcolor: 'background.paper',
50
+ borderRadius: '8px 8px 0 0',
51
+ }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1, flex: 1 }, children: [icon && _jsx(Box, { component: Icon, icon: icon, sx: { fontSize: '1.2em', color: 'text.secondary' } }), title && (_jsx(Typography, { variant: "body2", sx: { color: 'text.secondary', fontWeight: 500 }, children: title }))] }), actions && _jsx(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: actions })] }));
52
+ }
53
+ /** 通用操作按钮 */
54
+ function ActionButton({ className, icon, onClick, sx = {}, }) {
55
+ return (_jsx(IconButton, { className: className, size: "small", sx: {
56
+ color: 'text.secondary',
57
+ bgcolor: 'transparent',
58
+ '&:hover': {
59
+ color: 'text.primary',
60
+ bgcolor: 'action.hover',
61
+ },
62
+ ...sx,
63
+ }, onClick: onClick, children: _jsx(Box, { component: Icon, icon: icon, sx: { fontSize: '1em' } }) }));
64
+ }
65
+ /** 复制按钮 */
66
+ function CopyButton({ content, sx = {} }) {
67
+ const [isCopied, setIsCopied] = useState(false);
68
+ const handleCopy = async () => {
69
+ try {
70
+ await navigator.clipboard.writeText(content);
71
+ setIsCopied(true);
72
+ setTimeout(() => {
73
+ setIsCopied(false);
74
+ }, 1500);
75
+ }
76
+ catch (err) {
77
+ console.error('Failed to copy: ', err);
78
+ }
79
+ };
80
+ return (_jsx(ActionButton, { className: "copy-button", icon: isCopied ? 'tabler:check' : 'tabler:copy', onClick: handleCopy, sx: {
81
+ ...sx,
82
+ ...(isCopied && { color: 'success.main', '&:hover': { color: 'success.main' } }),
83
+ } }));
84
+ }
85
+ /** 折叠按钮 */
86
+ function FoldButton({ isCollapsed, onToggle, sx = {} }) {
87
+ return (_jsx(ActionButton, { className: "fold-button", icon: isCollapsed ? 'tabler:chevron-down' : 'tabler:chevron-up', onClick: onToggle, sx: sx }));
88
+ }
89
+ /** 折叠 Footer */
90
+ function FoldFooter({ remainingLines, isCollapsed, onToggle, }) {
91
+ const { locale = 'en' } = useLocaleContext();
92
+ const t = useMemoizedFn((key, data = {}) => translate(translations, key, locale, 'en', data));
93
+ return (_jsx(Box, { sx: {
94
+ display: 'flex',
95
+ alignItems: 'center',
96
+ justifyContent: 'space-between',
97
+ p: 1.5,
98
+ borderTop: 1,
99
+ borderColor: 'divider',
100
+ bgcolor: 'background.paper',
101
+ borderRadius: '0 0 8px 8px',
102
+ cursor: 'pointer',
103
+ '&:hover': {
104
+ bgcolor: 'action.hover',
105
+ },
106
+ }, onClick: onToggle, children: _jsxs(Typography, { variant: "body2", sx: { color: 'text.secondary', fontWeight: 500, display: 'flex', alignItems: 'center' }, children: [_jsx(Box, { component: Icon, icon: "tabler:dots", sx: { fontSize: '1em', mr: 1 } }), isCollapsed ? t('seeAllLines', { lines: remainingLines }) : t('collapseCode', { lines: remainingLines })] }) }));
107
+ }
108
+ export default function Code({ editorTheme, language, title, body = '', icon = '', foldable: _foldable = 'true', foldthreshold: _foldThreshold = '25', // ps: dataset 会强制将 key 小写
109
+ }) {
110
+ const [highlightedCode, setHighlightedCode] = useState('');
111
+ const [isLoading, setIsLoading] = useState(false);
112
+ const muiTheme = useTheme();
113
+ // 参数类型转换(html dataset 只支持 string value)
114
+ const foldable = _foldable === 'true';
115
+ let foldThreshold = parseInt(_foldThreshold, 10);
116
+ if (Number.isNaN(foldThreshold)) {
117
+ foldThreshold = 25;
118
+ }
119
+ const lineCount = body ? body.split('\n').length : 0;
120
+ const showFoldButton = foldable && lineCount > foldThreshold;
121
+ // 初始折叠状态:启用折叠且超过阈值就折叠
122
+ const [isCollapsed, setIsCollapsed] = useState(foldable && lineCount > foldThreshold);
123
+ // 获取最终需要显示的内容
124
+ const getDisplayContent = () => {
125
+ if (!isCollapsed || lineCount <= foldThreshold) {
126
+ return body;
127
+ }
128
+ const lines = body.split('\n');
129
+ const previewLines = lines.slice(0, foldThreshold);
130
+ return previewLines.join('\n');
131
+ };
132
+ const displayLineCount = isCollapsed && lineCount > foldThreshold ? foldThreshold : lineCount;
133
+ const gutter = displayLineCount > 0 ? Array.from({ length: displayLineCount }, (_, i) => i + 1).join('\n') : '';
134
+ const remainingLines = isCollapsed && lineCount > foldThreshold ? lineCount - foldThreshold : 0;
135
+ // 选择高亮主题
136
+ const shikiTheme = muiTheme.palette.mode === 'dark' ? 'github-dark' : 'github-light';
137
+ // 使用 shiki 进行代码高亮
138
+ useEffect(() => {
139
+ const highlightCode = async () => {
140
+ const contentToHighlight = getDisplayContent();
141
+ if (!contentToHighlight) {
142
+ setHighlightedCode('');
143
+ return;
144
+ }
145
+ setIsLoading(true);
146
+ try {
147
+ const html = await codeToHtml(contentToHighlight, {
148
+ lang: language,
149
+ theme: shikiTheme,
150
+ });
151
+ setHighlightedCode(html.replace(/<code([^>]*)>/g, `<code$1 class="${editorTheme.code}" data-gutter="${gutter}" data-language="${language}">`));
152
+ }
153
+ catch (error) {
154
+ console.error('Code highlighting failed:', error);
155
+ setHighlightedCode(contentToHighlight);
156
+ }
157
+ finally {
158
+ setIsLoading(false);
159
+ }
160
+ };
161
+ highlightCode();
162
+ }, [body, language, shikiTheme, isCollapsed, foldThreshold, gutter]);
163
+ const shouldShowHeader = title || icon;
164
+ const actions = (_jsxs(_Fragment, { children: [showFoldButton && _jsx(FoldButton, { isCollapsed: isCollapsed, onToggle: () => setIsCollapsed(!isCollapsed) }), _jsx(CopyButton, { content: body })] }));
165
+ return (_jsxs(XCode, { className: "x-code", children: [shouldShowHeader && (_jsx(CodeHeader, { title: title, icon: icon, actions: _jsx(Stack, { alignItems: "center", direction: "row", spacing: 0.5, children: actions }) })), !isLoading && highlightedCode ? (_jsxs(_Fragment, { children: [_jsx("div", { dangerouslySetInnerHTML: { __html: highlightedCode } }), !shouldShowHeader && (_jsx(Stack, { alignItems: "center", direction: "row", spacing: 0.5, sx: { position: 'absolute', right: '12px', top: '12px' }, children: actions }))] })) : (_jsx("pre", { children: _jsx("code", { className: editorTheme.code, "data-gutter": gutter, "data-language": language, style: {
166
+ opacity: 0.15,
167
+ filter: 'blur(0.5px)',
168
+ pointerEvents: 'none',
169
+ userSelect: 'none',
170
+ }, children: getDisplayContent() }) })), showFoldButton && (_jsx(FoldFooter, { remainingLines: remainingLines, isCollapsed: isCollapsed, onToggle: () => setIsCollapsed(!isCollapsed) }))] }));
171
+ }
@@ -1,6 +1,8 @@
1
1
  import Card from './Card';
2
2
  import Cards from './Cards';
3
+ import Code from './Code';
3
4
  declare const components: {
5
+ code: typeof Code;
4
6
  card: typeof Card;
5
7
  cards: typeof Cards;
6
8
  };
@@ -1,6 +1,8 @@
1
1
  import Card from './Card';
2
2
  import Cards from './Cards';
3
+ import Code from './Code';
3
4
  const components = {
5
+ code: Code,
4
6
  card: Card,
5
7
  cards: Cards,
6
8
  };
@@ -243,7 +243,7 @@ const createCustomTheme = (theme) => {
243
243
  /* white-space: pre; */
244
244
  overflow-x: auto;
245
245
  position: relative;
246
- border-radius: 8px;
246
+ border-radius: ${theme.shape.borderRadius}px;
247
247
  border: 1px solid ${palette.divider};
248
248
 
249
249
  &:before {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/editor",
3
- "version": "2.4.75",
3
+ "version": "2.4.76",
4
4
  "main": "lib/index.js",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -66,11 +66,12 @@
66
66
  "react-error-boundary": "^3.1.4",
67
67
  "react-player": "^2.16.0",
68
68
  "react-popper": "^2.3.0",
69
+ "shiki": "1.14.1",
69
70
  "split-pane-react": "^0.1.3",
70
71
  "ufo": "^1.5.4",
71
72
  "url-join": "^4.0.1",
72
73
  "zustand": "^4.5.5",
73
- "@blocklet/pdf": "2.4.75"
74
+ "@blocklet/pdf": "2.4.76"
74
75
  },
75
76
  "devDependencies": {
76
77
  "@babel/core": "^7.25.2",