├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json ├── src ├── clone.js ├── css-find.js ├── global-tags.js ├── html-file-to-dom.js ├── html-to-dom.js └── require-module.js └── test ├── annotations ├── button.html ├── index.html ├── item.html └── output.html ├── complex ├── icons │ ├── close.html │ ├── index.html │ └── save.html ├── index.html ├── layout │ ├── header.htm │ ├── index.htm │ └── list.htm └── output.html ├── extensions ├── component.xhtml ├── index.html └── output.html ├── find ├── index.html ├── item.html └── output.html ├── imports ├── .gitignore ├── globals.html ├── helpers │ ├── button.html │ ├── index.html │ └── input.html ├── index.html ├── layout.html └── node_modules │ ├── module1 │ ├── main.html │ └── package.json │ ├── module2 │ └── index.html │ └── module3.html ├── index.js └── merge ├── button.html ├── icons ├── close.html ├── index.html └── open.html ├── index.html ├── item.html ├── output.html └── text.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sergii Kliuchnyk 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | html-extend 2 | =========== 3 | 4 | [![Build Status](https://travis-ci.org/redexp/html-extend.svg?branch=master)](https://travis-ci.org/redexp/html-extend) 5 | 6 | ## Issue 7 | For example, you have some html file with `` tag with rich markup and you need it in another file but without some buttons, different classes and labels, or even worse, you will need to wrap some tag. You can solve it with dozens of parameters and if's but you markup will become unreadable. 8 | 9 | ## My solution 10 | Extend origin html file using es6 like module system and annotations. 11 | 12 | ## Install 13 | 14 | `npm install html-extend` 15 | 16 | ## Contents 17 | 18 | * [API](#api) 19 | * [render](#render) 20 | * [htmlFileToDom](#htmlfiletodom) 21 | * [htmlToDom](#htmltodom) 22 | * [domToHtml](#domtohtml) 23 | * [globalTags](#globaltags) 24 | * [setExtension](#setextension) 25 | * [Annotations](#annotations) 26 | * [export](#export) 27 | * [global](#global) 28 | * [import](#import) 29 | * Tags 30 | * [Path to tag](#path-to-tag) 31 | * [Add tag](#add-tag) 32 | * [Remove tag](#remove-tag) 33 | * [Rename tag](#rename-tag) 34 | * Attributes 35 | * [Add/rewrite attribute](#addrewrite-attribute) 36 | * [Remove attribute](#remove-attribute) 37 | * [Shadow attribute](#shadow-attribute) 38 | * [Add class](#add-class) 39 | * [Remove class](#remove-class) 40 | * Text 41 | * [Rewrite text in tag](#rewrite-text-in-tag) 42 | * [Remove text](#remove-text) 43 | * Annotations 44 | * [@find](#find) 45 | * [@append](#append) 46 | * [@prepend](#prepend) 47 | * [@remove](#remove) 48 | * [@empty](#empty) 49 | * [@appendTo](#appendto) 50 | * [@prependTo](#prependto) 51 | * [@insertBefore](#insertbefore) 52 | * [@insertAfter](#insertafter) 53 | * [Contribute](#contribute) 54 | 55 | 56 | ## API 57 | 58 | ```javascript 59 | /** 60 | * @param {String} filePath 61 | * @returns {HtmlString} 62 | */ 63 | render(filePath) 64 | ``` 65 | 66 | ```javascript 67 | /** 68 | * @param {String} filePath 69 | * @returns {HtmlModule} 70 | */ 71 | htmlFileToDom(filePath) 72 | ``` 73 | `{HtmlModule}` is dom object of [simple-html-dom-parser](https://github.com/redexp/simple-html-dom-parser) with `imports` and `exports` properties 74 | 75 | ```javascript 76 | /** 77 | * @param {String} html 78 | * @param {String} filePath - needed to resolve import path 79 | * @returns {HtmlModule} 80 | */ 81 | htmlToDom(html, filePath) 82 | ``` 83 | 84 | ```javascript 85 | /** 86 | * @param {DomObject} dom 87 | * @returns {String} 88 | */ 89 | domToHtml(dom) 90 | ``` 91 | 92 | ```javascript 93 | /** 94 | * @property {Object} 95 | */ 96 | require('html-extend').globalTags 97 | ``` 98 | It's hash where keys are names of global tags and values should be `DomObject` or html string or function (which will take `DomObject` and should return string or new `DomObject`). 99 | 100 | ```javascript 101 | /** 102 | * @param {Array|String} fileExtensions 103 | * @param {Function} handler 104 | */ 105 | function setExtension(fileExtensions, handler) 106 | ``` 107 | `handler` will take file path and should return hash with exported tag names, which values should be `DomObject` or html string or function (which will take `DomObject` and should return string or new `DomObject`). 108 | 109 | Example 110 | 111 | `component.xhtml` 112 | ```html 113 |
114 |

{label}

115 | 116 |
117 | ``` 118 | ```javascript 119 | var setExtension = require('html-extend').setExtension; 120 | 121 | setExtension('xhtml', function handler(file) { 122 | var html = fs.readFileSync(file).toString(); 123 | 124 | return { 125 | "default": function (tag) { 126 | var result = html; 127 | 128 | for (var name in tag.shadowAttr) { 129 | result = result.replace('{' + name + '}', tag.shadowAttr[name]); 130 | } 131 | 132 | return result; 133 | } 134 | }; 135 | }); 136 | ``` 137 | ```html 138 | import Component from './component' 139 | 140 |
141 | 142 |
143 | ``` 144 | ```html 145 |
146 |
147 |

Some Title

148 | 149 |
150 |
151 | ``` 152 | With `setExtension` you can even rewrite default `html` extension handler. 153 | 154 | To remove extension just set `null` 155 | ```javascript 156 | setExtension('xhtml', null); 157 | ``` 158 | 159 | 160 | ## Annotations 161 | Annotations is text or comment like `@annotationName` before tags which describes how tag should be modified. 162 | 163 | 164 | ## @export 165 | Annotation which used to export tags. The only option is the name of exported tag. It's same as in CommonJS when you write `exports.TagName` or in es6 `export TagName` will be `@export TagName`. Also as in es6 `export default` you can write `@export default` or just `@export` and this tag will be default for current module. You can export any tag from file, not just root tags. You can use dots and dashes in tag name. You can use as many export names as you wish. 166 | ```html 167 | @export default 168 | @export Layout 169 |
170 | @export ButtonXS 171 | 172 | 173 | 174 | 179 | 180 |
181 | ``` 182 | 183 | ## @global 184 | This annotation same as `@export` only it will export to global scope 185 | 186 | ## import 187 | `import` is a keyword, not annotation, because it's not binded to any tag, it should be only on top of file or after ``. Syntax is same as for es6. 188 | ```javascript 189 | import {TagAlias1, TagAlias2 as Item} from './path/to/file' 190 | import Layout from '/absolute/path/to/file' 191 | import * as Bootstrap from 'name-of-npm-package' 192 | ``` 193 | Then you can use those tags. 194 | ```html 195 | 196 | 197 | 198 | 199 | ``` 200 | As you can see you can share your html modules through npm and `import` will find it just like native `require()`. 201 | 202 | 203 | ## Path to tag 204 | You have two options to point on tag which you want to modify. 205 | 206 | **First** is write same tags tree to tag. 207 | ```html 208 | @export Item 209 |
210 |
    211 |
  • 212 | Title 1 213 |
  • 214 |
  • 215 | Title 2 216 |
  • 217 |
  • 218 | Title 3 219 |
  • 220 |
221 |
222 | ``` 223 | + 224 | ```html 225 | import {Item} from './module1' 226 | 227 | 228 |
    229 |
  • 230 | @prepend 231 |

    Title

    232 | 233 | @append 234 | 235 |
  • 236 |
237 |
238 | ``` 239 | = 240 | ```html 241 |
242 |
    243 |
  • Title

    244 | Title 245 |
  • 246 |
247 |
248 | ``` 249 | If you don't want or don't know tags names, simply write `` 250 | ```html 251 | 252 | 253 | 254 | ... 255 | 256 | 257 | 258 | ``` 259 | To point to third tag 260 | ```html 261 | 262 |
    263 | 264 | 265 | 266 | ... 267 | 268 |
269 |
270 | ``` 271 | **Second** is to use `@find` 272 | 273 | 274 | ## Add tag 275 | If in parent tag only one child and you write two then second will be added. 276 | ```html 277 | @export Item 278 |
279 |
    280 |
  • 281 |
282 |
283 | ``` 284 | + 285 | ```html 286 | import {Item} from './module1' 287 | 288 | 289 | 290 | 291 |
  • 292 |
    293 |
    294 | ``` 295 | = 296 | ```html 297 |
    298 |
      299 |
    • 300 |
    • 301 |
    302 |
    303 | ``` 304 | Also annotations like `@prepend` and `@append` can add tags. 305 | 306 | 307 | ## Remove tag 308 | See `@remove` 309 | 310 | 311 | ## Rename tag 312 | Just point to needed tag and write new name 313 | ```html 314 | @export Item 315 |
    316 |
      317 |
    • 318 |
    319 |
    320 | ``` 321 | + 322 | ```html 323 | import {Item} from './module1' 324 | 325 | 326 | 327 |
    328 | 329 | 330 | ``` 331 | = 332 | ```html 333 |
    334 |
      335 |
      336 |
    337 |
    338 | ``` 339 | 340 | 341 | ## Add/rewrite attribute 342 | Any attribute (except `class`) will be rewrited if it not exist in parent, it will be added. 343 | ```html 344 | @export Item 345 |
    346 |

    Title

    347 |
    348 | ``` 349 | + 350 | ```html 351 | import {Item} from './module1' 352 | 353 | 354 | 355 | 356 | ``` 357 | = 358 | ```html 359 |
    360 |

    Title

    361 |
    362 | ``` 363 | 364 | ## Remove attribute 365 | To remove attribute just write `!` before it 366 | ```html 367 | @export Item 368 |
    369 |

    Title

    370 |
    371 | ``` 372 | + 373 | ```html 374 | import {Item} from './module1' 375 | 376 | 377 |

    378 | 379 | ``` 380 | = 381 | ```html 382 |
    383 |

    Title

    384 |
    385 | ``` 386 | 387 | ## Shadow attribute 388 | When attribute name starts with `~` it means it's shadow attribute and it needed only to pass some value to extended module. This type of attribute will not add, remove or rewrite parent attribute. See example of **setExtension** function. 389 | 390 | ## Add class 391 | All class names will be added (not rewrited) to parent tag. 392 | ```html 393 | @export Item 394 |
    395 |

    Title

    396 |
    397 | ``` 398 | + 399 | ```html 400 | import {Item} from './module1' 401 | 402 | 403 |

    404 | 405 | ``` 406 | = 407 | ```html 408 |
    409 |

    Title

    410 |
    411 | ``` 412 | 413 | ## Remove class 414 | To remove class name write `!` before it 415 | ```html 416 | @export Item 417 |
    418 |

    Title

    419 |
    420 | ``` 421 | + 422 | ```html 423 | import {Item} from './module1' 424 | 425 | 426 |

    427 | 428 | ``` 429 | = 430 | ```html 431 |
    432 |

    Title

    433 |
    434 | ``` 435 | 436 | 437 | ## Rewrite text in tag 438 | Any text will rewrite parent text. 439 | ```html 440 | @export Item 441 |
    442 |

    Title

    443 |

    Title

    444 |

    445 |

    446 |
    447 |
    448 | ``` 449 | + 450 | ```html 451 | import {Item} from './module1' 452 | 453 | 454 |

    Main title

    455 | Sub title 456 | Title 457 | Title 458 | 459 | Title 460 | 461 | 462 |
    463 | ``` 464 | = 465 | ```html 466 |
    467 |

    Main title

    468 |

    Sub title

    469 |

    Title

    470 |

    Title

    471 |
    472 | Title 473 | 474 |
    475 |
    476 | ``` 477 | 478 | 479 | ## Remove text 480 | To remove text you need write some html entity like ` ` or if you no need space then `​` or similar. 481 | 482 | 483 | ## @find 484 | With this annotation you can point to tag with css selector. 485 | ```html 486 | @export Item 487 |
    488 |
    489 |
    490 |

    Title

    491 |
    492 |
    493 |

    Description

    494 |
    495 |
    496 |
    497 | ``` 498 | + 499 | ```html 500 | import {Item} from './module1' 501 | 502 | 503 | @find .header 504 | 505 | @append 506 | Sub title 507 | 508 | 509 | 510 | 511 | 512 | @append 513 |

    Text

    514 |
    515 |
    516 |
    517 | ``` 518 | = 519 | ```html 520 |
    521 |
    522 |
    523 |

    Title

    524 | Sub title
    525 |
    526 |

    Description

    527 |

    Text

    528 |
    529 |
    530 | ``` 531 | 532 | 533 | ## @append 534 | It will add tag to the end of current tag parent. 535 | ```html 536 | @export Item 537 |
    538 | Title 539 |
    540 | ``` 541 | + 542 | ```html 543 | import {Item} from './module1' 544 | 545 | 546 | @append 547 | 548 | 549 | Title 550 | 551 | ``` 552 | = 553 | ```html 554 |
    555 | Title 556 |
    557 | ``` 558 | If you want to add several tags then you need to write `@append` before each of them. 559 | 560 | 561 | ## @prepend 562 | Will add tag on first place of current parent 563 | ```html 564 | @export Item 565 |
    566 | Title 567 |
    568 | ``` 569 | + 570 | ```html 571 | import {Item} from './module1' 572 | 573 | 574 | @prepend 575 | 576 | 577 | 578 | 579 | ``` 580 | = 581 | ```html 582 |
    583 | Title 584 |
    585 | ``` 586 | 587 | 588 | ## @insert 589 | Will add tag on current place. 590 | ```html 591 | @export Item 592 |
    593 |

    Title

    594 |

    Description

    595 | 596 |
    597 | ``` 598 | + 599 | ```html 600 | import {Item} from 'module1' 601 | 602 | 603 |

    604 |

    605 | 606 | @insert 607 | 608 | 609 | ``` 610 | = 611 | ```html 612 |

    613 |

    Title

    614 |

    Description

    615 | 616 | 617 |
    618 | ``` 619 | 620 | 621 | ## @remove 622 | Will remove current tag 623 | ```html 624 | @export Item 625 |
    626 |
    627 | 628 | 629 |
    630 |
    631 | ``` 632 | + 633 | ```html 634 | import {Item} from './module1' 635 | 636 | 637 | 638 | @remove 639 | 640 | 641 | 642 | ``` 643 | = 644 | ```html 645 |
    646 |
    647 | 648 |
    649 |
    650 | ``` 651 | 652 | 653 | ## @empty 654 | Will remove children of current tag. 655 | ```html 656 | @export Item 657 |
    658 |
    659 |

    Description

    660 | 661 |
    662 |
    663 | ``` 664 | + 665 | ```html 666 | import {Item} from './module1' 667 | 668 | 669 | @empty 670 | 671 |

    Title

    672 |
    673 |
    674 | ``` 675 | = 676 | ```html 677 |
    678 |
    679 |

    Title

    680 |
    681 |
    682 | ``` 683 | 684 | 685 | ## @appendTo 686 | Will add tag to another tag by css selector. 687 | ```html 688 | @export Item 689 |
    690 |
    691 |

    Description

    692 |
    693 |
    694 | ``` 695 | + 696 | ```html 697 | import {Item} from './module1' 698 | 699 | 700 | @appendTo .description 701 | Read more 702 | 703 | ``` 704 | = 705 | ```html 706 |
    707 |
    708 |

    DescriptionRead more

    709 |
    710 |
    711 | ``` 712 | 713 | 714 | ## @prependTo 715 | Will add tag to the beginig of another tag by css selector 716 | ```html 717 | @export Item 718 |
    719 |
    720 |

    Description

    721 |
    722 |
    723 | ``` 724 | + 725 | ```html 726 | import {Item} from './module1' 727 | 728 | 729 | @prependTo .content 730 |

    Title

    731 |
    732 | ``` 733 | = 734 | ```html 735 |
    736 |

    Title

    737 |

    Description

    738 |
    739 |
    740 | ``` 741 | 742 | ## @insertBefore 743 | Will add tag before another tag by css selector 744 | ```html 745 | @export Item 746 |
    747 |
    748 |

    Title

    749 |

    Description

    750 |
    751 |
    752 | ``` 753 | + 754 | ```html 755 | import {Item} from './module1' 756 | 757 | 758 | @insertBefore .description 759 |

    Sub title

    760 |
    761 | ``` 762 | = 763 | ```html 764 |
    765 |
    766 |

    Title

    767 |

    Sub title

    Description

    768 |
    769 |
    770 | ``` 771 | 772 | 773 | ## @insertAfter 774 | Will add tag after another tag by css selector 775 | ```html 776 | @export Item 777 |
    778 |
    779 |

    Title

    780 |

    Description

    781 |
    782 |
    783 | ``` 784 | + 785 | ```html 786 | import {Item} from './module1' 787 | 788 | 789 | @insertAfter .content h1 790 |

    Sub title

    791 |
    792 | ``` 793 | = 794 | ```html 795 |
    796 |
    797 |

    Title

    Sub title

    798 |

    Description

    799 |
    800 |
    801 | ``` 802 | 803 | 804 | ## Contribute 805 | Help me improve this doc and any comments are welcome in issues. 806 | 807 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var toHtml = require('simple-html-dom-parser').getOuterHTML, 2 | htmlFileToDom = require('./src/html-file-to-dom'), 3 | htmlToDom = require('./src/html-to-dom'), 4 | globalTags = require('./src/global-tags'), 5 | setExtension = require('./src/require-module').setExtension; 6 | 7 | module.exports.render = render; 8 | 9 | module.exports.htmlToDom = htmlToDom; 10 | 11 | module.exports.htmlFileToDom = htmlFileToDom; 12 | 13 | module.exports.domToHtml = toHtml; 14 | 15 | module.exports.setExtension = setExtension; 16 | 17 | module.exports.globalTags = globalTags; 18 | 19 | setExtension(['html', 'htm'], function (file) { 20 | return htmlFileToDom(file).exports; 21 | }); 22 | 23 | /** 24 | * @param filePath 25 | * @returns {String} 26 | */ 27 | function render(filePath) { 28 | return toHtml(htmlFileToDom(filePath)); 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-extend", 3 | "version": "1.5.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": { 10 | "css-selector-parser": "^1.1.0", 11 | "resolve": "^1.1.7", 12 | "simple-html-dom-parser": "^1.1.2", 13 | "simple-object-query": "^1.6.0" 14 | }, 15 | "devDependencies": { 16 | "chai-shallow-deep-equal": "^1.4.0", 17 | "chai": "^3.5.0", 18 | "mocha": "^2.4.5" 19 | }, 20 | "scripts": { 21 | "test": "mocha" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/redexp/html-extend.git" 26 | }, 27 | "keywords": [ 28 | "html" 29 | ], 30 | "author": "Sergii Kliuchnyk", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/redexp/html-extend/issues" 34 | }, 35 | "homepage": "https://github.com/redexp/html-extend#readme" 36 | } 37 | -------------------------------------------------------------------------------- /src/clone.js: -------------------------------------------------------------------------------- 1 | module.exports = clone; 2 | 3 | function clone(node, parent) { 4 | var obj = {}; 5 | 6 | for (var field in node) { 7 | if (!node.hasOwnProperty(field) || field === 'prev' || field === 'next') continue; 8 | 9 | switch (field) { 10 | case 'parent': 11 | obj.parent = parent; 12 | break; 13 | 14 | case 'shadowDom': 15 | obj[field] = node[field]; 16 | break; 17 | 18 | case 'children': 19 | obj.children = [].concat(node.children); 20 | 21 | var prev = null, item = null; 22 | 23 | for (var i = 0, len = node.children.length; i < len; i++) { 24 | item = clone(node.children[i], obj); 25 | 26 | item.prev = prev; 27 | item.next = null; 28 | 29 | if (prev) { 30 | prev.next = item; 31 | } 32 | 33 | obj.children[i] = item; 34 | 35 | prev = item; 36 | } 37 | break; 38 | 39 | default: 40 | switch (typeOf(node[field])) { 41 | case 'object': 42 | obj[field] = clone(node[field]); 43 | break; 44 | 45 | case 'array': 46 | obj[field] = [].concat(node[field]); 47 | break; 48 | 49 | default: 50 | obj[field] = node[field]; 51 | } 52 | } 53 | } 54 | 55 | return obj; 56 | } 57 | 58 | function typeOf(val) { 59 | var type = typeof val; 60 | 61 | switch (type) { 62 | case 'object': 63 | return val ? Array.isArray(val) ? 'array' : 'object' : 'null'; 64 | 65 | default: 66 | return val; 67 | } 68 | } -------------------------------------------------------------------------------- /src/css-find.js: -------------------------------------------------------------------------------- 1 | var search = require('simple-object-query').search, 2 | CssSelectorParser = require('css-selector-parser').CssSelectorParser, 3 | cssParser = new CssSelectorParser(); 4 | 5 | cssParser 6 | .registerNestingOperators('>') 7 | .registerAttrEqualityMods('^', '$', '*', '~') 8 | ; 9 | 10 | module.exports = cssFind; 11 | 12 | function cssFind(root, rule) { 13 | if (typeof rule === 'string') { 14 | rule = cssParser.parse(rule); 15 | } 16 | 17 | if (rule.type === 'selectors') { 18 | for (var i = 0, len = rule.selectors.length; i < len; i++) { 19 | var res = cssFind(root, rule.selectors[i].rule); 20 | 21 | if (res) return res; 22 | } 23 | 24 | return; 25 | } 26 | else if (rule.type === 'ruleSet') { 27 | rule = rule.rule; 28 | } 29 | 30 | return search({ 31 | source: root, 32 | query: { 33 | type: 'tag' 34 | }, 35 | include: function (item) { 36 | if (rule.nestingOperator === '>' && item.parent && item.parent !== root) return false; 37 | 38 | return ( 39 | item.field === 'children' || 40 | item.path[item.path.length - 1] === 'children' 41 | ); 42 | }, 43 | callback: function (item) { 44 | if (item.target === root) return; 45 | 46 | var node = item.target; 47 | 48 | if (isCssValid(node, rule)) { 49 | if (!rule.rule) { 50 | return node; 51 | } 52 | 53 | return cssFind(node, rule.rule); 54 | } 55 | } 56 | }); 57 | } 58 | 59 | function isCssValid(node, rule) { 60 | var i, len; 61 | 62 | if (rule.tagName) { 63 | if (node.name !== rule.tagName) return false; 64 | } 65 | 66 | if (rule.classNames) { 67 | var classes = (node.attr['class'] || '').split(/\s+/); 68 | 69 | for (i = 0, len = rule.classNames.length; i < len; i++) { 70 | if (classes.indexOf(rule.classNames[i]) === -1) return false; 71 | } 72 | } 73 | 74 | if (rule.attrs) { 75 | for (i = 0, len = rule.attrs.length; i < len; i++) { 76 | var attr = rule.attrs[i]; 77 | 78 | if (!node.attr.hasOwnProperty(attr.name)) return false; 79 | 80 | switch (attr.operator) { 81 | case '=': 82 | if (node.attr[attr.name] !== attr.value) return false; 83 | break; 84 | 85 | case '^=': 86 | if (node.attr[attr.name].indexOf(attr.value) !== 0) return false; 87 | break; 88 | 89 | case '$=': 90 | if (node.attr[attr.name].slice(-attr.value.length) !== attr.value) return false; 91 | break; 92 | 93 | case '*=': 94 | if (node.attr[attr.name].indexOf(attr.value) === -1) return false; 95 | break; 96 | } 97 | } 98 | } 99 | 100 | if (rule.pseudos) { 101 | for (i = 0, len = rule.pseudos.length; i < len; i++) { 102 | var pseudo = rule.pseudos[i]; 103 | 104 | switch (pseudo.name) { 105 | case 'nth-child': 106 | case 'eq': 107 | if (getChildNodes(node.parent).indexOf(node) !== Number(pseudo.value) - 1) return false; 108 | break; 109 | } 110 | } 111 | } 112 | 113 | return true; 114 | } 115 | 116 | function getChildNodes(node) { 117 | var nodes = []; 118 | 119 | for (var i = 0, len = node.children.length; i < len; i++) { 120 | if (node.children[i].type === 'tag') { 121 | nodes.push(node.children[i]); 122 | } 123 | } 124 | 125 | return nodes; 126 | } -------------------------------------------------------------------------------- /src/global-tags.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /src/html-file-to-dom.js: -------------------------------------------------------------------------------- 1 | var htmlToDom = require('./html-to-dom'), 2 | fs = require('fs'); 3 | 4 | module.exports = htmlFileToDom; 5 | 6 | /** 7 | * @param {String} filePath 8 | * @returns {HtmlModule} 9 | */ 10 | function htmlFileToDom(filePath) { 11 | return htmlToDom(fs.readFileSync(filePath).toString(), filePath); 12 | } -------------------------------------------------------------------------------- /src/html-to-dom.js: -------------------------------------------------------------------------------- 1 | var parser = require('simple-html-dom-parser').parse, 2 | search = require('simple-object-query').search, 3 | get = require('simple-object-query').get, 4 | clone = require('./clone'), 5 | cssFind = require('./css-find'), 6 | requireModule = require('./require-module'), 7 | globalTags = require('./global-tags'), 8 | fs = require('fs'), 9 | util = require('util'), 10 | pt = require('path'); 11 | 12 | module.exports = htmlToDom; 13 | 14 | /** 15 | * @param {String} html 16 | * @param {String} filePath This param needs to resolve path of imported files 17 | * @returns {HtmlModule} 18 | */ 19 | function htmlToDom(html, filePath) { 20 | if (!filePath) { 21 | throw new Error('File path is required'); 22 | } 23 | 24 | var fileDir = pt.dirname(filePath); 25 | 26 | var dom = parser(html, { 27 | regex: { 28 | attribute: /[!~\w][\w:\-\.]*/ 29 | } 30 | }); 31 | 32 | // imports 33 | 34 | dom.imports = {}; 35 | 36 | var importsText = dom.children[0] && dom.children[0].type === 'text' ? 37 | dom.children[0] : 38 | dom.children.length >= 2 && dom.children[0].type === 'doctype' && dom.children[1].type === 'text' ? 39 | dom.children[1] : null; 40 | 41 | if (importsText) { 42 | importsText.data = getImportsFromText(importsText.data, function (item) { 43 | var importName = item.alias || item.name; 44 | 45 | switch (item.type) { 46 | case 'module': 47 | importName = importName === '*' ? '' : importName + '.'; 48 | 49 | var exports = requireModule(item.path, fileDir); 50 | 51 | for (var name in exports) { 52 | if (!exports.hasOwnProperty(name)) continue; 53 | 54 | dom.imports[importName + name] = { 55 | name: name, 56 | alias: importName + name, 57 | path: item.path, 58 | type: 'tag' 59 | }; 60 | } 61 | break; 62 | 63 | case 'default': 64 | dom.imports[importName] = { 65 | name: 'default', 66 | alias: importName, 67 | path: item.path, 68 | type: 'tag' 69 | }; 70 | break; 71 | 72 | case 'anonymous': 73 | requireModule(item.path, fileDir); 74 | break; 75 | 76 | default: 77 | dom.imports[importName] = item; 78 | } 79 | }); 80 | } 81 | 82 | // comments 83 | 84 | find(dom, {type: 'comment'}, function (item) { 85 | var comment = item.target, 86 | text = comment.next; 87 | 88 | if (!text || text.type !== 'text') { 89 | text = createText(); 90 | insertAfter(comment, text); 91 | } 92 | 93 | text.annotations = []; 94 | 95 | getAnnotationsFromText(comment.data, function (annotation) { 96 | text.annotations.push(annotation); 97 | }); 98 | 99 | if (text.annotations.length > 0 && (!text.next || text.next.type !== 'tag')) { 100 | throw new Error('After annotations should be a tag'); 101 | } 102 | 103 | if (comment.prev && comment.prev.type === 'text') { 104 | text.data = comment.prev.data + text.data; 105 | remove(comment.prev); 106 | } 107 | 108 | remove(comment); 109 | }); 110 | 111 | // annotations 112 | 113 | find(dom, {type: 'text'}, function (item) { 114 | var text = item.target; 115 | 116 | text.annotations = text.annotations || []; 117 | 118 | text.data = getAnnotationsFromText(text.data, function (annotation) { 119 | if (!text.next || text.next.type !== 'tag') { 120 | throw new Error('After annotations should be a tag'); 121 | } 122 | 123 | text.annotations.push(annotation); 124 | }); 125 | }); 126 | 127 | // shadow dom 128 | 129 | find(dom, {type: 'tag'}, function (item) { 130 | var tag = item.target; 131 | 132 | if (!dom.imports[tag.name] && !globalTags[tag.name]) return; 133 | 134 | tag.shadowAttr = {}; 135 | 136 | for (var name in tag.attr) { 137 | if (!tag.attr.hasOwnProperty(name) || name.charAt(0) !== '~') continue; 138 | 139 | var value = tag.attr[name]; 140 | 141 | delete tag.attr[name]; 142 | 143 | name = name.slice(1); 144 | 145 | tag.shadowAttr[name] = value; 146 | } 147 | 148 | var shadowDom; 149 | 150 | if (dom.imports[tag.name]) { 151 | var parent = dom.imports[tag.name]; 152 | 153 | shadowDom = requireModule(parent.path, fileDir)[parent.name]; 154 | } 155 | else { 156 | shadowDom = globalTags[tag.name]; 157 | } 158 | 159 | switch (typeof shadowDom) { 160 | case 'object': 161 | // ok 162 | break; 163 | 164 | case 'string': 165 | shadowDom = htmlToFirstTag(shadowDom, parent.path); 166 | break; 167 | 168 | case 'function': 169 | shadowDom = shadowDom(tag); 170 | 171 | switch (typeof shadowDom) { 172 | case 'object': 173 | // ok 174 | break; 175 | 176 | case 'string': 177 | shadowDom = htmlToFirstTag(shadowDom, parent.path); 178 | break; 179 | 180 | default: 181 | throw new Error('Extension tag handler should return string or dom object'); 182 | } 183 | break; 184 | 185 | case 'undefined': 186 | throw new Error('Undefined import tag ' + tag.name); 187 | 188 | default: 189 | throw new Error('Unknown type of tag'); 190 | } 191 | 192 | tag.shadowDom = clone(shadowDom); 193 | }); 194 | 195 | // compile 196 | 197 | find(dom, {type: 'tag', shadowDom: isObject}, function (item) { 198 | var root = item.target; 199 | 200 | compileShadowDom(root); 201 | 202 | var shadowDom = root.shadowDom; 203 | 204 | deepMergeShadowDom(root, shadowDom); 205 | 206 | replace(root, root.shadowDom); 207 | 208 | delete root.shadowDom; 209 | }); 210 | 211 | // exports 212 | 213 | dom.exports = {}; 214 | 215 | find(dom, {type: 'text'}, function (item) { 216 | var text = item.target; 217 | 218 | text.annotations.forEach(function (annotation) { 219 | switch (annotation.name) { 220 | 221 | case 'export': 222 | dom.exports[annotation.value.trim() || 'default'] = text.next; 223 | break; 224 | 225 | case 'global': 226 | var name = annotation.value.trim(); 227 | 228 | if (!name) { 229 | throw new Error('Name for global tag is required'); 230 | } 231 | 232 | globalTags[name] = text.next; 233 | break; 234 | } 235 | }); 236 | }); 237 | 238 | return dom; 239 | } 240 | 241 | function compileShadowDom(node) { 242 | search({ 243 | source: node, 244 | query: { 245 | type: 'tag', 246 | shadowDom: isObject 247 | }, 248 | include: function (item) { 249 | return ( 250 | item.field === 'children' || 251 | item.path[item.path.length - 1] === 'children' 252 | ); 253 | }, 254 | callback: function (item) { 255 | if (item.target === node) return; 256 | 257 | compileShadowDom(item.target); 258 | } 259 | }); 260 | 261 | find(node, {type: 'tag'}, function (item) { 262 | var tag = item.target, 263 | text = tag.prev; 264 | 265 | if (!( 266 | tag !== node && 267 | text && 268 | text.type === 'text' && 269 | text.annotations && 270 | text.annotations.length > 0 271 | )) { 272 | return; 273 | } 274 | 275 | if (tag.shadowDom) { 276 | merge(tag.shadowDom, tag); 277 | } 278 | 279 | var root = node, 280 | path = getPath(text, root), 281 | targetText = get(root.shadowDom, path); 282 | 283 | if (targetText && targetText.type === 'text') { 284 | merge(targetText, text); 285 | } 286 | else { 287 | targetText = insertTo(root.shadowDom, path, emptyClone(text)); 288 | } 289 | 290 | var targetTag = targetText.next, 291 | context, 292 | currentContainer, 293 | targetContainer, 294 | targetNode; 295 | 296 | text.annotations.forEach(function (annotation) { 297 | switch (annotation.name) { 298 | 299 | case 'prepend': 300 | path[path.length - 1] = 0; 301 | insertTo(root.shadowDom, path, context || flatClone(tag)); 302 | insertTo(text.parent, ['children', 0], tag); 303 | return; 304 | 305 | case 'append': 306 | remove(tag); 307 | path[path.length - 1] = Number.MAX_VALUE; 308 | insertTo(root.shadowDom, path, context || tag.shadowDom || flatClone(tag)); 309 | return; 310 | 311 | case 'insert': 312 | path[path.length - 1] = Number(path[path.length - 1]) + 1; 313 | insertTo(root.shadowDom, path, context || flatClone(tag)); 314 | insertTo(text.parent, ['children', path[path.length - 1]], tag); 315 | break; 316 | 317 | case 'remove': 318 | if (targetTag.next && targetTag.next.type === 'text') { 319 | targetText.data += targetTag.next.data; 320 | remove(targetTag.next); 321 | } 322 | 323 | remove(tag); 324 | remove(targetTag); 325 | break; 326 | 327 | case 'empty': 328 | merge(targetTag, tag); 329 | targetTag.children = []; 330 | break; 331 | 332 | case 'find': 333 | context = targetTag = cssFind(targetTag.parent, annotation.value); 334 | 335 | if (!context) { 336 | throw new Error(`Can't find tag by selector "${annotation.value}"`); 337 | } 338 | 339 | remove(tag); 340 | break; 341 | 342 | case 'appendTo': 343 | currentContainer = get(root.shadowDom, getPath(text.parent, root)); 344 | targetContainer = cssFind(currentContainer, annotation.value); 345 | 346 | remove(tag); 347 | appendTo(targetContainer, context || flatClone(tag)); 348 | break; 349 | 350 | case 'prependTo': 351 | currentContainer = get(root.shadowDom, getPath(text.parent, root)); 352 | targetContainer = cssFind(currentContainer, annotation.value); 353 | 354 | remove(tag); 355 | prependTo(targetContainer, context || flatClone(tag)); 356 | break; 357 | 358 | case 'insertAfter': 359 | currentContainer = get(root.shadowDom, getPath(text.parent, root)); 360 | targetNode = cssFind(currentContainer, annotation.value); 361 | 362 | remove(tag); 363 | insertAfter(targetNode, context || flatClone(tag)); 364 | break; 365 | 366 | case 'insertBefore': 367 | currentContainer = get(root.shadowDom, getPath(text.parent, root)); 368 | targetNode = cssFind(currentContainer, annotation.value); 369 | 370 | remove(tag); 371 | insertBefore(targetNode, context || flatClone(tag)); 372 | break; 373 | } 374 | }); 375 | 376 | if (context) { 377 | deepMergeShadowDom(tag, context); 378 | } 379 | 380 | if (text.next && text.next.type === 'text') { 381 | text.next.data = text.data + text.next.data; 382 | remove(text); 383 | } 384 | }); 385 | } 386 | 387 | function find(dom, query, cb) { 388 | return search({ 389 | source: dom, 390 | query: query, 391 | include: function (item) { 392 | return ( 393 | item.field === 'children' || 394 | item.path[item.path.length - 1] === 'children' 395 | ); 396 | }, 397 | callback: cb 398 | }); 399 | } 400 | 401 | function getImportsFromText(text, cb) { 402 | return text 403 | .replace(/^ *import +["'](.+)["'] *(as +[\w\.\-]+)? *(?:\r\n|\n)?/gm, function (x, path, alias) { 404 | alias = alias && alias.replace(/^as +/, ''); 405 | 406 | cb({ 407 | alias: alias, 408 | type: alias ? 'default' : 'anonymous', 409 | path: path 410 | }); 411 | 412 | return ''; 413 | }) 414 | .replace(/^ *import +(.+) +from +["'](.+)["'] *(?:\r\n|\n)?/gm, function (x, items, path) { 415 | items 416 | .replace(/\{([^}]+)}/, function (x, items) { 417 | items 418 | .split(/\s*,\s*/) 419 | .forEach(function (tag) { 420 | var name = tag.match(/^[\w\.\-]+/)[0], 421 | alias = tag.match(/as\s+([\w\.\-]+)$/); 422 | 423 | if (alias) alias = alias[1]; 424 | 425 | cb({ 426 | name: name, 427 | alias: alias, 428 | path: path, 429 | type: 'tag' 430 | }); 431 | }) 432 | ; 433 | 434 | return ''; 435 | }) 436 | .split(/\s*,\s*/) 437 | .forEach(function (tag) { 438 | if (!tag) return; 439 | 440 | var name = tag.match(/^(?:\*|[\w\.\-]+)/)[0], 441 | alias = tag.match(/as\s+([\w\.\-]+)$/); 442 | 443 | if (alias) alias = alias[1]; 444 | 445 | cb({ 446 | name: name, 447 | alias: alias, 448 | path: path, 449 | type: name === '*' ? 'module' : 'default' 450 | }); 451 | }) 452 | ; 453 | 454 | return ''; 455 | }); 456 | } 457 | 458 | function getAnnotationsFromText(text, cb) { 459 | return text.replace(/^ *\|? *@([\w\-]+) *(.*)(\r\n|\n)?/gm, function (x, name, value) { 460 | cb({ 461 | name: name, 462 | value: value 463 | }); 464 | 465 | return ''; 466 | }); 467 | } 468 | 469 | function isObject(val) { 470 | return !!val && typeof val === 'object'; 471 | } 472 | 473 | function mergeTag(target, source) { 474 | if (source.shadowDom !== target && source.name !== 'tag') { 475 | target.name = source.name; 476 | } 477 | 478 | var tAttr = target.attr, 479 | sAttr = source.attr; 480 | 481 | var name; 482 | 483 | if (sAttr['class']) { 484 | var tClasses = classHash(tAttr['class'] || ''), 485 | sClasses = classHash(sAttr['class'] || ''); 486 | 487 | for (name in sClasses) { 488 | if (!sClasses.hasOwnProperty(name)) continue; 489 | 490 | if (name.charAt(0) === '!') { 491 | delete tClasses[name.slice(1)]; 492 | 493 | continue; 494 | } 495 | 496 | tClasses[name] = true; 497 | } 498 | 499 | var classes = []; 500 | 501 | for (name in tClasses) { 502 | if (!tClasses.hasOwnProperty(name)) continue; 503 | 504 | classes.push(name); 505 | } 506 | 507 | tAttr['class'] = classes.join(' '); 508 | } 509 | 510 | for (name in sAttr) { 511 | if (!sAttr.hasOwnProperty(name) || name === 'class') continue; 512 | 513 | if (name.charAt(0) === '!') { 514 | delete tAttr[name.slice(1)]; 515 | 516 | continue; 517 | } 518 | 519 | tAttr[name] = sAttr[name]; 520 | } 521 | 522 | return target; 523 | } 524 | 525 | function mergeText(tagret, source) { 526 | if (!source.data.trim()) return; 527 | 528 | tagret.data = source.data; 529 | 530 | return tagret; 531 | } 532 | 533 | function merge(target, source) { 534 | if (target.type !== source.type) { 535 | throw new Error('Types are not equal'); 536 | } 537 | 538 | switch (target.type) { 539 | case 'tag': 540 | mergeTag(target, source); 541 | break; 542 | case 'text': 543 | mergeText(target, source); 544 | break; 545 | default: 546 | throw new Error('Unknown type'); 547 | } 548 | 549 | return target; 550 | } 551 | 552 | function deepMergeShadowDom(root, shadowDom) { 553 | merge(shadowDom, root); 554 | 555 | find(root, {type: /^(tag|text)$/}, function (item) { 556 | if (item.target === root) return; 557 | 558 | var tag = item.target, 559 | path = item.path, 560 | target = get(shadowDom, path); 561 | 562 | if (tag.shadowDom) { 563 | merge(tag.shadowDom, tag); 564 | if (target) { 565 | replace(target, tag.shadowDom); 566 | } 567 | else { 568 | insertTo(shadowDom, path, tag.shadowDom); 569 | } 570 | } 571 | else if (target && target.type === tag.type) { 572 | merge(target, tag); 573 | } 574 | else if (tag.type === 'text') { 575 | insertTo(shadowDom, path, flatClone(tag)); 576 | } 577 | else { 578 | insertTo(shadowDom, path, emptyClone(tag)); 579 | } 580 | }); 581 | } 582 | 583 | function classHash(classes) { 584 | var hash = {}; 585 | 586 | classes.split(/\s+/).forEach(function (name) { 587 | if (!name) return; 588 | 589 | hash[name] = true; 590 | }); 591 | 592 | return hash; 593 | } 594 | 595 | function insertTo(dom, path, node) { 596 | remove(node); 597 | 598 | var index = Number(path[path.length - 1]); 599 | 600 | var parent = get(dom, path.slice(0, -2)); 601 | 602 | parent.children.splice(index, 0, node); 603 | 604 | index = parent.children.indexOf(node); 605 | 606 | node.parent = parent; 607 | node.prev = parent.children[index - 1] || null; 608 | node.next = parent.children[index + 1] || null; 609 | 610 | if (node.prev) { 611 | node.prev.next = node; 612 | } 613 | 614 | if (node.next) { 615 | node.next.prev = node; 616 | } 617 | 618 | return node; 619 | } 620 | 621 | function appendTo(parent, node) { 622 | remove(node); 623 | 624 | insertTo(parent, ['children', Number.MAX_VALUE], node); 625 | } 626 | 627 | function prependTo(parent, node) { 628 | remove(node); 629 | 630 | insertTo(parent, ['children', 0], node); 631 | } 632 | 633 | function insertBefore(target, node) { 634 | remove(node); 635 | 636 | var index = target.parent.children.indexOf(target); 637 | 638 | insertTo(target.parent, ['children', index], node); 639 | } 640 | 641 | function insertAfter(target, node) { 642 | remove(node); 643 | 644 | var index = target.parent.children.indexOf(target); 645 | 646 | insertTo(target.parent, ['children', index + 1], node); 647 | } 648 | 649 | function replace(target, replacment) { 650 | replacment.parent = target.parent; 651 | replacment.prev = target.prev; 652 | replacment.next = target.next; 653 | 654 | if (target.parent) { 655 | var index = target.parent.children.indexOf(target); 656 | target.parent.children[index] = replacment; 657 | } 658 | 659 | if (target.prev) { 660 | target.prev.next = replacment; 661 | } 662 | 663 | if (target.next) { 664 | target.next.prev = replacment; 665 | } 666 | 667 | target.parent = target.prev = target.next = null; 668 | } 669 | 670 | function createTag(name) { 671 | return { 672 | type: 'tag', 673 | name: name || 'tag', 674 | attr: {}, 675 | children: [], 676 | parent: null, 677 | prev: null, 678 | next: null 679 | }; 680 | } 681 | 682 | function createText(data) { 683 | return { 684 | type: 'text', 685 | data: data || '', 686 | annotations: [], 687 | parent: null, 688 | prev: null, 689 | next: null 690 | }; 691 | } 692 | 693 | function emptyClone(node) { 694 | var clone; 695 | 696 | switch (node.type) { 697 | case 'tag': 698 | clone = createTag(); 699 | break; 700 | case 'text': 701 | clone = createText(); 702 | break; 703 | } 704 | 705 | return merge(clone, node); 706 | } 707 | 708 | function remove(node) { 709 | if (node.parent) { 710 | var index = node.parent.children.indexOf(node); 711 | node.parent.children.splice(index, 1); 712 | } 713 | 714 | if (node.prev) { 715 | node.prev.next = node.next; 716 | } 717 | 718 | if (node.next) { 719 | node.next.prev = node.prev; 720 | } 721 | 722 | node.parent = node.prev = node.next = null; 723 | } 724 | 725 | function getPath(node, root) { 726 | var path = []; 727 | 728 | if (node === root) return path; 729 | 730 | search({ 731 | query: { 732 | type: 'tag' 733 | }, 734 | source: node, 735 | include: function (item) { 736 | return ( 737 | item.field === 'parent' 738 | ); 739 | }, 740 | callback: function (item) { 741 | if (item.target === node) return; 742 | 743 | var parent = item.target, 744 | index = parent.children.indexOf(node); 745 | 746 | path.push(index, 'children'); 747 | 748 | node = parent; 749 | 750 | if (node === root) return true; 751 | } 752 | }); 753 | 754 | return path.reverse(); 755 | } 756 | 757 | function flatClone(node) { 758 | node = clone(node); 759 | 760 | find(node, {shadowDom: isObject}, function (item) { 761 | replace(item.target, flatClone(item.target.shadowDom)); 762 | }); 763 | 764 | return node; 765 | } 766 | 767 | function htmlToFirstTag(html, file) { 768 | html = htmlToDom(html, file); 769 | return html.children[0].type === 'tag' ? html.children[0] : html.children[1]; 770 | } -------------------------------------------------------------------------------- /src/require-module.js: -------------------------------------------------------------------------------- 1 | var resolve = require('resolve'), 2 | pt = require('path'); 3 | 4 | /** 5 | * @typedef {Object} HtmlModule 6 | * @property {Object} imports - Hash of imported tags 7 | * @property {Object} exports - Hash of exported tags 8 | * @property {Array} children 9 | */ 10 | 11 | var modules = {}, 12 | extensions = {}, 13 | extensionsList = []; 14 | 15 | requireModule.modules = modules; 16 | requireModule.setExtension = setExtension; 17 | 18 | module.exports = requireModule; 19 | 20 | function requireModule(path, baseDir) { 21 | var realPath = resolve.sync(path, { 22 | basedir: baseDir, 23 | extensions: extensionsList 24 | }); 25 | 26 | if (modules[realPath]) return modules[realPath]; 27 | 28 | var ext = pt.extname(realPath), 29 | cb = extensions[ext]; 30 | 31 | return modules[realPath] = cb(realPath); 32 | } 33 | 34 | /** 35 | * @param {Array|String} fileExtensions 36 | * @param {Function} handler 37 | */ 38 | function setExtension(fileExtensions, handler) { 39 | if (!Array.isArray(fileExtensions)) { 40 | fileExtensions = [fileExtensions]; 41 | } 42 | 43 | fileExtensions.forEach(function (ext) { 44 | ext = '.' + ext; 45 | 46 | if (!handler && extensions[ext]) { 47 | delete extensions[ext]; 48 | } 49 | else { 50 | extensions[ext] = handler; 51 | } 52 | }); 53 | 54 | extensionsList = Object.keys(extensions); 55 | } -------------------------------------------------------------------------------- /test/annotations/button.html: -------------------------------------------------------------------------------- 1 | 2 | @export default 3 | -------------------------------------------------------------------------------- /test/annotations/index.html: -------------------------------------------------------------------------------- 1 | import Item from './item' 2 | import Button from './button' 3 | 4 | 5 | Text 6 | 7 | @prepend 8 |

    Title

    9 | 10 | @append 11 | 18 | 19 |

    20 | Test 21 | 22 | @empty 23 | 24 | 25 | 26 |

    27 | 28 | @insert 29 |

    Wrapper

    30 | 31 | @appendTo .description 32 | 33 | 34 | 35 | 36 | @prependTo .description 37 | 38 | 39 | @insertAfter .description .cut 40 | 41 | 42 | @insertBefore .description .cut 43 | 44 | 45 | @remove 46 | 47 |
    48 | -------------------------------------------------------------------------------- /test/annotations/item.html: -------------------------------------------------------------------------------- 1 | 2 | @export default 3 |
    4 |

    Text test word

    5 |
    Desc
    6 |
    Last
    7 |
    -------------------------------------------------------------------------------- /test/annotations/output.html: -------------------------------------------------------------------------------- 1 | 2 |

    Title

    3 | Text 4 | 5 | 6 | 7 | 8 | 9 |

    10 | Test 11 | 12 | 13 | 14 | 15 |

    16 |

    Wrapper

    Desc
    17 | 18 |
    19 | -------------------------------------------------------------------------------- /test/complex/icons/close.html: -------------------------------------------------------------------------------- 1 | 2 | @export default 3 | -------------------------------------------------------------------------------- /test/complex/icons/index.html: -------------------------------------------------------------------------------- 1 | import Close from './close' 2 | import Save from './save' 3 | 4 | @export Close 5 | 6 | 7 | @export Save 8 | -------------------------------------------------------------------------------- /test/complex/icons/save.html: -------------------------------------------------------------------------------- 1 | 2 | @export default 3 | -------------------------------------------------------------------------------- /test/complex/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Layout from './layout' 4 | import List, {Item as TestItem} from './layout/list' 5 | import * as Icons, {Save} from './icons' 6 | 7 | 8 | @find h1 span 9 | @remove 10 | 11 | 12 |

    Test title

    13 | 14 | @prepend 15 | 16 | 17 | Test text 18 | 19 | 20 | 21 | 22 | Item 23 | 24 | 25 | @find i.close 26 | X 27 | 28 | 29 | 30 | 31 | 32 | @append 33 | 34 | 35 | @empty 36 | 39 |
    40 | -------------------------------------------------------------------------------- /test/complex/layout/header.htm: -------------------------------------------------------------------------------- 1 | import {Close as CloseIcon} from '../icons' 2 | 3 | @export default 4 |

    Title

    -------------------------------------------------------------------------------- /test/complex/layout/index.htm: -------------------------------------------------------------------------------- 1 | import Header from './header' 2 | import List from './list' 3 | 4 | @export default 5 |
    6 |
    Title
    7 | 8 | 9 | 10 |

    Description text

    11 |
    -------------------------------------------------------------------------------- /test/complex/layout/list.htm: -------------------------------------------------------------------------------- 1 | 2 | @export default 3 |
      4 | @export Item 5 |
    • 6 |
    -------------------------------------------------------------------------------- /test/complex/output.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 |

    Test title

    6 | 7 | 8 | 9 | Test text 10 | 11 | 20 | 21 | 24 |
    25 | -------------------------------------------------------------------------------- /test/extensions/component.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/extensions/index.html: -------------------------------------------------------------------------------- 1 | import Component from './component' 2 | 3 |
    4 | 5 |
    6 | -------------------------------------------------------------------------------- /test/extensions/output.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | 5 | 6 | 7 |
    8 | -------------------------------------------------------------------------------- /test/find/index.html: -------------------------------------------------------------------------------- 1 | import Item from './item' 2 | 3 | 4 | @find button[type="submit"] 5 | @remove 6 | 7 | 8 | @find h1 .close 9 | X 10 | 11 | 12 | 13 | @find .close 14 | @append 15 | 16 | 17 | 18 | 19 | @find .close 20 | @prepend 21 | 22 | 23 | 24 | 25 |

    26 | 27 | @find button[type="submit"] 28 | @insert 29 | 30 | 31 |

    32 | 33 | 34 | 35 |

    36 | 37 | @find button[type="submit"] 38 | @insert 39 | @empty 40 | 41 |
    42 | 43 | 44 | @find h1 > .close 45 | @appendTo p > button[type="submit"] 46 | 47 | 48 | 49 | 50 | @find p > button[type="button"] 51 | @prependTo p 52 | @empty 53 | 54 | 55 | 56 | 57 | @find p > button[type="submit"] 58 | @insertAfter p > button[type="button"] 59 | @empty 60 | 61 | 62 | 63 | 64 | @find p > button[type="submit"] 65 | @insertBefore p 66 | @empty 67 | 68 | 69 | -------------------------------------------------------------------------------- /test/find/item.html: -------------------------------------------------------------------------------- 1 | @export 2 |
    3 |

    Title

    4 | 5 |

    6 | 7 | 8 |

    9 | -------------------------------------------------------------------------------- /test/find/output.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 |

    Title X

    5 | 6 |

    7 | 8 |

    9 | 10 | 11 |
    12 |

    Title

    13 | 14 |

    15 | 16 | 17 |

    18 | 19 | 20 |
    21 |

    Title

    22 | 23 |

    24 | 25 | 26 |

    27 | 28 | 29 |
    30 |

    Title

    31 | 32 | 33 | 34 |

    35 | 36 | 37 |

    38 | 39 | 40 |
    41 |

    Title

    42 | 43 | 44 |

    45 | 46 | 47 |

    48 | 49 | 50 |
    51 |

    Title

    52 | 53 |

    54 | 55 | 56 |

    57 | 58 | 59 |
    60 |

    Title

    61 | 62 |

    63 | 64 | 65 |

    66 | 67 | 68 |
    69 |

    Title

    70 | 71 |

    72 | 73 | 74 |

    75 | 76 | 77 |
    78 |

    Title

    79 | 80 |

    81 | 82 | 83 |

    84 | 85 | -------------------------------------------------------------------------------- /test/imports/.gitignore: -------------------------------------------------------------------------------- 1 | !/node_modules -------------------------------------------------------------------------------- /test/imports/globals.html: -------------------------------------------------------------------------------- 1 | @global GlobalTag1 2 |
    3 | 4 | @global Global.Tag2 5 |
    -------------------------------------------------------------------------------- /test/imports/helpers/button.html: -------------------------------------------------------------------------------- 1 | 2 | @export default 3 | -------------------------------------------------------------------------------- /test/imports/helpers/index.html: -------------------------------------------------------------------------------- 1 | import Input from './input' 2 | import Button from './button' 3 | 4 | 7 |
    8 | 9 | 10 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /test/merge/icons/close.html: -------------------------------------------------------------------------------- 1 | 2 | @export 3 | -------------------------------------------------------------------------------- /test/merge/icons/index.html: -------------------------------------------------------------------------------- 1 | import Close from './close' 2 | import Open from './open' 3 | 4 | @export Close 5 | 6 | 7 | @export Open 8 | -------------------------------------------------------------------------------- /test/merge/icons/open.html: -------------------------------------------------------------------------------- 1 | @export 2 | -------------------------------------------------------------------------------- /test/merge/index.html: -------------------------------------------------------------------------------- 1 | import Item from './item' 2 | import Button from './button' 3 | import Text from './text' 4 | import * as Icons from './icons' 5 | 6 |
    7 |

    Title

    8 | 9 | 10 | 11 | Test 12 | 13 | 14 | Text 15 | 16 | 17 | Text 18 | 19 | 20 | Description 21 | 22 |
    27 | 28 | 29 | 30 | Test 31 | Test 32 | Test 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/merge/item.html: -------------------------------------------------------------------------------- 1 | @export default 2 |
    3 | 4 | Title 5 | 6 | 7 | 8 | 9 |

    Text

    10 | 11 | 12 |
    -------------------------------------------------------------------------------- /test/merge/output.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 |

    Title

    4 | 5 |
    6 | 7 | Test 8 | 9 | Title 10 | 11 | Text 12 | 13 | 14 | Text 15 | 16 | 17 |

    Description

    18 | 19 | 20 |
    21 | 22 | Title 23 | 24 | 25 | 26 | 27 |

    Text

    28 | 29 | 30 |
    31 |
    32 |
    33 | 34 | 35 | 36 | Test 37 | Test 38 | Test 39 | Text 40 | Text 41 | -------------------------------------------------------------------------------- /test/merge/text.html: -------------------------------------------------------------------------------- 1 | @export 2 | Text --------------------------------------------------------------------------------