MuseParser.java

package oqube.muse;
import java.util.List;
import java.util.ArrayList;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.Iterator;
import fr.lifl.parsing.ParserListenerDelegate;
import java.io.Reader;
import fr.lifl.parsing.ParserConfiguration;
import fr.lifl.parsing.ParserListener;
import java.io.BufferedReader;
import java.util.Stack;
import fr.lifl.parsing.ParserException;
import fr.lifl.parsing.Parser;
import fr.lifl.parsing.ParserPosition;
import fr.lifl.parsing.Namespace;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
 * Muse parser implementation. Current implementation is closed but future
 * evolutions shall allow some customization of parsing process.
 * 
 * @author abailly@oqube.muse.com
 * @version $Id$
 */
public class MuseParser implements Parser {
  private static final String EOL = System.getProperty("line.separator");
  /* delegate handling of listeners */
  private ParserListenerDelegate delegate = new ParserListenerDelegate();
  /* character stream to use */
  private Reader reader;
  /* buffer storing pre-parsed data */
  private StringBuffer tagContent;
  /*
   * hold current tag name in tag mode. This allows nesting of tags inside tags
   */
  private String currentTag;
  private String[][] currentArgs;
  public String getCurrentTag() {
    return currentTag;
  }
  public void setCurrentTag(String currentTag, String[][] nv) {
    this.currentTag = currentTag;
    this.currentArgs = nv;
  }
  public Reader getReader() {
    return reader;
  }
  /* debug mode */
  private boolean debug = false;
  /* log instance */
  private static Log log = LogFactory.getLog(MuseParser.class);
  public static Log getLog() {
    return log;
  }
  public static void setLog(Log log) {
    MuseParser.log = log;
  }
  /* current state - always equal to top of stack */
  private State state;
  /* stacked states */
  private Stack /* < State > */states = new Stack();
  /* current sink */
  private MuseSink sink;
  public MuseSink getSink() {
    return sink;
  }
  public void setSink(MuseSink sink) {
    this.sink = strong;
    strong.setWrappedSink(sink);
  }
  /*
   * push state/col pair as new state
   */
  private State push(int st, int col) {
    states.push(state);
    if (state != null) {
      StringBuffer sb = new StringBuffer();
      for (int i = 0; i < col; i++)
        sb.append(' ');
      sb.append(state.state).append(" > ").append(st);
      if (log.isDebugEnabled())
        log.debug(sb);
    }
    return state = new State(st, col);
  }
  /*
   * Return old state
   */
  private State pop() {
    State s = state;
    state = (State) states.pop();
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < state.col; i++)
      sb.append(' ');
    sb.append(state.state).append(" < ").append(s.state);
    if (log.isDebugEnabled()) {
      log.debug(sb);
    }
    return s;
  }
  /* current position - for error messages and reporting */
  private ParserPosition pos;
  public ParserPosition getPos() {
    return pos;
  }
  public void setPos(ParserPosition pos) {
    this.pos = pos;
  }
  /**
   * Handles paragraph liens
   */
  private AbstractLexer paralex = new AbstractLexer("^(\\S+.*)$") {
    /*
     * Rules for paragraph: - start paragraph if not in paragraph. First end
     * enclosing env - continue paragraph if in paragraph
     * 
     */
    public void handler() {
      switch (state.state) {
      case para:
        sink.text(matcher.group(1));
        break;
      case top:
        /* start new paragraph */
        sink.startPara();
        sink.text(matcher.group(1));
        push(para, 0);
        break;
      case tab:
        pop();
        handler();
        break;
      case item:
        pop();
        sink.endItem();
        handler();
        break;
      case list:
        pop();
        sink.endList();
        handler();
        break;
      case enums:
        pop();
        sink.endEnums();
        handler();
        break;
      case quote:
        pop();
        sink.endQuote();
        handler();
        break;
      case table:
        pop();
        sink.endTable();
        handler();
        break;
      case center:
        pop();
        sink.endCenter();
        handler();
        break;
      case head:
        pop();
        sink.endHeader();
        sink.startBody();
        handler();
        break;
      case tag:
        tagContent.append(matcher.group(0)).append(EOL);
        return;
      default:
        throw new ParserException("Invalid paragraph line " + matcher.group(1)
            + " @(" + pos + ")");
      }
    }
  };
  abstract class ItemLexer extends AbstractLexer {
    ItemLexer(String pat) {
      super(pat);
    }
    protected void handle(int type) {
      if (state.state == tag) {
        tagContent.append(matcher.group(0)).append(EOL);
        return;
      }
      int c = matcher.group(1).length();
      if (> state.col) {
        // new sub list
        push(type, c);
        push(item, c);
        if (type == list)
          sink.startList();
        else if (type == enums)
          sink.startEnums();
        // ask for item content to be aligned with first line
        c += matcher.group(2).length();
        push(tab, c);
        sink.startItem();
        sink.text(matcher.group(3));
      } else if (< state.col) {
        State s = pop(); // old state
        switch (s.state) {
        case para:
          sink.endPara();
          break;
        case head:
          sink.endHeader();
          sink.startBody();
          break;
        case tab:
          break;
        case item:
          sink.endItem();
          break;
        case list:
          sink.endList();
          break;
        case enums:
          sink.endEnums();
          break;
        case quote:
          sink.endQuote();
          break;
        case center:
          sink.endCenter();
        case table:
          sink.endTable();
          break;
        default:
          throw new ParserException("invalid indentation of list item "
              + matcher.group(0) + " @(" + pos + ")");
        }
        // recurse
        handler();
      } else {
        assert c == state.col;
        while (state.state != item) {
          switch (state.state) {
          case tab:
            pop();
            handler();
            return;
          default:
            throw new ParserException("Invalid indentation of list item "
                + matcher.group(0) + " at " + pos
                + ": should be at least one space further right");
          }
        }
        sink.endItem();
        c += matcher.group(2).length();
        push(tab, c);
        sink.startItem();
        sink.text(matcher.group(3));
      }
    }
  };
  /* handle unordered lists items */
  private AbstractLexer listlex = new ItemLexer("(\\s+)(-\\s+)(.*)") {
    public void handler() {
      handle(list);
    }
  };
  /* handle ordered lists items */
  private AbstractLexer enumlex = new ItemLexer("(\\s+)(\\d+\\.\\s+)(.*)") {
    public void handler() {
      handle(enums);
    }
  };
  /* handle lines starting with blanks */
  private AbstractLexer blanklex = new AbstractLexer("(\\s+)(\\S.*)") {
    public void handler() {
      // count blanks
      int ws = matcher.group(1).length();
      switch (state.state) {
      case top:
      case item:
      case para:
      case tab:
        if (ws >= 6 + state.col) {// center
          sink.startCenter();
          push(center, ws);
        } else if (ws > state.col) {
          sink.startQuote();
          push(quote, ws);
        }
        if (ws < state.col)
          throw new ParserException("Incorrect indentation line "
              + matcher.group(1) + "in state " + state + " @(" + pos + ")");
        sink.text(matcher.group(2));
        break;
      case quote:
      case center:
        if (ws < state.col)
          throw new ParserException("Incorrect indentation line "
              + matcher.group(1) + "in state " + state + " @(" + pos + ")");
        sink.text(matcher.group(2));
        break;
      case head:
        pop();
        sink.endHeader();
        sink.startBody();
        handler();
        break;
      case tag:
        tagContent.append(matcher.group(0)).append(EOL);
        return;
      default:
        throw new ParserException("Unexpected whitespace starting line "
            + matcher.group(1) + "in state " + state + " @(" + pos + ")");
      }
    }
  };
  /*
   * handle empty lines Empty liens terminate current block(s) and return parser
   * to toplevel state
   */
  private AbstractLexer emptylex = new AbstractLexer("^\\s*$") {
    public void handler() {
      while (state.state != top) {
        assert !states.isEmpty();
        // update state
        switch (state.state) {
        case para:
          sink.endPara();
          break;
        case list:
          sink.endList();
          break;
        case quote:
          sink.endQuote();
          break;
        case center:
          sink.endCenter();
          break;
        case enums:
          sink.endEnums();
          break;
        case item:
          sink.endItem();
          break;
        case head:
          sink.endHeader();
          sink.startBody();
          break;
        case tab: // NOP
          break;
        case table:
          sink.endTable();
          break;
        case tag:
          tagContent.append(EOL).append(EOL);
          return;
        default:
          throw new ParserException("Unexpected empty line in state " + state
              + " @" + pos + "");
        }
        pop();
      }
      assert state.state == top;
    }
  };
  /* handle headers */
  private AbstractLexer headerlex = new AbstractLexer("^(\\*+)\\s+(\\S+.*)$") {
    public void handler() {
      switch (state.state) {
      case head:
        pop();
        sink.endHeader();
        sink.startBody();
        break;
      case tag:
        tagContent.append(matcher.group(0)).append(EOL);
        return;
      }
      if (state.state != top)
        throw new ParserException("Cannot use header inside blocks: "
            + matcher.group(0) + " in state " + state.state + " @" + pos + "");
      /* count levels */
      int lvl = matcher.group(1