@chenyomi/leafer-htmltext-editor 1.0.8 → 1.0.10

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.
Files changed (56) hide show
  1. package/dist/index.cjs +4 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.mts +64 -0
  4. package/dist/index.d.ts +51 -5
  5. package/dist/index.mjs +4 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +16 -8
  8. package/dist/TextEditTool/index.d.ts +0 -17
  9. package/dist/TextEditTool/index.d.ts.map +0 -1
  10. package/dist/TextEditTool/index.js +0 -113
  11. package/dist/TextEditTool/utils.d.ts +0 -8
  12. package/dist/TextEditTool/utils.d.ts.map +0 -1
  13. package/dist/TextEditTool/utils.js +0 -256
  14. package/dist/TextEditor.d.ts +0 -30
  15. package/dist/TextEditor.d.ts.map +0 -1
  16. package/dist/TextEditor.js +0 -169
  17. package/dist/esm/TextEditTool/index.d.ts +0 -17
  18. package/dist/esm/TextEditTool/index.d.ts.map +0 -1
  19. package/dist/esm/TextEditTool/index.js +0 -110
  20. package/dist/esm/TextEditTool/utils.d.ts +0 -8
  21. package/dist/esm/TextEditTool/utils.d.ts.map +0 -1
  22. package/dist/esm/TextEditTool/utils.js +0 -248
  23. package/dist/esm/TextEditor.d.ts +0 -30
  24. package/dist/esm/TextEditor.d.ts.map +0 -1
  25. package/dist/esm/TextEditor.js +0 -166
  26. package/dist/esm/fonts/font.d.ts +0 -17
  27. package/dist/esm/fonts/font.d.ts.map +0 -1
  28. package/dist/esm/fonts/font.js +0 -68
  29. package/dist/esm/fonts/utils.d.ts +0 -9
  30. package/dist/esm/fonts/utils.d.ts.map +0 -1
  31. package/dist/esm/fonts/utils.js +0 -170
  32. package/dist/esm/index.d.ts +0 -18
  33. package/dist/esm/index.d.ts.map +0 -1
  34. package/dist/esm/utils.d.ts +0 -3
  35. package/dist/esm/utils.d.ts.map +0 -1
  36. package/dist/esm/utils.js +0 -266
  37. package/dist/fonts/font.d.ts +0 -17
  38. package/dist/fonts/font.d.ts.map +0 -1
  39. package/dist/fonts/font.js +0 -72
  40. package/dist/fonts/utils.d.ts +0 -9
  41. package/dist/fonts/utils.d.ts.map +0 -1
  42. package/dist/fonts/utils.js +0 -180
  43. package/dist/index.d.ts.map +0 -1
  44. package/dist/index.esm.js +0 -140
  45. package/dist/index.js +0 -178
  46. package/dist/utils.d.ts +0 -3
  47. package/dist/utils.d.ts.map +0 -1
  48. package/dist/utils.js +0 -271
  49. package/src/TextEditTool/index.ts +0 -132
  50. package/src/TextEditTool/utils.ts +0 -288
  51. package/src/TextEditor.ts +0 -213
  52. package/src/fonts/font.ts +0 -86
  53. package/src/fonts/utils.ts +0 -232
  54. package/src/htmltext-editor.css +0 -103
  55. package/src/index.ts +0 -163
  56. package/src/utils.ts +0 -294
