@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 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.1.2",
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
- "vue": "^3.0.5",
45
+ "@nuxt/kit": ">=3",
43
46
  "vite": ">=4",
44
- "@nuxt/kit": ">=3"
47
+ "vue": "^3.0.5"
45
48
  },
46
49
  "peerDependenciesMeta": {
47
50
  "vite": {
@@ -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'>} */
@@ -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="toolbarConfig"
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 {
@@ -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
+ }