├── .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 | [](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 | OK
172 |
173 | OK
174 |
179 | OK
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 | OK
235 |
236 |
237 |
238 | ```
239 | =
240 | ```html
241 |
242 |
243 | Title
244 | Title
245 | OK
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 |
283 | ```
284 | +
285 | ```html
286 | import {Item} from './module1'
287 |
288 | -
289 |
290 |
291 |
292 |
293 |
294 | ```
295 | =
296 | ```html
297 |
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 |
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 |
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 |
396 |
397 | ```
398 | +
399 | ```html
400 | import {Item} from './module1'
401 |
402 | -
403 |
404 |
405 | ```
406 | =
407 | ```html
408 |
409 |
410 |
411 | ```
412 |
413 | ## Remove class
414 | To remove class name write `!` before it
415 | ```html
416 | @export Item
417 |
418 |
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 |
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 |
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 |
OK
548 |
549 | Title
550 |
551 | ```
552 | =
553 | ```html
554 |
555 | Title
556 | OK
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 |
OK
576 |
577 |
578 |
579 | ```
580 | =
581 | ```html
582 | OK
583 |
584 |
585 | ```
586 |
587 |
588 | ## @insert
589 | Will add tag on current place.
590 | ```html
591 | @export Item
592 |
593 |
Title
594 |
Description
595 |
Ok
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 |
Ok
617 |
618 | ```
619 |
620 |
621 | ## @remove
622 | Will remove current tag
623 | ```html
624 | @export Item
625 |
626 |
627 |
628 | OK
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 | OK
648 |
649 |
650 | ```
651 |
652 |
653 | ## @empty
654 | Will remove children of current tag.
655 | ```html
656 | @export Item
657 |
658 |
659 |
Description
660 |
OK
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 |
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 |
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 | OK
--------------------------------------------------------------------------------
/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 |
12 | @remove
13 |
14 |
15 | @append
16 |
17 |
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 |
OK
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 |
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 | OK
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 |
7 |
8 |
9 |
10 |
Description text
11 |
--------------------------------------------------------------------------------
/test/complex/layout/list.htm:
--------------------------------------------------------------------------------
1 |
2 | @export default
3 |
--------------------------------------------------------------------------------
/test/complex/output.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Test text
10 |
11 |
20 |
21 |
24 |
OK
25 |
--------------------------------------------------------------------------------
/test/extensions/component.xhtml:
--------------------------------------------------------------------------------
1 |
2 | {type}
3 |
4 |
--------------------------------------------------------------------------------
/test/extensions/index.html:
--------------------------------------------------------------------------------
1 | import Component from './component'
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test/extensions/output.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | number
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 | Empty
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 |
--------------------------------------------------------------------------------
/test/find/output.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
19 |
20 |
28 |
29 |
39 |
40 |
49 |
50 |
58 |
59 |
67 |
68 |
76 |
77 |
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/imports/helpers/input.html:
--------------------------------------------------------------------------------
1 |
2 | @export default
3 |
--------------------------------------------------------------------------------
/test/imports/index.html:
--------------------------------------------------------------------------------
1 | import Layout, {Wrapper, Footer as TestFooter} from './layout'
2 | import * as Helpers, {Button} from './helpers'
3 | import './layout' as Main
4 | import './globals'
5 | import Module1 from 'module1'
6 | import Module2 from 'module2'
7 | import Module3 from 'module3'
8 |
9 |
--------------------------------------------------------------------------------
/test/imports/layout.html:
--------------------------------------------------------------------------------
1 |
2 | @export default
3 | @export Wrapper
4 |
5 | @export Header
6 |
Title
7 |
8 | @export Footer
9 |
10 |
--------------------------------------------------------------------------------
/test/imports/node_modules/module1/main.html:
--------------------------------------------------------------------------------
1 | @export
2 |
--------------------------------------------------------------------------------
/test/imports/node_modules/module1/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "module1",
3 | "main": "main.html"
4 | }
5 |
--------------------------------------------------------------------------------
/test/imports/node_modules/module2/index.html:
--------------------------------------------------------------------------------
1 | @export
2 |
--------------------------------------------------------------------------------
/test/imports/node_modules/module3.html:
--------------------------------------------------------------------------------
1 | @export
2 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | var chai = require('chai').use(require('chai-shallow-deep-equal')),
2 | expect = chai.expect,
3 | pt = require('path');
4 |
5 | var htmlFileToDom = require('../index').htmlFileToDom,
6 | render = require('../index').render;
7 |
8 | describe('imports', function () {
9 | it('should have imports', function () {
10 | var dom = htmlFileToDom(__dirname + '/imports/index.html');
11 |
12 | expect(dom.imports.Layout).to.deep.equal({
13 | name: 'default',
14 | alias: 'Layout',
15 | type: 'tag',
16 | path: './layout'
17 | });
18 |
19 | expect(dom.imports.Wrapper).to.deep.equal({
20 | name: 'Wrapper',
21 | alias: null,
22 | type: 'tag',
23 | path: './layout'
24 | });
25 |
26 | expect(dom.imports.TestFooter).to.deep.equal({
27 | name: 'Footer',
28 | alias: 'TestFooter',
29 | type: 'tag',
30 | path: './layout'
31 | });
32 |
33 | expect(dom.imports['Helpers.Input']).to.deep.equal({
34 | name: 'Input',
35 | alias: 'Helpers.Input',
36 | type: 'tag',
37 | path: './helpers'
38 | });
39 |
40 | expect(dom.imports['Helpers.Button']).to.deep.equal({
41 | name: 'Button',
42 | alias: 'Helpers.Button',
43 | type: 'tag',
44 | path: './helpers'
45 | });
46 |
47 | expect(dom.imports['Button']).to.deep.equal({
48 | name: 'Button',
49 | alias: null,
50 | type: 'tag',
51 | path: './helpers'
52 | });
53 |
54 | expect(dom.imports['Main']).to.deep.equal({
55 | name: 'default',
56 | alias: 'Main',
57 | type: 'tag',
58 | path: './layout'
59 | });
60 |
61 | expect(dom.imports).to.not.have.property('Footer');
62 | expect(dom.imports).to.not.have.property('GlobalTag1');
63 | expect(dom.imports).to.not.have.property('Global.Tag2');
64 |
65 | var g = require('../index').globalTags;
66 |
67 | expect(g).to.have.property('GlobalTag1');
68 | expect(g).to.have.property('Global.Tag2');
69 |
70 | expect(g['GlobalTag1']).to.shallowDeepEqual({
71 | type: 'tag',
72 | attr: {
73 | 'class': 'global-1'
74 | }
75 | });
76 |
77 | expect(g['Global.Tag2']).to.shallowDeepEqual({
78 | type: 'tag',
79 | attr: {
80 | 'class': 'global-2'
81 | }
82 | });
83 |
84 | expect(dom.imports['Module1']).to.deep.equal({
85 | name: 'default',
86 | alias: 'Module1',
87 | type: 'tag',
88 | path: 'module1'
89 | });
90 |
91 | expect(dom.imports['Module2']).to.deep.equal({
92 | name: 'default',
93 | alias: 'Module2',
94 | type: 'tag',
95 | path: 'module2'
96 | });
97 |
98 | expect(dom.imports['Module3']).to.deep.equal({
99 | name: 'default',
100 | alias: 'Module3',
101 | type: 'tag',
102 | path: 'module3'
103 | });
104 |
105 | expect(dom.children[1]).to.shallowDeepEqual({
106 | type: 'tag',
107 | attr: {
108 | 'class': 'wrapper'
109 | }
110 | });
111 |
112 | expect(dom.children[2]).to.shallowDeepEqual({
113 | type: 'tag',
114 | attr: {
115 | 'class': 'wrapper'
116 | }
117 | });
118 |
119 | expect(dom.children[3]).to.shallowDeepEqual({
120 | type: 'tag',
121 | attr: {
122 | 'class': 'footer'
123 | }
124 | });
125 |
126 | expect(dom.children[4]).to.shallowDeepEqual({
127 | type: 'tag',
128 | attr: {
129 | 'type': 'text'
130 | }
131 | });
132 |
133 | expect(dom.children[5]).to.shallowDeepEqual({
134 | type: 'tag',
135 | attr: {
136 | 'type': 'button'
137 | }
138 | });
139 |
140 | expect(dom.children[6]).to.shallowDeepEqual({
141 | type: 'tag',
142 | attr: {
143 | 'class': 'wrapper'
144 | }
145 | });
146 |
147 | expect(dom.children[7]).to.shallowDeepEqual({
148 | type: 'tag',
149 | attr: {
150 | 'class': 'global-1'
151 | }
152 | });
153 |
154 | expect(dom.children[8]).to.shallowDeepEqual({
155 | type: 'tag',
156 | attr: {
157 | 'class': 'global-2'
158 | }
159 | });
160 |
161 | expect(dom.children[9]).to.shallowDeepEqual({
162 | type: 'tag',
163 | attr: {
164 | 'class': 'module1'
165 | }
166 | });
167 |
168 | expect(dom.children[10]).to.shallowDeepEqual({
169 | type: 'tag',
170 | attr: {
171 | 'class': 'module2'
172 | }
173 | });
174 |
175 | expect(dom.children[11]).to.shallowDeepEqual({
176 | type: 'tag',
177 | attr: {
178 | 'class': 'module3'
179 | }
180 | });
181 | });
182 |
183 | it('should have exports', function () {
184 | var dom = htmlFileToDom(__dirname + '/imports/helpers/index.html');
185 |
186 | expect(dom.exports.default).to.shallowDeepEqual({
187 | type: 'tag',
188 | name: 'div'
189 | });
190 |
191 | expect(dom.exports.Input).to.shallowDeepEqual({
192 | type: 'tag',
193 | name: 'input'
194 | });
195 |
196 | expect(dom.exports.Button).to.shallowDeepEqual({
197 | type: 'tag',
198 | name: 'button'
199 | });
200 | });
201 |
202 | it('should not have shadow DOM', function () {
203 | var dom = htmlFileToDom(__dirname + '/imports/helpers/index.html');
204 |
205 | expect(dom.exports.Input).to.shallowDeepEqual({
206 | type: 'tag',
207 | name: 'input'
208 | });
209 |
210 | expect(dom.exports.Input).to.not.have.property('shadowDom');
211 | });
212 | });
213 |
214 | describe('merge', function () {
215 | it('should merge', function () {
216 | var input = render(__dirname + '/merge/index.html'),
217 | output = render(__dirname + '/merge/output.html');
218 |
219 | expect(input).to.equal(output);
220 | });
221 | });
222 |
223 | describe('annotations', function () {
224 | it('should handle annotations', function () {
225 | var dom = render(__dirname + '/annotations/index.html'),
226 | out = render(__dirname + '/annotations/output.html');
227 |
228 | expect(dom).to.equal(out);
229 | });
230 | });
231 |
232 | describe('cssFind', function () {
233 | var parser = require('simple-html-dom-parser').parse,
234 | cssFind = require('../src/css-find');
235 |
236 | it('should find node by css selector', function () {
237 | var node = parser(``);
238 |
239 | var target = cssFind(node, '.close');
240 |
241 | expect(target).to.shallowDeepEqual({
242 | name: 'i',
243 | attr: {
244 | "class": 'close'
245 | }
246 | });
247 |
248 | target = cssFind(node, 'span.close');
249 |
250 | expect(target).to.be.an('undefined');
251 |
252 | target = cssFind(node, 'a[href="index.html"] .close');
253 |
254 | expect(target).to.shallowDeepEqual({
255 | name: 'i',
256 | attr: {
257 | "class": 'close'
258 | }
259 | });
260 |
261 | target = cssFind(node, 'a[href^="index"] .close');
262 |
263 | expect(target).to.shallowDeepEqual({
264 | name: 'i',
265 | attr: {
266 | "class": 'close'
267 | }
268 | });
269 |
270 | target = cssFind(node, 'a[href$=".html"] .close');
271 |
272 | expect(target).to.shallowDeepEqual({
273 | name: 'i',
274 | attr: {
275 | "class": 'close'
276 | }
277 | });
278 |
279 | target = cssFind(node, 'a[href*="x.h"] .close');
280 |
281 | expect(target).to.shallowDeepEqual({
282 | name: 'i',
283 | attr: {
284 | "class": 'close'
285 | }
286 | });
287 |
288 | target = cssFind(node, 'a[href*="test"] .close');
289 |
290 | expect(target).to.be.an('undefined');
291 |
292 | target = cssFind(node, 'a > .close');
293 |
294 | expect(target).to.shallowDeepEqual({
295 | name: 'i',
296 | attr: {
297 | "class": 'close'
298 | }
299 | });
300 |
301 | target = cssFind(node, 'div > .close');
302 |
303 | expect(target).to.be.an('undefined');
304 | });
305 |
306 | it('should handle comma', function () {
307 | var node = parser(``);
308 |
309 | var target = cssFind(node, '.open, .close');
310 |
311 | expect(target).to.shallowDeepEqual({
312 | name: 'span',
313 | attr: {
314 | "class": 'open'
315 | }
316 | });
317 |
318 | target = cssFind(node, 'a[href="#"], span');
319 |
320 | expect(target).to.shallowDeepEqual({
321 | name: 'span',
322 | attr: {
323 | "class": 'open'
324 | }
325 | });
326 |
327 | target = cssFind(node, '.test, a > .close');
328 |
329 | expect(target).to.shallowDeepEqual({
330 | name: 'i',
331 | attr: {
332 | "class": 'close'
333 | }
334 | });
335 | });
336 |
337 | it('should handle pseudos', function () {
338 | var node = parser(``);
339 |
340 | var target = cssFind(node, 'span:eq(2)');
341 |
342 | expect(target).to.shallowDeepEqual({
343 | name: 'span',
344 | attr: {
345 | "class": 'close'
346 | }
347 | });
348 |
349 | target = cssFind(node, 'a > span:nth-child(1)');
350 |
351 | expect(target).to.shallowDeepEqual({
352 | name: 'span',
353 | attr: {
354 | "class": 'open'
355 | }
356 | });
357 |
358 | target = cssFind(node, 'a:eq(10)');
359 |
360 | expect(target).to.be.an('undefined');
361 | });
362 | });
363 |
364 | describe('find', function () {
365 | it('should handle find annotation', function () {
366 | var dom = render(__dirname + '/find/index.html'),
367 | out = render(__dirname + '/find/output.html');
368 |
369 | expect(dom).to.equal(out);
370 | });
371 | });
372 |
373 | describe('complex', function () {
374 | it('should handle everything', function () {
375 | var dom = render(__dirname + '/complex/index.html'),
376 | out = render(__dirname + '/complex/output.html');
377 |
378 | expect(dom).to.equal(out);
379 | });
380 | });
381 |
382 | describe('extensions', function () {
383 | var setExtension = require('../index').setExtension,
384 | fs = require('fs');
385 |
386 | it('should handle extension as function', function () {
387 | setExtension('xhtml', function (file) {
388 | var html = fs.readFileSync(file).toString();
389 |
390 | return {
391 | "default": function (tag) {
392 | var result = html,
393 | data = tag.shadowAttr;
394 |
395 | for (var name in data) {
396 | if (!data.hasOwnProperty(name)) continue;
397 |
398 | result = result.replace(new RegExp(`\\{${name}}`, 'g'), data[name]);
399 | }
400 |
401 | return result;
402 | }
403 | };
404 | });
405 |
406 | var dom = render(__dirname + '/extensions/index.html'),
407 | out = render(__dirname + '/extensions/output.html');
408 |
409 | expect(dom).to.equal(out);
410 | });
411 |
412 | it('should handle extension as string', function () {
413 | setExtension('xhtml', function (file) {
414 | return {
415 | "default": fs.readFileSync(file).toString()
416 | };
417 | });
418 |
419 | var dom = render(__dirname + '/extensions/index.html'),
420 | out = render(__dirname + '/extensions/output.html');
421 |
422 | expect(dom).to.equal(out);
423 | });
424 |
425 | it('should handle extension as dom object', function () {
426 | setExtension('xhtml', function (file) {
427 | var dom = htmlFileToDom(file);
428 |
429 | return {
430 | "default": dom.children[0]
431 | };
432 | });
433 |
434 | var dom = render(__dirname + '/extensions/index.html'),
435 | out = render(__dirname + '/extensions/output.html');
436 |
437 | expect(dom).to.equal(out);
438 | });
439 | });
--------------------------------------------------------------------------------
/test/merge/button.html:
--------------------------------------------------------------------------------
1 |
2 | @export default
3 |
--------------------------------------------------------------------------------
/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 |
23 |
24 |
25 |
26 |
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
--------------------------------------------------------------------------------