@open-mercato/shared 0.6.1-develop.3256.1.fe3dec2464 → 0.6.1-develop.3276.1.1fcd145078
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/dist/lib/html/sanitizeRichText.js +60 -4
- package/dist/lib/html/sanitizeRichText.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/html/__tests__/sanitizeRichText.test.ts +30 -2
- package/src/lib/html/sanitizeRichText.ts +53 -2
|
@@ -13,7 +13,11 @@ const RICH_TEXT_ALLOWED_TAGS = [
|
|
|
13
13
|
"h4",
|
|
14
14
|
"h5",
|
|
15
15
|
"h6",
|
|
16
|
+
"hr",
|
|
16
17
|
"i",
|
|
18
|
+
"img",
|
|
19
|
+
"input",
|
|
20
|
+
"label",
|
|
17
21
|
"li",
|
|
18
22
|
"ol",
|
|
19
23
|
"p",
|
|
@@ -21,6 +25,12 @@ const RICH_TEXT_ALLOWED_TAGS = [
|
|
|
21
25
|
"s",
|
|
22
26
|
"span",
|
|
23
27
|
"strong",
|
|
28
|
+
"table",
|
|
29
|
+
"tbody",
|
|
30
|
+
"td",
|
|
31
|
+
"th",
|
|
32
|
+
"thead",
|
|
33
|
+
"tr",
|
|
24
34
|
"u",
|
|
25
35
|
"ul"
|
|
26
36
|
];
|
|
@@ -29,7 +39,6 @@ const RICH_TEXT_DROP_WITH_CONTENT_TAGS = [
|
|
|
29
39
|
"embed",
|
|
30
40
|
"form",
|
|
31
41
|
"iframe",
|
|
32
|
-
"input",
|
|
33
42
|
"link",
|
|
34
43
|
"math",
|
|
35
44
|
"meta",
|
|
@@ -50,12 +59,59 @@ function sanitizeRichTextHtml(value) {
|
|
|
50
59
|
return sanitizeHtml(value, {
|
|
51
60
|
allowedTags: [...RICH_TEXT_ALLOWED_TAGS],
|
|
52
61
|
allowedAttributes: {
|
|
53
|
-
a: ["href", "title"]
|
|
62
|
+
a: ["href", "title"],
|
|
63
|
+
img: ["src", "alt", "width", "height"],
|
|
64
|
+
table: ["style"],
|
|
65
|
+
tr: [],
|
|
66
|
+
td: ["colspan", "rowspan", "style"],
|
|
67
|
+
th: ["colspan", "rowspan", "style"],
|
|
68
|
+
input: ["type", "checked", "disabled"],
|
|
69
|
+
label: [],
|
|
70
|
+
span: ["style"],
|
|
71
|
+
p: ["style"],
|
|
72
|
+
div: ["style"],
|
|
73
|
+
ul: ["data-task-list", "style"],
|
|
74
|
+
ol: ["style"],
|
|
75
|
+
li: ["style"],
|
|
76
|
+
h1: ["style"],
|
|
77
|
+
h2: ["style"],
|
|
78
|
+
h3: ["style"],
|
|
79
|
+
h4: ["style"],
|
|
80
|
+
h5: ["style"],
|
|
81
|
+
h6: ["style"]
|
|
54
82
|
},
|
|
55
83
|
allowedSchemes: ["http", "https", "mailto", "tel"],
|
|
56
|
-
allowedSchemesAppliedToAttributes: ["href"],
|
|
84
|
+
allowedSchemesAppliedToAttributes: ["href", "src"],
|
|
57
85
|
allowProtocolRelative: false,
|
|
58
|
-
nonTextTags: [...RICH_TEXT_DROP_WITH_CONTENT_TAGS]
|
|
86
|
+
nonTextTags: [...RICH_TEXT_DROP_WITH_CONTENT_TAGS],
|
|
87
|
+
allowedStyles: {
|
|
88
|
+
"*": {
|
|
89
|
+
"color": [/^#(?:[0-9a-f]{3}){1,2}$/i, /^rgb\([\d,\s]+\)$/i, /^rgba\([\d.,\s]+\)$/i],
|
|
90
|
+
"background-color": [/^#(?:[0-9a-f]{3}){1,2}$/i, /^rgb\([\d,\s]+\)$/i, /^rgba\([\d.,\s]+\)$/i],
|
|
91
|
+
"font-size": [/^\d+(?:\.\d+)?(?:px|em|rem|%)$/],
|
|
92
|
+
"font-weight": [/^(?:normal|bold|\d{3})$/],
|
|
93
|
+
"font-style": [/^(?:normal|italic|oblique)$/],
|
|
94
|
+
"text-align": [/^(?:left|right|center|justify)$/],
|
|
95
|
+
"text-decoration": [/^(?:none|underline|line-through)$/],
|
|
96
|
+
"width": [/^\d+(?:\.\d+)?(?:px|em|rem|%)$/],
|
|
97
|
+
"height": [/^\d+(?:\.\d+)?(?:px|em|rem|%)$/],
|
|
98
|
+
"border": [/^[\w\-:#.,\s()]+$/],
|
|
99
|
+
"border-collapse": [/^(?:collapse|separate)$/],
|
|
100
|
+
"padding": [/^[\d.\spxemr%]+$/],
|
|
101
|
+
"list-style": [/^(?:none|disc|circle|square|decimal)$/],
|
|
102
|
+
"padding-left": [/^\d+(?:\.\d+)?(?:px|em|rem|%)?$/]
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
transformTags: {
|
|
106
|
+
// Restrict <input> to checkbox toggles only — strip any other `type` so
|
|
107
|
+
// text/file/etc. inputs don't slip through the rich text channel.
|
|
108
|
+
"input": (tagName, attribs) => {
|
|
109
|
+
if (attribs.type === "checkbox") {
|
|
110
|
+
return { tagName: "input", attribs };
|
|
111
|
+
}
|
|
112
|
+
return { tagName: "", attribs: {}, text: "" };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
59
115
|
});
|
|
60
116
|
}
|
|
61
117
|
function sanitizeRichTextHref(value) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/html/sanitizeRichText.ts"],
|
|
4
|
-
"sourcesContent": ["import sanitizeHtml from 'sanitize-html'\n\nexport const RICH_TEXT_ALLOWED_TAGS = [\n 'a',\n 'b',\n 'blockquote',\n 'br',\n 'code',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'i',\n 'li',\n 'ol',\n 'p',\n 'pre',\n 's',\n 'span',\n 'strong',\n 'u',\n 'ul',\n] as const\n\nconst RICH_TEXT_DROP_WITH_CONTENT_TAGS = [\n 'button',\n 'embed',\n 'form',\n 'iframe',\n '
|
|
5
|
-
"mappings": "AAAA,OAAO,kBAAkB;AAElB,MAAM,yBAAyB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,MAAM,mCAAmC;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;
|
|
4
|
+
"sourcesContent": ["import sanitizeHtml from 'sanitize-html'\n\nexport const RICH_TEXT_ALLOWED_TAGS = [\n 'a',\n 'b',\n 'blockquote',\n 'br',\n 'code',\n 'div',\n 'em',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'hr',\n 'i',\n 'img',\n 'input',\n 'label',\n 'li',\n 'ol',\n 'p',\n 'pre',\n 's',\n 'span',\n 'strong',\n 'table',\n 'tbody',\n 'td',\n 'th',\n 'thead',\n 'tr',\n 'u',\n 'ul',\n] as const\n\nconst RICH_TEXT_DROP_WITH_CONTENT_TAGS = [\n 'button',\n 'embed',\n 'form',\n 'iframe',\n 'link',\n 'math',\n 'meta',\n 'object',\n 'option',\n 'script',\n 'select',\n 'style',\n 'svg',\n 'textarea',\n] as const\n\nconst HTML_LIKE_PATTERN = /<\\/?[a-z][\\s\\S]*>/i\n\nfunction escapeAttribute(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n\nexport type RichTextPasteContent = {\n command: 'insertHTML' | 'insertText'\n value: string\n}\n\nexport function sanitizeRichTextHtml(value: string | null | undefined): string {\n if (typeof value !== 'string' || value.length === 0) return ''\n\n return sanitizeHtml(value, {\n allowedTags: [...RICH_TEXT_ALLOWED_TAGS],\n allowedAttributes: {\n a: ['href', 'title'],\n img: ['src', 'alt', 'width', 'height'],\n table: ['style'],\n tr: [],\n td: ['colspan', 'rowspan', 'style'],\n th: ['colspan', 'rowspan', 'style'],\n input: ['type', 'checked', 'disabled'],\n label: [],\n span: ['style'],\n p: ['style'],\n div: ['style'],\n ul: ['data-task-list', 'style'],\n ol: ['style'],\n li: ['style'],\n h1: ['style'], h2: ['style'], h3: ['style'], h4: ['style'], h5: ['style'], h6: ['style'],\n },\n allowedSchemes: ['http', 'https', 'mailto', 'tel'],\n allowedSchemesAppliedToAttributes: ['href', 'src'],\n allowProtocolRelative: false,\n nonTextTags: [...RICH_TEXT_DROP_WITH_CONTENT_TAGS],\n allowedStyles: {\n '*': {\n 'color': [/^#(?:[0-9a-f]{3}){1,2}$/i, /^rgb\\([\\d,\\s]+\\)$/i, /^rgba\\([\\d.,\\s]+\\)$/i],\n 'background-color': [/^#(?:[0-9a-f]{3}){1,2}$/i, /^rgb\\([\\d,\\s]+\\)$/i, /^rgba\\([\\d.,\\s]+\\)$/i],\n 'font-size': [/^\\d+(?:\\.\\d+)?(?:px|em|rem|%)$/],\n 'font-weight': [/^(?:normal|bold|\\d{3})$/],\n 'font-style': [/^(?:normal|italic|oblique)$/],\n 'text-align': [/^(?:left|right|center|justify)$/],\n 'text-decoration': [/^(?:none|underline|line-through)$/],\n 'width': [/^\\d+(?:\\.\\d+)?(?:px|em|rem|%)$/],\n 'height': [/^\\d+(?:\\.\\d+)?(?:px|em|rem|%)$/],\n 'border': [/^[\\w\\-:#.,\\s()]+$/],\n 'border-collapse': [/^(?:collapse|separate)$/],\n 'padding': [/^[\\d.\\spxemr%]+$/],\n 'list-style': [/^(?:none|disc|circle|square|decimal)$/],\n 'padding-left': [/^\\d+(?:\\.\\d+)?(?:px|em|rem|%)?$/],\n },\n },\n transformTags: {\n // Restrict <input> to checkbox toggles only \u2014 strip any other `type` so\n // text/file/etc. inputs don't slip through the rich text channel.\n 'input': (tagName, attribs) => {\n if (attribs.type === 'checkbox') {\n return { tagName: 'input', attribs }\n }\n return { tagName: '', attribs: {}, text: '' }\n },\n },\n })\n}\n\nexport function sanitizeRichTextHref(value: string | null | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n if (!trimmed) return null\n\n const html = sanitizeRichTextHtml(`<a href=\"${escapeAttribute(trimmed)}\">x</a>`)\n const match = /\\shref=\"([^\"]+)\"/.exec(html)\n return match ? trimmed : null\n}\n\nexport function sanitizeRichTextPasteContent(html: string, text: string): RichTextPasteContent | null {\n if (html) {\n return { command: 'insertHTML', value: sanitizeRichTextHtml(html) }\n }\n\n if (!text) return null\n\n if (HTML_LIKE_PATTERN.test(text)) {\n return { command: 'insertHTML', value: sanitizeRichTextHtml(text) }\n }\n\n return { command: 'insertText', value: text }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.1-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.1-develop.3276.1.1fcd145078'\nexport const appVersion = APP_VERSION\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.1-develop.
|
|
3
|
+
"version": "0.6.1-develop.3276.1.1fcd145078",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"@mikro-orm/core": "^7.0.15",
|
|
93
93
|
"@mikro-orm/decorators": "^7.0.15",
|
|
94
94
|
"@mikro-orm/postgresql": "^7.0.15",
|
|
95
|
-
"@open-mercato/cache": "0.6.1-develop.
|
|
95
|
+
"@open-mercato/cache": "0.6.1-develop.3276.1.1fcd145078",
|
|
96
96
|
"dotenv": "^17.4.2",
|
|
97
97
|
"rate-limiter-flexible": "^11.1.0",
|
|
98
98
|
"reflect-metadata": "^0.2.2",
|
|
@@ -12,9 +12,12 @@ describe('sanitizeRichText', () => {
|
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
it('removes executable content and unsafe attributes', () => {
|
|
15
|
+
// `<img>` is on the allowlist (toolbar `insertImage`), so the tag stays
|
|
16
|
+
// but the `onerror` handler is stripped and the `color:red` keyword
|
|
17
|
+
// doesn't match the hex/rgb allowed-style regex so it falls away too.
|
|
15
18
|
expect(
|
|
16
19
|
sanitizeRichTextHtml('<p onclick="alert(1)" style="color:red">Hi<script>alert(1)</script><img src=x onerror=alert(2)>there</p>'),
|
|
17
|
-
).toBe('<p>
|
|
20
|
+
).toBe('<p>Hi<img src="x" />there</p>')
|
|
18
21
|
})
|
|
19
22
|
|
|
20
23
|
it('removes unsafe href values from links', () => {
|
|
@@ -44,10 +47,35 @@ describe('sanitizeRichText', () => {
|
|
|
44
47
|
),
|
|
45
48
|
).toEqual({
|
|
46
49
|
command: 'insertHTML',
|
|
47
|
-
value: '<p>Safe text</p><a>Bad link</a>',
|
|
50
|
+
value: '<p>Safe text</p><img src="x" /><a>Bad link</a>',
|
|
48
51
|
})
|
|
49
52
|
})
|
|
50
53
|
|
|
54
|
+
it('keeps RichEditor toolbar output: hr / image / table / inline-code / code-block / checklist', () => {
|
|
55
|
+
expect(sanitizeRichTextHtml('<hr />')).toBe('<hr />')
|
|
56
|
+
expect(sanitizeRichTextHtml('<img src="https://example.com/x.png" alt="" />')).toBe('<img src="https://example.com/x.png" alt="" />')
|
|
57
|
+
expect(sanitizeRichTextHtml('<table><tbody><tr><td>a</td></tr></tbody></table>')).toBe('<table><tbody><tr><td>a</td></tr></tbody></table>')
|
|
58
|
+
expect(sanitizeRichTextHtml('<p>before <code>inline</code> after</p>')).toBe('<p>before <code>inline</code> after</p>')
|
|
59
|
+
expect(sanitizeRichTextHtml('<pre>code block</pre>')).toBe('<pre>code block</pre>')
|
|
60
|
+
expect(sanitizeRichTextHtml('<ul data-task-list="true"><li><label><input type="checkbox" /> task</label></li></ul>')).toBe(
|
|
61
|
+
'<ul data-task-list="true"><li><label><input type="checkbox" /> task</label></li></ul>',
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('keeps color + font-size span styles emitted by the toolbar color picker / font size dropdown', () => {
|
|
66
|
+
expect(sanitizeRichTextHtml('<span style="color:#6366f1">blue</span>')).toBe('<span style="color:#6366f1">blue</span>')
|
|
67
|
+
expect(sanitizeRichTextHtml('<span style="font-size:16px">big</span>')).toBe('<span style="font-size:16px">big</span>')
|
|
68
|
+
// Disallowed style values (CSS keyword colors, url(), etc.) still get dropped.
|
|
69
|
+
expect(sanitizeRichTextHtml('<span style="color:red">red</span>')).toBe('<span>red</span>')
|
|
70
|
+
expect(sanitizeRichTextHtml('<span style="background-image:url(http://evil)">x</span>')).toBe('<span>x</span>')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('only allows <input type="checkbox"> — other input types are dropped', () => {
|
|
74
|
+
expect(sanitizeRichTextHtml('<input type="checkbox" checked />')).toBe('<input type="checkbox" checked />')
|
|
75
|
+
expect(sanitizeRichTextHtml('<input type="text" value="x" />')).toBe('')
|
|
76
|
+
expect(sanitizeRichTextHtml('<input type="file" />')).toBe('')
|
|
77
|
+
})
|
|
78
|
+
|
|
51
79
|
it('keeps non-html plain text paste as text', () => {
|
|
52
80
|
expect(sanitizeRichTextPasteContent('', 'Plain < not a tag')).toEqual({
|
|
53
81
|
command: 'insertText',
|
|
@@ -14,7 +14,11 @@ export const RICH_TEXT_ALLOWED_TAGS = [
|
|
|
14
14
|
'h4',
|
|
15
15
|
'h5',
|
|
16
16
|
'h6',
|
|
17
|
+
'hr',
|
|
17
18
|
'i',
|
|
19
|
+
'img',
|
|
20
|
+
'input',
|
|
21
|
+
'label',
|
|
18
22
|
'li',
|
|
19
23
|
'ol',
|
|
20
24
|
'p',
|
|
@@ -22,6 +26,12 @@ export const RICH_TEXT_ALLOWED_TAGS = [
|
|
|
22
26
|
's',
|
|
23
27
|
'span',
|
|
24
28
|
'strong',
|
|
29
|
+
'table',
|
|
30
|
+
'tbody',
|
|
31
|
+
'td',
|
|
32
|
+
'th',
|
|
33
|
+
'thead',
|
|
34
|
+
'tr',
|
|
25
35
|
'u',
|
|
26
36
|
'ul',
|
|
27
37
|
] as const
|
|
@@ -31,7 +41,6 @@ const RICH_TEXT_DROP_WITH_CONTENT_TAGS = [
|
|
|
31
41
|
'embed',
|
|
32
42
|
'form',
|
|
33
43
|
'iframe',
|
|
34
|
-
'input',
|
|
35
44
|
'link',
|
|
36
45
|
'math',
|
|
37
46
|
'meta',
|
|
@@ -66,11 +75,53 @@ export function sanitizeRichTextHtml(value: string | null | undefined): string {
|
|
|
66
75
|
allowedTags: [...RICH_TEXT_ALLOWED_TAGS],
|
|
67
76
|
allowedAttributes: {
|
|
68
77
|
a: ['href', 'title'],
|
|
78
|
+
img: ['src', 'alt', 'width', 'height'],
|
|
79
|
+
table: ['style'],
|
|
80
|
+
tr: [],
|
|
81
|
+
td: ['colspan', 'rowspan', 'style'],
|
|
82
|
+
th: ['colspan', 'rowspan', 'style'],
|
|
83
|
+
input: ['type', 'checked', 'disabled'],
|
|
84
|
+
label: [],
|
|
85
|
+
span: ['style'],
|
|
86
|
+
p: ['style'],
|
|
87
|
+
div: ['style'],
|
|
88
|
+
ul: ['data-task-list', 'style'],
|
|
89
|
+
ol: ['style'],
|
|
90
|
+
li: ['style'],
|
|
91
|
+
h1: ['style'], h2: ['style'], h3: ['style'], h4: ['style'], h5: ['style'], h6: ['style'],
|
|
69
92
|
},
|
|
70
93
|
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
|
|
71
|
-
allowedSchemesAppliedToAttributes: ['href'],
|
|
94
|
+
allowedSchemesAppliedToAttributes: ['href', 'src'],
|
|
72
95
|
allowProtocolRelative: false,
|
|
73
96
|
nonTextTags: [...RICH_TEXT_DROP_WITH_CONTENT_TAGS],
|
|
97
|
+
allowedStyles: {
|
|
98
|
+
'*': {
|
|
99
|
+
'color': [/^#(?:[0-9a-f]{3}){1,2}$/i, /^rgb\([\d,\s]+\)$/i, /^rgba\([\d.,\s]+\)$/i],
|
|
100
|
+
'background-color': [/^#(?:[0-9a-f]{3}){1,2}$/i, /^rgb\([\d,\s]+\)$/i, /^rgba\([\d.,\s]+\)$/i],
|
|
101
|
+
'font-size': [/^\d+(?:\.\d+)?(?:px|em|rem|%)$/],
|
|
102
|
+
'font-weight': [/^(?:normal|bold|\d{3})$/],
|
|
103
|
+
'font-style': [/^(?:normal|italic|oblique)$/],
|
|
104
|
+
'text-align': [/^(?:left|right|center|justify)$/],
|
|
105
|
+
'text-decoration': [/^(?:none|underline|line-through)$/],
|
|
106
|
+
'width': [/^\d+(?:\.\d+)?(?:px|em|rem|%)$/],
|
|
107
|
+
'height': [/^\d+(?:\.\d+)?(?:px|em|rem|%)$/],
|
|
108
|
+
'border': [/^[\w\-:#.,\s()]+$/],
|
|
109
|
+
'border-collapse': [/^(?:collapse|separate)$/],
|
|
110
|
+
'padding': [/^[\d.\spxemr%]+$/],
|
|
111
|
+
'list-style': [/^(?:none|disc|circle|square|decimal)$/],
|
|
112
|
+
'padding-left': [/^\d+(?:\.\d+)?(?:px|em|rem|%)?$/],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
transformTags: {
|
|
116
|
+
// Restrict <input> to checkbox toggles only — strip any other `type` so
|
|
117
|
+
// text/file/etc. inputs don't slip through the rich text channel.
|
|
118
|
+
'input': (tagName, attribs) => {
|
|
119
|
+
if (attribs.type === 'checkbox') {
|
|
120
|
+
return { tagName: 'input', attribs }
|
|
121
|
+
}
|
|
122
|
+
return { tagName: '', attribs: {}, text: '' }
|
|
123
|
+
},
|
|
124
|
+
},
|
|
74
125
|
})
|
|
75
126
|
}
|
|
76
127
|
|