UNPKG

15.2 kBJavaScriptView Raw
1/**
2 * A HtmlNode has the following attributes:
3 * * __type__:
4 * * __VOID__, __EMPTY__: this is a fake node, it will just aggregate nodes in the __children__ attribute.
5 * * __TAG__, __ELEMENT__: an element of the DOM.
6 * * __TEXT__: a text without any tag arround it.
7 * * __name__: the name of the element if this node is a DOM element.
8 * * __attribs__: an object of all the element attributes, if this node is a DOM element.
9 * * __text__: the content of the text element if this node is a text element.
10 * * __children__: an array of the children nodes.
11 * * __extra__: an object for free information addition.
12 *
13 * @module node
14 */
15
16var id = 1;
17
18/**
19 * Not a real element. Just a list of elements.
20 * Must contain `children`.
21 */
22exports.VOID = 0;
23
24/**
25 * DOM element tag.
26 * Has an attribute *name*.
27 * @const
28 */
29exports.TAG = 1;
30exports.ELEMENT = 1;
31
32
33/**
34 * HTML text node.
35 * Has an attribute *text*.
36 * @const
37 */
38exports.TEXT = 2;
39
40/**
41 * HTML CDATA section.
42 * @example
43 * <![CDATA[This a is a CDATA section...]]>
44 * @const
45 */
46exports.CDATA = 3;
47
48/**
49 * @example
50 * <?xml-stylesheet href="default.css" title="Default style"?>
51 */
52exports.PROCESSING = 4;
53
54/**
55 * HTML comment.
56 * @example
57 * <-- This is a comment -->
58 * @const
59 */
60exports.COMMENT = 5;
61
62/**
63 * @const
64 */
65exports.DOCTYPE = 6;
66
67/**
68 * @const
69 */
70exports.TYPE = 7;
71
72/**
73 * Example: `&amp;`, `&lt;`, ...
74 */
75exports.ENTITY = 8;
76
77/**
78 * Put the `text` attribute verbatim, without any transformation, nor parsing.
79 */
80exports.VERBATIM = 98;
81
82/**
83 * Every children is a diffrent HTML file. Usefull to generate several pages.
84 */
85exports.PAGES = 99;
86
87/**
88 * @return a deep copy of `root`.
89 */
90exports.clone = function(root) {
91 return JSON.parse(JSON.stringify(root));
92};
93
94/**
95 * Return the next incremental id.
96 */
97exports.nextId = function() {
98 return "W" + (id++);
99};
100
101/**
102 * @param {object} root root of the tree we want to look in.
103 * @param {string} name name of the searched TAG. Must be in lowercase.
104 * @return the first TAG node with the name `name`.
105 */
106exports.getElementByName = function(root, name) {
107 if (!root) return null;
108 if (root.name && root.name.toLowerCase() === name.toLowerCase()) return root;
109 if (Array.isArray(root.children)) {
110 var i, node;
111 for (i = 0 ; i < root.children.length ; i++) {
112 node = exports.getElementByName(root.children[i], name);
113 if (node !== null) return node;
114 }
115 }
116 return null;
117};
118
119/**
120 * Remove `child` from the children's list of `parent`.
121 */
122exports.removeChild = function(parent, child) {
123 if (Array.isArray(parent.children)) {
124 var i, node;
125 for (i = 0 ; i < parent.children.length ; i++) {
126 node = parent.children[i];
127 if (node === child) {
128 parent.children.splice(i, 1);
129 break;
130 }
131 }
132 }
133};
134
135exports.trim = function(root) {
136 var children = root.children;
137 function isEmpty(node) {
138 if (node.type != exports.TEXT) return false;
139 if (!node.text) return false;
140 if (node.text.trim().length == 0) return true;
141 return false;
142 }
143 while (children.length > 0 && isEmpty(children[0])) {
144 children.shift();
145 }
146 while (children.length > 0 && isEmpty(children[children.length - 1])) {
147 children.pop();
148 }
149 if (children.length > 0) {
150 if (children[0].type == exports.TEXT) {
151 children[0].text = children[0].text.trimLeft();
152 }
153 if (children[children.length - 1].type == exports.TEXT) {
154 children[children.length - 1].text = children[children.length - 1].text.trimRight();
155 }
156 }
157};
158
159exports.trimLeft = function(root) {
160 var children = root.children;
161 function isEmpty(node) {
162 if (node.type != exports.TEXT) return false;
163 if (!node.text) return false;
164 if (node.text.trim().length == 0) return true;
165 return false;
166 }
167 while (children.length > 0 && isEmpty(children[0])) {
168 children.shift();
169 }
170 if (children.length > 0) {
171 if (children[0].type == exports.TEXT) {
172 children[0].text = children[0].text.trimLeft();
173 }
174 }
175};
176
177exports.trimRight = function(root) {
178 var children = root.children;
179 function isEmpty(node) {
180 if (node.type != exports.TEXT) return false;
181 if (!node.text) return false;
182 if (node.text.trim().length == 0) return true;
183 return false;
184 }
185 while (children.length > 0 && isEmpty(children[children.length - 1])) {
186 children.pop();
187 }
188 if (children.length > 0) {
189 if (children[children.length - 1].type == exports.TEXT) {
190 children[children.length - 1].text = children[children.length - 1].text.trimRight();
191 }
192 }
193};
194
195/**
196 * Convert a node in HTML code.
197 */
198exports.toString = function(node) {
199 var txt = '',
200 key, val;
201 if (!node) return '';
202 if (node.type == exports.TAG) {
203 txt += "<" + node.name;
204 for (key in node.attribs) {
205 val = "" + node.attribs[key];
206 txt += " " + key;
207 if( typeof val === 'string' ) {
208 // Add a value on ly if it is a string.
209 txt += "=" + JSON.stringify(val) + "";
210 }
211 }
212 if (typeof node.html === 'string') {
213 txt += ">" + node.html;
214 }
215 else if (node.children && node.children.length > 0) {
216 txt += ">";
217 node.children.forEach(
218 function(child) {
219 txt += exports.toString(child);
220 }
221 );
222 txt += "</" + node.name + ">";
223 } else {
224 if (node.void) txt += ">";
225 else if (node.autoclose) txt += "/>";
226 else txt += "></" + node.name + ">";
227 }
228 }
229 else if (node.type == exports.ENTITY) {
230 txt += node.text;
231 }
232 else if (node.type == exports.VERBATIM) {
233 txt += node.text;
234 }
235 else if (node.type == exports.TEXT) {
236 txt += node.text.replace(/&/g, '&amp;').replace(/</g, '&lt;');
237 }
238 else if (node.type == exports.PROCESSING) {
239 txt += "<?" + node.name;
240 for (key in node.attribs) {
241 val = "" + node.attribs[key];
242 txt += " " + key + "=" + JSON.stringify(val) + "";
243 }
244 txt += "?>";
245 }
246 else if (typeof node.html === 'string') {
247 txt += node.html;
248 }
249 else if (node.children) {
250 node.children.forEach(
251 function(child) {
252 txt += exports.toString(child);
253 }
254 );
255 }
256
257 return txt;
258};
259/**
260 * Put on console a representation of the tree.
261 */
262exports.debug = function(node, indent) {
263 if (typeof indent === 'undefined') indent = '';
264 if (!node) {
265 return console.log(indent + "UNDEFINED!");
266 }
267 if (node.type == exports.TEXT) {
268 console.log(indent + "\"" + node.text.trim() + "\"");
269 } else {
270 if (node.children && node.children.length > 0) {
271 console.log(
272 indent + "<" + (node.name ? node.name : node.type) + "> "
273 + (node.attribs ? JSON.stringify(node.attribs) : '')
274 );
275 node.children.forEach(
276 function(child) {
277 exports.debug(child, indent + ' ');
278 }
279 );
280 console.log(indent + "</" + (node.name ? node.name : node.type) + "> ");
281 } else {
282 console.log(
283 indent + "<" + (node.name ? node.name : node.type) + " "
284 + (node.attribs ? JSON.stringify(node.attribs) : '') + " />"
285 );
286 }
287 }
288};
289/**
290 * Walk through the HTML tree and, eventually, replace branches.
291 * The functions used as arguments take only one argument: the current node.
292 * @param node {object} root node
293 * @param functionBottomUp {function} function to call when traversing the tree bottom-up.
294 * @param functionTopDowy {function} function to call when traversing the tree top-down.
295 * @param parent {object} parent node or *undefined*.
296 */
297exports.walk = function(node, functionBotomUp, functionTopDown, parent) {
298 if (!node) return;
299 var i, child, replacement, children;
300 if (typeof functionTopDown === 'function') {
301 functionTopDown(node, parent);
302 }
303 if (node.children) {
304 children = [];
305 node.children.forEach(
306 function(item) {
307 children.push(item);
308 }
309 );
310 children.forEach(
311 function(child) {
312 exports.walk(child, functionBotomUp, functionTopDown, node);
313 }
314 );
315 }
316 if (typeof functionBotomUp === 'function') {
317 return functionBotomUp(node, parent);
318 }
319};
320/**
321 * Get/Set an attribute.
322 */
323exports.att = function(node, name, value) {
324 if (typeof value === 'undefined') {
325 if (node.attribs) {
326 return node.attribs[name];
327 }
328 return undefined;
329 }
330 if (!node.attribs) {
331 node.attribs = {};
332 }
333 node.attribs[name] = value;
334};
335/**
336 * Return the text content of a node.
337 */
338exports.text = function(node, text) {
339 if (typeof text === 'undefined') {
340 if (node.type == exports.TEXT) {
341 return node.text;
342 }
343 if (node.children) {
344 var txt = "";
345 node.children.forEach(
346 function(child) {
347 txt += exports.text(child);
348 }
349 );
350 return txt;
351 } else {
352 return "";
353 }
354 } else {
355 node.children = [
356 {
357 type: exports.TEXT,
358 text: text
359 }
360 ];
361 }
362};
363/**
364 * Return a node representing a viewable error message.
365 */
366exports.createError = function(msg) {
367 return {
368 type: exports.TAG,
369 name: "div",
370 attribs: {
371 style: "margin:4px;padding:4px;border:2px solid #fff;color:#fff;background:#f00;"
372 + "box-shadow:0 0 2px #000;overflow:auto;font-family:monospace;font-size:1rem;"
373 },
374 children: [
375 {
376 type: exports.TEXT,
377 text: msg.replace("\n", "<br/>", "g")
378 }
379 ]
380 };
381};
382/**
383 * Add a class to a node of type TAG.
384 */
385exports.addClass = function(node, className) {
386 if (!node.attribs) {
387 node.attribs = {};
388 }
389 var classes = (node.attribs["class"] || "").split(/[ \t\n\r]/g);
390 var i;
391 for (i = 0 ; i < classes.length ; i++) {
392 if (className == classes[i]) return false;
393 }
394 var txt = "";
395 classes.push(className);
396 for (i = 0 ; i < classes.length ; i++) {
397 if (txt.length > 0) {
398 txt += " ";
399 }
400 txt += classes[i];
401 }
402 if (txt.length > 0) {
403 node.attribs["class"] = txt;
404 }
405 return true;
406};
407/**
408 * Return a node of type TAG représenting a javascript content.
409 */
410exports.createJavascript = function(code) {
411 return {
412 type: exports.TAG,
413 name: "script",
414 attribs: {
415 type: "text/javascript"
416 },
417 children: [
418 {
419 type: exports.TEXT,
420 text: "//<![CDATA[\n" + code + "//]]>"
421 }
422 ]
423 };
424};
425
426exports.forEachAttrib = function(node, func) {
427 var attribs = node.attribs,
428 attName, attValue, count = 0;
429 if (!attribs) return 0;
430 for (attName in attribs) {
431 attValue = attribs[attName];
432 if (typeof attValue === 'string') {
433 func(attName, attValue);
434 }
435 }
436 return count;
437};
438
439/**
440 * Apply a function on every child of a node.
441 */
442exports.forEachChild = function(node, func) {
443 var children = node.children, i, child;
444 if (!children) return false;
445 for (i = 0 ; i < children.length ; i++) {
446 child = children[i];
447 if (typeof child.type === 'undefined' || child.type == exports.VOID) {
448 exports.forEachChild(child, func);
449 }
450 else if (true === func(child, i)) {
451 break;
452 }
453 }
454 return i;
455};
456/**
457 * If a node's children is of type VOID, we must remove it and add its
458 * children to node's children.
459 *
460 * Then following tree:
461 * ```
462 * {
463 * children: [
464 * {
465 * children: [
466 * {type: Tree.TAG, name: "hr"},
467 * {
468 * children: [{type: Tree.TAG, name: "img"}]
469 * }
470 * {type: Tree.TEXT, text: "Hello"},
471 * ]
472 * },
473 * {type: Tree.TEXT, text: "World"},
474 * ],
475 * type: Tree.TAG,
476 * name: "div"
477 * }
478 * ```
479 * will be tranformed in:
480 * ```
481 * {
482 * children: [
483 * {type: Tree.TAG, name: "hr"},
484 * {type: Tree.TAG, name: "img"},
485 * {type: Tree.TEXT, text: "Hello"},
486 * {type: Tree.TEXT, text: "World"},
487 * ],
488 * type: Tree.TAG,
489 * name: "div"
490 * }
491 * ```
492 */
493exports.normalizeChildren = function(node, recurse) {
494 if (typeof recurse === 'undefined') recurse = false;
495 if (node.children) {
496 var children = [];
497 extractNonVoidChildren(node, children);
498 node.children = children;
499 if (recurse) {
500 node.children.forEach(
501 function(child) {
502 exports.normalizeChildren(child, true);
503 }
504 );
505 }
506 }
507};
508
509function extractNonVoidChildren(node, target) {
510 if (typeof target === 'undefined') target = [];
511 if (node.children && node.children.length > 0) {
512 node.children.forEach(
513 function(child) {
514 if (!child.type || child.type == exports.VOID) {
515 extractNonVoidChildren(child, target);
516 } else {
517 target.push(child);
518 }
519 }
520 );
521 }
522 return target;
523}
524
525/**
526 * @return The first children tag with name `tagname`, or `null` if not found.
527 */
528exports.findChild = function(root, tagname) {
529 if (!Array.isArray(root.children)) return null;
530 for (var i = 0 ; i < root.children.length ; i++) {
531 var item = root.children[i];
532 if (item.type == exports.TAG && item.name == tagname) return item;
533 }
534 return null;
535};
536
537/**
538 * If a tag called `tagname` exist among `root`'s children, return it.
539 * Otherwise, create a new tag, append it to `root` and return it.
540 */
541exports.findOrAppendChild = function(root, tagname, attribs, children) {
542 var child = exports.findChild(root, tagname);
543 if (child) return child;
544 child = exports.tag(tagname, attribs, children);
545 if (!Array.isArray(root.children)) {
546 root.children = [child];
547 } else {
548 root.children.push(child);
549 }
550 return child;
551};
552
553/**
554 * Return a div element.
555 */
556exports.div = function(attribs, children) {
557 return exports.tag("div", attribs, children);
558};
559
560/**
561 * @example
562 * // <span style='color: red'>ERROR</span>
563 * htmltree.tag("span", {style: "color: red"}, "ERROR");
564 * @example
565 * // <b><span class="dirty">OK!</span></b>
566 * htmltree.tag("b", {}, [
567 * htmltree.tag("span", "dirty", "OK!")
568 * ]);
569 */
570exports.tag = function(name, attribs, children) {
571 if (!attribs) attribs = {};
572 if (typeof attribs === 'string') attribs = {"class": attribs};
573 if (typeof children === 'undefined') children = [];
574 if (!Array.isArray(children)) {
575 if (typeof children === 'string') {
576 children = {
577 type: exports.TEXT,
578 text: children
579 };
580 }
581 children = [children];
582 }
583 return {
584 type: exports.TAG,
585 name: name,
586 attribs: attribs,
587 children: children
588 };
589};
590/**
591 * Return multi-lingual text.
592 */
593exports.createText = function(dic) {
594 var key, val, children = [];
595 for (key in dic) {
596 val = dic[key];
597 children.push(
598 {
599 type: exports.TAG,
600 name: "span",
601 attribs: {
602 lang: key
603 },
604 children: [
605 {
606 type: exports.TEXT,
607 text: val
608 }
609 ]
610 }
611 );
612 }
613 return children;
614};
615/**
616 * @description
617 * Remove all TEXT or COMMENT children.
618 *
619 * @param root the htmlnode from which you want to keep only TAG children.
620 * @memberof node
621 */
622exports.keepOnlyTagChildren = function(root) {
623 if (!root.children) return root;
624 var children = [];
625 root.children.forEach(
626 function(node) {
627 if (node.type == exports.TAG) {
628 children.push(node);
629 }
630 }
631 );
632 root.children = children;
633 return root;
634};
635
636
637/**
638 * @param {string} html.
639 * @return A node
640 */
641exports.fromHtml = function( html ) {
642
643};