@nerimity/html-embed 1.1.14 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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"];
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,28 @@ 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 parser = new htmlparser2.Parser({
214
+ onerror(error) {
215
+ isMalformed = true;
216
+ },
217
+ }, { decodeEntities: true, lowerCaseTags: true });
218
+ parser.write(html);
219
+ parser.end();
220
+ if (isMalformed) {
221
+ throw new Error("Invalid HTML structure detected.");
222
+ }
223
+ const openTags = (html.match(/<[a-zA-Z0-9]+/g) || []).length;
224
+ const closeTags = (html.match(/<\/[a-zA-Z0-9]+/g) || []).length;
225
+ const expectedCloseTags = openTags - (html.match(/<(img|br|hr)/g) || []).length;
226
+ if (expectedCloseTags !== closeTags) {
227
+ throw new Error("Mismatched or unclosed HTML tags.");
228
+ }
229
+ }
131
230
  function checkCSS(cssVal) {
132
231
  var _a;
133
232
  if (validate(cssVal).length) {
@@ -156,7 +255,7 @@ function checkCSS(cssVal) {
156
255
  }
157
256
  }
158
257
  function cssNameToJsName(name) {
159
- var split = name.split("-").filter(n => n);
258
+ var split = name.split("-").filter((n) => n);
160
259
  var output = "";
161
260
  for (var i = 0; i < split.length; i++) {
162
261
  if (i > 0 && split[i].length > 0 && !(i == 1 && split[i] == "ms")) {
package/package.json CHANGED
@@ -1,23 +1,24 @@
1
- {
2
- "name": "@nerimity/html-embed",
3
- "version": "1.1.14",
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
- }
23
- }
1
+ {
2
+ "name": "@nerimity/html-embed",
3
+ "version": "1.2.0",
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"]
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,100 @@ 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 parser = new htmlparser2.Parser(
186
+ {
187
+ onerror(error) {
188
+ isMalformed = true;
189
+ },
190
+ },
191
+ { decodeEntities: true, lowerCaseTags: true }
192
+ );
193
+
194
+ parser.write(html);
195
+ parser.end();
196
+
197
+ if (isMalformed) {
198
+ throw new Error("Invalid HTML structure detected.");
199
+ }
200
+
201
+ const openTags = (html.match(/<[a-zA-Z0-9]+/g) || []).length;
202
+ const closeTags = (html.match(/<\/[a-zA-Z0-9]+/g) || []).length;
134
203
 
204
+ const expectedCloseTags =
205
+ openTags - (html.match(/<(img|br|hr)/g) || []).length;
206
+
207
+ if (expectedCloseTags !== closeTags) {
208
+ throw new Error("Mismatched or unclosed HTML tags.");
209
+ }
210
+ }
135
211
 
136
212
  function checkCSS(cssVal: string) {
137
213
  if (validate(cssVal).length) {
138
- throw new Error("Invalid css!")
214
+ throw new Error("Invalid css!");
139
215
  }
140
- const openCurlyBracketCount = cssVal.match(/{/gi)
141
- const closeCurlyBracketCount = cssVal.match(/}/gi)
216
+ const openCurlyBracketCount = cssVal.match(/{/gi);
217
+ const closeCurlyBracketCount = cssVal.match(/}/gi);
142
218
  if (openCurlyBracketCount?.length !== closeCurlyBracketCount?.length) {
143
- throw new Error("Extra curly or missing curly brackets found in css!")
219
+ throw new Error("Extra curly or missing curly brackets found in css!");
144
220
  }
145
221
  const rules = css.parse(cssVal).stylesheet?.rules;
146
222
  if (!rules) return;
@@ -148,21 +224,19 @@ function checkCSS(cssVal: string) {
148
224
  const rule = rules[i];
149
225
  const declarations = (rule as any).declarations;
150
226
  for (let x = 0; x < declarations.length; x++) {
151
- const {property, value} = declarations[x];
227
+ const { property, value } = declarations[x];
152
228
  if (!allowedCssProperties.includes(cssNameToJsName(property))) {
153
- throw new Error(property + " style is not allowed!")
229
+ throw new Error(property + " style is not allowed!");
154
230
  }
155
231
  if (property === "position" && value === "fixed") {
156
- throw new Error(value + " value is not allowed for "+ property + "!")
232
+ throw new Error(value + " value is not allowed for " + property + "!");
157
233
  }
158
234
  }
159
-
160
235
  }
161
236
  }
162
237
 
163
-
164
238
  function cssNameToJsName(name: string) {
165
- var split = name.split("-").filter(n => n);
239
+ var split = name.split("-").filter((n) => n);
166
240
  var output = "";
167
241
  for (var i = 0; i < split.length; i++) {
168
242
  if (i > 0 && split[i].length > 0 && !(i == 1 && split[i] == "ms")) {
@@ -171,12 +245,11 @@ function cssNameToJsName(name: string) {
171
245
  output += split[i];
172
246
  }
173
247
  if (name.startsWith("-")) {
174
- output = "-" + output
248
+ output = "-" + output;
175
249
  }
176
250
  return output;
177
251
  }
178
252
 
179
- function jsNameToCssName(name: string)
180
- {
181
- return name.replace(/([A-Z])/g, "-$1").toLowerCase();
182
- }
253
+ function jsNameToCssName(name: string) {
254
+ return name.replace(/([A-Z])/g, "-$1").toLowerCase();
255
+ }