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

com.helger.html.markdown.Emitter Maven / Gradle / Ivy

/**
 * Copyright (C) 2014-2016 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * 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.helger.html.markdown;

import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.Nonnull;

import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.ext.CommonsArrayList;
import com.helger.commons.collection.ext.CommonsHashMap;
import com.helger.commons.collection.ext.ICommonsList;
import com.helger.commons.collection.ext.ICommonsMap;
import com.helger.commons.regex.RegExCache;
import com.helger.commons.string.StringHelper;
import com.helger.commons.url.ISimpleURL;
import com.helger.commons.url.SimpleURL;
import com.helger.html.entity.EHTMLEntity;
import com.helger.html.entity.HTMLEntity;
import com.helger.html.hc.ext.HCExtHelper;
import com.helger.html.hc.html.IHCElement;
import com.helger.html.hc.html.IHCElementWithChildren;
import com.helger.html.hc.html.embedded.HCImg;
import com.helger.html.hc.html.grouping.HCLI;
import com.helger.html.hc.html.textlevel.HCA;
import com.helger.html.hc.html.textlevel.HCAbbr;
import com.helger.html.hc.html.textlevel.HCCode;
import com.helger.html.hc.impl.HCCommentNode;
import com.helger.html.hc.impl.HCDOMWrapper;
import com.helger.html.hc.impl.HCEntityNode;
import com.helger.xml.microdom.IMicroDocument;
import com.helger.xml.microdom.IMicroElement;
import com.helger.xml.microdom.serialize.MicroReader;
import com.helger.xml.serialize.write.XMLEmitter;

/**
 * Emitter class responsible for generating HTML output.
 *
 * @author René Jeschke <[email protected]>
 */
final class Emitter
{
  /** Link references. */
  private final ICommonsMap  m_aLinkRefs = new CommonsHashMap <> ();
  /** The configuration. */
  private final MarkdownConfiguration m_aConfig;
  /** Extension flag. */
  private boolean m_bUseExtensions = false;
  /** Newline flag. */
  private boolean m_bConvertNewline2Br = false;
  /** Plugins references **/
  private final ICommonsMap  m_aPlugins = new CommonsHashMap <> ();

  /**
   * Constructor.
   *
   * @param aConfig
   *        config to use
   */
  public Emitter (@Nonnull final MarkdownConfiguration aConfig)
  {
    m_aConfig = aConfig;
    m_bUseExtensions = aConfig.isExtendedProfile ();
    m_bConvertNewline2Br = aConfig.isConvertNewline2Br ();
    for (final AbstractMarkdownPlugin plugin : aConfig.getAllPlugins ())
      register (plugin);
  }

  void setUseExtensions (final boolean bUseExtensions)
  {
    m_bUseExtensions = bUseExtensions;
  }

  public void register (@Nonnull final AbstractMarkdownPlugin plugin)
  {
    m_aPlugins.put (plugin.getPluginID (), plugin);
  }

  /**
   * Adds a LinkRef to this set of LinkRefs.
   *
   * @param key
   *        The key/id.
   * @param linkRef
   *        The LinkRef.
   */
  public void addLinkRef (@Nonnull final String key, final LinkRef linkRef)
  {
    m_aLinkRefs.put (key.toLowerCase (Locale.US), linkRef);
  }

