1// An html parser written in JavaScript 2// Based on http://ejohn.org/blog/pure-javascript-html-parser/ 3 4(function() { 5 var supports = (function() { 6 var supports = {}; 7 8 var html; 9 var work = this.document.createElement('div'); 10 11 html = "<P><I></P></I>"; 12 work.innerHTML = html; 13 supports.tagSoup = work.innerHTML !== html; 14 15 work.innerHTML = "<P><i><P></P></i></P>"; 16 supports.selfClose = work.childNodes.length === 2; 17 18 return supports; 19 })(); 20 21 22 23 // Regular Expressions for parsing tags and attributes 24 var startTag = /^<([\-A-Za-z0-9_]+)((?:\s+[\w\-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/; 25 var endTag = /^<\/([\-A-Za-z0-9_]+)[^>]*>/; 26 var attr = /([\-A-Za-z0-9_]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; 27 var fillAttr = /^(checked|compact|declare|defer|disabled|ismap|multiple|nohref|noresize|noshade|nowrap|readonly|selected)$/i; 28 29 var DEBUG = false; 30 31 function htmlParser(stream, options) { 32 stream = stream || ''; 33 34 // Options 35 options = options || {}; 36 37 for(var key in supports) { 38 if(supports.hasOwnProperty(key)) { 39 if(options.autoFix) { 40 options['fix_'+key] = true;//!supports[key]; 41 } 42 options.fix = options.fix || options['fix_'+key]; 43 } 44 } 45 46 var stack = []; 47 48 var append = function(str) { 49 stream += str; 50 }; 51 52 var prepend = function(str) { 53 stream = str + stream; 54 }; 55 56 // Order of detection matters: detection of one can only 57 // succeed if detection of previous didn't 58 var detect = { 59 comment: /^<!--/, 60 endTag: /^<\//, 61 atomicTag: /^<\s*(script|style|noscript)[\s>]/i, 62 startTag: /^</, 63 chars: /^[^<]/ 64 }; 65 66 // Detection has already happened when a reader is called. 67 var reader = { 68 69 comment: function() { 70 var index = stream.indexOf("-->"); 71 if ( index >= 0 ) { 72 return { 73 content: stream.substr(4, index), 74 length: index + 3 75 }; 76 } 77 }, 78 79 endTag: function() { 80 var match = stream.match( endTag ); 81 82 if ( match ) { 83 return { 84 tagName: match[1], 85 length: match[0].length 86 }; 87 } 88 }, 89 90 atomicTag: function() { 91 var start = reader.startTag(); 92 if(start) { 93 var rest = stream.slice(start.length); 94 // for optimization, we check first just for the end tag 95 if(rest.match(new RegExp("<\/\\s*" + start.tagName + "\\s*>", "i"))) { 96 // capturing the content is inefficient, so we do it inside the if 97 var match = rest.match(new RegExp("([\\s\\S]*?)<\/\\s*" + start.tagName + "\\s*>", "i")); 98 if(match) { 99 // good to go 100 return { 101 tagName: start.tagName, 102 attrs: start.attrs, 103 content: match[1], 104 length: match[0].length + start.length 105 }; 106 } 107 } 108 } 109 }, 110 111 startTag: function() { 112 var match = stream.match( startTag ); 113 114 if ( match ) { 115 var attrs = {}; 116 117 match[2].replace(attr, function(match, name) { 118 var value = arguments[2] || arguments[3] || arguments[4] || 119 fillAttr.test(name) && name || null; 120 121 attrs[name] = value; 122 }); 123 124 return { 125 tagName: match[1], 126 attrs: attrs, 127 unary: !!match[3], 128 length: match[0].length 129 }; 130 } 131 }, 132 133 chars: function() { 134 var index = stream.indexOf("<"); 135 return { 136 length: index >= 0 ? index : stream.length 137 }; 138 } 139 }; 140 141 var readToken = function() { 142 143 // Enumerate detects in order 144 for (var type in detect) { 145 146 if(detect[type].test(stream)) { 147 if(DEBUG) { console.log('suspected ' + type); } 148 149 var token = reader[type](); 150 if(token) { 151 if(DEBUG) { console.log('parsed ' + type, token); } 152 // Type 153 token.type = token.type || type; 154 // Entire text 155 token.text = stream.substr(0, token.length); 156 // Update the stream 157 stream = stream.slice(token.length); 158 159 return token; 160 } 161 return null; 162 } 163 } 164 }; 165 166 var readTokens = function(handlers) { 167 var tok; 168 while(tok = readToken()) { 169 // continue until we get an explicit "false" return 170 if(handlers[tok.type] && handlers[tok.type](tok) === false) { 171 return; 172 } 173 } 174 }; 175 176 var clear = function() { 177 var rest = stream; 178 stream = ''; 179 return rest; 180 }; 181 182 var rest = function() { 183 return stream; 184 }; 185 186 if(options.fix) { 187 (function() { 188 // Empty Elements - HTML 4.01 189 var EMPTY = /^(AREA|BASE|BASEFONT|BR|COL|FRAME|HR|IMG|INPUT|ISINDEX|LINK|META|PARAM|EMBED)$/i; 190 191 // Elements that you can| intentionally| leave open 192 // (and which close themselves) 193 var CLOSESELF = /^(COLGROUP|DD|DT|LI|OPTIONS|P|TD|TFOOT|TH|THEAD|TR)$/i; 194 195 196 var stack = []; 197 stack.last = function() { 198 return this[this.length - 1]; 199 }; 200 stack.lastTagNameEq = function(tagName) { 201 var last = this.last(); 202 return last && last.tagName && 203 last.tagName.toUpperCase() === tagName.toUpperCase(); 204 }; 205 206 stack.containsTagName = function(tagName) { 207 for(var i = 0, tok; tok = this[i]; i++) { 208 if(tok.tagName === tagName) { 209 return true; 210 } 211 } 212 return false;
213 }; 214 215 var correct = function(tok) { 216 if(tok && tok.type === 'startTag') { 217 // unary 218 tok.unary = EMPTY.test(tok.tagName) || tok.unary; 219 } 220 return tok; 221 }; 222 223 var readTokenImpl = readToken; 224 225 var peekToken = function() { 226 var tmp = stream; 227 var tok = correct(readTokenImpl()); 228 stream = tmp; 229 return tok; 230 }; 231 232 var closeLast = function() { 233 var tok = stack.pop(); 234 235 // prepend close tag to stream. 236 prepend('</'+tok.tagName+'>'); 237 }; 238 239 var handlers = { 240 startTag: function(tok) { 241 var tagName = tok.tagName; 242 // Fix tbody 243 if(tagName.toUpperCase() === 'TR' && stack.lastTagNameEq('TABLE')) { 244 prepend('<TBODY>'); 245 prepareNextToken(); 246 } else if(options.fix_selfClose && 247 CLOSESELF.test(tagName) && 248 stack.containsTagName(tagName)) { 249 if(stack.lastTagNameEq(tagName)) { 250 closeLast(); 251 } else { 252 prepend('</'+tok.tagName+'>'); 253 prepareNextToken(); 254 } 255 } else if (!tok.unary) { 256 stack.push(tok); 257 } 258 }, 259 260 endTag: function(tok) { 261 var last = stack.last(); 262 if(last) { 263 if(options.fix_tagSoup && !stack.lastTagNameEq(tok.tagName)) { 264 // cleanup tag soup 265 closeLast(); 266 } else { 267 stack.pop(); 268 } 269 } else if (options.fix_tagSoup) { 270 // cleanup tag soup part 2: skip this token 271 skipToken(); 272 } 273 } 274 }; 275 276 var skipToken = function() { 277 // shift the next token 278 readTokenImpl(); 279 280 prepareNextToken(); 281 }; 282 283 var prepareNextToken = function() { 284 var tok = peekToken(); 285 if(tok && handlers[tok.type]) { 286 handlers[tok.type](tok); 287 } 288 }; 289 290 // redefine readToken 291 readToken = function() { 292 prepareNextToken(); 293 return correct(readTokenImpl()); 294 }; 295 })(); 296 } 297 298 return { 299 append: append, 300 readToken: readToken, 301 readTokens: readTokens, 302 clear: clear, 303 rest: rest, 304 stack: stack 305 }; 306 307 } 308 309 htmlParser.supports = supports; 310 311 htmlParser.tokenToString = function(tok) { 312 var handler = { 313 comment: function(tok) { 314 return '<--' + tok.content + '-->'; 315 }, 316 endTag: function(tok) { 317 return '</'+tok.tagName+'>'; 318 }, 319 atomicTag: function(tok) { 320 console.log(tok); 321 return handler.startTag(tok) + 322 tok.content + 323 handler.endTag(tok); 324 }, 325 startTag: function(tok) { 326 var str = '<'+tok.tagName; 327 for (var key in tok.attrs) { 328 var val = tok.attrs[key]; 329 // escape quotes 330 str += ' '+key+'="'+(val ? val.replace(/(^|[^\\])"/g, '$1\\\"') : '')+'"'; 331 } 332 return str + (tok.unary ? '/>' : '>'); 333 }, 334 chars: function(tok) { 335 return tok.text; 336 } 337 }; 338 return handler[tok.type](tok); 339 }; 340 341 htmlParser.escapeAttributes = function(attrs) { 342 var escapedAttrs = {}; 343 // escape double-quotes for writing html as a string 344 345 for(var name in attrs) { 346 var value = attrs[name]; 347 escapedAttrs[name] = value && value.replace(/(^|[^\\])"/g, '$1\\\"'); 348 } 349 return escapedAttrs; 350 }; 351 352 for(var key in supports) { 353 htmlParser.browserHasFlaw = htmlParser.browserHasFlaw || (!supports[key]) && key; 354 } 355 356 this.htmlParser = htmlParser; 357})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.