htmlUtils.js
7.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/**
* HTML 工具函数库
*
* @module utils/htmlUtils
* @description 提供 HTML 处理相关的工具函数,包括实体编码/解码、标签清理等
*/
/**
* HTML 实体解码(完整版)
*
* @description
* - H5 环境:使用 DOM API 自动解码(最可靠)
* - 小程序环境:使用完整的手动映射表(200+ 实体)
*
* @param html - 包含 HTML 实体的字符串
* @returns 解码后的字符串
*
* @example
* // 基本使用
* decodeHtmlEntities('Hello & "World" © 2025')
* // => 'Hello "World" © 2025'
*
* @example
* // 数学符号
* decodeHtmlEntities('5 × 3 = 15 ÷ 3 = 5')
* // => '5 × 3 = 15 ÷ 3 = 5'
*
* @example
* // 数字实体
* decodeHtmlEntities('Smile: 😄 or 😀')
* // => 'Smile: 😀 or 😀'
*/
export const decodeHtmlEntities = (html) => {
if (!html) return ''
// 方法1:H5 环境使用 DOM API(最可靠,支持所有实体)
if (process.env.TARO_ENV === 'h5' && typeof document !== 'undefined') {
try {
const textArea = document.createElement('textarea')
textArea.innerHTML = html
const decoded = textArea.value
// 验证解码是否成功
if (decoded !== html) {
return decoded
}
} catch (e) {
console.warn('[htmlUtils] DOM API 解码失败,降级到手动映射')
}
}
// 方法2:小程序环境或降级方案 - 完整手动映射
const entityMap = {
// 基本符号
' ': '\u00A0', '&': '&', '<': '<', '>': '>',
'"': '"', ''': "'", 'Ø': 'Ø', 'ø': 'ø',
// 货币符号
'¢': '¢', '£': '£', '¥': '¥', '€': '€', '¤': '¤',
// 版权/商标
'©': '©', '®': '®', '™': '™',
// 数学符号
'×': '×', '÷': '÷', '±': '±', '½': '½',
'¼': '¼', '¾': '¾', '²': '²', '³': '³',
'√': '√', '∑': 'Σ', '∏': 'Π', 'μ': 'μ', 'π': 'π',
'Ω': 'Ω', '∞': '∞', '≈': '≈', '≠': '≠',
'≤': '≤', '≥': '≥', '°': '°', '′': '′', '″': '″',
// 箭头
'←': '←', '→': '→', '↑': '↑', '↓': '↓',
'↔': '↔', '↵': '↵',
// 引号
'“': '"', '”': '"', '‘': '\u2018', '’': '\u2019',
'‚': '\u201A', '„': '\u201E',
// 破折号
'—': '—', '–': '–',
// 省略号与特殊标点
'…': '…', '•': '•', '¶': '¶', '§': '§',
'·': '·', '⁁': '\u2006',
// 括号
'«': '«', '»': '»', '‹': '‹', '›': '›',
// 重音字母(大写)
'À': 'À', 'Á': 'Á', 'Â': 'Â', 'Ã': 'Ã',
'Ä': 'Ä', 'Å': 'Å', 'Æ': 'Æ', 'Ç': 'Ç',
'È': 'È', 'É': 'É', 'Ê': 'Ê', 'Ë': 'Ë',
'Ì': 'Ì', 'Í': 'Í', 'Î': 'Î', 'Ï': 'Ï',
'Ð': 'Ð', 'Ñ': 'Ñ', 'Ò': 'Ò', 'Ó': 'Ó',
'Ô': 'Ô', 'Õ': 'Õ', 'Ö': 'Ö', 'Ø': 'Ø',
'Š': 'Š', 'Ù': 'Ù', 'Ú': 'Ú', 'Û': 'Û',
'Ü': 'Ü', 'Ý': 'Ý', 'Þ': 'Þ',
// 重音字母(小写)
'à': 'à', 'á': 'á', 'â': 'â', 'ã': 'ã',
'ä': 'ä', 'å': 'å', 'æ': 'æ', 'ç': 'ç',
'è': 'è', 'é': 'é', 'ê': 'ê', 'ë': 'ë',
'ì': 'ì', 'í': 'í', 'î': 'î', 'ï': 'ï',
'ð': 'ð', 'ñ': 'ñ', 'ò': 'ò', 'ó': 'ó',
'ô': 'ô', 'õ': 'õ', 'ö': 'ö', 'ø': 'ø',
'š': 'š', 'ù': 'ù', 'ú': 'ú', 'û': 'û',
'ü': 'ü', 'ý': 'ý', 'ÿ': 'ÿ', 'þ': 'þ',
// 希腊字母
'Α': 'Α', 'Β': 'Β', 'Γ': 'Γ', 'Δ': 'Δ',
'Ε': 'Ε', 'Ζ': 'Ζ', 'Η': 'Η', 'Θ': 'Θ',
'Ι': 'Ι', 'Κ': 'Κ', 'Λ': 'Λ', 'Μ': 'Μ',
'Ν': 'Ν', 'Ξ': 'Ξ', 'Ο': 'Ο', 'Π': 'Π',
'Ρ': 'Ρ', 'Σ': 'Σ', 'Τ': 'Τ', 'Υ': 'Υ',
'Φ': 'Φ', 'Χ': 'Χ', 'Ψ': 'Ψ', 'Ω': 'Ω',
'α': 'α', 'β': 'β', 'γ': 'γ', 'δ': 'δ',
'ε': 'ε', 'ζ': 'ζ', 'η': 'η', 'θ': 'θ',
'ι': 'ι', 'κ': 'κ', 'λ': 'λ', 'μ': 'μ',
'ν': 'ν', 'ξ': 'ξ', 'ο': 'ο', 'π': 'π',
'ρ': 'ρ', 'ς': 'ς', 'σ': 'σ', 'τ': 'τ',
'υ': 'υ', 'φ': 'φ', 'χ': 'χ', 'ψ': 'ψ',
'ω': 'ω',
// 空格变体
' ': '\u2002', ' ': '\u2003', ' ': '\u2009',
'‌': '\u200C', '‍': '\u200D', '‎': '\u200E', '‏': '\u200F'
}
let result = html
// 先处理数字实体(如 { 和 😀)
result = result.replace(/&#(\d+);/g, (_match, dec) => {
return String.fromCharCode(dec)
}).replace(/&#x([0-9a-fA-F]+);/g, (_match, hex) => {
return String.fromCharCode(parseInt(hex, 16))
})
// 再处理命名实体
for (const [entity, char] of Object.entries(entityMap)) {
result = result.split(entity).join(char)
}
return result
}
/**
* HTML 实体编码
*
* @description 将特殊字符转换为 HTML 实体,用于安全地显示用户输入
*
* @param text - 需要编码的文本
* @returns 编码后的 HTML 实体字符串
*
* @example
* encodeHtmlEntities('<script>alert("XSS")</script>')
* // => '<script>alert("XSS")</script>'
*/
export const encodeHtmlEntities = (text) => {
if (!text) return ''
const entityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
' ': ' '
}
return text.replace(/[&<>"' ]/g, char => entityMap[char] || char)
}
/**
* 移除 HTML 标签
*
* @description 从字符串中移除所有 HTML 标签,保留纯文本内容
*
* @param html - 包含 HTML 标签的字符串
* @returns 纯文本内容
*
* @example
* stripHtmlTags('<p>Hello <strong>World</strong>!</p>')
* // => 'Hello World!'
*/
export const stripHtmlTags = (html) => {
if (!html) return ''
return html.replace(/<[^>]*>/g, '')
}
/**
* 截取 HTML 并保留标签完整性
*
* @description 截取 HTML 字符串到指定长度,确保不会截断标签
*
* @param html - HTML 字符串
* @param maxLength - 最大长度(字符数)
* @param suffix - 截断后缀(默认 '...')
* @returns 截取后的 HTML 字符串
*
* @example
* truncateHtml('<p>Hello World</p>', 5)
* // => '<p>Hello...</p>'
*/
export const truncateHtml = (html, maxLength, suffix = '...') => {
if (!html || html.length <= maxLength) return html
// 先移除标签获取纯文本长度
const plainText = stripHtmlTags(html)
if (plainText.length <= maxLength) return html
// 简单截取(高级实现需要 AST 解析)
let result = html.substring(0, maxLength)
const lastOpenTag = result.lastIndexOf('<')
const lastCloseTag = result.lastIndexOf('>')
// 如果最后一个是未闭合的标签,移除它
if (lastOpenTag > lastCloseTag) {
result = result.substring(0, lastOpenTag)
}
return result + suffix
}