  /**
   * Transforms the given block recursively into HTML.
   *
   * @param out
   *        The StringBuilder to write to.
   * @param aRoot
   *        The Block to process.
   */
  public void emit (final MarkdownHCStack out, final Block aRoot)
  {
    aRoot.removeSurroundingEmptyLines ();
    final IMarkdownDecorator aDecorator = m_aConfig.getDecorator ();

    switch (aRoot.m_eType)
    {
      case RULER:
        aDecorator.appendHorizontalRuler (out);
        return;
      case NONE:
      case XML:
      case XML_COMMENT:
        // No open required
        break;
      case HEADLINE:
        final IHCElementWithChildren  aHX = aDecorator.openHeadline (out, aRoot.m_nHeadlineDepth);
        if (m_bUseExtensions && aRoot.m_sId != null)
          aHX.setID (aRoot.m_sId);
        break;
      case PARAGRAPH:
        aDecorator.openParagraph (out);
        break;
      case CODE:
      case FENCED_CODE:
        if (m_aConfig.getCodeBlockEmitter () == null)
          aDecorator.openCodeBlock (out);
        break;
      case BLOCKQUOTE:
        aDecorator.openBlockquote (out);
        break;
      case UNORDERED_LIST:
        aDecorator.openUnorderedList (out);
        break;
      case ORDERED_LIST:
        aDecorator.openOrderedList (out);
        break;
      case LIST_ITEM:
        final HCLI aLI = aDecorator.openListItem (out);
        if (m_bUseExtensions && aRoot.m_sId != null)
          aLI.setID (aRoot.m_sId);
        break;
      default:
        break;
    }

    if (aRoot.hasLines ())
    {
      _emitLines (out, aRoot);
    }
    else
    {
      Block block = aRoot.m_aBlocks;
      while (block != null)
      {
        emit (out, block);
        block = block.m_aNext;
      }
    }

    switch (aRoot.m_eType)
    {
      case RULER:
      case NONE:
      case XML:
      case XML_COMMENT:
        break;
      case HEADLINE:
        aDecorator.closeHeadline (out, aRoot.m_nHeadlineDepth);
        break;
      case PARAGRAPH:
        aDecorator.closeParagraph (out);
        break;
      case CODE:
      case FENCED_CODE:
        if (m_aConfig.getCodeBlockEmitter () == null)
          aDecorator.closeCodeBlock (out);
        break;
      case BLOCKQUOTE:
        aDecorator.closeBlockquote (out);
        break;
      case UNORDERED_LIST:
        aDecorator.closeUnorderedList (out);
        break;
      case ORDERED_LIST:
        aDecorator.closeOrderedList (out);
        break;
      case LIST_ITEM:
        aDecorator.closeListItem (out);
        break;
      default:
        break;
    }
  }

  /**
   * Transforms lines into HTML.
   *
   * @param out
   *        The StringBuilder to write to.
   * @param block
   *        The Block to process.
   */
  private void _emitLines (final MarkdownHCStack out, final Block block)
  {
    switch (block.m_eType)
    {
      case CODE:
        _emitCodeLines (out, block.m_aLines, block.m_sMeta, true);
        break;
      case FENCED_CODE:
        _emitCodeLines (out, block.m_aLines, block.m_sMeta, false);
        break;
      case PLUGIN:
        emitPluginLines (out, block.m_aLines, block.m_sMeta);
        break;
      case XML:
        _emitXMLLines (out, block.m_aLines);
        break;
      case XML_COMMENT:
        _emitXMLComment (out, block.m_aLines);
        break;
      case PARAGRAPH:
        _emitMarkedLines (out, block.m_aLines);
        break;
      default:
        _emitMarkedLines (out, block.m_aLines);
        break;
    }
  }

  /**
   * Finds the position of the given Token in the given String.
   *
   * @param in
   *        The String to search on.
   * @param start
   *        The starting character position.
   * @param token
   *        The token to find.
   * @return The position of the token or -1 if none could be found.
   */
  private int _findInlineToken (final String in, final int start, final EMarkToken token)
  {
    int pos = start;
    while (pos < in.length ())
    {
      if (_getToken (in, pos) == token)
        return pos;
      pos++;
    }
    return -1;
  }

