/* * Copyright 2010 Alibaba Group Holding Limited. * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * @(#)Node.java 1.11 2000/08/16 * */ package org.w3c.tidy; /** * Node (c) 1998-2000 (W3C) MIT, INRIA, Keio University See Tidy.java for the * copyright notice. Derived from <a * href="http://www.w3.org/People/Raggett/tidy"> HTML Tidy Release 4 Aug * 2000</a> * * @author Dave Raggett <dsr@w3.org> * @author Andy Quick <ac.quick@sympatico.ca> (translation to Java) * @version 1.11, 2000/08/16 Tidy Release 4 Aug 2000 */ /* * Used for elements and text nodes element name is null for text nodes start * and end are offsets into lexbuf which contains the textual content of all * elements in the parse tree. parent and content allow traversal of the parse * tree in any direction. attributes are represented as a linked list of AttVal * nodes which hold the strings for attribute/value pairs. */ public class Node { public static final short RootNode = 0; public static final short DocTypeTag = 1; public static final short CommentTag = 2; public static final short ProcInsTag = 3; public static final short TextNode = 4; public static final short StartTag = 5; public static final short EndTag = 6; public static final short StartEndTag = 7; public static final short CDATATag = 8; public static final short SectionTag = 9; public static final short AspTag = 10; public static final short JsteTag = 11; public static final short PhpTag = 12; protected Node parent; protected Node prev; protected Node next; protected Node last; protected int start; /* start of span onto text array */ protected int end; /* end of span onto text array */ protected byte[] textarray; /* the text array */ protected short type; /* * TextNode, StartTag, EndTag etc. */ protected boolean closed; /* * true if closed by explicit end tag */ protected boolean implicit; /* true if inferred */ protected boolean linebreak; /* * true if followed by a line break */ protected Dict was; /* old tag when it was changed */ protected Dict tag; /* tag's dictionary definition */ protected String element; /* name (null for text nodes) */ protected AttVal attributes; protected Node content; public Node() { this(TextNode, null, 0, 0); } public Node(short type, byte[] textarray, int start, int end) { this.parent = null; this.prev = null; this.next = null; this.last = null; this.start = start; this.end = end; this.textarray = textarray; this.type = type; this.closed = false; this.implicit = false; this.linebreak = false; this.was = null; this.tag = null; this.element = null; this.attributes = null; this.content = null; } public Node(short type, byte[] textarray, int start, int end, String element, TagTable tt) { this.parent = null; this.prev = null; this.next = null; this.last = null; this.start = start; this.end = end; this.textarray = textarray; this.type = type; this.closed = false; this.implicit = false; this.linebreak = false; this.was = null; this.tag = null; this.element = element; this.attributes = null; this.content = null; if (type == StartTag || type == StartEndTag || type == EndTag) { tt.findTag(this); } } /* used to clone heading nodes when split by an <HR> */ @Override protected Object clone() { Node node = new Node(); node.parent = this.parent; if (this.textarray != null) { node.textarray = new byte[this.end - this.start]; node.start = 0; node.end = this.end - this.start; if (node.end > 0) { System.arraycopy(this.textarray, this.start, node.textarray, node.start, node.end); } } node.type = this.type; node.closed = this.closed; node.implicit = this.implicit; node.linebreak = this.linebreak; node.was = this.was; node.tag = this.tag; if (this.element != null) { node.element = this.element; } if (this.attributes != null) { node.attributes = (AttVal) this.attributes.clone(); } return node; } public AttVal getAttrByName(String name) { AttVal attr; for (attr = this.attributes; attr != null; attr = attr.next) { if (name != null && attr.attribute != null && attr.attribute.equals(name)) { break; } } return attr; } /* default method for checking an element's attributes */ public void checkAttributes(Lexer lexer) { AttVal attval; for (attval = this.attributes; attval != null; attval = attval.next) { attval.checkAttribute(lexer, this); } } public void checkUniqueAttributes(Lexer lexer) { AttVal attval; for (attval = this.attributes; attval != null; attval = attval.next) { if (attval.asp == null && attval.php == null) { attval.checkUniqueAttribute(lexer, this); } } } public void addAttribute(String name, String value) { AttVal av = new AttVal(null, null, null, null, '"', name, value); av.dict = AttributeTable.getDefaultAttributeTable().findAttribute(av); if (this.attributes == null) { this.attributes = av; } else /* append to end of attributes */ { AttVal here = this.attributes; while (here.next != null) { here = here.next; } here.next = av; } } /* remove attribute from node then free it */ public void removeAttribute(AttVal attr) { AttVal av; AttVal prev = null; AttVal next; for (av = this.attributes; av != null; av = next) { next = av.next; if (av == attr) { if (prev != null) { prev.next = next; } else { this.attributes = next; } } else { prev = av; } } } /* find doctype element */ public Node findDocType() { Node node; for (node = this.content; node != null && node.type != DocTypeTag; node = node.next) { ; } return node; } public void discardDocType() { Node node; node = findDocType(); if (node != null) { if (node.prev != null) { node.prev.next = node.next; } else { node.parent.content = node.next; } if (node.next != null) { node.next.prev = node.prev; } node.next = null; } } /* remove node from markup tree and discard it */ public static Node discardElement(Node element) { Node next = null; if (element != null) { next = element.next; removeNode(element); } return next; } /* insert node into markup tree */ public static void insertNodeAtStart(Node element, Node node) { node.parent = element; if (element.content == null) { element.last = node; } else { element.content.prev = node; // AQ added 13 Apr 2000 } node.next = element.content; node.prev = null; element.content = node; } /* insert node into markup tree */ public static void insertNodeAtEnd(Node element, Node node) { node.parent = element; node.prev = element.last; if (element.last != null) { element.last.next = node; } else { element.content = node; } element.last = node; } /* * insert node into markup tree in pace of element which is moved to become * the child of the node */ public static void insertNodeAsParent(Node element, Node node) { node.content = element; node.last = element; node.parent = element.parent; element.parent = node; if (node.parent.content == element) { node.parent.content = node; } if (node.parent.last == element) { node.parent.last = node; } node.prev = element.prev; element.prev = null; if (node.prev != null) { node.prev.next = node; } node.next = element.next; element.next = null; if (node.next != null) { node.next.prev = node; } } /* insert node into markup tree before element */ public static void insertNodeBeforeElement(Node element, Node node) { Node parent; parent = element.parent; node.parent = parent; node.next = element; node.prev = element.prev; element.prev = node; if (node.prev != null) { node.prev.next = node; } if (parent.content == element) { parent.content = node; } } /* insert node into markup tree after element */ public static void insertNodeAfterElement(Node element, Node node) { Node parent; parent = element.parent; node.parent = parent; // AQ - 13Jan2000 fix for parent == null if (parent != null && parent.last == element) { parent.last = node; } else { node.next = element.next; // AQ - 13Jan2000 fix for node.next == null if (node.next != null) { node.next.prev = node; } } element.next = node; node.prev = element; } public static void trimEmptyElement(Lexer lexer, Node element) { TagTable tt = lexer.configuration.tt; if (lexer.canPrune(element)) { if (element.type != TextNode) { Report.warning(lexer, element, null, Report.TRIM_EMPTY_ELEMENT); } discardElement(element); } else if (element.tag == tt.tagP && element.content == null) { /* replace <p></p> by <br><br> to preserve formatting */ Node node = lexer.inferredTag("br"); Node.coerceNode(lexer, element, tt.tagBr); Node.insertNodeAfterElement(element, node); } } /* * This maps <em>hello </em><strong>world</strong> to <em>hello</em> * <strong>world</strong> If last child of element is a text node then trim * trailing white space character moving it to after element's end tag. */ public static void trimTrailingSpace(Lexer lexer, Node element, Node last) { byte c; TagTable tt = lexer.configuration.tt; if (last != null && last.type == Node.TextNode && last.end > last.start) { c = lexer.lexbuf[last.end - 1]; if (c == 160 || c == (byte) ' ') { /* take care with <td> </td> */ if (element.tag == tt.tagTd || element.tag == tt.tagTh) { if (last.end > last.start + 1) { last.end -= 1; } } else { last.end -= 1; if ((element.tag.model & Dict.CM_INLINE) != 0 && !((element.tag.model & Dict.CM_FIELD) != 0)) { lexer.insertspace = true; } /* if empty string then delete from parse tree */ if (last.start == last.end) { trimEmptyElement(lexer, last); } } } } } /* * This maps <p>hello<em> world</em> to <p>hello <em>world</em> Trims * initial space, by moving it before the start tag, or if this element is * the first in parent's content, then by discarding the space */ public static void trimInitialSpace(Lexer lexer, Node element, Node text) { Node prev, node; // GLP: Local fix to Bug 119789. Remove this comment when parser.c is updated. // 31-Oct-00. if (text.type == TextNode && text.textarray[text.start] == (byte) ' ' && text.start < text.end) { if ((element.tag.model & Dict.CM_INLINE) != 0 && !((element.tag.model & Dict.CM_FIELD) != 0) && element.parent.content != element) { prev = element.prev; if (prev != null && prev.type == TextNode) { if (prev.textarray[prev.end - 1] != (byte) ' ') { prev.textarray[prev.end++] = (byte) ' '; } ++element.start; } else /* create new node */ { node = lexer.newNode(); // Local fix for bug 228486 (GLP). This handles the case // where we need to create a preceeding text node but there are // no "slots" in textarray that we can steal from the current // element. Therefore, we create a new textarray containing // just the blank. When Tidy is fixed, this should be removed. if (element.start >= element.end) { node.start = 0; node.end = 1; node.textarray = new byte[1]; } else { node.start = element.start++; node.end = element.start; node.textarray = element.textarray; } node.textarray[node.start] = (byte) ' '; node.prev = prev; if (prev != null) { prev.next = node; } node.next = element; element.prev = node; node.parent = element.parent; } } /* discard the space in current node */ ++text.start; } } /* * Move initial and trailing space out. This routine maps: hello<em> * world</em> to hello <em>world</em> and <em>hello * </em><strong>world</strong> to <em>hello</em> <strong>world</strong> */ public static void trimSpaces(Lexer lexer, Node element) { Node text = element.content; TagTable tt = lexer.configuration.tt; if (text != null && text.type == Node.TextNode && element.tag != tt.tagPre) { trimInitialSpace(lexer, element, text); } text = element.last; if (text != null && text.type == Node.TextNode) { trimTrailingSpace(lexer, element, text); } } public boolean isDescendantOf(Dict tag) { Node parent; for (parent = this.parent; parent != null; parent = parent.parent) { if (parent.tag == tag) { return true; } } return false; } /* * the doctype has been found after other tags, and needs moving to before * the html element */ public static void insertDocType(Lexer lexer, Node element, Node doctype) { TagTable tt = lexer.configuration.tt; Report.warning(lexer, element, doctype, Report.DOCTYPE_AFTER_TAGS); while (element.tag != tt.tagHtml) { element = element.parent; } insertNodeBeforeElement(element, doctype); } public Node findBody(TagTable tt) { Node node; node = this.content; while (node != null && node.tag != tt.tagHtml) { node = node.next; } if (node == null) { return null; } node = node.content; while (node != null && node.tag != tt.tagBody) { node = node.next; } return node; } public boolean isElement() { return this.type == StartTag || this.type == StartEndTag ? true : false; } /* * unexpected content in table row is moved to just before the table in * accordance with Netscape and IE. This code assumes that node hasn't been * inserted into the row. */ public static void moveBeforeTable(Node row, Node node, TagTable tt) { Node table; /* first find the table element */ for (table = row.parent; table != null; table = table.parent) { if (table.tag == tt.tagTable) { if (table.parent.content == table) { table.parent.content = node; } node.prev = table.prev; node.next = table; table.prev = node; node.parent = table.parent; if (node.prev != null) { node.prev.next = node; } break; } } } /* * if a table row is empty then insert an empty cell this practice is * consistent with browser behavior and avoids potential problems with row * spanning cells */ public static void fixEmptyRow(Lexer lexer, Node row) { Node cell; if (row.content == null) { cell = lexer.inferredTag("td"); insertNodeAtEnd(row, cell); Report.warning(lexer, row, cell, Report.MISSING_STARTTAG); } } public static void coerceNode(Lexer lexer, Node node, Dict tag) { Node tmp = lexer.inferredTag(tag.name); Report.warning(lexer, node, tmp, Report.OBSOLETE_ELEMENT); node.was = node.tag; node.tag = tag; node.type = StartTag; node.implicit = true; node.element = tag.name; } /* extract a node and its children from a markup tree */ public static void removeNode(Node node) { if (node.prev != null) { node.prev.next = node.next; } if (node.next != null) { node.next.prev = node.prev; } if (node.parent != null) { if (node.parent.content == node) { node.parent.content = node.next; } if (node.parent.last == node) { node.parent.last = node.prev; } } node.parent = node.prev = node.next = null; } public static boolean insertMisc(Node element, Node node) { if (node.type == CommentTag || node.type == ProcInsTag || node.type == CDATATag || node.type == SectionTag || node.type == AspTag || node.type == JsteTag || node.type == PhpTag) { insertNodeAtEnd(element, node); return true; } return false; } /* * used to determine how attributes without values should be printed this * was introduced to deal with user defined tags e.g. Cold Fusion */ public static boolean isNewNode(Node node) { if (node != null && node.tag != null) { return (node.tag.model & Dict.CM_NEW) != 0; } return true; } public boolean hasOneChild() { return this.content != null && this.content.next == null; } /* find html element */ public Node findHTML(TagTable tt) { Node node; for (node = this.content; node != null && node.tag != tt.tagHtml; node = node.next) { ; } return node; } public Node findHEAD(TagTable tt) { Node node; node = this.findHTML(tt); if (node != null) { for (node = node.content; node != null && node.tag != tt.tagHead; node = node.next) { ; } } return node; } public boolean checkNodeIntegrity() { Node child; boolean found = false; if (this.prev != null) { if (this.prev.next != this) { return false; } } if (this.next != null) { if (this.next.prev != this) { return false; } } if (this.parent != null) { if (this.prev == null && this.parent.content != this) { return false; } if (this.next == null && this.parent.last != this) { return false; } for (child = this.parent.content; child != null; child = child.next) { if (child == this) { found = true; break; } } if (!found) { return false; } } for (child = this.content; child != null; child = child.next) { if (!child.checkNodeIntegrity()) { return false; } } return true; } /* * Add class="foo" to node */ public static void addClass(Node node, String classname) { AttVal classattr = node.getAttrByName("class"); /* * if there already is a class attribute then append class name after a * space */ if (classattr != null) { classattr.value = classattr.value + " " + classname; } else { /* create new class attribute */ node.addAttribute("class", classname); } } /* --------------------- DEBUG -------------------------- */ private static final String[] nodeTypeString = { "RootNode", "DocTypeTag", "CommentTag", "ProcInsTag", "TextNode", "StartTag", "EndTag", "StartEndTag", "SectionTag", "AspTag", "PhpTag" }; @Override public String toString() { String s = ""; Node n = this; while (n != null) { s += "[Node type="; s += nodeTypeString[n.type]; s += ",element="; if (n.element != null) { s += n.element; } else { s += "null"; } if (n.type == TextNode || n.type == CommentTag || n.type == ProcInsTag) { s += ",text="; if (n.textarray != null && n.start <= n.end) { s += "\""; s += Lexer.getString(n.textarray, n.start, n.end - n.start); s += "\""; } else { s += "null"; } } s += ",content="; if (n.content != null) { s += n.content.toString(); } else { s += "null"; } s += "]"; if (n.next != null) { s += ","; } n = n.next; } return s; } /* --------------------- END DEBUG ---------------------- */ /* --------------------- DOM ---------------------------- */ protected org.w3c.dom.Node adapter = null; protected org.w3c.dom.Node getAdapter() { if (adapter == null) { switch (this.type) { case RootNode: adapter = new DOMDocumentImpl(this); break; case StartTag: case StartEndTag: adapter = new DOMElementImpl(this); break; case DocTypeTag: adapter = new DOMDocumentTypeImpl(this); break; case CommentTag: adapter = new DOMCommentImpl(this); break; case TextNode: adapter = new DOMTextImpl(this); break; case CDATATag: adapter = new DOMCDATASectionImpl(this); break; case ProcInsTag: adapter = new DOMProcessingInstructionImpl(this); break; default: adapter = new DOMNodeImpl(this); } } return adapter; } protected Node cloneNode(boolean deep) { Node node = (Node) this.clone(); if (deep) { Node child; Node newChild; for (child = this.content; child != null; child = child.next) { newChild = child.cloneNode(deep); insertNodeAtEnd(node, newChild); } } return node; } protected void setType(short newType) { this.type = newType; } /* --------------------- END DOM ------------------------ */ }