/********** XHTML LEXER/PARSER **********/ /* * @name xml * @description Use these methods to generate XML and XHTML compliant tags and * escape tag attributes correctly * @author Bermi Ferrer - http://bermi.org * @author David Heinemeier Hansson http://loudthinking.com */ WYMeditor.XmlHelper = function() { this._entitiesDiv = document.createElement('div'); return this; }; /* * @name tag * @description * Returns an empty HTML tag of type *name* which by default is XHTML * compliant. Setting *open* to true will create an open tag compatible * with HTML 4.0 and below. Add HTML attributes by passing an attributes * array to *options*. For attributes with no value like (disabled and * readonly), give it a value of true in the *options* array. * * Examples: * * this.tag('br') * # =>
* this.tag ('br', false, true) * # =>
* this.tag ('input', $({type:'text',disabled:true }) ) * # => */ WYMeditor.XmlHelper.prototype.tag = function(name, options, open) { options = options || false; open = open || false; return '<'+name+(options ? this.tagOptions(options) : '')+(open ? '>' : ' />'); }; /* * @name contentTag * @description * Returns a XML block tag of type *name* surrounding the *content*. Add * XML attributes by passing an attributes array to *options*. For attributes * with no value like (disabled and readonly), give it a value of true in * the *options* array. You can use symbols or strings for the attribute names. * * this.contentTag ('p', 'Hello world!' ) * # =>

Hello world!

* this.contentTag('div', this.contentTag('p', "Hello world!"), $({class : "strong"})) * # =>

Hello world!

* this.contentTag("select", options, $({multiple : true})) * # => */ WYMeditor.XmlHelper.prototype.contentTag = function(name, content, options) { options = options || false; return '<'+name+(options ? this.tagOptions(options) : '')+'>'+content+''; }; /* * @name cdataSection * @description * Returns a CDATA section for the given +content+. CDATA sections * are used to escape blocks of text containing characters which would * otherwise be recognized as markup. CDATA sections begin with the string * <![CDATA[ and } with (and may not contain) the string * ]]>. */ WYMeditor.XmlHelper.prototype.cdataSection = function(content) { return ''; }; /* * @name escapeOnce * @description * Returns the escaped +xml+ without affecting existing escaped entities. * * this.escapeOnce( "1 > 2 & 3") * # => "1 > 2 & 3" */ WYMeditor.XmlHelper.prototype.escapeOnce = function(xml) { return this._fixDoubleEscape(this.escapeEntities(xml)); }; /* * @name _fixDoubleEscape * @description * Fix double-escaped entities, such as &amp;, &#123;, etc. */ WYMeditor.XmlHelper.prototype._fixDoubleEscape = function(escaped) { return escaped.replace(/&([a-z]+|(#\d+));/ig, "&$1;"); }; /* * @name tagOptions * @description * Takes an array like the one generated by Tag.parseAttributes * [["src", "http://www.editam.com/?a=b&c=d&f=g"], ["title", "Editam, CMS"]] * or an object like {src:"http://www.editam.com/?a=b&c=d&f=g", title:"Editam, CMS"} * and returns a string properly escaped like * ' src = "http://www.editam.com/?a=b&c=d&f=g" title = "Editam, <Simplified> CMS"' * which is valid for strict XHTML */ WYMeditor.XmlHelper.prototype.tagOptions = function(options) { var xml = this; xml._formated_options = ''; for (var key in options) { var formated_options = ''; var value = options[key]; if(typeof value != 'function' && value.length > 0) { if(parseInt(key) == key && typeof value == 'object'){ key = value.shift(); value = value.pop(); } if(key != '' && value != ''){ xml._formated_options += ' '+key+'="'+xml.escapeOnce(value)+'"'; } } } return xml._formated_options; }; /* * @name escapeEntities * @description * Escapes XML/HTML entities <, >, & and ". If seccond parameter is set to false it * will not escape ". If set to true it will also escape ' */ WYMeditor.XmlHelper.prototype.escapeEntities = function(string, escape_quotes) { this._entitiesDiv.innerHTML = string; this._entitiesDiv.textContent = string; var result = this._entitiesDiv.innerHTML; if(typeof escape_quotes == 'undefined'){ if(escape_quotes != false) result = result.replace('"', '"'); if(escape_quotes == true) result = result.replace('"', '''); } return result; }; /* * Parses a string conatining tag attributes and values an returns an array formated like * [["src", "http://www.editam.com"], ["title", "Editam, Simplified CMS"]] */ WYMeditor.XmlHelper.prototype.parseAttributes = function(tag_attributes) { // Use a compounded regex to match single quoted, double quoted and unquoted attribute pairs var result = []; var matches = tag_attributes.split(/((=\s*")(")("))|((=\s*\')(\')(\'))|((=\s*[^>\s]*))/g); if(matches.toString() != tag_attributes){ for (var k in matches) { var v = matches[k]; if(typeof v != 'function' && v.length != 0){ var re = new RegExp('(\\w+)\\s*'+v); if(match = tag_attributes.match(re) ){ var value = v.replace(/^[\s=]+/, ""); var delimiter = value.charAt(0); delimiter = delimiter == '"' ? '"' : (delimiter=="'"?"'":''); if(delimiter != ''){ value = delimiter == '"' ? value.replace(/^"|"+$/g, '') : value.replace(/^'|'+$/g, ''); } tag_attributes = tag_attributes.replace(match[0],''); result.push([match[1] , value]); } } } } return result; }; /** * Compounded regular expression. Any of * the contained patterns could match and * when one does, it's label is returned. * * Constructor. Starts with no patterns. * @param boolean case True for case sensitive, false * for insensitive. * @access public * @author Marcus Baker (http://lastcraft.com) * @author Bermi Ferrer (http://bermi.org) */ WYMeditor.ParallelRegex = function(case_sensitive) { this._case = case_sensitive; this._patterns = []; this._labels = []; this._regex = null; return this; }; /** * Adds a pattern with an optional label. * @param string pattern Perl style regex, but ( and ) * lose the usual meaning. * @param string label Label of regex to be returned * on a match. * @access public */ WYMeditor.ParallelRegex.prototype.addPattern = function(pattern, label) { label = label || true; var count = this._patterns.length; this._patterns[count] = pattern; this._labels[count] = label; this._regex = null; }; /** * Attempts to match all patterns at once against * a string. * @param string subject String to match against. * * @return boolean True on success. * @return string match First matched portion of * subject. * @access public */ WYMeditor.ParallelRegex.prototype.match = function(subject) { if (this._patterns.length == 0) { return [false, '']; } var matches = subject.match(this._getCompoundedRegex()); if(!matches){ return [false, '']; } var match = matches[0]; for (var i = 1; i < matches.length; i++) { if (matches[i]) { return [this._labels[i-1], match]; } } return [true, matches[0]]; }; /** * Compounds the patterns into a single * regular expression separated with the * "or" operator. Caches the regex. * Will automatically escape (, ) and / tokens. * @param array patterns List of patterns in order. * @access private */ WYMeditor.ParallelRegex.prototype._getCompoundedRegex = function() { if (this._regex == null) { for (var i = 0, count = this._patterns.length; i < count; i++) { this._patterns[i] = '(' + this._untokenizeRegex(this._tokenizeRegex(this._patterns[i]).replace(/([\/\(\)])/g,'\\$1')) + ')'; } this._regex = new RegExp(this._patterns.join("|") ,this._getPerlMatchingFlags()); } return this._regex; }; /** * Escape lookahead/lookbehind blocks */ WYMeditor.ParallelRegex.prototype._tokenizeRegex = function(regex) { return regex. replace(/\(\?(i|m|s|x|U)\)/, '~~~~~~Tk1\$1~~~~~~'). replace(/\(\?(\-[i|m|s|x|U])\)/, '~~~~~~Tk2\$1~~~~~~'). replace(/\(\?\=(.*)\)/, '~~~~~~Tk3\$1~~~~~~'). replace(/\(\?\!(.*)\)/, '~~~~~~Tk4\$1~~~~~~'). replace(/\(\?\<\=(.*)\)/, '~~~~~~Tk5\$1~~~~~~'). replace(/\(\?\<\!(.*)\)/, '~~~~~~Tk6\$1~~~~~~'). replace(/\(\?\:(.*)\)/, '~~~~~~Tk7\$1~~~~~~'); }; /** * Unscape lookahead/lookbehind blocks */ WYMeditor.ParallelRegex.prototype._untokenizeRegex = function(regex) { return regex. replace(/~~~~~~Tk1(.{1})~~~~~~/, "(?\$1)"). replace(/~~~~~~Tk2(.{2})~~~~~~/, "(?\$1)"). replace(/~~~~~~Tk3(.*)~~~~~~/, "(?=\$1)"). replace(/~~~~~~Tk4(.*)~~~~~~/, "(?!\$1)"). replace(/~~~~~~Tk5(.*)~~~~~~/, "(?<=\$1)"). replace(/~~~~~~Tk6(.*)~~~~~~/, "(?", 'Comment'); }; WYMeditor.XhtmlLexer.prototype.addScriptTokens = function(scope) { this.addEntryPattern("", 'Script'); }; WYMeditor.XhtmlLexer.prototype.addCssTokens = function(scope) { this.addEntryPattern("", 'Css'); }; WYMeditor.XhtmlLexer.prototype.addTagTokens = function(scope) { this.addSpecialPattern("<\\s*[a-z0-9:\-]+\\s*>", scope, 'OpeningTag'); this.addEntryPattern("<[a-z0-9:\-]+"+'[\\\/ \\\>]+', scope, 'OpeningTag'); this.addInTagDeclarationTokens('OpeningTag'); this.addSpecialPattern("", scope, 'ClosingTag'); }; WYMeditor.XhtmlLexer.prototype.addInTagDeclarationTokens = function(scope) { this.addSpecialPattern('\\s+', scope, 'Ignore'); this.addAttributeTokens(scope); this.addExitPattern('/>', scope); this.addExitPattern('>', scope); }; WYMeditor.XhtmlLexer.prototype.addAttributeTokens = function(scope) { this.addSpecialPattern("\\s*[a-z-_0-9]*:?[a-z-_0-9]+\\s*(?=\=)\\s*", scope, 'TagAttributes'); this.addEntryPattern('=\\s*"', scope, 'DoubleQuotedAttribute'); this.addPattern("\\\\\"", 'DoubleQuotedAttribute'); this.addExitPattern('"', 'DoubleQuotedAttribute'); this.addEntryPattern("=\\s*'", scope, 'SingleQuotedAttribute'); this.addPattern("\\\\'", 'SingleQuotedAttribute'); this.addExitPattern("'", 'SingleQuotedAttribute'); this.addSpecialPattern('=\\s*[^>\\s]*', scope, 'UnquotedAttribute'); }; /** * XHTML Parser. * * This XHTML parser will trigger the events available on on * current SaxListener * * @author Bermi Ferrer (http://bermi.org) */ WYMeditor.XhtmlParser = function(Listener, mode) { var mode = mode || 'Text'; this._Lexer = new WYMeditor.XhtmlLexer(this); this._Listener = Listener; this._mode = mode; this._matches = []; this._last_match = ''; this._current_match = ''; return this; }; WYMeditor.XhtmlParser.prototype.parse = function(raw) { this._Lexer.parse(this.beforeParsing(raw)); return this.afterParsing(this._Listener.getResult()); }; WYMeditor.XhtmlParser.prototype.beforeParsing = function(raw) { if(raw.match(/class="MsoNormal"/) || raw.match(/ns = "urn:schemas-microsoft-com/)){ // Useful for cleaning up content pasted from other sources (MSWord) this._Listener.avoidStylingTagsAndAttributes(); } return this._Listener.beforeParsing(raw); }; WYMeditor.XhtmlParser.prototype.afterParsing = function(parsed) { if(this._Listener._avoiding_tags_implicitly){ this._Listener.allowStylingTagsAndAttributes(); } return this._Listener.afterParsing(parsed); }; WYMeditor.XhtmlParser.prototype.Ignore = function(match, state) { return true; }; WYMeditor.XhtmlParser.prototype.Text = function(text) { this._Listener.addContent(text); return true; }; WYMeditor.XhtmlParser.prototype.Comment = function(match, status) { return this._addNonTagBlock(match, status, 'addComment'); }; WYMeditor.XhtmlParser.prototype.Script = function(match, status) { return this._addNonTagBlock(match, status, 'addScript'); }; WYMeditor.XhtmlParser.prototype.Css = function(match, status) { return this._addNonTagBlock(match, status, 'addCss'); }; WYMeditor.XhtmlParser.prototype._addNonTagBlock = function(match, state, type) { switch (state){ case WYMeditor.LEXER_ENTER: this._non_tag = match; break; case WYMeditor.LEXER_UNMATCHED: this._non_tag += match; break; case WYMeditor.LEXER_EXIT: switch(type) { case 'addComment': this._Listener.addComment(this._non_tag+match); break; case 'addScript': this._Listener.addScript(this._non_tag+match); break; case 'addCss': this._Listener.addCss(this._non_tag+match); break; } } return true; }; WYMeditor.XhtmlParser.prototype.OpeningTag = function(match, state) { switch (state){ case WYMeditor.LEXER_ENTER: this._tag = this.normalizeTag(match); this._tag_attributes = {}; break; case WYMeditor.LEXER_SPECIAL: this._callOpenTagListener(this.normalizeTag(match)); break; case WYMeditor.LEXER_EXIT: this._callOpenTagListener(this._tag, this._tag_attributes); } return true; }; WYMeditor.XhtmlParser.prototype.ClosingTag = function(match, state) { this._callCloseTagListener(this.normalizeTag(match)); return true; }; WYMeditor.XhtmlParser.prototype._callOpenTagListener = function(tag, attributes) { var attributes = attributes || {}; this.autoCloseUnclosedBeforeNewOpening(tag); if(this._Listener.isBlockTag(tag)){ this._Listener._tag_stack.push(tag); this._Listener.fixNestingBeforeOpeningBlockTag(tag, attributes); this._Listener.openBlockTag(tag, attributes); this._increaseOpenTagCounter(tag); }else if(this._Listener.isInlineTag(tag)){ this._Listener.inlineTag(tag, attributes); }else{ this._Listener.openUnknownTag(tag, attributes); this._increaseOpenTagCounter(tag); } this._Listener.last_tag = tag; this._Listener.last_tag_opened = true; this._Listener.last_tag_attributes = attributes; }; WYMeditor.XhtmlParser.prototype._callCloseTagListener = function(tag) { if(this._decreaseOpenTagCounter(tag)){ this.autoCloseUnclosedBeforeTagClosing(tag); if(this._Listener.isBlockTag(tag)){ var expected_tag = this._Listener._tag_stack.pop(); if(expected_tag == false){ return; }else if(expected_tag != tag){ tag = expected_tag; } this._Listener.closeBlockTag(tag); }else{ this._Listener.closeUnknownTag(tag); } }else{ this._Listener.closeUnopenedTag(tag); } this._Listener.last_tag = tag; this._Listener.last_tag_opened = false; }; WYMeditor.XhtmlParser.prototype._increaseOpenTagCounter = function(tag) { this._Listener._open_tags[tag] = this._Listener._open_tags[tag] || 0; this._Listener._open_tags[tag]++; }; WYMeditor.XhtmlParser.prototype._decreaseOpenTagCounter = function(tag) { if(this._Listener._open_tags[tag]){ this._Listener._open_tags[tag]--; if(this._Listener._open_tags[tag] == 0){ this._Listener._open_tags[tag] = undefined; } return true; } return false; }; WYMeditor.XhtmlParser.prototype.autoCloseUnclosedBeforeNewOpening = function(new_tag) { this._autoCloseUnclosed(new_tag, false); }; WYMeditor.XhtmlParser.prototype.autoCloseUnclosedBeforeTagClosing = function(tag) { this._autoCloseUnclosed(tag, true); }; WYMeditor.XhtmlParser.prototype._autoCloseUnclosed = function(new_tag, closing) { var closing = closing || false; if(this._Listener._open_tags){ for (var tag in this._Listener._open_tags) { var counter = this._Listener._open_tags[tag]; if(counter > 0 && this._Listener.shouldCloseTagAutomatically(tag, new_tag, closing)){ this._callCloseTagListener(tag, true); } } } }; WYMeditor.XhtmlParser.prototype.getTagReplacements = function() { return this._Listener.getTagReplacements(); }; WYMeditor.XhtmlParser.prototype.normalizeTag = function(tag) { tag = tag.replace(/^([\s<\/>]*)|([\s<\/>]*)$/gm,'').toLowerCase(); var tags = this._Listener.getTagReplacements(); if(tags[tag]){ return tags[tag]; } return tag; }; WYMeditor.XhtmlParser.prototype.TagAttributes = function(match, state) { if(WYMeditor.LEXER_SPECIAL == state){ this._current_attribute = match; } return true; }; WYMeditor.XhtmlParser.prototype.DoubleQuotedAttribute = function(match, state) { if(WYMeditor.LEXER_UNMATCHED == state){ this._tag_attributes[this._current_attribute] = match; } return true; }; WYMeditor.XhtmlParser.prototype.SingleQuotedAttribute = function(match, state) { if(WYMeditor.LEXER_UNMATCHED == state){ this._tag_attributes[this._current_attribute] = match; } return true; }; WYMeditor.XhtmlParser.prototype.UnquotedAttribute = function(match, state) { this._tag_attributes[this._current_attribute] = match.replace(/^=/,''); return true; }; /** * XHTML Sax parser. * * @author Bermi Ferrer (http://bermi.org) */ WYMeditor.XhtmlSaxListener = function() { this.output = ''; this.helper = new WYMeditor.XmlHelper(); this._open_tags = {}; this.validator = WYMeditor.XhtmlValidator; this._tag_stack = []; this.avoided_tags = ['area']; this.entities = { ' ':' ','¡':'¡','¢':'¢', '£':'£','¤':'¤','¥':'¥', '¦':'¦','§':'§','¨':'¨', '©':'©','ª':'ª','«':'«', '¬':'¬','­':'­','®':'®', '¯':'¯','°':'°','±':'±', '²':'²','³':'³','´':'´', 'µ':'µ','¶':'¶','·':'·', '¸':'¸','¹':'¹','º':'º', '»':'»','¼':'¼','½':'½', '¾':'¾','¿':'¿','À':'À', 'Á':'Á','Â':'Â','Ã':'Ã', 'Ä':'Ä','Å':'Å','Æ':'Æ', 'Ç':'Ç','È':'È','É':'É', 'Ê':'Ê','Ë':'Ë','Ì':'Ì', 'Í':'Í','Î':'Î','Ï':'Ï', 'Ð':'Ð','Ñ':'Ñ','Ò':'Ò', 'Ó':'Ó','Ô':'Ô','Õ':'Õ', 'Ö':'Ö','×':'×','Ø':'Ø', 'Ù':'Ù','Ú':'Ú','Û':'Û', 'Ü':'Ü','Ý':'Ý','Þ':'Þ', 'ß':'ß','à':'à','á':'á', 'â':'â','ã':'ã','ä':'ä', 'å':'å','æ':'æ','ç':'ç', 'è':'è','é':'é','ê':'ê', 'ë':'ë','ì':'ì','í':'í', 'î':'î','ï':'ï','ð':'ð', 'ñ':'ñ','ò':'ò','ó':'ó', 'ô':'ô','õ':'õ','ö':'ö', '÷':'÷','ø':'ø','ù':'ù', 'ú':'ú','û':'û','ü':'ü', 'ý':'ý','þ':'þ','ÿ':'ÿ', 'Œ':'Œ','œ':'œ','Š':'Š', 'š':'š','Ÿ':'Ÿ','ƒ':'ƒ', 'ˆ':'ˆ','˜':'˜','Α':'Α', 'Β':'Β','Γ':'Γ','Δ':'Δ', 'Ε':'Ε','Ζ':'Ζ','Η':'Η', 'Θ':'Θ','Ι':'Ι','Κ':'Κ', 'Λ':'Λ','Μ':'Μ','Ν':'Ν', 'Ξ':'Ξ','Ο':'Ο','Π':'Π', 'Ρ':'Ρ','Σ':'Σ','Τ':'Τ', 'Υ':'Υ','Φ':'Φ','Χ':'Χ', 'Ψ':'Ψ','Ω':'Ω','α':'α', 'β':'β','γ':'γ','δ':'δ', 'ε':'ε','ζ':'ζ','η':'η', 'θ':'θ','ι':'ι','κ':'κ', 'λ':'λ','μ':'μ','ν':'ν', 'ξ':'ξ','ο':'ο','π':'π', 'ρ':'ρ','ς':'ς','σ':'σ', 'τ':'τ','υ':'υ','φ':'φ', 'χ':'χ','ψ':'ψ','ω':'ω', 'ϑ':'ϑ','ϒ':'ϒ','ϖ':'ϖ', ' ':' ',' ':' ',' ':' ', '‌':'‌','‍':'‍','‎':'‎', '‏':'‏','–':'–','—':'—', '‘':'‘','’':'’','‚':'‚', '“':'“','”':'”','„':'„', '†':'†','‡':'‡','•':'•', '…':'…','‰':'‰','′':'′', '″':'″','‹':'‹','›':'›', '‾':'‾','⁄':'⁄','€':'€', 'ℑ':'ℑ','℘':'℘','ℜ':'ℜ', '™':'™','ℵ':'ℵ','←':'←', '↑':'↑','→':'→','↓':'↓', '↔':'↔','↵':'↵','⇐':'⇐', '⇑':'⇑','⇒':'⇒','⇓':'⇓', '⇔':'⇔','∀':'∀','∂':'∂', '∃':'∃','∅':'∅','∇':'∇', '∈':'∈','∉':'∉','∋':'∋', '∏':'∏','∑':'∑','−':'−', '∗':'∗','√':'√','∝':'∝', '∞':'∞','∠':'∠','∧':'∧', '∨':'∨','∩':'∩','∪':'∪', '∫':'∫','∴':'∴','∼':'∼', '≅':'≅','≈':'≈','≠':'≠', '≡':'≡','≤':'≤','≥':'≥', '⊂':'⊂','⊃':'⊃','⊄':'⊄', '⊆':'⊆','⊇':'⊇','⊕':'⊕', '⊗':'⊗','⊥':'⊥','⋅':'⋅', '⌈':'⌈','⌉':'⌉','⌊':'⌊', '⌋':'⌋','⟨':'〈','⟩':'〉', '◊':'◊','♠':'♠','♣':'♣', '♥':'♥','♦':'♦'}; this.block_tags = ["a", "abbr", "acronym", "address", "area", "b", "base", "bdo", "big", "blockquote", "body", "button", "caption", "cite", "code", "col", "colgroup", "dd", "del", "div", "dfn", "dl", "dt", "em", "fieldset", "form", "head", "h1", "h2", "h3", "h4", "h5", "h6", "html", "i", "iframe", "ins", "kbd", "label", "legend", "li", "map", "noscript", "object", "ol", "optgroup", "option", "p", "pre", "q", "samp", "script", "select", "small", "span", "strong", "style", "sub", "sup", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "title", "tr", "tt", "ul", "var", "extends", "meter", "section", "article", "aside", "details", "header", "footer", "nav", "dialog", "figure", "figcaption", "address", "hgroup", "mark", "time", "canvas", "audio", "video", "source", "output", "progress", "ruby", "rt", "rp", "summary", "command"<%= ", #{Refinery::Core.wymeditor_whitelist_tags.keys.map{|k| %Q{"#{k}"}}.join(', ')}" if Refinery::Core.wymeditor_whitelist_tags.any? %>]; // Defines self-closing tags. this.inline_tags = ["br", "embed", "hr", "img", "input", "param", "source", "wbr"]; return this; }; WYMeditor.XhtmlSaxListener.prototype.shouldCloseTagAutomatically = function(tag, now_on_tag, closing) { var closing = closing || false; if(tag == 'td'){ if((closing && now_on_tag == 'tr') || (!closing && now_on_tag == 'td')){ return true; } } if(tag == 'option'){ if((closing && now_on_tag == 'select') || (!closing && now_on_tag == 'option')){ return true; } } return false; }; WYMeditor.XhtmlSaxListener.prototype.beforeParsing = function(raw) { this.output = ''; return raw; }; WYMeditor.XhtmlSaxListener.prototype.afterParsing = function(xhtml) { xhtml = this.replaceNamedEntities(xhtml); xhtml = this.joinRepeatedEntities(xhtml); xhtml = this.removeEmptyTags(xhtml); return xhtml; }; WYMeditor.XhtmlSaxListener.prototype.replaceNamedEntities = function(xhtml) { for (var entity in this.entities) { xhtml = xhtml.replace(new RegExp(entity, 'g'), this.entities[entity]); } return xhtml; }; WYMeditor.XhtmlSaxListener.prototype.joinRepeatedEntities = function(xhtml) { var tags = 'em|strong|sub|sup|acronym|pre|del|address'; return xhtml.replace(new RegExp('<\/('+tags+')><\\1>' ,''),''). replace(new RegExp('(\s*<('+tags+')>\s*){2}(.*)(\s*<\/\\2>\s*){2}' ,''),'<\$2>\$3<\$2>'); }; WYMeditor.XhtmlSaxListener.prototype.removeEmptyTags = function(xhtml) { return xhtml.replace(new RegExp('<('+this.block_tags.join("|").replace(/\|td/,'').replace(/\|th/, '')+')>(
| | |\\s)*<\/\\1>' ,'g'),''); }; WYMeditor.XhtmlSaxListener.prototype.removeBrInPre = function(xhtml) { var matches = xhtml.match(new RegExp(']*>(.*?)<\/pre>','gmi')); if(matches) { for(var i=0; i', 'g'), String.fromCharCode(13,10))); } } return xhtml; }; WYMeditor.XhtmlSaxListener.prototype.getResult = function() { return this.output; }; WYMeditor.XhtmlSaxListener.prototype.getTagReplacements = function() { return {'b':'strong', 'i':'em'}; }; WYMeditor.XhtmlSaxListener.prototype.addContent = function(text) { this.output += text; }; WYMeditor.XhtmlSaxListener.prototype.addComment = function(text) { if(this.remove_comments){ this.output += text; } }; WYMeditor.XhtmlSaxListener.prototype.addScript = function(text) { if(!this.remove_scripts){ this.output += text; } }; WYMeditor.XhtmlSaxListener.prototype.addCss = function(text) { if(!this.remove_embeded_styles){ this.output += text; } }; WYMeditor.XhtmlSaxListener.prototype.openBlockTag = function(tag, attributes) { this.output += this.helper.tag(tag, this.validator.getValidTagAttributes(tag, attributes), true); }; WYMeditor.XhtmlSaxListener.prototype.inlineTag = function(tag, attributes) { this.output += this.helper.tag(tag, this.validator.getValidTagAttributes(tag, attributes)); }; WYMeditor.XhtmlSaxListener.prototype.openUnknownTag = function(tag, attributes) { if(tag === 'area') { this.output += this.helper.tag(tag, attributes, true); } }; WYMeditor.XhtmlSaxListener.prototype.closeBlockTag = function(tag) { this.output = this.output.replace(/
$/, '')+this._getClosingTagContent('before', tag)+""+this._getClosingTagContent('after', tag); }; WYMeditor.XhtmlSaxListener.prototype.closeUnknownTag = function(tag) { //this.output += ""; }; WYMeditor.XhtmlSaxListener.prototype.closeUnopenedTag = function(tag) { this.output += ""; }; WYMeditor.XhtmlSaxListener.prototype.avoidStylingTagsAndAttributes = function() { this.avoided_tags = ['div','span']; this.validator.skipped_attributes = ['style']; this.validator.skipped_attribute_values = ['MsoNormal','main1']; // MS Word attributes for class this._avoiding_tags_implicitly = true; }; WYMeditor.XhtmlSaxListener.prototype.allowStylingTagsAndAttributes = function() { this.avoided_tags = []; this.validator.skipped_attributes = []; this.validator.skipped_attribute_values = []; this._avoiding_tags_implicitly = false; }; WYMeditor.XhtmlSaxListener.prototype.isBlockTag = function(tag) { return !WYMeditor.Helper.contains(this.avoided_tags, tag) && WYMeditor.Helper.contains(this.block_tags, tag); }; WYMeditor.XhtmlSaxListener.prototype.isInlineTag = function(tag) { return !WYMeditor.Helper.contains(this.avoided_tags, tag) && WYMeditor.Helper.contains(this.inline_tags, tag); }; WYMeditor.XhtmlSaxListener.prototype.insertContentAfterClosingTag = function(tag, content) { this._insertContentWhenClosingTag('after', tag, content); }; WYMeditor.XhtmlSaxListener.prototype.insertContentBeforeClosingTag = function(tag, content) { this._insertContentWhenClosingTag('before', tag, content); }; WYMeditor.XhtmlSaxListener.prototype.fixNestingBeforeOpeningBlockTag = function(tag, attributes) { if(tag != 'li' && (tag == 'ul' || tag == 'ol') && this.last_tag && !this.last_tag_opened && this.last_tag == 'li'){ this.output = this.output.replace(/<\/li>$/, ''); this.insertContentAfterClosingTag(tag, ''); } }; WYMeditor.XhtmlSaxListener.prototype._insertContentWhenClosingTag = function(position, tag, content) { if(!this['_insert_'+position+'_closing']){ this['_insert_'+position+'_closing'] = []; } if(!this['_insert_'+position+'_closing'][tag]){ this['_insert_'+position+'_closing'][tag] = []; } this['_insert_'+position+'_closing'][tag].push(content); }; WYMeditor.XhtmlSaxListener.prototype._getClosingTagContent = function(position, tag) { if( this['_insert_'+position+'_closing'] && this['_insert_'+position+'_closing'][tag] && this['_insert_'+position+'_closing'][tag].length > 0){ return this['_insert_'+position+'_closing'][tag].pop(); } return ''; }; /********** CSS PARSER **********/ WYMeditor.WymCssLexer = function(parser, only_wym_blocks) { var only_wym_blocks = (typeof only_wym_blocks == 'undefined' ? true : only_wym_blocks); $.extend(this, new WYMeditor.Lexer(parser, (only_wym_blocks?'Ignore':'WymCss'))); this.mapHandler('WymCss', 'Ignore'); if(only_wym_blocks == true){ this.addEntryPattern("/\\\x2a[<\\s]*WYMeditor[>\\s]*\\\x2a/", 'Ignore', 'WymCss'); this.addExitPattern("/\\\x2a[<\/\\s]*WYMeditor[>\\s]*\\\x2a/", 'WymCss'); } this.addSpecialPattern("[\\sa-z1-6]*\\\x2e[a-z-_0-9]+", 'WymCss', 'WymCssStyleDeclaration'); this.addEntryPattern("/\\\x2a", 'WymCss', 'WymCssComment'); this.addExitPattern("\\\x2a/", 'WymCssComment'); this.addEntryPattern("\x7b", 'WymCss', 'WymCssStyle'); this.addExitPattern("\x7d", 'WymCssStyle'); this.addEntryPattern("/\\\x2a", 'WymCssStyle', 'WymCssFeedbackStyle'); this.addExitPattern("\\\x2a/", 'WymCssFeedbackStyle'); return this; }; WYMeditor.WymCssParser = function() { this._in_style = false; this._has_title = false; this.only_wym_blocks = true; this.css_settings = {'classesItems':[], 'editorStyles':[], 'dialogStyles':[]}; return this; }; WYMeditor.WymCssParser.prototype.parse = function(raw, only_wym_blocks) { var only_wym_blocks = (typeof only_wym_blocks == 'undefined' ? this.only_wym_blocks : only_wym_blocks); this._Lexer = new WYMeditor.WymCssLexer(this, only_wym_blocks); this._Lexer.parse(raw); }; WYMeditor.WymCssParser.prototype.Ignore = function(match, state) { return true; }; WYMeditor.WymCssParser.prototype.WymCssComment = function(text, status) { if(text.match(/end[a-z0-9\s]*wym[a-z0-9\s]*/mi)){ return false; } if(status == WYMeditor.LEXER_UNMATCHED){ if(!this._in_style){ this._has_title = true; this._current_item = {'title':WYMeditor.Helper.trim(text)}; }else{ if(this._current_item[this._current_element]){ if(!this._current_item[this._current_element].expressions){ this._current_item[this._current_element].expressions = [text]; }else{ this._current_item[this._current_element].expressions.push(text); } } } this._in_style = true; } return true; }; WYMeditor.WymCssParser.prototype.WymCssStyle = function(match, status) { if(status == WYMeditor.LEXER_UNMATCHED){ match = WYMeditor.Helper.trim(match); if(match != ''){ this._current_item[this._current_element].style = match; } }else if (status == WYMeditor.LEXER_EXIT){ this._in_style = false; this._has_title = false; this.addStyleSetting(this._current_item); } return true; }; WYMeditor.WymCssParser.prototype.WymCssFeedbackStyle = function(match, status) { if(status == WYMeditor.LEXER_UNMATCHED){ this._current_item[this._current_element].feedback_style = match.replace(/^([\s\/\*]*)|([\s\/\*]*)$/gm,''); } return true; }; WYMeditor.WymCssParser.prototype.WymCssStyleDeclaration = function(match) { match = match.replace(/^([\s\.]*)|([\s\.*]*)$/gm, ''); var tag = ''; if(match.indexOf('.') > 0){ var parts = match.split('.'); this._current_element = parts[1]; var tag = parts[0]; }else{ this._current_element = match; } if(!this._has_title){ this._current_item = {'title':(!tag?'':tag.toUpperCase()+': ')+this._current_element}; this._has_title = true; } if(!this._current_item[this._current_element]){ this._current_item[this._current_element] = {'name':this._current_element}; } if(tag){ if(!this._current_item[this._current_element].tags){ this._current_item[this._current_element].tags = [tag]; }else{ this._current_item[this._current_element].tags.push(tag); } } return true; }; WYMeditor.WymCssParser.prototype.addStyleSetting = function(style_details) { for (var name in style_details){ var details = style_details[name]; if(typeof details == 'object' && name != 'title'){ this.css_settings.classesItems.push({ 'name': WYMeditor.Helper.trim(details.name), 'title': style_details.title, 'expr' : WYMeditor.Helper.trim((details.expressions||details.tags).join(', ')) }); if(details.feedback_style){ this.css_settings.editorStyles.push({ 'name': '.'+ WYMeditor.Helper.trim(details.name), 'css': details.feedback_style }); } if(details.style){ this.css_settings.dialogStyles.push({ 'name': '.'+ WYMeditor.Helper.trim(details.name), 'css': details.style }); } } } }; /********** HELPERS **********/ // Returns true if it is a text node with whitespaces only $.fn.isPhantomNode = function() { if (this[0].nodeType == 3) return !(/[^\t\n\r ]/.test(this[0].data)); return false; }; WYMeditor.isPhantomNode = function(n) { if (n.nodeType == 3) return !(/[^\t\n\r ]/.test(n.data)); return false; }; WYMeditor.isPhantomString = function(str) { return !(/[^\t\n\r ]/.test(str)); }; // Returns the Parents or the node itself // jqexpr = a jQuery expression $.fn.parentsOrSelf = function(jqexpr) { var n = this; if (n[0].nodeType == 3) n = n.parents().slice(0,1); // if (n.is(jqexpr)) // XXX should work, but doesn't (probably a jQuery bug) if (n.filter(jqexpr).size() == 1) return n; else return n.parents(jqexpr).slice(0,1); }; // String & array helpers WYMeditor.Helper = { //replace all instances of 'old' by 'rep' in 'str' string replaceAll: function(str, old, rep) { return(str.replace(new RegExp(old, "g"), rep)); }, //insert 'inserted' at position 'pos' in 'str' string insertAt: function(str, inserted, pos) { return(str.substr(0,pos) + inserted + str.substring(pos)); }, //trim 'str' string trim: function(str) { return str.replace(/^(\s*)|(\s*)$/gm,''); }, //return true if 'arr' array contains 'elem', or false contains: function(arr, elem) { for (var i = 0; i < arr.length; i++) { if (arr[i] === elem) return true; } return false; }, //return 'item' position in 'arr' array, or -1 indexOf: function(arr, item) { var ret=-1; for(var i = 0; i < arr.length; i++) { if (arr[i] == item) { ret = i; break; } } return ret; }, //return 'item' object in 'arr' array, checking its 'name' property, or null findByName: function(arr, name) { for(var i = 0; i < arr.length; i++) { var item = arr[i]; if(item.name == name) return(item); } return null; } }; function titleize(words) { if (words == null) return words; parts = []; $.each(words.replace(/\./, '').replace(/[-_]/, ' ').split(' '), function(index, part){ parts.push(part.substring(0,1).toUpperCase() + part.substring(1)); }); return parts.join(" "); }