@nerimity/html-embed 1.1.16 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiIA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM;;;;;;;;IAEtC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiLA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM;;;;;;;;IAGtC"}
package/dist/index.js CHANGED
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -6,10 +39,53 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.htmlToJson = htmlToJson;
7
40
  const htm_1 = __importDefault(require("htm"));
8
41
  const css_1 = __importDefault(require("css"));
42
+ const htmlparser2 = __importStar(require("htmlparser2"));
9
43
  const { validate } = require("csstree-validator");
10
- const allowedTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'img', 'span', 'strong', 'a', "style", "p", "ul", "li", "ol", "table", "thead", "tbody", "tr", "td", "th", "blockquote", "pre", "br", 'b', 'i', 'u', 'em', 'small', 'mark', 'sub', 'sup', 'code', 'hr', 'section', 'article', 'header', 'footer', 'nav'];
44
+ const allowedTags = [
45
+ "h1",
46
+ "h2",
47
+ "h3",
48
+ "h4",
49
+ "h5",
50
+ "h6",
51
+ "div",
52
+ "img",
53
+ "span",
54
+ "strong",
55
+ "a",
56
+ "style",
57
+ "p",
58
+ "ul",
59
+ "li",
60
+ "ol",
61
+ "table",
62
+ "thead",
63
+ "tbody",
64
+ "tr",
65
+ "td",
66
+ "th",
67
+ "blockquote",
68
+ "pre",
69
+ "br",
70
+ "b",
71
+ "i",
72
+ "u",
73
+ "em",
74
+ "small",
75
+ "mark",
76
+ "sub",
77
+ "sup",
78
+ "code",
79
+ "hr",
80
+ "section",
81
+ "article",
82
+ "header",
83
+ "footer",
84
+ "nav",
85
+ ];
11
86
  const allowedAttributes = ["href", "src", "color", "style", "class"];