package/dist/utils.js DELETED
@@ -1,271 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setHTMLText = exports.updataHtmlText = void 0;
4
- const _1 = require(".");
5
- const updataHtmlText = async (e, base64font, fontObj) => {
6
- const { scaleX, scaleY } = e.worldTransform;
7
- const zoomScale = Math.max(Math.abs(scaleX), Math.abs(scaleY));
8
- const dom = document.querySelector('#textInnerEditor');
9
- if (dom && e.data.textData?.fontFamily) {
10
- dom.style.fontFamily = e.data.textData.fontFamily;
11
- }
12
- if (dom && e.data.textData?.fontSize) {
13
- dom.style.fontSize = `${e.data.textData.fontSize * zoomScale}px`;
14
- }
15
- if (dom && e.data.textData?.lineHeight) {
16
- dom.style.lineHeight = e.data.textData.lineHeight;
17
- }
18
- if (dom && e.data.textData?.letterSpacing) {
19
- dom.style.letterSpacing = `${e.data.textData.letterSpacing}px`;
20
- }
21
- if (dom && e.data.textData?.textShadow) {
22
- dom.style.textShadow = e.data.textData.textShadow;
23
- }
24
- else {
25
- dom.style.textShadow = 'none';
26
- }
27
- if (dom && e.data.textData?.alignContent) {
28
- const qlEditor = dom.querySelector('.ql-editor');
29
- qlEditor.style.alignContent = e.data.textData.alignContent;
30
- }
31
- const quill = _1.quillManager.getQuill();
32
- const html = quill.getSemanticHTML();
33
- if (html === '<p></p>') {
34
- if (e.text.includes('<style>@font-face')) {
35
- e.text = e.text.split('</style>')[0] + '</style>';
36
- }
37
- else {
38
- e.text = '';
39
- }
40
- return;
41
- }
42
- if (e.text.includes('<style>@font-face')) {
43
- const style = e.text.split('</style>')[0];
44
- if (fontObj && !style.includes(fontObj.code)) {
45
- const addStyle = `@font-face {font-family: ${fontObj.code};src: url(${base64font}) format('woff2') } .ql-font-${fontObj?.code?.replace(/\s+/g, '')} {font-family: ${fontObj.name};}`;
46
- e.text =
47
- style +
48
- addStyle +
49
- '</style>' +
50
- addFontSizeToP(e, html, e.data.textData.fontSize, e.data.textData.lineHeight, e.data.textData.letterSpacing, e.data.textData.textShadow, e.data.textData?.alignContent);
51
- }
52
- else {
53
- e.text =
54
- style +
55
- '</style>' +
56
- addFontSizeToP(e, html, e.data.textData.fontSize, e.data.textData.lineHeight, e.data.textData.letterSpacing, e.data.textData.textShadow, e.data.textData?.alignContent);
57
- }
58
- }
59
- else if (base64font && fontObj) {
60
- const style = `<style>@font-face {font-family: ${e.data.textData.fontFamily.split(',')[0]};src: url(${base64font}) format('woff2') } .ql-font-${fontObj?.code?.replace(/\s+/g, '')} {font-family: ${fontObj.name};}</style>`;
61
- e.text =
62
- style +
63
- addFontSizeToP(e, html, e.data.textData.fontSize, e.data.textData.lineHeight, e.data.textData.letterSpacing, e.data.textData.textShadow, e.data.textData?.alignContent);
64
- }
65
- else {
66
- e.text = addFontSizeToP(e, html, e.data.textData.fontSize, e.data.textData.lineHeight, e.data.textData.letterSpacing, e.data.textData.textShadow, e.data.textData?.alignContent);
67
- }
68
- };
69
- exports.updataHtmlText = updataHtmlText;
70
- const addFontSizeToP = (e, html, fontSize = 16, lineHeight = '1.5', letterSpacing = '0', textShadow = 'none', alignContent = 'start') => {
71
- const { scaleX, scaleY } = e.worldTransform;
72
- const zoomScale = Math.max(Math.abs(scaleX), Math.abs(scaleY));
73
- const wrapper = document.createElement('div');
74
- wrapper.innerHTML = html;
75
- const wrapperStyle = {
76
- fontSize: `${fontSize}px`,
77
- lineHeight,
78
- letterSpacing: `${letterSpacing}px`,
79
- textShadow
80
- };
81
- wrapper.querySelectorAll('p,ol,ul').forEach((p) => {
82
- Object.assign(p.style, wrapperStyle);
83
- });
84
- let str = wrapper.innerHTML;
85
- if (/<p\b[^>]*><\/p>/.test(str)) {
86
- str = str.replace(/<p\b([^>]*)><\/p>/g, '<p$1>&nbsp;</p>');
87
- }
88
- let height;
89
- const div = document.querySelector('#textInnerEditor');
90
- const quill = _1.quillManager.getQuill();
91
- const actualHeight = Number((quill.scroll.domNode.scrollHeight / zoomScale).toFixed(0));
92
- const actualWidth = Number((quill.scroll.domNode.scrollWidth / zoomScale).toFixed(0));
93
- if (['center', 'end'].includes(e.data.textData.alignContent)) {
94
- if (e.parent.height < actualHeight) {
95
- e.data.textData.alignContent = 'start';
96
- height = (actualHeight || e.__layout.boxBounds.height) + 'px';
97
- }
98
- else {
99
- height = `${e.parent.height}px`;
100
- }
101
- }
102
- else {
103
- if (e.parent.height < actualHeight) {
104
- height = (actualHeight || e.__layout.boxBounds.height) + 'px';
105
- }
106
- else {
107
- height = `${e.parent.height}px`;
108
- }
109
- }
110
- const style = `<style>sub,sup{font-size:63%;}.ql-ui{position:absolute}ol,ul{counter-reset:list-0;padding-left:1.5em;margin:0}ol>li,ul>li{counter-increment:list-0;list-style-type:none;position:relative;padding-left:0;margin:0}ol>li::before{content:counter(list-0,decimal) '. ';position:absolute;left:-1.5em;width:1.2em;text-align:right}ul>li::before{content:'\u2022';position:absolute;left:-1.5em;width:1.2em;text-align:right}li[data-list]{counter-set:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-align-center{text-align:center}.ql-align-right{text-align:right}.ql-align-left{text-align:left}.ql-align-justify{text-align:justify}</style>`;
111
- let divBox = '';
112
- if (e.parent.children[0].tag.includes('Shape')) {
113
- divBox =
114
- style +
115
- `<div style="width: ${e.parent.width}px;height: ${e.parent.height}px;overflow-wrap:break-word;word-break:break-all;align-content:center;">${str}</div>`;
116
- }
117
- else if (e.data.canChangeBox) {
118
- divBox =
119
- style +
120
- `<div style="width: ${e.parent.width}px;height:${height};overflow-wrap:break-word;word-break:break-all;align-content:${alignContent};">${str}</div>`;
121
- }
122
- else {
123
- if (e.data.textData.italic) {
124
- divBox = style + str;
125
- }
126
- else {
127
- console.log(actualHeight);
128
- divBox = style + `<div style="width: ${actualWidth - 10}px;">${str}</div>`;
129
- }
130
- }
131
- console.log(divBox, '最终的html内容');
132
- return divBox;
133
- };
134
- const setHTMLText = (key, value, base64font) => {
135
- const quill = _1.quillManager.getQuill();
136
- const { editor, canvas } = _1.quillManager.getCanvas();
137
- console.log(editor, _1.quillManager.getCanvas(), 'editor');
138
- if (!quill) {
139
- return;
140
- }
141
- const range = JSON.parse(localStorage.getItem('selection-change') || '{}');
142
- _1.quillManager.dateEdit(async (e) => {
143
- if (key === 'font') {
144
- const fontSimpleName = value.code.replace(/\s+/g, '');
145
- if (editor.innerEditing) {
146
- if (range && range.length) {
147
- quill.formatText(range.index, range.length, key, fontSimpleName);
148
- }
149
- else {
150
- quill.formatText(0, quill.getLength() - 1, key, fontSimpleName);
151
- }
152
- (0, exports.updataHtmlText)(e, base64font ?? null, value ?? null);
153
- }
154
- else {
155
- _1.quillManager.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
156
- quill.formatText(0, quill.getLength() - 1, key, fontSimpleName);
157
- (0, exports.updataHtmlText)(e, base64font ?? null, value ?? null);
158
- }
159
- }
160
- else if (key === 'fontSize') {
161
- e.data.textData[key] = value;
162
- _1.quillManager.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
163
- (0, exports.updataHtmlText)(e, base64font ?? null);
164
- }
165
- else if (key === 'textCase') {
166
- if (editor.innerEditing) {
167
- const text = quill.getText(range.index, range.length);
168
- const formats = quill.getFormat(range.index, range.length);
169
- quill.deleteText(range.index, range.length);
170
- let convertedText;
171
- if (text === text.toUpperCase() && /[A-Z]/.test(text)) {
172
- convertedText = text.toLowerCase();
173
- }
174
- else if (text === text.toLowerCase() && /[a-z]/.test(text)) {
175
- convertedText = text.toUpperCase();
176
- }
177
- else {
178
- convertedText = text.toUpperCase();
179
- }
180
- quill.insertText(range.index, convertedText, formats);
181
- range && editor.innerEditing && quill.setSelection(range.index, range.length);
182
- }
183
- }
184
- else if (key === 'script') {
185
- let val = 'sub';
186
- if (value === 'super')
187
- val = 'sup';
188
- if (editor.innerEditing) {
189
- if (range && range.length) {
190
- quill.formatText(range.index, range.length, key, quill.getFormat(range).script === value ? false : val);
191
- }
192
- else {
193
- quill.formatText(0, quill.getLength() - 1, key, quill.getFormat().script === value ? false : val);
194
- }
195
- }
196
- else {
197
- _1.quillManager.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
198
- quill.formatText(0, quill.getLength() - 1, key, quill.getFormat().script === value ? false : val);
199
- (0, exports.updataHtmlText)(e);
200
- }
201
- }
202
- else if (key === 'align') {
203
- _1.quillManager.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
204
- if (editor.innerEditing) {
205
- quill.format(key, value);
206
- }
207
- else {
208
- quill.formatLine(0, quill.getLength(), key, value);
209
- }
210
- (0, exports.updataHtmlText)(e);
211
- }
212
- else if (key === 'alignContent') {
213
- _1.quillManager.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
214
- e.data.textData[key] = value;
215
- (0, exports.updataHtmlText)(e);
216
- }
217
- else if (key === 'color') {
218
- _1.quillManager.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
219
- quill.formatText(0, quill.getLength() - 1, key, value);
220
- if (e.tag === 'HTMLText') {
221
- (0, exports.updataHtmlText)(e);
222
- }
223
- else if (e.parent.findOne('HTMLText')) {
224
- (0, exports.updataHtmlText)(e.parent.findOne('HTMLText'));
225
- }
226
- }
227
- else if (key === 'textShadow') {
228
- _1.quillManager.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
229
- e.data.textData[key] = value;
230
- (0, exports.updataHtmlText)(e);
231
- }
232
- else if (key === 'list') {
233
- _1.quillManager.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
234
- if (editor.innerEditing) {
235
- const [line] = quill.getLine(range?.index || 0);
236
- if (line.formats().list) {
237
- quill.format(key, false);
238
- }
239
- else {
240
- quill.format(key, value);
241
- }
242
- }
243
- else {
244
- const [line] = quill.getLine(range?.index || 0);
245
- if (line.formats().list) {
246
- quill.formatLine(0, quill.getLength(), key, false);
247
- }
248
- else {
249
- quill.formatLine(0, quill.getLength(), key, value);
250
- }
251
- }
252
- (0, exports.updataHtmlText)(e);
253
- }
254
- else {
255
- if (editor.innerEditing) {
256
- if (range && range.length) {
257
- quill.formatText(range.index, range.length, key, !quill.getFormat(range)[key]);
258
- }
259
- else {
260
- quill.formatText(0, quill.getLength() - 1, key, !quill.getFormat()[key]);
261
- }
262
- }
263
- else {
264
- _1.quillManager.isMultiSelect() && quill.clipboard.dangerouslyPasteHTML(e.text);
265
- quill.formatText(0, quill.getLength() - 1, key, !quill.getFormat()[key]);
266
- (0, exports.updataHtmlText)(e);
267
- }
268
- }
269
- }, 1);
270
- };
271
- exports.setHTMLText = setHTMLText;
@@ -1,132 +0,0 @@
1
- import {
2
- EditTool,
3
- EditorScaleEvent,
4
- registerEditTool,
5
- } from "@leafer-in/editor";
6
- import { PointerEvent } from "leafer-ui";
7
- import { quillManager } from "../";
8
- import { updataHtmlText } from "../utils";
9
- console.log('初始化文本外部编辑插件');
10
- @registerEditTool()
11
- // 定义插件类,继承自 EditTool,处理矩形圆角编辑交互
12
- export class TextEditTool extends EditTool {
13
- // 插件标识标签
14
- public get tag() {
15
- return "TextEditTool";
16
- }
17
- public quill: any = null;
18
- private updateBoxDebounced: (text: any) => void;
19
- // 构造函数,初始化控制点并加入视图
20
- constructor(editor: any) {
21
- super(editor);
22
- this.eventIds = []; // 存储事件绑定ID
23
- this.updateBoxDebounced = this.debounce((text: any) => {
24
- updataHtmlText(text);
25
- }, 300);
26
- }
27
- // 自定义防抖函数替代 lodash/debounce
28
- private debounce<T extends (...args: any[]) => any>(
29
- func: T,
30
- wait: number,
31
- ): T {
32
- let timeout: number | null = null;
33
- return ((...args: any[]) => {
34
- if (timeout !== null) {
35
- window.clearTimeout(timeout);
36
- }
37
- timeout = window.setTimeout(
38
- () => func.apply(this, args),
39
- wait,
40
- ) as unknown as number;
41
- }) as T;
42
- }
43
- // 绑定事件
44
- public addEvent(): void {
45
- const { editor } = quillManager.getCanvas();
46
- const text = editor._target.findOne("HTMLText");
47
- const { scaleX, scaleY } = text.worldTransform;
48
- const zoomScale = Math.max(Math.abs(scaleX), Math.abs(scaleY));
49
- const div: any = document.querySelector("#textInnerEditor");
50
- const { style } = div;
51
- this.eventIds = [
52
- editor.on_(EditorScaleEvent.SCALE, (e: any) => {
53
- if (!text.data.canChangeBox) {
54
- text.data.canChangeBox = true;
55
- }
56
- if (text.data.canChangeBox) {
57
- style.width = text.parent.width * zoomScale + "px";
58
- style.height = "auto";
59
- }
60
- this.updateBoxDebounced(text);
61
- }),
62
- editor.on_(PointerEvent.DOUBLE_TAP, () => {
63
- if (!text.parent.locked) {
64
- editor.openInnerEditor(text, true);
65
- }
66
- }),
67
- ];
68
- }
69
-
70
- // 生命周期钩子:插件加载时绑定事件
71
- public onLoad(): void {
72
- const { editor } = quillManager.getCanvas();
73
- console.log(editor.target, 123);
74
- const text: any = editor.target.findOne("HTMLText");
75
-
76
- const { scaleX, scaleY } = text.worldTransform;
77
- const zoomScale = Math.max(Math.abs(scaleX), Math.abs(scaleY));
78
- this.addEvent();
79
- this.quill = quillManager.getQuill();
80
- this.quill.clipboard.dangerouslyPasteHTML(text.text);
81
- const div: any = document.querySelector("#textInnerEditor");
82
- const { style } = div;
83
-
84
- if (text.data.canChangeBox) {
85
- style.width = text.parent.width * zoomScale + "px";
86
- style.height = text.parent.height * zoomScale + "px";
87
- } else {
88
- style.width = "auto";
89
- style.height = "auto";
90
- }
91
- // 每次进来就恢复初始的 canChangeBox 和 自动的宽高
92
- // text.set({
93
- // width: 0,
94
- // height: 0
95
- // })
96
- // text.data.canChangeBox = false
97
- }
98
-
99
- private isUpdatingPoints = false;
100
- private curveAmount = 0;
101
- public updateChangeBoxBound(text: any): void {
102
- text &&
103
- text.set({
104
- width: text.__layout.boxBounds.width,
105
- height: text.__layout.boxBounds.height,
106
- });
107
- }
108
- // 生命周期钩子:插件更新时,因为这是全局的生命周期所以判断一定要细致仔细
109
- public onUpdate(): void {
110
- const { editor } = quillManager.getCanvas();
111
- const text = editor._target.findOne("HTMLText");
112
- const el = editor._target;
113
- console.log("文本bound更新");
114
-
115
- if (this.curveAmount == (text as any).curveAmount) return;
116
- if (this.isUpdatingPoints) return;
117
- // 每次进来就恢复初始的 canChangeBox 和 自动的宽高
118
- // text.set({
119
- // width: 0,
120
- // height: 0
121
- // })
122
- }
123
-
124
- // 生命周期钩子:插件卸载时解绑事件
125
- public onUnload(): void {
126
- const { editor } = quillManager.getCanvas();
127
- editor.off_(this.eventIds);
128
- }
129
-
130
- // 生命周期钩子:销毁插件时清理资源
131
- public onDestroy(): void {}
132
- }
@@ -1,288 +0,0 @@
1
- import { HTMLText } from '@leafer-in/html'; // 导入 html 插件
2
- import { Box, Path } from 'leafer-ui'
3
- export function getArcRadius(fontSize: number, curveAmount: number): number {
4
- if (curveAmount === 0) return Infinity
5
- const theta = (330 * fontSize * 72) / 96
6
- const radius = theta / Math.abs(curveAmount)
7
- // 根据正负仅决定方向,不影响半径大小
8
- return radius
9
- }
10
-
11
- export const handleShowCurve = (element: any, op: boolean) => {
12
- const box = element.findOne('Box')
13
- box && element.remove(box)
14
- const text = element.findOne('HTMLText') as any
15
- // 下一帧再生成新路径
16
- const { boxBounds, x, y } = text
17
- const { width } = boxBounds
18
- if (!text.curveAmount) {
19
- // 当移动到0的时候显示原来的文字
20
- text.opacity = 1
21
- // 这个为了解决其他地方有隐藏了这个的情况下
22
- text.visible = true
23
- return
24
- }
25
- text.text = text.text.replace(/\u200B/g, '').replace(/<br\s*\/?>/gi, '\n') // 把 <br> 转换成换行符
26
- text.opacity = 0
27
- const radius = getArcRadius(text.fontSize, text.curveAmount)
28
- const C = 2 * Math.PI * radius
29
- const dis = (text.text as string).length + 2
30
- const tW = width / dis
31
- let startW = 0
32
- if (C > tW * (text.text as string).length) {
33
- startW = (C - tW * (text.text as string).length) / 2
34
- }
35
- const startWPr = (startW * 360) / C
36
- const group = new Box({
37
- x: 0,
38
- y: 0,
39
- editable: false,
40
- resizeChildren: true
41
- })
42
- let path
43
- const offsetK = text.fontSize * text.lineHeight.value || 0
44
- const offsetY = text.curveAmount > 0 ? radius + offsetK : -radius
45
- if (text.curveAmount > 0) {
46
- path = `G ${x + width / 2} ${offsetY} ${radius} ${radius} 90 ${startWPr} ${360 - startWPr} 0`
47
- } else {
48
- const a = 180 - startWPr
49
- path = `G ${x + width / 2} ${offsetY} ${radius} ${radius} 90 ${a} ${-a} 1`
50
- }
51
- const pathLine = new Path({
52
- x: 0,
53
- y: 0,
54
- motionPath: true,
55
- editable: false,
56
- path: path
57
- })
58
- // 给Box添加新的Path轨迹
59
- group.add(pathLine)
60
- // op如果内部编辑输入的时候显示预测轨迹
61
- group.set({
62
- opacity: op ? 0.2 : 1
63
- })
64
- Array.from(text.text as string).forEach((ch, i) => {
65
- const str = new HTMLText({
66
- text: ch,
67
- around: text.curveAmount > 0 ? 'bottom' : 'top',
68
- fontSize: text.fontSize,
69
- fontFamily: text.fontFamily,
70
- fontWeight: text.fontWeight,
71
- motion: i * tW + tW / 2,
72
- textDecoration: text.textDecoration,
73
- textCase: text.textCase,
74
- textAlign: text.textAlign,
75
- motionRotation: text.curveAmount > 0 ? 3 : -2,
76
- fill: text.fill
77
- })
78
- group.add(str)
79
- })
80
- element.add(group)
81
- }
82
-
83
- // base 映射:只需把普通字符 -> 上标/下标(任意已有的上/下标字符)列出来一次
84
- const baseSuperscript: Record<string, string> = {
85
- '0': '⁰',
86
- '1': '¹',
87
- '2': '²',
88
- '3': '³',
89
- '4': '⁴',
90
- '5': '⁵',
91
- '6': '⁶',
92
- '7': '⁷',
93
- '8': '⁸',
94
- '9': '⁹',
95
- a: 'ᵃ',
96
- b: 'ᵇ',
97
- c: 'ᶜ',
98
- d: 'ᵈ',
99
- e: 'ᵉ',
100
- f: 'ᶠ',
101
- g: 'ᵍ',
102
- h: 'ʰ',
103
- i: 'ⁱ',
104
- j: 'ʲ',
105
- k: 'ᵏ',
106
- l: 'ˡ',
107
- m: 'ᵐ',
108
- n: 'ⁿ',
109
- o: 'ᵒ',
110
- p: 'ᵖ',
111
- r: 'ʳ',
112
- s: 'ˢ',
113
- t: 'ᵗ',
114
- u: 'ᵘ',
115
- v: 'ᵛ',
116
- w: 'ʷ',
117
- x: 'ˣ',
118
- y: 'ʸ',
119
- z: 'ᶻ',
120
- A: 'ᴬ',
121
- B: 'ᴮ',
122
- D: 'ᴰ',
123
- E: 'ᴱ',
124
- G: 'ᴳ',
125
- H: 'ᴴ',
126
- I: 'ᴵ',
127
- J: 'ᴶ',
128
- K: 'ᴷ',
129
- L: 'ᴸ',
130
- M: 'ᴹ',
131
- N: 'ᴺ',
132
- O: 'ᴼ',
133
- P: 'ᴾ',
134
- R: 'ᴿ',
135
- T: 'ᵀ',
136
- U: 'ᵁ',
137
- W: 'ᵂ',
138
- '+': '⁺',
139
- '-': '⁻',
140
- '=': '⁼',
141
- '(': '⁽',
142
- ')': '⁾'
143
- }
144
-
145
- const baseSubscript: Record<string, string> = {
146
- '0': '₀',
147
- '1': '₁',
148
- '2': '₂',
149
- '3': '₃',
150
- '4': '₄',
151
- '5': '₅',
152
- '6': '₆',
153
- '7': '₇',
154
- '8': '₈',
155
- '9': '₉',
156
- a: 'ₐ',
157
- e: 'ₑ',
158
- h: 'ₕ',
159
- i: 'ᵢ',
160
- j: 'ⱼ',
161
- k: 'ₖ',
162
- l: 'ₗ',
163
- m: 'ₘ',
164
- n: 'ₙ',
165
- o: 'ₒ',
166
- p: 'ₚ',
167
- r: 'ᵣ',
168
- s: 'ₛ',
169
- t: 'ₜ',
170
- u: 'ᵤ',
171
- v: 'ᵥ',
172
- x: 'ₓ',
173
- '+': '₊',
174
- '-': '₋',
175
- '=': '₌',
176
- '(': '₍',
177
- ')': '₎',
178
- // 常见希腊字母的下标形式(如需要可加入)
179
- β: 'ᵦ',
180
- γ: 'ᵧ',
181
- ρ: 'ᵨ',
182
- φ: 'ᵩ',
183
- χ: 'ᵪ'
184
- }
185
-
186
- // helper: 从 base 映射中收集所有已知上标/下标字符(用于反向映射)
187
- function invertMap(map: Record<string, string>): Record<string, string> {
188
- const inv: Record<string, string> = {}
189
- for (const k in map) {
190
- inv[map[k]] = map[k] // key: 上/下标字符 -> value: 同样的上/下标字符(作为标准形)
191
- }
192
- return inv
193
- }
194
-
195
- const knownSupers = invertMap(baseSuperscript) // '¹' -> '¹', 'ᵃ'->'ᵃ' ...
196
- const knownSubs = invertMap(baseSubscript) // '₁' -> '₁', 'ₐ'->'ₐ' ...
197
-
198
- // 生成最终的“鲁棒”映射:接受普通字符、上标字符、下标字符,统一输出目标形式
199
- function buildSuperscriptMap(baseSup: Record<string, string>, baseSub: Record<string, string>): Record<string, string> {
200
- const out: Record<string, string> = {}
201
-
202
- // 普通字符 -> 上标
203
- for (const k in baseSup) out[k] = baseSup[k]
204
-
205
- // 已知的下标字符(如 '₁')也映射到对应的上标(如果我们在 baseSup 找到相同的 base char)
206
- // 为了做到这一点,我们需要找到下标字符对应的 base 字符:遍历 baseSub 找到哪些 base 的下标字符
207
- for (const base in baseSub) {
208
- const subChar = baseSub[base]
209
- // 如果 base 在 baseSup 有对应的上标,则把 subChar -> 那个上标
210
- if (base in baseSup) out[subChar] = baseSup[base]
211
- }
212
-
213
- // 如果输入已经是上标(例如 '¹'),也把它映射为标准上标(自身)
214
- for (const supChar in knownSupers) out[supChar] = supChar
215
-
216
- return out
217
- }
218
-
219
- function buildSubscriptMap(baseSup: Record<string, string>, baseSub: Record<string, string>): Record<string, string> {
220
- const out: Record<string, string> = {}
221
-
222
- // 普通字符 -> 下标
223
- for (const k in baseSub) out[k] = baseSub[k]
224
-
225
- // 已知的上标字符也映射到对应的下标(如果 baseSup 有同名 base)
226
- for (const base in baseSup) {
227
- const supChar = baseSup[base]
228
- if (base in baseSub) out[supChar] = baseSub[base]
229
- }
230
-
231
- // 已经是下标的字符映射到自身
232
- for (const subChar in knownSubs) out[subChar] = subChar
233
-
234
- return out
235
- }
236
-
237
- const superscriptMap = buildSuperscriptMap(baseSuperscript, baseSubscript)
238
- const subscriptMap = buildSubscriptMap(baseSuperscript, baseSubscript)
239
-
240
- // 转换函数(逐字符替换;可以根据需要改成群组替换)
241
- export function toSuperscript(input: string): string {
242
- let out = ''
243
- for (const ch of input) {
244
- out += superscriptMap[ch] ?? ch
245
- }
246
- return out
247
- }
248
-
249
- export function toSubscript(input: string): string {
250
- let out = ''
251
- for (const ch of input) {
252
- out += subscriptMap[ch] ?? ch
253
- }
254
- return out
255
- }
256
-
257
- export const superscriptMapVal = Object.values(superscriptMap)
258
-
259
- export const subscriptMapVal = Object.values(subscriptMap)
260
-
261
- // 构建 normalMap:所有上标、下标字符 → 对应的普通 base 字符
262
- function buildNormalMap(baseSup: Record<string, string>, baseSub: Record<string, string>): Record<string, string> {
263
- const normal: Record<string, string> = {}
264
-
265
- // 上标的 value → key(普通)
266
- for (const base in baseSup) {
267
- const supChar = baseSup[base]
268
- normal[supChar] = base
269
- }
270
-
271
- // 下标的 value → key(普通)
272
- for (const base in baseSub) {
273
- const subChar = baseSub[base]
274
- normal[subChar] = base
275
- }
276
-
277
- return normal
278
- }
279
-
280
- const normalMap = buildNormalMap(baseSuperscript, baseSubscript)
281
-
282
- export function toNormal(input: string): string {
283
- let out = ''
284
- for (const ch of input) {
285
- out += normalMap[ch] ?? ch
286
- }
287
- return out
288
- }