@mnykj/alleditor-dual 1.1.2 → 1.2.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/README.md +21 -0
- package/package.json +7 -4
- package/src/DualEditor.vue +158 -2
- package/src/defaultToolbar.ts +100 -0
- package/src/importMenus.ts +110 -0
- package/src/index.ts +17 -0
- package/src/markdown.ts +46 -0
- package/src/stripJavascriptFromHtml.ts +28 -0
package/README.md
CHANGED
|
@@ -49,6 +49,27 @@ const html = ref('<p>hello</p>')
|
|
|
49
49
|
</template>
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
默认工具栏已带 **导入 Markdown**、**导入 HTML**:
|
|
53
|
+
- `.md` → wang 引擎
|
|
54
|
+
- `.html` → ue 引擎(并剥离 script / 事件属性)
|
|
55
|
+
|
|
56
|
+
不需要时:`<DualEditor :enable-import="false" />`。自定义 `toolbarConfig.toolbarKeys` 时,若未包含这两项会自动补上(`enableImport` 为 true)。
|
|
57
|
+
|
|
58
|
+
仅注册菜单、自己接文件选择(如创作中心):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import {
|
|
62
|
+
registerImportMenus,
|
|
63
|
+
bindImportMenuHandlers,
|
|
64
|
+
} from '@mnykj/alleditor-dual'
|
|
65
|
+
|
|
66
|
+
registerImportMenus()
|
|
67
|
+
bindImportMenuHandlers({
|
|
68
|
+
onImportHtml: () => htmlInput.click(),
|
|
69
|
+
onImportMarkdown: () => mdInput.click(),
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
52
73
|
一般不用再写 `ueditor-base`。若自建 CDN,可覆盖:
|
|
53
74
|
|
|
54
75
|
```vue
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mnykj/alleditor-dual",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AllEditor dual-engine (wang + UE HTML) Vue3 components, with bundled UEditor static assets",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "AllEditor dual-engine (wang + UE HTML) Vue3 components, with bundled UEditor static assets and MD/HTML import menus",
|
|
5
5
|
"author": "mnykj",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -36,12 +36,15 @@
|
|
|
36
36
|
"access": "public",
|
|
37
37
|
"registry": "https://registry.npmjs.org/"
|
|
38
38
|
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"markdown-it": "^14.3.3"
|
|
41
|
+
},
|
|
39
42
|
"peerDependencies": {
|
|
40
43
|
"@mnykj/alleditor": "^1.1.1",
|
|
41
44
|
"@mnykj/alleditor-for-vue": "^1.1.0",
|
|
42
|
-
"
|
|
45
|
+
"@nuxt/kit": ">=3",
|
|
43
46
|
"vite": ">=4",
|
|
44
|
-
"
|
|
47
|
+
"vue": "^3.0.5"
|
|
45
48
|
},
|
|
46
49
|
"peerDependenciesMeta": {
|
|
47
50
|
"vite": {
|
package/src/DualEditor.vue
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* AllEditor 双引擎门面:按 HTML 协议自动选 wang / ue,共用 wang 工具栏。
|
|
4
4
|
*
|
|
5
|
-
* v-model 存「可入库」HTML
|
|
5
|
+
* v-model 存「可入库」HTML(默认 wang/ue 都包 data-alleditor-engine 根节点)。
|
|
6
|
+
* 内置 MD / HTML 导入菜单(默认工具栏可见)。
|
|
6
7
|
* 宿主需自行提供 UEditor 静态资源(默认 /ueditor/)。
|
|
7
8
|
*/
|
|
8
9
|
import {
|
|
9
10
|
computed,
|
|
10
11
|
nextTick,
|
|
11
12
|
onBeforeUnmount,
|
|
13
|
+
onMounted,
|
|
12
14
|
ref,
|
|
13
15
|
shallowRef,
|
|
14
16
|
watch,
|
|
@@ -21,6 +23,17 @@ import {
|
|
|
21
23
|
} from '@mnykj/alleditor'
|
|
22
24
|
import UeHtmlEngine from './UeHtmlEngine.vue'
|
|
23
25
|
import { bindUeToolbarAdapter } from './ueToolbar'
|
|
26
|
+
import {
|
|
27
|
+
bindImportMenuHandlers,
|
|
28
|
+
registerImportMenus,
|
|
29
|
+
} from './importMenus'
|
|
30
|
+
import { mergeToolbarConfig } from './defaultToolbar'
|
|
31
|
+
import {
|
|
32
|
+
extractHtmlBody,
|
|
33
|
+
isEffectivelyEmptyHtml,
|
|
34
|
+
markdownToHtml,
|
|
35
|
+
} from './markdown'
|
|
36
|
+
import { stripJavascriptFromHtml } from './stripJavascriptFromHtml'
|
|
24
37
|
|
|
25
38
|
const props = defineProps({
|
|
26
39
|
modelValue: { type: String, default: '' },
|
|
@@ -33,6 +46,10 @@ const props = defineProps({
|
|
|
33
46
|
wangPlaceholder: { type: String, default: '<p><br></p>' },
|
|
34
47
|
/** 为 true 时仅 ue 写外层 div;默认 false,wang/ue 都包 data-alleditor-engine */
|
|
35
48
|
wrapOnlyWhenUe: { type: Boolean, default: false },
|
|
49
|
+
/** 是否注册并显示 MD/HTML 导入按钮(默认开启) */
|
|
50
|
+
enableImport: { type: Boolean, default: true },
|
|
51
|
+
/** 正文非空时导入是否 confirm 确认替换 */
|
|
52
|
+
confirmReplaceOnImport: { type: Boolean, default: true },
|
|
36
53
|
toolbarSelector: {
|
|
37
54
|
type: String,
|
|
38
55
|
default: '.alleditor-dual .w-e-toolbar, .alleditor-dual .editor-toolbar',
|
|
@@ -46,6 +63,7 @@ const emit = defineEmits([
|
|
|
46
63
|
'wang-created',
|
|
47
64
|
'ue-created',
|
|
48
65
|
'ue-ready',
|
|
66
|
+
'import',
|
|
49
67
|
])
|
|
50
68
|
|
|
51
69
|
/** @type {import('vue').Ref<'wang' | 'ue'>} */
|
|
@@ -55,11 +73,17 @@ const wangEditorRef = shallowRef(null)
|
|
|
55
73
|
const ueEngineRef = shallowRef(null)
|
|
56
74
|
const wangHtml = ref(props.wangPlaceholder)
|
|
57
75
|
const ueHtml = ref('')
|
|
76
|
+
const mdFileInputRef = ref(null)
|
|
77
|
+
const htmlFileInputRef = ref(null)
|
|
58
78
|
let toolbarBinding = null
|
|
59
79
|
let applyingExternal = false
|
|
60
80
|
|
|
61
81
|
const useUe = computed(() => engineMode.value === 'ue')
|
|
62
82
|
|
|
83
|
+
const resolvedToolbarConfig = computed(() =>
|
|
84
|
+
mergeToolbarConfig(props.toolbarConfig, { enableImport: props.enableImport })
|
|
85
|
+
)
|
|
86
|
+
|
|
63
87
|
function bodyOf(html) {
|
|
64
88
|
return unwrapEditorEngineHtml(html || '') || '<p><br></p>'
|
|
65
89
|
}
|
|
@@ -180,6 +204,96 @@ function setEngineMode(mode, html) {
|
|
|
180
204
|
: body,
|
|
181
205
|
{ forceEngine: next }
|
|
182
206
|
)
|
|
207
|
+
if (next === 'ue' || next === 'wang') {
|
|
208
|
+
emitModel(body)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function currentBodyForEmptyCheck() {
|
|
213
|
+
return bodyOf(getHtml())
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function confirmReplaceIfNeeded() {
|
|
217
|
+
if (!props.confirmReplaceOnImport) return true
|
|
218
|
+
if (isEffectivelyEmptyHtml(currentBodyForEmptyCheck())) return true
|
|
219
|
+
return window.confirm('导入将替换当前正文内容,是否继续?')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function openMarkdownPicker() {
|
|
223
|
+
if (props.disabled) return
|
|
224
|
+
mdFileInputRef.value?.click?.()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function openHtmlPicker() {
|
|
228
|
+
if (props.disabled) return
|
|
229
|
+
htmlFileInputRef.value?.click?.()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function onMarkdownFileSelected(e) {
|
|
233
|
+
const file = e.target?.files?.[0]
|
|
234
|
+
if (e.target) e.target.value = ''
|
|
235
|
+
if (!file) return
|
|
236
|
+
if (!/\.md$/i.test(file.name)) {
|
|
237
|
+
window.alert('请选择 .md 文件')
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
if (!confirmReplaceIfNeeded()) return
|
|
241
|
+
try {
|
|
242
|
+
const text = await file.text()
|
|
243
|
+
const html = await markdownToHtml(text || '')
|
|
244
|
+
setEngineMode('wang', html)
|
|
245
|
+
emit('import', {
|
|
246
|
+
type: 'markdown',
|
|
247
|
+
fileName: file.name,
|
|
248
|
+
html: getHtml(),
|
|
249
|
+
})
|
|
250
|
+
} catch (err) {
|
|
251
|
+
console.error(err)
|
|
252
|
+
window.alert(err?.message || '导入 Markdown 失败')
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function onHtmlFileSelected(e) {
|
|
257
|
+
const file = e.target?.files?.[0]
|
|
258
|
+
if (e.target) e.target.value = ''
|
|
259
|
+
if (!file) return
|
|
260
|
+
const nameOk =
|
|
261
|
+
/\.html?$/i.test(file.name) ||
|
|
262
|
+
/text\/html/i.test(file.type || '')
|
|
263
|
+
if (!nameOk) {
|
|
264
|
+
window.alert('请选择 .html / .htm 文件')
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
if (!confirmReplaceIfNeeded()) return
|
|
268
|
+
try {
|
|
269
|
+
const text = await file.text()
|
|
270
|
+
const body = stripJavascriptFromHtml(extractHtmlBody(text || ''))
|
|
271
|
+
const html = body.trim() || '<p><br></p>'
|
|
272
|
+
setEngineMode('ue', html)
|
|
273
|
+
emit('import', {
|
|
274
|
+
type: 'html',
|
|
275
|
+
fileName: file.name,
|
|
276
|
+
html: getHtml(),
|
|
277
|
+
})
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error(err)
|
|
280
|
+
window.alert(err?.message || '导入 HTML 失败')
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function wireImportHandlers() {
|
|
285
|
+
if (!props.enableImport) {
|
|
286
|
+
bindImportMenuHandlers({
|
|
287
|
+
onImportHtml: null,
|
|
288
|
+
onImportMarkdown: null,
|
|
289
|
+
})
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
registerImportMenus()
|
|
293
|
+
bindImportMenuHandlers({
|
|
294
|
+
onImportHtml: openHtmlPicker,
|
|
295
|
+
onImportMarkdown: openMarkdownPicker,
|
|
296
|
+
})
|
|
183
297
|
}
|
|
184
298
|
|
|
185
299
|
watch(
|
|
@@ -207,10 +321,23 @@ watch(engineMode, (mode) => {
|
|
|
207
321
|
else unbindToolbar()
|
|
208
322
|
})
|
|
209
323
|
|
|
324
|
+
watch(
|
|
325
|
+
() => props.enableImport,
|
|
326
|
+
() => wireImportHandlers()
|
|
327
|
+
)
|
|
328
|
+
|
|
210
329
|
applyIncoming(props.modelValue)
|
|
211
330
|
|
|
331
|
+
onMounted(() => {
|
|
332
|
+
wireImportHandlers()
|
|
333
|
+
})
|
|
334
|
+
|
|
212
335
|
onBeforeUnmount(() => {
|
|
213
336
|
unbindToolbar()
|
|
337
|
+
bindImportMenuHandlers({
|
|
338
|
+
onImportHtml: null,
|
|
339
|
+
onImportMarkdown: null,
|
|
340
|
+
})
|
|
214
341
|
const ed = wangEditorRef.value
|
|
215
342
|
if (ed && !ed.isDestroyed) {
|
|
216
343
|
try {
|
|
@@ -228,6 +355,8 @@ defineExpose({
|
|
|
228
355
|
setEngineMode,
|
|
229
356
|
getWangEditor: () => wangEditorRef.value,
|
|
230
357
|
getUeEngine: () => ueEngineRef.value,
|
|
358
|
+
openMarkdownPicker,
|
|
359
|
+
openHtmlPicker,
|
|
231
360
|
engineMode,
|
|
232
361
|
})
|
|
233
362
|
</script>
|
|
@@ -237,7 +366,7 @@ defineExpose({
|
|
|
237
366
|
<Toolbar
|
|
238
367
|
v-if="wangEditorRef"
|
|
239
368
|
:editor="wangEditorRef"
|
|
240
|
-
:default-config="
|
|
369
|
+
:default-config="resolvedToolbarConfig"
|
|
241
370
|
:mode="mode"
|
|
242
371
|
class="alleditor-dual__toolbar editor-toolbar"
|
|
243
372
|
/>
|
|
@@ -266,6 +395,22 @@ defineExpose({
|
|
|
266
395
|
/>
|
|
267
396
|
</div>
|
|
268
397
|
</div>
|
|
398
|
+
<input
|
|
399
|
+
v-if="enableImport"
|
|
400
|
+
ref="mdFileInputRef"
|
|
401
|
+
type="file"
|
|
402
|
+
accept=".md,text/markdown"
|
|
403
|
+
class="alleditor-dual__file-input"
|
|
404
|
+
@change="onMarkdownFileSelected"
|
|
405
|
+
/>
|
|
406
|
+
<input
|
|
407
|
+
v-if="enableImport"
|
|
408
|
+
ref="htmlFileInputRef"
|
|
409
|
+
type="file"
|
|
410
|
+
accept=".html,.htm,text/html"
|
|
411
|
+
class="alleditor-dual__file-input"
|
|
412
|
+
@change="onHtmlFileSelected"
|
|
413
|
+
/>
|
|
269
414
|
</div>
|
|
270
415
|
</template>
|
|
271
416
|
|
|
@@ -327,4 +472,15 @@ defineExpose({
|
|
|
327
472
|
min-height: 0;
|
|
328
473
|
height: 100%;
|
|
329
474
|
}
|
|
475
|
+
|
|
476
|
+
.alleditor-dual__file-input {
|
|
477
|
+
position: absolute !important;
|
|
478
|
+
width: 1px !important;
|
|
479
|
+
height: 1px !important;
|
|
480
|
+
padding: 0 !important;
|
|
481
|
+
margin: -1px !important;
|
|
482
|
+
overflow: hidden !important;
|
|
483
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
484
|
+
border: 0 !important;
|
|
485
|
+
}
|
|
330
486
|
</style>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DualEditor 默认工具栏:AllEditor 标准项 + MD/HTML 导入。
|
|
3
|
+
* 宿主传入 toolbarConfig.toolbarKeys 时可完全覆盖。
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_DUAL_TOOLBAR_KEYS: Array<string | Record<string, any>> = [
|
|
6
|
+
'headerSelect',
|
|
7
|
+
'blockquote',
|
|
8
|
+
'|',
|
|
9
|
+
'bold',
|
|
10
|
+
'underline',
|
|
11
|
+
'italic',
|
|
12
|
+
{
|
|
13
|
+
key: 'group-more-style',
|
|
14
|
+
title: '更多',
|
|
15
|
+
iconSvg:
|
|
16
|
+
'<svg viewBox="0 0 1024 1024"><path d="M204.8 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M505.6 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path><path d="M806.4 505.6m-76.8 0a76.8 76.8 0 1 0 153.6 0 76.8 76.8 0 1 0-153.6 0Z"></path></svg>',
|
|
17
|
+
menuKeys: ['through', 'code', 'sup', 'sub', 'clearStyle'],
|
|
18
|
+
},
|
|
19
|
+
'color',
|
|
20
|
+
'bgColor',
|
|
21
|
+
'|',
|
|
22
|
+
'fontSize',
|
|
23
|
+
'fontFamily',
|
|
24
|
+
'lineHeight',
|
|
25
|
+
'|',
|
|
26
|
+
'bulletedList',
|
|
27
|
+
'numberedList',
|
|
28
|
+
'todo',
|
|
29
|
+
{
|
|
30
|
+
key: 'group-justify',
|
|
31
|
+
title: '对齐',
|
|
32
|
+
iconSvg:
|
|
33
|
+
'<svg viewBox="0 0 1024 1024"><path d="M768 793.6v102.4H51.2v-102.4h716.8z m204.8-230.4v102.4H51.2v-102.4h921.6z m-204.8-230.4v102.4H51.2v-102.4h716.8zM972.8 102.4v102.4H51.2V102.4h921.6z"></path></svg>',
|
|
34
|
+
menuKeys: ['justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
key: 'group-indent',
|
|
38
|
+
title: '缩进',
|
|
39
|
+
iconSvg:
|
|
40
|
+
'<svg viewBox="0 0 1024 1024"><path d="M0 64h1024v128H0z m384 192h640v128H384z m0 192h640v128H384z m0 192h640v128H384zM0 832h1024v128H0z m0-128V320l256 192z"></path></svg>',
|
|
41
|
+
menuKeys: ['indent', 'delIndent'],
|
|
42
|
+
},
|
|
43
|
+
'|',
|
|
44
|
+
'emotion',
|
|
45
|
+
'insertLink',
|
|
46
|
+
{
|
|
47
|
+
key: 'group-image',
|
|
48
|
+
title: '图片',
|
|
49
|
+
iconSvg:
|
|
50
|
+
'<svg viewBox="0 0 1024 1024"><path d="M959.877 128l0.123 0.123v767.775l-0.123 0.122H64.102l-0.122-0.122V128.123l0.122-0.123h895.775zM960 64H64C28.795 64 0 92.795 0 128v768c0 35.205 28.795 64 64 64h896c35.205 0 64-28.795 64-64V128c0-35.205-28.795-64-64-64zM832 288.01c0 53.023-42.988 96.01-96.01 96.01s-96.01-42.987-96.01-96.01S682.967 192 735.99 192 832 234.988 832 288.01zM896 832H128V704l224.01-384 256 320h64l224.01-192z"></path></svg>',
|
|
51
|
+
menuKeys: ['insertImage', 'uploadImage'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'group-video',
|
|
55
|
+
title: '视频',
|
|
56
|
+
iconSvg:
|
|
57
|
+
'<svg viewBox="0 0 1024 1024"><path d="M981.184 160.096C837.568 139.456 678.848 128 512 128S186.432 139.456 42.816 160.096C15.296 267.808 0 386.848 0 512s15.264 244.16 42.816 351.904C186.464 884.544 345.152 896 512 896s325.568-11.456 469.184-32.096C1008.704 756.192 1024 637.152 1024 512s-15.264-244.16-42.816-351.904zM384 704V320l320 192-320 192z"></path></svg>',
|
|
58
|
+
menuKeys: ['insertVideo', 'uploadVideo'],
|
|
59
|
+
},
|
|
60
|
+
'insertTable',
|
|
61
|
+
'codeBlock',
|
|
62
|
+
'divider',
|
|
63
|
+
'|',
|
|
64
|
+
'undo',
|
|
65
|
+
'redo',
|
|
66
|
+
'|',
|
|
67
|
+
'importMarkdown',
|
|
68
|
+
'sourceCode',
|
|
69
|
+
'|',
|
|
70
|
+
'fullScreen',
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
export function mergeToolbarConfig(
|
|
74
|
+
userConfig: Record<string, any> | null | undefined,
|
|
75
|
+
options?: { enableImport?: boolean }
|
|
76
|
+
) {
|
|
77
|
+
const enableImport = options?.enableImport !== false
|
|
78
|
+
const user = userConfig && typeof userConfig === 'object' ? { ...userConfig } : {}
|
|
79
|
+
if (Array.isArray(user.toolbarKeys) && user.toolbarKeys.length) {
|
|
80
|
+
if (!enableImport) return user
|
|
81
|
+
const keys = user.toolbarKeys
|
|
82
|
+
const hasMd = keys.some(
|
|
83
|
+
(k: any) => k === 'importMarkdown' || k?.key === 'importMarkdown'
|
|
84
|
+
)
|
|
85
|
+
const hasHtml = keys.some(
|
|
86
|
+
(k: any) => k === 'sourceCode' || k?.key === 'sourceCode'
|
|
87
|
+
)
|
|
88
|
+
if (hasMd && hasHtml) return user
|
|
89
|
+
const next = [...keys]
|
|
90
|
+
if (!hasMd) next.push('importMarkdown')
|
|
91
|
+
if (!hasHtml) next.push('sourceCode')
|
|
92
|
+
return { ...user, toolbarKeys: next }
|
|
93
|
+
}
|
|
94
|
+
const keys = enableImport
|
|
95
|
+
? DEFAULT_DUAL_TOOLBAR_KEYS
|
|
96
|
+
: DEFAULT_DUAL_TOOLBAR_KEYS.filter(
|
|
97
|
+
(k) => k !== 'importMarkdown' && k !== 'sourceCode'
|
|
98
|
+
)
|
|
99
|
+
return { ...user, toolbarKeys: keys }
|
|
100
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Boot } from '@mnykj/alleditor'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 工具栏「HTML」:导入 .html(外观对齐百度 UEditor)。
|
|
5
|
+
* 实际打开文件选择由 SourceCodeMenu.attrs.onImport 接到宿主。
|
|
6
|
+
*/
|
|
7
|
+
export class SourceCodeMenu {
|
|
8
|
+
title = '导入 HTML'
|
|
9
|
+
tag = 'button'
|
|
10
|
+
iconSvg =
|
|
11
|
+
'<svg viewBox="0 0 44 24" width="44" height="24"><text x="22" y="16" text-anchor="middle" font-size="12" font-weight="700" font-family="Segoe UI,Helvetica,Arial,sans-serif" fill="#1a73e8">HTML</text></svg>'
|
|
12
|
+
|
|
13
|
+
getValue() {
|
|
14
|
+
return ''
|
|
15
|
+
}
|
|
16
|
+
isActive() {
|
|
17
|
+
return !!(SourceCodeMenu.attrs as { active?: boolean }).active
|
|
18
|
+
}
|
|
19
|
+
isDisabled() {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
exec() {
|
|
23
|
+
const handler = SourceCodeMenu.attrs.onImport
|
|
24
|
+
if (typeof handler === 'function') handler()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static attrs: {
|
|
28
|
+
active: boolean
|
|
29
|
+
onImport: null | (() => void)
|
|
30
|
+
onToggle: null | (() => void)
|
|
31
|
+
} = {
|
|
32
|
+
active: false,
|
|
33
|
+
onImport: null,
|
|
34
|
+
onToggle: null,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 工具栏:导入 Markdown 文件(仅 .md)。
|
|
40
|
+
*/
|
|
41
|
+
export class ImportMarkdownMenu {
|
|
42
|
+
title = '导入 Markdown'
|
|
43
|
+
tag = 'button'
|
|
44
|
+
iconSvg =
|
|
45
|
+
'<svg viewBox="0 0 16 16"><path d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"></path></svg>'
|
|
46
|
+
|
|
47
|
+
getValue() {
|
|
48
|
+
return ''
|
|
49
|
+
}
|
|
50
|
+
isActive() {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
isDisabled() {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
exec() {
|
|
57
|
+
const handler = ImportMarkdownMenu.attrs.onImport
|
|
58
|
+
if (typeof handler === 'function') handler()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static attrs: {
|
|
62
|
+
onImport: null | (() => void)
|
|
63
|
+
} = {
|
|
64
|
+
onImport: null,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let menusRegistered = false
|
|
69
|
+
|
|
70
|
+
/** 注册导入菜单(幂等;HMR 安全) */
|
|
71
|
+
export function registerImportMenus() {
|
|
72
|
+
if (menusRegistered) return
|
|
73
|
+
try {
|
|
74
|
+
Boot.registerMenu({
|
|
75
|
+
key: 'sourceCode',
|
|
76
|
+
factory() {
|
|
77
|
+
return new SourceCodeMenu()
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
} catch {
|
|
81
|
+
/* 重复注册忽略 */
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
Boot.registerMenu({
|
|
85
|
+
key: 'importMarkdown',
|
|
86
|
+
factory() {
|
|
87
|
+
return new ImportMarkdownMenu()
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
} catch {
|
|
91
|
+
/* 重复注册忽略 */
|
|
92
|
+
}
|
|
93
|
+
menusRegistered = true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function bindImportMenuHandlers(handlers: {
|
|
97
|
+
onImportHtml?: null | (() => void)
|
|
98
|
+
onImportMarkdown?: null | (() => void)
|
|
99
|
+
onToggleSource?: null | (() => void)
|
|
100
|
+
}) {
|
|
101
|
+
if (handlers.onImportHtml !== undefined) {
|
|
102
|
+
SourceCodeMenu.attrs.onImport = handlers.onImportHtml
|
|
103
|
+
}
|
|
104
|
+
if (handlers.onToggleSource !== undefined) {
|
|
105
|
+
SourceCodeMenu.attrs.onToggle = handlers.onToggleSource
|
|
106
|
+
}
|
|
107
|
+
if (handlers.onImportMarkdown !== undefined) {
|
|
108
|
+
ImportMarkdownMenu.attrs.onImport = handlers.onImportMarkdown
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,23 @@ export type { UeCommandRunner, BindUeToolbarOptions, UeToolbarBinding } from './
|
|
|
16
16
|
|
|
17
17
|
export { DEFAULT_UEDITOR_PUBLIC_BASE } from './ueditorBase'
|
|
18
18
|
|
|
19
|
+
export {
|
|
20
|
+
SourceCodeMenu,
|
|
21
|
+
ImportMarkdownMenu,
|
|
22
|
+
registerImportMenus,
|
|
23
|
+
bindImportMenuHandlers,
|
|
24
|
+
} from './importMenus'
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_DUAL_TOOLBAR_KEYS,
|
|
27
|
+
mergeToolbarConfig,
|
|
28
|
+
} from './defaultToolbar'
|
|
29
|
+
export {
|
|
30
|
+
markdownToHtml,
|
|
31
|
+
extractHtmlBody,
|
|
32
|
+
isEffectivelyEmptyHtml,
|
|
33
|
+
} from './markdown'
|
|
34
|
+
export { stripJavascriptFromHtml } from './stripJavascriptFromHtml'
|
|
35
|
+
|
|
19
36
|
// 协议工具从核心包再导出,方便只装 dual 的调用方
|
|
20
37
|
export type { AllEditorEngine } from '@mnykj/alleditor'
|
|
21
38
|
export {
|
package/src/markdown.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** @type {import('markdown-it') | null} */
|
|
2
|
+
let markdownItInstance: any = null
|
|
3
|
+
|
|
4
|
+
export async function markdownToHtml(text: string): Promise<string> {
|
|
5
|
+
if (!markdownItInstance) {
|
|
6
|
+
const mod = await import('markdown-it')
|
|
7
|
+
const MarkdownIt = mod.default || mod
|
|
8
|
+
markdownItInstance = new MarkdownIt({
|
|
9
|
+
html: false,
|
|
10
|
+
linkify: true,
|
|
11
|
+
breaks: true,
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
let html = markdownItInstance.render(text || '')
|
|
15
|
+
if (!String(html).trim()) html = '<p><br></p>'
|
|
16
|
+
return html
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 从完整 HTML 文档取出 body 内片段 */
|
|
20
|
+
export function extractHtmlBody(text: string): string {
|
|
21
|
+
let s = String(text || '').replace(/^\uFEFF/, '')
|
|
22
|
+
const bodyMatch = s.match(/<body[^>]*>([\s\S]*?)<\/body>/i)
|
|
23
|
+
if (bodyMatch) s = bodyMatch[1]
|
|
24
|
+
return s.trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isEffectivelyEmptyHtml(html: string): boolean {
|
|
28
|
+
const s = String(html ?? '').trim()
|
|
29
|
+
if (!s) return true
|
|
30
|
+
if (
|
|
31
|
+
/<(img|video|audio|iframe|embed|object|source|hr|table|svg|blockquote|pre|ul|ol|h[1-6])\b/i.test(
|
|
32
|
+
s
|
|
33
|
+
)
|
|
34
|
+
) {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
const text = s
|
|
38
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
39
|
+
.replace(/ /gi, ' ')
|
|
40
|
+
.replace(/<br\s*\/?>/gi, '')
|
|
41
|
+
.replace(/<p>\s*<\/p>/gi, '')
|
|
42
|
+
.replace(/<div[^>]*>\s*<\/div>/gi, '')
|
|
43
|
+
.replace(/<[^>]+>/g, '')
|
|
44
|
+
.trim()
|
|
45
|
+
return text === ''
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导入 HTML 时剥离 JS:script、事件属性、javascript: 伪协议。
|
|
3
|
+
*/
|
|
4
|
+
export function stripJavascriptFromHtml(html: string): string {
|
|
5
|
+
let s = String(html ?? '')
|
|
6
|
+
|
|
7
|
+
const open = '<' + 'script'
|
|
8
|
+
const close = '<' + '/' + 'script>'
|
|
9
|
+
const blockRe = new RegExp(`${open}\\b[^>]*>[\\s\\S]*?${close}`, 'gi')
|
|
10
|
+
const selfRe = new RegExp(`${open}\\b[^>]*\\/?>`, 'gi')
|
|
11
|
+
|
|
12
|
+
s = s.replace(blockRe, '')
|
|
13
|
+
s = s.replace(selfRe, '')
|
|
14
|
+
|
|
15
|
+
s = s.replace(/\s+on[a-zA-Z]+\s*=\s*(['"])[\s\S]*?\1/g, '')
|
|
16
|
+
s = s.replace(/\s+on[a-zA-Z]+\s*=\s*[^\s>]+/g, '')
|
|
17
|
+
|
|
18
|
+
s = s.replace(
|
|
19
|
+
/\s(href|src|action|formaction|xlink:href)\s*=\s*(['"])\s*javascript:[\s\S]*?\2/gi,
|
|
20
|
+
' $1=$2#$2'
|
|
21
|
+
)
|
|
22
|
+
s = s.replace(
|
|
23
|
+
/\s(href|src|action|formaction|xlink:href)\s*=\s*(['"])?\s*javascript:[^\s>]*/gi,
|
|
24
|
+
' $1="#"'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return s
|
|
28
|
+
}
|