blob: fc5361b11f5368acdf18d3797004049516fdfb2a [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* 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.
*/
package com.intellij.lang.html.structureView;
import com.intellij.ide.structureView.StructureViewTreeElement;
import com.intellij.openapi.util.Computable;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.SortedList;
import com.intellij.util.containers.Stack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedList;
// Algorithm described at http://www.w3.org/html/wg/drafts/html/master/sections.html#outlines
// One of implementations: http://hoyois.github.com/html5outliner/ (https://github.com/hoyois/html5outliner)
class Html5SectionsProcessor {
private static class SectionHolder {
private final XmlTag myTag;
private final LinkedList<Section> myChildren = new LinkedList<Section>();
private SectionHolder(final XmlTag tag) {
myTag = tag;
}
public void addChildSection(final Section section) {
myChildren.add(section);
}
public LinkedList<Section> getChildren() {
return myChildren;
}
public XmlTag getTag() {
return myTag;
}
}
private static class Section extends SectionHolder {
private Section myParent = null;
private XmlTag myHeader = null;
public Section(final XmlTag tag) {
super(tag);
}
@Override
public void addChildSection(final Section section) {
section.myParent = this;
super.addChildSection(section);
}
public XmlTag getHeader() {
return myHeader;
}
public void setHeader(final XmlTag header) {
myHeader = header;
}
public Section getParent() {
return myParent;
}
}
private static final String[] SECTIONING_ROOT_ELEMENTS = {"blockquote", "body", "details", "dialog", "fieldset", "figure", "td"};
private static final String[] SECTIONING_CONTENT_ELEMENTS = {"article", "aside", "nav", "section"};
private static final String[] HEADER_ELEMENTS = {"h1", "h2", "h3", "h4", "h5", "h6"};
private static final String HGROUP_ELEMENT = "hgroup";
private final Collection<SectionHolder> myRootSectionHolders = new SortedList<SectionHolder>(new Comparator<SectionHolder>() {
@Override
public int compare(final SectionHolder first, final SectionHolder second) {
return first.getTag().getTextRange().getStartOffset() - second.getTag().getTextRange().getStartOffset();
}
});
private SectionHolder myCurrentOutlinee = null;
private Section myCurrentSection = null;
private final Stack<SectionHolder> myStack = new Stack<SectionHolder>();
public static Collection<Html5SectionTreeElement> processAndGetRootSections(final XmlTag rootTag) {
final Html5SectionsProcessor processor = new Html5SectionsProcessor();
processRecursively(rootTag, processor);
return processor.getRootSections();
}
private static void processRecursively(final XmlTag tag, final Html5SectionsProcessor processor) {
if (tag.getAttribute("hidden") != null) return;
processor.tagEntered(tag);
if (!isHeader(tag)) {
for (final XmlTag subTag : tag.getSubTags()) {
processRecursively(subTag, processor);
}
}
processor.tagExited(tag);
}
private void tagEntered(final XmlTag tag) {
if (isSectioningContentElement(tag) || isSectioningRootElement(tag)) {
if (myCurrentOutlinee != null) {
myStack.push(myCurrentOutlinee);
}
myCurrentOutlinee = new SectionHolder(tag);
myCurrentSection = new Section(tag);
myCurrentOutlinee.addChildSection(myCurrentSection);
}
else if (myCurrentOutlinee == null) {
// do nothing
}
else if (isHeader(tag)) {
if (myCurrentSection.getHeader() == null) {
myCurrentSection.setHeader(tag);
}
else if (myCurrentOutlinee.getChildren().getLast().getHeader() == null ||
compareHeaderRanks(tag, myCurrentOutlinee.getChildren().getLast().getHeader()) >= 0) {
myCurrentSection = new Section(tag);
myCurrentSection.setHeader(tag);
myCurrentOutlinee.addChildSection(myCurrentSection);
}
else {
Section candidateSection = myCurrentSection;
do {
if (compareHeaderRanks(tag, candidateSection.getHeader()) < 0) {
myCurrentSection = new Section(tag);
myCurrentSection.setHeader(tag);
candidateSection.addChildSection(myCurrentSection);
break;
}
candidateSection = candidateSection.getParent();
}
while (true);
}
//myStack.push(); not needed, because our iterator doesn't enter hidden elements
}
}
private void tagExited(final XmlTag tag) {
if (!myStack.isEmpty() && myStack.peek().getTag() == tag) {
assert false;
}
else if (!myStack.isEmpty() && isHeader(tag)) {
// do nothing
}
else if (!myStack.isEmpty() && isSectioningContentElement(tag)) {
final SectionHolder exitedSectioningContent = myCurrentOutlinee;
assert exitedSectioningContent.getTag() == tag;
myCurrentOutlinee = myStack.pop();
myCurrentSection = myCurrentOutlinee.getChildren().getLast();
for (Section section : exitedSectioningContent.getChildren()) {
myCurrentSection.addChildSection(section);
}
}
else if (!myStack.isEmpty() && isSectioningRootElement(tag)) {
final SectionHolder exitedSectioningRoot = myCurrentOutlinee;
assert exitedSectioningRoot.getTag() == tag;
myRootSectionHolders.add(exitedSectioningRoot);
myCurrentOutlinee = myStack.pop();
myCurrentSection = myCurrentOutlinee.getChildren().getLast();
while (!myCurrentSection.getChildren().isEmpty()) {
myCurrentSection = myCurrentSection.getChildren().getLast();
}
}
else if (isSectioningContentElement(tag) || isSectioningRootElement(tag)) {
assert myStack.isEmpty();
assert myCurrentOutlinee.getTag() == tag;
myRootSectionHolders.add(myCurrentOutlinee);
// reset algorithm
myCurrentOutlinee = null;
myCurrentSection = null;
}
}
private Collection<Html5SectionTreeElement> getRootSections() {
final Collection<Html5SectionTreeElement> result = new ArrayList<Html5SectionTreeElement>();
for (SectionHolder sectionHolder : myRootSectionHolders) {
for (Section section : sectionHolder.getChildren()) {
result.add(createHtml5SectionTreeElement(section));
}
}
return result;
}
private static Html5SectionTreeElement createHtml5SectionTreeElement(final Section section) {
return new Html5SectionTreeElement(section.getTag(),
createChildrenComputable(section.getChildren()),
getHeaderText(section.getHeader()));
}
private static Computable<Collection<StructureViewTreeElement>> createChildrenComputable(final Collection<Section> children) {
return new Computable<Collection<StructureViewTreeElement>>() {
@Override
public Collection<StructureViewTreeElement> compute() {
final Collection<StructureViewTreeElement> result = new ArrayList<StructureViewTreeElement>();
for (Section section : children) {
result.add(createHtml5SectionTreeElement(section));
}
return result;
}
};
}
private static String getHeaderText(final @Nullable XmlTag header) {
if (header == null) return null;
final StringBuilder buf = new StringBuilder();
if (HGROUP_ELEMENT.equalsIgnoreCase(header.getLocalName())) {
for (XmlTag subTag : header.getSubTags()) {
if (ArrayUtil.contains(subTag.getLocalName().toLowerCase(), HEADER_ELEMENTS)) {
if (buf.length() > 0) {
buf.append(" ");
}
appendTextRecursively(subTag, buf, HtmlTagTreeElement.MAX_TEXT_LENGTH * 2);
}
}
}
else {
appendTextRecursively(header, buf, HtmlTagTreeElement.MAX_TEXT_LENGTH * 2);
}
return buf.toString();
}
private static void appendTextRecursively(final XmlTag tag, final StringBuilder buf, final int maximumTextLength) {
if (buf.length() >= maximumTextLength) return;
final String text = tag.getValue().getTrimmedText();
if (!text.isEmpty()) {
buf.append(text);
}
else {
for (XmlTag subTag : tag.getSubTags()) {
appendTextRecursively(subTag, buf, maximumTextLength);
}
}
}
private static boolean isSectioningRootElement(final XmlTag tag) {
return ArrayUtil.contains(tag.getLocalName().toLowerCase(), SECTIONING_ROOT_ELEMENTS);
}
private static boolean isSectioningContentElement(final XmlTag tag) {
return ArrayUtil.contains(tag.getLocalName().toLowerCase(), SECTIONING_CONTENT_ELEMENTS);
}
private static boolean isHeader(final XmlTag tag) {
return ArrayUtil.contains(tag.getLocalName().toLowerCase(), HEADER_ELEMENTS) || HGROUP_ELEMENT.equalsIgnoreCase(tag.getLocalName());
}
private static int compareHeaderRanks(final @NotNull XmlTag header1, final @NotNull XmlTag header2) {
return getHeaderRank(header2) - getHeaderRank(header1);
}
private static int getHeaderRank(final XmlTag header) {
if (HGROUP_ELEMENT.equalsIgnoreCase(header.getLocalName())) {
int minIndex = HEADER_ELEMENTS.length;
for (XmlTag subTag : header.getSubTags()) {
final int index = ArrayUtil.indexOf(HEADER_ELEMENTS, subTag.getLocalName().toLowerCase());
if (index < minIndex) {
minIndex = index;
if (minIndex == 0) break;
}
}
if (minIndex == HEADER_ELEMENTS.length) {
// no headers is equivalent to <h1>
minIndex = 0;
}
return minIndex + 1;
}
final int index = ArrayUtil.indexOf(HEADER_ELEMENTS, header.getLocalName().toLowerCase());
if (index < 0) throw new IllegalArgumentException(header.getName());
return index + 1;
}
}