12
87
  const allowedCssProperties = [
88
+ "opacity",
13
89
  "background-clip",
14
90
  "-webkitBackgroundClip",
15
91
  "-webkitTextFillColor",
@@ -66,7 +142,9 @@ const allowedCssProperties = [
66
142
  "alignContent",
67
143
  "alignSelf",
68
144
  "whiteSpace",
145
+ "wordBreak",
69
146
  "fontFamily",
147
+ "fontStyle",
70
148
  "fontSize",
71
149
  "fontWeight",
72
150
  "zIndex",
@@ -91,7 +169,7 @@ function h(tag, props, ...children) {
91
169
  }
92
170
  // check if attributes are safe
93
171
  if (props) {
94
- const unsafeAttribute = Object.keys(props).find(prop => !allowedAttributes.includes(prop));
172
+ const unsafeAttribute = Object.keys(props).find((prop) => !allowedAttributes.includes(prop));
95
173
  if (unsafeAttribute) {
96
174
  throw new Error(unsafeAttribute + " attribute is not allowed!");
97
175
  }
@@ -99,7 +177,7 @@ function h(tag, props, ...children) {
99
177
  // check if styles are safe
100
178
  if (props === null || props === void 0 ? void 0 : props.style) {
101
179
  const styles = props.style.split(";");
102
- const unsafeCssProperty = styles.find(style => {
180
+ const unsafeCssProperty = styles.find((style) => {
103
181
  if (style === "")
104
182
  return false;
105
183
  const keyVal = style.split(":");
@@ -115,7 +193,8 @@ function h(tag, props, ...children) {
115
193
  }
116
194
  }
117
195
  if (props === null || props === void 0 ? void 0 : props.href) {
118
- if (!props.href.startsWith("http://") && !props.href.startsWith("https://")) {
196
+ if (!props.href.startsWith("http://") &&
197
+ !props.href.startsWith("https://")) {
119
198
  throw new Error("href must start with http:// or https://");
120
199
  }
121
200
  }
@@ -126,8 +205,38 @@ function h(tag, props, ...children) {
126
205
  }
127
206
  const _html = htm_1.default.bind(h);
128
207
  function htmlToJson(html) {
208
+ validateHtmlStructure(html);
129
209
  return _html([html]);
130
210
  }
211
+ function validateHtmlStructure(html) {
212
+ let isMalformed = false;
213
+ const voidElements = ["img", "br", "hr", "input", "meta", "link"];
214
+ const parser = new htmlparser2.Parser({
215
+ onopentag(name) {
216
+ if (voidElements.includes(name.toLowerCase())) {
217
+ const tagClosing = html.substring(parser.startIndex, html.indexOf(">", parser.startIndex) + 1);
218
+ if (!tagClosing.endsWith("/>")) {
219
+ throw new Error(`The <${name}> tag must be self-closed with a slash (e.g., <${name} />).`);
220
+ }
221
+ }
222
+ },
223
+ onerror(error) {
224
+ isMalformed = true;
225
+ },
226
+ }, { decodeEntities: true, lowerCaseTags: true });
227
+ parser.write(html);
228
+ parser.end();
229
+ if (isMalformed) {
230
+ throw new Error("Invalid HTML structure detected.");
231
+ }
232
+ const openTags = (html.match(/<[a-zA-Z0-9]+/g) || []).length;
233
+ const closeTags = (html.match(/<\/[a-zA-Z0-9]+/g) || []).length;
234
+ const voidTagsFound = (html.match(/<(img|br|hr|input|meta|link)/g) || []).length;
235
+ const expectedCloseTags = openTags - (html.match(/<(img|br|hr)/g) || []).length;
236
+ if (openTags - voidTagsFound !== closeTags) {
237
+ throw new Error("Mismatched or unclosed HTML tags.");
238
+ }
239
+ }
131
240
  function checkCSS(cssVal) {
132
241
  var _a;
133
242
  if (validate(cssVal).length) {
@@ -156,7 +265,7 @@ function checkCSS(cssVal) {
156
265
  }
157
266
  }
158
267
  function cssNameToJsName(name) {
159
- var split = name.split("-").filter(n => n);
268
+ var split = name.split("-").filter((n) => n);
160
269
  var output = "";
161
270
  for (var i = 0; i < split.length; i++) {
162
271
  if (i > 0 && split[i].length > 0 && !(i == 1 && split[i] == "ms")) {
package/example.js CHANGED
@@ -4,16 +4,8 @@ const { htmlToJson } = require("./dist");
4
4
 
5
5
  // throw error because invalid css
6
6
  console.log(htmlToJson(`
7
- <div class="owo">
8
- test
9
- <a href="https://google.com">test</a>
10
- </div>
7
+ <dIv class="owo" style="owo: lol"; onclick="Lol">
8
+ <br />
9
+ </dIv>
11
10
 
12
-
13
- <style>
14
- }
15
- .owo {
16
- color: red;
17
- }
18
- </style>
19
11
  `))
package/package.json CHANGED
@@ -1,23 +1,24 @@
1
- {
2
- "name": "@nerimity/html-embed",
3
- "version": "1.1.16",
4
- "description": "",
5
- "main": "dist/index.js",
6
- "keywords": [],
7
- "author": "",
8
- "license": "ISC",
9
- "devDependencies": {
10
- "@types/css": "^0.0.37",
11
- "@types/node": "^22.10.10",
12
- "typescript": "^5.5.2"
13
- },
14
- "dependencies": {
15
- "css": "^3.0.0",
16
- "csstree-validator": "^4.0.1",
17
- "htm": "^3.1.0"
18
- },
19
- "scripts": {
20
- "build": "tsc",
21
- "example": "tsc & node example.js"
22
- }
23
- }
1
+ {
2
+ "name": "@nerimity/html-embed",
3
+ "version": "1.2.1",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "example": "tsc & node example.js"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "ISC",
13
+ "devDependencies": {
14
+ "@types/css": "^0.0.37",
15
+ "@types/node": "^22.10.10",
16
+ "typescript": "^5.5.2"
17
+ },
18
+ "dependencies": {
19
+ "css": "^3.0.0",
20
+ "csstree-validator": "^4.0.1",
21
+ "htm": "^3.1.0",
22
+ "htmlparser2": "^10.0.0"
23
+ }
24
+ }
package/src/index.ts CHANGED
@@ -1,11 +1,53 @@
1
- import htm from 'htm';
2
- import css from 'css';
3
- const {validate} = require("csstree-validator")
1
+ import htm from "htm";
2
+ import css from "css";
3
+ import * as htmlparser2 from "htmlparser2";
4
+ const { validate } = require("csstree-validator");
4
5
 
5
-
6
- const allowedTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'img', 'span', 'strong', 'a', "style", "p", "ul", "li", "ol", "table", "thead", "tbody", "tr", "td", "th", "blockquote", "pre", "br", 'b', 'i', 'u', 'em', 'small', 'mark', 'sub', 'sup', 'code', 'hr', 'section', 'article', 'header', 'footer', 'nav'];
7
- const allowedAttributes = ["href", "src", "color", "style", "class"]
6
+ const allowedTags = [
7
+ "h1",
8
+ "h2",
9
+ "h3",
10
+ "h4",
11
+ "h5",
12
+ "h6",
13
+ "div",
14
+ "img",
15
+ "span",
16
+ "strong",
17
+ "a",
18
+ "style",
19
+ "p",
20
+ "ul",
21
+ "li",
22
+ "ol",
23
+ "table",
24
+ "thead",
25
+ "tbody",
26
+ "tr",
27
+ "td",
28
+ "th",
29
+ "blockquote",
30
+ "pre",
31
+ "br",
32
+ "b",
33
+ "i",
34
+ "u",
35
+ "em",
36
+ "small",
37
+ "mark",
38
+ "sub",
39
+ "sup",
40
+ "code",
41
+ "hr",
42
+ "section",
43
+ "article",
44
+ "header",
45
+ "footer",
46
+ "nav",
47
+ ];
48
+ const allowedAttributes = ["href", "src", "color", "style", "class"];
8
49
  const allowedCssProperties = [
50
+ "opacity",
9
51
  "background-clip",
10
52
  "-webkitBackgroundClip",
11
53
  "-webkitTextFillColor",
@@ -62,7 +104,9 @@ const allowedCssProperties = [
62
104
  "alignContent",
63
105
  "alignSelf",
64
106
  "whiteSpace",
107
+ "wordBreak",
65
108
  "fontFamily",
109
+ "fontStyle",
66
110
  "fontSize",
67
111
  "fontWeight",
68
112
  "zIndex",
@@ -79,68 +123,111 @@ const allowedCssProperties = [
79
123
  "overflowY",
80
124
  "userSelect",
81
125
  "pointerEvents",
82
- ]
83
-
126
+ ];
84
127
 
85
128
  function h(tag: string, props: any, ...children: any[]) {
86
129
  // check if tag is safe
87
130
  if (!allowedTags.includes(tag.toLowerCase())) {
88
- throw new Error(tag+" tag is not allowed!")
131
+ throw new Error(tag + " tag is not allowed!");
89
132
  }
90
133
  // check if attributes are safe
91
134
  if (props) {
92
- const unsafeAttribute = Object.keys(props).find(prop => !allowedAttributes.includes(prop));
135
+ const unsafeAttribute = Object.keys(props).find(
136
+ (prop) => !allowedAttributes.includes(prop)
137
+ );
93
138
  if (unsafeAttribute) {
94
- throw new Error(unsafeAttribute+" attribute is not allowed!")
139
+ throw new Error(unsafeAttribute + " attribute is not allowed!");
95
140
  }
96
141
  }
97
142
  // check if styles are safe
98
143
  if (props?.style) {
99
144
  const styles: string[] = props.style.split(";");
100
- const unsafeCssProperty = styles.find(style => {
145
+ const unsafeCssProperty = styles.find((style) => {
101
146
  if (style === "") return false;
102
- const keyVal = style.split(":")
103
- const key = keyVal[0].trim()
104
- const value = keyVal[1].trim()
147
+ const keyVal = style.split(":");
148
+ const key = keyVal[0].trim();
149
+ const value = keyVal[1].trim();
105
150
  if (key === "position" && value === "fixed") {
106
- throw new Error(value + " value is not allowed for "+ key + "!")
151
+ throw new Error(value + " value is not allowed for " + key + "!");
107
152
  }
108
- return !allowedCssProperties.includes(cssNameToJsName(key))
109
- })
153
+ return !allowedCssProperties.includes(cssNameToJsName(key));
154
+ });
110
155
  if (unsafeCssProperty) {
111
- throw new Error(unsafeCssProperty.split(":")[0].trim() +" property is not allowed!")
156
+ throw new Error(
157
+ unsafeCssProperty.split(":")[0].trim() + " property is not allowed!"
158
+ );
112
159
  }
113
160
  }
114
161
  if (props?.href) {
115
- if (!props.href.startsWith("http://") && !props.href.startsWith("https://")) {
116
- throw new Error("href must start with http:// or https://")
162
+ if (
163
+ !props.href.startsWith("http://") &&
164
+ !props.href.startsWith("https://")
165
+ ) {
166
+ throw new Error("href must start with http:// or https://");
117
167
  }
118
168
  }
119
169
  if (tag.toLowerCase() === "style" && children[0]) {
120
170
  checkCSS(children[0]);
121
171
  }
122
-
172
+
123
173
  return { tag, attributes: props, content: children };
124
174
  }
125
175
 
126
176
  const _html = htm.bind(h);
127
177
 
128
-
129
-
130
178
  export function htmlToJson(html: string) {
131
- return _html([html] as any)
179
+ validateHtmlStructure(html);
180
+ return _html([html] as any);
132
181
  }
133
182
 
183
+ function validateHtmlStructure(html: string) {
184
+ let isMalformed = false;
185
+ const voidElements = ["img", "br", "hr", "input", "meta", "link"];
186
+ const parser = new htmlparser2.Parser(
187
+ {
188
+ onopentag(name) {
189
+ if (voidElements.includes(name.toLowerCase())) {
190
+ const tagClosing = html.substring(parser.startIndex, html.indexOf(">", parser.startIndex) + 1);
191
+
192
+ if (!tagClosing.endsWith("/>")) {
193
+ throw new Error(`The <${name}> tag must be self-closed with a slash (e.g., <${name} />).`);
194
+ }
195
+ }
196
+ },
197
+ onerror(error) {
198
+ isMalformed = true;
199
+ },
200
+ },
201
+ { decodeEntities: true, lowerCaseTags: true }
202
+ );
203
+
204
+ parser.write(html);
205
+ parser.end();
134
206
 
207
+ if (isMalformed) {
208
+ throw new Error("Invalid HTML structure detected.");
209
+ }
210
+
211
+ const openTags = (html.match(/<[a-zA-Z0-9]+/g) || []).length;
212
+ const closeTags = (html.match(/<\/[a-zA-Z0-9]+/g) || []).length;
213
+ const voidTagsFound = (html.match(/<(img|br|hr|input|meta|link)/g) || []).length;
214
+
215
+ const expectedCloseTags =
216
+ openTags - (html.match(/<(img|br|hr)/g) || []).length;
217
+
218
+ if (openTags - voidTagsFound !== closeTags) {
219
+ throw new Error("Mismatched or unclosed HTML tags.");
220
+ }
221
+ }
135
222
 
136
223
  function checkCSS(cssVal: string) {
137
224
  if (validate(cssVal).length) {
138
- throw new Error("Invalid css!")
225
+ throw new Error("Invalid css!");
139
226
  }
140
- const openCurlyBracketCount = cssVal.match(/{/gi)
141
- const closeCurlyBracketCount = cssVal.match(/}/gi)
227
+ const openCurlyBracketCount = cssVal.match(/{/gi);
228
+ const closeCurlyBracketCount = cssVal.match(/}/gi);
142
229
  if (openCurlyBracketCount?.length !== closeCurlyBracketCount?.length) {
143
- throw new Error("Extra curly or missing curly brackets found in css!")
230
+ throw new Error("Extra curly or missing curly brackets found in css!");
144
231
  }
145
232
  const rules = css.parse(cssVal).stylesheet?.rules;
146
233
  if (!rules) return;
@@ -148,21 +235,19 @@ function checkCSS(cssVal: string) {
148
235
  const rule = rules[i];
149
236
  const declarations = (rule as any).declarations;
150
237
  for (let x = 0; x < declarations.length; x++) {
151
- const {property, value} = declarations[x];
238
+ const { property, value } = declarations[x];
152
239
  if (!allowedCssProperties.includes(cssNameToJsName(property))) {
153
- throw new Error(property + " style is not allowed!")
240
+ throw new Error(property + " style is not allowed!");
154
241
  }
155
242
  if (property === "position" && value === "fixed") {
156
- throw new Error(value + " value is not allowed for "+ property + "!")
243
+ throw new Error(value + " value is not allowed for " + property + "!");
157
244
  }
158
245
  }
159
-
160
246
  }
161
247
  }
162
248
 
163
-
164
249
  function cssNameToJsName(name: string) {
165
- var split = name.split("-").filter(n => n);
250
+ var split = name.split("-").filter((n) => n);
166
251
  var output = "";
167
252
  for (var i = 0; i < split.length; i++) {
168
253
  if (i > 0 && split[i].length > 0 && !(i == 1 && split[i] == "ms")) {
@@ -171,12 +256,11 @@ function cssNameToJsName(name: string) {
171
256
  output += split[i];
172
257
  }
173
258
  if (name.startsWith("-")) {
174
- output = "-" + output
259
+ output = "-" + output;
175
260
  }
176
261
  return output;
177
262
  }
178
263
 
179
- function jsNameToCssName(name: string)
180
- {
181
- return name.replace(/([A-Z])/g, "-$1").toLowerCase();
264
+ function jsNameToCssName(name: string) {
265
+ return name.replace(/([A-Z])/g, "-$1").toLowerCase();
182
266
  }