html-minifier 1.2.0 → 1.5.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.
package/src/htmlparser.js CHANGED
@@ -28,506 +28,502 @@
28
28
 
29
29
  /* global ActiveXObject, DOMDocument */
30
30
 
31
- (function(global) {
32
- 'use strict';
33
-
34
- // Regular Expressions for parsing tags and attributes
35
- var singleAttrIdentifier = /([\w:\.-]+)/,
36
- singleAttrAssign = /=/,
37
- singleAttrAssigns = [ singleAttrAssign ],
38
- singleAttrValues = [
39
- /"((?:\\.|[^"])*)"/.source, // attr value double quotes
40
- /'((?:\\.|[^'])*)'/.source, // attr value, single quotes
41
- /([^>\s]+)/.source // attr value, no quotes
42
- ],
43
- startTagOpen = /^<([\w:-]+)/,
44
- startTagClose = /\s*(\/?)>/,
45
- endTag = /^<\/([\w:-]+)[^>]*>/,
46
- endingSlash = /\/>$/,
47
- doctype = /^<!DOCTYPE [^>]+>/i;
48
-
49
- var IS_REGEX_CAPTURING_BROKEN = false;
50
- 'x'.replace(/x(.)?/g, function(m, g) {
51
- IS_REGEX_CAPTURING_BROKEN = g === '';
52
- });
53
-
54
- // Empty Elements
55
- var empty = makeMap('area,base,basefont,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr');
56
-
57
- // Inline Elements
58
- var inline = makeMap('a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,noscript,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,svg,textarea,tt,u,var');
59
-
60
- // Elements that you can, intentionally, leave open
61
- // (and which close themselves)
62
- var closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source');
63
-
64
- // Attributes that have their values filled in disabled='disabled'
65
- var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected');
66
-
67
- // Special Elements (can contain anything)
68
- var special = makeMap('script,style');
69
-
70
- var reCache = {}, stackedTag, reStackedTag, tagMatch;
71
-
72
- function startTagForHandler( handler ) {
73
- var customStartTagAttrs;
74
-
75
- var startTagAttrs = new RegExp(
76
- '(?:\\s*[\\w:\\.-]+'
77
- + '(?:\\s*'
78
- + '(?:' + joinSingleAttrAssigns(handler) + ')'
79
- + '\\s*(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>\\s]+)'
80
- + ')?'
81
- + ')*'
82
- );
83
-
84
- if ( handler.customAttrSurround ) {
85
- var attrClauses = [];
86
-
87
- for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
88
- // Capture the custom attribute opening and closing markup surrounding the standard attribute rules
89
- attrClauses[i] = '(?:\\s*'
90
- + handler.customAttrSurround[i][0].source
91
- + '\\s*'
92
- + startTagAttrs.source
93
- + '\\s*'
94
- + handler.customAttrSurround[i][1].source
95
- + ')';
96
- }
97
- attrClauses.unshift(startTagAttrs.source);
98
-
99
- customStartTagAttrs = new RegExp(
100
- '((?:' + attrClauses.join('|') + ')*)'
101
- );
102
- }
103
- else {
104
- // No custom attribute wrappers specified, so just capture the standard attribute rules
105
- customStartTagAttrs = new RegExp('(' + startTagAttrs.source + ')');
106
- }
107
-
108
- return new RegExp(startTagOpen.source + customStartTagAttrs.source + startTagClose.source);
109
- }
110
-
111
- function attrForHandler( handler ) {
112
- var singleAttr = new RegExp(
113
- singleAttrIdentifier.source
114
- + '(?:\\s*'
115
- + '(' + joinSingleAttrAssigns( handler ) + ')'
116
- + '\\s*'
117
- + '(?:'
118
- + singleAttrValues.join('|')
119
- + ')'
120
- + ')?'
121
- );
122
-
123
- if ( handler.customAttrSurround ) {
124
- var attrClauses = [];
125
- for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
126
- attrClauses[i] = '(?:'
127
- + '(' + handler.customAttrSurround[i][0].source + ')\\s*'
128
- + singleAttr.source
129
- + '\\s*(' + handler.customAttrSurround[i][1].source + ')'
130
- + ')';
131
- }
132
- attrClauses.unshift('(?:' + singleAttr.source + ')');
133
-
134
- return new RegExp(attrClauses.join('|'), 'g');
135
- }
136
- else {
137
- return new RegExp(singleAttr.source, 'g');
31
+ 'use strict';
32
+
33
+ var createMapFromString = require('./utils').createMapFromString;
34
+
35
+ function makeMap(values) {
36
+ return createMapFromString(values, true);
37
+ }
38
+
39
+ // Regular Expressions for parsing tags and attributes
40
+ var singleAttrIdentifier = /([^\s"'<>\/=]+)/,
41
+ singleAttrAssign = /=/,
42
+ singleAttrAssigns = [singleAttrAssign],
43
+ singleAttrValues = [
44
+ // attr value double quotes
45
+ /"([^"]*)"+/.source,
46
+ // attr value, single quotes
47
+ /'([^']*)'+/.source,
48
+ // attr value, no quotes
49
+ /([^\s"'=<>`]+)/.source
50
+ ],
51
+ // https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
52
+ qnameCapture = (function() {
53
+ var ncname = require('ncname').source.slice(1, -1);
54
+ return '((?:' + ncname + '\\:)?' + ncname + ')';
55
+ })(),
56
+ startTagOpen = new RegExp('^<' + qnameCapture),
57
+ startTagClose = /^\s*(\/?)>/,
58
+ endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>'),
59
+ doctype = /^<!DOCTYPE [^>]+>/i;
60
+
61
+ var IS_REGEX_CAPTURING_BROKEN = false;
62
+ 'x'.replace(/x(.)?/g, function(m, g) {
63
+ IS_REGEX_CAPTURING_BROKEN = g === '';
64
+ });
65
+
66
+ // Empty Elements
67
+ var empty = makeMap('area,base,basefont,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr');
68
+
69
+ // Inline Elements
70
+ var inline = makeMap('a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,noscript,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,svg,textarea,tt,u,var');
71
+
72
+ // Elements that you can, intentionally, leave open
73
+ // (and which close themselves)
74
+ var closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source');
75
+
76
+ // Attributes that have their values filled in disabled='disabled'
77
+ var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected');
78
+
79
+ // Special Elements (can contain anything)
80
+ var special = makeMap('script,style');
81
+
82
+ // HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
83
+ // Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
84
+ var nonPhrasing = makeMap('address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track');
85
+
86
+ var reCache = {};
87
+
88
+ function attrForHandler(handler) {
89
+ var pattern = singleAttrIdentifier.source +
90
+ '(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
91
+ '\\s*(?:' + singleAttrValues.join('|') + '))?';
92
+ if (handler.customAttrSurround) {
93
+ var attrClauses = [];
94
+ for (var i = handler.customAttrSurround.length - 1; i >= 0; i--) {
95
+ attrClauses[i] = '(?:' +
96
+ '(' + handler.customAttrSurround[i][0].source + ')\\s*' +
97
+ pattern +
98
+ '\\s*(' + handler.customAttrSurround[i][1].source + ')' +
99
+ ')';
138
100
  }
101
+ attrClauses.push('(?:' + pattern + ')');
102
+ pattern = '(?:' + attrClauses.join('|') + ')';
139
103
  }
140
-
141
- function joinSingleAttrAssigns( handler ) {
142
- return singleAttrAssigns.concat(
143
- handler.customAttrAssign || []
144
- ).map(function (assign) {
145
- return '(?:' + assign.source + ')';
146
- }).join('|');
147
- }
148
-
149
- var HTMLParser = global.HTMLParser = function( html, handler ) {
150
- var index, match, stack = [], last = html, prevTag, nextTag;
151
- stack.last = function() {
152
- var last = this[ this.length - 1 ];
153
- return last && last.tag;
154
- };
155
-
156
- var startTag = startTagForHandler(handler);
157
- var attr = attrForHandler(handler);
158
-
159
- while ( html ) {
160
- // Make sure we're not in a script or style element
161
- if ( !stack.last() || !special[ stack.last() ] ) {
162
-
104
+ return new RegExp('^\\s*' + pattern);
105
+ }
106
+
107
+ function joinSingleAttrAssigns(handler) {
108
+ return singleAttrAssigns.concat(
109
+ handler.customAttrAssign || []
110
+ ).map(function(assign) {
111
+ return '(?:' + assign.source + ')';
112
+ }).join('|');
113
+ }
114
+
115
+ function HTMLParser(html, handler) {
116
+ var stack = [], lastTag;
117
+ var attribute = attrForHandler(handler);
118
+ var last, prevTag, nextTag;
119
+ while (html) {
120
+ last = html;
121
+ // Make sure we're not in a script or style element
122
+ if (!lastTag || !special(lastTag)) {
123
+ var textEnd = html.indexOf('<');
124
+ if (textEnd === 0) {
163
125
  // Comment:
164
- if ( /^<!--/.test( html ) ) {
165
- index = html.indexOf('-->');
126
+ if (/^<!--/.test(html)) {
127
+ var commentEnd = html.indexOf('-->');
166
128
 
167
- if ( index >= 0 ) {
168
- if ( handler.comment ) {
169
- handler.comment( html.substring( 4, index ) );
129
+ if (commentEnd >= 0) {
130
+ if (handler.comment) {
131
+ handler.comment(html.substring(4, commentEnd));
170
132
  }
171
- html = html.substring( index + 3 );
133
+ html = html.substring(commentEnd + 3);
172
134
  prevTag = '';
173
135
  continue;
174
136
  }
175
137
  }
176
138
 
177
139
  // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
178
- if ( /^<!\[/.test( html ) ) {
179
- index = html.indexOf(']>');
140
+ if (/^<!\[/.test(html)) {
141
+ var conditionalEnd = html.indexOf(']>');
180
142
 
181
- if (index >= 0) {
182
- if ( handler.comment ) {
183
- handler.comment( html.substring(2, index + 1 ), true /* non-standard */ );
143
+ if (conditionalEnd >= 0) {
144
+ if (handler.comment) {
145
+ handler.comment(html.substring(2, conditionalEnd + 1), true /* non-standard */);
184
146
  }
185
- html = html.substring( index + 2 );
147
+ html = html.substring(conditionalEnd + 2);
186
148
  prevTag = '';
187
149
  continue;
188
150
  }
189
151
  }
190
152
 
191
153
  // Doctype:
192
- if ( (match = doctype.exec( html )) ) {
193
- if ( handler.doctype ) {
194
- handler.doctype( match[0] );
154
+ var doctypeMatch = html.match(doctype);
155
+ if (doctypeMatch) {
156
+ if (handler.doctype) {
157
+ handler.doctype(doctypeMatch[0]);
195
158
  }
196
- html = html.substring( match[0].length );
159
+ html = html.substring(doctypeMatch[0].length);
197
160
  prevTag = '';
198
161
  continue;
199
162
  }
200
163
 
201
164
  // End tag:
202
- if ( /^<\//.test( html ) ) {
203
- match = html.match( endTag );
204
- if ( match ) {
205
- html = html.substring( match[0].length );
206
- match[0].replace( endTag, parseEndTag );
207
- prevTag = '/' + match[1].toLowerCase();
208
- continue;
209
- }
210
-
165
+ var endTagMatch = html.match(endTag);
166
+ if (endTagMatch) {
167
+ html = html.substring(endTagMatch[0].length);
168
+ endTagMatch[0].replace(endTag, parseEndTag);
169
+ prevTag = '/' + endTagMatch[1].toLowerCase();
170
+ continue;
211
171
  }
172
+
212
173
  // Start tag:
213
- if ( /^</.test( html ) ) {
214
- match = html.match( startTag );
215
- if ( match ) {
216
- html = html.substring( match[0].length );
217
- match[0].replace( startTag, parseStartTag );
218
- prevTag = match[1].toLowerCase();
219
- continue;
220
- }
174
+ var startTagMatch = parseStartTag(html);
175
+ if (startTagMatch) {
176
+ html = startTagMatch.rest;
177
+ handleStartTag(startTagMatch);
178
+ prevTag = startTagMatch.tagName.toLowerCase();
179
+ continue;
221
180
  }
181
+ }
222
182
 
223
- index = html.indexOf('<');
224
-
225
- var text = index < 0 ? html : html.substring( 0, index );
226
- html = index < 0 ? '' : html.substring( index );
183
+ var text;
184
+ if (textEnd >= 0) {
185
+ text = html.substring(0, textEnd);
186
+ html = html.substring(textEnd);
187
+ }
188
+ else {
189
+ text = html;
190
+ html = '';
191
+ }
227
192
 
228
- // next tag
229
- tagMatch = html.match( startTag );
230
- if (tagMatch) {
231
- nextTag = tagMatch[1];
193
+ // next tag
194
+ var nextTagMatch = parseStartTag(html);
195
+ if (nextTagMatch) {
196
+ nextTag = nextTagMatch.tagName;
197
+ }
198
+ else {
199
+ nextTagMatch = html.match(endTag);
200
+ if (nextTagMatch) {
201
+ nextTag = '/' + nextTagMatch[1];
232
202
  }
233
203
  else {
234
- tagMatch = html.match( endTag );
235
- if (tagMatch) {
236
- nextTag = '/' + tagMatch[1];
237
- }
238
- else {
239
- nextTag = '';
240
- }
241
- }
242
-
243
- if ( handler.chars ) {
244
- handler.chars(text, prevTag, nextTag);
204
+ nextTag = '';
245
205
  }
246
- prevTag = '';
247
-
248
206
  }
249
- else {
250
207
 
251
- stackedTag = stack.last().toLowerCase();
252
- reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)<\/' + stackedTag + '[^>]*>', 'i'));
208
+ if (handler.chars) {
209
+ handler.chars(text, prevTag, nextTag);
210
+ }
211
+ prevTag = '';
253
212
 
254
- html = html.replace(reStackedTag, function(all, text) {
255
- if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
256
- text = text
257
- .replace(/<!--([\s\S]*?)-->/g, '$1')
258
- .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
259
- }
213
+ }
214
+ else {
215
+ var stackedTag = lastTag.toLowerCase();
216
+ var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)</' + stackedTag + '[^>]*>', 'i'));
217
+
218
+ html = html.replace(reStackedTag, function(all, text) {
219
+ if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
220
+ text = text
221
+ .replace(/<!--([\s\S]*?)-->/g, '$1')
222
+ .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
223
+ }
260
224
 
261
- if ( handler.chars ) {
262
- handler.chars( text );
263
- }
225
+ if (handler.chars) {
226
+ handler.chars(text);
227
+ }
264
228
 
265
- return '';
266
- });
229
+ return '';
230
+ });
267
231
 
268
- parseEndTag( '', stackedTag );
269
- }
232
+ parseEndTag('</' + stackedTag + '>', stackedTag);
233
+ }
270
234
 
271
- if ( html === last ) {
272
- throw 'Parse Error: ' + html;
273
- }
274
- last = html;
235
+ if (html === last) {
236
+ throw new Error('Parse Error: ' + html);
275
237
  }
238
+ }
276
239
 
240
+ if (!handler.partialMarkup) {
277
241
  // Clean up any remaining tags
278
242
  parseEndTag();
243
+ }
279
244
 
280
- function parseStartTag( tag, tagName, rest, unary ) {
281
- var unarySlash = false;
282
-
283
- while ( !handler.html5 && stack.last() && inline[ stack.last() ]) {
284
- parseEndTag( '', stack.last() );
245
+ function parseStartTag(input) {
246
+ var start = input.match(startTagOpen);
247
+ if (start) {
248
+ var match = {
249
+ tagName: start[1],
250
+ attrs: []
251
+ };
252
+ input = input.slice(start[0].length);
253
+ var end, attr;
254
+ while (!(end = input.match(startTagClose)) && (attr = input.match(attribute))) {
255
+ input = input.slice(attr[0].length);
256
+ match.attrs.push(attr);
285
257
  }
286
-
287
- if ( closeSelf[ tagName ] && stack.last() === tagName ) {
288
- parseEndTag( '', tagName );
258
+ if (end) {
259
+ match.unarySlash = end[1];
260
+ match.rest = input.slice(end[0].length);
261
+ return match;
289
262
  }
263
+ }
264
+ }
290
265
 
291
- unary = empty[ tagName ] || !!unary;
266
+ function handleStartTag(match) {
267
+ var tagName = match.tagName;
268
+ var unarySlash = match.unarySlash;
292
269
 
293
- var attrs = [];
270
+ if (handler.html5 && lastTag === 'p' && nonPhrasing(tagName)) {
271
+ parseEndTag('', lastTag);
272
+ }
294
273
 
295
- rest.replace(attr, function () {
296
- var name, value, fallbackValue, customOpen, customClose, customAssign, quote;
297
- var ncp = 7; // number of captured parts, scalar
274
+ if (!handler.html5) {
275
+ while (lastTag && inline(lastTag)) {
276
+ parseEndTag('', lastTag);
277
+ }
278
+ }
298
279
 
299
- // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
300
- if (IS_REGEX_CAPTURING_BROKEN && arguments[0].indexOf('""') === -1) {
301
- if (arguments[3] === '') { arguments[3] = undefined; }
302
- if (arguments[4] === '') { arguments[4] = undefined; }
303
- if (arguments[5] === '') { arguments[5] = undefined; }
304
- }
280
+ if (closeSelf(tagName) && lastTag === tagName) {
281
+ parseEndTag('', tagName);
282
+ }
305
283
 
306
- name = arguments[1];
307
- if ( name ) {
308
- customAssign = arguments[2];
309
- fallbackValue = arguments[3];
310
- value = fallbackValue || arguments[4] || arguments[5];
284
+ var unary = empty(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash;
311
285
 
312
- if (customAssign) {
313
- quote = arguments[0].charAt(name.length + customAssign.length);
314
- quote = (quote === '\'' || quote === '"') ? quote : '';
315
- }
286
+ var attrs = match.attrs.map(function(args) {
287
+ var name, value, customOpen, customClose, customAssign, quote;
288
+ var ncp = 7; // number of captured parts, scalar
289
+
290
+ // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
291
+ if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
292
+ if (args[3] === '') { delete args[3]; }
293
+ if (args[4] === '') { delete args[4]; }
294
+ if (args[5] === '') { delete args[5]; }
295
+ }
316
296
 
297
+ function populate(index) {
298
+ customAssign = args[index];
299
+ value = args[index + 1];
300
+ if (typeof value !== 'undefined') {
301
+ return '"';
317
302
  }
318
- else if ( handler.customAttrSurround ) {
319
- for ( var i = handler.customAttrSurround.length - 1; i >= 0; i-- ) {
320
- name = arguments[i * ncp + 7];
321
- customAssign = arguments[i * ncp + 8];
322
- if ( name ) {
323
- fallbackValue = arguments[i * ncp + 9];
324
- value = fallbackValue
325
- || arguments[i * ncp + 10]
326
- || arguments[i * ncp + 11];
327
- customOpen = arguments[i * ncp + 6];
328
- customClose = arguments[i * ncp + 12];
329
- break;
330
- }
331
- }
303
+ value = args[index + 2];
304
+ if (typeof value !== 'undefined') {
305
+ return '\'';
332
306
  }
333
-
334
- if ( value === undefined ) {
335
- value = fillAttrs[name] ? name : fallbackValue;
307
+ value = args[index + 3];
308
+ if (typeof value === 'undefined' && fillAttrs(name)) {
309
+ value = name;
336
310
  }
311
+ return '';
312
+ }
337
313
 
338
- attrs.push({
339
- name: name,
340
- value: value,
341
- customAssign: customAssign || '=',
342
- customOpen: customOpen || '',
343
- customClose: customClose || '',
344
- quote: quote || ''
345
- });
346
- });
347
-
348
- if ( !unary ) {
349
- stack.push( { tag: tagName, attrs: attrs } );
314
+ var j = 1;
315
+ if (handler.customAttrSurround) {
316
+ for (var i = 0, l = handler.customAttrSurround.length; i < l; i++, j += ncp) {
317
+ name = args[j + 1];
318
+ if (name) {
319
+ quote = populate(j + 2);
320
+ customOpen = args[j];
321
+ customClose = args[j + 6];
322
+ break;
323
+ }
324
+ }
350
325
  }
351
- else {
352
- unarySlash = tag.match( endingSlash );
326
+
327
+ if (!name && (name = args[j])) {
328
+ quote = populate(j + 1);
353
329
  }
354
330
 
331
+ return {
332
+ name: name,
333
+ value: value,
334
+ customAssign: customAssign || '=',
335
+ customOpen: customOpen || '',
336
+ customClose: customClose || '',
337
+ quote: quote || ''
338
+ };
339
+ });
355
340
 
356
- if ( handler.start ) {
357
- handler.start( tagName, attrs, unary, unarySlash );
358
- }
341
+ if (!unary) {
342
+ stack.push({ tag: tagName, attrs: attrs });
343
+ lastTag = tagName;
344
+ unarySlash = '';
345
+ }
346
+
347
+ if (handler.start) {
348
+ handler.start(tagName, attrs, unary, unarySlash);
359
349
  }
350
+ }
360
351
 
361
- function parseEndTag( tag, tagName ) {
362
- var pos;
352
+ function parseEndTag(tag, tagName) {
353
+ var pos;
363
354
 
364
- // If no tag name is provided, clean shop
365
- if ( !tagName ) {
366
- pos = 0;
367
- }
368
- else {
369
- // Find the closest opened tag of the same type
370
- var needle = tagName.toLowerCase();
371
- for ( pos = stack.length - 1; pos >= 0; pos-- ) {
372
- if ( stack[ pos ].tag.toLowerCase() === needle ) {
373
- break;
374
- }
355
+ // Find the closest opened tag of the same type
356
+ if (tagName) {
357
+ var needle = tagName.toLowerCase();
358
+ for (pos = stack.length - 1; pos >= 0; pos--) {
359
+ if (stack[pos].tag.toLowerCase() === needle) {
360
+ break;
375
361
  }
376
362
  }
363
+ }
364
+ // If no tag name is provided, clean shop
365
+ else {
366
+ pos = 0;
367
+ }
377
368
 
378
- if ( pos >= 0 ) {
379
- // Close all the open elements, up the stack
380
- for ( var i = stack.length - 1; i >= pos; i-- ) {
381
- if ( handler.end ) {
382
- handler.end( stack[ i ].tag, stack[ i ].attrs );
383
- }
369
+ if (pos >= 0) {
370
+ // Close all the open elements, up the stack
371
+ for (var i = stack.length - 1; i >= pos; i--) {
372
+ if (handler.end) {
373
+ handler.end(stack[i].tag, stack[i].attrs, i > pos || !tag);
384
374
  }
375
+ }
385
376
 
386
- // Remove the open elements from the stack
387
- stack.length = pos;
377
+ // Remove the open elements from the stack
378
+ stack.length = pos;
379
+ lastTag = pos && stack[pos - 1].tag;
380
+ }
381
+ else if (tagName.toLowerCase() === 'br') {
382
+ if (handler.start) {
383
+ handler.start(tagName, [], true, '');
388
384
  }
389
385
  }
390
- };
391
-
392
- global.HTMLtoXML = function( html ) {
393
- var results = '';
386
+ else if (tagName.toLowerCase() === 'p') {
387
+ if (handler.start) {
388
+ handler.start(tagName, [], false, '', true);
389
+ }
390
+ if (handler.end) {
391
+ handler.end(tagName, []);
392
+ }
393
+ }
394
+ }
395
+ }
394
396
 
395
- new HTMLParser(html, {
396
- start: function( tag, attrs, unary ) {
397
- results += '<' + tag;
397
+ exports.HTMLParser = HTMLParser;
398
+ exports.HTMLtoXML = function(html) {
399
+ var results = '';
398
400
 
399
- for ( var i = 0; i < attrs.length; i++ ) {
400
- results += ' ' + attrs[i].name + '="' + (attrs[i].value || '').replace(/"/g, '&#34;') + '"';
401
- }
401
+ new HTMLParser(html, {
402
+ start: function(tag, attrs, unary) {
403
+ results += '<' + tag;
402
404
 
403
- results += (unary ? '/' : '') + '>';
404
- },
405
- end: function( tag ) {
406
- results += '</' + tag + '>';
407
- },
408
- chars: function( text ) {
409
- results += text;
410
- },
411
- comment: function( text ) {
412
- results += '<!--' + text + '-->';
413
- },
414
- ignore: function(text) {
415
- results += text;
405
+ for (var i = 0, len = attrs.length; i < len; i++) {
406
+ results += ' ' + attrs[i].name + '="' + (attrs[i].value || '').replace(/"/g, '&#34;') + '"';
416
407
  }
417
- });
418
408
 
419
- return results;
420
- };
409
+ results += (unary ? '/' : '') + '>';
410
+ },
411
+ end: function(tag) {
412
+ results += '</' + tag + '>';
413
+ },
414
+ chars: function(text) {
415
+ results += text;
416
+ },
417
+ comment: function(text) {
418
+ results += '<!--' + text + '-->';
419
+ },
420
+ ignore: function(text) {
421
+ results += text;
422
+ }
423
+ });
421
424
 
422
- global.HTMLtoDOM = function( html, doc ) {
423
- // There can be only one of these elements
424
- var one = makeMap('html,head,body,title');
425
+ return results;
426
+ };
425
427
 
426
- // Enforce a structure for the document
427
- var structure = {
428
- link: 'head',
429
- base: 'head'
430
- };
428
+ exports.HTMLtoDOM = function(html, doc) {
429
+ // There can be only one of these elements
430
+ var one = {
431
+ html: true,
432
+ head: true,
433
+ body: true,
434
+ title: true
435
+ };
431
436
 
432
- if ( !doc ) {
433
- if ( typeof DOMDocument !== 'undefined' ) {
434
- doc = new DOMDocument();
435
- }
436
- else if ( typeof document !== 'undefined' && document.implementation && document.implementation.createDocument ) {
437
- doc = document.implementation.createDocument('', '', null);
438
- }
439
- else if ( typeof ActiveX !== 'undefined' ) {
440
- doc = new ActiveXObject('Msxml.DOMDocument');
441
- }
437
+ // Enforce a structure for the document
438
+ var structure = {
439
+ link: 'head',
440
+ base: 'head'
441
+ };
442
442
 
443
- }
444
- else {
445
- doc = doc.ownerDocument ||
446
- doc.getOwnerDocument && doc.getOwnerDocument() ||
447
- doc;
448
- }
443
+ if (doc) {
444
+ doc = doc.ownerDocument || doc.getOwnerDocument && doc.getOwnerDocument() || doc;
445
+ }
446
+ else if (typeof DOMDocument !== 'undefined') {
447
+ doc = new DOMDocument();
448
+ }
449
+ else if (typeof document !== 'undefined' && document.implementation && document.implementation.createDocument) {
450
+ doc = document.implementation.createDocument('', '', null);
451
+ }
452
+ else if (typeof ActiveX !== 'undefined') {
453
+ doc = new ActiveXObject('Msxml.DOMDocument');
454
+ }
449
455
 
450
- var elems = [],
456
+ var elems = [],
451
457
  documentElement = doc.documentElement ||
452
458
  doc.getDocumentElement && doc.getDocumentElement();
453
459
 
454
- // If we're dealing with an empty document then we
455
- // need to pre-populate it with the HTML document structure
456
- if ( !documentElement && doc.createElement ) {
457
- (function() {
458
- var html = doc.createElement('html');
459
- var head = doc.createElement('head');
460
- head.appendChild( doc.createElement('title') );
461
- html.appendChild( head );
462
- html.appendChild( doc.createElement('body') );
463
- doc.appendChild( html );
464
- })();
465
- }
460
+ // If we're dealing with an empty document then we
461
+ // need to pre-populate it with the HTML document structure
462
+ if (!documentElement && doc.createElement) {
463
+ (function() {
464
+ var html = doc.createElement('html');
465
+ var head = doc.createElement('head');
466
+ head.appendChild(doc.createElement('title'));
467
+ html.appendChild(head);
468
+ html.appendChild(doc.createElement('body'));
469
+ doc.appendChild(html);
470
+ })();
471
+ }
466
472
 
467
- // Find all the unique elements
468
- if ( doc.getElementsByTagName ) {
469
- for ( var i in one ) {
470
- one[ i ] = doc.getElementsByTagName( i )[0];
471
- }
473
+ // Find all the unique elements
474
+ if (doc.getElementsByTagName) {
475
+ for (var i in one) {
476
+ one[i] = doc.getElementsByTagName(i)[0];
472
477
  }
478
+ }
473
479
 
474
- // If we're working with a document, inject contents into
475
- // the body element
476
- var curParentNode = one.body;
477
-
478
- new HTMLParser( html, {
479
- start: function( tagName, attrs, unary ) {
480
- // If it's a pre-built element, then we can ignore
481
- // its construction
482
- if ( one[ tagName ] ) {
483
- curParentNode = one[ tagName ];
484
- return;
485
- }
486
-
487
- var elem = doc.createElement( tagName );
488
-
489
- for ( var attr in attrs ) {
490
- elem.setAttribute( attrs[ attr ].name, attrs[ attr ].value );
491
- }
480
+ // If we're working with a document, inject contents into
481
+ // the body element
482
+ var curParentNode = one.body;
483
+
484
+ new HTMLParser(html, {
485
+ start: function(tagName, attrs, unary) {
486
+ // If it's a pre-built element, then we can ignore
487
+ // its construction
488
+ if (one[tagName]) {
489
+ curParentNode = one[tagName];
490
+ return;
491
+ }
492
492
 
493
- if ( structure[ tagName ] && typeof one[ structure[ tagName ] ] !== 'boolean' ) {
494
- one[ structure[ tagName ] ].appendChild( elem );
495
- }
496
- else if ( curParentNode && curParentNode.appendChild ) {
497
- curParentNode.appendChild( elem );
498
- }
493
+ var elem = doc.createElement(tagName);
499
494
 
500
- if ( !unary ) {
501
- elems.push( elem );
502
- curParentNode = elem;
503
- }
504
- },
505
- end: function( /* tag */ ) {
506
- elems.length -= 1;
507
-
508
- // Init the new parentNode
509
- curParentNode = elems[ elems.length - 1 ];
510
- },
511
- chars: function( text ) {
512
- curParentNode.appendChild( doc.createTextNode( text ) );
513
- },
514
- comment: function( /*text*/ ) {
515
- // create comment node
516
- },
517
- ignore: function( /* text */ ) {
518
- // What to do here?
495
+ for (var attr in attrs) {
496
+ elem.setAttribute(attrs[attr].name, attrs[attr].value);
519
497
  }
520
- });
521
498
 
522
- return doc;
523
- };
499
+ if (structure[tagName] && typeof one[structure[tagName]] !== 'boolean') {
500
+ one[structure[tagName]].appendChild(elem);
501
+ }
502
+ else if (curParentNode && curParentNode.appendChild) {
503
+ curParentNode.appendChild(elem);
504
+ }
524
505
 
525
- function makeMap(str) {
526
- var obj = {}, items = str.split(',');
527
- for ( var i = 0; i < items.length; i++ ) {
528
- obj[ items[i] ] = true;
529
- obj[ items[i].toUpperCase() ] = true;
506
+ if (!unary) {
507
+ elems.push(elem);
508
+ curParentNode = elem;
509
+ }
510
+ },
511
+ end: function(/* tag */) {
512
+ elems.length -= 1;
513
+
514
+ // Init the new parentNode
515
+ curParentNode = elems[elems.length - 1];
516
+ },
517
+ chars: function(text) {
518
+ curParentNode.appendChild(doc.createTextNode(text));
519
+ },
520
+ comment: function(/* text */) {
521
+ // create comment node
522
+ },
523
+ ignore: function(/* text */) {
524
+ // What to do here?
530
525
  }
531
- return obj;
532
- }
533
- })(typeof exports === 'undefined' ? this : exports);
526
+ });
527
+
528
+ return doc;
529
+ };