@mnykj/alleditor-dual 1.1.2 → 1.2.1

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 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,59 +1,62 @@
1
- {
2
- "name": "@mnykj/alleditor-dual",
3
- "version": "1.1.2",
4
- "description": "AllEditor dual-engine (wang + UE HTML) Vue3 components, with bundled UEditor static assets",
5
- "author": "mnykj",
6
- "license": "MIT",
7
- "type": "module",
8
- "main": "./src/index.ts",
9
- "module": "./src/index.ts",
10
- "types": "./src/index.ts",
11
- "exports": {
12
- ".": "./src/index.ts",
13
- "./UeHtmlEngine.vue": "./src/UeHtmlEngine.vue",
14
- "./DualEditor.vue": "./src/DualEditor.vue",
15
- "./vite": "./vite.ts",
16
- "./nuxt": "./nuxt.ts",
17
- "./ueditor/*": "./ueditor/*"
18
- },
19
- "files": [
20
- "src",
21
- "ueditor",
22
- "vite.ts",
23
- "nuxt.ts",
24
- "README.md",
25
- "scripts"
26
- ],
27
- "keywords": [
28
- "AllEditor",
29
- "alleditor",
30
- "dual-engine",
31
- "ueditor",
32
- "vue3",
33
- "mnykj"
34
- ],
35
- "publishConfig": {
36
- "access": "public",
37
- "registry": "https://registry.npmjs.org/"
38
- },
39
- "peerDependencies": {
40
- "@mnykj/alleditor": "^1.1.1",
41
- "@mnykj/alleditor-for-vue": "^1.1.0",
42
- "vue": "^3.0.5",
43
- "vite": ">=4",
44
- "@nuxt/kit": ">=3"
45
- },
46
- "peerDependenciesMeta": {
47
- "vite": {
48
- "optional": true
49
- },
50
- "@nuxt/kit": {
51
- "optional": true
52
- }
53
- },
54
- "scripts": {
55
- "sync:ueditor": "node ./scripts/sync-ueditor.mjs",
56
- "prepack": "node ./scripts/sync-ueditor.mjs",
57
- "build": "node -e \"console.log('@mnykj/alleditor-dual ships Vue/TS source + ueditor assets; no bundle step')\""
58
- }
59
- }
1
+ {
2
+ "name": "@mnykj/alleditor-dual",
3
+ "version": "1.2.1",
4
+ "description": "AllEditor dual-engine (wang + UE HTML) Vue3 components, with bundled UEditor static assets and MD/HTML import menus",
5
+ "author": "mnykj",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./src/index.ts",
9
+ "module": "./src/index.ts",
10
+ "types": "./src/index.ts",
11
+ "exports": {
12
+ ".": "./src/index.ts",
13
+ "./UeHtmlEngine.vue": "./src/UeHtmlEngine.vue",
14
+ "./DualEditor.vue": "./src/DualEditor.vue",
15
+ "./vite": "./vite.ts",
16
+ "./nuxt": "./nuxt.ts",
17
+ "./ueditor/*": "./ueditor/*"
18
+ },
19
+ "files": [
20
+ "src",
21
+ "ueditor",
22
+ "vite.ts",
23
+ "nuxt.ts",
24
+ "README.md",
25
+ "scripts"
26
+ ],
27
+ "keywords": [
28
+ "AllEditor",
29
+ "alleditor",
30
+ "dual-engine",
31
+ "ueditor",
32
+ "vue3",
33
+ "mnykj"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "registry": "https://registry.npmjs.org/"
38
+ },
39
+ "dependencies": {
40
+ "markdown-it": "^14.3.3"
41
+ },
42
+ "peerDependencies": {
43
+ "@mnykj/alleditor": "^1.1.1",
44
+ "@mnykj/alleditor-for-vue": "^1.1.0",
45
+ "@nuxt/kit": ">=3",
46
+ "vite": ">=4",
47
+ "vue": "^3.0.5"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "vite": {
51
+ "optional": true
52
+ },
53
+ "@nuxt/kit": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "scripts": {
58
+ "sync:ueditor": "node ./scripts/sync-ueditor.mjs",
59
+ "prepack": "node ./scripts/sync-ueditor.mjs",
60
+ "build": "node -e \"console.log('@mnykj/alleditor-dual ships Vue/TS source + ueditor assets; no bundle step')\""
61
+ }
62
+ }
@@ -2,13 +2,15 @@
2
2
  /**
3
3
  * AllEditor 双引擎门面:按 HTML 协议自动选 wang / ue,共用 wang 工具栏。
4
4
  *
5
- * v-model 存「可入库」HTML(UE 模式会包 data-alleditor-engine 根节点)。
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'>} */
@@ -53,13 +71,26 @@ const engineMode = ref(detectEditorEngine(props.modelValue))
53
71
  const rootRef = ref(null)
54
72
  const wangEditorRef = shallowRef(null)
55
73
  const ueEngineRef = shallowRef(null)
56
- const wangHtml = ref(props.wangPlaceholder)
57
- const ueHtml = ref('')
74
+ // 首帧即用正文初始化,避免先挂占位再 setHtml 时带着失效选区炸 Slate
75
+ const wangHtml = ref(
76
+ engineMode.value === 'wang'
77
+ ? unwrapEditorEngineHtml(props.modelValue || '') || props.wangPlaceholder
78
+ : props.wangPlaceholder
79
+ )
80
+ const ueHtml = ref(
81
+ engineMode.value === 'ue' ? unwrapEditorEngineHtml(props.modelValue || '') : ''
82
+ )
83
+ const mdFileInputRef = ref(null)
84
+ const htmlFileInputRef = ref(null)
58
85
  let toolbarBinding = null
59
86
  let applyingExternal = false
60
87
 
61
88
  const useUe = computed(() => engineMode.value === 'ue')
62
89
 
90
+ const resolvedToolbarConfig = computed(() =>
91
+ mergeToolbarConfig(props.toolbarConfig, { enableImport: props.enableImport })
92
+ )
93
+
63
94
  function bodyOf(html) {
64
95
  return unwrapEditorEngineHtml(html || '') || '<p><br></p>'
65
96
  }
@@ -180,6 +211,96 @@ function setEngineMode(mode, html) {
180
211
  : body,
181
212
  { forceEngine: next }
182
213
  )
214
+ if (next === 'ue' || next === 'wang') {
215
+ emitModel(body)
216
+ }
217
+ }
218
+
219
+ function currentBodyForEmptyCheck() {
220
+ return bodyOf(getHtml())
221
+ }
222
+
223
+ function confirmReplaceIfNeeded() {
224
+ if (!props.confirmReplaceOnImport) return true
225
+ if (isEffectivelyEmptyHtml(currentBodyForEmptyCheck())) return true
226
+ return window.confirm('导入将替换当前正文内容,是否继续?')
227
+ }
228
+
229
+ function openMarkdownPicker() {
230
+ if (props.disabled) return
231
+ mdFileInputRef.value?.click?.()
232
+ }
233
+
234
+ function openHtmlPicker() {
235
+ if (props.disabled) return
236
+ htmlFileInputRef.value?.click?.()
237
+ }
238
+
239
+ async function onMarkdownFileSelected(e) {
240
+ const file = e.target?.files?.[0]
241
+ if (e.target) e.target.value = ''
242
+ if (!file) return
243
+ if (!/\.md$/i.test(file.name)) {
244
+ window.alert('请选择 .md 文件')
245
+ return
246
+ }
247
+ if (!confirmReplaceIfNeeded()) return
248
+ try {
249
+ const text = await file.text()
250
+ const html = await markdownToHtml(text || '')
251
+ setEngineMode('wang', html)
252
+ emit('import', {
253
+ type: 'markdown',
254
+ fileName: file.name,
255
+ html: getHtml(),
256
+ })
257
+ } catch (err) {
258
+ console.error(err)
259
+ window.alert(err?.message || '导入 Markdown 失败')
260
+ }
261
+ }
262
+
263
+ async function onHtmlFileSelected(e) {
264
+ const file = e.target?.files?.[0]
265
+ if (e.target) e.target.value = ''
266
+ if (!file) return
267
+ const nameOk =
268
+ /\.html?$/i.test(file.name) ||
269
+ /text\/html/i.test(file.type || '')
270
+ if (!nameOk) {
271
+ window.alert('请选择 .html / .htm 文件')
272
+ return
273
+ }
274
+ if (!confirmReplaceIfNeeded()) return
275
+ try {
276
+ const text = await file.text()
277
+ const body = stripJavascriptFromHtml(extractHtmlBody(text || ''))
278
+ const html = body.trim() || '<p><br></p>'
279
+ setEngineMode('ue', html)
280
+ emit('import', {
281
+ type: 'html',
282
+ fileName: file.name,
283
+ html: getHtml(),
284
+ })
285
+ } catch (err) {
286
+ console.error(err)
287
+ window.alert(err?.message || '导入 HTML 失败')
288
+ }
289
+ }
290
+
291
+ function wireImportHandlers() {
292
+ if (!props.enableImport) {
293
+ bindImportMenuHandlers({
294
+ onImportHtml: null,
295
+ onImportMarkdown: null,
296
+ })
297
+ return
298
+ }
299
+ registerImportMenus()
300
+ bindImportMenuHandlers({
301
+ onImportHtml: openHtmlPicker,
302
+ onImportMarkdown: openMarkdownPicker,
303
+ })
183
304
  }
