/** * SaxParser.js * * Copyright, Moxiecode Systems AB * Released under LGPL License. * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ (function(tinymce) { /** * This class parses HTML code using pure JavaScript and executes various events for each item it finds. It will * always execute the events in the right order for tag soup code like
. It will also remove elements * and attributes that doesn't fit the schema if the validate setting is enabled. * * @example * var parser = new tinymce.html.SaxParser({ * validate: true, * * comment: function(text) { * console.log('Comment:', text); * }, * * cdata: function(text) { * console.log('CDATA:', text); * }, * * text: function(text, raw) { * console.log('Text:', text, 'Raw:', raw); * }, * * start: function(name, attrs, empty) { * console.log('Start:', name, attrs, empty); * }, * * end: function(name) { * console.log('End:', name); * }, * * pi: function(name, text) { * console.log('PI:', name, text); * }, * * doctype: function(text) { * console.log('DocType:', text); * } * }, schema); * @class tinymce.html.SaxParser * @version 3.4 */ /** * Constructs a new SaxParser instance. * * @constructor * @method SaxParser * @param {Object} settings Name/value collection of settings. comment, cdata, text, start and end are callbacks. * @param {tinymce.html.Schema} schema HTML Schema class to use when parsing. */ tinymce.html.SaxParser = function(settings, schema) { var self = this, noop = function() {}; settings = settings || {}; self.schema = schema = schema || new tinymce.html.Schema(); if (settings.fix_self_closing !== false) settings.fix_self_closing = true; // Add handler functions from settings and setup default handlers tinymce.each('comment cdata text start end pi doctype'.split(' '), function(name) { if (name) self[name] = settings[name] || noop; }); /** * Parses the specified HTML string and executes the callbacks for each item it finds. * * @example * new SaxParser({...}).parse('text'); * @method parse * @param {String} html Html string to sax parse. */ self.parse = function(html) { var self = this, matches, index = 0, value, endRegExp, stack = [], attrList, i, text, name, isInternalElement, removeInternalElements, shortEndedElements, fillAttrsMap, isShortEnded, validate, elementRule, isValidElement, attr, attribsValue, invalidPrefixRegExp, validAttributesMap, validAttributePatterns, attributesRequired, attributesDefault, attributesForced, selfClosing, tokenRegExp, attrRegExp, specialElements, attrValue, idCount = 0, decode = tinymce.html.Entities.decode, fixSelfClosing, isIE; function processEndTag(name) { var pos, i; // Find position of parent of the same type pos = stack.length; while (pos--) { if (stack[pos].name === name) break; } // Found parent if (pos >= 0) { // Close all the open elements for (i = stack.length - 1; i >= pos; i--) { name = stack[i]; if (name.valid) self.end(name.name); } // Remove the open elements from the stack stack.length = pos; } }; function parseAttribute(match, name, value, val2, val3) { var attrRule, i; name = name.toLowerCase(); value = name in fillAttrsMap ? name : decode(value || val2 || val3 || ''); // Handle boolean attribute than value attribute // Validate name and value if (validate && !isInternalElement && name.indexOf('data-') !== 0) { attrRule = validAttributesMap[name]; // Find rule by pattern matching if (!attrRule && validAttributePatterns) { i = validAttributePatterns.length; while (i--) { attrRule = validAttributePatterns[i]; if (attrRule.pattern.test(name)) break; } // No rule matched if (i === -1) attrRule = null; } // No attribute rule found if (!attrRule) return; // Validate value if (attrRule.validValues && !(value in attrRule.validValues)) return; } // Add attribute to list and map attrList.map[name] = value; attrList.push({ name: name, value: value }); }; // Precompile RegExps and map objects tokenRegExp = new RegExp('<(?:' + '(?:!--([\\w\\W]*?)-->)|' + // Comment '(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|' + // CDATA '(?:!DOCTYPE([\\w\\W]*?)>)|' + // DOCTYPE '(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|' + // PI '(?:\\/([^>]+)>)|' + // End element '(?:([A-Za-z0-9\\-\\:\\.]+)((?:\\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\\/|\\s+)>)' + // Start element ')', 'g'); attrRegExp = /([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g; specialElements = { 'script' : /<\/script[^>]*>/gi, 'style' : /<\/style[^>]*>/gi, 'noscript' : /<\/noscript[^>]*>/gi }; // Setup lookup tables for empty elements and boolean attributes shortEndedElements = schema.getShortEndedElements(); selfClosing = settings.self_closing_elements || schema.getSelfClosingElements(); fillAttrsMap = schema.getBoolAttrs(); validate = settings.validate; removeInternalElements = settings.remove_internals; fixSelfClosing = settings.fix_self_closing; isIE = tinymce.isIE; invalidPrefixRegExp = /^:/; while (matches = tokenRegExp.exec(html)) { // Text if (index < matches.index) self.text(decode(html.substr(index, matches.index - index))); if (value = matches[6]) { // End element value = value.toLowerCase(); // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements if (isIE && invalidPrefixRegExp.test(value)) value = value.substr(1); processEndTag(value); } else if (value = matches[7]) { // Start element value = value.toLowerCase(); // IE will add a ":" in front of elements it doesn't understand like custom elements or HTML5 elements if (isIE && invalidPrefixRegExp.test(value)) value = value.substr(1); isShortEnded = value in shortEndedElements; // Is self closing tag for example an