  /**
   * Checks if there is a valid markdown link definition.
   *
   * @param out
   *        The StringBuilder containing the generated output.
   * @param in
   *        Input String.
   * @param start
   *        Starting position.
   * @param token
   *        Either LINK or IMAGE.
   * @return The new position or -1 if there is no valid markdown link.
   */
  private int _checkInlineLink (final MarkdownHCStack out, final String in, final int start, final EMarkToken token)
  {
    boolean isAbbrev = false;
    int pos = start + (token == EMarkToken.LINK ? 1 : 2);
    final StringBuilder temp = new StringBuilder ();

    temp.setLength (0);
    pos = MarkdownHelper.readMdLinkId (temp, in, pos);
    if (pos < start)
      return -1;

    final String name = temp.toString ();
    String link = null, comment = null;
    final int oldPos = pos++;
    pos = MarkdownHelper.skipSpaces (in, pos);
    if (pos < start)
    {
      final LinkRef lr = m_aLinkRefs.get (name.toLowerCase (Locale.US));
      if (lr == null)
        return -1;
      isAbbrev = lr.isAbbrev ();
      link = lr.getLink ();
      comment = lr.getTitle ();
      pos = oldPos;
    }
    else
      if (in.charAt (pos) == '(')
      {
        pos++;
        pos = MarkdownHelper.skipSpaces (in, pos);
        if (pos < start)
          return -1;
        temp.setLength (0);
        final boolean useLt = in.charAt (pos) == '<';
        pos = useLt ? MarkdownHelper.readUntil (temp, in, pos + 1, '>') : MarkdownHelper.readMdLink (temp, in, pos);
        if (pos < start)
          return -1;
        if (useLt)
          pos++;
        link = temp.toString ();

        if (in.charAt (pos) == ' ')
        {
          pos = MarkdownHelper.skipSpaces (in, pos);
          if (pos > start && in.charAt (pos) == '"')
          {
            pos++;
            temp.setLength (0);
            pos = MarkdownHelper.readUntil (temp, in, pos, '"');
            if (pos < start)
              return -1;
            comment = temp.toString ();
            pos++;
            pos = MarkdownHelper.skipSpaces (in, pos);
            if (pos == -1)
              return -1;
          }
        }
        if (in.charAt (pos) != ')')
          return -1;
      }
      else
        if (in.charAt (pos) == '[')
        {
          pos++;
          temp.setLength (0);
          pos = MarkdownHelper.readRawUntil (temp, in, pos, ']');
          if (pos < start)
            return -1;
          final String id = temp.length () > 0 ? temp.toString () : name;
          final LinkRef lr = m_aLinkRefs.get (id.toLowerCase (Locale.US));
          if (lr != null)
          {
            link = lr.getLink ();
            comment = lr.getTitle ();
          }
        }
        else
        {
          final LinkRef lr = m_aLinkRefs.get (name.toLowerCase (Locale.US));
          if (lr == null)
            return -1;
          isAbbrev = lr.isAbbrev ();
          link = lr.getLink ();
          comment = lr.getTitle ();
          pos = oldPos;
        }

    if (link == null)
      return -1;

    if (token == EMarkToken.LINK)
    {
      if (isAbbrev && comment != null)
      {
        if (!m_bUseExtensions)
          return -1;
        out.push (new HCAbbr ().setTitle (comment));
        _recursiveEmitLine (out, name, 0, EMarkToken.NONE);
        out.pop ();
      }
      else
      {
        final HCA aLink = m_aConfig.getDecorator ().openLink (out);
        aLink.setHref (new SimpleURL (link));
        if (comment != null)
          aLink.setTitle (comment);
        _recursiveEmitLine (out, name, 0, EMarkToken.NONE);
        m_aConfig.getDecorator ().closeLink (out);
      }
    }
    else
    {
      final HCImg aImg = m_aConfig.getDecorator ().appendImage (out);
      aImg.setSrc (new SimpleURL (link));
      aImg.setAlt (name);
      if (comment != null)
        aImg.setTitle (comment);
    }

    return pos;
  }