184
305
 
185
306
  watch(
@@ -207,10 +328,23 @@ watch(engineMode, (mode) => {
207
328
  else unbindToolbar()
208
329
  })
209
330
 
331
+ watch(
332
+ () => props.enableImport,
333
+ () => wireImportHandlers()
334
+ )
335
+
210
336
  applyIncoming(props.modelValue)
211
337
 
338
+ onMounted(() => {
339
+ wireImportHandlers()
340
+ })
341
+
212
342
  onBeforeUnmount(() => {
213
343
  unbindToolbar()
344
+ bindImportMenuHandlers({
345
+ onImportHtml: null,
346
+ onImportMarkdown: null,
347
+ })
214
348
  const ed = wangEditorRef.value
215
349
  if (ed && !ed.isDestroyed) {
216
350
  try {
@@ -228,6 +362,8 @@ defineExpose({
228
362
  setEngineMode,
229
363
  getWangEditor: () => wangEditorRef.value,
230
364
  getUeEngine: () => ueEngineRef.value,
365
+ openMarkdownPicker,
366
+ openHtmlPicker,
231
367
  engineMode,
232
368
  })
233
369
  </script>
@@ -237,7 +373,7 @@ defineExpose({
237
373
  <Toolbar
238
374
  v-if="wangEditorRef"
239
375
  :editor="wangEditorRef"
240
- :default-config="toolbarConfig"
376
+ :default-config="resolvedToolbarConfig"
241
377
  :mode="mode"
242
378
  class="alleditor-dual__toolbar editor-toolbar"
243
379
  />
@@ -266,6 +402,22 @@ defineExpose({
266
402
  />
267
403
  </div>
268
404
  </div>
405
+ <input
406
+ v-if="enableImport"
407
+ ref="mdFileInputRef"
408
+ type="file"
409
+ accept=".md,text/markdown"
410
+ class="alleditor-dual__file-input"
411
+ @change="onMarkdownFileSelected"
412
+ />
413
+ <input
414
+ v-if="enableImport"
415
+ ref="htmlFileInputRef"
416
+ type="file"
417
+ accept=".html,.htm,text/html"
418
+ class="alleditor-dual__file-input"
419
+ @change="onHtmlFileSelected"
420
+ />
269
421
  </div>
270
422
  </template>
271
423
 
@@ -327,4 +479,15 @@ defineExpose({
327
479
  min-height: 0;
328
480
  height: 100%;
329
481
  }
482
+
483
+ .alleditor-dual__file-input {
484
+ position: absolute !important;
485
+ width: 1px !important;
486
+ height: 1px !important;
487
+ padding: 0 !important;
488
+ margin: -1px !important;
489
+ overflow: hidden !important;
490
+ clip: rect(0, 0, 0, 0) !important;
491
+ border: 0 !important;
492
+ }
330
493
  </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 {
@@ -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(/&nbsp;/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
+ }