• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

leizongmin / js-xss / #146

08 Nov 2023 01:19PM UTC coverage: 95.567% (-0.6%) from 96.164%
#146

push

travis-pro

123robi
add support for onTagRemoved

219 of 241 branches covered (0.0%)

5 of 6 new or added lines in 2 files covered. (83.33%)

5 existing lines in 2 files now uncovered.

388 of 406 relevant lines covered (95.57%)

176.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.79
/lib/parser.js
1
/**
2
 * Simple HTML Parser
3
 *
4
 * @author Zongmin Lei<leizongmin@gmail.com>
5
 */
6

7
var _ = require("./util");
1✔
8

9
/**
10
 * get tag name
11
 *
12
 * @param {String} html e.g. '<a hef="#">'
13
 * @return {String}
14
 */
15
function getTagName(html) {
16
  var i = _.spaceIndex(html);
253✔
17
  var tagName;
18
  if (i === -1) {
253✔
19
    tagName = html.slice(1, -1);
138✔
20
  } else {
21
    tagName = html.slice(1, i + 1);
115✔
22
  }
23
  tagName = _.trim(tagName).toLowerCase();
253✔
24
  if (tagName.slice(0, 1) === "/") tagName = tagName.slice(1);
253✔
25
  if (tagName.slice(-1) === "/") tagName = tagName.slice(0, -1);
253✔
26
  return tagName;
253✔
27
}
28

29
/**
30
 * is close tag?
31
 *
32
 * @param {String} html 如:'<a hef="#">'
33
 * @return {Boolean}
34
 */
35
function isClosing(html) {
36
  return html.slice(0, 2) === "</";
253✔
37
}
38

39
/**
40
 * is valid empty tag
41
 * Valid empty tags: https://www.w3.org/TR/html4/index/elements.html
42
 * @param {String} html 如:'<a hef="#">'
43
 * @return {Boolean}
44
 */
45
var validEmpty = ['<area', '<base', '<basefont', '<br', '<col', '<frame', '<hr', '<hr', '<img', '<input', '<isindex', '<link', '<meta', '<param'];
1✔
46
function isValidEmpty(html) {
47
  var isValidEmpty = false;
253✔
48
  validEmpty.forEach((validEmptyTag) => {
253✔
49
    if (html.toLowerCase().startsWith(validEmptyTag)) {
3,542✔
50
      isValidEmpty = true;
46✔
51
    }
52
  });
53

54
  return isValidEmpty;
253✔
55
}
56

57
/**
58
 * parse input html and returns processed html
59
 *
60
 * @param {String} html
61
 * @param {Function} onTag e.g. function (sourcePosition, position, tag, html, isClosing)
62
 * @param {Function} escapeHtml
63
 * @return {String}
64
 */
65
function parseTag(html, onTag, escapeHtml) {
66
  "use strict";
67

68
  var rethtml = "";
150✔
69
  var lastPos = 0;
150✔
70
  var tagStart = false;
150✔
71
  var quoteStart = false;
150✔
72
  var currentPos = 0;
150✔
73
  var len = html.length;
150✔
74
  var currentTagName = "";
150✔
75
  var currentHtml = "";
150✔
76

77
  chariterator: for (currentPos = 0; currentPos < len; currentPos++) {
150✔
78
    var c = html.charAt(currentPos);
5,057✔
79
    if (tagStart === false) {
5,057✔
80
      if (c === "<") {
624✔
81
        tagStart = currentPos;
255✔
82
        continue;
255✔
83
      }
84
    } else {
85
      if (quoteStart === false) {
4,433✔
86
        if (c === "<") {
3,113✔
87
          rethtml += escapeHtml(html.slice(lastPos, currentPos));
7✔
88
          tagStart = currentPos;
7✔
89
          lastPos = currentPos;
7✔
90
          continue;
7✔
91
        }
92
        if (c === ">" || currentPos === len - 1) {
3,106✔
93
          rethtml += escapeHtml(html.slice(lastPos, tagStart));
253✔
94
          currentHtml = html.slice(tagStart, currentPos + 1);
253✔
95
          currentTagName = getTagName(currentHtml);
253✔
96
          rethtml += onTag(
253✔
97
            tagStart,
98
            rethtml.length,
99
            currentTagName,
100
            currentHtml,
101
            isClosing(currentHtml),
102
            isValidEmpty(currentHtml)
103
          );
104
          lastPos = currentPos + 1;
253✔
105
          tagStart = false;
253✔
106
          continue;
253✔
107
        }
108
        if (c === '"' || c === "'") {
2,853✔
109
          var i = 1;
142✔
110
          var ic = html.charAt(currentPos - i);
142✔
111

112
          while (ic.trim() === "" || ic === "=") {
142✔
113
            if (ic === "=") {
131✔
114
              quoteStart = c;
116✔
115
              continue chariterator;
116✔
116
            }
117
            ic = html.charAt(currentPos - ++i);
15✔
118
          }
119
        }
120
      } else {
121
        if (c === quoteStart) {
1,320✔
122
          quoteStart = false;
116✔
123
          continue;
116✔
124
        }
125
      }
126
    }
127
  }
128
  if (lastPos < len) {
150✔
129
    rethtml += escapeHtml(html.substr(lastPos));
22✔
130
  }
131

132
  return rethtml;
150✔
133
}
134

