Verzeichnisstruktur phpBB-3.2.0


Veröffentlicht
06.01.2017

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

      (Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

Parser.js

Zuletzt modifiziert: 09.10.2024, 12:56 - Dateigröße: 47.50 KiB


 0001  /**#@+
 0002  * Boolean rules bitfield
 0003  */
 0004  /** @const */ var RULE_AUTO_CLOSE        = 1 << 0;
 0005  /** @const */ var RULE_AUTO_REOPEN       = 1 << 1;
 0006  /** @const */ var RULE_BREAK_PARAGRAPH   = 1 << 2;
 0007  /** @const */ var RULE_CREATE_PARAGRAPHS = 1 << 3;
 0008  /** @const */ var RULE_DISABLE_AUTO_BR   = 1 << 4;
 0009  /** @const */ var RULE_ENABLE_AUTO_BR    = 1 << 5;
 0010  /** @const */ var RULE_IGNORE_TAGS       = 1 << 6;
 0011  /** @const */ var RULE_IGNORE_TEXT       = 1 << 7;
 0012  /** @const */ var RULE_IGNORE_WHITESPACE = 1 << 8;
 0013  /** @const */ var RULE_IS_TRANSPARENT    = 1 << 9;
 0014  /** @const */ var RULE_PREVENT_BR        = 1 << 10;
 0015  /** @const */ var RULE_SUSPEND_AUTO_BR   = 1 << 11;
 0016  /** @const */ var RULE_TRIM_FIRST_LINE   = 1 << 12;
 0017  /**#@-*/
 0018   
 0019  /**
 0020  * @const Bitwise disjunction of rules related to automatic line breaks
 0021  */
 0022  var RULES_AUTO_LINEBREAKS = RULE_DISABLE_AUTO_BR | RULE_ENABLE_AUTO_BR | RULE_SUSPEND_AUTO_BR;
 0023   
 0024  /**
 0025  * @const Bitwise disjunction of rules that are inherited by subcontexts
 0026  */
 0027  var RULES_INHERITANCE = RULE_ENABLE_AUTO_BR;
 0028   
 0029  /**
 0030  * @const All the characters that are considered whitespace
 0031  */
 0032  var WHITESPACE = " \n\t";
 0033   
 0034  /**
 0035  * @type {!Object.<string,!number>} Number of open tags for each tag name
 0036  */
 0037  var cntOpen;
 0038   
 0039  /**
 0040  * @type {!Object.<string,!number>} Number of times each tag has been used
 0041  */
 0042  var cntTotal;
 0043   
 0044  /**
 0045  * @type {!Object} Current context
 0046  */
 0047  var context;
 0048   
 0049  /**
 0050  * @type {!number} How hard the parser has worked on fixing bad markup so far
 0051  */
 0052  var currentFixingCost;
 0053   
 0054  /**
 0055  * @type {Tag} Current tag being processed
 0056  */
 0057  var currentTag;
 0058   
 0059  /**
 0060  * @type {!boolean} Whether the output contains "rich" tags, IOW any tag that is not <p> or <br/>
 0061  */
 0062  var isRich;
 0063   
 0064  /**
 0065  * @type {!Logger} This parser's logger
 0066  */
 0067  var logger = new Logger;
 0068   
 0069  /**
 0070  * @type {!number} How hard the parser should work on fixing bad markup
 0071  */
 0072  var maxFixingCost = 1000;
 0073   
 0074  /**
 0075  * @type {!Object} Associative array of namespace prefixes in use in document (prefixes used as key)
 0076  */
 0077  var namespaces;
 0078   
 0079  /**
 0080  * @type {!Array.<!Tag>} Stack of open tags (instances of Tag)
 0081  */
 0082  var openTags;
 0083   
 0084  /**
 0085  * @type {!string} This parser's output
 0086  */
 0087  var output;
 0088   
 0089  /**
 0090  * @type {!Object.<!Object>}
 0091  */
 0092  var plugins;
 0093   
 0094  /**
 0095  * @type {!number} Position of the cursor in the original text
 0096  */
 0097  var pos;
 0098   
 0099  /**
 0100  * @type {!Object} Variables registered for use in filters
 0101  */
 0102  var registeredVars;
 0103   
 0104  /**
 0105  * @type {!Object} Root context, used at the root of the document
 0106  */
 0107  var rootContext;
 0108   
 0109  /**
 0110  * @type {!Object} Tags' config
 0111  * @const
 0112  */
 0113  var tagsConfig;
 0114   
 0115  /**
 0116  * @type {!Array.<!Tag>} Tag storage
 0117  */
 0118  var tagStack;
 0119   
 0120  /**
 0121  * @type {!boolean} Whether the tags in the stack are sorted
 0122  */
 0123  var tagStackIsSorted;
 0124   
 0125  /**
 0126  * @type {!string} Text being parsed
 0127  */
 0128  var text;
 0129   
 0130  /**
 0131  * @type {!number} Length of the text being parsed
 0132  */
 0133  var textLen;
 0134   
 0135  /**
 0136  * @type {!number} Counter incremented everytime the parser is reset. Used to as a canary to detect
 0137  *                 whether the parser was reset during execution
 0138  */
 0139  var uid = 0;
 0140   
 0141  /**
 0142  * @type {!number} Position before which we output text verbatim, without paragraphs or linebreaks
 0143  */
 0144  var wsPos;
 0145   
 0146  //==========================================================================
 0147  // Public API
 0148  //==========================================================================
 0149   
 0150  /**
 0151  * Disable a tag
 0152  *
 0153  * @param {!string} tagName Name of the tag
 0154  */
 0155  function disableTag(tagName)
 0156  {
 0157      if (tagsConfig[tagName])
 0158      {
 0159          copyTagConfig(tagName).isDisabled = true;
 0160      }
 0161  }
 0162   
 0163  /**
 0164  * Enable a tag
 0165  *
 0166  * @param {!string} tagName Name of the tag
 0167  */
 0168  function enableTag(tagName)
 0169  {
 0170      if (tagsConfig[tagName])
 0171      {
 0172          copyTagConfig(tagName).isDisabled = false;
 0173      }
 0174  }
 0175   
 0176  /**
 0177  * Get this parser's Logger instance
 0178  *
 0179  * @return {!Logger}
 0180  */
 0181  function getLogger()
 0182  {
 0183      return logger;
 0184  }
 0185   
 0186  /**
 0187  * Parse a text
 0188  *
 0189  * @param  {!string} _text Text to parse
 0190  * @return {!string}       XML representation
 0191  */
 0192  function parse(_text)
 0193  {
 0194      // Reset the parser and save the uid
 0195      reset(_text);
 0196      var _uid = uid;
 0197   
 0198      // Do the heavy lifting
 0199      executePluginParsers();
 0200      processTags();
 0201   
 0202      // Finalize the document
 0203      finalizeOutput();
 0204   
 0205      // Check the uid in case a plugin or a filter reset the parser mid-execution
 0206      if (uid !== _uid)
 0207      {
 0208          throw 'The parser has been reset during execution';
 0209      }
 0210   
 0211      // Log a warning if the fixing cost limit was exceeded
 0212      if (currentFixingCost > maxFixingCost)
 0213      {
 0214          logger.warn('Fixing cost limit exceeded');
 0215      }
 0216   
 0217      return output;
 0218  }
 0219   
 0220  /**
 0221  * Reset the parser for a new parsing
 0222  *
 0223  * @param {!string} _text Text to be parsed
 0224  */
 0225  function reset(_text)
 0226  {
 0227      // Normalize CR/CRLF to LF, remove control characters that aren't allowed in XML
 0228      _text = _text.replace(/\r\n?/g, "\n");
 0229      _text = _text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]+/g, '');
 0230   
 0231      // Clear the logs
 0232      logger.clear();
 0233   
 0234      // Initialize the rest
 0235      cntOpen           = {};
 0236      cntTotal          = {};
 0237      currentFixingCost = 0;
 0238      currentTag        = null;
 0239      isRich            = false;
 0240      namespaces        = {};
 0241      openTags          = [];
 0242      output            = '';
 0243      pos               = 0;
 0244      tagStack          = [];
 0245      tagStackIsSorted  = false;
 0246      text              = _text;
 0247      textLen           = text.length;
 0248      wsPos             = 0;
 0249   
 0250      // Initialize the root context
 0251      context = rootContext;
 0252      context.inParagraph = false;
 0253   
 0254      // Bump the UID
 0255      ++uid;
 0256  }
 0257   
 0258  /**
 0259  * Change a tag's tagLimit
 0260  *
 0261  * NOTE: the default tagLimit should generally be set during configuration instead
 0262  *
 0263  * @param {!string} tagName  The tag's name, in UPPERCASE
 0264  * @param {!number} tagLimit
 0265  */
 0266  function setTagLimit(tagName, tagLimit)
 0267  {
 0268      if (tagsConfig[tagName])
 0269      {
 0270          copyTagConfig(tagName).tagLimit = tagLimit;
 0271      }
 0272  }
 0273   
 0274  /**
 0275  * Change a tag's nestingLimit
 0276  *
 0277  * NOTE: the default nestingLimit should generally be set during configuration instead
 0278  *
 0279  * @param {!string} tagName      The tag's name, in UPPERCASE
 0280  * @param {!number} nestingLimit
 0281  */
 0282  function setNestingLimit(tagName, nestingLimit)
 0283  {
 0284      if (tagsConfig[tagName])
 0285      {
 0286          copyTagConfig(tagName).nestingLimit = nestingLimit;
 0287      }
 0288  }
 0289   
 0290  /**
 0291  * Copy a tag's config
 0292  *
 0293  * This method ensures that the tag's config is its own object and not shared with another
 0294  * identical tag
 0295  *
 0296  * @param  {!string} tagName Tag's name
 0297  * @return {!Object}         Tag's config
 0298  */
 0299  function copyTagConfig(tagName)
 0300  {
 0301      var tagConfig = {}, k;
 0302      for (k in tagsConfig[tagName])
 0303      {
 0304          tagConfig[k] = tagsConfig[tagName][k];
 0305      }
 0306   
 0307      return tagsConfig[tagName] = tagConfig;
 0308  }
 0309   
 0310  //==========================================================================
 0311  // Filter processing
 0312  //==========================================================================
 0313   
 0314  /**
 0315  * Execute all the attribute preprocessors of given tag
 0316  *
 0317  * @private
 0318  *
 0319  * @param  {!Tag}     tag       Source tag
 0320  * @param  {!Object}  tagConfig Tag's config
 0321  * @return {!boolean}           Unconditionally TRUE
 0322  */
 0323  function executeAttributePreprocessors(tag, tagConfig)
 0324  {
 0325      if (tagConfig.attributePreprocessors)
 0326      {
 0327          tagConfig.attributePreprocessors.forEach(function(attributePreprocessor)
 0328          {
 0329              var attrName = attributePreprocessor[0],
 0330                  regexp   = attributePreprocessor[1],
 0331                  map      = attributePreprocessor[2];
 0332   
 0333              if (!tag.hasAttribute(attrName))
 0334              {
 0335                  return;
 0336              }
 0337   
 0338              executeAttributePreprocessor(tag, attrName, regexp, map);
 0339          });
 0340      }
 0341   
 0342      return true;
 0343  }
 0344   
 0345  /**
 0346  * Execute an attribute preprocessor
 0347  *
 0348  * @param  {!Tag}            tag
 0349  * @param  {!string}         attrName
 0350  * @param  {!string}         regexp
 0351  * @param  {!Array<!string>} map
 0352  */
 0353  function executeAttributePreprocessor(tag, attrName, regexp, map)
 0354  {
 0355      var attrValue = tag.getAttribute(attrName),
 0356          captures  = getNamedCaptures(attrValue, regexp, map),
 0357          k;
 0358      
 0359      for (k in captures)
 0360      {
 0361          // Attribute preprocessors cannot overwrite other attributes but they can
 0362          // overwrite themselves
 0363          if (k === attrName || !tag.hasAttribute(k))
 0364          {
 0365              tag.setAttribute(k, captures[k]);
 0366          }
 0367      }
 0368  }
 0369   
 0370  /**
 0371  * Execute a regexp and return the values of the mapped captures
 0372  *
 0373  * @param  {!string}                  attrValue
 0374  * @param  {!string}                  regexp
 0375  * @param  {!Array<!string>}          map
 0376  * @return {!Object<!string,!string>}
 0377  */
 0378  function getNamedCaptures(attrValue, regexp, map)
 0379  {
 0380      var m = regexp.exec(attrValue);
 0381      if (!m)
 0382      {
 0383          return [];
 0384      }
 0385   
 0386      var values = {};
 0387      map.forEach(function(k, i)
 0388      {
 0389          if (typeof m[i] === 'string' && m[i] !== '')
 0390          {
 0391              values[k] = m[i];
 0392          }
 0393      });
 0394   
 0395      return values;
 0396  }
 0397   
 0398  /**
 0399  * Filter the attributes of given tag
 0400  *
 0401  * @private
 0402  *
 0403  * @param  {!Tag}     tag            Tag being checked
 0404  * @param  {!Object}  tagConfig      Tag's config
 0405  * @param  {!Object}  registeredVars Vars registered for use in attribute filters
 0406  * @param  {!Logger}  logger         This parser's Logger instance
 0407  * @return {!boolean}           Whether the whole attribute set is valid
 0408  */
 0409  function filterAttributes(tag, tagConfig, registeredVars, logger)
 0410  {
 0411      if (!tagConfig.attributes)
 0412      {
 0413          tag.setAttributes({});
 0414   
 0415          return true;
 0416      }
 0417   
 0418      var attrName, attrConfig;
 0419   
 0420      // Generate values for attributes with a generator set
 0421      if (HINT.attributeGenerator)
 0422      {
 0423          for (attrName in tagConfig.attributes)
 0424          {
 0425              attrConfig = tagConfig.attributes[attrName];
 0426   
 0427              if (attrConfig.generator)
 0428              {
 0429                  tag.setAttribute(attrName, attrConfig.generator(attrName));
 0430              }
 0431          }
 0432      }
 0433   
 0434      // Filter and remove invalid attributes
 0435      var attributes = tag.getAttributes();
 0436      for (attrName in attributes)
 0437      {
 0438          var attrValue = attributes[attrName];
 0439   
 0440          // Test whether this attribute exists and remove it if it doesn't
 0441          if (!tagConfig.attributes[attrName])
 0442          {
 0443              tag.removeAttribute(attrName);
 0444              continue;
 0445          }
 0446   
 0447          attrConfig = tagConfig.attributes[attrName];
 0448   
 0449          // Test whether this attribute has a filterChain
 0450          if (!attrConfig.filterChain)
 0451          {
 0452              continue;
 0453          }
 0454   
 0455          // Record the name of the attribute being filtered into the logger
 0456          logger.setAttribute(attrName);
 0457   
 0458          for (var i = 0; i < attrConfig.filterChain.length; ++i)
 0459          {
 0460              // NOTE: attrValue is intentionally set as the first argument to facilitate inlining
 0461              attrValue = attrConfig.filterChain[i](attrValue, attrName);
 0462   
 0463              if (attrValue === false)
 0464              {
 0465                  tag.removeAttribute(attrName);
 0466                  break;
 0467              }
 0468          }
 0469   
 0470          // Update the attribute value if it's valid
 0471          if (attrValue !== false)
 0472          {
 0473              tag.setAttribute(attrName, attrValue);
 0474          }
 0475   
 0476          // Remove the attribute's name from the logger
 0477          logger.unsetAttribute();
 0478      }
 0479   
 0480      // Iterate over the attribute definitions to handle missing attributes
 0481      for (attrName in tagConfig.attributes)
 0482      {
 0483          attrConfig = tagConfig.attributes[attrName];
 0484   
 0485          // Test whether this attribute is missing
 0486          if (!tag.hasAttribute(attrName))
 0487          {
 0488              if (HINT.attributeDefaultValue && attrConfig.defaultValue !== undefined)
 0489              {
 0490                  // Use the attribute's default value
 0491                  tag.setAttribute(attrName, attrConfig.defaultValue);
 0492              }
 0493              else if (attrConfig.required)
 0494              {
 0495                  // This attribute is missing, has no default value and is required, which means
 0496                  // the attribute set is invalid
 0497                  return false;
 0498              }
 0499          }
 0500      }
 0501   
 0502      return true;
 0503  }
 0504   
 0505  /**
 0506  * Execute given tag's filterChain
 0507  *
 0508  * @param  {!Tag}     tag Tag to filter
 0509  * @return {!boolean}     Whether the tag is valid
 0510  */
 0511  function filterTag(tag)
 0512  {
 0513      var tagName   = tag.getName(),
 0514          tagConfig = tagsConfig[tagName],
 0515          isValid   = true;
 0516   
 0517      if (tagConfig.filterChain)
 0518      {
 0519          // Record the tag being processed into the logger it can be added to the context of
 0520          // messages logged during the execution
 0521          logger.setTag(tag);
 0522   
 0523          for (var i = 0; i < tagConfig.filterChain.length; ++i)
 0524          {
 0525              if (!tagConfig.filterChain[i](tag, tagConfig))
 0526              {
 0527                  isValid = false;
 0528                  break;
 0529              }
 0530          }
 0531   
 0532          // Remove the tag from the logger
 0533          logger.unsetTag();
 0534      }
 0535   
 0536      return isValid;
 0537  }
 0538   
 0539  //==========================================================================
 0540  // Output handling
 0541  //==========================================================================
 0542   
 0543  /**
 0544  * Replace Unicode characters outside the BMP with XML entities in the output
 0545  */
 0546  function encodeUnicodeSupplementaryCharacters()
 0547  {
 0548      output = output.replace(
 0549          /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
 0550          encodeUnicodeSupplementaryCharactersCallback
 0551      );
 0552  }
 0553   
 0554  /**
 0555  * Encode given surrogate pair into an XML entity
 0556  *
 0557  * @param  {!string} pair Surrogate pair
 0558  * @return {!string}      XML entity
 0559  */
 0560  function encodeUnicodeSupplementaryCharactersCallback(pair)
 0561  {
 0562      var cp = (pair.charCodeAt(0) << 10) + pair.charCodeAt(1) - 56613888;
 0563   
 0564      return '&#' + cp + ';';
 0565  }
 0566   
 0567  /**
 0568  * Finalize the output by appending the rest of the unprocessed text and create the root node
 0569  */
 0570  function finalizeOutput()
 0571  {
 0572      var tmp;
 0573   
 0574      // Output the rest of the text and close the last paragraph
 0575      outputText(textLen, 0, true);
 0576   
 0577      // Remove empty tag pairs, e.g. <I><U></U></I> as well as empty paragraphs
 0578      do
 0579      {
 0580          tmp = output;
 0581          output = output.replace(/<([^ />]+)><\/\1>/g, '');
 0582      }
 0583      while (output !== tmp);
 0584   
 0585      // Merge consecutive <i> tags
 0586      output = output.replace(/<\/i><i>/g, '', output);
 0587   
 0588      // Encode Unicode characters that are outside of the BMP
 0589      encodeUnicodeSupplementaryCharacters();
 0590   
 0591      // Use a <r> root if the text is rich, or <t> for plain text (including <p></p> and <br/>)
 0592      var tagName = (isRich) ? 'r' : 't';
 0593   
 0594      // Prepare the root node with all the namespace declarations
 0595      tmp = '<' + tagName;
 0596      if (HINT.namespaces)
 0597      {
 0598          for (var prefix in namespaces)
 0599          {
 0600              tmp += ' xmlns:' + prefix + '="urn:s9e:TextFormatter:' + prefix + '"';
 0601          }
 0602      }
 0603   
 0604      output = tmp + '>' + output + '</' + tagName + '>';
 0605  }
 0606   
 0607  /**
 0608  * Append a tag to the output
 0609  *
 0610  * @param {!Tag} tag Tag to append
 0611  */
 0612  function outputTag(tag)
 0613  {
 0614      isRich = true;
 0615   
 0616      var tagName    = tag.getName(),
 0617          tagPos     = tag.getPos(),
 0618          tagLen     = tag.getLen(),
 0619          tagFlags   = tag.getFlags(),
 0620          skipBefore = 0,
 0621          skipAfter  = 0;
 0622   
 0623      if (HINT.RULE_IGNORE_WHITESPACE && (tagFlags & RULE_IGNORE_WHITESPACE))
 0624      {
 0625          skipBefore = 1;
 0626          skipAfter  = (tag.isEndTag()) ? 2 : 1;
 0627      }
 0628   
 0629      // Current paragraph must end before the tag if:
 0630      //  - the tag is a start (or self-closing) tag and it breaks paragraphs, or
 0631      //  - the tag is an end tag (but not self-closing)
 0632      var closeParagraph = false;
 0633      if (tag.isStartTag())
 0634      {
 0635          if (HINT.RULE_BREAK_PARAGRAPH && (tagFlags & RULE_BREAK_PARAGRAPH))
 0636          {
 0637              closeParagraph = true;
 0638          }
 0639      }
 0640      else
 0641      {
 0642          closeParagraph = true;
 0643      }
 0644   
 0645      // Let the cursor catch up with this tag's position
 0646      outputText(tagPos, skipBefore, closeParagraph);
 0647   
 0648      // Capture the text consumed by the tag
 0649      var tagText = (tagLen)
 0650                  ? htmlspecialchars_noquotes(text.substr(tagPos, tagLen))
 0651                  : '';
 0652   
 0653      // Output current tag
 0654      if (tag.isStartTag())
 0655      {
 0656          // Handle paragraphs before opening the tag
 0657          if (!HINT.RULE_BREAK_PARAGRAPH || !(tagFlags & RULE_BREAK_PARAGRAPH))
 0658          {
 0659              outputParagraphStart(tagPos);
 0660          }
 0661   
 0662          // Record this tag's namespace, if applicable
 0663          if (HINT.namespaces)
 0664          {
 0665              var colonPos = tagName.indexOf(':');
 0666              if (colonPos > 0)
 0667              {
 0668                  namespaces[tagName.substr(0, colonPos)] = 0;
 0669              }
 0670          }
 0671   
 0672          // Open the start tag and add its attributes, but don't close the tag
 0673          output += '<' + tagName;
 0674   
 0675          // We output the attributes in lexical order. Helps canonicalizing the output and could
 0676          // prove useful someday
 0677          var attributes = tag.getAttributes(),
 0678              attributeNames = [];
 0679          for (var attrName in attributes)
 0680          {
 0681              attributeNames.push(attrName);
 0682          }
 0683          attributeNames.sort(
 0684              function(a, b)
 0685              {
 0686                  return (a > b) ? 1 : -1;
 0687              }
 0688          );
 0689          attributeNames.forEach(
 0690              function(attrName)
 0691              {
 0692                  output += ' ' + attrName + '="' + htmlspecialchars_compat(attributes[attrName].toString()).replace(/\n/g, '&#10;') + '"';
 0693              }
 0694          );
 0695   
 0696          if (tag.isSelfClosingTag())
 0697          {
 0698              if (tagLen)
 0699              {
 0700                  output += '>' + tagText + '</' + tagName + '>';
 0701              }
 0702              else
 0703              {
 0704                  output += '/>';
 0705              }
 0706          }
 0707          else if (tagLen)
 0708          {
 0709              output += '><s>' + tagText + '</s>';
 0710          }
 0711          else
 0712          {
 0713              output += '>';
 0714          }
 0715      }
 0716      else
 0717      {
 0718          if (tagLen)
 0719          {
 0720              output += '<e>' + tagText + '</e>';
 0721          }
 0722   
 0723          output += '</' + tagName + '>';
 0724      }
 0725   
 0726      // Move the cursor past the tag
 0727      pos = tagPos + tagLen;
 0728   
 0729      // Skip newlines (no other whitespace) after this tag
 0730      wsPos = pos;
 0731      while (skipAfter && wsPos < textLen && text.charAt(wsPos) === "\n")
 0732      {
 0733          // Decrement the number of lines to skip
 0734          --skipAfter;
 0735   
 0736          // Move the cursor past the newline
 0737          ++wsPos;
 0738      }
 0739  }
 0740   
 0741  /**
 0742  * Output the text between the cursor's position (included) and given position (not included)
 0743  *
 0744  * @param  {!number}  catchupPos     Position we're catching up to
 0745  * @param  {!number}  maxLines       Maximum number of lines to ignore at the end of the text
 0746  * @param  {!boolean} closeParagraph Whether to close the paragraph at the end, if applicable
 0747  */
 0748  function outputText(catchupPos, maxLines, closeParagraph)
 0749  {
 0750      if (closeParagraph)
 0751      {
 0752          if (!(context.flags & RULE_CREATE_PARAGRAPHS))
 0753          {
 0754              closeParagraph = false;
 0755          }
 0756          else
 0757          {
 0758              // Ignore any number of lines at the end if we're closing a paragraph
 0759              maxLines = -1;
 0760          }
 0761      }
 0762   
 0763      if (pos >= catchupPos)
 0764      {
 0765          // We're already there, close the paragraph if applicable and return
 0766          if (closeParagraph)
 0767          {
 0768              outputParagraphEnd();
 0769          }
 0770      }
 0771   
 0772      // Skip over previously identified whitespace if applicable
 0773      if (wsPos > pos)
 0774      {
 0775          var skipPos = Math.min(catchupPos, wsPos);
 0776          output += text.substr(pos, skipPos - pos);
 0777          pos = skipPos;
 0778   
 0779          if (pos >= catchupPos)
 0780          {
 0781              // Skipped everything. Close the paragraph if applicable and return
 0782              if (closeParagraph)
 0783              {
 0784                  outputParagraphEnd();
 0785              }
 0786          }
 0787      }
 0788   
 0789      var catchupLen, catchupText;
 0790   
 0791      // Test whether we're even supposed to output anything
 0792      if (HINT.RULE_IGNORE_TEXT && context.flags & RULE_IGNORE_TEXT)
 0793      {
 0794          catchupLen  = catchupPos - pos,
 0795          catchupText = text.substr(pos, catchupLen);
 0796   
 0797          // If the catchup text is not entirely composed of whitespace, we put it inside ignore tags
 0798          if (!/^[ \n\t]*$/.test(catchupText))
 0799          {
 0800              catchupText = '<i>' + catchupText + '</i>';
 0801          }
 0802   
 0803          output += catchupText;
 0804          pos = catchupPos;
 0805   
 0806          if (closeParagraph)
 0807          {
 0808              outputParagraphEnd();
 0809          }
 0810   
 0811          return;
 0812      }
 0813   
 0814      // Compute the amount of text to ignore at the end of the output
 0815      var ignorePos = catchupPos,
 0816          ignoreLen = 0;
 0817   
 0818      // Ignore as many lines (including whitespace) as specified
 0819      while (maxLines && --ignorePos >= pos)
 0820      {
 0821          var c = text.charAt(ignorePos);
 0822          if (c !== ' ' && c !== "\n" && c !== "\t")
 0823          {
 0824              break;
 0825          }
 0826   
 0827          if (c === "\n")
 0828          {
 0829              --maxLines;
 0830          }
 0831   
 0832          ++ignoreLen;
 0833      }
 0834   
 0835      // Adjust catchupPos to ignore the text at the end
 0836      catchupPos -= ignoreLen;
 0837   
 0838      // Break down the text in paragraphs if applicable
 0839      if (HINT.RULE_CREATE_PARAGRAPHS && context.flags & RULE_CREATE_PARAGRAPHS)
 0840      {
 0841          if (!context.inParagraph)
 0842          {
 0843              outputWhitespace(catchupPos);
 0844   
 0845              if (catchupPos > pos)
 0846              {
 0847                  outputParagraphStart(catchupPos);
 0848              }
 0849          }
 0850   
 0851          // Look for a paragraph break in this text
 0852          var pbPos = text.indexOf("\n\n", pos);
 0853   
 0854          while (pbPos > -1 && pbPos < catchupPos)
 0855          {
 0856              outputText(pbPos, 0, true);
 0857              outputParagraphStart(catchupPos);
 0858   
 0859              pbPos = text.indexOf("\n\n", pos);
 0860          }
 0861      }
 0862   
 0863      // Capture, escape and output the text
 0864      if (catchupPos > pos)
 0865      {
 0866          catchupText = htmlspecialchars_noquotes(
 0867              text.substr(pos, catchupPos - pos)
 0868          );
 0869   
 0870          // Format line breaks if applicable
 0871          if (HINT.RULE_ENABLE_AUTO_BR && (context.flags & RULES_AUTO_LINEBREAKS) === RULE_ENABLE_AUTO_BR)
 0872          {
 0873              catchupText = catchupText.replace(/\n/g, "<br/>\n");
 0874          }
 0875   
 0876          output += catchupText;
 0877      }
 0878   
 0879      // Close the paragraph if applicable
 0880      if (closeParagraph)
 0881      {
 0882          outputParagraphEnd();
 0883      }
 0884   
 0885      // Add the ignored text if applicable
 0886      if (ignoreLen)
 0887      {
 0888          output += text.substr(catchupPos, ignoreLen);
 0889      }
 0890   
 0891      // Move the cursor past the text
 0892      pos = catchupPos + ignoreLen;
 0893  }
 0894   
 0895  /**
 0896  * Output a linebreak tag
 0897  *
 0898  * @param  {!Tag} tag
 0899  * @return void
 0900  */
 0901  function outputBrTag(tag)
 0902  {
 0903      outputText(tag.getPos(), 0, false);
 0904      output += '<br/>';
 0905  }
 0906   
 0907  /**
 0908  * Output an ignore tag
 0909  *
 0910  * @param  {!Tag} tag
 0911  * @return void
 0912  */
 0913  function outputIgnoreTag(tag)
 0914  {
 0915      var tagPos = tag.getPos(),
 0916          tagLen = tag.getLen();
 0917   
 0918      // Capture the text to ignore
 0919      var ignoreText = text.substr(tagPos, tagLen);
 0920   
 0921      // Catch up with the tag's position then output the tag
 0922      outputText(tagPos, 0, false);
 0923      output += '<i>' + htmlspecialchars_noquotes(ignoreText) + '</i>';
 0924      isRich = true;
 0925   
 0926      // Move the cursor past this tag
 0927      pos = tagPos + tagLen;
 0928  }
 0929   
 0930  /**
 0931  * Start a paragraph between current position and given position, if applicable
 0932  *
 0933  * @param  {!number} maxPos Rightmost position at which the paragraph can be opened
 0934  */
 0935  function outputParagraphStart(maxPos)
 0936  {
 0937      if (!HINT.RULE_CREATE_PARAGRAPHS)
 0938      {
 0939          return;
 0940      }
 0941   
 0942      // Do nothing if we're already in a paragraph, or if we don't use paragraphs
 0943      if (context.inParagraph
 0944       || !(context.flags & RULE_CREATE_PARAGRAPHS))
 0945      {
 0946          return;
 0947      }
 0948   
 0949      // Output the whitespace between pos and maxPos if applicable
 0950      outputWhitespace(maxPos);
 0951   
 0952      // Open the paragraph, but only if it's not at the very end of the text
 0953      if (pos < textLen)
 0954      {
 0955          output += '<p>';
 0956          context.inParagraph = true;
 0957      }
 0958  }
 0959   
 0960  /**
 0961  * Close current paragraph at current position if applicable
 0962  */
 0963  function outputParagraphEnd()
 0964  {
 0965      // Do nothing if we're not in a paragraph
 0966      if (!context.inParagraph)
 0967      {
 0968          return;
 0969      }
 0970   
 0971      output += '</p>';
 0972      context.inParagraph = false;
 0973  }
 0974   
 0975  /**
 0976  * Output the content of a verbatim tag
 0977  *
 0978  * @param {!Tag} tag
 0979  */
 0980  function outputVerbatim(tag)
 0981  {
 0982      var flags = context.flags;
 0983      context.flags = tag.getFlags();
 0984      outputText(currentTag.getPos() + currentTag.getLen(), 0, false);
 0985      context.flags = flags;
 0986  }
 0987   
 0988  /**
 0989  * Skip as much whitespace after current position as possible
 0990  *
 0991  * @param  {!number} maxPos Rightmost character to be skipped
 0992  */
 0993  function outputWhitespace(maxPos)
 0994  {
 0995      while (pos < maxPos && " \n\t".indexOf(text.charAt(pos)) > -1)
 0996      {
 0997          output += text.charAt(pos);
 0998          ++pos;
 0999      }
 1000  }
 1001   
 1002  //==========================================================================
 1003  // Plugins handling
 1004  //==========================================================================
 1005   
 1006  /**
 1007  * Disable a plugin
 1008  *
 1009  * @param {!string} pluginName Name of the plugin
 1010  */
 1011  function disablePlugin(pluginName)
 1012  {
 1013      if (plugins[pluginName])
 1014      {
 1015          plugins[pluginName].isDisabled = true;
 1016      }
 1017  }
 1018   
 1019  /**
 1020  * Enable a plugin
 1021  *
 1022  * @param {!string} pluginName Name of the plugin
 1023  */
 1024  function enablePlugin(pluginName)
 1025  {
 1026      if (plugins[pluginName])
 1027      {
 1028          plugins[pluginName].isDisabled = false;
 1029      }
 1030  }
 1031   
 1032  /**
 1033  * Execute given plugin
 1034  *
 1035  * @param {!string} pluginName Plugin's name
 1036  */
 1037  function executePluginParser(pluginName)
 1038  {
 1039      var pluginConfig = plugins[pluginName];
 1040      if (pluginConfig.quickMatch && text.indexOf(pluginConfig.quickMatch) < 0)
 1041      {
 1042          return;
 1043      }
 1044   
 1045      var matches = [];
 1046      if (pluginConfig.regexp)
 1047      {
 1048          matches = getMatches(pluginConfig.regexp, pluginConfig.regexpLimit);
 1049          if (!matches.length)
 1050          {
 1051              return;
 1052          }
 1053      }
 1054   
 1055      // Execute the plugin's parser, which will add tags via addStartTag() and others
 1056      getPluginParser(pluginName)(text, matches);
 1057  }
 1058   
 1059  /**
 1060  * Execute all the plugins
 1061  */
 1062  function executePluginParsers()
 1063  {
 1064      for (var pluginName in plugins)
 1065      {
 1066          if (!plugins[pluginName].isDisabled)
 1067          {
 1068              executePluginParser(pluginName);
 1069          }
 1070      }
 1071  }
 1072   
 1073  /**
 1074  * Get regexp matches in a manner similar to preg_match_all() with PREG_SET_ORDER | PREG_OFFSET_CAPTURE
 1075  *
 1076  * @param  {!RegExp} regexp
 1077  * @param  {!number} limit
 1078  * @return {!Array.<!Array>}
 1079  */
 1080  function getMatches(regexp, limit)
 1081  {
 1082      // Reset the regexp
 1083      regexp.lastIndex = 0;
 1084      var matches = [], cnt = 0, m;
 1085      while (++cnt <= limit && (m = regexp.exec(text)))
 1086      {
 1087          // NOTE: coercing m.index to a number because Closure Compiler thinks pos is a string otherwise
 1088          var pos   = +m['index'],
 1089              match = [[m[0], pos]],
 1090              i = 0;
 1091          while (++i < m.length)
 1092          {
 1093              var str = m[i];
 1094   
 1095              // Sub-expressions that were not evaluated return undefined
 1096              if (str === undefined)
 1097              {
 1098                  match.push(['', -1]);
 1099              }
 1100              else
 1101              {
 1102                  match.push([str, text.indexOf(str, pos)]);
 1103                  pos += str.length;
 1104              }
 1105          }
 1106   
 1107          matches.push(match);
 1108      }
 1109   
 1110      return matches;
 1111  }
 1112   
 1113  /**
 1114  * Get the callback for given plugin's parser
 1115  *
 1116  * @param  {!string}   pluginName
 1117  * @return {!function(string, Array)}
 1118  */
 1119  function getPluginParser(pluginName)
 1120  {
 1121      return plugins[pluginName].parser;
 1122  }
 1123   
 1124  /**
 1125  * Register a parser
 1126  *
 1127  * Can be used to add a new parser with no plugin config, or pre-generate a parser for an
 1128  * existing plugin
 1129  *
 1130  * @param  {!string}   pluginName
 1131  * @param  {!Function} parser
 1132  * @param  {RegExp}   regexp
 1133  * @param  {number}   limit
 1134  */
 1135  function registerParser(pluginName, parser, regexp, limit)
 1136  {
 1137      // Create an empty config for this plugin to ensure it is executed
 1138      if (!plugins[pluginName])
 1139      {
 1140          plugins[pluginName] = {};
 1141      }
 1142      if (regexp)
 1143      {
 1144          plugins[pluginName].regexp = regexp;
 1145          plugins[pluginName].limit  = limit || Infinity;
 1146      }
 1147      plugins[pluginName].parser = parser;
 1148  }
 1149   
 1150  //==========================================================================
 1151  // Rules handling
 1152  //==========================================================================
 1153   
 1154  /**
 1155  * Apply closeAncestor rules associated with given tag
 1156  *
 1157  * @param  {!Tag}     tag Tag
 1158  * @return {!boolean}     Whether a new tag has been added
 1159  */
 1160  function closeAncestor(tag)
 1161  {
 1162      if (!HINT.closeAncestor)
 1163      {
 1164          return false;
 1165      }
 1166   
 1167      if (openTags.length)
 1168      {
 1169          var tagName   = tag.getName(),
 1170              tagConfig = tagsConfig[tagName];
 1171   
 1172          if (tagConfig.rules.closeAncestor)
 1173          {
 1174              var i = openTags.length;
 1175   
 1176              while (--i >= 0)
 1177              {
 1178                  var ancestor     = openTags[i],
 1179                      ancestorName = ancestor.getName();
 1180   
 1181                  if (tagConfig.rules.closeAncestor[ancestorName])
 1182                  {
 1183                      // We have to close this ancestor. First we reinsert this tag...
 1184                      tagStack.push(tag);
 1185   
 1186                      // ...then we add a new end tag for it
 1187                      addMagicEndTag(ancestor, tag.getPos());
 1188   
 1189                      return true;
 1190                  }
 1191              }
 1192          }
 1193      }
 1194   
 1195      return false;
 1196  }
 1197   
 1198  /**
 1199  * Apply closeParent rules associated with given tag
 1200  *
 1201  * @param  {!Tag}     tag Tag
 1202  * @return {!boolean}     Whether a new tag has been added
 1203  */
 1204  function closeParent(tag)
 1205  {
 1206      if (!HINT.closeParent)
 1207      {
 1208          return false;
 1209      }
 1210   
 1211      if (openTags.length)
 1212      {
 1213          var tagName   = tag.getName(),
 1214              tagConfig = tagsConfig[tagName];
 1215   
 1216          if (tagConfig.rules.closeParent)
 1217          {
 1218              var parent     = openTags[openTags.length - 1],
 1219                  parentName = parent.getName();
 1220   
 1221              if (tagConfig.rules.closeParent[parentName])
 1222              {
 1223                  // We have to close that parent. First we reinsert the tag...
 1224                  tagStack.push(tag);
 1225   
 1226                  // ...then we add a new end tag for it
 1227                  addMagicEndTag(parent, tag.getPos());
 1228   
 1229                  return true;
 1230              }
 1231          }
 1232      }
 1233   
 1234      return false;
 1235  }
 1236   
 1237  /**
 1238  * Apply the createChild rules associated with given tag
 1239  *
 1240  * @param {!Tag} tag Tag
 1241  */
 1242  function createChild(tag)
 1243  {
 1244      if (!HINT.createChild)
 1245      {
 1246          return;
 1247      }
 1248   
 1249      var tagConfig = tagsConfig[tag.getName()];
 1250      if (tagConfig.rules.createChild)
 1251      {
 1252          var priority = -1000,
 1253              _text    = text.substr(pos),
 1254              tagPos   = pos + _text.length - _text.replace(/^[ \n\r\t]+/, '').length;
 1255          tagConfig.rules.createChild.forEach(function(tagName)
 1256          {
 1257              addStartTag(tagName, tagPos, 0, ++priority);
 1258          });
 1259      }
 1260  }
 1261   
 1262  /**
 1263  * Apply fosterParent rules associated with given tag
 1264  *
 1265  * NOTE: this rule has the potential for creating an unbounded loop, either if a tag tries to
 1266  *       foster itself or two or more tags try to foster each other in a loop. We mitigate the
 1267  *       risk by preventing a tag from creating a child of itself (the parent still gets closed)
 1268  *       and by checking and increasing the currentFixingCost so that a loop of multiple tags
 1269  *       do not run indefinitely. The default tagLimit and nestingLimit also serve to prevent the
 1270  *       loop from running indefinitely
 1271  *
 1272  * @param  {!Tag}     tag Tag
 1273  * @return {!boolean}     Whether a new tag has been added
 1274  */
 1275  function fosterParent(tag)
 1276  {
 1277      if (!HINT.fosterParent)
 1278      {
 1279          return false;
 1280      }
 1281   
 1282      if (openTags.length)
 1283      {
 1284          var tagName   = tag.getName(),
 1285              tagConfig = tagsConfig[tagName];
 1286   
 1287          if (tagConfig.rules.fosterParent)
 1288          {
 1289              var parent     = openTags[openTags.length - 1],
 1290                  parentName = parent.getName();
 1291   
 1292              if (tagConfig.rules.fosterParent[parentName])
 1293              {
 1294                  if (parentName !== tagName && currentFixingCost < maxFixingCost)
 1295                  {
 1296                      // Add a 0-width copy of the parent tag right after this tag, with a worse
 1297                      // priority and make it depend on this tag
 1298                      var child = addCopyTag(parent, tag.getPos() + tag.getLen(), 0, tag.getSortPriority() + 1);
 1299                      tag.cascadeInvalidationTo(child);
 1300                  }
 1301   
 1302                  // Reinsert current tag
 1303                  tagStack.push(tag);
 1304   
 1305                  // And finally close its parent with a priority that ensures it is processed
 1306                  // before this tag
 1307                  addMagicEndTag(parent, tag.getPos(), tag.getSortPriority() - 1);
 1308   
 1309                  // Adjust the fixing cost to account for the additional tags/processing
 1310                  currentFixingCost += 4;
 1311   
 1312                  return true;
 1313              }
 1314          }
 1315      }
 1316   
 1317      return false;
 1318  }
 1319   
 1320  /**
 1321  * Apply requireAncestor rules associated with given tag
 1322  *
 1323  * @param  {!Tag}     tag Tag
 1324  * @return {!boolean}     Whether this tag has an unfulfilled requireAncestor requirement
 1325  */
 1326  function requireAncestor(tag)
 1327  {
 1328      if (!HINT.requireAncestor)
 1329      {
 1330          return false;
 1331      }
 1332   
 1333      var tagName   = tag.getName(),
 1334          tagConfig = tagsConfig[tagName];
 1335   
 1336      if (tagConfig.rules.requireAncestor)
 1337      {
 1338          var i = tagConfig.rules.requireAncestor.length;
 1339          while (--i >= 0)
 1340          {
 1341              var ancestorName = tagConfig.rules.requireAncestor[i];
 1342              if (cntOpen[ancestorName])
 1343              {
 1344                  return false;
 1345              }
 1346          }
 1347   
 1348          logger.err('Tag requires an ancestor', {
 1349              'requireAncestor' : tagConfig.rules.requireAncestor.join(', '),
 1350              'tag'             : tag
 1351          });
 1352   
 1353          return true;
 1354      }
 1355   
 1356      return false;
 1357  }
 1358   
 1359  //==========================================================================
 1360  // Tag processing
 1361  //==========================================================================
 1362   
 1363  /**
 1364  * Create and add an end tag for given start tag at given position
 1365  *
 1366  * @param  {!Tag}    startTag Start tag
 1367  * @param  {!number} tagPos   End tag's position (will be adjusted for whitespace if applicable)
 1368  * @return {!Tag}
 1369  */
 1370  function addMagicEndTag(startTag, tagPos)
 1371  {
 1372      var tagName = startTag.getName();
 1373   
 1374      // Adjust the end tag's position if whitespace is to be minimized
 1375      if (HINT.RULE_IGNORE_WHITESPACE && (startTag.getFlags() & RULE_IGNORE_WHITESPACE))
 1376      {
 1377          tagPos = getMagicPos(tagPos);
 1378      }
 1379   
 1380      // Add a 0-width end tag that is paired with the given start tag
 1381      var endTag = addEndTag(tagName, tagPos, 0);
 1382      endTag.pairWith(startTag);
 1383   
 1384      return endTag;
 1385  }
 1386   
 1387  /**
 1388  * Compute the position of a magic end tag, adjusted for whitespace
 1389  *
 1390  * @param  {!number} tagPos Rightmost possible position for the tag
 1391  * @return {!number}
 1392  */
 1393  function getMagicPos(tagPos)
 1394  {
 1395      // Back up from given position to the cursor's position until we find a character that
 1396      // is not whitespace
 1397      while (tagPos > pos && WHITESPACE.indexOf(text.charAt(tagPos - 1)) > -1)
 1398      {
 1399          --tagPos;
 1400      }
 1401   
 1402      return tagPos;
 1403  }
 1404   
 1405  /**
 1406  * Test whether given start tag is immediately followed by a closing tag
 1407  *
 1408  * @param  {!Tag} tag Start tag (including self-closing)
 1409  * @return {!boolean}
 1410  */
 1411  function isFollowedByClosingTag(tag)
 1412  {
 1413      return (!tagStack.length) ? false : tagStack[tagStack.length - 1].canClose(tag);
 1414  }
 1415   
 1416  /**
 1417  * Process all tags in the stack
 1418  */
 1419  function processTags()
 1420  {
 1421      if (!tagStack.length)
 1422      {
 1423          return;
 1424      }
 1425   
 1426      // Initialize the count tables
 1427      for (var tagName in tagsConfig)
 1428      {
 1429          cntOpen[tagName]  = 0;
 1430          cntTotal[tagName] = 0;
 1431      }
 1432   
 1433      // Process the tag stack, close tags that were left open and repeat until done
 1434      do
 1435      {
 1436          while (tagStack.length)
 1437          {
 1438              if (!tagStackIsSorted)
 1439              {
 1440                  sortTags();
 1441              }
 1442   
 1443              currentTag = tagStack.pop();
 1444              processCurrentTag();
 1445          }
 1446   
 1447          // Close tags that were left open
 1448          openTags.forEach(function (startTag)
 1449          {
 1450              // NOTE: we add tags in hierarchical order (ancestors to descendants) but since
 1451              //       the stack is processed in LIFO order, it means that tags get closed in
 1452              //       the correct order, from descendants to ancestors
 1453              addMagicEndTag(startTag, textLen);
 1454          });
 1455      }
 1456      while (tagStack.length);
 1457  }
 1458   
 1459  /**
 1460  * Process current tag
 1461  */
 1462  function processCurrentTag()
 1463  {
 1464      // Invalidate current tag if tags are disabled and current tag would not close the last open
 1465      // tag and is not a system tag
 1466      if ((context.flags & RULE_IGNORE_TAGS)
 1467       && !currentTag.canClose(openTags[openTags.length - 1])
 1468       && !currentTag.isSystemTag())
 1469      {
 1470          currentTag.invalidate();
 1471      }
 1472   
 1473      var tagPos = currentTag.getPos(),
 1474          tagLen = currentTag.getLen();
 1475   
 1476      // Test whether the cursor passed this tag's position already
 1477      if (pos > tagPos && !currentTag.isInvalid())
 1478      {
 1479          // Test whether this tag is paired with a start tag and this tag is still open
 1480          var startTag = currentTag.getStartTag();
 1481   
 1482          if (startTag && openTags.indexOf(startTag) >= 0)
 1483          {
 1484              // Create an end tag that matches current tag's start tag, which consumes as much of
 1485              // the same text as current tag and is paired with the same start tag
 1486              addEndTag(
 1487                  startTag.getName(),
 1488                  pos,
 1489                  Math.max(0, tagPos + tagLen - pos)
 1490              ).pairWith(startTag);
 1491   
 1492              // Note that current tag is not invalidated, it's merely replaced
 1493              return;
 1494          }
 1495   
 1496          // If this is an ignore tag, try to ignore as much as the remaining text as possible
 1497          if (currentTag.isIgnoreTag())
 1498          {
 1499              var ignoreLen = tagPos + tagLen - pos;
 1500   
 1501              if (ignoreLen > 0)
 1502              {
 1503                  // Create a new ignore tag and move on
 1504                  addIgnoreTag(pos, ignoreLen);
 1505   
 1506                  return;
 1507              }
 1508          }
 1509   
 1510          // Skipped tags are invalidated
 1511          currentTag.invalidate();
 1512      }
 1513   
 1514      if (currentTag.isInvalid())
 1515      {
 1516          return;
 1517      }
 1518   
 1519      if (currentTag.isIgnoreTag())
 1520      {
 1521          outputIgnoreTag(currentTag);
 1522      }
 1523      else if (currentTag.isBrTag())
 1524      {
 1525          // Output the tag if it's allowed, ignore it otherwise
 1526          if (!HINT.RULE_PREVENT_BR || !(context.flags & RULE_PREVENT_BR))
 1527          {
 1528              outputBrTag(currentTag);
 1529          }
 1530      }
 1531      else if (currentTag.isParagraphBreak())
 1532      {
 1533          outputText(currentTag.getPos(), 0, true);
 1534      }
 1535      else if (currentTag.isVerbatim())
 1536      {
 1537          outputVerbatim(currentTag);
 1538      }
 1539      else if (currentTag.isStartTag())
 1540      {
 1541          processStartTag(currentTag);
 1542      }
 1543      else
 1544      {
 1545          processEndTag(currentTag);
 1546      }
 1547  }
 1548   
 1549  /**
 1550  * Process given start tag (including self-closing tags) at current position
 1551  *
 1552  * @param {!Tag} tag Start tag (including self-closing)
 1553  */
 1554  function processStartTag(tag)
 1555  {
 1556      var tagName   = tag.getName(),
 1557          tagConfig = tagsConfig[tagName];
 1558   
 1559      // 1. Check that this tag has not reached its global limit tagLimit
 1560      // 2. Execute this tag's filterChain, which will filter/validate its attributes
 1561      // 3. Apply closeParent, closeAncestor and fosterParent rules
 1562      // 4. Check for nestingLimit
 1563      // 5. Apply requireAncestor rules
 1564      //
 1565      // This order ensures that the tag is valid and within the set limits before we attempt to
 1566      // close parents or ancestors. We need to close ancestors before we can check for nesting
 1567      // limits, whether this tag is allowed within current context (the context may change
 1568      // as ancestors are closed) or whether the required ancestors are still there (they might
 1569      // have been closed by a rule.)
 1570      if (cntTotal[tagName] >= tagConfig.tagLimit)
 1571      {
 1572          logger.err(
 1573              'Tag limit exceeded',
 1574              {
 1575                  'tag'      : tag,
 1576                  'tagName'  : tagName,
 1577                  'tagLimit' : tagConfig.tagLimit
 1578              }
 1579          );
 1580          tag.invalidate();
 1581   
 1582          return;
 1583      }
 1584   
 1585      if (!filterTag(tag))
 1586      {
 1587          tag.invalidate();
 1588   
 1589          return;
 1590      }
 1591   
 1592      if (fosterParent(tag) || closeParent(tag) || closeAncestor(tag))
 1593      {
 1594          // This tag parent/ancestor needs to be closed, we just return (the tag is still valid)
 1595          return;
 1596      }
 1597   
 1598      if (cntOpen[tagName] >= tagConfig.nestingLimit)
 1599      {
 1600          logger.err(
 1601              'Nesting limit exceeded',
 1602              {
 1603                  'tag'          : tag,
 1604                  'tagName'      : tagName,
 1605                  'nestingLimit' : tagConfig.nestingLimit
 1606              }
 1607          );
 1608          tag.invalidate();
 1609   
 1610          return;
 1611      }
 1612   
 1613      if (!tagIsAllowed(tagName))
 1614      {
 1615          var msg     = 'Tag is not allowed in this context',
 1616              context = {'tag': tag, 'tagName': tagName};
 1617          if (tag.getLen() > 0)
 1618          {
 1619              logger.warn(msg, context);
 1620          }
 1621          else
 1622          {
 1623              logger.debug(msg, context);
 1624          }
 1625          tag.invalidate();
 1626   
 1627          return;
 1628      }
 1629   
 1630      if (requireAncestor(tag))
 1631      {
 1632          tag.invalidate();
 1633   
 1634          return;
 1635      }
 1636   
 1637      // If this tag has an autoClose rule and it's not paired with an end tag or followed by an
 1638      // end tag, we replace it with a self-closing tag with the same properties
 1639      if (HINT.RULE_AUTO_CLOSE
 1640       && tag.getFlags() & RULE_AUTO_CLOSE
 1641       && !tag.getEndTag()
 1642       && !isFollowedByClosingTag(tag))
 1643      {
 1644          var newTag = new Tag(Tag.SELF_CLOSING_TAG, tagName, tag.getPos(), tag.getLen());
 1645          newTag.setAttributes(tag.getAttributes());
 1646          newTag.setFlags(tag.getFlags());
 1647   
 1648          tag = newTag;
 1649      }
 1650   
 1651      if (HINT.RULE_TRIM_FIRST_LINE
 1652       && tag.getFlags() & RULE_TRIM_FIRST_LINE
 1653       && !tag.getEndTag()
 1654       && text.charAt(tag.getPos() + tag.getLen()) === "\n")
 1655      {
 1656          addIgnoreTag(tag.getPos() + tag.getLen(), 1);
 1657      }
 1658   
 1659      // This tag is valid, output it and update the context
 1660      outputTag(tag);
 1661      pushContext(tag);
 1662   
 1663      // Apply the createChild rules if applicable
 1664      createChild(tag);
 1665  }
 1666   
 1667  /**
 1668  * Process given end tag at current position
 1669  *
 1670  * @param {!Tag} tag End tag
 1671  */
 1672  function processEndTag(tag)
 1673  {
 1674      var tagName = tag.getName();
 1675   
 1676      if (!cntOpen[tagName])
 1677      {
 1678          // This is an end tag with no start tag
 1679          return;
 1680      }
 1681   
 1682      /**
 1683      * @type {!Array.<!Tag>} List of tags need to be closed before given tag
 1684      */
 1685      var closeTags = [];
 1686   
 1687      // Iterate through all open tags from last to first to find a match for our tag
 1688      var i = openTags.length;
 1689      while (--i >= 0)
 1690      {
 1691          var openTag = openTags[i];
 1692   
 1693          if (tag.canClose(openTag))
 1694          {
 1695              break;
 1696          }
 1697   
 1698          closeTags.push(openTag);
 1699          ++currentFixingCost;
 1700      }
 1701   
 1702      if (i < 0)
 1703      {
 1704          // Did not find a matching tag
 1705          logger.debug('Skipping end tag with no start tag', {'tag': tag});
 1706   
 1707          return;
 1708      }
 1709   
 1710      // Only reopen tags if we haven't exceeded our "fixing" budget
 1711      var keepReopening = HINT.RULE_AUTO_REOPEN && (currentFixingCost < maxFixingCost),
 1712          reopenTags    = [];
 1713      closeTags.forEach(function(openTag)
 1714      {
 1715          var openTagName = openTag.getName();
 1716   
 1717          // Test whether this tag should be reopened automatically
 1718          if (keepReopening)
 1719          {
 1720              if (openTag.getFlags() & RULE_AUTO_REOPEN)
 1721              {
 1722                  reopenTags.push(openTag);
 1723              }
 1724              else
 1725              {
 1726                  keepReopening = false;
 1727              }
 1728          }
 1729   
 1730          // Find the earliest position we can close this open tag
 1731          var tagPos = tag.getPos();
 1732          if (HINT.RULE_IGNORE_WHITESPACE && openTag.getFlags() & RULE_IGNORE_WHITESPACE)
 1733          {
 1734              tagPos = getMagicPos(tagPos);
 1735          }
 1736   
 1737          // Output an end tag to close this start tag, then update the context
 1738          var endTag = new Tag(Tag.END_TAG, openTagName, tagPos, 0);
 1739          endTag.setFlags(openTag.getFlags());
 1740          outputTag(endTag);
 1741          popContext();
 1742      });
 1743   
 1744      // Output our tag, moving the cursor past it, then update the context
 1745      outputTag(tag);
 1746      popContext();
 1747   
 1748      // If our fixing budget allows it, peek at upcoming tags and remove end tags that would
 1749      // close tags that are already being closed now. Also, filter our list of tags being
 1750      // reopened by removing those that would immediately be closed
 1751      if (closeTags.length && currentFixingCost < maxFixingCost)
 1752      {
 1753          /**
 1754          * @type {number} Rightmost position of the portion of text to ignore
 1755          */
 1756          var ignorePos = pos;
 1757   
 1758          i = tagStack.length;
 1759          while (--i >= 0 && ++currentFixingCost < maxFixingCost)
 1760          {
 1761              var upcomingTag = tagStack[i];
 1762   
 1763              // Test whether the upcoming tag is positioned at current "ignore" position and it's
 1764              // strictly an end tag (not a start tag or a self-closing tag)
 1765              if (upcomingTag.getPos() > ignorePos
 1766               || upcomingTag.isStartTag())
 1767              {
 1768                  break;
 1769              }
 1770   
 1771              // Test whether this tag would close any of the tags we're about to reopen
 1772              var j = closeTags.length;
 1773   
 1774              while (--j >= 0 && ++currentFixingCost < maxFixingCost)
 1775              {
 1776                  if (upcomingTag.canClose(closeTags[j]))
 1777                  {
 1778                      // Remove the tag from the lists and reset the keys
 1779                      closeTags.splice(j, 1);
 1780   
 1781                      if (reopenTags[j])
 1782                      {
 1783                          reopenTags.splice(j, 1);
 1784                      }
 1785   
 1786                      // Extend the ignored text to cover this tag
 1787                      ignorePos = Math.max(
 1788                          ignorePos,
 1789                          upcomingTag.getPos() + upcomingTag.getLen()
 1790                      );
 1791   
 1792                      break;
 1793                  }
 1794              }
 1795          }
 1796   
 1797          if (ignorePos > pos)
 1798          {
 1799              /**
 1800              * @todo have a method that takes (pos,len) rather than a Tag
 1801              */
 1802              outputIgnoreTag(new Tag(Tag.SELF_CLOSING_TAG, 'i', pos, ignorePos - pos));
 1803          }
 1804      }
 1805   
 1806      // Re-add tags that need to be reopened, at current cursor position
 1807      reopenTags.forEach(function(startTag)
 1808      {
 1809          var newTag = addCopyTag(startTag, pos, 0);
 1810   
 1811          // Re-pair the new tag
 1812          var endTag = startTag.getEndTag();
 1813          if (endTag)
 1814          {
 1815              newTag.pairWith(endTag);
 1816          }
 1817      });
 1818  }
 1819   
 1820  /**
 1821  * Update counters and replace current context with its parent context
 1822  */
 1823  function popContext()
 1824  {
 1825      var tag = openTags.pop();
 1826      --cntOpen[tag.getName()];
 1827      context = context.parentContext;
 1828  }
 1829   
 1830  /**
 1831  * Update counters and replace current context with a new context based on given tag
 1832  *
 1833  * If given tag is a self-closing tag, the context won't change
 1834  *
 1835  * @param {!Tag} tag Start tag (including self-closing)
 1836  */
 1837  function pushContext(tag)
 1838  {
 1839      var tagName   = tag.getName(),
 1840          tagFlags  = tag.getFlags(),
 1841          tagConfig = tagsConfig[tagName];
 1842   
 1843      ++cntTotal[tagName];
 1844   
 1845      // If this is a self-closing tag, the context remains the same
 1846      if (tag.isSelfClosingTag())
 1847      {
 1848          return;
 1849      }
 1850   
 1851      // Recompute the allowed tags
 1852      var allowed = [];
 1853      if (HINT.RULE_IS_TRANSPARENT && (tagFlags & RULE_IS_TRANSPARENT))
 1854      {
 1855          context.allowed.forEach(function(v, k)
 1856          {
 1857              allowed.push(tagConfig.allowed[k] & v);
 1858          });
 1859      }
 1860      else
 1861      {
 1862          context.allowed.forEach(function(v, k)
 1863          {
 1864              allowed.push(tagConfig.allowed[k] & ((v & 0xFF00) | (v >> 8)));
 1865          });
 1866      }
 1867   
 1868      // Use this tag's flags as a base for this context and add inherited rules
 1869      var flags = tagFlags | (context.flags & RULES_INHERITANCE);
 1870   
 1871      // RULE_DISABLE_AUTO_BR turns off RULE_ENABLE_AUTO_BR
 1872      if (flags & RULE_DISABLE_AUTO_BR)
 1873      {
 1874          flags &= ~RULE_ENABLE_AUTO_BR;
 1875      }
 1876   
 1877      ++cntOpen[tagName];
 1878      openTags.push(tag);
 1879      context = {
 1880          allowed       : allowed,
 1881          flags         : flags,
 1882          parentContext : context
 1883      };
 1884  }
 1885   
 1886  /**
 1887  * Return whether given tag is allowed in current context
 1888  *
 1889  * @param  {!string}  tagName
 1890  * @return {!boolean}
 1891  */
 1892  function tagIsAllowed(tagName)
 1893  {
 1894      var n = tagsConfig[tagName].bitNumber;
 1895   
 1896      return !!(context.allowed[n >> 3] & (1 << (n & 7)));
 1897  }
 1898   
 1899  //==========================================================================
 1900  // Tag stack
 1901  //==========================================================================
 1902   
 1903  /**
 1904  * Add a start tag
 1905  *
 1906  * @param  {!string} name Name of the tag
 1907  * @param  {!number} pos  Position of the tag in the text
 1908  * @param  {!number} len  Length of text consumed by the tag
 1909  * @param  {number}  prio Tags' priority
 1910  * @return {!Tag}
 1911  */
 1912  function addStartTag(name, pos, len, prio)
 1913  {
 1914      return addTag(Tag.START_TAG, name, pos, len, prio || 0);
 1915  }
 1916   
 1917  /**
 1918  * Add an end tag
 1919  *
 1920  * @param  {!string} name Name of the tag
 1921  * @param  {!number} pos  Position of the tag in the text
 1922  * @param  {!number} len  Length of text consumed by the tag
 1923  * @param  {number}  prio Tags' priority
 1924  * @return {!Tag}
 1925  */
 1926  function addEndTag(name, pos, len, prio)
 1927  {
 1928      return addTag(Tag.END_TAG, name, pos, len, prio || 0);
 1929  }
 1930   
 1931  /**
 1932  * Add a self-closing tag
 1933  *
 1934  * @param  {!string} name Name of the tag
 1935  * @param  {!number} pos  Position of the tag in the text
 1936  * @param  {!number} len  Length of text consumed by the tag
 1937  * @param  {number}  prio Tags' priority
 1938  * @return {!Tag}
 1939  */
 1940  function addSelfClosingTag(name, pos, len, prio)
 1941  {
 1942      return addTag(Tag.SELF_CLOSING_TAG, name, pos, len, prio || 0);
 1943  }
 1944   
 1945  /**
 1946  * Add a 0-width "br" tag to force a line break at given position
 1947  *
 1948  * @param  {!number} pos  Position of the tag in the text
 1949  * @param  {number}  prio Tags' priority
 1950  * @return {!Tag}
 1951  */
 1952  function addBrTag(pos, prio)
 1953  {
 1954      return addTag(Tag.SELF_CLOSING_TAG, 'br', pos, 0, prio || 0);
 1955  }
 1956   
 1957  /**
 1958  * Add an "ignore" tag
 1959  *
 1960  * @param  {!number} pos  Position of the tag in the text
 1961  * @param  {!number} len  Length of text consumed by the tag
 1962  * @param  {number}  prio Tags' priority
 1963  * @return {!Tag}
 1964  */
 1965  function addIgnoreTag(pos, len, prio)
 1966  {
 1967      return addTag(Tag.SELF_CLOSING_TAG, 'i', pos, Math.min(len, textLen - pos), prio || 0);
 1968  }
 1969   
 1970  /**
 1971  * Add a paragraph break at given position
 1972  *
 1973  * Uses a zero-width tag that is actually never output in the result
 1974  *
 1975  * @param  {!number} pos  Position of the tag in the text
 1976  * @param  {number}  prio Tags' priority
 1977  * @return {!Tag}
 1978  */
 1979  function addParagraphBreak(pos, prio)
 1980  {
 1981      return addTag(Tag.SELF_CLOSING_TAG, 'pb', pos, 0, prio || 0);
 1982  }
 1983   
 1984  /**
 1985  * Add a copy of given tag at given position and length
 1986  *
 1987  * @param  {!Tag}    tag Original tag
 1988  * @param  {!number} pos Copy's position
 1989  * @param  {!number} len Copy's length
 1990  * @param  {number}  prio Tags' priority
 1991  * @return {!Tag}         Copy tag
 1992  */
 1993  function addCopyTag(tag, pos, len, prio)
 1994  {
 1995      var copy = addTag(tag.getType(), tag.getName(), pos, len, tag.getSortPriority());
 1996      copy.setAttributes(tag.getAttributes());
 1997   
 1998      return copy;
 1999  }
 2000   
 2001  /**
 2002  * Add a tag
 2003  *
 2004  * @param  {!number} type Tag's type
 2005  * @param  {!string} name Name of the tag
 2006  * @param  {!number} pos  Position of the tag in the text
 2007  * @param  {!number} len  Length of text consumed by the tag
 2008  * @param  {number}  prio Tags' priority
 2009  * @return {!Tag}
 2010  */
 2011  function addTag(type, name, pos, len, prio)
 2012  {
 2013      // Create the tag
 2014      var tag = new Tag(type, name, pos, len, prio || 0);
 2015   
 2016      // Set this tag's rules bitfield
 2017      if (tagsConfig[name])
 2018      {
 2019          tag.setFlags(tagsConfig[name].rules.flags);
 2020      }
 2021   
 2022      // Invalidate this tag if it's an unknown tag, a disabled tag, if either of its length or
 2023      // position is negative or if it's out of bounds
 2024      if (!tagsConfig[name] && !tag.isSystemTag())
 2025      {
 2026          tag.invalidate();
 2027      }
 2028      else if (tagsConfig[name] && tagsConfig[name].isDisabled)
 2029      {
 2030          logger.warn(
 2031              'Tag is disabled',
 2032              {
 2033                  'tag'     : tag,
 2034                  'tagName' : name
 2035              }
 2036          );
 2037          tag.invalidate();
 2038      }
 2039      else if (len < 0 || pos < 0 || pos + len > textLen)
 2040      {
 2041          tag.invalidate();
 2042      }
 2043      else
 2044      {
 2045          insertTag(tag);
 2046      }
 2047   
 2048      return tag;
 2049  }
 2050   
 2051  /**
 2052  * Insert given tag in the tag stack
 2053  *
 2054  * @param {!Tag} tag
 2055  */
 2056  function insertTag(tag)
 2057  {
 2058      if (!tagStackIsSorted)
 2059      {
 2060          tagStack.push(tag);
 2061      }
 2062      else
 2063      {
 2064          // Scan the stack and copy every tag to the next slot until we find the correct index
 2065          var i = tagStack.length;
 2066          while (i > 0 && compareTags(tagStack[i - 1], tag) > 0)
 2067          {
 2068              tagStack[i] = tagStack[i - 1];
 2069              --i;
 2070          }
 2071          tagStack[i] = tag;
 2072      }
 2073  }
 2074   
 2075  /**
 2076  * Add a pair of tags
 2077  *
 2078  * @param  {!string} name     Name of the tags
 2079  * @param  {!number} startPos Position of the start tag
 2080  * @param  {!number} startLen Length of the start tag
 2081  * @param  {!number} endPos   Position of the start tag
 2082  * @param  {!number} endLen   Length of the start tag
 2083  * @param  {number}  prio     Start tag's priority (the end tag will be set to minus that value)
 2084  * @return {!Tag}             Start tag
 2085  */
 2086  function addTagPair(name, startPos, startLen, endPos, endLen, prio)
 2087  {
 2088      // NOTE: the end tag is added first to try to keep the stack in the correct order
 2089      var endTag   = addEndTag(name, endPos, endLen, -prio || 0),
 2090          startTag = addStartTag(name, startPos, startLen, prio || 0);
 2091      startTag.pairWith(endTag);
 2092   
 2093      return startTag;
 2094  }
 2095   
 2096  /**
 2097  * Add a tag that represents a verbatim copy of the original text
 2098  *
 2099  * @param  {!number} pos  Position of the tag in the text
 2100  * @param  {!number} len  Length of text consumed by the tag
 2101  * @return {!Tag}
 2102  */
 2103  function addVerbatim(pos, len, prio)
 2104  {
 2105      return addTag(Tag.SELF_CLOSING_TAG, 'v', pos, len, prio || 0);
 2106  }
 2107   
 2108  /**
 2109  * Sort tags by position and precedence
 2110  */
 2111  function sortTags()
 2112  {
 2113      tagStack.sort(compareTags);
 2114      tagStackIsSorted = true;
 2115  }
 2116   
 2117  /**
 2118  * sortTags() callback
 2119  *
 2120  * Tags are stored as a stack, in LIFO order. We sort tags by position _descending_ so that they
 2121  * are processed in the order they appear in the text.
 2122  *
 2123  * @param  {!Tag}    a First tag to compare
 2124  * @param  {!Tag}    b Second tag to compare
 2125  * @return {!number}
 2126  */
 2127  function compareTags(a, b)
 2128  {
 2129      var aPos = a.getPos(),
 2130          bPos = b.getPos();
 2131   
 2132      // First we order by pos descending
 2133      if (aPos !== bPos)
 2134      {
 2135          return bPos - aPos;
 2136      }
 2137   
 2138      // If the tags start at the same position, we'll use their sortPriority if applicable. Tags
 2139      // with a lower value get sorted last, which means they'll be processed first. IOW, -10 is
 2140      // processed before 10
 2141      if (a.getSortPriority() !== b.getSortPriority())
 2142      {
 2143          return b.getSortPriority() - a.getSortPriority();
 2144      }
 2145   
 2146      // If the tags start at the same position and have the same priority, we'll sort them
 2147      // according to their length, with special considerations for  zero-width tags
 2148      var aLen = a.getLen(),
 2149          bLen = b.getLen();
 2150   
 2151      if (!aLen || !bLen)
 2152      {
 2153          // Zero-width end tags are ordered after zero-width start tags so that a pair that ends
 2154          // with a zero-width tag has the opportunity to be closed before another pair starts
 2155          // with a zero-width tag. For example, the pairs that would enclose each of the letters
 2156          // in the string "XY". Self-closing tags are ordered between end tags and start tags in
 2157          // an attempt to keep them out of tag pairs
 2158          if (!aLen && !bLen)
 2159          {
 2160              var order = {};
 2161              order[Tag.END_TAG]          = 0;
 2162              order[Tag.SELF_CLOSING_TAG] = 1;
 2163              order[Tag.START_TAG]        = 2;
 2164   
 2165              return order[b.getType()] - order[a.getType()];
 2166          }
 2167   
 2168          // Here, we know that only one of a or b is a zero-width tags. Zero-width tags are
 2169          // ordered after wider tags so that they have a chance to be processed before the next
 2170          // character is consumed, which would force them to be skipped
 2171          return (aLen) ? -1 : 1;
 2172      }
 2173   
 2174      // Here we know that both tags start at the same position and have a length greater than 0.
 2175      // We sort tags by length ascending, so that the longest matches are processed first. If
 2176      // their length is identical, the order is undefined as PHP's sort isn't stable
 2177      return aLen - bLen;
 2178  }