├── .gitignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── SpecRunner.html ├── bower.json ├── build ├── html-string.js └── html-string.min.js ├── external └── fsm.js ├── npm-debug.log ├── package.json ├── spec ├── html-string-spec.js └── spec-helper.js └── src ├── characters.coffee ├── namespace.coffee ├── spec ├── html-string-spec.coffee └── spec-helper.coffee ├── strings.coffee └── tags.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | jasmine 2 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: 5 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | # Project configuration 4 | grunt.initConfig({ 5 | 6 | pkg: grunt.file.readJSON('package.json') 7 | 8 | coffee: 9 | options: 10 | join: true 11 | 12 | build: 13 | files: 14 | 'src/tmp/html-string.js': [ 15 | 'src/namespace.coffee' 16 | 'src/strings.coffee' 17 | 'src/tags.coffee' 18 | 'src/characters.coffee' 19 | ] 20 | 21 | spec: 22 | files: 23 | 'spec/spec-helper.js': 'src/spec/spec-helper.coffee' 24 | 'spec/html-string-spec.js': 'src/spec/html-string-spec.coffee' 25 | 26 | uglify: 27 | options: 28 | banner: '/*! <%= pkg.name %> v<%= pkg.version %> by <%= pkg.author.name %> <<%= pkg.author.email %>> (<%= pkg.author.url %>) */\n' 29 | mangle: false 30 | 31 | build: 32 | src: 'build/html-string.js' 33 | dest: 'build/html-string.min.js' 34 | 35 | concat: 36 | build: 37 | src: [ 38 | 'external/fsm.js' 39 | 'src/tmp/html-string.js' 40 | ] 41 | dest: 'build/html-string.js' 42 | 43 | clean: 44 | build: ['src/tmp'] 45 | 46 | jasmine: 47 | build: 48 | src: ['build/html-string.js'] 49 | options: 50 | specs: 'spec/html-string-spec.js' 51 | helpers: 'spec/spec-helper.js' 52 | 53 | watch: 54 | build: 55 | files: ['src/*.coffee'] 56 | tasks: ['build'] 57 | 58 | spec: 59 | files: ['src/spec/*.coffee'] 60 | tasks: ['spec'] 61 | }) 62 | 63 | # Plug-ins 64 | grunt.loadNpmTasks 'grunt-contrib-clean' 65 | grunt.loadNpmTasks 'grunt-contrib-coffee' 66 | grunt.loadNpmTasks 'grunt-contrib-concat' 67 | grunt.loadNpmTasks 'grunt-contrib-jasmine' 68 | grunt.loadNpmTasks 'grunt-contrib-uglify' 69 | grunt.loadNpmTasks 'grunt-contrib-watch' 70 | 71 | # Tasks 72 | grunt.registerTask 'build', [ 73 | 'coffee:build' 74 | 'concat:build' 75 | 'uglify:build' 76 | 'clean:build' 77 | ] 78 | 79 | grunt.registerTask 'spec', [ 80 | 'coffee:spec' 81 | ] 82 | 83 | grunt.registerTask 'watch-build', ['watch:build'] 84 | grunt.registerTask 'watch-spec', ['watch:spec'] 85 | 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Getme Limited (http://getme.co.uk) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTMLString 2 | 3 | [![Build Status](https://travis-ci.org/GetmeUK/HTMLString.svg?branch=master)](https://travis-ci.org/GetmeUK/HTMLString) 4 | 5 | > An HTML parser written in JavaScript that's probably not what you're looking for. 6 | 7 | ## Install 8 | 9 | **Using bower** 10 | 11 | ``` 12 | bower install --save HTMLString 13 | ``` 14 | 15 | **Using npm** 16 | 17 | ``` 18 | npm install --save HTMLString 19 | ``` 20 | 21 | ## Building 22 | To build the library you'll need to use Grunt. First install the required node modules ([grunt-cli](http://gruntjs.com/getting-started) must be installed): 23 | ``` 24 | git clone https://github.com/GetmeUK/HTMLString.git 25 | cd HTMLString 26 | npm install 27 | ``` 28 | 29 | Then run `grunt build` to build the project. 30 | 31 | ## Testing 32 | To test the library you'll need to use Jasmine. First install Jasmine: 33 | ``` 34 | git clone https://github.com/pivotal/jasmine.git 35 | mkdir HTMLString/jasmine 36 | mv jasmine/dist/jasmine-standalone-2.0.3.zip HTMLString/jasmine 37 | cd HTMLString/jasmine 38 | unzip jasmine-standalone-2.0.3.zip 39 | ``` 40 | 41 | Then open `HTMLString/SpecRunner.html` in a browser to run the tests. 42 | 43 | Alternatively you can use `grunt jasmine` to run the tests from the command line. 44 | 45 | ## Documentation 46 | Full documentation is available at http://getcontenttools.com/api/html-string 47 | 48 | ## Browser support 49 | - Chrome 50 | - Firefox 51 | - IE9+ 52 | -------------------------------------------------------------------------------- /SpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTMLString spec runner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTMLString", 3 | "description": "An HTML parser written in JavaScript that's probably not what you're looking for.", 4 | "main": "build/html-string.js", 5 | "authors": [ 6 | { 7 | "name": "Anthony Blackshaw", 8 | "email": "ant@getme.co.uk", 9 | "url": "https://github.com/anthonyjb" 10 | } 11 | ], 12 | "license": "MIT", 13 | "keywords": [ 14 | "html", 15 | "parser" 16 | ], 17 | "homepage": "http://getcontenttools.com/api/html-string", 18 | "repository": { 19 | "type": "git", 20 | "url": "git@github.com:GetmeUK/HTMLString.git" 21 | }, 22 | "moduleType": [ 23 | "globals" 24 | ], 25 | "ignore": [ 26 | "**/.*", 27 | "node_modules", 28 | "bower_components", 29 | "test", 30 | "tests" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /build/html-string.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var FSM, exports; 3 | 4 | FSM = {}; 5 | 6 | FSM.Machine = (function() { 7 | function Machine(context) { 8 | this.context = context; 9 | this._stateTransitions = {}; 10 | this._stateTransitionsAny = {}; 11 | this._defaultTransition = null; 12 | this._initialState = null; 13 | this._currentState = null; 14 | } 15 | 16 | Machine.prototype.addTransition = function(action, state, nextState, callback) { 17 | if (!nextState) { 18 | nextState = state; 19 | } 20 | return this._stateTransitions[[action, state]] = [nextState, callback]; 21 | }; 22 | 23 | Machine.prototype.addTransitions = function(actions, state, nextState, callback) { 24 | var action, _i, _len, _results; 25 | if (!nextState) { 26 | nextState = state; 27 | } 28 | _results = []; 29 | for (_i = 0, _len = actions.length; _i < _len; _i++) { 30 | action = actions[_i]; 31 | _results.push(this.addTransition(action, state, nextState, callback)); 32 | } 33 | return _results; 34 | }; 35 | 36 | Machine.prototype.addTransitionAny = function(state, nextState, callback) { 37 | if (!nextState) { 38 | nextState = state; 39 | } 40 | return this._stateTransitionsAny[state] = [nextState, callback]; 41 | }; 42 | 43 | Machine.prototype.setDefaultTransition = function(state, callback) { 44 | return this._defaultTransition = [state, callback]; 45 | }; 46 | 47 | Machine.prototype.getTransition = function(action, state) { 48 | if (this._stateTransitions[[action, state]]) { 49 | return this._stateTransitions[[action, state]]; 50 | } else if (this._stateTransitionsAny[state]) { 51 | return this._stateTransitionsAny[state]; 52 | } else if (this._defaultTransition) { 53 | return this._defaultTransition; 54 | } 55 | throw new Error("Transition is undefined: (" + action + ", " + state + ")"); 56 | }; 57 | 58 | Machine.prototype.getCurrentState = function() { 59 | return this._currentState; 60 | }; 61 | 62 | Machine.prototype.setInitialState = function(state) { 63 | this._initialState = state; 64 | if (!this._currentState) { 65 | return this.reset(); 66 | } 67 | }; 68 | 69 | Machine.prototype.reset = function() { 70 | return this._currentState = this._initialState; 71 | }; 72 | 73 | Machine.prototype.process = function(action) { 74 | var result; 75 | result = this.getTransition(action, this._currentState); 76 | if (result[1]) { 77 | result[1].call(this.context || (this.context = this), action); 78 | } 79 | return this._currentState = result[0]; 80 | }; 81 | 82 | return Machine; 83 | 84 | })(); 85 | 86 | if (typeof window !== 'undefined') { 87 | window.FSM = FSM; 88 | } 89 | 90 | if (typeof module !== 'undefined' && module.exports) { 91 | exports = module.exports = FSM; 92 | } 93 | 94 | }).call(this); 95 | 96 | (function() { 97 | var ALPHA_CHARS, ALPHA_NUMERIC_CHARS, ATTR_DELIM, ATTR_ENTITY_DOUBLE_DELIM, ATTR_ENTITY_NO_DELIM, ATTR_ENTITY_SINGLE_DELIM, ATTR_NAME, ATTR_NAME_CHARS, ATTR_NAME_FIND_VALUE, ATTR_OR_TAG_END, ATTR_VALUE_DOUBLE_DELIM, ATTR_VALUE_NO_DELIM, ATTR_VALUE_SINGLE_DELIM, CHAR_OR_ENTITY_OR_TAG, CLOSING_TAG, ENTITY, ENTITY_CHARS, HTMLString, OPENING_TAG, OPENNING_OR_CLOSING_TAG, TAG_NAME_CHARS, TAG_NAME_CLOSING, TAG_NAME_MUST_CLOSE, TAG_NAME_OPENING, TAG_OPENING_SELF_CLOSING, exports, _Parser, 98 | __slice = [].slice, 99 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 100 | 101 | HTMLString = {}; 102 | 103 | if (typeof window !== 'undefined') { 104 | window.HTMLString = HTMLString; 105 | } 106 | 107 | if (typeof module !== 'undefined' && module.exports) { 108 | exports = module.exports = HTMLString; 109 | } 110 | 111 | HTMLString.String = (function() { 112 | String._parser = null; 113 | 114 | function String(html, preserveWhitespace) { 115 | if (preserveWhitespace == null) { 116 | preserveWhitespace = false; 117 | } 118 | this._preserveWhitespace = preserveWhitespace; 119 | if (html) { 120 | if (HTMLString.String._parser === null) { 121 | HTMLString.String._parser = new _Parser(); 122 | } 123 | this.characters = HTMLString.String._parser.parse(html, this._preserveWhitespace).characters; 124 | } else { 125 | this.characters = []; 126 | } 127 | } 128 | 129 | String.prototype.isWhitespace = function() { 130 | var c, _i, _len, _ref; 131 | _ref = this.characters; 132 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 133 | c = _ref[_i]; 134 | if (!c.isWhitespace()) { 135 | return false; 136 | } 137 | } 138 | return true; 139 | }; 140 | 141 | String.prototype.length = function() { 142 | return this.characters.length; 143 | }; 144 | 145 | String.prototype.preserveWhitespace = function() { 146 | return this._preserveWhitespace; 147 | }; 148 | 149 | String.prototype.capitalize = function() { 150 | var c, newString; 151 | newString = this.copy(); 152 | if (newString.length()) { 153 | c = newString.characters[0]._c.toUpperCase(); 154 | newString.characters[0]._c = c; 155 | } 156 | return newString; 157 | }; 158 | 159 | String.prototype.charAt = function(index) { 160 | return this.characters[index].copy(); 161 | }; 162 | 163 | String.prototype.concat = function() { 164 | var c, indexChar, inheritFormat, inheritedTags, newString, string, strings, tail, _i, _j, _k, _l, _len, _len1, _len2, _ref, _ref1; 165 | strings = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), inheritFormat = arguments[_i++]; 166 | if (!(typeof inheritFormat === 'undefined' || typeof inheritFormat === 'boolean')) { 167 | strings.push(inheritFormat); 168 | inheritFormat = true; 169 | } 170 | newString = this.copy(); 171 | for (_j = 0, _len = strings.length; _j < _len; _j++) { 172 | string = strings[_j]; 173 | if (string.length === 0) { 174 | continue; 175 | } 176 | tail = string; 177 | if (typeof string === 'string') { 178 | tail = new HTMLString.String(string, this._preserveWhitespace); 179 | } 180 | if (inheritFormat && newString.length()) { 181 | indexChar = newString.charAt(newString.length() - 1); 182 | inheritedTags = indexChar.tags(); 183 | if (indexChar.isTag()) { 184 | inheritedTags.shift(); 185 | } 186 | if (typeof string !== 'string') { 187 | tail = tail.copy(); 188 | } 189 | _ref = tail.characters; 190 | for (_k = 0, _len1 = _ref.length; _k < _len1; _k++) { 191 | c = _ref[_k]; 192 | c.addTags.apply(c, inheritedTags); 193 | } 194 | } 195 | _ref1 = tail.characters; 196 | for (_l = 0, _len2 = _ref1.length; _l < _len2; _l++) { 197 | c = _ref1[_l]; 198 | newString.characters.push(c); 199 | } 200 | } 201 | return newString; 202 | }; 203 | 204 | String.prototype.contains = function(substring) { 205 | var c, found, from, i, _i, _len, _ref; 206 | if (typeof substring === 'string') { 207 | return this.text().indexOf(substring) > -1; 208 | } 209 | from = 0; 210 | while (from <= (this.length() - substring.length())) { 211 | found = true; 212 | _ref = substring.characters; 213 | for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { 214 | c = _ref[i]; 215 | if (!c.eq(this.characters[i + from])) { 216 | found = false; 217 | break; 218 | } 219 | } 220 | if (found) { 221 | return true; 222 | } 223 | from++; 224 | } 225 | return false; 226 | }; 227 | 228 | String.prototype.endsWith = function(substring) { 229 | var c, characters, i, _i, _len, _ref; 230 | if (typeof substring === 'string') { 231 | return substring === '' || this.text().slice(-substring.length) === substring; 232 | } 233 | characters = this.characters.slice().reverse(); 234 | _ref = substring.characters.slice().reverse(); 235 | for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { 236 | c = _ref[i]; 237 | if (!c.eq(characters[i])) { 238 | return false; 239 | } 240 | } 241 | return true; 242 | }; 243 | 244 | String.prototype.format = function() { 245 | var c, from, i, newString, tags, to, _i; 246 | from = arguments[0], to = arguments[1], tags = 3 <= arguments.length ? __slice.call(arguments, 2) : []; 247 | if (to < 0) { 248 | to = this.length() + to + 1; 249 | } 250 | if (from < 0) { 251 | from = this.length() + from; 252 | } 253 | newString = this.copy(); 254 | for (i = _i = from; from <= to ? _i < to : _i > to; i = from <= to ? ++_i : --_i) { 255 | c = newString.characters[i]; 256 | c.addTags.apply(c, tags); 257 | } 258 | return newString; 259 | }; 260 | 261 | String.prototype.hasTags = function() { 262 | var c, found, strict, tags, _i, _j, _len, _ref; 263 | tags = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), strict = arguments[_i++]; 264 | if (!(typeof strict === 'undefined' || typeof strict === 'boolean')) { 265 | tags.push(strict); 266 | strict = false; 267 | } 268 | found = false; 269 | _ref = this.characters; 270 | for (_j = 0, _len = _ref.length; _j < _len; _j++) { 271 | c = _ref[_j]; 272 | if (c.hasTags.apply(c, tags)) { 273 | found = true; 274 | } else { 275 | if (strict) { 276 | return false; 277 | } 278 | } 279 | } 280 | return found; 281 | }; 282 | 283 | String.prototype.html = function() { 284 | var c, closingTag, closingTags, head, html, openHeads, openTag, openTags, tag, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _m, _ref, _ref1, _ref2, _ref3; 285 | html = ''; 286 | openTags = []; 287 | openHeads = []; 288 | closingTags = []; 289 | _ref = this.characters; 290 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 291 | c = _ref[_i]; 292 | closingTags = []; 293 | _ref1 = openTags.slice().reverse(); 294 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 295 | openTag = _ref1[_j]; 296 | closingTags.push(openTag); 297 | if (!c.hasTags(openTag)) { 298 | for (_k = 0, _len2 = closingTags.length; _k < _len2; _k++) { 299 | closingTag = closingTags[_k]; 300 | html += closingTag.tail(); 301 | openTags.pop(); 302 | openHeads.pop(); 303 | } 304 | closingTags = []; 305 | } 306 | } 307 | _ref2 = c._tags; 308 | for (_l = 0, _len3 = _ref2.length; _l < _len3; _l++) { 309 | tag = _ref2[_l]; 310 | if (openHeads.indexOf(tag.head()) === -1) { 311 | if (!tag.selfClosing()) { 312 | head = tag.head(); 313 | html += head; 314 | openTags.push(tag); 315 | openHeads.push(head); 316 | } 317 | } 318 | } 319 | if (c._tags.length > 0 && c._tags[0].selfClosing()) { 320 | html += c._tags[0].head(); 321 | } 322 | html += c.c(); 323 | } 324 | _ref3 = openTags.reverse(); 325 | for (_m = 0, _len4 = _ref3.length; _m < _len4; _m++) { 326 | tag = _ref3[_m]; 327 | html += tag.tail(); 328 | } 329 | return html; 330 | }; 331 | 332 | String.prototype.indexOf = function(substring, from) { 333 | var c, found, i, _i, _len, _ref; 334 | if (from == null) { 335 | from = 0; 336 | } 337 | if (from < 0) { 338 | from = 0; 339 | } 340 | if (typeof substring === 'string') { 341 | return this.text().indexOf(substring, from); 342 | } 343 | while (from <= (this.length() - substring.length())) { 344 | found = true; 345 | _ref = substring.characters; 346 | for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { 347 | c = _ref[i]; 348 | if (!c.eq(this.characters[i + from])) { 349 | found = false; 350 | break; 351 | } 352 | } 353 | if (found) { 354 | return from; 355 | } 356 | from++; 357 | } 358 | return -1; 359 | }; 360 | 361 | String.prototype.insert = function(index, substring, inheritFormat) { 362 | var c, head, indexChar, inheritedTags, middle, newString, tail, _i, _j, _k, _len, _len1, _len2, _ref, _ref1, _ref2; 363 | if (inheritFormat == null) { 364 | inheritFormat = true; 365 | } 366 | head = this.slice(0, index); 367 | tail = this.slice(index); 368 | if (index < 0) { 369 | index = this.length() + index; 370 | } 371 | middle = substring; 372 | if (typeof substring === 'string') { 373 | middle = new HTMLString.String(substring, this._preserveWhitespace); 374 | } 375 | if (inheritFormat && index > 0) { 376 | indexChar = this.charAt(index - 1); 377 | inheritedTags = indexChar.tags(); 378 | if (indexChar.isTag()) { 379 | inheritedTags.shift(); 380 | } 381 | if (typeof substring !== 'string') { 382 | middle = middle.copy(); 383 | } 384 | _ref = middle.characters; 385 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 386 | c = _ref[_i]; 387 | c.addTags.apply(c, inheritedTags); 388 | } 389 | } 390 | newString = head; 391 | _ref1 = middle.characters; 392 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 393 | c = _ref1[_j]; 394 | newString.characters.push(c); 395 | } 396 | _ref2 = tail.characters; 397 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 398 | c = _ref2[_k]; 399 | newString.characters.push(c); 400 | } 401 | return newString; 402 | }; 403 | 404 | String.prototype.lastIndexOf = function(substring, from) { 405 | var c, characters, found, i, skip, _i, _j, _len, _len1; 406 | if (from == null) { 407 | from = 0; 408 | } 409 | if (from < 0) { 410 | from = 0; 411 | } 412 | characters = this.characters.slice(from).reverse(); 413 | from = 0; 414 | if (typeof substring === 'string') { 415 | if (!this.contains(substring)) { 416 | return -1; 417 | } 418 | substring = substring.split('').reverse(); 419 | while (from <= (characters.length - substring.length)) { 420 | found = true; 421 | skip = 0; 422 | for (i = _i = 0, _len = substring.length; _i < _len; i = ++_i) { 423 | c = substring[i]; 424 | if (characters[i + from].isTag()) { 425 | skip += 1; 426 | } 427 | if (c !== characters[skip + i + from].c()) { 428 | found = false; 429 | break; 430 | } 431 | } 432 | if (found) { 433 | return from; 434 | } 435 | from++; 436 | } 437 | return -1; 438 | } 439 | substring = substring.characters.slice().reverse(); 440 | while (from <= (characters.length - substring.length)) { 441 | found = true; 442 | for (i = _j = 0, _len1 = substring.length; _j < _len1; i = ++_j) { 443 | c = substring[i]; 444 | if (!c.eq(characters[i + from])) { 445 | found = false; 446 | break; 447 | } 448 | } 449 | if (found) { 450 | return from; 451 | } 452 | from++; 453 | } 454 | return -1; 455 | }; 456 | 457 | String.prototype.optimize = function() { 458 | var c, closingTag, closingTags, head, lastC, len, openHeads, openTag, openTags, runLength, runLengthSort, runLengths, run_length, t, tag, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _len5, _len6, _m, _n, _o, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _results; 459 | openTags = []; 460 | openHeads = []; 461 | lastC = null; 462 | _ref = this.characters.slice().reverse(); 463 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 464 | c = _ref[_i]; 465 | c._runLengthMap = {}; 466 | c._runLengthMapSize = 0; 467 | closingTags = []; 468 | _ref1 = openTags.slice().reverse(); 469 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 470 | openTag = _ref1[_j]; 471 | closingTags.push(openTag); 472 | if (!c.hasTags(openTag)) { 473 | for (_k = 0, _len2 = closingTags.length; _k < _len2; _k++) { 474 | closingTag = closingTags[_k]; 475 | openTags.pop(); 476 | openHeads.pop(); 477 | } 478 | closingTags = []; 479 | } 480 | } 481 | _ref2 = c._tags; 482 | for (_l = 0, _len3 = _ref2.length; _l < _len3; _l++) { 483 | tag = _ref2[_l]; 484 | if (openHeads.indexOf(tag.head()) === -1) { 485 | if (!tag.selfClosing()) { 486 | openTags.push(tag); 487 | openHeads.push(tag.head()); 488 | } 489 | } 490 | } 491 | for (_m = 0, _len4 = openTags.length; _m < _len4; _m++) { 492 | tag = openTags[_m]; 493 | head = tag.head(); 494 | if (!lastC) { 495 | c._runLengthMap[head] = [tag, 1]; 496 | continue; 497 | } 498 | if (!c._runLengthMap[head]) { 499 | c._runLengthMap[head] = [tag, 0]; 500 | } 501 | run_length = 0; 502 | if (lastC._runLengthMap[head]) { 503 | run_length = lastC._runLengthMap[head][1]; 504 | } 505 | c._runLengthMap[head][1] = run_length + 1; 506 | } 507 | lastC = c; 508 | } 509 | runLengthSort = function(a, b) { 510 | return b[1] - a[1]; 511 | }; 512 | _ref3 = this.characters; 513 | _results = []; 514 | for (_n = 0, _len5 = _ref3.length; _n < _len5; _n++) { 515 | c = _ref3[_n]; 516 | len = c._tags.length; 517 | if ((len > 0 && c._tags[0].selfClosing() && len < 3) || len < 2) { 518 | continue; 519 | } 520 | runLengths = []; 521 | _ref4 = c._runLengthMap; 522 | for (tag in _ref4) { 523 | runLength = _ref4[tag]; 524 | runLengths.push(runLength); 525 | } 526 | runLengths.sort(runLengthSort); 527 | _ref5 = c._tags.slice(); 528 | for (_o = 0, _len6 = _ref5.length; _o < _len6; _o++) { 529 | tag = _ref5[_o]; 530 | if (!tag.selfClosing()) { 531 | c.removeTags(tag); 532 | } 533 | } 534 | _results.push(c.addTags.apply(c, (function() { 535 | var _len7, _p, _results1; 536 | _results1 = []; 537 | for (_p = 0, _len7 = runLengths.length; _p < _len7; _p++) { 538 | t = runLengths[_p]; 539 | _results1.push(t[0]); 540 | } 541 | return _results1; 542 | })())); 543 | } 544 | return _results; 545 | }; 546 | 547 | String.prototype.slice = function(from, to) { 548 | var c, newString; 549 | newString = new HTMLString.String('', this._preserveWhitespace); 550 | newString.characters = (function() { 551 | var _i, _len, _ref, _results; 552 | _ref = this.characters.slice(from, to); 553 | _results = []; 554 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 555 | c = _ref[_i]; 556 | _results.push(c.copy()); 557 | } 558 | return _results; 559 | }).call(this); 560 | return newString; 561 | }; 562 | 563 | String.prototype.split = function(separator, limit) { 564 | var count, end, i, index, indexes, lastIndex, start, substrings, _i, _ref; 565 | if (separator == null) { 566 | separator = ''; 567 | } 568 | if (limit == null) { 569 | limit = 0; 570 | } 571 | lastIndex = 0; 572 | count = 0; 573 | indexes = [0]; 574 | while (true) { 575 | if (limit > 0 && count > limit) { 576 | break; 577 | } 578 | index = this.indexOf(separator, lastIndex); 579 | if (index === -1) { 580 | break; 581 | } 582 | indexes.push(index); 583 | lastIndex = index + 1; 584 | } 585 | indexes.push(this.length()); 586 | substrings = []; 587 | for (i = _i = 0, _ref = indexes.length - 2; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) { 588 | start = indexes[i]; 589 | if (i > 0) { 590 | start += 1; 591 | } 592 | end = indexes[i + 1]; 593 | substrings.push(this.slice(start, end)); 594 | } 595 | return substrings; 596 | }; 597 | 598 | String.prototype.startsWith = function(substring) { 599 | var c, i, _i, _len, _ref; 600 | if (typeof substring === 'string') { 601 | return this.text().slice(0, substring.length) === substring; 602 | } 603 | _ref = substring.characters; 604 | for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { 605 | c = _ref[i]; 606 | if (!c.eq(this.characters[i])) { 607 | return false; 608 | } 609 | } 610 | return true; 611 | }; 612 | 613 | String.prototype.substr = function(from, length) { 614 | if (length <= 0) { 615 | return new HTMLString.String('', this._preserveWhitespace); 616 | } 617 | if (from < 0) { 618 | from = this.length() + from; 619 | } 620 | if (length === void 0) { 621 | length = this.length() - from; 622 | } 623 | return this.slice(from, from + length); 624 | }; 625 | 626 | String.prototype.substring = function(from, to) { 627 | if (to === void 0) { 628 | to = this.length(); 629 | } 630 | return this.slice(from, to); 631 | }; 632 | 633 | String.prototype.text = function() { 634 | var c, text, _i, _len, _ref; 635 | text = ''; 636 | _ref = this.characters; 637 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 638 | c = _ref[_i]; 639 | if (c.isTag()) { 640 | if (c.isTag('br')) { 641 | text += '\n'; 642 | } 643 | continue; 644 | } 645 | if (c.c() === ' ') { 646 | text += c.c(); 647 | continue; 648 | } 649 | text += c.c(); 650 | } 651 | return this.constructor.decode(text); 652 | }; 653 | 654 | String.prototype.toLowerCase = function() { 655 | var c, newString, _i, _len, _ref; 656 | newString = this.copy(); 657 | _ref = newString.characters; 658 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 659 | c = _ref[_i]; 660 | if (c._c.length === 1) { 661 | c._c = c._c.toLowerCase(); 662 | } 663 | } 664 | return newString; 665 | }; 666 | 667 | String.prototype.toUpperCase = function() { 668 | var c, newString, _i, _len, _ref; 669 | newString = this.copy(); 670 | _ref = newString.characters; 671 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 672 | c = _ref[_i]; 673 | if (c._c.length === 1) { 674 | c._c = c._c.toUpperCase(); 675 | } 676 | } 677 | return newString; 678 | }; 679 | 680 | String.prototype.trim = function() { 681 | var c, from, newString, to, _i, _j, _len, _len1, _ref, _ref1; 682 | _ref = this.characters; 683 | for (from = _i = 0, _len = _ref.length; _i < _len; from = ++_i) { 684 | c = _ref[from]; 685 | if (!c.isWhitespace()) { 686 | break; 687 | } 688 | } 689 | _ref1 = this.characters.slice().reverse(); 690 | for (to = _j = 0, _len1 = _ref1.length; _j < _len1; to = ++_j) { 691 | c = _ref1[to]; 692 | if (!c.isWhitespace()) { 693 | break; 694 | } 695 | } 696 | to = this.length() - to - 1; 697 | newString = new HTMLString.String('', this._preserveWhitespace); 698 | newString.characters = (function() { 699 | var _k, _len2, _ref2, _results; 700 | _ref2 = this.characters.slice(from, +to + 1 || 9e9); 701 | _results = []; 702 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 703 | c = _ref2[_k]; 704 | _results.push(c.copy()); 705 | } 706 | return _results; 707 | }).call(this); 708 | return newString; 709 | }; 710 | 711 | String.prototype.trimLeft = function() { 712 | var c, from, newString, to, _i, _len, _ref; 713 | to = this.length() - 1; 714 | _ref = this.characters; 715 | for (from = _i = 0, _len = _ref.length; _i < _len; from = ++_i) { 716 | c = _ref[from]; 717 | if (!c.isWhitespace()) { 718 | break; 719 | } 720 | } 721 | newString = new HTMLString.String('', this._preserveWhitespace); 722 | newString.characters = (function() { 723 | var _j, _len1, _ref1, _results; 724 | _ref1 = this.characters.slice(from, +to + 1 || 9e9); 725 | _results = []; 726 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 727 | c = _ref1[_j]; 728 | _results.push(c.copy()); 729 | } 730 | return _results; 731 | }).call(this); 732 | return newString; 733 | }; 734 | 735 | String.prototype.trimRight = function() { 736 | var c, from, newString, to, _i, _len, _ref; 737 | from = 0; 738 | _ref = this.characters.slice().reverse(); 739 | for (to = _i = 0, _len = _ref.length; _i < _len; to = ++_i) { 740 | c = _ref[to]; 741 | if (!c.isWhitespace()) { 742 | break; 743 | } 744 | } 745 | to = this.length() - to - 1; 746 | newString = new HTMLString.String('', this._preserveWhitespace); 747 | newString.characters = (function() { 748 | var _j, _len1, _ref1, _results; 749 | _ref1 = this.characters.slice(from, +to + 1 || 9e9); 750 | _results = []; 751 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 752 | c = _ref1[_j]; 753 | _results.push(c.copy()); 754 | } 755 | return _results; 756 | }).call(this); 757 | return newString; 758 | }; 759 | 760 | String.prototype.unformat = function() { 761 | var c, from, i, newString, tags, to, _i; 762 | from = arguments[0], to = arguments[1], tags = 3 <= arguments.length ? __slice.call(arguments, 2) : []; 763 | if (to < 0) { 764 | to = this.length() + to + 1; 765 | } 766 | if (from < 0) { 767 | from = this.length() + from; 768 | } 769 | newString = this.copy(); 770 | for (i = _i = from; from <= to ? _i < to : _i > to; i = from <= to ? ++_i : --_i) { 771 | c = newString.characters[i]; 772 | c.removeTags.apply(c, tags); 773 | } 774 | return newString; 775 | }; 776 | 777 | String.prototype.copy = function() { 778 | var c, stringCopy; 779 | stringCopy = new HTMLString.String('', this._preserveWhitespace); 780 | stringCopy.characters = (function() { 781 | var _i, _len, _ref, _results; 782 | _ref = this.characters; 783 | _results = []; 784 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 785 | c = _ref[_i]; 786 | _results.push(c.copy()); 787 | } 788 | return _results; 789 | }).call(this); 790 | return stringCopy; 791 | }; 792 | 793 | String.decode = function(string) { 794 | var textarea; 795 | textarea = document.createElement('textarea'); 796 | textarea.innerHTML = string; 797 | return textarea.textContent; 798 | }; 799 | 800 | String.encode = function(string) { 801 | var textarea; 802 | textarea = document.createElement('textarea'); 803 | textarea.textContent = string; 804 | return textarea.innerHTML; 805 | }; 806 | 807 | String.join = function(separator, strings) { 808 | var joined, s, _i, _len; 809 | joined = strings.shift(); 810 | for (_i = 0, _len = strings.length; _i < _len; _i++) { 811 | s = strings[_i]; 812 | joined = joined.concat(separator, s); 813 | } 814 | return joined; 815 | }; 816 | 817 | return String; 818 | 819 | })(); 820 | 821 | ALPHA_CHARS = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz-_$'.split(''); 822 | 823 | ALPHA_NUMERIC_CHARS = ALPHA_CHARS.concat('1234567890'.split('')); 824 | 825 | ATTR_NAME_CHARS = ALPHA_NUMERIC_CHARS.concat([':']); 826 | 827 | ENTITY_CHARS = ALPHA_NUMERIC_CHARS.concat(['#']); 828 | 829 | TAG_NAME_CHARS = ALPHA_NUMERIC_CHARS.concat([':']); 830 | 831 | CHAR_OR_ENTITY_OR_TAG = 1; 832 | 833 | ENTITY = 2; 834 | 835 | OPENNING_OR_CLOSING_TAG = 3; 836 | 837 | OPENING_TAG = 4; 838 | 839 | CLOSING_TAG = 5; 840 | 841 | TAG_NAME_OPENING = 6; 842 | 843 | TAG_NAME_CLOSING = 7; 844 | 845 | TAG_OPENING_SELF_CLOSING = 8; 846 | 847 | TAG_NAME_MUST_CLOSE = 9; 848 | 849 | ATTR_OR_TAG_END = 10; 850 | 851 | ATTR_NAME = 11; 852 | 853 | ATTR_NAME_FIND_VALUE = 12; 854 | 855 | ATTR_DELIM = 13; 856 | 857 | ATTR_VALUE_SINGLE_DELIM = 14; 858 | 859 | ATTR_VALUE_DOUBLE_DELIM = 15; 860 | 861 | ATTR_VALUE_NO_DELIM = 16; 862 | 863 | ATTR_ENTITY_NO_DELIM = 17; 864 | 865 | ATTR_ENTITY_SINGLE_DELIM = 18; 866 | 867 | ATTR_ENTITY_DOUBLE_DELIM = 19; 868 | 869 | _Parser = (function() { 870 | function _Parser() { 871 | this.fsm = new FSM.Machine(this); 872 | this.fsm.setInitialState(CHAR_OR_ENTITY_OR_TAG); 873 | this.fsm.addTransitionAny(CHAR_OR_ENTITY_OR_TAG, null, function(c) { 874 | return this._pushChar(c); 875 | }); 876 | this.fsm.addTransition('<', CHAR_OR_ENTITY_OR_TAG, OPENNING_OR_CLOSING_TAG); 877 | this.fsm.addTransition('&', CHAR_OR_ENTITY_OR_TAG, ENTITY); 878 | this.fsm.addTransition('END', CHAR_OR_ENTITY_OR_TAG, null); 879 | this.fsm.addTransitions(ENTITY_CHARS, ENTITY, null, function(c) { 880 | return this.entity += c; 881 | }); 882 | this.fsm.addTransition(';', ENTITY, CHAR_OR_ENTITY_OR_TAG, function() { 883 | this._pushChar("&" + this.entity + ";"); 884 | return this.entity = ''; 885 | }); 886 | this.fsm.addTransitionAny(ENTITY, CHAR_OR_ENTITY_OR_TAG, function(c) { 887 | var _i, _len, _ref; 888 | this._pushChar('&'); 889 | _ref = this.entity.split(''); 890 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 891 | c = _ref[_i]; 892 | this._pushChar(c); 893 | } 894 | this.entity = ''; 895 | return this._back(); 896 | }); 897 | this.fsm.addTransition('END', ENTITY, null, function() { 898 | var c, _i, _len, _ref; 899 | this._pushChar('&'); 900 | _ref = this.entity.split(''); 901 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 902 | c = _ref[_i]; 903 | this._pushChar(c); 904 | } 905 | return this.entity = ''; 906 | }); 907 | this.fsm.addTransitions([' ', '\n'], OPENNING_OR_CLOSING_TAG); 908 | this.fsm.addTransitions(ALPHA_CHARS, OPENNING_OR_CLOSING_TAG, OPENING_TAG, function() { 909 | return this._back(); 910 | }); 911 | this.fsm.addTransition('/', OPENNING_OR_CLOSING_TAG, CLOSING_TAG); 912 | this.fsm.addTransitions([' ', '\n'], OPENING_TAG); 913 | this.fsm.addTransitions(ALPHA_CHARS, OPENING_TAG, TAG_NAME_OPENING, function() { 914 | return this._back(); 915 | }); 916 | this.fsm.addTransitions([' ', '\n'], CLOSING_TAG); 917 | this.fsm.addTransitions(ALPHA_CHARS, CLOSING_TAG, TAG_NAME_CLOSING, function() { 918 | return this._back(); 919 | }); 920 | this.fsm.addTransitions(TAG_NAME_CHARS, TAG_NAME_OPENING, null, function(c) { 921 | return this.tagName += c; 922 | }); 923 | this.fsm.addTransitions([' ', '\n'], TAG_NAME_OPENING, ATTR_OR_TAG_END); 924 | this.fsm.addTransition('/', TAG_NAME_OPENING, TAG_OPENING_SELF_CLOSING, function() { 925 | return this.selfClosing = true; 926 | }); 927 | this.fsm.addTransition('>', TAG_NAME_OPENING, CHAR_OR_ENTITY_OR_TAG, function() { 928 | return this._pushTag(); 929 | }); 930 | this.fsm.addTransitions([' ', '\n'], TAG_OPENING_SELF_CLOSING); 931 | this.fsm.addTransition('>', TAG_OPENING_SELF_CLOSING, CHAR_OR_ENTITY_OR_TAG, function() { 932 | return this._pushTag(); 933 | }); 934 | this.fsm.addTransitions([' ', '\n'], ATTR_OR_TAG_END); 935 | this.fsm.addTransition('/', ATTR_OR_TAG_END, TAG_OPENING_SELF_CLOSING, function() { 936 | return this.selfClosing = true; 937 | }); 938 | this.fsm.addTransition('>', ATTR_OR_TAG_END, CHAR_OR_ENTITY_OR_TAG, function() { 939 | return this._pushTag(); 940 | }); 941 | this.fsm.addTransitions(ALPHA_CHARS, ATTR_OR_TAG_END, ATTR_NAME, function() { 942 | return this._back(); 943 | }); 944 | this.fsm.addTransitions(TAG_NAME_CHARS, TAG_NAME_CLOSING, null, function(c) { 945 | return this.tagName += c; 946 | }); 947 | this.fsm.addTransitions([' ', '\n'], TAG_NAME_CLOSING, TAG_NAME_MUST_CLOSE); 948 | this.fsm.addTransition('>', TAG_NAME_CLOSING, CHAR_OR_ENTITY_OR_TAG, function() { 949 | return this._popTag(); 950 | }); 951 | this.fsm.addTransitions([' ', '\n'], TAG_NAME_MUST_CLOSE); 952 | this.fsm.addTransition('>', TAG_NAME_MUST_CLOSE, CHAR_OR_ENTITY_OR_TAG, function() { 953 | return this._popTag(); 954 | }); 955 | this.fsm.addTransitions(ATTR_NAME_CHARS, ATTR_NAME, null, function(c) { 956 | return this.attributeName += c; 957 | }); 958 | this.fsm.addTransitions([' ', '\n'], ATTR_NAME, ATTR_NAME_FIND_VALUE); 959 | this.fsm.addTransition('=', ATTR_NAME, ATTR_DELIM); 960 | this.fsm.addTransitions([' ', '\n'], ATTR_NAME_FIND_VALUE); 961 | this.fsm.addTransition('=', ATTR_NAME_FIND_VALUE, ATTR_DELIM); 962 | this.fsm.addTransitions('>', ATTR_NAME, ATTR_OR_TAG_END, function() { 963 | this._pushAttribute(); 964 | return this._back(); 965 | }); 966 | this.fsm.addTransitionAny(ATTR_NAME_FIND_VALUE, ATTR_OR_TAG_END, function() { 967 | this._pushAttribute(); 968 | return this._back(); 969 | }); 970 | this.fsm.addTransitions([' ', '\n'], ATTR_DELIM); 971 | this.fsm.addTransition('\'', ATTR_DELIM, ATTR_VALUE_SINGLE_DELIM); 972 | this.fsm.addTransition('"', ATTR_DELIM, ATTR_VALUE_DOUBLE_DELIM); 973 | this.fsm.addTransitions(ALPHA_NUMERIC_CHARS.concat(['&'], ATTR_DELIM, ATTR_VALUE_NO_DELIM, function() { 974 | return this._back(); 975 | })); 976 | this.fsm.addTransition(' ', ATTR_VALUE_NO_DELIM, ATTR_OR_TAG_END, function() { 977 | return this._pushAttribute(); 978 | }); 979 | this.fsm.addTransitions(['/', '>'], ATTR_VALUE_NO_DELIM, ATTR_OR_TAG_END, function() { 980 | this._back(); 981 | return this._pushAttribute(); 982 | }); 983 | this.fsm.addTransition('&', ATTR_VALUE_NO_DELIM, ATTR_ENTITY_NO_DELIM); 984 | this.fsm.addTransitionAny(ATTR_VALUE_NO_DELIM, null, function(c) { 985 | return this.attributeValue += c; 986 | }); 987 | this.fsm.addTransition('\'', ATTR_VALUE_SINGLE_DELIM, ATTR_OR_TAG_END, function() { 988 | return this._pushAttribute(); 989 | }); 990 | this.fsm.addTransition('&', ATTR_VALUE_SINGLE_DELIM, ATTR_ENTITY_SINGLE_DELIM); 991 | this.fsm.addTransitionAny(ATTR_VALUE_SINGLE_DELIM, null, function(c) { 992 | return this.attributeValue += c; 993 | }); 994 | this.fsm.addTransition('"', ATTR_VALUE_DOUBLE_DELIM, ATTR_OR_TAG_END, function() { 995 | return this._pushAttribute(); 996 | }); 997 | this.fsm.addTransition('&', ATTR_VALUE_DOUBLE_DELIM, ATTR_ENTITY_DOUBLE_DELIM); 998 | this.fsm.addTransitionAny(ATTR_VALUE_DOUBLE_DELIM, null, function(c) { 999 | return this.attributeValue += c; 1000 | }); 1001 | this.fsm.addTransitions(ENTITY_CHARS, ATTR_ENTITY_NO_DELIM, null, function(c) { 1002 | return this.entity += c; 1003 | }); 1004 | this.fsm.addTransitions(ENTITY_CHARS, ATTR_ENTITY_SINGLE_DELIM, null, function(c) { 1005 | return this.entity += c; 1006 | }); 1007 | this.fsm.addTransitions(ENTITY_CHARS, ATTR_ENTITY_DOUBLE_DELIM, null, function(c) { 1008 | return this.entity += c; 1009 | }); 1010 | this.fsm.addTransition(';', ATTR_ENTITY_NO_DELIM, ATTR_VALUE_NO_DELIM, function() { 1011 | this.attributeValue += "&" + this.entity + ";"; 1012 | return this.entity = ''; 1013 | }); 1014 | this.fsm.addTransition(';', ATTR_ENTITY_SINGLE_DELIM, ATTR_VALUE_SINGLE_DELIM, function() { 1015 | this.attributeValue += "&" + this.entity + ";"; 1016 | return this.entity = ''; 1017 | }); 1018 | this.fsm.addTransition(';', ATTR_ENTITY_DOUBLE_DELIM, ATTR_VALUE_DOUBLE_DELIM, function() { 1019 | this.attributeValue += "&" + this.entity + ";"; 1020 | return this.entity = ''; 1021 | }); 1022 | this.fsm.addTransitionAny(ATTR_ENTITY_NO_DELIM, ATTR_VALUE_NO_DELIM, function(c) { 1023 | this.attributeValue += '&' + this.entity; 1024 | this.entity = ''; 1025 | return this._back(); 1026 | }); 1027 | this.fsm.addTransitionAny(ATTR_ENTITY_SINGLE_DELIM, ATTR_VALUE_SINGLE_DELIM, function(c) { 1028 | this.attributeValue += '&' + this.entity; 1029 | this.entity = ''; 1030 | return this._back(); 1031 | }); 1032 | this.fsm.addTransitionAny(ATTR_ENTITY_DOUBLE_DELIM, ATTR_VALUE_DOUBLE_DELIM, function(c) { 1033 | this.attributeValue += '&' + this.entity; 1034 | this.entity = ''; 1035 | return this._back(); 1036 | }); 1037 | } 1038 | 1039 | _Parser.prototype._back = function() { 1040 | return this.head--; 1041 | }; 1042 | 1043 | _Parser.prototype._pushAttribute = function() { 1044 | this.attributes[this.attributeName] = this.attributeValue; 1045 | this.attributeName = ''; 1046 | return this.attributeValue = ''; 1047 | }; 1048 | 1049 | _Parser.prototype._pushChar = function(c) { 1050 | var character, lastCharacter; 1051 | character = new HTMLString.Character(c, this.tags); 1052 | if (this._preserveWhitespace) { 1053 | this.string.characters.push(character); 1054 | return; 1055 | } 1056 | if (this.string.length() && !character.isTag() && !character.isEntity() && character.isWhitespace()) { 1057 | lastCharacter = this.string.characters[this.string.length() - 1]; 1058 | if (lastCharacter.isWhitespace() && !lastCharacter.isTag() && !lastCharacter.isEntity()) { 1059 | return; 1060 | } 1061 | } 1062 | return this.string.characters.push(character); 1063 | }; 1064 | 1065 | _Parser.prototype._pushTag = function() { 1066 | var tag, _ref; 1067 | tag = new HTMLString.Tag(this.tagName, this.attributes); 1068 | this.tags.push(tag); 1069 | if (tag.selfClosing()) { 1070 | this._pushChar(''); 1071 | this.tags.pop(); 1072 | if (!this.selfClosed && (_ref = this.tagName, __indexOf.call(HTMLString.Tag.SELF_CLOSING, _ref) >= 0)) { 1073 | this.fsm.reset(); 1074 | } 1075 | } 1076 | this.tagName = ''; 1077 | this.selfClosed = false; 1078 | return this.attributes = {}; 1079 | }; 1080 | 1081 | _Parser.prototype._popTag = function() { 1082 | var character, tag; 1083 | while (true) { 1084 | tag = this.tags.pop(); 1085 | if (this.string.length()) { 1086 | character = this.string.characters[this.string.length() - 1]; 1087 | if (!character.isTag() && !character.isEntity() && character.isWhitespace()) { 1088 | character.removeTags(tag); 1089 | } 1090 | } 1091 | if (tag.name() === this.tagName.toLowerCase()) { 1092 | break; 1093 | } 1094 | } 1095 | return this.tagName = ''; 1096 | }; 1097 | 1098 | _Parser.prototype.parse = function(html, preserveWhitespace) { 1099 | var character, error; 1100 | this._preserveWhitespace = preserveWhitespace; 1101 | this.reset(); 1102 | html = this.preprocess(html); 1103 | this.fsm.parser = this; 1104 | while (this.head < html.length) { 1105 | character = html[this.head]; 1106 | try { 1107 | this.fsm.process(character); 1108 | } catch (_error) { 1109 | error = _error; 1110 | throw new Error("Error at char " + this.head + " >> " + error); 1111 | } 1112 | this.head++; 1113 | } 1114 | this.fsm.process('END'); 1115 | return this.string; 1116 | }; 1117 | 1118 | _Parser.prototype.preprocess = function(html) { 1119 | html = html.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); 1120 | html = html.replace(//g, ''); 1121 | if (!this._preserveWhitespace) { 1122 | html = html.replace(/\s+/g, ' '); 1123 | } 1124 | return html; 1125 | }; 1126 | 1127 | _Parser.prototype.reset = function() { 1128 | this.fsm.reset(); 1129 | this.head = 0; 1130 | this.string = new HTMLString.String(); 1131 | this.entity = ''; 1132 | this.tags = []; 1133 | this.tagName = ''; 1134 | this.selfClosing = false; 1135 | this.attributes = {}; 1136 | this.attributeName = ''; 1137 | return this.attributeValue = ''; 1138 | }; 1139 | 1140 | return _Parser; 1141 | 1142 | })(); 1143 | 1144 | HTMLString.Tag = (function() { 1145 | function Tag(name, attributes) { 1146 | var k, v; 1147 | this._name = name.toLowerCase(); 1148 | this._selfClosing = HTMLString.Tag.SELF_CLOSING[this._name] === true; 1149 | this._head = null; 1150 | this._attributes = {}; 1151 | for (k in attributes) { 1152 | v = attributes[k]; 1153 | this._attributes[k] = v; 1154 | } 1155 | } 1156 | 1157 | Tag.SELF_CLOSING = { 1158 | 'area': true, 1159 | 'base': true, 1160 | 'br': true, 1161 | 'hr': true, 1162 | 'img': true, 1163 | 'input': true, 1164 | 'link meta': true, 1165 | 'wbr': true 1166 | }; 1167 | 1168 | Tag.prototype.head = function() { 1169 | var components, k, v, _ref; 1170 | if (!this._head) { 1171 | components = []; 1172 | _ref = this._attributes; 1173 | for (k in _ref) { 1174 | v = _ref[k]; 1175 | if (v) { 1176 | components.push("" + k + "=\"" + v + "\""); 1177 | } else { 1178 | components.push("" + k); 1179 | } 1180 | } 1181 | components.sort(); 1182 | components.unshift(this._name); 1183 | this._head = "<" + (components.join(' ')) + ">"; 1184 | } 1185 | return this._head; 1186 | }; 1187 | 1188 | Tag.prototype.name = function() { 1189 | return this._name; 1190 | }; 1191 | 1192 | Tag.prototype.selfClosing = function() { 1193 | return this._selfClosing; 1194 | }; 1195 | 1196 | Tag.prototype.tail = function() { 1197 | if (this._selfClosing) { 1198 | return ''; 1199 | } 1200 | return ""; 1201 | }; 1202 | 1203 | Tag.prototype.attr = function(name, value) { 1204 | if (value === void 0) { 1205 | return this._attributes[name]; 1206 | } 1207 | this._attributes[name] = value; 1208 | return this._head = null; 1209 | }; 1210 | 1211 | Tag.prototype.removeAttr = function(name) { 1212 | if (this._attributes[name] === void 0) { 1213 | return; 1214 | } 1215 | delete this._attributes[name]; 1216 | return this._head = null; 1217 | }; 1218 | 1219 | Tag.prototype.copy = function() { 1220 | return new HTMLString.Tag(this._name, this._attributes); 1221 | }; 1222 | 1223 | return Tag; 1224 | 1225 | })(); 1226 | 1227 | HTMLString.Character = (function() { 1228 | function Character(c, tags) { 1229 | this._c = c; 1230 | if (c.length > 1) { 1231 | this._c = c.toLowerCase(); 1232 | } 1233 | this._tags = []; 1234 | this.addTags.apply(this, tags); 1235 | } 1236 | 1237 | Character.prototype.c = function() { 1238 | return this._c; 1239 | }; 1240 | 1241 | Character.prototype.isEntity = function() { 1242 | return this._c.length > 1; 1243 | }; 1244 | 1245 | Character.prototype.isTag = function(tagName) { 1246 | if (this._tags.length === 0 || !this._tags[0].selfClosing()) { 1247 | return false; 1248 | } 1249 | if (tagName && this._tags[0].name() !== tagName) { 1250 | return false; 1251 | } 1252 | return true; 1253 | }; 1254 | 1255 | Character.prototype.isWhitespace = function() { 1256 | var _ref; 1257 | return ((_ref = this._c) === ' ' || _ref === '\n' || _ref === ' ') || this.isTag('br'); 1258 | }; 1259 | 1260 | Character.prototype.tags = function() { 1261 | var t; 1262 | return (function() { 1263 | var _i, _len, _ref, _results; 1264 | _ref = this._tags; 1265 | _results = []; 1266 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 1267 | t = _ref[_i]; 1268 | _results.push(t.copy()); 1269 | } 1270 | return _results; 1271 | }).call(this); 1272 | }; 1273 | 1274 | Character.prototype.addTags = function() { 1275 | var tag, tags, _i, _len, _results; 1276 | tags = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 1277 | _results = []; 1278 | for (_i = 0, _len = tags.length; _i < _len; _i++) { 1279 | tag = tags[_i]; 1280 | if (Array.isArray(tag)) { 1281 | continue; 1282 | } 1283 | if (tag.selfClosing()) { 1284 | if (!this.isTag()) { 1285 | this._tags.unshift(tag.copy()); 1286 | } 1287 | continue; 1288 | } 1289 | _results.push(this._tags.push(tag.copy())); 1290 | } 1291 | return _results; 1292 | }; 1293 | 1294 | Character.prototype.eq = function(c) { 1295 | var tag, tags, _i, _j, _len, _len1, _ref, _ref1; 1296 | if (this.c() !== c.c()) { 1297 | return false; 1298 | } 1299 | if (this._tags.length !== c._tags.length) { 1300 | return false; 1301 | } 1302 | tags = {}; 1303 | _ref = this._tags; 1304 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 1305 | tag = _ref[_i]; 1306 | tags[tag.head()] = true; 1307 | } 1308 | _ref1 = c._tags; 1309 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 1310 | tag = _ref1[_j]; 1311 | if (!tags[tag.head()]) { 1312 | return false; 1313 | } 1314 | } 1315 | return true; 1316 | }; 1317 | 1318 | Character.prototype.hasTags = function() { 1319 | var tag, tagHeads, tagNames, tags, _i, _j, _len, _len1, _ref; 1320 | tags = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 1321 | tagNames = {}; 1322 | tagHeads = {}; 1323 | _ref = this._tags; 1324 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 1325 | tag = _ref[_i]; 1326 | tagNames[tag.name()] = true; 1327 | tagHeads[tag.head()] = true; 1328 | } 1329 | for (_j = 0, _len1 = tags.length; _j < _len1; _j++) { 1330 | tag = tags[_j]; 1331 | if (typeof tag === 'string') { 1332 | if (tagNames[tag] === void 0) { 1333 | return false; 1334 | } 1335 | } else { 1336 | if (tagHeads[tag.head()] === void 0) { 1337 | return false; 1338 | } 1339 | } 1340 | } 1341 | return true; 1342 | }; 1343 | 1344 | Character.prototype.removeTags = function() { 1345 | var heads, names, newTags, tag, tags, _i, _len; 1346 | tags = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 1347 | if (tags.length === 0) { 1348 | this._tags = []; 1349 | return; 1350 | } 1351 | names = {}; 1352 | heads = {}; 1353 | for (_i = 0, _len = tags.length; _i < _len; _i++) { 1354 | tag = tags[_i]; 1355 | if (typeof tag === 'string') { 1356 | names[tag] = tag; 1357 | } else { 1358 | heads[tag.head()] = tag; 1359 | } 1360 | } 1361 | newTags = []; 1362 | return this._tags = this._tags.filter(function(tag) { 1363 | if (!heads[tag.head()] && !names[tag.name()]) { 1364 | return tag; 1365 | } 1366 | }); 1367 | }; 1368 | 1369 | Character.prototype.copy = function() { 1370 | var t; 1371 | return new HTMLString.Character(this._c, (function() { 1372 | var _i, _len, _ref, _results; 1373 | _ref = this._tags; 1374 | _results = []; 1375 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 1376 | t = _ref[_i]; 1377 | _results.push(t.copy()); 1378 | } 1379 | return _results; 1380 | }).call(this)); 1381 | }; 1382 | 1383 | return Character; 1384 | 1385 | })(); 1386 | 1387 | }).call(this); 1388 | -------------------------------------------------------------------------------- /build/html-string.min.js: -------------------------------------------------------------------------------- 1 | /*! HTMLString v1.0.6 by Anthony Blackshaw (https://github.com/anthonyjb) */ 2 | (function(){var FSM,exports;FSM={},FSM.Machine=function(){function Machine(context){this.context=context,this._stateTransitions={},this._stateTransitionsAny={},this._defaultTransition=null,this._initialState=null,this._currentState=null}return Machine.prototype.addTransition=function(action,state,nextState,callback){return nextState||(nextState=state),this._stateTransitions[[action,state]]=[nextState,callback]},Machine.prototype.addTransitions=function(actions,state,nextState,callback){var action,_i,_len,_results;for(nextState||(nextState=state),_results=[],_i=0,_len=actions.length;_len>_i;_i++)action=actions[_i],_results.push(this.addTransition(action,state,nextState,callback));return _results},Machine.prototype.addTransitionAny=function(state,nextState,callback){return nextState||(nextState=state),this._stateTransitionsAny[state]=[nextState,callback]},Machine.prototype.setDefaultTransition=function(state,callback){return this._defaultTransition=[state,callback]},Machine.prototype.getTransition=function(action,state){if(this._stateTransitions[[action,state]])return this._stateTransitions[[action,state]];if(this._stateTransitionsAny[state])return this._stateTransitionsAny[state];if(this._defaultTransition)return this._defaultTransition;throw new Error("Transition is undefined: ("+action+", "+state+")")},Machine.prototype.getCurrentState=function(){return this._currentState},Machine.prototype.setInitialState=function(state){return this._initialState=state,this._currentState?void 0:this.reset()},Machine.prototype.reset=function(){return this._currentState=this._initialState},Machine.prototype.process=function(action){var result;return result=this.getTransition(action,this._currentState),result[1]&&result[1].call(this.context||(this.context=this),action),this._currentState=result[0]},Machine}(),"undefined"!=typeof window&&(window.FSM=FSM),"undefined"!=typeof module&&module.exports&&(exports=module.exports=FSM)}).call(this),function(){var ALPHA_CHARS,ALPHA_NUMERIC_CHARS,ATTR_DELIM,ATTR_ENTITY_DOUBLE_DELIM,ATTR_ENTITY_NO_DELIM,ATTR_ENTITY_SINGLE_DELIM,ATTR_NAME,ATTR_NAME_CHARS,ATTR_NAME_FIND_VALUE,ATTR_OR_TAG_END,ATTR_VALUE_DOUBLE_DELIM,ATTR_VALUE_NO_DELIM,ATTR_VALUE_SINGLE_DELIM,CHAR_OR_ENTITY_OR_TAG,CLOSING_TAG,ENTITY,ENTITY_CHARS,HTMLString,OPENING_TAG,OPENNING_OR_CLOSING_TAG,TAG_NAME_CHARS,TAG_NAME_CLOSING,TAG_NAME_MUST_CLOSE,TAG_NAME_OPENING,TAG_OPENING_SELF_CLOSING,exports,_Parser,__slice=[].slice,__indexOf=[].indexOf||function(item){for(var i=0,l=this.length;l>i;i++)if(i in this&&this[i]===item)return i;return-1};HTMLString={},"undefined"!=typeof window&&(window.HTMLString=HTMLString),"undefined"!=typeof module&&module.exports&&(exports=module.exports=HTMLString),HTMLString.String=function(){function String(html,preserveWhitespace){null==preserveWhitespace&&(preserveWhitespace=!1),this._preserveWhitespace=preserveWhitespace,html?(null===HTMLString.String._parser&&(HTMLString.String._parser=new _Parser),this.characters=HTMLString.String._parser.parse(html,this._preserveWhitespace).characters):this.characters=[]}return String._parser=null,String.prototype.isWhitespace=function(){var c,_i,_len,_ref;for(_ref=this.characters,_i=0,_len=_ref.length;_len>_i;_i++)if(c=_ref[_i],!c.isWhitespace())return!1;return!0},String.prototype.length=function(){return this.characters.length},String.prototype.preserveWhitespace=function(){return this._preserveWhitespace},String.prototype.capitalize=function(){var c,newString;return newString=this.copy(),newString.length()&&(c=newString.characters[0]._c.toUpperCase(),newString.characters[0]._c=c),newString},String.prototype.charAt=function(index){return this.characters[index].copy()},String.prototype.concat=function(){var c,indexChar,inheritFormat,inheritedTags,newString,string,strings,tail,_i,_j,_k,_l,_len,_len1,_len2,_ref,_ref1;for(strings=2<=arguments.length?__slice.call(arguments,0,_i=arguments.length-1):(_i=0,[]),inheritFormat=arguments[_i++],"undefined"!=typeof inheritFormat&&"boolean"!=typeof inheritFormat&&(strings.push(inheritFormat),inheritFormat=!0),newString=this.copy(),_j=0,_len=strings.length;_len>_j;_j++)if(string=strings[_j],0!==string.length){if(tail=string,"string"==typeof string&&(tail=new HTMLString.String(string,this._preserveWhitespace)),inheritFormat&&newString.length())for(indexChar=newString.charAt(newString.length()-1),inheritedTags=indexChar.tags(),indexChar.isTag()&&inheritedTags.shift(),"string"!=typeof string&&(tail=tail.copy()),_ref=tail.characters,_k=0,_len1=_ref.length;_len1>_k;_k++)c=_ref[_k],c.addTags.apply(c,inheritedTags);for(_ref1=tail.characters,_l=0,_len2=_ref1.length;_len2>_l;_l++)c=_ref1[_l],newString.characters.push(c)}return newString},String.prototype.contains=function(substring){var c,found,from,i,_i,_len,_ref;if("string"==typeof substring)return this.text().indexOf(substring)>-1;for(from=0;from<=this.length()-substring.length();){for(found=!0,_ref=substring.characters,i=_i=0,_len=_ref.length;_len>_i;i=++_i)if(c=_ref[i],!c.eq(this.characters[i+from])){found=!1;break}if(found)return!0;from++}return!1},String.prototype.endsWith=function(substring){var c,characters,i,_i,_len,_ref;if("string"==typeof substring)return""===substring||this.text().slice(-substring.length)===substring;for(characters=this.characters.slice().reverse(),_ref=substring.characters.slice().reverse(),i=_i=0,_len=_ref.length;_len>_i;i=++_i)if(c=_ref[i],!c.eq(characters[i]))return!1;return!0},String.prototype.format=function(){var c,from,i,newString,tags,to,_i;for(from=arguments[0],to=arguments[1],tags=3<=arguments.length?__slice.call(arguments,2):[],0>to&&(to=this.length()+to+1),0>from&&(from=this.length()+from),newString=this.copy(),i=_i=from;to>=from?to>_i:_i>to;i=to>=from?++_i:--_i)c=newString.characters[i],c.addTags.apply(c,tags);return newString},String.prototype.hasTags=function(){var c,found,strict,tags,_i,_j,_len,_ref;for(tags=2<=arguments.length?__slice.call(arguments,0,_i=arguments.length-1):(_i=0,[]),strict=arguments[_i++],"undefined"!=typeof strict&&"boolean"!=typeof strict&&(tags.push(strict),strict=!1),found=!1,_ref=this.characters,_j=0,_len=_ref.length;_len>_j;_j++)if(c=_ref[_j],c.hasTags.apply(c,tags))found=!0;else if(strict)return!1;return found},String.prototype.html=function(){var c,closingTag,closingTags,head,html,openHeads,openTag,openTags,tag,_i,_j,_k,_l,_len,_len1,_len2,_len3,_len4,_m,_ref,_ref1,_ref2,_ref3;for(html="",openTags=[],openHeads=[],closingTags=[],_ref=this.characters,_i=0,_len=_ref.length;_len>_i;_i++){for(c=_ref[_i],closingTags=[],_ref1=openTags.slice().reverse(),_j=0,_len1=_ref1.length;_len1>_j;_j++)if(openTag=_ref1[_j],closingTags.push(openTag),!c.hasTags(openTag)){for(_k=0,_len2=closingTags.length;_len2>_k;_k++)closingTag=closingTags[_k],html+=closingTag.tail(),openTags.pop(),openHeads.pop();closingTags=[]}for(_ref2=c._tags,_l=0,_len3=_ref2.length;_len3>_l;_l++)tag=_ref2[_l],-1===openHeads.indexOf(tag.head())&&(tag.selfClosing()||(head=tag.head(),html+=head,openTags.push(tag),openHeads.push(head)));c._tags.length>0&&c._tags[0].selfClosing()&&(html+=c._tags[0].head()),html+=c.c()}for(_ref3=openTags.reverse(),_m=0,_len4=_ref3.length;_len4>_m;_m++)tag=_ref3[_m],html+=tag.tail();return html},String.prototype.indexOf=function(substring,from){var c,found,i,_i,_len,_ref;if(null==from&&(from=0),0>from&&(from=0),"string"==typeof substring)return this.text().indexOf(substring,from);for(;from<=this.length()-substring.length();){for(found=!0,_ref=substring.characters,i=_i=0,_len=_ref.length;_len>_i;i=++_i)if(c=_ref[i],!c.eq(this.characters[i+from])){found=!1;break}if(found)return from;from++}return-1},String.prototype.insert=function(index,substring,inheritFormat){var c,head,indexChar,inheritedTags,middle,newString,tail,_i,_j,_k,_len,_len1,_len2,_ref,_ref1,_ref2;if(null==inheritFormat&&(inheritFormat=!0),head=this.slice(0,index),tail=this.slice(index),0>index&&(index=this.length()+index),middle=substring,"string"==typeof substring&&(middle=new HTMLString.String(substring,this._preserveWhitespace)),inheritFormat&&index>0)for(indexChar=this.charAt(index-1),inheritedTags=indexChar.tags(),indexChar.isTag()&&inheritedTags.shift(),"string"!=typeof substring&&(middle=middle.copy()),_ref=middle.characters,_i=0,_len=_ref.length;_len>_i;_i++)c=_ref[_i],c.addTags.apply(c,inheritedTags);for(newString=head,_ref1=middle.characters,_j=0,_len1=_ref1.length;_len1>_j;_j++)c=_ref1[_j],newString.characters.push(c);for(_ref2=tail.characters,_k=0,_len2=_ref2.length;_len2>_k;_k++)c=_ref2[_k],newString.characters.push(c);return newString},String.prototype.lastIndexOf=function(substring,from){var c,characters,found,i,skip,_i,_j,_len,_len1;if(null==from&&(from=0),0>from&&(from=0),characters=this.characters.slice(from).reverse(),from=0,"string"==typeof substring){if(!this.contains(substring))return-1;for(substring=substring.split("").reverse();from<=characters.length-substring.length;){for(found=!0,skip=0,i=_i=0,_len=substring.length;_len>_i;i=++_i)if(c=substring[i],characters[i+from].isTag()&&(skip+=1),c!==characters[skip+i+from].c()){found=!1;break}if(found)return from;from++}return-1}for(substring=substring.characters.slice().reverse();from<=characters.length-substring.length;){for(found=!0,i=_j=0,_len1=substring.length;_len1>_j;i=++_j)if(c=substring[i],!c.eq(characters[i+from])){found=!1;break}if(found)return from;from++}return-1},String.prototype.optimize=function(){var c,closingTag,closingTags,head,lastC,len,openHeads,openTag,openTags,runLength,runLengthSort,runLengths,run_length,t,tag,_i,_j,_k,_l,_len,_len1,_len2,_len3,_len4,_len5,_len6,_m,_n,_o,_ref,_ref1,_ref2,_ref3,_ref4,_ref5,_results;for(openTags=[],openHeads=[],lastC=null,_ref=this.characters.slice().reverse(),_i=0,_len=_ref.length;_len>_i;_i++){for(c=_ref[_i],c._runLengthMap={},c._runLengthMapSize=0,closingTags=[],_ref1=openTags.slice().reverse(),_j=0,_len1=_ref1.length;_len1>_j;_j++)if(openTag=_ref1[_j],closingTags.push(openTag),!c.hasTags(openTag)){for(_k=0,_len2=closingTags.length;_len2>_k;_k++)closingTag=closingTags[_k],openTags.pop(),openHeads.pop();closingTags=[]}for(_ref2=c._tags,_l=0,_len3=_ref2.length;_len3>_l;_l++)tag=_ref2[_l],-1===openHeads.indexOf(tag.head())&&(tag.selfClosing()||(openTags.push(tag),openHeads.push(tag.head())));for(_m=0,_len4=openTags.length;_len4>_m;_m++)tag=openTags[_m],head=tag.head(),lastC?(c._runLengthMap[head]||(c._runLengthMap[head]=[tag,0]),run_length=0,lastC._runLengthMap[head]&&(run_length=lastC._runLengthMap[head][1]),c._runLengthMap[head][1]=run_length+1):c._runLengthMap[head]=[tag,1];lastC=c}for(runLengthSort=function(a,b){return b[1]-a[1]},_ref3=this.characters,_results=[],_n=0,_len5=_ref3.length;_len5>_n;_n++)if(c=_ref3[_n],len=c._tags.length,!(len>0&&c._tags[0].selfClosing()&&3>len||2>len)){runLengths=[],_ref4=c._runLengthMap;for(tag in _ref4)runLength=_ref4[tag],runLengths.push(runLength);for(runLengths.sort(runLengthSort),_ref5=c._tags.slice(),_o=0,_len6=_ref5.length;_len6>_o;_o++)tag=_ref5[_o],tag.selfClosing()||c.removeTags(tag);_results.push(c.addTags.apply(c,function(){var _len7,_p,_results1;for(_results1=[],_p=0,_len7=runLengths.length;_len7>_p;_p++)t=runLengths[_p],_results1.push(t[0]);return _results1}()))}return _results},String.prototype.slice=function(from,to){var c,newString;return newString=new HTMLString.String("",this._preserveWhitespace),newString.characters=function(){var _i,_len,_ref,_results;for(_ref=this.characters.slice(from,to),_results=[],_i=0,_len=_ref.length;_len>_i;_i++)c=_ref[_i],_results.push(c.copy());return _results}.call(this),newString},String.prototype.split=function(separator,limit){var count,end,i,index,indexes,lastIndex,start,substrings,_i,_ref;for(null==separator&&(separator=""),null==limit&&(limit=0),lastIndex=0,count=0,indexes=[0];;){if(limit>0&&count>limit)break;if(index=this.indexOf(separator,lastIndex),-1===index)break;indexes.push(index),lastIndex=index+1}for(indexes.push(this.length()),substrings=[],i=_i=0,_ref=indexes.length-2;_ref>=0?_ref>=_i:_i>=_ref;i=_ref>=0?++_i:--_i)start=indexes[i],i>0&&(start+=1),end=indexes[i+1],substrings.push(this.slice(start,end));return substrings},String.prototype.startsWith=function(substring){var c,i,_i,_len,_ref;if("string"==typeof substring)return this.text().slice(0,substring.length)===substring;for(_ref=substring.characters,i=_i=0,_len=_ref.length;_len>_i;i=++_i)if(c=_ref[i],!c.eq(this.characters[i]))return!1;return!0},String.prototype.substr=function(from,length){return 0>=length?new HTMLString.String("",this._preserveWhitespace):(0>from&&(from=this.length()+from),void 0===length&&(length=this.length()-from),this.slice(from,from+length))},String.prototype.substring=function(from,to){return void 0===to&&(to=this.length()),this.slice(from,to)},String.prototype.text=function(){var c,text,_i,_len,_ref;for(text="",_ref=this.characters,_i=0,_len=_ref.length;_len>_i;_i++)c=_ref[_i],c.isTag()?c.isTag("br")&&(text+="\n"):text+=(" "!==c.c(),c.c());return this.constructor.decode(text)},String.prototype.toLowerCase=function(){var c,newString,_i,_len,_ref;for(newString=this.copy(),_ref=newString.characters,_i=0,_len=_ref.length;_len>_i;_i++)c=_ref[_i],1===c._c.length&&(c._c=c._c.toLowerCase());return newString},String.prototype.toUpperCase=function(){var c,newString,_i,_len,_ref;for(newString=this.copy(),_ref=newString.characters,_i=0,_len=_ref.length;_len>_i;_i++)c=_ref[_i],1===c._c.length&&(c._c=c._c.toUpperCase());return newString},String.prototype.trim=function(){var c,from,newString,to,_i,_j,_len,_len1,_ref,_ref1;for(_ref=this.characters,from=_i=0,_len=_ref.length;_len>_i&&(c=_ref[from],c.isWhitespace());from=++_i);for(_ref1=this.characters.slice().reverse(),to=_j=0,_len1=_ref1.length;_len1>_j&&(c=_ref1[to],c.isWhitespace());to=++_j);return to=this.length()-to-1,newString=new HTMLString.String("",this._preserveWhitespace),newString.characters=function(){var _k,_len2,_ref2,_results;for(_ref2=this.characters.slice(from,+to+1||9e9),_results=[],_k=0,_len2=_ref2.length;_len2>_k;_k++)c=_ref2[_k],_results.push(c.copy());return _results}.call(this),newString},String.prototype.trimLeft=function(){var c,from,newString,to,_i,_len,_ref;for(to=this.length()-1,_ref=this.characters,from=_i=0,_len=_ref.length;_len>_i&&(c=_ref[from],c.isWhitespace());from=++_i);return newString=new HTMLString.String("",this._preserveWhitespace),newString.characters=function(){var _j,_len1,_ref1,_results;for(_ref1=this.characters.slice(from,+to+1||9e9),_results=[],_j=0,_len1=_ref1.length;_len1>_j;_j++)c=_ref1[_j],_results.push(c.copy());return _results}.call(this),newString},String.prototype.trimRight=function(){var c,from,newString,to,_i,_len,_ref;for(from=0,_ref=this.characters.slice().reverse(),to=_i=0,_len=_ref.length;_len>_i&&(c=_ref[to],c.isWhitespace());to=++_i);return to=this.length()-to-1,newString=new HTMLString.String("",this._preserveWhitespace),newString.characters=function(){var _j,_len1,_ref1,_results;for(_ref1=this.characters.slice(from,+to+1||9e9),_results=[],_j=0,_len1=_ref1.length;_len1>_j;_j++)c=_ref1[_j],_results.push(c.copy());return _results}.call(this),newString},String.prototype.unformat=function(){var c,from,i,newString,tags,to,_i;for(from=arguments[0],to=arguments[1],tags=3<=arguments.length?__slice.call(arguments,2):[],0>to&&(to=this.length()+to+1),0>from&&(from=this.length()+from),newString=this.copy(),i=_i=from;to>=from?to>_i:_i>to;i=to>=from?++_i:--_i)c=newString.characters[i],c.removeTags.apply(c,tags);return newString},String.prototype.copy=function(){var c,stringCopy;return stringCopy=new HTMLString.String("",this._preserveWhitespace),stringCopy.characters=function(){var _i,_len,_ref,_results;for(_ref=this.characters,_results=[],_i=0,_len=_ref.length;_len>_i;_i++)c=_ref[_i],_results.push(c.copy());return _results}.call(this),stringCopy},String.decode=function(string){var textarea;return textarea=document.createElement("textarea"),textarea.innerHTML=string,textarea.textContent},String.encode=function(string){var textarea;return textarea=document.createElement("textarea"),textarea.textContent=string,textarea.innerHTML},String.join=function(separator,strings){var joined,s,_i,_len;for(joined=strings.shift(),_i=0,_len=strings.length;_len>_i;_i++)s=strings[_i],joined=joined.concat(separator,s);return joined},String}(),ALPHA_CHARS="AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz-_$".split(""),ALPHA_NUMERIC_CHARS=ALPHA_CHARS.concat("1234567890".split("")),ATTR_NAME_CHARS=ALPHA_NUMERIC_CHARS.concat([":"]),ENTITY_CHARS=ALPHA_NUMERIC_CHARS.concat(["#"]),TAG_NAME_CHARS=ALPHA_NUMERIC_CHARS.concat([":"]),CHAR_OR_ENTITY_OR_TAG=1,ENTITY=2,OPENNING_OR_CLOSING_TAG=3,OPENING_TAG=4,CLOSING_TAG=5,TAG_NAME_OPENING=6,TAG_NAME_CLOSING=7,TAG_OPENING_SELF_CLOSING=8,TAG_NAME_MUST_CLOSE=9,ATTR_OR_TAG_END=10,ATTR_NAME=11,ATTR_NAME_FIND_VALUE=12,ATTR_DELIM=13,ATTR_VALUE_SINGLE_DELIM=14,ATTR_VALUE_DOUBLE_DELIM=15,ATTR_VALUE_NO_DELIM=16,ATTR_ENTITY_NO_DELIM=17,ATTR_ENTITY_SINGLE_DELIM=18,ATTR_ENTITY_DOUBLE_DELIM=19,_Parser=function(){function _Parser(){this.fsm=new FSM.Machine(this),this.fsm.setInitialState(CHAR_OR_ENTITY_OR_TAG),this.fsm.addTransitionAny(CHAR_OR_ENTITY_OR_TAG,null,function(c){return this._pushChar(c)}),this.fsm.addTransition("<",CHAR_OR_ENTITY_OR_TAG,OPENNING_OR_CLOSING_TAG),this.fsm.addTransition("&",CHAR_OR_ENTITY_OR_TAG,ENTITY),this.fsm.addTransition("END",CHAR_OR_ENTITY_OR_TAG,null),this.fsm.addTransitions(ENTITY_CHARS,ENTITY,null,function(c){return this.entity+=c}),this.fsm.addTransition(";",ENTITY,CHAR_OR_ENTITY_OR_TAG,function(){return this._pushChar("&"+this.entity+";"),this.entity=""}),this.fsm.addTransitionAny(ENTITY,CHAR_OR_ENTITY_OR_TAG,function(c){var _i,_len,_ref;for(this._pushChar("&"),_ref=this.entity.split(""),_i=0,_len=_ref.length;_len>_i;_i++)c=_ref[_i],this._pushChar(c);return this.entity="",this._back()}),this.fsm.addTransition("END",ENTITY,null,function(){var c,_i,_len,_ref;for(this._pushChar("&"),_ref=this.entity.split(""),_i=0,_len=_ref.length;_len>_i;_i++)c=_ref[_i],this._pushChar(c);return this.entity=""}),this.fsm.addTransitions([" ","\n"],OPENNING_OR_CLOSING_TAG),this.fsm.addTransitions(ALPHA_CHARS,OPENNING_OR_CLOSING_TAG,OPENING_TAG,function(){return this._back()}),this.fsm.addTransition("/",OPENNING_OR_CLOSING_TAG,CLOSING_TAG),this.fsm.addTransitions([" ","\n"],OPENING_TAG),this.fsm.addTransitions(ALPHA_CHARS,OPENING_TAG,TAG_NAME_OPENING,function(){return this._back()}),this.fsm.addTransitions([" ","\n"],CLOSING_TAG),this.fsm.addTransitions(ALPHA_CHARS,CLOSING_TAG,TAG_NAME_CLOSING,function(){return this._back()}),this.fsm.addTransitions(TAG_NAME_CHARS,TAG_NAME_OPENING,null,function(c){return this.tagName+=c}),this.fsm.addTransitions([" ","\n"],TAG_NAME_OPENING,ATTR_OR_TAG_END),this.fsm.addTransition("/",TAG_NAME_OPENING,TAG_OPENING_SELF_CLOSING,function(){return this.selfClosing=!0}),this.fsm.addTransition(">",TAG_NAME_OPENING,CHAR_OR_ENTITY_OR_TAG,function(){return this._pushTag()}),this.fsm.addTransitions([" ","\n"],TAG_OPENING_SELF_CLOSING),this.fsm.addTransition(">",TAG_OPENING_SELF_CLOSING,CHAR_OR_ENTITY_OR_TAG,function(){return this._pushTag()}),this.fsm.addTransitions([" ","\n"],ATTR_OR_TAG_END),this.fsm.addTransition("/",ATTR_OR_TAG_END,TAG_OPENING_SELF_CLOSING,function(){return this.selfClosing=!0}),this.fsm.addTransition(">",ATTR_OR_TAG_END,CHAR_OR_ENTITY_OR_TAG,function(){return this._pushTag()}),this.fsm.addTransitions(ALPHA_CHARS,ATTR_OR_TAG_END,ATTR_NAME,function(){return this._back()}),this.fsm.addTransitions(TAG_NAME_CHARS,TAG_NAME_CLOSING,null,function(c){return this.tagName+=c}),this.fsm.addTransitions([" ","\n"],TAG_NAME_CLOSING,TAG_NAME_MUST_CLOSE),this.fsm.addTransition(">",TAG_NAME_CLOSING,CHAR_OR_ENTITY_OR_TAG,function(){return this._popTag()}),this.fsm.addTransitions([" ","\n"],TAG_NAME_MUST_CLOSE),this.fsm.addTransition(">",TAG_NAME_MUST_CLOSE,CHAR_OR_ENTITY_OR_TAG,function(){return this._popTag()}),this.fsm.addTransitions(ATTR_NAME_CHARS,ATTR_NAME,null,function(c){return this.attributeName+=c}),this.fsm.addTransitions([" ","\n"],ATTR_NAME,ATTR_NAME_FIND_VALUE),this.fsm.addTransition("=",ATTR_NAME,ATTR_DELIM),this.fsm.addTransitions([" ","\n"],ATTR_NAME_FIND_VALUE),this.fsm.addTransition("=",ATTR_NAME_FIND_VALUE,ATTR_DELIM),this.fsm.addTransitions(">",ATTR_NAME,ATTR_OR_TAG_END,function(){return this._pushAttribute(),this._back()}),this.fsm.addTransitionAny(ATTR_NAME_FIND_VALUE,ATTR_OR_TAG_END,function(){return this._pushAttribute(),this._back()}),this.fsm.addTransitions([" ","\n"],ATTR_DELIM),this.fsm.addTransition("'",ATTR_DELIM,ATTR_VALUE_SINGLE_DELIM),this.fsm.addTransition('"',ATTR_DELIM,ATTR_VALUE_DOUBLE_DELIM),this.fsm.addTransitions(ALPHA_NUMERIC_CHARS.concat(["&"],ATTR_DELIM,ATTR_VALUE_NO_DELIM,function(){return this._back()})),this.fsm.addTransition(" ",ATTR_VALUE_NO_DELIM,ATTR_OR_TAG_END,function(){return this._pushAttribute()}),this.fsm.addTransitions(["/",">"],ATTR_VALUE_NO_DELIM,ATTR_OR_TAG_END,function(){return this._back(),this._pushAttribute()}),this.fsm.addTransition("&",ATTR_VALUE_NO_DELIM,ATTR_ENTITY_NO_DELIM),this.fsm.addTransitionAny(ATTR_VALUE_NO_DELIM,null,function(c){return this.attributeValue+=c}),this.fsm.addTransition("'",ATTR_VALUE_SINGLE_DELIM,ATTR_OR_TAG_END,function(){return this._pushAttribute()}),this.fsm.addTransition("&",ATTR_VALUE_SINGLE_DELIM,ATTR_ENTITY_SINGLE_DELIM),this.fsm.addTransitionAny(ATTR_VALUE_SINGLE_DELIM,null,function(c){return this.attributeValue+=c}),this.fsm.addTransition('"',ATTR_VALUE_DOUBLE_DELIM,ATTR_OR_TAG_END,function(){return this._pushAttribute()}),this.fsm.addTransition("&",ATTR_VALUE_DOUBLE_DELIM,ATTR_ENTITY_DOUBLE_DELIM),this.fsm.addTransitionAny(ATTR_VALUE_DOUBLE_DELIM,null,function(c){return this.attributeValue+=c}),this.fsm.addTransitions(ENTITY_CHARS,ATTR_ENTITY_NO_DELIM,null,function(c){return this.entity+=c}),this.fsm.addTransitions(ENTITY_CHARS,ATTR_ENTITY_SINGLE_DELIM,null,function(c){return this.entity+=c}),this.fsm.addTransitions(ENTITY_CHARS,ATTR_ENTITY_DOUBLE_DELIM,null,function(c){return this.entity+=c}),this.fsm.addTransition(";",ATTR_ENTITY_NO_DELIM,ATTR_VALUE_NO_DELIM,function(){return this.attributeValue+="&"+this.entity+";",this.entity=""}),this.fsm.addTransition(";",ATTR_ENTITY_SINGLE_DELIM,ATTR_VALUE_SINGLE_DELIM,function(){return this.attributeValue+="&"+this.entity+";",this.entity=""}),this.fsm.addTransition(";",ATTR_ENTITY_DOUBLE_DELIM,ATTR_VALUE_DOUBLE_DELIM,function(){return this.attributeValue+="&"+this.entity+";",this.entity=""}),this.fsm.addTransitionAny(ATTR_ENTITY_NO_DELIM,ATTR_VALUE_NO_DELIM,function(){return this.attributeValue+="&"+this.entity,this.entity="",this._back()}),this.fsm.addTransitionAny(ATTR_ENTITY_SINGLE_DELIM,ATTR_VALUE_SINGLE_DELIM,function(){return this.attributeValue+="&"+this.entity,this.entity="",this._back()}),this.fsm.addTransitionAny(ATTR_ENTITY_DOUBLE_DELIM,ATTR_VALUE_DOUBLE_DELIM,function(){return this.attributeValue+="&"+this.entity,this.entity="",this._back()})}return _Parser.prototype._back=function(){return this.head--},_Parser.prototype._pushAttribute=function(){return this.attributes[this.attributeName]=this.attributeValue,this.attributeName="",this.attributeValue=""},_Parser.prototype._pushChar=function(c){var character,lastCharacter;return character=new HTMLString.Character(c,this.tags),this._preserveWhitespace?void this.string.characters.push(character):!this.string.length()||character.isTag()||character.isEntity()||!character.isWhitespace()||(lastCharacter=this.string.characters[this.string.length()-1],!lastCharacter.isWhitespace()||lastCharacter.isTag()||lastCharacter.isEntity())?this.string.characters.push(character):void 0},_Parser.prototype._pushTag=function(){var tag,_ref;return tag=new HTMLString.Tag(this.tagName,this.attributes),this.tags.push(tag),tag.selfClosing()&&(this._pushChar(""),this.tags.pop(),!this.selfClosed&&(_ref=this.tagName,__indexOf.call(HTMLString.Tag.SELF_CLOSING,_ref)>=0)&&this.fsm.reset()),this.tagName="",this.selfClosed=!1,this.attributes={}},_Parser.prototype._popTag=function(){for(var character,tag;;)if(tag=this.tags.pop(),this.string.length()&&(character=this.string.characters[this.string.length()-1],character.isTag()||character.isEntity()||!character.isWhitespace()||character.removeTags(tag)),tag.name()===this.tagName.toLowerCase())break;return this.tagName=""},_Parser.prototype.parse=function(html,preserveWhitespace){var character,error;for(this._preserveWhitespace=preserveWhitespace,this.reset(),html=this.preprocess(html),this.fsm.parser=this;this.head> "+error)}this.head++}return this.fsm.process("END"),this.string},_Parser.prototype.preprocess=function(html){return html=html.replace(/\r\n/g,"\n").replace(/\r/g,"\n"),html=html.replace(//g,""),this._preserveWhitespace||(html=html.replace(/\s+/g," ")),html},_Parser.prototype.reset=function(){return this.fsm.reset(),this.head=0,this.string=new HTMLString.String,this.entity="",this.tags=[],this.tagName="",this.selfClosing=!1,this.attributes={},this.attributeName="",this.attributeValue=""},_Parser}(),HTMLString.Tag=function(){function Tag(name,attributes){var k,v;this._name=name.toLowerCase(),this._selfClosing=HTMLString.Tag.SELF_CLOSING[this._name]===!0,this._head=null,this._attributes={};for(k in attributes)v=attributes[k],this._attributes[k]=v}return Tag.SELF_CLOSING={area:!0,base:!0,br:!0,hr:!0,img:!0,input:!0,"link meta":!0,wbr:!0},Tag.prototype.head=function(){var components,k,v,_ref;if(!this._head){components=[],_ref=this._attributes;for(k in _ref)v=_ref[k],components.push(v?""+k+'="'+v+'"':""+k);components.sort(),components.unshift(this._name),this._head="<"+components.join(" ")+">"}return this._head},Tag.prototype.name=function(){return this._name},Tag.prototype.selfClosing=function(){return this._selfClosing},Tag.prototype.tail=function(){return this._selfClosing?"":""},Tag.prototype.attr=function(name,value){return void 0===value?this._attributes[name]:(this._attributes[name]=value,this._head=null)},Tag.prototype.removeAttr=function(name){return void 0!==this._attributes[name]?(delete this._attributes[name],this._head=null):void 0},Tag.prototype.copy=function(){return new HTMLString.Tag(this._name,this._attributes)},Tag}(),HTMLString.Character=function(){function Character(c,tags){this._c=c,c.length>1&&(this._c=c.toLowerCase()),this._tags=[],this.addTags.apply(this,tags)}return Character.prototype.c=function(){return this._c},Character.prototype.isEntity=function(){return this._c.length>1},Character.prototype.isTag=function(tagName){return 0!==this._tags.length&&this._tags[0].selfClosing()?tagName&&this._tags[0].name()!==tagName?!1:!0:!1},Character.prototype.isWhitespace=function(){var _ref;return" "===(_ref=this._c)||"\n"===_ref||" "===_ref||this.isTag("br")},Character.prototype.tags=function(){var t;return function(){var _i,_len,_ref,_results;for(_ref=this._tags,_results=[],_i=0,_len=_ref.length;_len>_i;_i++)t=_ref[_i],_results.push(t.copy());return _results}.call(this)},Character.prototype.addTags=function(){var tag,tags,_i,_len,_results;for(tags=1<=arguments.length?__slice.call(arguments,0):[],_results=[],_i=0,_len=tags.length;_len>_i;_i++)tag=tags[_i],Array.isArray(tag)||(tag.selfClosing()?this.isTag()||this._tags.unshift(tag.copy()):_results.push(this._tags.push(tag.copy())));return _results},Character.prototype.eq=function(c){var tag,tags,_i,_j,_len,_len1,_ref,_ref1;if(this.c()!==c.c())return!1;if(this._tags.length!==c._tags.length)return!1;for(tags={},_ref=this._tags,_i=0,_len=_ref.length;_len>_i;_i++)tag=_ref[_i],tags[tag.head()]=!0;for(_ref1=c._tags,_j=0,_len1=_ref1.length;_len1>_j;_j++)if(tag=_ref1[_j],!tags[tag.head()])return!1;return!0},Character.prototype.hasTags=function(){var tag,tagHeads,tagNames,tags,_i,_j,_len,_len1,_ref;for(tags=1<=arguments.length?__slice.call(arguments,0):[],tagNames={},tagHeads={},_ref=this._tags,_i=0,_len=_ref.length;_len>_i;_i++)tag=_ref[_i],tagNames[tag.name()]=!0,tagHeads[tag.head()]=!0;for(_j=0,_len1=tags.length;_len1>_j;_j++)if(tag=tags[_j],"string"==typeof tag){if(void 0===tagNames[tag])return!1}else if(void 0===tagHeads[tag.head()])return!1;return!0},Character.prototype.removeTags=function(){var heads,names,newTags,tag,tags,_i,_len;if(tags=1<=arguments.length?__slice.call(arguments,0):[],0===tags.length)return void(this._tags=[]);for(names={},heads={},_i=0,_len=tags.length;_len>_i;_i++)tag=tags[_i],"string"==typeof tag?names[tag]=tag:heads[tag.head()]=tag;return newTags=[],this._tags=this._tags.filter(function(tag){return heads[tag.head()]||names[tag.name()]?void 0:tag})},Character.prototype.copy=function(){var t;return new HTMLString.Character(this._c,function(){var _i,_len,_ref,_results;for(_ref=this._tags,_results=[],_i=0,_len=_ref.length;_len>_i;_i++)t=_ref[_i],_results.push(t.copy());return _results}.call(this))},Character}()}.call(this); -------------------------------------------------------------------------------- /external/fsm.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var FSM, exports; 3 | 4 | FSM = {}; 5 | 6 | FSM.Machine = (function() { 7 | function Machine(context) { 8 | this.context = context; 9 | this._stateTransitions = {}; 10 | this._stateTransitionsAny = {}; 11 | this._defaultTransition = null; 12 | this._initialState = null; 13 | this._currentState = null; 14 | } 15 | 16 | Machine.prototype.addTransition = function(action, state, nextState, callback) { 17 | if (!nextState) { 18 | nextState = state; 19 | } 20 | return this._stateTransitions[[action, state]] = [nextState, callback]; 21 | }; 22 | 23 | Machine.prototype.addTransitions = function(actions, state, nextState, callback) { 24 | var action, _i, _len, _results; 25 | if (!nextState) { 26 | nextState = state; 27 | } 28 | _results = []; 29 | for (_i = 0, _len = actions.length; _i < _len; _i++) { 30 | action = actions[_i]; 31 | _results.push(this.addTransition(action, state, nextState, callback)); 32 | } 33 | return _results; 34 | }; 35 | 36 | Machine.prototype.addTransitionAny = function(state, nextState, callback) { 37 | if (!nextState) { 38 | nextState = state; 39 | } 40 | return this._stateTransitionsAny[state] = [nextState, callback]; 41 | }; 42 | 43 | Machine.prototype.setDefaultTransition = function(state, callback) { 44 | return this._defaultTransition = [state, callback]; 45 | }; 46 | 47 | Machine.prototype.getTransition = function(action, state) { 48 | if (this._stateTransitions[[action, state]]) { 49 | return this._stateTransitions[[action, state]]; 50 | } else if (this._stateTransitionsAny[state]) { 51 | return this._stateTransitionsAny[state]; 52 | } else if (this._defaultTransition) { 53 | return this._defaultTransition; 54 | } 55 | throw new Error("Transition is undefined: (" + action + ", " + state + ")"); 56 | }; 57 | 58 | Machine.prototype.getCurrentState = function() { 59 | return this._currentState; 60 | }; 61 | 62 | Machine.prototype.setInitialState = function(state) { 63 | this._initialState = state; 64 | if (!this._currentState) { 65 | return this.reset(); 66 | } 67 | }; 68 | 69 | Machine.prototype.reset = function() { 70 | return this._currentState = this._initialState; 71 | }; 72 | 73 | Machine.prototype.process = function(action) { 74 | var result; 75 | result = this.getTransition(action, this._currentState); 76 | if (result[1]) { 77 | result[1].call(this.context || (this.context = this), action); 78 | } 79 | return this._currentState = result[0]; 80 | }; 81 | 82 | return Machine; 83 | 84 | })(); 85 | 86 | if (typeof window !== 'undefined') { 87 | window.FSM = FSM; 88 | } 89 | 90 | if (typeof module !== 'undefined' && module.exports) { 91 | exports = module.exports = FSM; 92 | } 93 | 94 | }).call(this); 95 | -------------------------------------------------------------------------------- /npm-debug.log: -------------------------------------------------------------------------------- 1 | 0 info it worked if it ends with ok 2 | 1 verbose cli [ '/usr/local/bin/node', '/usr/local/bin/npm', 'publish' ] 3 | 2 info using npm@1.4.28 4 | 3 info using node@v0.10.33 5 | 4 verbose publish [ '.' ] 6 | 5 verbose cache add [ '.', null ] 7 | 6 verbose cache add name=undefined spec="." args=[".",null] 8 | 7 verbose parsed url { protocol: null, 9 | 7 verbose parsed url slashes: null, 10 | 7 verbose parsed url auth: null, 11 | 7 verbose parsed url host: null, 12 | 7 verbose parsed url port: null, 13 | 7 verbose parsed url hostname: null, 14 | 7 verbose parsed url hash: null, 15 | 7 verbose parsed url search: null, 16 | 7 verbose parsed url query: null, 17 | 7 verbose parsed url pathname: '.', 18 | 7 verbose parsed url path: '.', 19 | 7 verbose parsed url href: '.' } 20 | 8 silly lockFile 3a52ce78- . 21 | 9 verbose lock . /home/anthony/.npm/3a52ce78-.lock 22 | 10 verbose tar pack [ '/home/anthony/.npm/HTMLString/1.0.6/package.tgz', '.' ] 23 | 11 verbose tarball /home/anthony/.npm/HTMLString/1.0.6/package.tgz 24 | 12 verbose folder . 25 | 13 info prepublish HTMLString@1.0.6 26 | 14 silly lockFile 1f1177db-tar tar://. 27 | 15 verbose lock tar://. /home/anthony/.npm/1f1177db-tar.lock 28 | 16 silly lockFile 423837ce-npm-HTMLString-1-0-6-package-tgz tar:///home/anthony/.npm/HTMLString/1.0.6/package.tgz 29 | 17 verbose lock tar:///home/anthony/.npm/HTMLString/1.0.6/package.tgz /home/anthony/.npm/423837ce-npm-HTMLString-1-0-6-package-tgz.lock 30 | 18 silly lockFile 1f1177db-tar tar://. 31 | 19 silly lockFile 1f1177db-tar tar://. 32 | 20 silly lockFile 423837ce-npm-HTMLString-1-0-6-package-tgz tar:///home/anthony/.npm/HTMLString/1.0.6/package.tgz 33 | 21 silly lockFile 423837ce-npm-HTMLString-1-0-6-package-tgz tar:///home/anthony/.npm/HTMLString/1.0.6/package.tgz 34 | 22 silly lockFile b99c44c8-ony-npm-HTMLString-1-0-6-package /home/anthony/.npm/HTMLString/1.0.6/package 35 | 23 verbose lock /home/anthony/.npm/HTMLString/1.0.6/package /home/anthony/.npm/b99c44c8-ony-npm-HTMLString-1-0-6-package.lock 36 | 24 silly lockFile b99c44c8-ony-npm-HTMLString-1-0-6-package /home/anthony/.npm/HTMLString/1.0.6/package 37 | 25 silly lockFile b99c44c8-ony-npm-HTMLString-1-0-6-package /home/anthony/.npm/HTMLString/1.0.6/package 38 | 26 silly lockFile 3a52ce78- . 39 | 27 silly lockFile 3a52ce78- . 40 | 28 silly publish { name: 'HTMLString', 41 | 28 silly publish description: 'An HTML parser written in JavaScript that\'s probably not what you\'re looking for.', 42 | 28 silly publish version: '1.0.6', 43 | 28 silly publish keywords: [ 'html', 'parser' ], 44 | 28 silly publish author: 45 | 28 silly publish { name: 'Anthony Blackshaw', 46 | 28 silly publish email: 'ant@getme.co.uk', 47 | 28 silly publish url: 'https://github.com/anthonyjb' }, 48 | 28 silly publish main: 'build/html-string.js', 49 | 28 silly publish devDependencies: 50 | 28 silly publish { grunt: '~0.4.5', 51 | 28 silly publish 'grunt-contrib-clean': '^0.6.0', 52 | 28 silly publish 'grunt-contrib-coffee': '^0.11.1', 53 | 28 silly publish 'grunt-contrib-concat': '^0.5.0', 54 | 28 silly publish 'grunt-contrib-jasmine': '^0.9.2', 55 | 28 silly publish 'grunt-contrib-uglify': '^0.5.1', 56 | 28 silly publish 'grunt-contrib-watch': '^0.6.1' }, 57 | 28 silly publish scripts: { test: 'grunt jasmine --verbose' }, 58 | 28 silly publish repository: 59 | 28 silly publish { type: 'git', 60 | 28 silly publish url: 'https://github.com/GetmeUK/HTMLString.git' }, 61 | 28 silly publish license: 'MIT', 62 | 28 silly publish readme: '# HTMLString\n\n[![Build Status](https://travis-ci.org/GetmeUK/HTMLString.svg?branch=master)](https://travis-ci.org/GetmeUK/HTMLString)\n\n> An HTML parser written in JavaScript that\'s probably not what you\'re looking for.\n\n## Install\n\n**Using bower**\n\n```\nbower install --save HTMLString\n```\n\n**Using npm**\n\n```\nnpm install --save HTMLString\n```\n\n## Building\nTo build the library you\'ll need to use Grunt. First install the required node modules ([grunt-cli](http://gruntjs.com/getting-started) must be installed):\n```\ngit clone https://github.com/GetmeUK/HTMLString.git\ncd HTMLString\nnpm install\n```\n\nThen run `grunt build` to build the project.\n\n## Testing\nTo test the library you\'ll need to use Jasmine. First install Jasmine:\n```\ngit clone https://github.com/pivotal/jasmine.git\nmkdir HTMLString/jasmine\nmv jasmine/dist/jasmine-standalone-2.0.3.zip HTMLString/jasmine\ncd HTMLString/jasmine\nunzip jasmine-standalone-2.0.3.zip\n```\n\nThen open `HTMLString/SpecRunner.html` in a browser to run the tests.\n\nAlternatively you can use `grunt jasmine` to run the tests from the command line.\n\n## Documentation\nFull documentation is available at http://getcontenttools.com/api/html-string\n\n## Browser support\n- Chrome\n- Firefox\n- IE9+\n', 63 | 28 silly publish readmeFilename: 'README.md', 64 | 28 silly publish gitHead: '8ca1028b8f00c0ae20e25887e57999c7eb6fb03b', 65 | 28 silly publish bugs: { url: 'https://github.com/GetmeUK/HTMLString/issues' }, 66 | 28 silly publish homepage: 'https://github.com/GetmeUK/HTMLString', 67 | 28 silly publish _id: 'HTMLString@1.0.6', 68 | 28 silly publish _shasum: '0dbd2b90ad9ff5634d21f366ecbf9c4954477425', 69 | 28 silly publish _from: '.' } 70 | 29 verbose request where is /HTMLString 71 | 30 verbose request registry https://registry.npmjs.org/ 72 | 31 verbose request id 9915c72d0522b64e 73 | 32 verbose url raw /HTMLString 74 | 33 verbose url resolving [ 'https://registry.npmjs.org/', './HTMLString' ] 75 | 34 verbose url resolved https://registry.npmjs.org/HTMLString 76 | 35 verbose request where is https://registry.npmjs.org/HTMLString 77 | 36 info trying registry request attempt 1 at 21:57:55 78 | 37 http PUT https://registry.npmjs.org/HTMLString 79 | 38 http 401 https://registry.npmjs.org/HTMLString 80 | 39 verbose headers { 'content-type': 'application/json', 81 | 39 verbose headers 'cache-control': 'max-age=300', 82 | 39 verbose headers 'content-length': '42', 83 | 39 verbose headers 'accept-ranges': 'bytes', 84 | 39 verbose headers date: 'Sun, 23 Oct 2016 20:57:49 GMT', 85 | 39 verbose headers via: '1.1 varnish', 86 | 39 verbose headers connection: 'keep-alive', 87 | 39 verbose headers 'x-served-by': 'cache-lcy1130-LCY', 88 | 39 verbose headers 'x-cache': 'MISS', 89 | 39 verbose headers 'x-cache-hits': '0', 90 | 39 verbose headers 'x-timer': 'S1477256268.696056,VS0,VE1166', 91 | 39 verbose headers vary: 'Accept-Encoding' } 92 | 40 error publish Failed PUT 401 93 | 41 error Error: Could not authenticate getmeuk : HTMLString 94 | 41 error at RegClient. (/usr/local/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:308:14) 95 | 41 error at Request._callback (/usr/local/lib/node_modules/npm/node_modules/npm-registry-client/lib/request.js:246:65) 96 | 41 error at Request.self.callback (/usr/local/lib/node_modules/npm/node_modules/request/request.js:236:22) 97 | 41 error at Request.emit (events.js:98:17) 98 | 41 error at Request. (/usr/local/lib/node_modules/npm/node_modules/request/request.js:1142:14) 99 | 41 error at Request.emit (events.js:117:20) 100 | 41 error at IncomingMessage. (/usr/local/lib/node_modules/npm/node_modules/request/request.js:1096:12) 101 | 41 error at IncomingMessage.emit (events.js:117:20) 102 | 41 error at _stream_readable.js:943:16 103 | 41 error at process._tickCallback (node.js:419:13) 104 | 42 error If you need help, you may report this *entire* log, 105 | 42 error including the npm and node versions, at: 106 | 42 error 107 | 43 error System Linux 3.13.0-100-generic 108 | 44 error command "/usr/local/bin/node" "/usr/local/bin/npm" "publish" 109 | 45 error cwd /home/anthony/Desktop/Work/Public/CoffeeScript/github/HTMLString 110 | 46 error node -v v0.10.33 111 | 47 error npm -v 1.4.28 112 | 48 verbose exit [ 1, true ] 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTMLString", 3 | "description": "An HTML parser written in JavaScript that's probably not what you're looking for.", 4 | "version": "1.0.7", 5 | "keywords": [ 6 | "html", 7 | "parser" 8 | ], 9 | "author": { 10 | "name": "Anthony Blackshaw", 11 | "email": "ant@getme.co.uk", 12 | "url": "https://github.com/anthonyjb" 13 | }, 14 | "main": "build/html-string.js", 15 | "devDependencies": { 16 | "grunt": "~0.4.5", 17 | "grunt-contrib-clean": "^0.6.0", 18 | "grunt-contrib-coffee": "^0.11.1", 19 | "grunt-contrib-concat": "^0.5.0", 20 | "grunt-contrib-jasmine": "^0.9.2", 21 | "grunt-contrib-uglify": "^0.5.1", 22 | "grunt-contrib-watch": "^0.6.1" 23 | }, 24 | "scripts": { 25 | "test": "grunt jasmine --verbose" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/GetmeUK/HTMLString.git" 30 | }, 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /spec/html-string-spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var quotes; 3 | 4 | quotes = { 5 | Gates: 'We all need people who will give us feedback. That\'s how we improve. ', 6 | Kernighan: ' \n Everyone knows that debugging is twice as hard as writing a program in\n the first place. So if you\'re as clever as you can be when you write\n it, how will you ever debug it?\n ', 7 | Ritchie: 'I can\'t recall any difficulty in making the C language definition completely\nopen - any discussion on the matter tended to mention languages whose\ninventors tried to keep tight control, and consequent ill fate.', 8 | Turing: 'Machines take me by
surprise with great frequency.
', 9 | Wozniak: 'all the best people in life seem to like LINUX.', 10 | WozniakNamespaced: 'all the best people in life seem to like LINUX.', 11 | WozniakWhitespace: 'all the best people in life seem to like LINUX.', 12 | AmbiguousAmpersand: '& &amp &foo && && &end' 13 | }; 14 | 15 | describe('HTMLString.String()', function() { 16 | it('should parse and render text (no HTML)', function() { 17 | var string; 18 | string = new HTMLString.String(quotes.Wozniak); 19 | return expect(string.text()).toBe(quotes.Wozniak); 20 | }); 21 | it('should parse and render text (no HTML with whitespace preserved)', function() { 22 | var string; 23 | string = new HTMLString.String(quotes.WozniakWhitespace, true); 24 | return expect(string.text()).toBe(quotes.WozniakWhitespace); 25 | }); 26 | it('should parse and render a string (HTML)', function() { 27 | var string; 28 | string = new HTMLString.String(quotes.Turing); 29 | expect(string.html()).toBe(quotes.Turing); 30 | string = new HTMLString.String(quotes.WozniakNamespaced); 31 | return expect(string.html()).toBe(quotes.WozniakNamespaced); 32 | }); 33 | return it('should parse and render a string (HTML with ambiguous ampersands)', function() { 34 | var string; 35 | string = new HTMLString.String(quotes.AmbiguousAmpersand); 36 | expect(string.html()).toBe(quotes.AmbiguousAmpersand); 37 | return console.log(string.html()); 38 | }); 39 | }); 40 | 41 | describe('HTMLString.String.isWhitespace()', function() { 42 | it('should return true if a string consists entirely of whitespace characters', function() { 43 | var string; 44 | string = new HTMLString.String(" 
"); 45 | return expect(string.isWhitespace()).toBe(true); 46 | }); 47 | return it('should return false if a string contains any non-whitespace character', function() { 48 | var string; 49 | string = new HTMLString.String("  a
"); 50 | return expect(string.isWhitespace()).toBe(false); 51 | }); 52 | }); 53 | 54 | describe('HTMLString.String.length()', function() { 55 | return it('should return the length of a string', function() { 56 | var string; 57 | string = new HTMLString.String(quotes.Turing); 58 | return expect(string.length()).toBe(52); 59 | }); 60 | }); 61 | 62 | describe('HTMLString.String.preserveWhitespace()', function() { 63 | return it('should return the true if whitespace is reserved for the string', function() { 64 | var string; 65 | string = new HTMLString.String(quotes.Turing); 66 | expect(string.preserveWhitespace()).toBe(false); 67 | string = new HTMLString.String(quotes.Turing, true); 68 | return expect(string.preserveWhitespace()).toBe(true); 69 | }); 70 | }); 71 | 72 | describe('HTMLString.String.capitalize()', function() { 73 | return it('should capitalize the first character of a string', function() { 74 | var newString, string; 75 | string = new HTMLString.String(quotes.Wozniak); 76 | newString = string.capitalize(); 77 | return expect(newString.charAt(0).c()).toBe('A'); 78 | }); 79 | }); 80 | 81 | describe('HTMLString.String.charAt()', function() { 82 | return it('should return a character from a string at the specified index', function() { 83 | var string; 84 | string = new HTMLString.String(quotes.Turing); 85 | return expect(string.charAt(18).c()).toBe('y'); 86 | }); 87 | }); 88 | 89 | describe('HTMLString.String.concat()', function() { 90 | return it('should combine 2 or more strings and return a new string', function() { 91 | var newString, stringA, stringB; 92 | stringA = new HTMLString.String(quotes.Turing); 93 | stringB = new HTMLString.String(quotes.Wozniak); 94 | newString = stringA.concat(stringB); 95 | return expect(newString.html()).toBe('Machines take me by
surprise with great frequency.all the best people in life seem to like LINUX.
'); 96 | }); 97 | }); 98 | 99 | describe('HTMLString.String.contain()', function() { 100 | return it('should return true if a string contains a substring', function() { 101 | var string, substring; 102 | string = new HTMLString.String(quotes.Turing); 103 | substring = new HTMLString.String('take me'); 104 | expect(string.contains('take me')).toBe(true); 105 | return expect(string.contains(substring)).toBe(true); 106 | }); 107 | }); 108 | 109 | describe('HTMLString.String.endswith()', function() { 110 | return it('should return true if a string ends with a substring.`', function() { 111 | var string, substring; 112 | string = new HTMLString.String(quotes.Turing); 113 | substring = new HTMLString.String('great frequency.'); 114 | expect(string.endsWith("great" + (HTMLString.String.decode(' ')) + "frequency.")).toBe(true); 115 | return expect(string.endsWith(substring)).toBe(true); 116 | }); 117 | }); 118 | 119 | describe('HTMLString.String.format()', function() { 120 | return it('should format a selection of characters in a string (add tags)', function() { 121 | var string; 122 | string = new HTMLString.String(quotes.Ritchie); 123 | string = string.format(0, -1, new HTMLString.Tag('q')); 124 | string = string.format(19, 29, new HTMLString.Tag('a', { 125 | href: 'http://www.getme.co.uk' 126 | }), new HTMLString.Tag('b')); 127 | return expect(string.html()).toBe("I can't recall any difficulty in making the C language definition completely open - any discussion on the matter tended to mention languages whose inventors tried to keep tight control, and consequent ill fate.".trim()); 128 | }); 129 | }); 130 | 131 | describe('HTMLString.String.hasTags()', function() { 132 | return it('should return true if the string has and characters formatted with the specifed tags', function() { 133 | var string; 134 | string = new HTMLString.String(quotes.Kernighan).trim(); 135 | expect(string.hasTags(new HTMLString.Tag('q'), 'b')).toBe(true); 136 | return expect(string.hasTags(new HTMLString.Tag('q')), true).toBe(true); 137 | }); 138 | }); 139 | 140 | describe('HTMLString.String.indexOf()', function() { 141 | return it('should return the first position of an substring in another string, or -1 if there is no match', function() { 142 | var string, substring; 143 | string = new HTMLString.String(quotes.Kernighan).trim(); 144 | substring = new HTMLString.String('debugging'); 145 | expect(string.indexOf('debugging')).toBe(20); 146 | expect(string.indexOf(substring)).toBe(20); 147 | return expect(string.indexOf(substring, 30)).toBe(-1); 148 | }); 149 | }); 150 | 151 | describe('HTMLString.String.insert()', function() { 152 | return it('should insert a string into another string and return a new string', function() { 153 | var newString, stringA, stringB; 154 | stringA = new HTMLString.String(quotes.Kernighan).trim(); 155 | stringB = new HTMLString.String(quotes.Turing); 156 | newString = stringA.insert(9, stringB); 157 | newString = newString.insert(9 + stringB.length(), ' - new string inserted - '); 158 | expect(newString.html()).toBe('Everyone Machines take me by
surprise with great frequency. - new string inserted -
knows that debugging is twice as hard as writing a program in the first place. So if you\'re as clever as you can be when you write it, how will you ever debug it?
'); 159 | newString = stringA.insert(9, ' - insert unformatted string - ', false); 160 | return expect(newString.html()).toBe('Everyone - insert unformatted string - knows that debugging is twice as hard as writing a program in the first place. So if you\'re as clever as you can be when you write it, how will you ever debug it?'); 161 | }); 162 | }); 163 | 164 | describe('HTMLString.String.lastIndexOf()', function() { 165 | return it('should return the last position of an substring in another string, or -1 if there is no match', function() { 166 | var string, substring; 167 | string = new HTMLString.String(quotes.Kernighan).trim(); 168 | substring = new HTMLString.String('debugging'); 169 | expect(string.lastIndexOf('debugging')).toBe(142); 170 | expect(string.lastIndexOf(substring)).toBe(142); 171 | return expect(string.lastIndexOf(substring, 30)).toBe(-1); 172 | }); 173 | }); 174 | 175 | describe('HTMLString.String.optimize()', function() { 176 | return it('should optimize the string (in place) so that tags are restacked in order of longest running', function() { 177 | var string; 178 | string = new HTMLString.String(quotes.Gates); 179 | string.optimize(); 180 | return expect(string.html()).toBe('We all need people who will give us feedback. That\'s how we improve. '); 181 | }); 182 | }); 183 | 184 | describe('HTMLString.String.slice()', function() { 185 | return it('should extract a section of a string and return a new string', function() { 186 | var newString, string; 187 | string = new HTMLString.String(quotes.Kernighan).trim(); 188 | newString = string.slice(10, 19); 189 | return expect(newString.html()).toBe('nows that'); 190 | }); 191 | }); 192 | 193 | describe('HTMLString.String.split()', function() { 194 | return it('should split a string by the separator and return a list of sub-strings', function() { 195 | var string, substrings; 196 | string = new HTMLString.String(' ', true); 197 | substrings = string.split(' '); 198 | expect(substrings.length).toBe(2); 199 | expect(substrings[0].length()).toBe(0); 200 | expect(substrings[1].length()).toBe(0); 201 | string = new HTMLString.String(quotes.Kernighan).trim(); 202 | substrings = string.split('a'); 203 | expect(substrings.length).toBe(11); 204 | substrings = string.split('@@'); 205 | expect(substrings.length).toBe(1); 206 | substrings = string.split(new HTMLString.String('e')); 207 | return expect(substrings.length).toBe(2); 208 | }); 209 | }); 210 | 211 | describe('HTMLString.String.startsWith()', function() { 212 | return it('should return true if a string starts with a substring.`', function() { 213 | var string, substring; 214 | string = new HTMLString.String(quotes.Turing); 215 | substring = new HTMLString.String('Machines take'); 216 | expect(string.startsWith("Machines take")).toBe(true); 217 | return expect(string.startsWith(substring)).toBe(true); 218 | }); 219 | }); 220 | 221 | describe('HTMLString.String.substr()', function() { 222 | return it('should return a subset of a string from an offset for a specified length`', function() { 223 | var newString, string; 224 | string = new HTMLString.String(quotes.Kernighan).trim(); 225 | newString = string.substr(10, 9); 226 | return expect(newString.html()).toBe('nows that'); 227 | }); 228 | }); 229 | 230 | describe('HTMLString.String.substring()', function() { 231 | return it('should return a subset of a string between 2 indexes`', function() { 232 | var newString, string; 233 | string = new HTMLString.String(quotes.Kernighan).trim(); 234 | newString = string.substring(10, 19); 235 | return expect(newString.html()).toBe('nows that'); 236 | }); 237 | }); 238 | 239 | describe('HTMLString.String.toLowerCase()', function() { 240 | return it('should return a copy of a string converted to lower case.`', function() { 241 | var newString, string; 242 | string = new HTMLString.String(quotes.Turing); 243 | newString = string.toLowerCase(); 244 | return expect(newString.html()).toBe('machines take me by
surprise with great frequency.
'); 245 | }); 246 | }); 247 | 248 | describe('HTMLString.String.toUpperCase()', function() { 249 | return it('should return a copy of a string converted to upper case.`', function() { 250 | var newString, string; 251 | string = new HTMLString.String(quotes.Turing); 252 | newString = string.toUpperCase(); 253 | return expect(newString.html()).toBe('MACHINES TAKE ME BY
SURPRISE WITH GREAT FREQUENCY.
'); 254 | }); 255 | }); 256 | 257 | describe('HTMLString.String.trim()', function() { 258 | return it('should return a copy of a string converted with whitespaces trimmed from both ends.`', function() { 259 | var newString, string; 260 | string = new HTMLString.String(quotes.Kernighan); 261 | newString = string.trim(); 262 | expect(newString.characters[0].isWhitespace()).toBe(false); 263 | return expect(newString.characters[newString.length() - 1].isWhitespace()).toBe(false); 264 | }); 265 | }); 266 | 267 | describe('HTMLString.String.trimLeft()', function() { 268 | return it('should return a copy of a string converted with whitespaces trimmed from the left.`', function() { 269 | var newString, string; 270 | string = new HTMLString.String(quotes.Kernighan); 271 | newString = string.trimLeft(); 272 | expect(newString.characters[0].isWhitespace()).toBe(false); 273 | return expect(newString.characters[newString.length() - 1].isWhitespace()).toBe(true); 274 | }); 275 | }); 276 | 277 | describe('HTMLString.String.trimRight)', function() { 278 | return it('should return a copy of a string converted with whitespaces trimmed from the left.`', function() { 279 | var newString, string; 280 | string = new HTMLString.String(quotes.Kernighan); 281 | newString = string.trimRight(); 282 | expect(newString.characters[0].isWhitespace()).toBe(true); 283 | return expect(newString.characters[newString.length() - 1].isWhitespace()).toBe(false); 284 | }); 285 | }); 286 | 287 | describe('HTMLString.String.unformat()', function() { 288 | return it('should return a sting unformatted (no tags)', function() { 289 | var clearAllString, clearEachString, string; 290 | string = new HTMLString.String(quotes.Kernighan).trim(); 291 | clearEachString = string.unformat(0, -1, new HTMLString.Tag('q'), new HTMLString.Tag('span', { 292 | "class": 'question' 293 | }), new HTMLString.Tag('b')); 294 | expect(clearEachString.html()).toBe(string.text()); 295 | clearEachString = string.unformat(0, -1, 'q', 'span', 'b'); 296 | expect(clearEachString.html()).toBe(string.text()); 297 | clearAllString = string.unformat(0, -1); 298 | return expect(clearAllString.html()).toBe(string.text()); 299 | }); 300 | }); 301 | 302 | }).call(this); 303 | -------------------------------------------------------------------------------- /spec/spec-helper.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 4 | }).call(this); 5 | -------------------------------------------------------------------------------- /src/characters.coffee: -------------------------------------------------------------------------------- 1 | class HTMLString.Character 2 | 3 | # A HTML character 4 | 5 | constructor: (c, tags) -> 6 | @_c = c 7 | 8 | # Entities are stored lower case 9 | if c.length > 1 10 | @_c = c.toLowerCase() 11 | 12 | # Add the tags 13 | @_tags = [] 14 | @addTags.apply(@, tags) 15 | 16 | # Read-only properties 17 | 18 | c: () -> 19 | # Return the native character string 20 | return @_c 21 | 22 | isEntity: () -> 23 | # Return true if the character is an entity 24 | return @_c.length > 1 25 | 26 | isTag: (tagName) -> 27 | # Return true if the character is a self-closing tag (e.g br, img), 28 | # optionally a tagName can be specified to match against. 29 | 30 | if @_tags.length == 0 or not @_tags[0].selfClosing() 31 | return false 32 | 33 | if tagName and @_tags[0].name() != tagName 34 | return false 35 | 36 | return true 37 | 38 | isWhitespace: () -> 39 | # Return true if the character represents a whitespace character 40 | return @_c in [' ', '\n', ' '] or @isTag('br') 41 | 42 | tags: () -> 43 | # Return the tags for this character 44 | return (t.copy() for t in @_tags) 45 | 46 | # Methods 47 | 48 | addTags: (tags...) -> 49 | # Add tag(s) to the character 50 | for tag in tags 51 | 52 | # HACK: Fix for IE edge (see issue: 53 | # https://github.com/GetmeUK/ContentTools/issues/258#issuecomment-228931486 54 | # 55 | # ~ Anthony Blackshaw , 28th June 2016 56 | if Array.isArray(tag) 57 | continue; 58 | 59 | # Any self closing tag has to be inserted as the first tag 60 | if tag.selfClosing() 61 | # You can't add a self closing tag to a character that is a tag 62 | if not @isTag() 63 | @_tags.unshift(tag.copy()) 64 | 65 | continue 66 | 67 | # Add the tag to the stack 68 | @_tags.push(tag.copy()) 69 | 70 | eq: (c) -> 71 | # Return true if the specified character is equal to this character 72 | 73 | # Check characters are the same 74 | if @c() != c.c() 75 | return false 76 | 77 | # Check the number of tags are the same 78 | if @_tags.length != c._tags.length 79 | return false 80 | 81 | # Check tags are the same 82 | tags = {} 83 | for tag in @_tags 84 | tags[tag.head()] = true 85 | 86 | for tag in c._tags 87 | if not tags[tag.head()] 88 | return false 89 | 90 | return true 91 | 92 | hasTags: (tags...) -> 93 | # Return true if the tags specified format this character 94 | tagNames = {} 95 | tagHeads = {} 96 | for tag in @_tags 97 | tagNames[tag.name()] = true 98 | tagHeads[tag.head()] = true 99 | 100 | # If a tag is supplied as a string we test if a tag with that name is 101 | # characters tags, if a tag instance is supplied then we check for 102 | # exact tag. 103 | for tag in tags 104 | if typeof tag == 'string' 105 | if tagNames[tag] == undefined 106 | return false 107 | else 108 | if tagHeads[tag.head()] == undefined 109 | return false 110 | 111 | return true 112 | 113 | removeTags: (tags...) -> 114 | # Remove tag(s) from the character 115 | 116 | # If no tags are provide we remove all tags 117 | if tags.length == 0 118 | @_tags = [] 119 | return 120 | 121 | names = {} 122 | heads = {} 123 | for tag in tags 124 | if typeof tag == 'string' 125 | names[tag] = tag 126 | else 127 | heads[tag.head()] = tag 128 | 129 | newTags = [] 130 | @_tags = @_tags.filter (tag) -> 131 | if not heads[tag.head()] and not names[tag.name()] 132 | return tag 133 | 134 | copy: () -> 135 | # Return a copy of the character 136 | return new HTMLString.Character(@_c, (t.copy() for t in @_tags)) -------------------------------------------------------------------------------- /src/namespace.coffee: -------------------------------------------------------------------------------- 1 | HTMLString = {} 2 | 3 | 4 | # Export the namespace 5 | 6 | # Browser (via window) 7 | if typeof window != 'undefined' 8 | window.HTMLString = HTMLString 9 | 10 | # Node/Browserify 11 | if typeof module != 'undefined' and module.exports 12 | exports = module.exports = HTMLString -------------------------------------------------------------------------------- /src/spec/html-string-spec.coffee: -------------------------------------------------------------------------------- 1 | quotes = { 2 | Gates: '''We all need people who will give us feedback. That's how we improve. ''' 3 | Kernighan: ''' 4 | Everyone knows that debugging is twice as hard as writing a program in 5 | the first place. So if you're as clever as you can be when you write 6 | it, how will you ever debug it? 7 | ''' 8 | Ritchie: ''' 9 | I can't recall any difficulty in making the C language definition completely 10 | open - any discussion on the matter tended to mention languages whose 11 | inventors tried to keep tight control, and consequent ill fate.''' 12 | Turing: ''' 13 | Machines take me by
surprise with great frequency.
14 | ''' 15 | Wozniak: ''' 16 | all the best people in life seem to like LINUX. 17 | ''' 18 | WozniakNamespaced: ''' 19 | all the best people in life seem to like LINUX. 20 | ''' 21 | WozniakWhitespace: ''' 22 | all the best people in life seem to like LINUX. 23 | ''' 24 | AmbiguousAmpersand: ''' 25 | & &amp &foo && && &end 26 | ''' 27 | } 28 | 29 | describe 'HTMLString.String()', () -> 30 | 31 | it 'should parse and render text (no HTML)', () -> 32 | string = new HTMLString.String(quotes.Wozniak) 33 | expect(string.text()).toBe quotes.Wozniak 34 | 35 | it 'should parse and render text (no HTML with whitespace preserved)', () -> 36 | string = new HTMLString.String(quotes.WozniakWhitespace, true) 37 | expect(string.text()).toBe quotes.WozniakWhitespace 38 | 39 | it 'should parse and render a string (HTML)', () -> 40 | string = new HTMLString.String(quotes.Turing) 41 | expect(string.html()).toBe quotes.Turing 42 | 43 | # Check names spaced tags are supported 44 | string = new HTMLString.String(quotes.WozniakNamespaced) 45 | expect(string.html()).toBe quotes.WozniakNamespaced 46 | 47 | it 'should parse and render a string (HTML with ambiguous ampersands)', () -> 48 | string = new HTMLString.String(quotes.AmbiguousAmpersand) 49 | expect(string.html()).toBe quotes.AmbiguousAmpersand 50 | 51 | console.log string.html() 52 | 53 | 54 | describe 'HTMLString.String.isWhitespace()', () -> 55 | 56 | it 'should return true if a string consists entirely of whitespace \ 57 | characters', () -> 58 | 59 | string = new HTMLString.String(" 
") 60 | expect(string.isWhitespace()).toBe true 61 | 62 | it 'should return false if a string contains any non-whitespace \ 63 | character', () -> 64 | 65 | string = new HTMLString.String("  a
") 66 | expect(string.isWhitespace()).toBe false 67 | 68 | 69 | describe 'HTMLString.String.length()', () -> 70 | 71 | it 'should return the length of a string', () -> 72 | string = new HTMLString.String(quotes.Turing) 73 | expect(string.length()).toBe 52 74 | 75 | describe 'HTMLString.String.preserveWhitespace()', () -> 76 | 77 | it 'should return the true if whitespace is reserved for the string', () -> 78 | 79 | # Not preserved 80 | string = new HTMLString.String(quotes.Turing) 81 | expect(string.preserveWhitespace()).toBe false 82 | 83 | # Preserved 84 | string = new HTMLString.String(quotes.Turing, true) 85 | expect(string.preserveWhitespace()).toBe true 86 | 87 | 88 | describe 'HTMLString.String.capitalize()', () -> 89 | 90 | it 'should capitalize the first character of a string', () -> 91 | string = new HTMLString.String(quotes.Wozniak) 92 | newString = string.capitalize() 93 | expect(newString.charAt(0).c()).toBe 'A' 94 | 95 | 96 | describe 'HTMLString.String.charAt()', () -> 97 | 98 | it 'should return a character from a string at the specified index', () -> 99 | string = new HTMLString.String(quotes.Turing) 100 | expect(string.charAt(18).c()).toBe 'y' 101 | 102 | 103 | describe 'HTMLString.String.concat()', () -> 104 | 105 | it 'should combine 2 or more strings and return a new string', () -> 106 | stringA = new HTMLString.String(quotes.Turing) 107 | stringB = new HTMLString.String(quotes.Wozniak) 108 | newString = stringA.concat(stringB) 109 | 110 | expect(newString.html()).toBe '''Machines take me by
surprise with great frequency.all the best people in life seem to like LINUX.
''' 111 | 112 | describe 'HTMLString.String.contain()', () -> 113 | 114 | it 'should return true if a string contains a substring', () -> 115 | string = new HTMLString.String(quotes.Turing) 116 | substring = new HTMLString.String('take me') 117 | 118 | expect(string.contains('take me')).toBe true 119 | expect(string.contains(substring)).toBe true 120 | 121 | 122 | describe 'HTMLString.String.endswith()', () -> 123 | 124 | it 'should return true if a string ends with a substring.`', () -> 125 | string = new HTMLString.String(quotes.Turing) 126 | substring = new HTMLString.String( 127 | 'great frequency.' 128 | ) 129 | 130 | expect( 131 | string.endsWith( 132 | "great#{ HTMLString.String.decode(' ') }frequency." 133 | ) 134 | ).toBe true 135 | expect(string.endsWith(substring)).toBe true 136 | 137 | 138 | describe 'HTMLString.String.format()', () -> 139 | 140 | it 'should format a selection of characters in a string (add tags)', () -> 141 | string = new HTMLString.String(quotes.Ritchie) 142 | string = string.format(0, -1, new HTMLString.Tag('q')) 143 | string = string.format( 144 | 19, 145 | 29, 146 | new HTMLString.Tag('a', {href: 'http://www.getme.co.uk'}), 147 | new HTMLString.Tag('b') 148 | ) 149 | 150 | expect(string.html()).toBe """ 151 | I can't recall any difficulty in making the C language definition completely open - any discussion on the matter tended to mention languages whose inventors tried to keep tight control, and consequent ill fate. 152 | """.trim() 153 | 154 | 155 | describe 'HTMLString.String.hasTags()', () -> 156 | 157 | it 'should return true if the string has and characters formatted with the \ 158 | specifed tags', () -> 159 | 160 | string = new HTMLString.String(quotes.Kernighan).trim() 161 | 162 | expect( 163 | string.hasTags( 164 | new HTMLString.Tag('q'), 165 | 'b' 166 | ) 167 | ).toBe true 168 | expect(string.hasTags(new HTMLString.Tag('q')), true).toBe true 169 | 170 | 171 | describe 'HTMLString.String.indexOf()', () -> 172 | 173 | it 'should return the first position of an substring in another string, \ 174 | or -1 if there is no match', () -> 175 | 176 | string = new HTMLString.String(quotes.Kernighan).trim() 177 | substring = new HTMLString.String('debugging') 178 | 179 | expect(string.indexOf('debugging')).toBe 20 180 | expect(string.indexOf(substring)).toBe 20 181 | expect(string.indexOf(substring, 30)).toBe -1 182 | 183 | 184 | describe 'HTMLString.String.insert()', () -> 185 | 186 | it 'should insert a string into another string and return a new \ 187 | string', () -> 188 | 189 | stringA = new HTMLString.String(quotes.Kernighan).trim() 190 | stringB = new HTMLString.String(quotes.Turing) 191 | 192 | # Inherit formatting 193 | newString = stringA.insert(9, stringB) 194 | newString = newString.insert(9 + stringB.length(), ' - new string inserted - ') 195 | expect(newString.html()).toBe '''Everyone Machines take me by
surprise with great frequency. - new string inserted -
knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?
''' 196 | 197 | # Do not inherit formatting 198 | newString = stringA.insert(9, ' - insert unformatted string - ', false) 199 | expect(newString.html()).toBe '''Everyone - insert unformatted string - knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?''' 200 | 201 | 202 | describe 'HTMLString.String.lastIndexOf()', () -> 203 | 204 | it 'should return the last position of an substring in another string, \ 205 | or -1 if there is no match', () -> 206 | 207 | string = new HTMLString.String(quotes.Kernighan).trim() 208 | substring = new HTMLString.String('debugging') 209 | 210 | expect(string.lastIndexOf('debugging')).toBe 142 211 | expect(string.lastIndexOf(substring)).toBe 142 212 | expect(string.lastIndexOf(substring, 30)).toBe -1 213 | 214 | 215 | describe 'HTMLString.String.optimize()', () -> 216 | 217 | it 'should optimize the string (in place) so that tags are restacked in \ 218 | order of longest running', () -> 219 | 220 | string = new HTMLString.String(quotes.Gates) 221 | string.optimize() 222 | 223 | expect(string.html()).toBe '''We all need people who will give us feedback. That's how we improve. ''' 224 | 225 | 226 | describe 'HTMLString.String.slice()', () -> 227 | 228 | it 'should extract a section of a string and return a new string', () -> 229 | string = new HTMLString.String(quotes.Kernighan).trim() 230 | newString = string.slice(10, 19) 231 | 232 | expect(newString.html()).toBe '''nows that''' 233 | 234 | 235 | describe 'HTMLString.String.split()', () -> 236 | 237 | it 'should split a string by the separator and return a list of \ 238 | sub-strings', () -> 239 | string = new HTMLString.String(' ', true) 240 | substrings = string.split(' ') 241 | expect(substrings.length).toBe 2 242 | expect(substrings[0].length()).toBe 0 243 | expect(substrings[1].length()).toBe 0 244 | 245 | string = new HTMLString.String(quotes.Kernighan).trim() 246 | 247 | substrings = string.split('a') 248 | expect(substrings.length).toBe 11 249 | 250 | substrings = string.split('@@') 251 | expect(substrings.length).toBe 1 252 | 253 | substrings = string.split( 254 | new HTMLString.String( 255 | 'e' 256 | ) 257 | ) 258 | expect(substrings.length).toBe 2 259 | 260 | 261 | describe 'HTMLString.String.startsWith()', () -> 262 | 263 | it 'should return true if a string starts with a substring.`', () -> 264 | string = new HTMLString.String(quotes.Turing) 265 | substring = new HTMLString.String( 266 | 'Machines take' 267 | ) 268 | 269 | expect(string.startsWith("Machines take")).toBe true 270 | expect(string.startsWith(substring)).toBe true 271 | 272 | 273 | describe 'HTMLString.String.substr()', () -> 274 | 275 | it 'should return a subset of a string from an offset for a specified \ 276 | length`', () -> 277 | 278 | string = new HTMLString.String(quotes.Kernighan).trim() 279 | newString = string.substr(10, 9) 280 | 281 | expect(newString.html()).toBe '''nows that''' 282 | 283 | 284 | describe 'HTMLString.String.substring()', () -> 285 | 286 | it 'should return a subset of a string between 2 indexes`', () -> 287 | string = new HTMLString.String(quotes.Kernighan).trim() 288 | newString = string.substring(10, 19) 289 | 290 | expect(newString.html()).toBe '''nows that''' 291 | 292 | 293 | describe 'HTMLString.String.toLowerCase()', () -> 294 | 295 | it 'should return a copy of a string converted to lower case.`', () -> 296 | string = new HTMLString.String(quotes.Turing) 297 | newString = string.toLowerCase() 298 | 299 | expect(newString.html()).toBe '''machines take me by
surprise with great frequency.
''' 300 | 301 | 302 | describe 'HTMLString.String.toUpperCase()', () -> 303 | 304 | it 'should return a copy of a string converted to upper case.`', () -> 305 | string = new HTMLString.String(quotes.Turing) 306 | newString = string.toUpperCase() 307 | 308 | expect(newString.html()).toBe '''MACHINES TAKE ME BY
SURPRISE WITH GREAT FREQUENCY.
''' 309 | 310 | 311 | describe 'HTMLString.String.trim()', () -> 312 | 313 | it 'should return a copy of a string converted with whitespaces trimmed \ 314 | from both ends.`', () -> 315 | 316 | string = new HTMLString.String(quotes.Kernighan) 317 | newString = string.trim() 318 | 319 | expect(newString.characters[0].isWhitespace()).toBe false 320 | expect( 321 | newString.characters[newString.length() - 1].isWhitespace() 322 | ).toBe false 323 | 324 | 325 | describe 'HTMLString.String.trimLeft()', () -> 326 | 327 | it 'should return a copy of a string converted with whitespaces trimmed \ 328 | from the left.`', () -> 329 | 330 | string = new HTMLString.String(quotes.Kernighan) 331 | newString = string.trimLeft() 332 | 333 | expect(newString.characters[0].isWhitespace()).toBe false 334 | expect( 335 | newString.characters[newString.length() - 1].isWhitespace() 336 | ).toBe true 337 | 338 | 339 | describe 'HTMLString.String.trimRight)', () -> 340 | 341 | it 'should return a copy of a string converted with whitespaces trimmed \ 342 | from the left.`', () -> 343 | 344 | string = new HTMLString.String(quotes.Kernighan) 345 | newString = string.trimRight() 346 | 347 | expect(newString.characters[0].isWhitespace()).toBe true 348 | expect( 349 | newString.characters[newString.length() - 1].isWhitespace() 350 | ).toBe false 351 | 352 | 353 | describe 'HTMLString.String.unformat()', () -> 354 | 355 | it 'should return a sting unformatted (no tags)', () -> 356 | string = new HTMLString.String(quotes.Kernighan).trim() 357 | 358 | # Test clearing tags individually 359 | clearEachString = string.unformat( 360 | 0, 361 | -1, 362 | new HTMLString.Tag('q'), 363 | new HTMLString.Tag('span', {class: 'question'}), 364 | new HTMLString.Tag('b') 365 | ) 366 | expect(clearEachString.html()).toBe string.text() 367 | 368 | # Test clearing tags by name 369 | clearEachString = string.unformat(0, -1, 'q', 'span', 'b') 370 | expect(clearEachString.html()).toBe string.text() 371 | 372 | # Test clearing all tags in one go 373 | clearAllString = string.unformat(0, -1) 374 | expect(clearAllString.html()).toBe string.text() -------------------------------------------------------------------------------- /src/spec/spec-helper.coffee: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetmeUK/HTMLString/1a41c2da17fa96331227883b45f68e696edcf8b9/src/spec/spec-helper.coffee -------------------------------------------------------------------------------- /src/strings.coffee: -------------------------------------------------------------------------------- 1 | class HTMLString.String 2 | 3 | # A string of HTML 4 | 5 | @_parser = null 6 | 7 | constructor: (html, preserveWhitespace=false) -> 8 | @_preserveWhitespace = preserveWhitespace 9 | 10 | if html 11 | # For performance we only initialize the parser once and only the 12 | # first time a HTMLString instances is initialized with html. 13 | if HTMLString.String._parser is null 14 | HTMLString.String._parser = new _Parser() 15 | 16 | @characters = HTMLString.String._parser.parse( 17 | html, 18 | @_preserveWhitespace 19 | ).characters 20 | else 21 | @characters = [] 22 | 23 | # Read-only properties 24 | 25 | isWhitespace: () -> 26 | # Return true if the string consists entirely of whitespace characters 27 | for c in @characters 28 | if not c.isWhitespace() 29 | return false 30 | return true 31 | 32 | length: () -> 33 | # Return the length of the string 34 | return @characters.length 35 | 36 | preserveWhitespace: () -> 37 | # Return true if the string is flagged to preserve whitespace 38 | return @_preserveWhitespace 39 | 40 | # Methods 41 | 42 | capitalize: () -> 43 | # Return a copy of the string with the first letter capitalized 44 | newString = @copy() 45 | if newString.length() 46 | c = newString.characters[0]._c.toUpperCase() 47 | newString.characters[0]._c = c 48 | return newString 49 | 50 | charAt: (index) -> 51 | # Return a single character from the string at the specified index 52 | return @characters[index].copy() 53 | 54 | concat: (strings..., inheritFormat) -> 55 | # Combine 2 or more strings and returns a new string. Optionally you 56 | # can specify whether the strings each inherit the previous strings 57 | # format (default true). 58 | 59 | # Check if the last argument was a string or the `inheritFormat` flag 60 | if not (typeof inheritFormat == 'undefined' or 61 | typeof inheritFormat == 'boolean') 62 | 63 | strings.push(inheritFormat) 64 | inheritFormat = true 65 | 66 | # Concat the strings 67 | newString = @copy() 68 | for string in strings 69 | 70 | # Skip empty strings 71 | if string.length == 0 72 | continue 73 | 74 | # If the string is supplied as text then convert it to an unformatted 75 | # string. 76 | tail = string 77 | if typeof string == 'string' 78 | tail = new HTMLString.String(string, @_preserveWhitespace) 79 | 80 | # Inherit the format of the existing string 81 | if inheritFormat and newString.length() 82 | indexChar = newString.charAt(newString.length() - 1) 83 | inheritedTags = indexChar.tags() 84 | 85 | # Don't inherit self-closing tags 86 | if indexChar.isTag() 87 | inheritedTags.shift() 88 | 89 | if typeof string != 'string' 90 | tail = tail.copy() 91 | 92 | for c in tail.characters 93 | c.addTags.apply(c, inheritedTags) 94 | 95 | # Build the new string 96 | for c in tail.characters 97 | newString.characters.push(c) 98 | 99 | return newString 100 | 101 | contains: (substring) -> 102 | # Return true if the string contains the specified sub-string 103 | 104 | # Compare to text 105 | if typeof substring == 'string' 106 | return @text().indexOf(substring) > -1 107 | 108 | # Compare to html string 109 | from = 0 110 | while from <= (@length() - substring.length()) 111 | found = true 112 | for c, i in substring.characters 113 | if not c.eq(@characters[i + from]) 114 | found = false 115 | break 116 | if found 117 | return true 118 | from++ 119 | 120 | return false 121 | 122 | endsWith: (substring) -> 123 | # Return true if the string ends with the specified sub-string 124 | 125 | # Compare to text 126 | if typeof substring == 'string' 127 | return (substring == '' or 128 | @text().slice(-substring.length) == substring) 129 | 130 | # Compare to html string 131 | characters = @characters.slice().reverse() 132 | for c, i in substring.characters.slice().reverse() 133 | if not c.eq(characters[i]) 134 | return false 135 | 136 | return true 137 | 138 | format: (from, to, tags...) -> 139 | # Apply the specified tags to a range (from, to) of characters in the 140 | # string. 141 | 142 | # Support for negative indexes on from/to 143 | if to < 0 144 | to = @length() + to + 1 145 | 146 | if from < 0 147 | from = @length() + from 148 | 149 | newString = @copy() 150 | for i in [from...to] 151 | c = newString.characters[i] 152 | c.addTags.apply(c, tags) 153 | 154 | return newString 155 | 156 | hasTags: (tags..., strict) -> 157 | # Return true if the specified tags are applied to some or all 158 | # (strict=true) characters within the string (default false). 159 | 160 | # Check if the last argument was a tag or the `strict` flag 161 | if not (typeof strict == 'undefined' or 162 | typeof strict == 'boolean') 163 | tags.push(strict) 164 | strict = false 165 | 166 | found = false 167 | for c in @characters 168 | if c.hasTags.apply(c, tags) 169 | found = true 170 | else 171 | if strict 172 | return false 173 | 174 | return found 175 | 176 | html: () -> 177 | # Return a HTML version of the string 178 | 179 | html = '' 180 | openTags = [] 181 | openHeads = [] 182 | closingTags = [] 183 | 184 | for c in @characters 185 | 186 | # Close tags 187 | closingTags = [] 188 | for openTag in openTags.slice().reverse() 189 | closingTags.push(openTag) 190 | if not c.hasTags(openTag) 191 | for closingTag in closingTags 192 | html += closingTag.tail() 193 | openTags.pop() 194 | openHeads.pop() 195 | closingTags = [] 196 | 197 | # Open tags 198 | for tag in c._tags 199 | if openHeads.indexOf(tag.head()) == -1 200 | if not tag.selfClosing() 201 | head = tag.head() 202 | html += head 203 | openTags.push(tag) 204 | openHeads.push(head) 205 | 206 | # If the character is a self-closing tag add it to the HTML after 207 | # all other tags. 208 | if c._tags.length > 0 and c._tags[0].selfClosing() 209 | html += c._tags[0].head() 210 | 211 | html += c.c() 212 | 213 | for tag in openTags.reverse() 214 | html += tag.tail() 215 | 216 | return html 217 | 218 | indexOf: (substring, from=0) -> 219 | # Return the index of the first occurrence of the specified sub-string, 220 | # -1 is returned if no match is found. 221 | 222 | if from < 0 223 | from = 0 224 | 225 | # Find text 226 | if typeof substring == 'string' 227 | return @text().indexOf(substring, from) 228 | 229 | # Find html string 230 | while from <= (@length() - substring.length()) 231 | found = true 232 | for c, i in substring.characters 233 | if not c.eq(@characters[i + from]) 234 | found = false 235 | break 236 | if found 237 | return from 238 | from++ 239 | 240 | return -1 241 | 242 | insert: (index, substring, inheritFormat=true) -> 243 | # Insert the specified sub-string at the specified index 244 | 245 | head = @slice(0, index) 246 | tail = @slice(index) 247 | 248 | if index < 0 249 | index = @length() + index 250 | 251 | # If the string is supplied as text then convert it to an unformatted 252 | # string. 253 | middle = substring 254 | if typeof substring == 'string' 255 | middle = new HTMLString.String(substring, @_preserveWhitespace) 256 | 257 | # Inherit the format of the existing string 258 | if inheritFormat and index > 0 259 | indexChar = @charAt(index - 1) 260 | inheritedTags = indexChar.tags() 261 | 262 | # Don't inherit self-closing tags 263 | if indexChar.isTag() 264 | inheritedTags.shift() 265 | 266 | if typeof substring != 'string' 267 | middle = middle.copy() 268 | 269 | for c in middle.characters 270 | c.addTags.apply(c, inheritedTags) 271 | 272 | # Build the new string 273 | newString = head 274 | for c in middle.characters 275 | newString.characters.push(c) 276 | for c in tail.characters 277 | newString.characters.push(c) 278 | return newString 279 | 280 | lastIndexOf: (substring, from=0) -> 281 | # Return the index of the last occurrence of the specified sub-string, 282 | # -1 is returned if no match is found. 283 | 284 | if from < 0 285 | from = 0 286 | 287 | characters = @characters.slice(from).reverse() 288 | 289 | # The from offset is applied by the slice so we reset it to 0 290 | from = 0 291 | 292 | # Find text 293 | if typeof substring == 'string' 294 | 295 | # Check the this string contains the specified string before 296 | # perform a full search. 297 | if not @contains(substring) 298 | return -1 299 | 300 | # Find text 301 | substring = substring.split('').reverse() 302 | while from <= (characters.length - substring.length) 303 | found = true 304 | skip = 0 305 | for c, i in substring 306 | if characters[i + from].isTag() 307 | skip += 1 308 | if c != characters[skip + i + from].c() 309 | found = false 310 | break 311 | if found 312 | return from 313 | from++ 314 | 315 | return -1 316 | 317 | # Find html string 318 | substring = substring.characters.slice().reverse() 319 | while from <= (characters.length - substring.length) 320 | found = true 321 | for c, i in substring 322 | if not c.eq(characters[i + from]) 323 | found = false 324 | break 325 | if found 326 | return from 327 | from++ 328 | 329 | return -1 330 | 331 | optimize: () -> 332 | # Optimize the string so that tags are stacked in order of run length 333 | openTags = [] 334 | openHeads = [] 335 | lastC = null # Last character 336 | 337 | for c in @characters.slice().reverse() 338 | c._runLengthMap = {} 339 | c._runLengthMapSize = 0 340 | 341 | # Close tags 342 | closingTags = [] 343 | for openTag in openTags.slice().reverse() 344 | closingTags.push(openTag) 345 | if not c.hasTags(openTag) 346 | for closingTag in closingTags 347 | openTags.pop() 348 | openHeads.pop() 349 | closingTags = [] 350 | 351 | # Open tags 352 | for tag in c._tags 353 | if openHeads.indexOf(tag.head()) == -1 354 | unless tag.selfClosing() 355 | openTags.push(tag) 356 | openHeads.push(tag.head()) 357 | 358 | # Calculate the run length of each tag 359 | for tag in openTags 360 | head = tag.head() 361 | 362 | # If this is the first character set the run length to 1 and 363 | # continue. 364 | if not lastC 365 | c._runLengthMap[head] = [tag, 1] 366 | continue 367 | 368 | # If there isn't one already add an entry for the tag against 369 | # the character. 370 | if not c._runLengthMap[head] 371 | c._runLengthMap[head] = [tag, 0] 372 | 373 | # Check to see if the last character also had this tag applied 374 | # and if so use the run length as a basis. 375 | run_length = 0 376 | if lastC._runLengthMap[head] 377 | run_length = lastC._runLengthMap[head][1] 378 | 379 | # Increment the run length for this character and tag 380 | c._runLengthMap[head][1] = run_length + 1 381 | 382 | lastC = c 383 | 384 | # Order the tags for each character based on their run length 385 | runLengthSort = (a, b) -> 386 | return b[1] - a[1] 387 | 388 | for c in @characters 389 | # Check for characters where there's only a single tag applied in 390 | # which case there's no need to apply a re-order. 391 | len = c._tags.length 392 | if (len > 0 and c._tags[0].selfClosing() and len < 3) or len < 2 393 | continue 394 | 395 | # Build a list of tags and sort them in order or run length 396 | runLengths = [] 397 | for tag, runLength of c._runLengthMap 398 | runLengths.push(runLength) 399 | runLengths.sort(runLengthSort) 400 | 401 | # Re-add the characters tags in run length order 402 | for tag in c._tags.slice() 403 | unless tag.selfClosing() 404 | c.removeTags(tag) 405 | c.addTags.apply(c, (t[0] for t in runLengths)) 406 | 407 | slice: (from, to) -> 408 | # Extract a section of the string and return a new string 409 | newString = new HTMLString.String('', @_preserveWhitespace) 410 | newString.characters = (c.copy() for c in @characters.slice(from, to)) 411 | return newString 412 | 413 | split: (separator='', limit=0) -> 414 | # Split the string by the separator and return a list of sub-strings 415 | 416 | # Build a list of indexes for the separator in the string 417 | lastIndex = 0 418 | count = 0 419 | indexes = [0] 420 | loop 421 | if limit > 0 and count > limit 422 | break 423 | index = @indexOf(separator, lastIndex) 424 | if index == -1 425 | break 426 | indexes.push(index) 427 | lastIndex = index + 1 428 | 429 | indexes.push(@length()) 430 | 431 | # Build a list of sub-strings based on the split indexes 432 | substrings = [] 433 | for i in [0..(indexes.length - 2)] 434 | start = indexes[i] 435 | if i > 0 436 | start += 1 437 | end = indexes[i + 1] 438 | substrings.push(@slice(start, end)) 439 | 440 | return substrings 441 | 442 | startsWith: (substring) -> 443 | # Return true if the string starts with the specified substring 444 | 445 | # Compare to text 446 | if typeof substring == 'string' 447 | return @text().slice(0, substring.length) == substring 448 | 449 | # Compare to html string 450 | for c, i in substring.characters 451 | if not c.eq(@characters[i]) 452 | return false 453 | 454 | return true 455 | 456 | substr: (from, length) -> 457 | # Return a subset of a string between from and length, if length isn't 458 | # specified it will default to the end of the string. 459 | 460 | # Check for zero or negative length selections 461 | if length <= 0 462 | return new HTMLString.String('', @_preserveWhitespace) 463 | 464 | if from < 0 465 | from = @length() + from 466 | 467 | if length == undefined 468 | length = @length() - from 469 | 470 | return @slice(from, from + length) 471 | 472 | substring: (from, to) -> 473 | # Return a subset of a string between from and to, if to isn't 474 | # specified it will default to the end of the string. 475 | if to == undefined 476 | to = @length() 477 | 478 | return @slice(from, to) 479 | 480 | text: () -> 481 | # Return a text version of the string 482 | text = '' 483 | for c in @characters 484 | 485 | # Handle tag characters 486 | if c.isTag() 487 | 488 | # Handle line breaks 489 | if c.isTag('br') 490 | text += '\n' 491 | continue 492 | 493 | # Prevent multiple spaces (other than  ) 494 | if c.c() == ' ' 495 | text += c.c() 496 | continue 497 | 498 | text += c.c() 499 | 500 | return @constructor.decode(text) 501 | 502 | toLowerCase: () -> 503 | # Return a copy of the string converted to lower case 504 | newString = @copy() 505 | for c in newString.characters 506 | if c._c.length == 1 507 | c._c = c._c.toLowerCase() 508 | return newString 509 | 510 | toUpperCase: () -> 511 | # Return a copy of the string converted to upper case 512 | newString = @copy() 513 | for c in newString.characters 514 | if c._c.length == 1 515 | c._c = c._c.toUpperCase() 516 | return newString 517 | 518 | trim: () -> 519 | # Return a copy of the string with whitespace trimmed from either end 520 | 521 | # Find the first non-whitespace character 522 | for c, from in @characters 523 | if not c.isWhitespace() 524 | break 525 | 526 | # Find the last non-whitespace character 527 | for c, to in @characters.slice().reverse() 528 | if not c.isWhitespace() 529 | break 530 | 531 | to = @length() - to - 1 532 | 533 | newString = new HTMLString.String('', @_preserveWhitespace) 534 | newString.characters = (c.copy() for c in @characters[from..to]) 535 | 536 | return newString 537 | 538 | trimLeft: () -> 539 | # Return a copy of the string with whitespaces trimmed from the left 540 | to = @length() - 1 541 | 542 | # Find the first non-whitespace character 543 | for c, from in @characters 544 | if not c.isWhitespace() 545 | break 546 | 547 | newString = new HTMLString.String('', @_preserveWhitespace) 548 | newString.characters = (c.copy() for c in @characters[from..to]) 549 | 550 | return newString 551 | 552 | trimRight: () -> 553 | # Return a copy of the string with whitespaces trimmed from the right 554 | from = 0 555 | 556 | # Find the last non-whitespace character 557 | for c, to in @characters.slice().reverse() 558 | if not c.isWhitespace() 559 | break 560 | 561 | to = @length() - to - 1 562 | 563 | newString = new HTMLString.String('', @_preserveWhitespace) 564 | newString.characters = (c.copy() for c in @characters[from..to]) 565 | 566 | return newString 567 | 568 | unformat: (from, to, tags...) -> 569 | # Remove the specified tags from a range (from, to) of characters in 570 | # the string. Specifying no tags will clear all formatting form the 571 | # selection. 572 | 573 | # Support for negative indexes on from/to 574 | if to < 0 575 | to = @length() + to + 1 576 | 577 | if from < 0 578 | from = @length() + from 579 | 580 | newString = @copy() 581 | for i in [from...to] 582 | c = newString.characters[i] 583 | c.removeTags.apply(c, tags) 584 | 585 | return newString 586 | 587 | copy: () -> 588 | # Return a copy of the string 589 | stringCopy = new HTMLString.String('', @_preserveWhitespace) 590 | stringCopy.characters = (c.copy() for c in @characters) 591 | return stringCopy 592 | 593 | # Class methods 594 | 595 | @decode: (string) -> 596 | # Decode entities within the specified string 597 | textarea = document.createElement('textarea') 598 | textarea.innerHTML = string 599 | return textarea.textContent 600 | 601 | @encode: (string) -> 602 | # Encode entities within the specified string 603 | textarea = document.createElement('textarea') 604 | textarea.textContent = string 605 | return textarea.innerHTML 606 | 607 | @join: (separator, strings) -> 608 | # Join a list of strings together 609 | joined = strings.shift() 610 | for s in strings 611 | joined = joined.concat(separator, s) 612 | return joined 613 | 614 | 615 | # Constants 616 | 617 | # Define character sets 618 | ALPHA_CHARS = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz-_$'.split('') 619 | ALPHA_NUMERIC_CHARS = ALPHA_CHARS.concat('1234567890'.split('')) 620 | ATTR_NAME_CHARS = ALPHA_NUMERIC_CHARS.concat([':']) 621 | ENTITY_CHARS = ALPHA_NUMERIC_CHARS.concat(['#']) 622 | TAG_NAME_CHARS = ALPHA_NUMERIC_CHARS.concat([':']) 623 | 624 | # Define the parser states 625 | CHAR_OR_ENTITY_OR_TAG = 1 626 | ENTITY = 2 627 | OPENNING_OR_CLOSING_TAG = 3 628 | OPENING_TAG = 4 629 | CLOSING_TAG = 5 630 | TAG_NAME_OPENING = 6 631 | TAG_NAME_CLOSING = 7 632 | TAG_OPENING_SELF_CLOSING = 8 633 | TAG_NAME_MUST_CLOSE = 9 634 | ATTR_OR_TAG_END = 10 635 | ATTR_NAME = 11 636 | ATTR_NAME_FIND_VALUE = 12 637 | ATTR_DELIM = 13 638 | ATTR_VALUE_SINGLE_DELIM = 14 639 | ATTR_VALUE_DOUBLE_DELIM = 15 640 | ATTR_VALUE_NO_DELIM = 16 641 | ATTR_ENTITY_NO_DELIM = 17 642 | ATTR_ENTITY_SINGLE_DELIM = 18 643 | ATTR_ENTITY_DOUBLE_DELIM = 19 644 | 645 | 646 | class _Parser 647 | 648 | # A HTML parser for creating HTML strings 649 | 650 | constructor: () -> 651 | 652 | # Build the parser FSM 653 | @fsm = new FSM.Machine(@) 654 | @fsm.setInitialState(CHAR_OR_ENTITY_OR_TAG) 655 | 656 | # Character or tag 657 | @fsm.addTransitionAny CHAR_OR_ENTITY_OR_TAG, null, (c) -> 658 | @_pushChar(c) 659 | 660 | @fsm.addTransition '<', CHAR_OR_ENTITY_OR_TAG, OPENNING_OR_CLOSING_TAG 661 | @fsm.addTransition '&', CHAR_OR_ENTITY_OR_TAG, ENTITY 662 | @fsm.addTransition 'END', CHAR_OR_ENTITY_OR_TAG, null 663 | 664 | # Entity 665 | @fsm.addTransitions ENTITY_CHARS, ENTITY, null, (c) -> 666 | @entity += c 667 | 668 | @fsm.addTransition ';', ENTITY, CHAR_OR_ENTITY_OR_TAG, () -> 669 | @_pushChar("&#{ @entity };") 670 | @entity = '' 671 | 672 | @fsm.addTransitionAny ENTITY, CHAR_OR_ENTITY_OR_TAG, (c) -> 673 | @_pushChar('&') 674 | for c in @entity.split('') 675 | @_pushChar(c) 676 | @entity = '' 677 | @_back() 678 | 679 | @fsm.addTransition 'END', ENTITY, null, () -> 680 | @_pushChar('&') 681 | for c in @entity.split('') 682 | @_pushChar(c) 683 | @entity = '' 684 | 685 | # Opening or closing Tag 686 | @fsm.addTransitions [' ', '\n'], OPENNING_OR_CLOSING_TAG 687 | @fsm.addTransitions ALPHA_CHARS, OPENNING_OR_CLOSING_TAG, OPENING_TAG, () -> 688 | @_back() 689 | 690 | @fsm.addTransition '/', OPENNING_OR_CLOSING_TAG, CLOSING_TAG 691 | 692 | # Opening tag 693 | @fsm.addTransitions [' ', '\n'], OPENING_TAG 694 | @fsm.addTransitions ALPHA_CHARS, OPENING_TAG, TAG_NAME_OPENING, () -> 695 | @_back() 696 | 697 | # Closing tag 698 | @fsm.addTransitions [' ', '\n'], CLOSING_TAG 699 | @fsm.addTransitions ALPHA_CHARS, CLOSING_TAG, TAG_NAME_CLOSING, () -> 700 | @_back() 701 | 702 | # Tag name opening 703 | @fsm.addTransitions TAG_NAME_CHARS, TAG_NAME_OPENING, null, (c) -> 704 | @tagName += c 705 | 706 | @fsm.addTransitions [' ', '\n'], TAG_NAME_OPENING, ATTR_OR_TAG_END 707 | @fsm.addTransition '/', TAG_NAME_OPENING, TAG_OPENING_SELF_CLOSING, () -> 708 | @selfClosing = true 709 | 710 | @fsm.addTransition '>', TAG_NAME_OPENING, CHAR_OR_ENTITY_OR_TAG, () -> 711 | @_pushTag() 712 | 713 | @fsm.addTransitions [' ', '\n'], TAG_OPENING_SELF_CLOSING 714 | @fsm.addTransition '>', TAG_OPENING_SELF_CLOSING, CHAR_OR_ENTITY_OR_TAG, () -> 715 | @_pushTag() 716 | 717 | @fsm.addTransitions [' ', '\n'], ATTR_OR_TAG_END 718 | @fsm.addTransition '/', ATTR_OR_TAG_END, TAG_OPENING_SELF_CLOSING, () -> 719 | @selfClosing = true 720 | 721 | @fsm.addTransition '>', ATTR_OR_TAG_END, CHAR_OR_ENTITY_OR_TAG, () -> 722 | @_pushTag() 723 | 724 | @fsm.addTransitions ALPHA_CHARS, ATTR_OR_TAG_END, ATTR_NAME, () -> 725 | @_back() 726 | 727 | # Tag name closing 728 | @fsm.addTransitions TAG_NAME_CHARS, TAG_NAME_CLOSING, null, (c) -> 729 | @tagName += c 730 | 731 | @fsm.addTransitions [' ', '\n'], TAG_NAME_CLOSING, TAG_NAME_MUST_CLOSE 732 | @fsm.addTransition '>', TAG_NAME_CLOSING, CHAR_OR_ENTITY_OR_TAG, () -> 733 | @_popTag() 734 | 735 | @fsm.addTransitions [' ', '\n'], TAG_NAME_MUST_CLOSE 736 | @fsm.addTransition '>', TAG_NAME_MUST_CLOSE, CHAR_OR_ENTITY_OR_TAG, () -> 737 | @_popTag() 738 | 739 | # Attribute name 740 | @fsm.addTransitions ATTR_NAME_CHARS, ATTR_NAME, null, (c) -> 741 | @attributeName += c 742 | 743 | @fsm.addTransitions [' ', '\n'], ATTR_NAME, ATTR_NAME_FIND_VALUE 744 | @fsm.addTransition '=', ATTR_NAME, ATTR_DELIM 745 | @fsm.addTransitions [' ', '\n'], ATTR_NAME_FIND_VALUE 746 | @fsm.addTransition '=', ATTR_NAME_FIND_VALUE, ATTR_DELIM 747 | 748 | @fsm.addTransitions '>', ATTR_NAME, ATTR_OR_TAG_END, () -> 749 | @_pushAttribute() 750 | @_back() 751 | 752 | @fsm.addTransitionAny ATTR_NAME_FIND_VALUE, ATTR_OR_TAG_END, () -> 753 | @_pushAttribute() 754 | @_back() 755 | 756 | # Attribute delimiter 757 | @fsm.addTransitions [' ', '\n'], ATTR_DELIM 758 | @fsm.addTransition '\'', ATTR_DELIM, ATTR_VALUE_SINGLE_DELIM 759 | @fsm.addTransition '"', ATTR_DELIM, ATTR_VALUE_DOUBLE_DELIM 760 | 761 | # Fix for browsers (including IE) that output quoted attributes 762 | @fsm.addTransitions ALPHA_NUMERIC_CHARS.concat ['&'], ATTR_DELIM, ATTR_VALUE_NO_DELIM, () -> 763 | @_back() 764 | 765 | @fsm.addTransition ' ', ATTR_VALUE_NO_DELIM, ATTR_OR_TAG_END, () -> 766 | @_pushAttribute() 767 | 768 | @fsm.addTransitions ['/', '>'], ATTR_VALUE_NO_DELIM, ATTR_OR_TAG_END, () -> 769 | @_back() 770 | @_pushAttribute() 771 | 772 | @fsm.addTransition '&', ATTR_VALUE_NO_DELIM, ATTR_ENTITY_NO_DELIM 773 | @fsm.addTransitionAny ATTR_VALUE_NO_DELIM, null, (c) -> 774 | @attributeValue += c 775 | 776 | # Attribute value single delimiter 777 | @fsm.addTransition '\'', ATTR_VALUE_SINGLE_DELIM, ATTR_OR_TAG_END, () -> 778 | @_pushAttribute() 779 | 780 | @fsm.addTransition '&', ATTR_VALUE_SINGLE_DELIM, ATTR_ENTITY_SINGLE_DELIM 781 | @fsm.addTransitionAny ATTR_VALUE_SINGLE_DELIM, null, (c) -> 782 | @attributeValue += c 783 | 784 | # Attribte value double delimiter 785 | @fsm.addTransition '"', ATTR_VALUE_DOUBLE_DELIM, ATTR_OR_TAG_END, () -> 786 | @_pushAttribute() 787 | 788 | @fsm.addTransition '&', ATTR_VALUE_DOUBLE_DELIM, ATTR_ENTITY_DOUBLE_DELIM 789 | @fsm.addTransitionAny ATTR_VALUE_DOUBLE_DELIM, null, (c) -> 790 | @attributeValue += c 791 | 792 | # Entity in attribute value 793 | @fsm.addTransitions ENTITY_CHARS, ATTR_ENTITY_NO_DELIM, null, (c) -> 794 | @entity += c 795 | 796 | @fsm.addTransitions ENTITY_CHARS, ATTR_ENTITY_SINGLE_DELIM, null, (c) -> 797 | @entity += c 798 | 799 | @fsm.addTransitions ENTITY_CHARS, ATTR_ENTITY_DOUBLE_DELIM, null, (c) -> 800 | @entity += c 801 | 802 | @fsm.addTransition ';', ATTR_ENTITY_NO_DELIM, ATTR_VALUE_NO_DELIM, () -> 803 | @attributeValue += "&#{ @entity };" 804 | @entity = '' 805 | 806 | @fsm.addTransition ';', ATTR_ENTITY_SINGLE_DELIM, ATTR_VALUE_SINGLE_DELIM, () -> 807 | @attributeValue += "&#{ @entity };" 808 | @entity = '' 809 | 810 | @fsm.addTransition ';', ATTR_ENTITY_DOUBLE_DELIM, ATTR_VALUE_DOUBLE_DELIM, () -> 811 | @attributeValue += "&#{ @entity };" 812 | @entity = '' 813 | 814 | @fsm.addTransitionAny ATTR_ENTITY_NO_DELIM, ATTR_VALUE_NO_DELIM, (c) -> 815 | @attributeValue += '&' + @entity 816 | @entity = '' 817 | @_back() 818 | 819 | @fsm.addTransitionAny ATTR_ENTITY_SINGLE_DELIM, ATTR_VALUE_SINGLE_DELIM, (c) -> 820 | @attributeValue += '&' + @entity 821 | @entity = '' 822 | @_back() 823 | 824 | @fsm.addTransitionAny ATTR_ENTITY_DOUBLE_DELIM, ATTR_VALUE_DOUBLE_DELIM, (c) -> 825 | @attributeValue += '&' + @entity 826 | @entity = '' 827 | @_back() 828 | 829 | # Parsing methods 830 | 831 | _back: () -> 832 | # Move the parsing head back one characters 833 | @head-- 834 | 835 | _pushAttribute: () -> 836 | # Remember an attribute for the current tag 837 | 838 | @attributes[@attributeName] = @attributeValue 839 | 840 | # Reset the attribute 841 | @attributeName = '' 842 | @attributeValue = '' 843 | 844 | _pushChar: (c) -> 845 | # Push a character on to the string 846 | character = new HTMLString.Character(c, @tags) 847 | 848 | # Do we need to preserve whitespace? 849 | if @_preserveWhitespace 850 | @string.characters.push(character) 851 | return 852 | 853 | # If the character is whitespace only add it if the last character 854 | # isn't (e.g don't build strings containing extra unused spaces). 855 | if @string.length() and not character.isTag() and 856 | not character.isEntity() and character.isWhitespace() 857 | 858 | lastCharacter = @string.characters[@string.length() - 1] 859 | if lastCharacter.isWhitespace() and not lastCharacter.isTag() and 860 | not lastCharacter.isEntity() 861 | return 862 | 863 | @string.characters.push(character) 864 | 865 | _pushTag: () -> 866 | # Push a tag on to the stack applied to characters 867 | 868 | # Push the Tag on to the stack 869 | tag = new HTMLString.Tag(@tagName, @attributes) 870 | @tags.push(tag) 871 | 872 | # Adding an empty character for self closing tags 873 | if tag.selfClosing() 874 | @._pushChar('') 875 | @tags.pop() 876 | 877 | # Check if the tag was self closed and if not update the FSM to 878 | # close it. 879 | if not @selfClosed and @tagName in HTMLString.Tag.SELF_CLOSING 880 | @fsm.reset() 881 | 882 | # Reset the tag buffers 883 | @tagName = '' 884 | @selfClosed = false 885 | @attributes = {} 886 | 887 | _popTag: () -> 888 | # Pop a tag from the stack applied to characters 889 | 890 | # Balanced the tags 891 | loop 892 | tag = @tags.pop() 893 | 894 | # Push whitespace at the end of a tag out 895 | if @string.length() 896 | character = @string.characters[@string.length() - 1] 897 | if not character.isTag() and 898 | not character.isEntity() and 899 | character.isWhitespace() 900 | character.removeTags(tag) 901 | 902 | break if tag.name() == @tagName.toLowerCase() 903 | 904 | # Reset the tag buffers 905 | @tagName = '' 906 | 907 | # Methods 908 | 909 | parse: (html, preserveWhitespace) -> 910 | # Parse a HTML string 911 | @_preserveWhitespace = preserveWhitespace 912 | 913 | # Prepare for parsing 914 | @reset() 915 | html = @preprocess(html) 916 | @fsm.parser = @ 917 | 918 | # Parse the HTML 919 | while @head < html.length 920 | character = html[@head] 921 | try 922 | @fsm.process(character) 923 | catch error 924 | throw new Error("Error at char #{@head} >> #{error}") 925 | 926 | @head++ 927 | 928 | @fsm.process('END') 929 | 930 | return @string 931 | 932 | preprocess: (html) -> 933 | # Preprocess a HTML string to prepare it for parsing 934 | 935 | # Normalize line endings 936 | html = html.replace(/\r\n/g, '\n').replace(/\r/g, '\n') 937 | 938 | # Remove any comments 939 | html = html.replace(//g, '') 940 | 941 | # Replace multiple spaces with a single space 942 | if not @_preserveWhitespace 943 | html = html.replace(/\s+/g, ' ') 944 | 945 | return html 946 | 947 | reset: () -> 948 | # Reset the parser ready to parse a HTML string 949 | @fsm.reset() 950 | 951 | # The index of the character we're currently parsing 952 | @head = 0 953 | 954 | # Temporary properties to store the current parser state 955 | @string = new HTMLString.String() 956 | @entity = '' 957 | @tags = [] 958 | @tagName = '' 959 | @selfClosing = false 960 | @attributes = {} 961 | @attributeName = '' 962 | @attributeValue = '' -------------------------------------------------------------------------------- /src/tags.coffee: -------------------------------------------------------------------------------- 1 | class HTMLString.Tag 2 | 3 | # A HTML tag 4 | 5 | constructor: (name, attributes) -> 6 | @_name = name.toLowerCase() 7 | @_selfClosing = HTMLString.Tag.SELF_CLOSING[@_name] == true 8 | @_head = null 9 | 10 | # Copy the attributes 11 | @_attributes = {} 12 | for k, v of attributes 13 | @_attributes[k] = v 14 | 15 | # Constants 16 | 17 | # A list of tags that must self close 18 | @SELF_CLOSING = { 19 | 'area': true, 20 | 'base': true, 21 | 'br': true, 22 | 'hr': true, 23 | 'img': true, 24 | 'input': true, 25 | 'link meta': true, 26 | 'wbr': true 27 | } 28 | 29 | # Read only properties 30 | 31 | head: () -> 32 | # Return the head of the tag 33 | 34 | # For performance we cache the head part of the tag 35 | if not @_head 36 | components = [] 37 | 38 | for k, v of @_attributes 39 | if v 40 | components.push("#{ k }=\"#{ v }\"") 41 | else 42 | components.push("#{ k }") 43 | components.sort() 44 | 45 | components.unshift(@_name) 46 | 47 | @_head = "<#{ components.join(' ') }>" 48 | 49 | return @_head 50 | 51 | name: () -> 52 | # Return the tag's name 53 | return @_name 54 | 55 | selfClosing: () -> 56 | # Return true if the tag is self closing 57 | return @_selfClosing 58 | 59 | tail: () -> 60 | # Return the tail of the tag 61 | if @_selfClosing 62 | return '' 63 | return "" 64 | 65 | # Methods 66 | 67 | attr: (name, value) -> 68 | # Get/Set the value of an attribute 69 | 70 | if value == undefined 71 | return @_attributes[name] 72 | 73 | # Set the attribute 74 | @_attributes[name] = value 75 | 76 | # Clear the head cache 77 | @_head = null 78 | 79 | removeAttr: (name) -> 80 | # Remove an attribute from the tag 81 | 82 | if @_attributes[name] == undefined 83 | return 84 | 85 | # Remove the attribute 86 | delete @_attributes[name] 87 | 88 | # Clear the head cache 89 | @_head = null 90 | 91 | copy: () -> 92 | # Return a copy of the tag 93 | return new HTMLString.Tag(@_name, @_attributes) --------------------------------------------------------------------------------