135
var REGEXP_ILLEGAL_ATTR_NAME = /[^a-zA-Z0-9\\_:.-]/gim;
1✔
136

137
/**
138
 * parse input attributes and returns processed attributes
139
 *
140
 * @param {String} html e.g. `href="#" target="_blank"`
141
 * @param {Function} onAttr e.g. `function (name, value)`
142
 * @return {String}
143
 */
144
function parseAttr(html, onAttr) {
145
  "use strict";
146

147
  var lastPos = 0;
123✔
148
  var lastMarkPos = 0;
123✔
149
  var retAttrs = [];
123✔
150
  var tmpName = false;
123✔
151
  var len = html.length;
123✔
152

153
  function addAttr(name, value) {
154
    name = _.trim(name);
218✔
155
    name = name.replace(REGEXP_ILLEGAL_ATTR_NAME, "").toLowerCase();
218✔
156
    if (name.length < 1) return;
218✔
157
    var ret = onAttr(name, value || "");
162✔
158
    if (ret) retAttrs.push(ret);
162✔
159
  }
160

161
  // 逐个分析字符
162
  for (var i = 0; i < len; i++) {
123✔
163
    var c = html.charAt(i);
1,870✔
164
    var v, j;
165
    if (tmpName === false && c === "=") {
1,870✔
166
      tmpName = html.slice(lastPos, i);
145✔
167
      lastPos = i + 1;
145✔
168
      lastMarkPos = html.charAt(lastPos) === '"' || html.charAt(lastPos) === "'" ? lastPos : findNextQuotationMark(html, i + 1);
145✔
169
      continue;
145✔
170
    }
171
    if (tmpName !== false) {
1,725✔
172
      if (
880✔
173
        i === lastMarkPos
174
      ) {
175
        j = html.indexOf(c, i + 1);
103✔
176
        if (j === -1) {
103!
UNCOV
177
          break;
×
178
        } else {
179
          v = _.trim(html.slice(lastMarkPos + 1, j));
103✔
180
          addAttr(tmpName, v);
103✔
181
          tmpName = false;
103✔
182
          i = j;
103✔
183
          lastPos = i + 1;
103✔
184
          continue;
103✔
185
        }
186
      }
187
    }
188
    if (/\s|\n|\t/.test(c)) {
1,622✔
189
      html = html.replace(/\s|\n|\t/g, " ");
138✔
190
      if (tmpName === false) {
138✔
191
        j = findNextEqual(html, i);
87✔
192
        if (j === -1) {
87✔
193
          v = _.trim(html.slice(lastPos, i));
64✔
194
          addAttr(v);
64✔
195
          tmpName = false;
64✔
196
          lastPos = i + 1;
64✔
197
          continue;
64✔
198
        } else {
199
          i = j - 1;
23✔
200
          continue;
23✔
201
        }
202
      } else {
203
        j = findBeforeEqual(html, i - 1);
51✔
204
        if (j === -1) {
51✔
205
          v = _.trim(html.slice(lastPos, i));
27✔
206
          v = stripQuoteWrap(v);
27✔
207
          addAttr(tmpName, v);
27✔
208
          tmpName = false;
27✔
209
          lastPos = i + 1;
27✔
210
          continue;
27✔
211
        } else {
212
          continue;
24✔
213
        }
214
      }
215
    }
216
  }
217

218
  if (lastPos < html.length) {
123✔
219
    if (tmpName === false) {
24✔
220
      addAttr(html.slice(lastPos));
9✔
221
    } else {
222
      addAttr(tmpName, stripQuoteWrap(_.trim(html.slice(lastPos))));
15✔
223
    }
224
  }
225

226
  return _.trim(retAttrs.join(" "));
123✔
227
}
228

229
function findNextEqual(str, i) {
230
  for (; i < str.length; i++) {
87✔
231
    var c = str[i];
240✔
232
    if (c === " ") continue;
240✔
233
    if (c === "=") return i;
87✔
234
    return -1;
64✔
235
  }
236
}
237

238
function findNextQuotationMark(str, i) {
239
  for (; i < str.length; i++) {
52✔
240
    var c = str[i];
76✔
241
    if (c === " ") continue;
76✔
242
    if (c === "'" || c === '"') return i;
52✔
243
    return -1;
42✔
244
  }
245
}
246

247
function findBeforeEqual(str, i) {
248
  for (; i > 0; i--) {
51✔
249
    var c = str[i];
53✔
250
    if (c === " ") continue;
53✔
251
    if (c === "=") return i;
51✔
252
    return -1;
27✔
253
  }
254
}
255

256
function isQuoteWrapString(text) {
257
  if (
42!
258
    (text[0] === '"' && text[text.length - 1] === '"') ||
84!
259
    (text[0] === "'" && text[text.length - 1] === "'")
260
  ) {
UNCOV
261
    return true;
×
262
  } else {
263
    return false;
42✔
264
  }
265
}
266

267
function stripQuoteWrap(text) {
268
  if (isQuoteWrapString(text)) {
42!
UNCOV
269
    return text.substr(1, text.length - 2);
×
270
  } else {
271
    return text;
42✔
272
  }
273
}
274

275
exports.parseTag = parseTag;
1✔
276
exports.parseAttr = parseAttr;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc