blob: a682e663578ed0f6e6ae34098e25479ca60e8910 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.netbeans.modules.html.parser;
import java.util.*;
import org.netbeans.modules.html.editor.lib.api.ProblemDescription;
import org.netbeans.modules.html.editor.lib.api.elements.*;
import org.netbeans.modules.web.common.api.LexerUtils;
import org.openide.util.CharSequences;
/**
*
* @author marekfukala
*/
public class ElementsFactory {
private CharSequence source;
ElementsFactory(CharSequence source) {
this.source = source;
}
Text createText(int startOffset, int endOffset) {
return new Text(startOffset, endOffset, source);
}
ModifiableCloseTag createCloseTag(int startOffset, int endOffset, byte nameLen) {
return new CommonCloseTag(source, startOffset, endOffset, nameLen);
}
ModifiableOpenTag createOpenTag(int startOffset, int endOffset, byte nameLen) {
return new CommonOpenTag(source, startOffset, endOffset, nameLen);
}
ModifiableOpenTag createVirtualOpenTag(CharSequence name) {
return new VirtualOpenTag(name);
}
ModifiableOpenTag createEmptyOpenTag(int startOffset, int endOffset, byte nameLen) {
return new EmptyOpenTag(source, startOffset, endOffset, nameLen);
}
Root createRoot() {
return new Root(source, null);
}
CommonAttribute createAttribute(int nameOffset, int valueOffset, short nameLen, int valueLen) {
return new CommonAttribute(source, nameOffset, valueOffset, nameLen, valueLen);
}
CommonAttribute createAttribute(int nameOffset, short nameLen) {
return new CommonAttribute(source, nameOffset, nameLen);
}
static interface ModifiableElement extends Element {
public void detachFromParent();
public void setEndOffset(int endOffset);
public void setParent(Node parent);
}
static interface ModifiableCloseTag extends ModifiableElement, CloseTag {
public void setMatchingOpenTag(OpenTag openTag);
}
static interface ModifiableOpenTag extends ModifiableElement, OpenTag {
public void addChild(Element element);
public void insertChildBefore(Element toInsert, Element element);
public void addChildren(Collection<Element> element);
public void removeChildren(Collection<Element> children);
public void removeChild(Element element);
public void setSemanticEndOffset(int endOffset);
public void setMatchingCloseTag(CloseTag closeTag);
public void setAttribute(Attribute attribute);
}
static class Text implements ModifiableElement {
private int from, to;
private CharSequence source;
private Node parent;
public Text(int from, int to, CharSequence source) {
this.from = from;
this.to = to;
this.source = source;
}
@Override
public int from() {
return from;
}
@Override
public int to() {
return to;
}
@Override
public ElementType type() {
return ElementType.TEXT;
}
@Override
public CharSequence image() {
return source.subSequence(from, to);
}
@Override
public CharSequence id() {
return null;
}
@Override
public Collection<ProblemDescription> problems() {
return Collections.emptyList();
}
@Override
public Node parent() {
return parent;
}
@Override
public void detachFromParent() {
}
@Override
public void setEndOffset(int endOffset) {
to = endOffset;
}
@Override
public void setParent(Node parent) {
this.parent = parent;
}
@Override
public String toString() {
return new StringBuilder()
.append(type().name())
.append(';')
.append(' ')
.append(from())
.append("-")
.append(to())
.append(' ')
.append('"')
.append(escapeEOLs(image()))
.append('"')
.toString();
}
private String escapeEOLs(CharSequence text) {
StringBuilder sb = new StringBuilder();
for(int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if(c == '\n') {
sb.append("\\n");
} else {
sb.append(c);
}
}
return sb.toString();
}
}
abstract static class TagElement implements Named, ModifiableElement {
//32-64
protected CharSequence source;
//32
//element start
private int startOffset;
//16
//element end - element start. Should fit into 2^16
private short endOffsetToStartOffsetDiff;
//8
//lenght of the name of the element
private byte nameLen;
//32-64
private Node parent;
//sum (32bit JVM): 120bits=15bytes
public TagElement(CharSequence source, int startOffset, int endOffset, byte nameLen) {
assert nameLen >= 0;
this.source = source;
this.startOffset = startOffset;
setEndOffset(endOffset);
setNameLen(nameLen);
}
@Override
public void setEndOffset(int endOffset) {
this.endOffsetToStartOffsetDiff = (short) (endOffset - startOffset);
}
protected void setNameLen(byte nameLen) {
assert nameLen >= 0;
this.nameLen = nameLen;
}
//1 for open tag (< between startOffset and the name), 2 for end tag (</ ...)
protected abstract int nameOffsetToStartOffsetDiff();
private int nameOffset() {
return startOffset + nameOffsetToStartOffsetDiff();
}
@Override
public CharSequence name() {
return source.subSequence(nameOffset(), nameOffset() + nameLen);
}
@Override
public CharSequence namespacePrefix() {
int colonIndex = CharSequences.indexOf(name(), ":");
return colonIndex == -1 ? null : name().subSequence(0, colonIndex);
}
@Override
public CharSequence unqualifiedName() {
int colonIndex = CharSequences.indexOf(name(), ":");
return colonIndex == -1 ? name() : name().subSequence(colonIndex + 1, name().length());
}
@Override
public int from() {
return startOffset;
}
@Override
public int to() {
return startOffset + endOffsetToStartOffsetDiff;
}
@Override
public CharSequence image() {
return source.subSequence(from(), to());
}
@Override
public CharSequence id() {
return name();
}
@Override
public Collection<ProblemDescription> problems() {
return Collections.emptyList();
}
@Override
public void setParent(Node parent) {
this.parent = parent;
}
@Override
public Node parent() {
return parent;
}
@Override
public String toString() {
return new StringBuilder().append(name()).append("(").append(type().name()).append(")").append("; ").append(from()).append("-").append(to()).toString();
}
@Override
public void detachFromParent() {
// ModifiableOpenTag mot = (ModifiableOpenTag) parent();
// mot.removeChild(this);
setParent(null);
}
}
static class EmptyOpenTag extends TagElement implements OpenTag, ModifiableOpenTag {
private Collection<Attribute> attrs;
public EmptyOpenTag(CharSequence source, int startOffset, int endOffset, byte nameLen) {
super(source, startOffset, endOffset, nameLen);
}
void addAttribute(Attribute attr) {
if(attrs == null) {
attrs = new ArrayList<Attribute>();
}
attrs.add(attr);
}
void addAttributes(Collection<Attribute> attributes) {
if(attrs == null) {
attrs = new ArrayList<Attribute>();
}
attrs.addAll(attributes);
}
@Override
public Collection<Attribute> attributes() {
return attrs == null ? Collections.<Attribute>emptyList() : attrs;
}
@Override
public Collection<Attribute> attributes(AttributeFilter filter) {
Collection<Attribute> filtered = new ArrayList<Attribute>(1);
for (Attribute a : attributes()) {
if (filter.accepts(a)) {
filtered.add(a);
}
}
return filtered;
}
@Override
public Attribute getAttribute(String name) {
//typically very low number of attrs so the linear search doesn't hurt
for (Attribute a : attributes()) {
if (LexerUtils.equals(name, a.name(), true, false)) {
return a;
}
}
return null;
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public CloseTag matchingCloseTag() {
return null;
}
@Override
public int semanticEnd() {
return to();
}
@Override
public Collection<Element> children() {
return Collections.emptyList();
}
@Override
public Collection<Element> children(ElementType type) {
return Collections.emptyList();
}
@Override
public Collection<Element> children(ElementFilter filter) {
return Collections.emptyList();
}
@Override
public <T extends Element> Collection<T> children(Class<T> type) {
return Collections.emptyList();
}
@Override
public ElementType type() {
return ElementType.OPEN_TAG;
}
@Override
protected int nameOffsetToStartOffsetDiff() {
return 1; //"<".length();
}
@Override
public void addChild(Element element) {
//no-op
// throw new IllegalStateException();
}
@Override
public void removeChild(Element element) {
//no-op
// throw new IllegalStateException();
}
@Override
public void setSemanticEndOffset(int endOffset) {
//no-op
}
@Override
public void setAttribute(Attribute attribute) {
addAttribute(attribute);
}
@Override
public void removeChildren(Collection<Element> children) {
//no-op
// throw new IllegalStateException();
}
@Override
public void addChildren(Collection<Element> element) {
//no-op
// throw new IllegalStateException();
}
@Override
public void insertChildBefore(Element toInsert, Element element) {
//no-op
// throw new IllegalStateException();
}
@Override
public void setMatchingCloseTag(CloseTag closeTag) {
//no-op
// throw new IllegalStateException();
}
@Override
public String toString() {
return new StringBuilder(super.toString()).append(isEmpty() ? "(self close tag)" : "").toString();
}
}
static class CommonOpenTag extends EmptyOpenTag {
private List<Element> children;
private CloseTag matchingEndTag;
private int logicalEndOffset;
public CommonOpenTag(CharSequence source, int startOffset, int endOffset, byte nameLen) {
super(source, startOffset, endOffset, nameLen);
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public void setMatchingCloseTag(CloseTag endTag) {
this.matchingEndTag = endTag;
}
@Override
public CloseTag matchingCloseTag() {
return matchingEndTag;
}
@Override
public void addChild(Element child) {
if (children == null) {
children = new ArrayList<Element>(1);
}
children.add(child);
((ModifiableElement)child).setParent(this);
}
@Override
public void addChildren(Collection<Element> elements) {
for(Element e : new LinkedList<Element>(elements)) {
addChild(e);
}
}
@Override
public void removeChild(Element element) {
if (children == null) {
return;
}
children.remove(element);
if (children.isEmpty()) {
children = null;
}
((ModifiableElement)element).setParent(null);
}
@Override
public void removeChildren(Collection<Element> toRemove) {
if(children == null) {
return ;
}
Iterator<Element> childrenIterator = toRemove.iterator();
while(childrenIterator.hasNext()) {
Element child = childrenIterator.next();
((ModifiableElement)child).setParent(null);
childrenIterator.remove();
}
if(children().isEmpty()) {
children = null;
}
}
@Override
public void insertChildBefore(Element toInsert, Element element) {
int index = children.indexOf(element);
if(index == -1) {
return ;
}
children.add(index, toInsert);
((ModifiableElement)toInsert).setParent(this);
}
@Override
public Collection<Element> children() {
return children == null ? Collections.<Element>emptyList() : children;
}
@Override
public Collection<Element> children(ElementType type) {
Collection<Element> filtered = new ArrayList<Element>();
for (Element e : children()) {
if (e.type() == type) {
filtered.add(e);
}
}
return filtered;
}
@Override
public Collection<Element> children(ElementFilter filter) {
Collection<Element> filtered = new ArrayList<Element>();
for (Element e : children()) {
if (filter.accepts(e)) {
filtered.add(e);
}
}
return filtered;
}
@Override
public <T extends Element> Collection<T> children(Class<T> type) {
Collection<T> filtered = new ArrayList<T>();
for (Element child : children()) {
if (type.isAssignableFrom(child.getClass())) {
filtered.add(type.cast(child));
}
}
return filtered;
}
@Override
public void setSemanticEndOffset(int endOffset) {
this.logicalEndOffset = endOffset;
}
@Override
public int semanticEnd() {
return logicalEndOffset;
}
}
static class CommonCloseTag extends TagElement implements ModifiableCloseTag {
private OpenTag matchingOpenTag;
public CommonCloseTag(CharSequence source, int startOffset, int endOffset, byte nameLen) {
super(source, startOffset, endOffset, nameLen);
}
@Override
public ElementType type() {
return ElementType.CLOSE_TAG;
}
@Override
public OpenTag matchingOpenTag() {
return matchingOpenTag;
}
@Override
public void setMatchingOpenTag(OpenTag openTag) {
this.matchingOpenTag = openTag;
}
@Override
protected int nameOffsetToStartOffsetDiff() {
return 2; //"</".length();
}
}
static class VirtualOpenTag implements OpenTag, ModifiableOpenTag {
private Collection<Attribute> attrs;
private CharSequence name;
private List<Element> children;
private CloseTag matchingCloseTag;
private int logicalEndOffset;
private Node parent;
public VirtualOpenTag(CharSequence name) {
this.name = name;
}
void addAttribute(Attribute attr) {
if(attrs == null) {
attrs = new ArrayList<Attribute>(1);
}
attrs.add(attr);
}
void addAttributes(Collection<Attribute> attributes) {
if(attrs == null) {
attrs = new ArrayList<Attribute>(1);
}
attrs.addAll(attributes);
}
@Override
public Collection<Attribute> attributes() {
return attrs == null ? Collections.<Attribute>emptyList() : attrs;
}
@Override
public Collection<Attribute> attributes(AttributeFilter filter) {
Collection<Attribute> filtered = new ArrayList<Attribute>(1);
for (Attribute a : attributes()) {
if (filter.accepts(a)) {
filtered.add(a);
}
}
return filtered;
}
@Override
public Attribute getAttribute(String name) {
//typically very low number of attrs so the linear search doesn't hurt
for (Attribute a : attributes()) {
if (LexerUtils.equals(name, a.name(), true, false)) {
return a;
}
}
return null;
}
@Override
public boolean isEmpty() {
return false;
}
@Override
public CloseTag matchingCloseTag() {
return matchingCloseTag;
}
@Override
public int semanticEnd() {
return logicalEndOffset;
}
@Override
public CharSequence name() {
return name;
}
@Override
public CharSequence namespacePrefix() {
int colonIndex = CharSequences.indexOf(name(), ":");
return colonIndex == -1 ? null : name().subSequence(0, colonIndex);
}
@Override
public CharSequence unqualifiedName() {
int colonIndex = CharSequences.indexOf(name(), ":");
return colonIndex == -1 ? name() : name().subSequence(colonIndex + 1, name().length());
}
@Override
public int from() {
return -1;
}
@Override
public int to() {
return -1;
}
@Override
public ElementType type() {
return ElementType.OPEN_TAG;
}
@Override
public CharSequence image() {
return null;
}
@Override
public CharSequence id() {
return name();
}
@Override
public Collection<ProblemDescription> problems() {
return Collections.emptyList();
}
@Override
public Node parent() {
return parent;
}
@Override
public Collection<Element> children() {
return children == null ? Collections.<Element>emptyList() : children;
}
@Override
public Collection<Element> children(ElementType type) {
Collection<Element> filtered = new ArrayList<Element>();
for (Element e : children()) {
if (e.type() == type) {
filtered.add(e);
}
}
return filtered;
}
@Override
public Collection<Element> children(ElementFilter filter) {
Collection<Element> filtered = new ArrayList<Element>();
for (Element e : children()) {
if (filter.accepts(e)) {
filtered.add(e);
}
}
return filtered;
}
@Override
public <T extends Element> Collection<T> children(Class<T> type) {
Collection<T> filtered = new ArrayList<T>();
for (Element child : children()) {
if (type.isAssignableFrom(child.getClass())) {
filtered.add(type.cast(child));
}
}
return filtered;
}
@Override
public void insertChildBefore(Element toInsert, Element element) {
if(children == null) {
children = new ArrayList<Element>();
}
int index = children.indexOf(element);
if(index == -1) {
return ;
}
children.add(index, toInsert);
((ModifiableElement)toInsert).setParent(this);
}
@Override
public void setParent(Node parent) {
this.parent = parent;
}
@Override
public void addChild(Element child) {
if (children == null) {
children = new ArrayList<Element>(1);
}
children.add(child);
((ModifiableElement)child).setParent(this);
}
@Override
public void addChildren(Collection<Element> elements) {
for(Element e : new LinkedList<Element>(elements)) {
addChild(e);
}
}
@Override
public void removeChild(Element element) {
if (children == null) {
return;
}
children.remove(element);
if (children.isEmpty()) {
children = null;
}
((ModifiableElement)element).setParent(null);
}
@Override
public void removeChildren(Collection<Element> toRemove) {
if(children == null) {
return ;
}
Iterator<Element> childrenIterator = toRemove.iterator();
while(childrenIterator.hasNext()) {
Element child = childrenIterator.next();
((ModifiableElement)child).setParent(null);
childrenIterator.remove();
}
if(children().isEmpty()) {
children = null;
}
}
@Override
public void setEndOffset(int endOffset) {
//no-op
}
@Override
public void detachFromParent() {
ModifiableOpenTag mot = (ModifiableOpenTag) parent();
if(mot != null) {
mot.removeChild(this);
setParent(null);
}
}
@Override
public void setSemanticEndOffset(int endOffset) {
this.logicalEndOffset = endOffset;
}
@Override
public void setAttribute(Attribute attribute) {
addAttribute(attribute);
}
@Override
public void setMatchingCloseTag(CloseTag closeTag) {
this.matchingCloseTag = closeTag;
}
@Override
public String toString() {
return new StringBuilder().append(name()).append("(").append(type().name()).append(")").append("; (virtual)").toString();
}
}
//it would be better design/memory usage not to reuse commonopentag, but since there's just
//one instance some of the spare fields are acceptable
static class Root extends CommonOpenTag implements FeaturedNode {
private static final String ROOT = "root"; //NOI18N
private String namespace;
public Root(CharSequence source, String namespace) {
super(source, 0, source.length(), (byte) 0);
this.namespace = namespace;
}
@Override
public CharSequence name() {
return ROOT;
}
//must be overridden since the TagElement class uses 'from' - 'to' diff in short value,
@Override
public int to() {
return source.length();
}
@Override
public ElementType type() {
return ElementType.ROOT;
}
@Override
public Object getProperty(String propertyName) {
if (propertyName.equalsIgnoreCase("namespace")) { //NOI18N
return namespace;
}
return null;
}
}
static class CommonAttribute implements Attribute {
private CharSequence source;
private int nameOffset;
private short valueOffset2nameOffsetDiff;
private short nameLen;
private int valueLen;
public CommonAttribute(CharSequence source, int nameOffset, short nameLen) {
this.source = source;
this.nameOffset = nameOffset;
this.valueOffset2nameOffsetDiff = -1;
this.nameLen = nameLen;
this.valueLen = -1;
}
public CommonAttribute(CharSequence source, int nameOffset, int valueOffset, short nameLen, int valueLen) {
this.source = source;
this.nameOffset = nameOffset;
this.valueOffset2nameOffsetDiff = (short) (valueOffset - nameOffset);
this.nameLen = nameLen;
this.valueLen = valueLen;
}
@Override
public int nameOffset() {
return nameOffset;
}
@Override
public CharSequence name() {
return source.subSequence(nameOffset, nameOffset + nameLen);
}
@Override
public int valueOffset() {
return valueOffset2nameOffsetDiff == -1 ? -1 : nameOffset + valueOffset2nameOffsetDiff;
}
@Override
public CharSequence value() {
if(valueLen == -1) {
return null;
}
//Bug 221821 - StringIndexOutOfBoundsException: String index out of range: 234
int diff = source.length() - valueOffset();
if(diff < 0) {
assert false : String.format("valueOffset:%s > source.length:%s",
valueOffset(), source.length()); //report in dev builds
return null;
}
if(diff < valueLen) {
assert false : String.format("valueOffset:%s + valueLen:%s > source.length:%s",
valueOffset(), valueLen, source.length()); //report in dev builds
return null;
}
return source.subSequence(valueOffset(), valueOffset() + valueLen);
}
@Override
public boolean isValueQuoted() {
if(value() == null) {
return false;
}
if (valueLen < 2) {
return false;
} else {
CharSequence value = value();
return ((value.charAt(0) == '\'' || value.charAt(0) == '"')
&& (value.charAt(value.length() - 1) == '\'' || value.charAt(value.length() - 1) == '"'));
}
}
@Override
public CharSequence unquotedValue() {
if(value() == null) {
return null;
}
return isValueQuoted() ? value().subSequence(1, value().length() - 1) : value();
}
@Override
public CharSequence namespacePrefix() {
int colonIndex = CharSequences.indexOf(name(), ":");
return colonIndex == -1 ? null : name().subSequence(0, colonIndex);
}
@Override
public CharSequence unqualifiedName() {
int colonIndex = CharSequences.indexOf(name(), ":");
return colonIndex == -1 ? name() : name().subSequence(colonIndex + 1, name().length());
}
@Override
public int from() {
return nameOffset();
}
@Override
public int to() {
return value() != null
? valueOffset() + valueLen
: nameOffset() + nameLen;
}
@Override
public ElementType type() {
return ElementType.ATTRIBUTE;
}
@Override
public CharSequence image() {
return source.subSequence(from(), to());
}
@Override
public CharSequence id() {
return type().name();
}
@Override
public Collection<ProblemDescription> problems() {
return Collections.emptyList();
}
@Override
public Node parent() {
return null;
}
}
}