  /**
   * Check if there is a valid HTML tag here. This method also transforms auto
   * links and mailto auto links.
   *
   * @param out
   *        The StringBuilder to write to.
   * @param in
   *        Input String.
   * @param nStart
   *        Starting position.
   * @return The new position or -1 if nothing valid has been found.
   */
  private int _checkInlineHtml (final MarkdownHCStack out, final String in, final int nStart)
  {
    final StringBuilder aTemp = new StringBuilder ();

    // Check for auto links
    aTemp.setLength (0);
    int nPos = MarkdownHelper.readUntil (aTemp, in, nStart + 1, ':', ' ', '>', '\n');
    if (nPos != -1 && in.charAt (nPos) == ':' && MarkdownHTML.isLinkPrefix (aTemp.toString ()))
    {
      nPos = MarkdownHelper.readUntil (aTemp, in, nPos, '>');
      if (nPos != -1)
      {
        final String sLink = aTemp.toString ();
        final HCA aLink = m_aConfig.getDecorator ().openLink (out);
        aLink.setHref (new SimpleURL (sLink)).addChild (sLink);
        m_aConfig.getDecorator ().closeLink (out);
        return nPos;
      }
    }

    // Check for mailto or address auto link
    aTemp.setLength (0);
    nPos = MarkdownHelper.readUntil (aTemp, in, nStart + 1, '@', ' ', '>', '\n');
    if (nPos != -1 && in.charAt (nPos) == '@')
    {
      nPos = MarkdownHelper.readUntil (aTemp, in, nPos, '>');
      if (nPos != -1)
      {
        final String sLink = aTemp.toString ();
        final HCA aLink = m_aConfig.getDecorator ().openLink (out);
        if (sLink.startsWith ("@"))
        {
          // address auto links
          final String sAddress = sLink.substring (1);
          final ISimpleURL aUrl = new SimpleURL ("https://maps.google.com/maps").add ("q", sAddress);
          aLink.setHref (aUrl).addChild (sAddress);
        }
        else
        {
          // mailto auto links
          aLink.setHref (new SimpleURL ("mailto:" + sLink)).addChild (sLink);
        }
        m_aConfig.getDecorator ().closeLink (out);
        return nPos;
      }
    }

    // Check for inline html
    if (nStart + 2 < in.length ())
    {
      nPos = nStart;
      if (nStart + 3 < in.length () &&
          in.charAt (nStart + 1) == '!' &&
          in.charAt (nStart + 2) == '-' &&
          in.charAt (nStart + 3) == '-')
      {
        nPos = nStart + 4;
        final int nCommentStartPos = nPos;
        while (true)
        {
          while (nPos < in.length () && in.charAt (nPos) != '-')
            nPos++;

          if (nPos == in.length ())
          {
            // FIXME End of line in comment
            return -1;
          }
          if (nPos + 2 < in.length () && in.charAt (nPos + 1) == '-' && in.charAt (nPos + 2) == '>')
          {
            // XML comment inline
            out.append (new HCCommentNode (in.substring (nCommentStartPos, nPos)));
            return nPos + 2;
          }
          nPos++;
        }
      }

      aTemp.setLength (0);
      final int t = MarkdownHelper.readXMLElement (aTemp, in, nStart, m_aConfig.isSafeMode ());
      if (t != -1)
      {
        final String sElement = aTemp.toString ();
        if (sElement.endsWith ("/>"))
        {
          // Self closed tag - can be parsed
          final IMicroDocument aXML = MicroReader.readMicroXML (sElement);
          if (aXML == null)
            throw new MarkdownException ("Failed to parse XML: " + sElement);
          // And use the root element
          out.append (new HCDOMWrapper (aXML.getDocumentElement ().detachFromParent ()));
        }
        else
          if (sElement.startsWith ("";
            final IMicroDocument aXML = MicroReader.readMicroXML (sParseCode);
            if (aXML == null)
              throw new MarkdownException ("Failed to parse XML: " + sParseCode);
            final IMicroElement eRoot = aXML.getDocumentElement ();

            // And use the root element
            final IHCElement  aHC = HCExtHelper.createHCElementFromName (eRoot.getTagName ());
            if (aHC == null)
              throw new MarkdownException ("Failed to get HC element: " + eRoot.getTagName ());

            // Clone all attributes
            eRoot.forAllAttributes (aAttr -> aHC.setCustomAttr (aAttr.getAttributeName (),
                                                                   aAttr.getAttributeValue ()));

            if (aHC.getElement ().mayBeSelfClosed ())
            {
              // e.g. 
out.append (aHC); } else { // Push out.push (aHC); } } return t - 1; } } return -1; } /** * Check if this is a valid XML/HTML entity. * * @param out * The StringBuilder to write to. * @param in * Input String. * @param start * Starting position * @return The new position or -1 if this entity in invalid. */ private static int _checkInlineEntity (final StringBuilder out, final String in, final int start) { final int pos = MarkdownHelper.readUntil (out, in, start, ';'); if (pos < 0 || out.length () < 3) return -1; if (out.charAt (1) == '#') { if (out.charAt (2) == 'x' || out.charAt (2) == 'X') { if (out.length () < 4) return -1; for (int i = 3; i < out.length (); i++) { final char c = out.charAt (i); if ((c < '0' || c > '9') && ((c < 'a' || c > 'f') && (c < 'A' || c > 'F'))) return -1; } } else { for (int i = 2; i < out.length (); i++) { final char c = out.charAt (i); if (c < '0' || c > '9') return -1; } } out.append (';'); } else { for (int i = 1; i < out.length (); i++) { final char c = out.charAt (i); if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z')) return -1; } out.append (';'); return EHTMLEntity.isValidEntityReference (out.toString ()) ? pos : -1; } return pos; } /** * Recursively scans through the given line, taking care of any markdown * stuff. * * @param out * The StringBuilder to write to. * @param in * Input String. * @param start * Start position. * @param token * The matching Token (for e.g. '*') * @return The position of the matching Token or -1 if token was NONE or no * Token could be found. */ private int _recursiveEmitLine (final MarkdownHCStack out, final String in, final int start, final EMarkToken token) { int pos = start; int a; int b; final MarkdownHCStack temp = new MarkdownHCStack (); final StringBuilder tempSB = new StringBuilder (); while (pos < in.length ()) { final EMarkToken mt = _getToken (in, pos); if (token != EMarkToken.NONE) if (mt == token || (token == EMarkToken.EM_STAR && mt == EMarkToken.STRONG_STAR) || (token == EMarkToken.EM_UNDERSCORE && mt == EMarkToken.STRONG_UNDERSCORE)) return pos; switch (mt) { case IMAGE: case LINK: b = _checkInlineLink (out, in, pos, mt); if (b > 0) { pos = b; } else { out.append (in.charAt (pos)); } break; case EM_STAR: case EM_UNDERSCORE: temp.reset (); b = _recursiveEmitLine (temp, in, pos + 1, mt); if (b > 0) { m_aConfig.getDecorator ().openEmphasis (out); out.append (temp.getRoot ()); m_aConfig.getDecorator ().closeEmphasis (out); pos = b; } else { out.append (in.charAt (pos)); } break; case STRONG_STAR: case STRONG_UNDERSCORE: temp.reset (); b = _recursiveEmitLine (temp, in, pos + 2, mt); if (b > 0) { m_aConfig.getDecorator ().openStrong (out); out.append (temp.getRoot ()); m_aConfig.getDecorator ().closeStrong (out); pos = b + 1; } else { out.append (in.charAt (pos)); } break; case STRIKE: temp.reset (); b = _recursiveEmitLine (temp, in, pos + 2, mt); if (b > 0) { m_aConfig.getDecorator ().openStrike (out); out.append (temp.getRoot ()); m_aConfig.getDecorator ().closeStrike (out); pos = b + 1; } else { out.append (in.charAt (pos)); } break; case SUPER: temp.reset (); b = _recursiveEmitLine (temp, in, pos + 1, mt); if (b > 0) { m_aConfig.getDecorator ().openSuper (out); out.append (temp.getRoot ()); m_aConfig.getDecorator ().closeSuper (out); pos = b; } else { out.append (in.charAt (pos)); } break; case CODE_SINGLE: case CODE_DOUBLE: a = pos + (mt == EMarkToken.CODE_DOUBLE ? 2 : 1); b = _findInlineToken (in, a, mt); if (b > 0) { pos = b + (mt == EMarkToken.CODE_DOUBLE ? 1 : 0); while (a < b && in.charAt (a) == ' ') a++; if (a < b) { while (in.charAt (b - 1) == ' ') b--; final HCCode aCode = m_aConfig.getDecorator ().openCodeSpan (out); aCode.addChild (in.substring (a, b)); m_aConfig.getDecorator ().closeCodeSpan (out); } } else { out.append (in.charAt (pos)); } break; case HTML: b = _checkInlineHtml (out, in, pos); if (b > 0) { pos = b; } else { out.append ('<'); } break; case ENTITY: tempSB.setLength (0); b = _checkInlineEntity (tempSB, in, pos); if (b > 0) { // Remove leading '&' and trailing ';' out.append (new HCEntityNode (new HTMLEntity (tempSB.substring (1, tempSB.length () - 1)), " ")); pos = b; } else { out.append ('&'); } break; case X_LINK_OPEN: temp.reset (); b = _recursiveEmitLine (temp, in, pos + 2, EMarkToken.X_LINK_CLOSE); if (b > 0 && m_aConfig.getSpecialLinkEmitter () != null) { m_aConfig.getSpecialLinkEmitter ().emitSpan (out, temp); pos = b + 1; } else { out.append (in.charAt (pos)); } break; case X_COPY: out.append (HCEntityNode.newCopy ()); pos += 2; break; case X_REG: out.append (new HCEntityNode (EHTMLEntity.copy, "(r)")); pos += 2; break; case X_TRADE: out.append (new HCEntityNode (EHTMLEntity.trade, "TM")); pos += 3; break; case X_NDASH: out.append (new HCEntityNode (EHTMLEntity.ndash, "--")); pos++; break; case X_MDASH: out.append (new HCEntityNode (EHTMLEntity.mdash, "---")); pos += 2; break; case X_HELLIP: out.append (new HCEntityNode (EHTMLEntity.hellip, "...")); pos += 2; break; case X_LAQUO: out.append (new HCEntityNode (EHTMLEntity.laquo, "<<")); pos++; break; case X_RAQUO: out.append (new HCEntityNode (EHTMLEntity.raquo, ">>")); pos++; break; case X_RDQUO: out.append (new HCEntityNode (EHTMLEntity.rdquo, "\"")); break; case X_LDQUO: out.append (new HCEntityNode (EHTMLEntity.ldquo, "\"")); break; case ESCAPE: pos++; out.append (in.charAt (pos)); break; default: out.append (in.charAt (pos)); break; } pos++; } return -1; } /** * Turns every whitespace character into a space character. * * @param c * Character to check * @return 32 is c was a whitespace, c otherwise */ private static char _whitespaceToSpace (final char c) { return Character.isWhitespace (c) ? ' ' : c; } /** * Check if there is any markdown Token. * * @param in * Input String. * @param pos * Starting position. * @return The Token. */ @Nonnull private EMarkToken _getToken (final String in, final int pos) { final char c0 = pos > 0 ? _whitespaceToSpace (in.charAt (pos - 1)) : ' '; final char c = _whitespaceToSpace (in.charAt (pos)); final char c1 = pos + 1 < in.length () ? _whitespaceToSpace (in.charAt (pos + 1)) : ' '; final char c2 = pos + 2 < in.length () ? _whitespaceToSpace (in.charAt (pos + 2)) : ' '; final char c3 = pos + 3 < in.length () ? _whitespaceToSpace (in.charAt (pos + 3)) : ' '; switch (c) { case '*': if (c1 == '*') { return c0 != ' ' || c2 != ' ' ? EMarkToken.STRONG_STAR : EMarkToken.EM_STAR; } return c0 != ' ' || c1 != ' ' ? EMarkToken.EM_STAR : EMarkToken.NONE; case '_': if (c1 == '_') { return c0 != ' ' || c2 != ' ' ? EMarkToken.STRONG_UNDERSCORE : EMarkToken.EM_UNDERSCORE; } if (m_bUseExtensions) { return Character.isLetterOrDigit (c0) && c0 != '_' && Character.isLetterOrDigit (c1) ? EMarkToken.NONE : EMarkToken.EM_UNDERSCORE; } return c0 != ' ' || c1 != ' ' ? EMarkToken.EM_UNDERSCORE : EMarkToken.NONE; case '~': if (m_bUseExtensions && c1 == '~') { return EMarkToken.STRIKE; } return EMarkToken.NONE; case '!': if (c1 == '[') return EMarkToken.IMAGE; return EMarkToken.NONE; case '[': if (m_bUseExtensions && c1 == '[') return EMarkToken.X_LINK_OPEN; return EMarkToken.LINK; case ']': if (m_bUseExtensions && c1 == ']') return EMarkToken.X_LINK_CLOSE; return EMarkToken.NONE; case '`': return c1 == '`' ? EMarkToken.CODE_DOUBLE : EMarkToken.CODE_SINGLE; case '\\': if (MarkdownHelper.isEscapeChar (c1)) return EMarkToken.ESCAPE; return EMarkToken.NONE; case '<': if (m_bUseExtensions && c1 == '<') return EMarkToken.X_LAQUO; return EMarkToken.HTML; case '&': return EMarkToken.ENTITY; default: if (m_bUseExtensions) { switch (c) { case '-': if (c1 == '-') return c2 == '-' ? EMarkToken.X_MDASH : EMarkToken.X_NDASH; break; case '^': return c0 == '^' || c1 == '^' ? EMarkToken.NONE : EMarkToken.SUPER; case '>': if (c1 == '>') return EMarkToken.X_RAQUO; break; case '.': if (c1 == '.' && c2 == '.') return EMarkToken.X_HELLIP; break; case '(': if (c1 == 'C' && c2 == ')') return EMarkToken.X_COPY; if (c1 == 'R' && c2 == ')') return EMarkToken.X_REG; if (c1 == 'T' & c2 == 'M' & c3 == ')') return EMarkToken.X_TRADE; break; case '"': if (!Character.isLetterOrDigit (c0) && c1 != ' ') return EMarkToken.X_LDQUO; if (c0 != ' ' && !Character.isLetterOrDigit (c1)) return EMarkToken.X_RDQUO; break; } } return EMarkToken.NONE; } } /** * Writes a set of markdown lines into the StringBuilder. * * @param out * The StringBuilder to write to. * @param lines * The lines to write. */ private void _emitMarkedLines (final MarkdownHCStack out, final Line lines) { final StringBuilder in = new StringBuilder (); Line line = lines; while (line != null) { if (!line.m_bIsEmpty) { in.append (line.m_sValue.substring (line.m_nLeading, line.m_sValue.length () - line.m_nTrailing)); if (line.m_nTrailing >= 2 && !m_bConvertNewline2Br) in.append ("
"); } if (line.m_aNext != null) { in.append ('\n'); if (m_bConvertNewline2Br) in.append ("
"); } line = line.m_aNext; } _recursiveEmitLine (out, in.toString (), 0, EMarkToken.NONE); } /** * Writes a set of raw lines into the StringBuilder. * * @param out * The StringBuilder to write to. * @param lines * The lines to write. */ private void _emitXMLLines (final MarkdownHCStack out, final Line lines) { Line line = lines; if (m_aConfig.isSafeMode ()) { final StringBuilder temp = new StringBuilder (); while (line != null) { if (!line.m_bIsEmpty) temp.append (line.m_sValue.trim ()); line = line.m_aNext; } final String in = temp.toString (); for (int pos = 0; pos < in.length (); pos++) { if (in.charAt (pos) == '<') { temp.setLength (0); final int t = MarkdownHelper.readXMLElement (temp, in, pos, m_aConfig.isSafeMode ()); if (t != -1) { // XXX Is this correct??? out.append (temp.toString ()); pos = t; } else { out.append (in.charAt (pos)); } } else { out.append (in.charAt (pos)); } } } else { final StringBuilder aXML = new StringBuilder (); int nLines = 0; while (line != null) { if (!line.m_bIsEmpty) { aXML.append (line.m_sValue.trim ()); ++nLines; } line = line.m_aNext; } String sXML = aXML.toString (); if (nLines == 1 && !sXML.contains ("/>") && !sXML.contains (" sXML = sXML.substring (0, sXML.length () - 1) + "/>"; } final IMicroDocument aDoc = MicroReader.readMicroXML (sXML); if (aDoc == null) throw new MarkdownException ("Failed to parse XML: " + sXML); out.append (new HCDOMWrapper (aDoc.getDocumentElement ().detachFromParent ())); } } private void _emitXMLComment (final MarkdownHCStack out, final Line lines) { Line line = lines; final StringBuilder aXML = new StringBuilder (); while (line != null) { if (!line.m_bIsEmpty) { // Append without trimming! aXML.append (line.m_sValue); } aXML.append ('\n'); line = line.m_aNext; } // Trim only once, so that newlines before or after a comment start/close is // removed final String sContent = StringHelper.trimStartAndEnd (aXML.toString ().trim (), XMLEmitter.COMMENT_START, XMLEmitter.COMMENT_END); out.append (new HCCommentNode (sContent)); } /** * Writes a code block into the StringBuilder. * * @param out * The StringBuilder to write to. * @param aLines * The lines to write. * @param meta * Meta information. */ private void _emitCodeLines (final MarkdownHCStack out, final Line aLines, @Nonnull final String meta, final boolean bRemoveIndent) { Line aLine = aLines; if (m_aConfig.getCodeBlockEmitter () != null) { final ICommonsList list = new CommonsArrayList <> (); while (aLine != null) { if (aLine.m_bIsEmpty) list.add (""); else list.add (bRemoveIndent ? aLine.m_sValue.substring (4) : aLine.m_sValue); aLine = aLine.m_aNext; } m_aConfig.getCodeBlockEmitter ().emitBlock (out, list, meta); } else { while (aLine != null) { if (!aLine.m_bIsEmpty) out.append (aLine.m_sValue.substring (4)); out.append ('\n'); aLine = aLine.m_aNext; } } } /** * interprets a plugin block into the StringBuilder. * * @param out * The StringBuilder to write to. * @param lines * The lines to write. * @param meta * Meta information. */ protected void emitPluginLines (final MarkdownHCStack out, final Line lines, @Nonnull final String meta) { Line line = lines; String idPlugin = meta; String sparams = null; ICommonsMap params = null; final int iow = meta.indexOf (' '); if (iow != -1) { idPlugin = meta.substring (0, iow); sparams = meta.substring (iow + 1); if (sparams != null) { params = parsePluginParams (sparams); } } if (params == null) { params = new CommonsHashMap <> (); } final ICommonsList list = new CommonsArrayList <> (); while (line != null) { if (line.m_bIsEmpty) list.add (""); else list.add (line.m_sValue); line = line.m_aNext; } final AbstractMarkdownPlugin plugin = m_aPlugins.get (idPlugin); if (plugin != null) { plugin.emit (out, list, params); } } @Nonnull @ReturnsMutableCopy protected ICommonsMap parsePluginParams (@Nonnull final String s) { final ICommonsMap ret = new CommonsHashMap <> (); final Pattern aPattern = RegExCache.getPattern ("(\\w+)=\"*((?<=\")[^\"]+(?=\")|([^\\s]+))\"*"); final Matcher aMatcher = aPattern.matcher (s); while (aMatcher.find ()) ret.put (aMatcher.group (1), aMatcher.group (2)); return ret; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy