All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fitnesse.wikitext.parser.Headings Maven / Gradle / Ivy

There is a newer version: 20181217
Show newest version
package fitnesse.wikitext.parser;

import fitnesse.html.HtmlElement;
import fitnesse.html.HtmlTag;
import fitnesse.html.HtmlUtil;

import java.util.Arrays;
import java.util.List;
import java.util.Stack;

/**
 * Generates a ordered list of all headers from within the current wiki page.
 */
public class Headings extends SymbolType implements Rule, Translation {

  public static final Headings symbolType = new Headings();

  private static final String STYLE = "STYLE";
  private static final String[] OPTION_KEYS = new String[]{STYLE};

  public Headings() {
    super("Headings");
    wikiMatcher(new Matcher().startLineOrCell().string("!headings"));
    wikiRule(this);
    htmlTranslation(this);
  }

  @Override
  public Maybe parse(Symbol current, Parser parser) {
    final Symbol body = parser.parseToEnd(SymbolType.Newline);
    new OptionParser(current, body).parse();
    current.add(body);
    return new Maybe<>(current);
  }

  @Override
  public String toTarget(Translator translator, Symbol current) {
    final List headerLines = extractHeaderLines(translator);
    HeadingContentBuilder headingContentBuilder = new HeadingContentBuilder(headerLines,
      ListStyle.byNameIgnoreCase(current.getProperty(STYLE)));
    HtmlElement html = headingContentBuilder.htmlElements();
    return html.html();
  }

  private List extractHeaderLines(final Translator translator) {
    HtmlTranslator htmlTranslator = (HtmlTranslator) translator;
    SourcePage sourcePage = htmlTranslator.getPage();
    return sourcePage.getSymbols(HeaderLine.symbolType);
  }

  /**
   * Allowed values of an ordered list from CSS.
   */
  public enum ListStyle {

    DECIMAL("decimal"), DECIMAL_LEADING_ZERO("decimal-leading-zero"), LOWER_ROMAN("lower-roman"),
    UPPER_ROMAN("upper-roman"), LOWER_ALPHA("lower-alpha"), UPPER_ALPHA("upper-alpha"),
    NONE("none");

    private final String name;

    ListStyle(final String name) {
      this.name = name;
    }

    static ListStyle byNameIgnoreCase(String name) {
      for (final ListStyle listStyle : values()) {
        if (listStyle.name.equalsIgnoreCase(name)) {
          return listStyle;
        }
      }
      return DECIMAL;
    }

  }

  class HeadingContentBuilder {

    private final List headerLines;
    private final Stack stack = new Stack<>();
    private ListStyle listStyle = ListStyle.DECIMAL;
    private HtmlTag rootElement = null;
    private boolean processed;

    HeadingContentBuilder(final List headerLines, final ListStyle listStyle) {
      this.headerLines = headerLines;
      this.listStyle = listStyle;
      rootElement = new HtmlTag("div");
      rootElement.addAttribute("class", "contents");
      rootElement.add(HtmlUtil.makeBold("Contents:"));
      stack.push(rootElement);
    }

    HtmlElement htmlElements() {
      for (final Symbol headerLine : headerLines) {
        processed = false;
        htmlElements(headerLine);
      }
      return rootElement;
    }

    private void htmlElements(final Symbol headerLine) {
      addListElement(headerLine);
      goToParent(headerLine);
      addListItemElement(headerLine);
      if (!processed) {
        htmlElements(headerLine);
      }
    }

    private void addListElement(final Symbol headerLine) {
      if (getLevel(headerLine) > currentLevel()) {
        HtmlTag listElement = new HtmlTag("ol");
        listElement.addAttribute("style", "list-style-type: " + listStyle.name + ";");
        stack.peek().add(listElement);
        stack.push(listElement);
      }
    }

    private void goToParent(final Symbol headerLine) {
      if (getLevel(headerLine) < currentLevel()) {
        stack.pop();
      }
    }

    private void addListItemElement(final Symbol headerLine) {
      if (getLevel(headerLine) == currentLevel()) {
        final HtmlTag listitemElement = new HtmlTag("li");
        listitemElement.addAttribute("class", "heading" + currentLevel());
        final String textFromHeaderLine = extractTextFromHeaderLine(headerLine);
        final HtmlTag anchorElement = new HtmlTag("a", textFromHeaderLine);
        anchorElement.addAttribute("href",
          "#" + HtmlUtil.remainRfc3986UnreservedCharacters(textFromHeaderLine));
        listitemElement.add(anchorElement);
        stack.peek().add(listitemElement);
        processed = true;
      }
    }

    private int currentLevel() {
      return stack.size() - 1;
    }

    private int getLevel(final Symbol headerLine) {
      return Integer.parseInt(headerLine.getProperty(LineRule.Level));
    }

    private String extractTextFromHeaderLine(final Symbol headerLine) {
      final StringBuilder sb = new StringBuilder();
      headerLine.walkPreOrder(new SymbolTreeWalker() {
        @Override
        public boolean visit(final Symbol node) {
          if (node.isType(SymbolType.Text) || node.isType(Literal.symbolType) ||
            node.isType(Whitespace)) {
            sb.append(node.getContent());
          }
          return true;
        }

        @Override
        public boolean visitChildren(final Symbol node) {
          return true;
        }
      });
      return sb.toString();
    }

  }

  class OptionParser {

    private final Symbol current;
    private final Symbol body;

    private String previousOption = null;

    OptionParser(final Symbol current, final Symbol body) {
      this.current = current;
      this.body = body;
    }

    void parse() {
      for (final Symbol option : body.getChildren()) {
        handleSymbol(option);
      }
      finishSymbols();
    }

    private void handleSymbol(final Symbol option) {
      if (!option.isType(SymbolType.Whitespace)) {
        handleNonWhitespace(option);
      }
    }

    private void finishSymbols() {
      handleOptionAsValue(null);
    }

    private void handleNonWhitespace(final Symbol symbol) {
      String option = symbol.getContent();
      if (isOptionAKey(option)) {
        handleOptionAsKeyCandidate(option);
      } else {
        handleOptionAsValue(option);
      }
    }

    private void handleOptionAsValue(final String option) {
      if (isOptionAKey(previousOption)) {
        addToOptions(option);
      }
      previousOption = null;
    }

    private void handleOptionAsKeyCandidate(final String option) {
      if (isOptionAKey(previousOption)) {
        addToOptions(null);
      }
      previousOption = option;
    }

    private boolean isOptionAKey(final String candidate) {
      return candidate != null
        && candidate.startsWith("-")
        && Arrays.asList(OPTION_KEYS).contains(normalizeOptionKey(candidate));
    }

    private String normalizeOptionKey(final String candidate) {
      return candidate.substring(1).toUpperCase();
    }

    private void addToOptions(final String optionValue) {
      current.putProperty(normalizeOptionKey(previousOption), optionValue);
    }

  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy