@zzclub/pipeline 0.2.0 → 0.4.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 +366 -170
- package/package.json +4 -7
- package/src/adapter-loader.ts +138 -0
- package/src/adapter-types.ts +172 -0
- package/src/adapters/builtin-image-renderer.ts +353 -0
- package/src/adapters/builtin-markdown-renderer.ts +63 -0
- package/src/adapters/index.ts +2 -0
- package/src/args.test.ts +118 -0
- package/src/args.ts +90 -0
- package/src/cli.ts +51 -0
- package/src/commands/abandon.test.ts +104 -0
- package/src/commands/abandon.ts +202 -0
- package/src/commands/attach-body-images.ts +88 -0
- package/src/commands/attach-body.ts +86 -0
- package/src/commands/attach-newspic-spec.ts +41 -0
- package/src/commands/checkpoint.test.ts +91 -0
- package/src/commands/checkpoint.ts +66 -0
- package/src/commands/config.ts +106 -0
- package/src/commands/cos-upload.ts +62 -0
- package/src/commands/doctor.ts +66 -0
- package/src/commands/find-run.ts +55 -0
- package/src/commands/hermes-metrics.ts +226 -0
- package/src/commands/imgx.ts +60 -0
- package/src/commands/ingest-handoff.ts +509 -0
- package/src/commands/init.ts +126 -0
- package/src/commands/prepare-finalize.ts +348 -0
- package/src/commands/prepare.ts +310 -0
- package/src/commands/publish.ts +147 -0
- package/src/commands/reconcile.ts +36 -0
- package/src/commands/render.ts +244 -0
- package/src/commands/reset.test.ts +151 -0
- package/src/commands/reset.ts +159 -0
- package/src/commands/review.ts +77 -0
- package/src/commands/status.ts +68 -0
- package/src/commands/sync-blog.ts +39 -0
- package/src/commands/tasks.ts +50 -0
- package/src/commands/wechat-export.ts +68 -0
- package/src/config.ts +0 -0
- package/src/imgx/ascii-portrait.ts +77 -0
- package/src/imgx/assets/fonts/AlimamaShuHeiTi-Bold.ttf +0 -0
- package/src/imgx/assets/fonts/LXGWNeoZhiSongPlus.ttf +0 -0
- package/src/imgx/assets/fonts/LXGWWenKai-Regular.ttf +0 -0
- package/src/imgx/assets/icons/logo.png +0 -0
- package/src/imgx/assets/icons/logo.svg +8 -0
- package/{dist/assets → src}/imgx/assets/templates/longform-3-4.html +1 -1
- package/{dist/assets → src}/imgx/assets/templates/poster-3-4.html +69 -30
- package/{dist/assets → src}/imgx/assets/templates/wechat-cover-split.html +58 -27
- package/src/imgx/cli.ts +57 -0
- package/src/imgx/geometry.ts +68 -0
- package/src/imgx/index.ts +5 -0
- package/src/imgx/longform-theme.ts +243 -0
- package/src/imgx/poster-recipe.ts +81 -0
- package/src/imgx/pretext-adapter.ts +14 -0
- package/src/imgx/pretext-runtime.ts +61 -0
- package/src/imgx/references/ascii-portrait-3-4.md +59 -0
- package/src/imgx/references/longform-3-4.md +173 -0
- package/src/imgx/references/poster-3-4.md +87 -0
- package/src/imgx/references/tips-3-4.md +69 -0
- package/src/imgx/references/wechat-cover-split.md +89 -0
- package/src/imgx/references/x-like-posts.md +139 -0
- package/src/imgx/render-article.test.ts +53 -0
- package/src/imgx/render-article.ts +1364 -0
- package/src/imgx/render-ascii-portrait.ts +53 -0
- package/src/imgx/render-card.ts +232 -0
- package/src/imgx/render-x-like-posts.ts +235 -0
- package/src/imgx/runtime.ts +403 -0
- package/src/imgx/typographic-scale.test.ts +56 -0
- package/src/imgx/typographic-scale.ts +44 -0
- package/src/output.ts +632 -0
- package/src/plugins.ts +101 -0
- package/src/profiles.ts +149 -0
- package/src/providers/blog.test.ts +58 -0
- package/src/providers/blog.ts +154 -0
- package/src/providers/cos.ts +183 -0
- package/src/providers/index.test.ts +26 -0
- package/src/providers/index.ts +185 -0
- package/src/providers/wechat.test.ts +215 -0
- package/src/providers/wechat.ts +548 -0
- package/src/routes.ts +287 -0
- package/src/runtime-paths.ts +179 -0
- package/src/schema/config.ts +133 -0
- package/src/schema/state.ts +376 -0
- package/src/spawn.ts +103 -0
- package/src/state.ts +265 -0
- package/src/task-manager.ts +657 -0
- package/src/task-views.ts +222 -0
- package/src/text.test.ts +689 -0
- package/src/text.ts +683 -0
- package/src/wechat-preview/assets/fonts/SweiCurveLegCJKsc-Regular.woff +0 -0
- package/src/wechat-preview/browser/editor-export.css +241 -0
- package/src/wechat-preview/browser/editor-export.ts +103 -0
- package/src/wechat-preview/frontmatter-handler.ts +73 -0
- package/src/wechat-preview/index.test.ts +154 -0
- package/src/wechat-preview/index.ts +294 -0
- package/src/wechat-preview/themes.ts +141 -0
- package/src/wechat-preview/wechat-formatter.test.ts +288 -0
- package/src/wechat-preview/wechat-formatter.ts +1506 -0
- package/src/wechat-preview.test.ts +73 -0
- package/src/workflow-materials.test.ts +225 -0
- package/src/workflow-materials.ts +217 -0
- package/src/workflow.test.ts +2087 -0
- package/dist/assets/imgx/assets/icons/avatar_jinx_cartoon.jpg +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo-square.png +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo.jpg +0 -0
- package/dist/assets/imgx/assets/icons/fishbone-logo.png +0 -0
- package/dist/assets/imgx/assets/icons/openclaw-logo.svg +0 -22
- package/dist/assets/imgx/assets/icons/zzclub-logo-black.jpg +0 -0
- package/dist/assets/imgx/assets/icons/zzclub-logo-gray.svg +0 -25
- package/dist/cli.js +0 -432
- package/dist/node_modules/@chenglou/pretext/dist/analysis.d.ts +0 -33
- package/dist/node_modules/@chenglou/pretext/dist/analysis.js +0 -1063
- package/dist/node_modules/@chenglou/pretext/dist/bidi.d.ts +0 -1
- package/dist/node_modules/@chenglou/pretext/dist/bidi.js +0 -175
- package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.d.ts +0 -4
- package/dist/node_modules/@chenglou/pretext/dist/generated/bidi-data.js +0 -979
- package/dist/node_modules/@chenglou/pretext/dist/layout.d.ts +0 -70
- package/dist/node_modules/@chenglou/pretext/dist/layout.js +0 -496
- package/dist/node_modules/@chenglou/pretext/dist/line-break.d.ts +0 -36
- package/dist/node_modules/@chenglou/pretext/dist/line-break.js +0 -820
- package/dist/node_modules/@chenglou/pretext/dist/measurement.d.ts +0 -28
- package/dist/node_modules/@chenglou/pretext/dist/measurement.js +0 -219
- package/dist/node_modules/@chenglou/pretext/dist/rich-inline.d.ts +0 -51
- package/dist/node_modules/@chenglou/pretext/dist/rich-inline.js +0 -401
- /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.d.ts +0 -0
- /package/{dist/assets → src}/imgx/assets/browser/obstacle-flow.js +0 -0
- /package/{dist/assets → src}/imgx/assets/icons/clover.svg +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-3-4.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/ascii-portrait-tile.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/tips-3-4.html +0 -0
- /package/{dist/assets → src}/imgx/assets/templates/x-like-posts.html +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/.vite/manifest.json +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/SweiCurveLegCJKsc-Regular-B3Ut5bwH.woff +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/assets/style-BYNXnUvc.css +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/browser-dist/editor-export.js +0 -0
- /package/{dist/assets → src}/wechat-preview/assets/templates/export-shell.html +0 -0
|
@@ -0,0 +1,1506 @@
|
|
|
1
|
+
export interface WechatExportTheme {
|
|
2
|
+
containerStyle?: string
|
|
3
|
+
footerText?: string
|
|
4
|
+
footerStyle?: string
|
|
5
|
+
fontFamily?: string
|
|
6
|
+
bodyColor?: string
|
|
7
|
+
mutedColor?: string
|
|
8
|
+
primaryColor?: string
|
|
9
|
+
dividerColor?: string
|
|
10
|
+
blockquoteBorderColor?: string
|
|
11
|
+
bodyLineHeight?: string
|
|
12
|
+
bodyLetterSpacing?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function escapeHtml(value: string): string {
|
|
16
|
+
return value
|
|
17
|
+
.replaceAll('&', '&')
|
|
18
|
+
.replaceAll('<', '<')
|
|
19
|
+
.replaceAll('>', '>')
|
|
20
|
+
.replaceAll('"', '"')
|
|
21
|
+
.replaceAll("'", ''')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 微信公众号允许的标签白名单
|
|
25
|
+
const ALLOWED_TAGS = [
|
|
26
|
+
'h1',
|
|
27
|
+
'h2',
|
|
28
|
+
'h3',
|
|
29
|
+
'h4',
|
|
30
|
+
'h5',
|
|
31
|
+
'h6',
|
|
32
|
+
'p',
|
|
33
|
+
'blockquote',
|
|
34
|
+
'ul',
|
|
35
|
+
'ol',
|
|
36
|
+
'li',
|
|
37
|
+
'pre',
|
|
38
|
+
'code',
|
|
39
|
+
'span',
|
|
40
|
+
'strong',
|
|
41
|
+
'em',
|
|
42
|
+
'b',
|
|
43
|
+
'i',
|
|
44
|
+
'u',
|
|
45
|
+
's',
|
|
46
|
+
'del',
|
|
47
|
+
'a',
|
|
48
|
+
'img',
|
|
49
|
+
'br',
|
|
50
|
+
'hr',
|
|
51
|
+
'table',
|
|
52
|
+
'thead',
|
|
53
|
+
'tbody',
|
|
54
|
+
'tr',
|
|
55
|
+
'th',
|
|
56
|
+
'td',
|
|
57
|
+
'section',
|
|
58
|
+
'div', // div 会被转换为 section
|
|
59
|
+
'figure',
|
|
60
|
+
'figcaption',
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
// 允许的属性白名单
|
|
64
|
+
const ALLOWED_ATTRS: Record<string, string[]> = {
|
|
65
|
+
'*': ['style', 'data-code-block'],
|
|
66
|
+
'img': ['src', 'alt', 'width', 'height', 'data-src', 'data-ratio', 'data-w'],
|
|
67
|
+
'a': ['href', 'target', 'title'],
|
|
68
|
+
'th': ['colspan', 'rowspan', 'align'],
|
|
69
|
+
'td': ['colspan', 'rowspan', 'align'],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 需要提取的 CSS 属性白名单 (合并了 demo 中的 EffectCssAttrs)
|
|
73
|
+
const STYLE_WHITELIST = [
|
|
74
|
+
'font-family',
|
|
75
|
+
'font-size',
|
|
76
|
+
'font-weight',
|
|
77
|
+
'font-style',
|
|
78
|
+
'color',
|
|
79
|
+
'text-align',
|
|
80
|
+
'line-height',
|
|
81
|
+
'letter-spacing', // 字间距
|
|
82
|
+
'text-decoration',
|
|
83
|
+
'background-color',
|
|
84
|
+
'background-image',
|
|
85
|
+
'border',
|
|
86
|
+
'border-top',
|
|
87
|
+
'border-right',
|
|
88
|
+
'border-bottom',
|
|
89
|
+
'border-left',
|
|
90
|
+
'border-color',
|
|
91
|
+
'border-width',
|
|
92
|
+
'border-style',
|
|
93
|
+
'border-radius',
|
|
94
|
+
'padding',
|
|
95
|
+
'padding-top',
|
|
96
|
+
'padding-right',
|
|
97
|
+
'padding-bottom',
|
|
98
|
+
'padding-left',
|
|
99
|
+
'margin',
|
|
100
|
+
'margin-top',
|
|
101
|
+
'margin-right',
|
|
102
|
+
'margin-bottom',
|
|
103
|
+
'margin-left',
|
|
104
|
+
// 'display', // 移除 display,防止 flex 布局复制导致缺少对齐属性错乱
|
|
105
|
+
'width',
|
|
106
|
+
'max-width',
|
|
107
|
+
// 'height', // 移除 height,防止高度固定导致内容溢出
|
|
108
|
+
'list-style-type',
|
|
109
|
+
'list-style-position',
|
|
110
|
+
'white-space',
|
|
111
|
+
'word-break',
|
|
112
|
+
'overflow-x',
|
|
113
|
+
// 'vertical-align', // 移除 vertical-align,防止基线对齐问题
|
|
114
|
+
'box-sizing',
|
|
115
|
+
'text-size-adjust', // demo
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
// 排除的类名
|
|
119
|
+
const EXCLUDE_CLASS_LIST = [
|
|
120
|
+
'md-editor-copy-button', // 复制按钮
|
|
121
|
+
'md-editor-icon',
|
|
122
|
+
'md-editor-katex-inline', // 不支持 kateX 公式导出
|
|
123
|
+
'md-editor-katex-block',
|
|
124
|
+
// Crepe Image Block exclude
|
|
125
|
+
'operation',
|
|
126
|
+
'operation-item',
|
|
127
|
+
'image-resize-handle',
|
|
128
|
+
'image-edit',
|
|
129
|
+
// 'not-prose', // 排除不需要样式的元素
|
|
130
|
+
// milkedown里代码块tools
|
|
131
|
+
'cm-gutters',
|
|
132
|
+
'cm-layer',
|
|
133
|
+
'cm-announced',
|
|
134
|
+
'tools',
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
// 驼峰转连字符
|
|
138
|
+
// const camelCaseToHyphen = (str: string) => {
|
|
139
|
+
// return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
140
|
+
// }
|
|
141
|
+
|
|
142
|
+
// 缓存
|
|
143
|
+
// const htmlCache: Record<string, Record<string, string>> = {}
|
|
144
|
+
// const styleValueCache: Record<string, string> = {}
|
|
145
|
+
|
|
146
|
+
interface LinkReference {
|
|
147
|
+
text: string
|
|
148
|
+
url: string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface ImageDimensionInput {
|
|
152
|
+
attrWidth?: string | null
|
|
153
|
+
attrHeight?: string | null
|
|
154
|
+
styleWidth?: string | null
|
|
155
|
+
styleHeight?: string | null
|
|
156
|
+
dataHeight?: string | null
|
|
157
|
+
renderedWidth?: number | null
|
|
158
|
+
renderedHeight?: number | null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function normalizeCssLength(value?: string | null): string | null {
|
|
162
|
+
const trimmed = value?.trim()
|
|
163
|
+
if (!trimmed || trimmed === 'auto')
|
|
164
|
+
return null
|
|
165
|
+
if (/^\d+(?:\.\d+)?$/.test(trimmed))
|
|
166
|
+
return `${trimmed}px`
|
|
167
|
+
return trimmed
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function resolveImageDimensionStyles(input: ImageDimensionInput): string[] {
|
|
171
|
+
const styles: string[] = []
|
|
172
|
+
const renderedWidth = Number.isFinite(input.renderedWidth) ? input.renderedWidth ?? 0 : 0
|
|
173
|
+
const renderedHeight = Number.isFinite(input.renderedHeight) ? input.renderedHeight ?? 0 : 0
|
|
174
|
+
const explicitWidth = normalizeCssLength(input.attrWidth) ?? normalizeCssLength(input.styleWidth)
|
|
175
|
+
const explicitHeight
|
|
176
|
+
= normalizeCssLength(input.attrHeight)
|
|
177
|
+
?? normalizeCssLength(input.styleHeight)
|
|
178
|
+
?? normalizeCssLength(input.dataHeight)
|
|
179
|
+
|
|
180
|
+
if (explicitWidth) {
|
|
181
|
+
styles.push(`width: ${explicitWidth}`)
|
|
182
|
+
styles.push(`height: ${explicitHeight ?? 'auto'}`)
|
|
183
|
+
return styles
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (renderedWidth > 0) {
|
|
187
|
+
styles.push(`width: ${Math.round(renderedWidth)}px`)
|
|
188
|
+
styles.push('max-width: 100%')
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
styles.push('max-width: 100%')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (explicitHeight) {
|
|
195
|
+
styles.push(`height: ${explicitHeight}`)
|
|
196
|
+
}
|
|
197
|
+
else if (renderedHeight > 0) {
|
|
198
|
+
styles.push(`height: ${Math.round(renderedHeight)}px`)
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
styles.push('height: auto')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return styles
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function rewriteStyleDeclarations(
|
|
208
|
+
styleText: string,
|
|
209
|
+
replacements: Record<string, string>,
|
|
210
|
+
): string {
|
|
211
|
+
const entries = new Map<string, string>()
|
|
212
|
+
|
|
213
|
+
styleText
|
|
214
|
+
.split(';')
|
|
215
|
+
.map(part => part.trim())
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.forEach((part) => {
|
|
218
|
+
const separatorIndex = part.indexOf(':')
|
|
219
|
+
if (separatorIndex === -1)
|
|
220
|
+
return
|
|
221
|
+
const key = part.slice(0, separatorIndex).trim()
|
|
222
|
+
const value = part.slice(separatorIndex + 1).trim()
|
|
223
|
+
if (!key)
|
|
224
|
+
return
|
|
225
|
+
entries.set(key, value)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
Object.entries(replacements).forEach(([key, value]) => {
|
|
229
|
+
entries.set(key, value)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
return Array.from(entries.entries())
|
|
233
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
234
|
+
.join('; ')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function normalizeInlineEmphasisStyles(html: string): string {
|
|
238
|
+
return html.replace(
|
|
239
|
+
/<(strong|em|b|i)\b([^>]*?)style="([^"]*)"/g,
|
|
240
|
+
(_match, tagName: string, beforeStyle: string, styleText: string) => {
|
|
241
|
+
const normalized = rewriteStyleDeclarations(styleText, {
|
|
242
|
+
'font-size': 'inherit',
|
|
243
|
+
'line-height': 'inherit',
|
|
244
|
+
'letter-spacing': 'inherit',
|
|
245
|
+
})
|
|
246
|
+
return `<${tagName}${beforeStyle}style="${normalized}"`
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function normalizeLinkStyles(html: string): string {
|
|
252
|
+
return html.replace(
|
|
253
|
+
/<(span|a)\b([^>]*?)style="([^"]*text-decoration:\s*underline[^"]*)"/g,
|
|
254
|
+
(_match, tagName: string, beforeStyle: string, styleText: string) => {
|
|
255
|
+
const normalized = rewriteStyleDeclarations(styleText, {
|
|
256
|
+
'text-decoration': 'underline wavy',
|
|
257
|
+
})
|
|
258
|
+
return `<${tagName}${beforeStyle}style="${normalized}"`
|
|
259
|
+
},
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function normalizeHrStyles(html: string): string {
|
|
264
|
+
return html.replace(
|
|
265
|
+
/<hr\b([^>]*?)style="([^"]*)"/g,
|
|
266
|
+
(_match, beforeStyle: string, styleText: string) => {
|
|
267
|
+
const normalized = rewriteStyleDeclarations(styleText, {
|
|
268
|
+
'display': 'block',
|
|
269
|
+
'height': '0',
|
|
270
|
+
'padding': '0',
|
|
271
|
+
'padding-top': '0',
|
|
272
|
+
'padding-right': '0',
|
|
273
|
+
'padding-bottom': '0',
|
|
274
|
+
'padding-left': '0',
|
|
275
|
+
'margin': '2em 0',
|
|
276
|
+
'background-color': 'transparent',
|
|
277
|
+
'background-image': 'none',
|
|
278
|
+
'border': 'none',
|
|
279
|
+
'border-top': '1px solid #dadce0',
|
|
280
|
+
'overflow-x': 'visible',
|
|
281
|
+
'font-size': '0',
|
|
282
|
+
'line-height': '0',
|
|
283
|
+
'color': 'transparent',
|
|
284
|
+
})
|
|
285
|
+
return `<hr${beforeStyle}style="${normalized}"`
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface ParsedTagInfo {
|
|
291
|
+
tagName: string | null
|
|
292
|
+
isClosing: boolean
|
|
293
|
+
isSelfClosing: boolean
|
|
294
|
+
preservesWhitespace: boolean
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function parseHtmlTag(tagText: string): ParsedTagInfo {
|
|
298
|
+
const trimmed = tagText.trim()
|
|
299
|
+
if (!trimmed.startsWith('<') || trimmed.startsWith('<!--')) {
|
|
300
|
+
return {
|
|
301
|
+
tagName: null,
|
|
302
|
+
isClosing: false,
|
|
303
|
+
isSelfClosing: false,
|
|
304
|
+
preservesWhitespace: false,
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const isClosing = /^<\//.test(trimmed)
|
|
309
|
+
const nameMatch = trimmed.match(/^<\/?\s*([a-zA-Z0-9-]+)/)
|
|
310
|
+
const tagName = nameMatch?.[1]?.toLowerCase() ?? null
|
|
311
|
+
const isSelfClosing = /\/\s*>$/.test(trimmed) || ['img', 'br', 'hr', 'input', 'meta', 'link'].includes(tagName ?? '')
|
|
312
|
+
const preservesWhitespace = !isClosing && Boolean(
|
|
313
|
+
tagName && ['pre', 'code'].includes(tagName)
|
|
314
|
+
|| /\sdata-code-block=(["'])true\1/i.test(trimmed),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
tagName,
|
|
319
|
+
isClosing,
|
|
320
|
+
isSelfClosing,
|
|
321
|
+
preservesWhitespace,
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function minifyHtmlPreservingCodeBlocks(html: string): string {
|
|
326
|
+
const openStack: Array<{ tagName: string; preservesWhitespace: boolean }> = []
|
|
327
|
+
let result = ''
|
|
328
|
+
let index = 0
|
|
329
|
+
|
|
330
|
+
while (index < html.length) {
|
|
331
|
+
const nextTagIndex = html.indexOf('<', index)
|
|
332
|
+
|
|
333
|
+
if (nextTagIndex === -1) {
|
|
334
|
+
const tail = html.slice(index)
|
|
335
|
+
result += openStack.some(entry => entry.preservesWhitespace) || tail.trim() ? tail : ''
|
|
336
|
+
break
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const textChunk = html.slice(index, nextTagIndex)
|
|
340
|
+
if (openStack.some(entry => entry.preservesWhitespace) || textChunk.trim()) {
|
|
341
|
+
result += textChunk
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let tagEndIndex = nextTagIndex
|
|
345
|
+
let quoteChar: '"' | "'" | null = null
|
|
346
|
+
while (tagEndIndex < html.length) {
|
|
347
|
+
const char = html[tagEndIndex]
|
|
348
|
+
if (quoteChar) {
|
|
349
|
+
if (char === quoteChar) {
|
|
350
|
+
quoteChar = null
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else if (char === '"' || char === "'") {
|
|
354
|
+
quoteChar = char
|
|
355
|
+
}
|
|
356
|
+
else if (char === '>') {
|
|
357
|
+
break
|
|
358
|
+
}
|
|
359
|
+
tagEndIndex += 1
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const tagText = html.slice(nextTagIndex, Math.min(tagEndIndex + 1, html.length))
|
|
363
|
+
result += tagText
|
|
364
|
+
|
|
365
|
+
const tagInfo = parseHtmlTag(tagText)
|
|
366
|
+
if (tagInfo.tagName) {
|
|
367
|
+
if (tagInfo.isClosing) {
|
|
368
|
+
for (let stackIndex = openStack.length - 1; stackIndex >= 0; stackIndex -= 1) {
|
|
369
|
+
const entry = openStack[stackIndex]
|
|
370
|
+
openStack.pop()
|
|
371
|
+
if (entry?.tagName === tagInfo.tagName) {
|
|
372
|
+
break
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
else if (!tagInfo.isSelfClosing) {
|
|
377
|
+
openStack.push({
|
|
378
|
+
tagName: tagInfo.tagName,
|
|
379
|
+
preservesWhitespace: tagInfo.preservesWhitespace,
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
index = tagEndIndex + 1
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return result.trim()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function removeStyle(styles: string[], property: string): void {
|
|
391
|
+
for (let index = styles.length - 1; index >= 0; index -= 1) {
|
|
392
|
+
if (styles[index]?.startsWith(`${property}:`)) {
|
|
393
|
+
styles.splice(index, 1)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function setStyle(styles: string[], property: string, value: string): void {
|
|
399
|
+
removeStyle(styles, property)
|
|
400
|
+
styles.push(`${property}: ${value}`)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isCodeContextElement(el: HTMLElement | null | undefined): boolean {
|
|
404
|
+
if (!el)
|
|
405
|
+
return false
|
|
406
|
+
|
|
407
|
+
return el.hasAttribute('data-code-block')
|
|
408
|
+
|| el.classList.contains('milkdown-code-block')
|
|
409
|
+
|| el.classList.contains('cm-editor')
|
|
410
|
+
|| el.classList.contains('cm-content')
|
|
411
|
+
|| el.classList.contains('cm-line')
|
|
412
|
+
|| el.classList.contains('cm-lineWrapping')
|
|
413
|
+
|| el.tagName.toLowerCase() === 'pre'
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function hasPreformattedComputedStyle(el: HTMLElement | null | undefined): boolean {
|
|
417
|
+
if (!el || typeof window === 'undefined' || typeof window.getComputedStyle !== 'function')
|
|
418
|
+
return false
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const computed = window.getComputedStyle(el)
|
|
422
|
+
const whiteSpace = (computed.whiteSpace || '').toLowerCase()
|
|
423
|
+
return whiteSpace.startsWith('pre')
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return false
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function hasBlockCodeAncestor(el: HTMLElement | null | undefined): boolean {
|
|
431
|
+
let current = el
|
|
432
|
+
while (current) {
|
|
433
|
+
if (isCodeContextElement(current) || hasPreformattedComputedStyle(current))
|
|
434
|
+
return true
|
|
435
|
+
current = current.parentElement
|
|
436
|
+
}
|
|
437
|
+
return false
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function normalizeTypography(
|
|
441
|
+
styles: string[],
|
|
442
|
+
originalTagName: string,
|
|
443
|
+
outTagName: string,
|
|
444
|
+
theme: WechatExportTheme,
|
|
445
|
+
el: HTMLElement,
|
|
446
|
+
isInCodeBlockContext: boolean,
|
|
447
|
+
): void {
|
|
448
|
+
const fontFamily = theme.fontFamily
|
|
449
|
+
?? "'SweiCurveLeg', 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
|
|
450
|
+
const bodyColor = theme.bodyColor ?? '#2f3a33'
|
|
451
|
+
const mutedColor = theme.mutedColor ?? '#66756b'
|
|
452
|
+
const primaryColor = theme.primaryColor ?? '#486b58'
|
|
453
|
+
const dividerColor = theme.dividerColor ?? '#d8e0da'
|
|
454
|
+
const blockquoteBorderColor = theme.blockquoteBorderColor ?? '#9cb2a3'
|
|
455
|
+
const bodyLineHeight = theme.bodyLineHeight ?? '1.92'
|
|
456
|
+
const bodyLetterSpacing = theme.bodyLetterSpacing ?? '0.03em'
|
|
457
|
+
const parentTagName = el.parentElement?.tagName.toLowerCase()
|
|
458
|
+
const isInsideCodeBlock = isInCodeBlockContext || hasBlockCodeAncestor(el)
|
|
459
|
+
|
|
460
|
+
const clearTextFrame = () => {
|
|
461
|
+
removeStyle(styles, 'width')
|
|
462
|
+
removeStyle(styles, 'max-width')
|
|
463
|
+
removeStyle(styles, 'font-family')
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (isInsideCodeBlock && (outTagName === 'span' || outTagName === 'strong' || outTagName === 'em' || outTagName === 'b' || outTagName === 'i' || outTagName === 'u' || outTagName === 's' || outTagName === 'del')) {
|
|
467
|
+
clearTextFrame()
|
|
468
|
+
setStyle(styles, 'font-family', 'inherit')
|
|
469
|
+
setStyle(styles, 'font-size', 'inherit')
|
|
470
|
+
setStyle(styles, 'line-height', 'inherit')
|
|
471
|
+
setStyle(styles, 'letter-spacing', 'inherit')
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (originalTagName === 'a' || outTagName === 'a') {
|
|
476
|
+
clearTextFrame()
|
|
477
|
+
setStyle(styles, 'font-family', fontFamily)
|
|
478
|
+
setStyle(styles, 'font-size', 'inherit')
|
|
479
|
+
setStyle(styles, 'color', primaryColor)
|
|
480
|
+
setStyle(styles, 'text-decoration', 'underline wavy')
|
|
481
|
+
setStyle(styles, 'line-height', bodyLineHeight)
|
|
482
|
+
setStyle(styles, 'letter-spacing', bodyLetterSpacing)
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(originalTagName)) {
|
|
487
|
+
clearTextFrame()
|
|
488
|
+
setStyle(styles, 'font-family', fontFamily)
|
|
489
|
+
setStyle(styles, 'font-weight', originalTagName === 'h3' ? '600' : '700')
|
|
490
|
+
setStyle(styles, 'color', originalTagName === 'h1' ? bodyColor : primaryColor)
|
|
491
|
+
|
|
492
|
+
if (originalTagName === 'h1') {
|
|
493
|
+
setStyle(styles, 'font-size', '2.1em')
|
|
494
|
+
setStyle(styles, 'line-height', '1.32')
|
|
495
|
+
setStyle(styles, 'letter-spacing', '-0.02em')
|
|
496
|
+
setStyle(styles, 'margin', '1.6em 0 0.7em')
|
|
497
|
+
}
|
|
498
|
+
else if (originalTagName === 'h2') {
|
|
499
|
+
setStyle(styles, 'font-size', '18px')
|
|
500
|
+
setStyle(styles, 'line-height', '1.4')
|
|
501
|
+
setStyle(styles, 'letter-spacing', '-0.01em')
|
|
502
|
+
setStyle(styles, 'margin', '1.45em 0 0.62em')
|
|
503
|
+
}
|
|
504
|
+
else if (originalTagName === 'h3') {
|
|
505
|
+
setStyle(styles, 'font-size', '1.14em')
|
|
506
|
+
setStyle(styles, 'line-height', '1.58')
|
|
507
|
+
setStyle(styles, 'letter-spacing', '0.01em')
|
|
508
|
+
setStyle(styles, 'margin', '1.28em 0 0.52em')
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
setStyle(styles, 'font-size', '1em')
|
|
512
|
+
setStyle(styles, 'line-height', '1.65')
|
|
513
|
+
setStyle(styles, 'letter-spacing', '0.02em')
|
|
514
|
+
setStyle(styles, 'margin', '1.15em 0 0.45em')
|
|
515
|
+
}
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (outTagName === 'p') {
|
|
520
|
+
clearTextFrame()
|
|
521
|
+
setStyle(styles, 'font-family', fontFamily)
|
|
522
|
+
setStyle(styles, 'font-size', parentTagName === 'blockquote' ? '15px' : '16px')
|
|
523
|
+
setStyle(styles, 'color', parentTagName === 'blockquote' ? mutedColor : bodyColor)
|
|
524
|
+
setStyle(styles, 'line-height', bodyLineHeight)
|
|
525
|
+
setStyle(styles, 'letter-spacing', bodyLetterSpacing)
|
|
526
|
+
|
|
527
|
+
if (parentTagName === 'td' || parentTagName === 'th' || parentTagName === 'blockquote') {
|
|
528
|
+
setStyle(styles, 'margin', '0')
|
|
529
|
+
}
|
|
530
|
+
else if (!(el.parentElement && (el.parentElement.classList.contains('content-dom') || el.parentElement.closest('.list-item')))) {
|
|
531
|
+
setStyle(styles, 'margin', '1.12em 0')
|
|
532
|
+
}
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (outTagName === 'blockquote') {
|
|
537
|
+
clearTextFrame()
|
|
538
|
+
setStyle(styles, 'font-family', fontFamily)
|
|
539
|
+
setStyle(styles, 'color', mutedColor)
|
|
540
|
+
setStyle(styles, 'line-height', bodyLineHeight)
|
|
541
|
+
setStyle(styles, 'letter-spacing', bodyLetterSpacing)
|
|
542
|
+
setStyle(styles, 'margin', '1.35em 0')
|
|
543
|
+
setStyle(styles, 'padding-left', '16px')
|
|
544
|
+
setStyle(styles, 'border-left', `4px solid ${blockquoteBorderColor}`)
|
|
545
|
+
setStyle(styles, 'background-color', 'rgba(72, 107, 88, 0.04)')
|
|
546
|
+
return
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (outTagName === 'ul' || outTagName === 'ol') {
|
|
550
|
+
clearTextFrame()
|
|
551
|
+
setStyle(styles, 'font-family', fontFamily)
|
|
552
|
+
setStyle(styles, 'color', bodyColor)
|
|
553
|
+
setStyle(styles, 'line-height', bodyLineHeight)
|
|
554
|
+
setStyle(styles, 'letter-spacing', bodyLetterSpacing)
|
|
555
|
+
setStyle(styles, 'margin', '1em 0')
|
|
556
|
+
setStyle(styles, 'padding-left', '1.2em')
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (outTagName === 'span' || outTagName === 'strong' || outTagName === 'em' || outTagName === 'b' || outTagName === 'i' || outTagName === 'u' || outTagName === 's' || outTagName === 'del') {
|
|
561
|
+
clearTextFrame()
|
|
562
|
+
setStyle(styles, 'font-family', fontFamily)
|
|
563
|
+
setStyle(styles, 'font-size', 'inherit')
|
|
564
|
+
setStyle(styles, 'line-height', bodyLineHeight)
|
|
565
|
+
setStyle(styles, 'letter-spacing', bodyLetterSpacing)
|
|
566
|
+
if (outTagName === 'strong' || outTagName === 'b') {
|
|
567
|
+
setStyle(styles, 'font-weight', '700')
|
|
568
|
+
setStyle(styles, 'color', bodyColor)
|
|
569
|
+
}
|
|
570
|
+
if (outTagName === 'em' || outTagName === 'i') {
|
|
571
|
+
setStyle(styles, 'font-style', 'italic')
|
|
572
|
+
}
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (outTagName === 'hr') {
|
|
577
|
+
clearTextFrame()
|
|
578
|
+
removeStyle(styles, 'padding')
|
|
579
|
+
removeStyle(styles, 'padding-top')
|
|
580
|
+
removeStyle(styles, 'padding-right')
|
|
581
|
+
removeStyle(styles, 'padding-bottom')
|
|
582
|
+
removeStyle(styles, 'padding-left')
|
|
583
|
+
removeStyle(styles, 'background-color')
|
|
584
|
+
removeStyle(styles, 'background-image')
|
|
585
|
+
removeStyle(styles, 'font-size')
|
|
586
|
+
removeStyle(styles, 'line-height')
|
|
587
|
+
removeStyle(styles, 'color')
|
|
588
|
+
removeStyle(styles, 'overflow-x')
|
|
589
|
+
removeStyle(styles, 'border-radius')
|
|
590
|
+
setStyle(styles, 'border', 'none')
|
|
591
|
+
setStyle(styles, 'border-top', `1px solid ${dividerColor}`)
|
|
592
|
+
setStyle(styles, 'display', 'block')
|
|
593
|
+
setStyle(styles, 'height', '0')
|
|
594
|
+
setStyle(styles, 'padding', '0')
|
|
595
|
+
setStyle(styles, 'background-color', 'transparent')
|
|
596
|
+
setStyle(styles, 'overflow-x', 'visible')
|
|
597
|
+
setStyle(styles, 'margin', '2em 0')
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (outTagName === 'table') {
|
|
602
|
+
clearTextFrame()
|
|
603
|
+
setStyle(styles, 'font-family', fontFamily)
|
|
604
|
+
setStyle(styles, 'font-size', '15px')
|
|
605
|
+
setStyle(styles, 'line-height', '1.75')
|
|
606
|
+
setStyle(styles, 'letter-spacing', '0.02em')
|
|
607
|
+
setStyle(styles, 'width', '100%')
|
|
608
|
+
setStyle(styles, 'margin', '1.4em 0')
|
|
609
|
+
setStyle(styles, 'border-collapse', 'collapse')
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (outTagName === 'th' || outTagName === 'td') {
|
|
614
|
+
clearTextFrame()
|
|
615
|
+
setStyle(styles, 'font-family', fontFamily)
|
|
616
|
+
setStyle(styles, 'font-size', '15px')
|
|
617
|
+
setStyle(styles, 'line-height', '1.72')
|
|
618
|
+
setStyle(styles, 'letter-spacing', '0.02em')
|
|
619
|
+
setStyle(styles, 'padding', '10px 12px')
|
|
620
|
+
setStyle(styles, 'border', `1px solid ${dividerColor}`)
|
|
621
|
+
if (outTagName === 'th') {
|
|
622
|
+
setStyle(styles, 'font-weight', '700')
|
|
623
|
+
setStyle(styles, 'background-color', 'rgba(72, 107, 88, 0.06)')
|
|
624
|
+
setStyle(styles, 'color', primaryColor)
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
setStyle(styles, 'color', bodyColor)
|
|
628
|
+
setStyle(styles, 'background-color', '#ffffff')
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* 生成自定义的上标 HTML (用于替代 sup 标签)
|
|
635
|
+
*/
|
|
636
|
+
function getSupHtml(content: string): string {
|
|
637
|
+
// 微信公众号不支持伪元素(inline style无法定义),且 position: relative 兼容性不佳
|
|
638
|
+
// 改用 vertical-align: super 配合 line-height: 0
|
|
639
|
+
return `<span style="font-size: 0.75em; vertical-align: super; line-height: 0; margin: 0 2px;">${content}</span>`
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* 获取单个 DOM 的带内联样式的 HTML (所见即所得模式)
|
|
644
|
+
* @param node 要处理的节点
|
|
645
|
+
* @param references 链接引用数组
|
|
646
|
+
* @param targetStyles 目标样式属性列表
|
|
647
|
+
* @param isInCodeBlock 是否在代码块内 (用于空格处理)
|
|
648
|
+
*/
|
|
649
|
+
function getOneDomCssStyle(
|
|
650
|
+
node: Node,
|
|
651
|
+
references: LinkReference[] = [],
|
|
652
|
+
theme: WechatExportTheme = {},
|
|
653
|
+
targetStyles?: string[],
|
|
654
|
+
isInCodeBlock: boolean = false,
|
|
655
|
+
): string {
|
|
656
|
+
if (!node)
|
|
657
|
+
return ''
|
|
658
|
+
|
|
659
|
+
// 文本节点
|
|
660
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
661
|
+
const text = node.nodeValue || ''
|
|
662
|
+
const textParent = node.parentElement
|
|
663
|
+
const shouldPreserveWhitespace = isInCodeBlock || hasBlockCodeAncestor(textParent)
|
|
664
|
+
// 在代码块内,保留所有空格和换行
|
|
665
|
+
// 将所有空格转换为 以防止被 HTML 合并
|
|
666
|
+
if (shouldPreserveWhitespace && text) {
|
|
667
|
+
return text
|
|
668
|
+
.replace(/ /g, '\u00A0') // 所有空格转为
|
|
669
|
+
.replace(/\t/g, '\u00A0\u00A0\u00A0\u00A0') // 制表符转为 4 个
|
|
670
|
+
}
|
|
671
|
+
return text
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// 注释节点
|
|
675
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
676
|
+
return ''
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// 只处理元素节点
|
|
680
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
681
|
+
return ''
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const el = node as HTMLElement
|
|
685
|
+
const tagName = el.tagName.toLowerCase()
|
|
686
|
+
|
|
687
|
+
// 标记代码块容器,用于后续压缩时保护
|
|
688
|
+
let isCodeBlockContainer = false
|
|
689
|
+
if (el.classList.contains('milkdown-code-block')) {
|
|
690
|
+
isCodeBlockContainer = true
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// 判断当前是否进入代码块区域(用于子节点的空格保护)
|
|
694
|
+
// 包括 milkdown-code-block、CodeMirror 容器(cm-content、cm-editor)、以及传统的 pre/code
|
|
695
|
+
const currentIsInCodeBlock = isInCodeBlock
|
|
696
|
+
|| isCodeContextElement(el)
|
|
697
|
+
|| hasPreformattedComputedStyle(el)
|
|
698
|
+
|
|
699
|
+
// 降级兼容易处理:列表项 (LI) 统一转为 "- 文本" 形式
|
|
700
|
+
// 解决微信端列表渲染的各种对齐、间距和兼容性问题
|
|
701
|
+
if (tagName === 'li') {
|
|
702
|
+
let contentHtml = ''
|
|
703
|
+
let nestedListHtml = ''
|
|
704
|
+
|
|
705
|
+
// 辅助函数:提取子元素内容,尽可能扁平化 Block 元素 (P, DIV, SECTION) 为 Inline,以保持文本流
|
|
706
|
+
const extractInlineContent = (target: Node): string => {
|
|
707
|
+
let resHtml = ''
|
|
708
|
+
if (target.hasChildNodes()) {
|
|
709
|
+
Array.from(target.childNodes).forEach((child) => {
|
|
710
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
711
|
+
// 文本节点直接处理
|
|
712
|
+
resHtml += getOneDomCssStyle(child, references, theme, [], isInCodeBlock)
|
|
713
|
+
}
|
|
714
|
+
else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
715
|
+
const cEl = child as HTMLElement
|
|
716
|
+
const cTag = cEl.tagName.toLowerCase()
|
|
717
|
+
|
|
718
|
+
// 忽略图标容器
|
|
719
|
+
if (cEl.classList.contains('label-wrapper'))
|
|
720
|
+
return
|
|
721
|
+
|
|
722
|
+
// 嵌套列表稍后单独处理
|
|
723
|
+
if (['ul', 'ol'].includes(cTag))
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
// 遇到 Block 容器,递归提取内容 (剥离外壳)
|
|
727
|
+
if (['p', 'div', 'section', 'article', 'aside'].includes(cTag)) {
|
|
728
|
+
resHtml += extractInlineContent(child)
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
// Inline 元素 (span, strong, code, a...), 保留样式
|
|
732
|
+
resHtml += getOneDomCssStyle(child, references, theme, [], isInCodeBlock)
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
return resHtml
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// 1. 尝试获取 Milkdown/Crepe 的内容容器 (.content-dom)
|
|
741
|
+
const contentDom = el.querySelector('.content-dom')
|
|
742
|
+
if (contentDom) {
|
|
743
|
+
contentHtml = extractInlineContent(contentDom)
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
// 2. 非 Standard Milkdown 结构,直接处理 li 子元素
|
|
747
|
+
contentHtml = extractInlineContent(el)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// 3. 处理嵌套列表 (递归)
|
|
751
|
+
const nestedLists = el.querySelectorAll(':scope > ul, :scope > ol')
|
|
752
|
+
nestedLists.forEach((list) => {
|
|
753
|
+
nestedListHtml += getOneDomCssStyle(list, references, theme, [], isInCodeBlock)
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
// 构造最终 HTML - 文本缩进布局 (Hanging Indent)
|
|
757
|
+
// 摒弃 Flex,使用 margin/padding/text-indent 组合,兼容性最好
|
|
758
|
+
const rowStyle = [
|
|
759
|
+
'margin: 0.32em 0',
|
|
760
|
+
`line-height: ${theme.bodyLineHeight ?? '1.92'}`,
|
|
761
|
+
`color: ${theme.bodyColor ?? '#2f3a33'}`,
|
|
762
|
+
'font-size: 15px',
|
|
763
|
+
`font-family: ${theme.fontFamily ?? "'SweiCurveLeg', 'PingFang SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"}`,
|
|
764
|
+
'padding-left: 1.35em',
|
|
765
|
+
'text-indent: -0.92em',
|
|
766
|
+
`letter-spacing: ${theme.bodyLetterSpacing ?? '0.03em'}`,
|
|
767
|
+
].join('; ')
|
|
768
|
+
|
|
769
|
+
let rowHtml = ''
|
|
770
|
+
if (contentHtml.trim()) {
|
|
771
|
+
rowHtml = `<p style="${rowStyle}">- ${contentHtml}</p>`
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return rowHtml + nestedListHtml
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// 任务列表 checkbox 处理
|
|
778
|
+
if (tagName === 'input' && (el.getAttribute('type') === 'checkbox' || el.classList.contains('task-list-item-checkbox'))) {
|
|
779
|
+
const isChecked = el.hasAttribute('checked') || (el as HTMLInputElement).checked
|
|
780
|
+
// 使用 emoji 替代 checkbox
|
|
781
|
+
// 强制使用 inline-flex 并垂直居中,确保在 flex 容器中对齐
|
|
782
|
+
return `<span style="display: inline-flex; align-items: center; justify-content: center; margin-right: 6px; width: 1.2em; height: 1.2em; font-size: 1em;">${isChecked ? '✅' : '❌'}</span>`
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// sup 标签处理 (使用自定义样式替代)
|
|
786
|
+
if (tagName === 'sup') {
|
|
787
|
+
// 递归处理子元素以保留内部样式
|
|
788
|
+
let childrenHtml = ''
|
|
789
|
+
if (el.hasChildNodes()) {
|
|
790
|
+
Array.from(el.childNodes).forEach((child) => {
|
|
791
|
+
childrenHtml += getOneDomCssStyle(child, references, theme, undefined, currentIsInCodeBlock)
|
|
792
|
+
})
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
childrenHtml = el.innerHTML
|
|
796
|
+
}
|
|
797
|
+
return getSupHtml(childrenHtml)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Crepe 列表图标 (SVG) 处理 - 转为 Base64 图片
|
|
801
|
+
// 宽松匹配:只要包含了 SVG 的 milkdown-icon (bullet 或 label) 都进行处理
|
|
802
|
+
if (tagName === 'span' && el.classList.contains('milkdown-icon') && (el.classList.contains('bullet') || el.classList.contains('label'))) {
|
|
803
|
+
try {
|
|
804
|
+
const svg = el.querySelector('svg')
|
|
805
|
+
if (svg) {
|
|
806
|
+
const serializer = new XMLSerializer()
|
|
807
|
+
const svgString = serializer.serializeToString(svg)
|
|
808
|
+
// 修正颜色:强制使用黑色或深色
|
|
809
|
+
// 对于 checkbox (label),如果是 checked 状态,路径通常是 filled
|
|
810
|
+
// 这里统一将 currentColor 替换为 #333333
|
|
811
|
+
const fixedSvgString = svgString.replace(/currentColor/g, '#333333')
|
|
812
|
+
|
|
813
|
+
const base64 = window.btoa(unescape(encodeURIComponent(fixedSvgString)))
|
|
814
|
+
const imgSrc = `data:image/svg+xml;base64,${base64}`
|
|
815
|
+
|
|
816
|
+
// 根据类型调整尺寸和位置
|
|
817
|
+
let iconStyle = 'display: inline-block; flex-shrink: 0;'
|
|
818
|
+
|
|
819
|
+
// 普通行高假设为 1.6 * 16px ≈ 25.6px 或 24px。视实际编辑器样式而定。
|
|
820
|
+
// 这里按常见的 line-height: 1.6 (约 26px) 或 1.5 (24px) 来计算对齐。
|
|
821
|
+
// 假设行高为 24px。
|
|
822
|
+
|
|
823
|
+
if (el.classList.contains('label')) {
|
|
824
|
+
// Task list checkbox (增大至 22px)
|
|
825
|
+
iconStyle += 'width: 22px; height: 22px; margin-right: 4px;'
|
|
826
|
+
// 垂直偏移:(24 - 22) / 2 = 1px。
|
|
827
|
+
iconStyle += 'margin-top: 1px;'
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
// Bullet point (增大至 12px)
|
|
831
|
+
iconStyle += 'width: 12px; height: 12px; margin-right: 6px;'
|
|
832
|
+
// 垂直偏移:(24 - 12) / 2 = 6px。
|
|
833
|
+
iconStyle += 'margin-top: 6px;'
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// 返回 img
|
|
837
|
+
return `<img src="${imgSrc}" style="${iconStyle}" />`
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch (e) {
|
|
841
|
+
console.error('List icon conversion failed', e)
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Mermaid 图表处理 (转为图片)
|
|
846
|
+
if (el.classList.contains('md-editor-mermaid')) {
|
|
847
|
+
try {
|
|
848
|
+
const svg = el.querySelector('svg')
|
|
849
|
+
if (svg) {
|
|
850
|
+
// 获取 SVG 的实际渲染宽度,避免图片过大
|
|
851
|
+
const rect = svg.getBoundingClientRect()
|
|
852
|
+
const width = rect.width
|
|
853
|
+
|
|
854
|
+
const serializer = new XMLSerializer()
|
|
855
|
+
const svgString = serializer.serializeToString(svg)
|
|
856
|
+
// 处理 Unicode 字符
|
|
857
|
+
const base64 = window.btoa(unescape(encodeURIComponent(svgString)))
|
|
858
|
+
const imgSrc = `data:image/svg+xml;base64,${base64}`
|
|
859
|
+
|
|
860
|
+
// 设置宽度样式,如果获取到了有效宽度
|
|
861
|
+
const widthStyle = width ? `width: ${width}px;` : ''
|
|
862
|
+
return `<img src="${imgSrc}" style="display: block; margin: 14px auto; max-width: 100%; ${widthStyle} height: auto;" />`
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
catch (e) {
|
|
866
|
+
console.error('Mermaid SVG conversion failed:', e)
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// 忽略无效标签 (Crepe caption-input 除外)
|
|
871
|
+
if (['script', 'style', 'button', 'link', 'meta', 'input', 'textarea', 'select'].includes(tagName)) {
|
|
872
|
+
// 特殊处理 Crepe 图片描述输入框
|
|
873
|
+
if (tagName === 'input' && el.classList.contains('caption-input')) {
|
|
874
|
+
const val = (el as HTMLInputElement).value
|
|
875
|
+
if (!val)
|
|
876
|
+
return ''
|
|
877
|
+
// 返回居中的 figcaption
|
|
878
|
+
return `<figcaption style="margin-top: 6px; text-align: center; color: #888; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">${val}</figcaption>`
|
|
879
|
+
}
|
|
880
|
+
return ''
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// 检查排除的 class
|
|
884
|
+
if (el.classList && Array.from(el.classList).some(c => EXCLUDE_CLASS_LIST.includes(c))) {
|
|
885
|
+
return ''
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// 标签过滤
|
|
889
|
+
let outTagName = tagName
|
|
890
|
+
if (!ALLOWED_TAGS.includes(tagName)) {
|
|
891
|
+
if (el.hasChildNodes()) {
|
|
892
|
+
let childrenHtml = ''
|
|
893
|
+
Array.from(el.childNodes).forEach((child) => {
|
|
894
|
+
childrenHtml += getOneDomCssStyle(child, references, theme, undefined, currentIsInCodeBlock)
|
|
895
|
+
})
|
|
896
|
+
return childrenHtml
|
|
897
|
+
}
|
|
898
|
+
return ''
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// div -> section
|
|
902
|
+
if (outTagName === 'div') {
|
|
903
|
+
outTagName = 'section'
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// h1-h6 -> section (避免移动端编辑器覆盖标题标签的默认样式)
|
|
907
|
+
if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(outTagName)) {
|
|
908
|
+
outTagName = 'section'
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// 链接处理 (a -> span + sup)
|
|
912
|
+
let linkSupHtml = ''
|
|
913
|
+
if (outTagName === 'a') {
|
|
914
|
+
const href = el.getAttribute('href')
|
|
915
|
+
// 简单的外部链接判断 (包含 http 且不是锚点)
|
|
916
|
+
if (href && (href.startsWith('http') || href.startsWith('//'))) {
|
|
917
|
+
const text = el.textContent || ''
|
|
918
|
+
references.push({ text, url: href })
|
|
919
|
+
const index = references.length
|
|
920
|
+
linkSupHtml = getSupHtml(`[${index}]`)
|
|
921
|
+
// 将 a 标签转换为 span,保留样式但移除链接功能
|
|
922
|
+
outTagName = 'span'
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
// 获取计算样式
|
|
926
|
+
const computedStyle = window.getComputedStyle(el)
|
|
927
|
+
const styles: string[] = []
|
|
928
|
+
|
|
929
|
+
// 确定要提取的属性列表
|
|
930
|
+
const attrsToExtract = (targetStyles && targetStyles.length > 0) ? targetStyles : [...STYLE_WHITELIST]
|
|
931
|
+
|
|
932
|
+
// Crepe 列表项 (List Item) 特殊处理
|
|
933
|
+
if (el.classList.contains('list-item')) {
|
|
934
|
+
// 强制使用 flex 布局,确保图标/序号和内容在同一行
|
|
935
|
+
if (!attrsToExtract.includes('display'))
|
|
936
|
+
attrsToExtract.push('display')
|
|
937
|
+
if (!attrsToExtract.includes('align-items'))
|
|
938
|
+
attrsToExtract.push('align-items')
|
|
939
|
+
// 移除可能导致换行的属性
|
|
940
|
+
// flex-direction 默认为 row
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Crepe 列表 Label (序号/图标容器) 特殊处理
|
|
944
|
+
if (el.classList.contains('label-wrapper')) {
|
|
945
|
+
if (!attrsToExtract.includes('display'))
|
|
946
|
+
attrsToExtract.push('display')
|
|
947
|
+
// 确保不换行
|
|
948
|
+
if (!attrsToExtract.includes('white-space'))
|
|
949
|
+
attrsToExtract.push('white-space')
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Crepe Image Wrapper 特殊处理:强制居中对齐
|
|
953
|
+
if (el.classList.contains('image-wrapper')) {
|
|
954
|
+
// 强制使用 flex 居中
|
|
955
|
+
if (!attrsToExtract.includes('display'))
|
|
956
|
+
attrsToExtract.push('display')
|
|
957
|
+
if (!attrsToExtract.includes('justify-content'))
|
|
958
|
+
attrsToExtract.push('justify-content')
|
|
959
|
+
if (!attrsToExtract.includes('align-items'))
|
|
960
|
+
attrsToExtract.push('align-items')
|
|
961
|
+
if (!attrsToExtract.includes('flex-direction'))
|
|
962
|
+
attrsToExtract.push('flex-direction')
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// 特殊处理:对于任务列表项、代码块头部、代码块内容,保留 display 属性以维持布局
|
|
966
|
+
if (el.classList.contains('task-list-item')
|
|
967
|
+
|| el.classList.contains('md-editor-code-head')
|
|
968
|
+
|| el.classList.contains('md-editor-code-flag') // 圆点容器
|
|
969
|
+
|| el.classList.contains('md-editor-code-action') // 语言标识容器
|
|
970
|
+
|| tagName === 'pre'
|
|
971
|
+
|| tagName === 'code') {
|
|
972
|
+
if (!attrsToExtract.includes('display'))
|
|
973
|
+
attrsToExtract.push('display')
|
|
974
|
+
if (!attrsToExtract.includes('align-items'))
|
|
975
|
+
attrsToExtract.push('align-items')
|
|
976
|
+
if (!attrsToExtract.includes('justify-content'))
|
|
977
|
+
attrsToExtract.push('justify-content')
|
|
978
|
+
if (!attrsToExtract.includes('flex-direction'))
|
|
979
|
+
attrsToExtract.push('flex-direction')
|
|
980
|
+
if (!attrsToExtract.includes('flex-wrap'))
|
|
981
|
+
attrsToExtract.push('flex-wrap')
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// 特殊处理:代码块头部的圆点需要 height 和 width
|
|
985
|
+
const parent = el.parentElement
|
|
986
|
+
// 注意:实际类名是 md-editor-code-flag,不是 md-editor-code-head-dots
|
|
987
|
+
const isCodeHeadDot = parent && parent.classList.contains('md-editor-code-flag') && tagName === 'span'
|
|
988
|
+
// 如果是代码块头部的圆点,直接返回固定样式的 span,避免样式丢失
|
|
989
|
+
if (isCodeHeadDot) {
|
|
990
|
+
const computedDotStyle = window.getComputedStyle(el)
|
|
991
|
+
const bgColor = computedDotStyle.backgroundColor || '#ff5f56'
|
|
992
|
+
// 强制设置圆点样式:固定 12px 圆形,带背景色和右间距
|
|
993
|
+
return `<span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${bgColor}; margin-right: 8px;"></span>`
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
attrsToExtract.forEach((attr) => {
|
|
997
|
+
const camelAttr = attr.replace(/-([a-z])/g, g => (g[1] ? g[1].toUpperCase() : ''))
|
|
998
|
+
let value = computedStyle[camelAttr as any]
|
|
999
|
+
|
|
1000
|
+
// 过滤无效值
|
|
1001
|
+
if (!value || value === 'initial' || value === 'none' || value === 'normal' || value === 'auto') {
|
|
1002
|
+
// 保留 display 的特殊情况
|
|
1003
|
+
if (attr === 'display') {
|
|
1004
|
+
// 忽略默认的 display
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
// 颜色透明忽略
|
|
1008
|
+
if (value === 'rgba(0, 0, 0, 0)')
|
|
1009
|
+
return
|
|
1010
|
+
// 0px 忽略
|
|
1011
|
+
if (value === '0px')
|
|
1012
|
+
return
|
|
1013
|
+
// 过滤 border: 0px ...
|
|
1014
|
+
if (attr.startsWith('border') && value && value.startsWith('0px') && attr !== 'border-radius')
|
|
1015
|
+
return
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// 强制保留的属性 (demo 逻辑)
|
|
1020
|
+
if (outTagName === 'pre') {
|
|
1021
|
+
if (attr === 'white-space')
|
|
1022
|
+
value = 'pre-wrap'
|
|
1023
|
+
if (attr === 'word-break')
|
|
1024
|
+
value = 'break-all'
|
|
1025
|
+
if (attr === 'overflow-x')
|
|
1026
|
+
value = 'auto'
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// code 标签样式处理 (根据上下文判断)
|
|
1030
|
+
if (outTagName === 'code') {
|
|
1031
|
+
// 注意: 这里在提取样式阶段,具体处理在后面的专门代码块中
|
|
1032
|
+
// 此处先保留原逻辑,避免过早干预
|
|
1033
|
+
if (attr === 'white-space') {
|
|
1034
|
+
// 暂不强制设置,留给后续判断
|
|
1035
|
+
// value = 'pre-wrap'
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 微信公众号不支持 grid 布局,转换为 flex
|
|
1040
|
+
if (attr === 'display' && value === 'grid') {
|
|
1041
|
+
value = 'flex'
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (value) {
|
|
1045
|
+
styles.push(`${attr}:${value}`)
|
|
1046
|
+
}
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
// Crepe 列表项强制样式修正
|
|
1050
|
+
if (el.classList.contains('list-item')) {
|
|
1051
|
+
// 强制 Flex Row
|
|
1052
|
+
// 移除可能存在的 block
|
|
1053
|
+
const displayIdx = styles.findIndex(s => s.startsWith('display:'))
|
|
1054
|
+
if (displayIdx > -1)
|
|
1055
|
+
styles.splice(displayIdx, 1)
|
|
1056
|
+
styles.push('display: flex')
|
|
1057
|
+
|
|
1058
|
+
// 顶部对齐或基线对齐,防止文字过多时图标位置奇怪
|
|
1059
|
+
styles.push('align-items: flex-start')
|
|
1060
|
+
|
|
1061
|
+
// 间距
|
|
1062
|
+
if (!styles.some(s => s.startsWith('margin-bottom:'))) {
|
|
1063
|
+
styles.push('margin-bottom: 2px')
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Crepe 列表 Label 修正
|
|
1068
|
+
if (el.classList.contains('label-wrapper')) {
|
|
1069
|
+
styles.push('flex-shrink: 0') // 防止被压缩
|
|
1070
|
+
styles.push('margin-right: 6px') // 和内容的间距
|
|
1071
|
+
// 移除了 margin-top: 2px,因为现在通过 icon 的 margin-top 来控制精确对齐
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Crepe 列表内容修正
|
|
1075
|
+
if (el.classList.contains('content-dom')) {
|
|
1076
|
+
styles.push('flex: 1') // 占据剩余空间
|
|
1077
|
+
styles.push('min-width: 0') // 防止溢出
|
|
1078
|
+
// 强制移除 content-dom 可能存在的 margin,防止多行列表间距过大
|
|
1079
|
+
const marginIdx = styles.findIndex(s => s.startsWith('margin:'))
|
|
1080
|
+
if (marginIdx > -1)
|
|
1081
|
+
styles.splice(marginIdx, 1)
|
|
1082
|
+
styles.push('margin: 0')
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Crepe Image Wrapper 强制修正样式
|
|
1086
|
+
if (el.classList.contains('image-wrapper')) {
|
|
1087
|
+
// 强制宽度100%或居中
|
|
1088
|
+
const widthIdx = styles.findIndex(s => s.startsWith('width:'))
|
|
1089
|
+
if (widthIdx > -1)
|
|
1090
|
+
styles.splice(widthIdx, 1)
|
|
1091
|
+
styles.push('width: 100%') // 铺满容器,配合内部 img 的 margin: auto 或 flex center
|
|
1092
|
+
|
|
1093
|
+
// 强制 margin: 0
|
|
1094
|
+
const marginIdx = styles.findIndex(s => s.startsWith('margin:'))
|
|
1095
|
+
if (marginIdx > -1)
|
|
1096
|
+
styles.splice(marginIdx, 1)
|
|
1097
|
+
styles.push('margin: 0')
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Blockquote 引用块特殊处理
|
|
1101
|
+
if (outTagName === 'blockquote') {
|
|
1102
|
+
// 移除原有 border-left (如果有)
|
|
1103
|
+
const borderLeftIdx = styles.findIndex(s => s.startsWith('border-left:'))
|
|
1104
|
+
if (borderLeftIdx > -1)
|
|
1105
|
+
styles.splice(borderLeftIdx, 1)
|
|
1106
|
+
|
|
1107
|
+
// 移除原有 padding-left
|
|
1108
|
+
const paddingLeftIdx = styles.findIndex(s => s.startsWith('padding-left:'))
|
|
1109
|
+
if (paddingLeftIdx > -1)
|
|
1110
|
+
styles.splice(paddingLeftIdx, 1)
|
|
1111
|
+
|
|
1112
|
+
// 移除原有 color
|
|
1113
|
+
const colorIdx = styles.findIndex(s => s.startsWith('color:'))
|
|
1114
|
+
if (colorIdx > -1)
|
|
1115
|
+
styles.splice(colorIdx, 1)
|
|
1116
|
+
|
|
1117
|
+
// 注入模拟的引用条样式 (Github 风格)
|
|
1118
|
+
styles.push(`border-left: 4px solid ${theme.blockquoteBorderColor ?? '#9cb2a3'}`)
|
|
1119
|
+
styles.push('padding-left: 16px')
|
|
1120
|
+
styles.push(`color: ${theme.mutedColor ?? '#66756b'}`)
|
|
1121
|
+
// 确保有垂直间距
|
|
1122
|
+
if (!styles.some(s => s.startsWith('margin:'))) {
|
|
1123
|
+
styles.push('margin-top: 1.2em')
|
|
1124
|
+
styles.push('margin-bottom: 1.2em')
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// 列表项特殊处理 (li)
|
|
1129
|
+
if (outTagName === 'li') {
|
|
1130
|
+
// 强制移除 li 自身的 margin/padding,防止额外间距
|
|
1131
|
+
const marginIdx = styles.findIndex(s => s.startsWith('margin:'))
|
|
1132
|
+
if (marginIdx > -1)
|
|
1133
|
+
styles.splice(marginIdx, 1)
|
|
1134
|
+
|
|
1135
|
+
// 只保留极小的底部间距
|
|
1136
|
+
const marginBottomIdx = styles.findIndex(s => s.startsWith('margin-bottom:'))
|
|
1137
|
+
if (marginBottomIdx > -1)
|
|
1138
|
+
styles.splice(marginBottomIdx, 1)
|
|
1139
|
+
styles.push('margin-bottom: 2px')
|
|
1140
|
+
|
|
1141
|
+
const paddingIdx = styles.findIndex(s => s.startsWith('padding:'))
|
|
1142
|
+
if (paddingIdx > -1)
|
|
1143
|
+
styles.splice(paddingIdx, 1)
|
|
1144
|
+
styles.push('padding: 0')
|
|
1145
|
+
|
|
1146
|
+
// 强制移除 list-style,防止任务列表出现双重标记
|
|
1147
|
+
if (el.classList.contains('task-list-item')) {
|
|
1148
|
+
styles.push('list-style: none')
|
|
1149
|
+
|
|
1150
|
+
// 强制 Flex 布局
|
|
1151
|
+
// 先移除可能存在的 display (比如 block)
|
|
1152
|
+
const displayIdx = styles.findIndex(s => s.startsWith('display:'))
|
|
1153
|
+
if (displayIdx > -1)
|
|
1154
|
+
styles.splice(displayIdx, 1)
|
|
1155
|
+
styles.push('display: flex')
|
|
1156
|
+
|
|
1157
|
+
// 强制移除可能存在的 align-items: normal,然后设置为 flex-start 以确保多行文本时图标顶对齐
|
|
1158
|
+
const alignIdx = styles.findIndex(s => s.startsWith('align-items:'))
|
|
1159
|
+
if (alignIdx > -1)
|
|
1160
|
+
styles.splice(alignIdx, 1)
|
|
1161
|
+
styles.push('align-items: flex-start')
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// 代码块头部特殊处理 (grid -> flex 布局兼容)
|
|
1166
|
+
if (el.classList.contains('md-editor-code-head')) {
|
|
1167
|
+
// 确保使用 flex 布局
|
|
1168
|
+
const displayIdx = styles.findIndex(s => s.startsWith('display:'))
|
|
1169
|
+
if (displayIdx > -1)
|
|
1170
|
+
styles.splice(displayIdx, 1)
|
|
1171
|
+
styles.push('display: flex')
|
|
1172
|
+
// 强制横向排列
|
|
1173
|
+
const flexDirIdx = styles.findIndex(s => s.startsWith('flex-direction:'))
|
|
1174
|
+
if (flexDirIdx > -1)
|
|
1175
|
+
styles.splice(flexDirIdx, 1)
|
|
1176
|
+
styles.push('flex-direction: row')
|
|
1177
|
+
// 左右分布
|
|
1178
|
+
if (!styles.some(s => s.startsWith('justify-content:'))) {
|
|
1179
|
+
styles.push('justify-content: space-between')
|
|
1180
|
+
}
|
|
1181
|
+
// 垂直居中
|
|
1182
|
+
if (!styles.some(s => s.startsWith('align-items:'))) {
|
|
1183
|
+
styles.push('align-items: center')
|
|
1184
|
+
}
|
|
1185
|
+
// 移除固定宽度,让内容自适应
|
|
1186
|
+
const widthIdx = styles.findIndex(s => s.startsWith('width:'))
|
|
1187
|
+
if (widthIdx > -1)
|
|
1188
|
+
styles.splice(widthIdx, 1)
|
|
1189
|
+
styles.push('width: 100%')
|
|
1190
|
+
// 强制高度为紧凑的小细条
|
|
1191
|
+
const heightIdx = styles.findIndex(s => s.startsWith('height:'))
|
|
1192
|
+
if (heightIdx > -1)
|
|
1193
|
+
styles.splice(heightIdx, 1)
|
|
1194
|
+
// 移除可能导致高度问题的 line-height
|
|
1195
|
+
const lineHeightIdx = styles.findIndex(s => s.startsWith('line-height:'))
|
|
1196
|
+
if (lineHeightIdx > -1)
|
|
1197
|
+
styles.splice(lineHeightIdx, 1)
|
|
1198
|
+
styles.push('height: 32px')
|
|
1199
|
+
styles.push('line-height: 32px')
|
|
1200
|
+
styles.push('padding: 0 12px')
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// 代码块头部的圆点容器特殊处理
|
|
1204
|
+
if (el.classList.contains('md-editor-code-flag')) {
|
|
1205
|
+
// 移除固定宽度,使用自适应
|
|
1206
|
+
const widthIdx = styles.findIndex(s => s.startsWith('width:'))
|
|
1207
|
+
if (widthIdx > -1)
|
|
1208
|
+
styles.splice(widthIdx, 1)
|
|
1209
|
+
styles.push('width: auto')
|
|
1210
|
+
// 确保是 flex
|
|
1211
|
+
const displayIdx = styles.findIndex(s => s.startsWith('display:'))
|
|
1212
|
+
if (displayIdx > -1)
|
|
1213
|
+
styles.splice(displayIdx, 1)
|
|
1214
|
+
styles.push('display: flex')
|
|
1215
|
+
styles.push('align-items: center')
|
|
1216
|
+
styles.push('flex-direction: row')
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// 代码块头部的语言标识容器特殊处理
|
|
1220
|
+
if (el.classList.contains('md-editor-code-action')) {
|
|
1221
|
+
// 移除固定宽度,使用自适应
|
|
1222
|
+
const widthIdx = styles.findIndex(s => s.startsWith('width:'))
|
|
1223
|
+
if (widthIdx > -1)
|
|
1224
|
+
styles.splice(widthIdx, 1)
|
|
1225
|
+
styles.push('width: auto')
|
|
1226
|
+
// 确保是 flex 横向排列
|
|
1227
|
+
const displayIdx = styles.findIndex(s => s.startsWith('display:'))
|
|
1228
|
+
if (displayIdx > -1)
|
|
1229
|
+
styles.splice(displayIdx, 1)
|
|
1230
|
+
styles.push('display: flex')
|
|
1231
|
+
styles.push('align-items: center')
|
|
1232
|
+
styles.push('flex-direction: row')
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Milkdown 代码块容器特殊处理
|
|
1236
|
+
if (el.classList.contains('milkdown-code-block')) {
|
|
1237
|
+
// 移除原有 padding (工具栏相关的大 padding)
|
|
1238
|
+
const paddingIdx = styles.findIndex(s => s.startsWith('padding:'))
|
|
1239
|
+
if (paddingIdx > -1)
|
|
1240
|
+
styles.splice(paddingIdx, 1)
|
|
1241
|
+
// 强制设置紧凑的 padding
|
|
1242
|
+
styles.push('padding: 10px')
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// 语言标识 span 特殊处理
|
|
1246
|
+
if (el.classList.contains('md-editor-code-lang')) {
|
|
1247
|
+
// 移除固定宽度
|
|
1248
|
+
const widthIdx = styles.findIndex(s => s.startsWith('width:'))
|
|
1249
|
+
if (widthIdx > -1)
|
|
1250
|
+
styles.splice(widthIdx, 1)
|
|
1251
|
+
styles.push('width: auto')
|
|
1252
|
+
// 确保横向显示
|
|
1253
|
+
const displayIdx = styles.findIndex(s => s.startsWith('display:'))
|
|
1254
|
+
if (displayIdx > -1)
|
|
1255
|
+
styles.splice(displayIdx, 1)
|
|
1256
|
+
styles.push('display: inline-block')
|
|
1257
|
+
// 调整行高
|
|
1258
|
+
const lineHeightIdx = styles.findIndex(s => s.startsWith('line-height:'))
|
|
1259
|
+
if (lineHeightIdx > -1)
|
|
1260
|
+
styles.splice(lineHeightIdx, 1)
|
|
1261
|
+
styles.push('line-height: 1')
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// code 标签特殊处理
|
|
1265
|
+
if (outTagName === 'code') {
|
|
1266
|
+
// 判断是否为行内 code (父元素不是 pre)
|
|
1267
|
+
const isInlineCode = !parent || parent.tagName.toLowerCase() !== 'pre'
|
|
1268
|
+
|
|
1269
|
+
if (isInlineCode) {
|
|
1270
|
+
// 行内 code: 作为强调样式,允许正常折行
|
|
1271
|
+
// 移除可能导致不换行的样式
|
|
1272
|
+
const whiteSpaceIdx = styles.findIndex(s => s.startsWith('white-space:'))
|
|
1273
|
+
if (whiteSpaceIdx > -1)
|
|
1274
|
+
styles.splice(whiteSpaceIdx, 1)
|
|
1275
|
+
styles.push('white-space: normal')
|
|
1276
|
+
|
|
1277
|
+
// 允许单词内断行,避免单词过长撑破布局
|
|
1278
|
+
if (!styles.some(s => s.startsWith('word-break:')))
|
|
1279
|
+
styles.push('word-break: break-word')
|
|
1280
|
+
|
|
1281
|
+
// 移除固定宽度,防止安卓机型显示异常
|
|
1282
|
+
const widthIdx = styles.findIndex(s => s.startsWith('width:'))
|
|
1283
|
+
if (widthIdx > -1)
|
|
1284
|
+
styles.splice(widthIdx, 1)
|
|
1285
|
+
|
|
1286
|
+
const maxWidthIdx = styles.findIndex(s => s.startsWith('max-width:'))
|
|
1287
|
+
if (maxWidthIdx > -1)
|
|
1288
|
+
styles.splice(maxWidthIdx, 1)
|
|
1289
|
+
|
|
1290
|
+
// 确保 inline 显示,不独占一行
|
|
1291
|
+
const displayIdx = styles.findIndex(s => s.startsWith('display:'))
|
|
1292
|
+
if (displayIdx > -1)
|
|
1293
|
+
styles.splice(displayIdx, 1)
|
|
1294
|
+
styles.push('display: inline')
|
|
1295
|
+
|
|
1296
|
+
// 移除可能存在的固定高度
|
|
1297
|
+
const heightIdx = styles.findIndex(s => s.startsWith('height:'))
|
|
1298
|
+
if (heightIdx > -1)
|
|
1299
|
+
styles.splice(heightIdx, 1)
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
// 代码块内的 code: 保留空格和换行
|
|
1303
|
+
if (!styles.some(s => s.startsWith('white-space:')))
|
|
1304
|
+
styles.push('white-space: pre-wrap')
|
|
1305
|
+
if (!styles.some(s => s.startsWith('word-break:')))
|
|
1306
|
+
styles.push('word-break: break-all')
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// pre 标签特殊处理 (确保空格和换行保留)
|
|
1311
|
+
if (outTagName === 'pre') {
|
|
1312
|
+
if (!styles.some(s => s.startsWith('white-space:'))) {
|
|
1313
|
+
styles.push('white-space: pre-wrap')
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// 图片特殊处理
|
|
1318
|
+
if (outTagName === 'img') {
|
|
1319
|
+
// 获取图片在页面上的实际渲染尺寸
|
|
1320
|
+
const rect = el.getBoundingClientRect()
|
|
1321
|
+
const renderedWidth = rect.width
|
|
1322
|
+
const renderedHeight = rect.height
|
|
1323
|
+
|
|
1324
|
+
// 移除可能存在的固定宽高
|
|
1325
|
+
const widthIdx = styles.findIndex(s => s.startsWith('width:'))
|
|
1326
|
+
if (widthIdx > -1)
|
|
1327
|
+
styles.splice(widthIdx, 1)
|
|
1328
|
+
const heightIdx = styles.findIndex(s => s.startsWith('height:'))
|
|
1329
|
+
if (heightIdx > -1)
|
|
1330
|
+
styles.splice(heightIdx, 1)
|
|
1331
|
+
const maxWidthIdx = styles.findIndex(s => s.startsWith('max-width:'))
|
|
1332
|
+
if (maxWidthIdx > -1)
|
|
1333
|
+
styles.splice(maxWidthIdx, 1)
|
|
1334
|
+
|
|
1335
|
+
styles.push(...resolveImageDimensionStyles({
|
|
1336
|
+
attrWidth: el.getAttribute('width'),
|
|
1337
|
+
attrHeight: el.getAttribute('height'),
|
|
1338
|
+
styleWidth: el.style.width,
|
|
1339
|
+
styleHeight: el.style.height,
|
|
1340
|
+
dataHeight: el.getAttribute('data-height'),
|
|
1341
|
+
renderedWidth,
|
|
1342
|
+
renderedHeight,
|
|
1343
|
+
}))
|
|
1344
|
+
|
|
1345
|
+
if (!styles.some(s => s.startsWith('display:'))) {
|
|
1346
|
+
styles.push('display: block')
|
|
1347
|
+
styles.push('margin: 14px auto')
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
normalizeTypography(styles, tagName, outTagName, theme, el, currentIsInCodeBlock)
|
|
1352
|
+
|
|
1353
|
+
const styleStr = styles.join(';')
|
|
1354
|
+
|
|
1355
|
+
// 递归处理子元素
|
|
1356
|
+
let childrenHtml = ''
|
|
1357
|
+
if (el.childNodes && el.childNodes.length > 0) {
|
|
1358
|
+
Array.from(el.childNodes).forEach((child) => {
|
|
1359
|
+
// 针对 pre/code 的子元素进行样式精简,防止污染
|
|
1360
|
+
// 如果当前是 pre,子元素只保留颜色等基本属性?
|
|
1361
|
+
// demo 逻辑:child.tagName === 'pre' ? ['color'] : []
|
|
1362
|
+
// 这里我们采用更智能的策略:如果父级是 pre/code,子级 span (通常是高亮) 只保留颜色和字体样式
|
|
1363
|
+
let childTargetStyles: string[] | undefined
|
|
1364
|
+
if (['pre', 'code'].includes(outTagName) && child.nodeName === 'SPAN') {
|
|
1365
|
+
childTargetStyles = ['color', 'font-weight', 'font-style', 'text-decoration', 'background-color', 'white-space']
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
childrenHtml += getOneDomCssStyle(child, references, theme, childTargetStyles, currentIsInCodeBlock)
|
|
1369
|
+
})
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
// 如果没有子节点 (可能是空标签,或者某些特殊情况),尝试使用 innerHTML 作为兜底
|
|
1373
|
+
// 注意:这可能会导致内部标签没有被内联样式处理,但至少能保留内容
|
|
1374
|
+
if (el.innerHTML) {
|
|
1375
|
+
childrenHtml = el.innerHTML
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// 组装属性
|
|
1380
|
+
const attrs: string[] = []
|
|
1381
|
+
if (styleStr) {
|
|
1382
|
+
// 修复 font-family 等可能包含双引号导致 style 属性截断的问题
|
|
1383
|
+
const safeStyleStr = styleStr.replace(/"/g, '\'')
|
|
1384
|
+
attrs.push(`style="${safeStyleStr}"`)
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// 添加代码块标记
|
|
1388
|
+
if (isCodeBlockContainer) {
|
|
1389
|
+
attrs.push('data-code-block="true"')
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const allowedForTag = ALLOWED_ATTRS[outTagName] || []
|
|
1393
|
+
const globalAllowed = ALLOWED_ATTRS['*'] || []
|
|
1394
|
+
const allAllowed = new Set([...allowedForTag, ...globalAllowed])
|
|
1395
|
+
|
|
1396
|
+
Array.from(el.attributes).forEach((attr) => {
|
|
1397
|
+
if (allAllowed.has(attr.name)) {
|
|
1398
|
+
if (attr.name === 'style')
|
|
1399
|
+
return
|
|
1400
|
+
attrs.push(`${attr.name}="${attr.value}"`)
|
|
1401
|
+
}
|
|
1402
|
+
})
|
|
1403
|
+
|
|
1404
|
+
if (['img', 'br', 'hr'].includes(outTagName)) {
|
|
1405
|
+
return `<${outTagName} ${attrs.join(' ')} />`
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// 如果是链接,在内容后面追加上标
|
|
1409
|
+
return `<${outTagName} ${attrs.join(' ')}>${childrenHtml}</${outTagName}>${linkSupHtml}`
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* 生成微信公众号格式的 HTML
|
|
1414
|
+
*/
|
|
1415
|
+
export const getWeChatStyledHTML = (rootEl: HTMLElement, theme: WechatExportTheme = {}): string => {
|
|
1416
|
+
if (!rootEl)
|
|
1417
|
+
return ''
|
|
1418
|
+
|
|
1419
|
+
// 清空缓存 (如果启用了缓存)
|
|
1420
|
+
// htmlCache = {}
|
|
1421
|
+
// styleValueCache = {}
|
|
1422
|
+
|
|
1423
|
+
// 外层容器样式
|
|
1424
|
+
const containerStyle = theme.containerStyle || `
|
|
1425
|
+
padding: 20px 16px;
|
|
1426
|
+
background-size: 20px 20px;
|
|
1427
|
+
background-position: center center;
|
|
1428
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
1429
|
+
font-size: 16px;
|
|
1430
|
+
color: #333;
|
|
1431
|
+
word-wrap: break-word;
|
|
1432
|
+
letter-spacing: 2px;
|
|
1433
|
+
`.replace(/\s+/g, ' ').trim()
|
|
1434
|
+
|
|
1435
|
+
const references: LinkReference[] = []
|
|
1436
|
+
let contentHtml = ''
|
|
1437
|
+
Array.from(rootEl.childNodes).forEach((child) => {
|
|
1438
|
+
contentHtml += getOneDomCssStyle(child, references, theme)
|
|
1439
|
+
})
|
|
1440
|
+
|
|
1441
|
+
// 生成相关链接部分
|
|
1442
|
+
let referencesHtml = ''
|
|
1443
|
+
if (references.length > 0) {
|
|
1444
|
+
const refList = references.map((ref, index) => {
|
|
1445
|
+
return `<li style="margin-bottom: 5px;">[${index + 1}] ${ref.text}: ${ref.url}</li>`
|
|
1446
|
+
}).join('')
|
|
1447
|
+
|
|
1448
|
+
referencesHtml = `
|
|
1449
|
+
<section style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee;">
|
|
1450
|
+
<h3 style="font-size: 16px; font-weight: bold; margin-bottom: 10px;">相关链接</h3>
|
|
1451
|
+
<ul style="font-size: 14px; color: #666; padding-left: 20px; margin: 0;">
|
|
1452
|
+
${refList}
|
|
1453
|
+
</ul>
|
|
1454
|
+
</section>
|
|
1455
|
+
`
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const footerText = theme.footerText ?? 'Powered by ZotePad'
|
|
1459
|
+
const footerStyle = theme.footerStyle || 'margin-top: 32px; text-align: center; font-size: 12px; color: #999;'
|
|
1460
|
+
const footer = footerText
|
|
1461
|
+
? `
|
|
1462
|
+
<section style="${footerStyle}">
|
|
1463
|
+
<p style="margin: 0;">${escapeHtml(footerText)}</p>
|
|
1464
|
+
</section>
|
|
1465
|
+
`
|
|
1466
|
+
: ''
|
|
1467
|
+
|
|
1468
|
+
return `<section style="${containerStyle}">${contentHtml}${referencesHtml}${footer}</section>`
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* 生成压缩版微信公众号格式的 HTML (适用于手机端公众号助手)
|
|
1473
|
+
*/
|
|
1474
|
+
export const getWeChatMinimalHTML = (rootEl: HTMLElement, theme: WechatExportTheme = {}): string => {
|
|
1475
|
+
if (!rootEl)
|
|
1476
|
+
return ''
|
|
1477
|
+
|
|
1478
|
+
// 获取完整版 HTML
|
|
1479
|
+
let result = getWeChatStyledHTML(rootEl, theme)
|
|
1480
|
+
|
|
1481
|
+
// 压缩 HTML:移除标签之间纯装饰性的空白文本节点,
|
|
1482
|
+
// 但保留 data-code-block / pre / code 内部的空格和换行。
|
|
1483
|
+
result = minifyHtmlPreservingCodeBlocks(result)
|
|
1484
|
+
|
|
1485
|
+
return normalizeHrStyles(normalizeLinkStyles(normalizeInlineEmphasisStyles(result)))
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* 复制 HTML 到剪贴板 (使用 ClipboardItem 以支持富文本)
|
|
1490
|
+
*/
|
|
1491
|
+
export const copyToClipboard = async (html: string) => {
|
|
1492
|
+
try {
|
|
1493
|
+
const blobHtml = new Blob([html], { type: 'text/html' })
|
|
1494
|
+
const blobText = new Blob([html], { type: 'text/plain' })
|
|
1495
|
+
const item = new ClipboardItem({
|
|
1496
|
+
'text/html': blobHtml,
|
|
1497
|
+
'text/plain': blobText,
|
|
1498
|
+
})
|
|
1499
|
+
await navigator.clipboard.write([item])
|
|
1500
|
+
return true
|
|
1501
|
+
}
|
|
1502
|
+
catch (e) {
|
|
1503
|
+
console.error('Copy failed:', e)
|
|
1504
|
+
return false
|
|
1505
|
+
}
|
|
1506
|
+
}
|