vendor/assets/javascripts/epiceditor.js.erb in epic-editor-rails-0.2.3 vs vendor/assets/javascripts/epiceditor.js.erb in epic-editor-rails-0.2.4

- old
+ new

@@ -11,11 +11,11 @@ * @returns {undefined} */ function _applyAttrs(context, attrs) { for (var attr in attrs) { if (attrs.hasOwnProperty(attr)) { - context[attr] = attrs[attr]; + context.setAttribute(attr, attrs[attr]); } } } /** @@ -335,11 +335,11 @@ , defaultContent: '' , autoSave: 100 // Set to false for no auto saving } , theme: { base: '<%= asset_path("base/epiceditor.css") %>' , preview: '<%= asset_path("preview/github.css") %>' - , editor: '<%= asset_path("editor/epic-light.css") %>' + , editor: '<%= asset_path("editor/epic-dark.css") %>' } , focusOnLoad: false , shortcut: { modifier: 18 // alt keycode , fullscreen: 70 // f keycode , preview: 80 // p keycode @@ -405,10 +405,20 @@ self.element = document.getElementById(self.settings.container); } else if (typeof self.settings.container == 'object') { self.element = self.settings.container; } + + if (typeof self.settings.textarea == 'undefined' && typeof self.element != 'undefined') { + var textareas = self.element.getElementsByTagName('textarea'); + if (textareas.length > 0) { + self.settings.textarea = textareas[0]; + _applyStyles(self.settings.textarea, { + display: 'none' + }); + } + } // Figure out the file name. If no file name is given we'll use the ID. // If there's no ID either we'll use a namespaced file name that's incremented // based on the calling order. As long as it doesn't change, drafts will be saved. if (!self.settings.file.name) { @@ -565,19 +575,25 @@ , previewer: '<div id="epiceditor-preview"></div>' , editor: '<!doctype HTML>' }; // Write an iframe and then select it for the editor - self.element.innerHTML = '<iframe scrolling="no" frameborder="0" id= "' + self._instanceId + '"></iframe>'; + iframeElement = document.createElement('iframe'); + _applyAttrs(iframeElement, { + scrolling: 'no', + frameborder: 0, + id: self._instanceId + }); + + + self.element.appendChild(iframeElement); // Because browsers add things like invisible padding and margins and stuff // to iframes, we need to set manually set the height so that the height // doesn't keep increasing (by 2px?) every time reflow() is called. // FIXME: Figure out how to fix this without setting this self.element.style.height = self.element.offsetHeight + 'px'; - - iframeElement = document.getElementById(self._instanceId); // Store a reference to the iframeElement itself self.iframeElement = iframeElement; // Grab the innards of the iframe (returns the document.body) @@ -682,21 +698,24 @@ utilBtns = self.iframe.getElementById('epiceditor-utilbar'); // TODO: Move into fullscreen setup function (_setupFullscreen) _elementStates = {} - self._goFullscreen = function (el) { + self._goFullscreen = function (el, callback) { + callback = callback || function () {}; + var wait = 0; this._fixScrollbars('auto'); if (self.is('fullscreen')) { - self._exitFullscreen(el); + self._exitFullscreen(el, callback); return; } if (nativeFs) { if (nativeFsWebkit) { el.webkitRequestFullScreen(); + wait = 750; } else if (nativeFsMoz) { el.mozRequestFullScreen(); } else if (nativeFsW3C) { @@ -704,89 +723,103 @@ } } _isInEdit = self.is('edit'); - // Set the state of EE in fullscreen - // We set edit and preview to true also because they're visible - // we might want to allow fullscreen edit mode without preview (like a "zen" mode) - self._eeState.fullscreen = true; - self._eeState.edit = true; - self._eeState.preview = true; - // Cache calculations - var windowInnerWidth = window.innerWidth - , windowInnerHeight = window.innerHeight - , windowOuterWidth = window.outerWidth - , windowOuterHeight = window.outerHeight; + // Why does this need to be in a randomly "750"ms setTimeout? WebKit's + // implementation of fullscreen seem to trigger the webkitfullscreenchange + // event _after_ everything is done. Instead, it triggers _during_ the + // transition. This means calculations of what's half, 100%, etc are wrong + // so to combat this we throw down the hammer with a setTimeout and wait + // to trigger our calculation code. + // See: https://code.google.com/p/chromium/issues/detail?id=181116 + setTimeout(function () { + // Set the state of EE in fullscreen + // We set edit and preview to true also because they're visible + // we might want to allow fullscreen edit mode without preview (like a "zen" mode) + self._eeState.fullscreen = true; + self._eeState.edit = true; + self._eeState.preview = true; - // Without this the scrollbars will get hidden when scrolled to the bottom in faux fullscreen (see #66) - if (!nativeFs) { - windowOuterHeight = window.innerHeight; - } + // Cache calculations + var windowInnerWidth = window.innerWidth + , windowInnerHeight = window.innerHeight + , windowOuterWidth = window.outerWidth + , windowOuterHeight = window.outerHeight; - // This MUST come first because the editor is 100% width so if we change the width of the iframe or wrapper - // the editor's width wont be the same as before - _elementStates.editorIframe = _saveStyleState(self.editorIframe, 'save', { - 'width': windowOuterWidth / 2 + 'px' - , 'height': windowOuterHeight + 'px' - , 'float': 'left' // Most browsers - , 'cssFloat': 'left' // FF - , 'styleFloat': 'left' // Older IEs - , 'display': 'block' - , 'position': 'static' - , 'left': '' - }); + // Without this the scrollbars will get hidden when scrolled to the bottom in faux fullscreen (see #66) + if (!nativeFs) { + windowOuterHeight = window.innerHeight; + } - // the previewer - _elementStates.previewerIframe = _saveStyleState(self.previewerIframe, 'save', { - 'width': windowOuterWidth / 2 + 'px' - , 'height': windowOuterHeight + 'px' - , 'float': 'right' // Most browsers - , 'cssFloat': 'right' // FF - , 'styleFloat': 'right' // Older IEs - , 'display': 'block' - , 'position': 'static' - , 'left': '' - }); + // This MUST come first because the editor is 100% width so if we change the width of the iframe or wrapper + // the editor's width wont be the same as before + _elementStates.editorIframe = _saveStyleState(self.editorIframe, 'save', { + 'width': windowOuterWidth / 2 + 'px' + , 'height': windowOuterHeight + 'px' + , 'float': 'left' // Most browsers + , 'cssFloat': 'left' // FF + , 'styleFloat': 'left' // Older IEs + , 'display': 'block' + , 'position': 'static' + , 'left': '' + }); - // Setup the containing element CSS for fullscreen - _elementStates.element = _saveStyleState(self.element, 'save', { - 'position': 'fixed' - , 'top': '0' - , 'left': '0' - , 'width': '100%' - , 'z-index': '9999' // Most browsers - , 'zIndex': '9999' // Firefox - , 'border': 'none' - , 'margin': '0' - // Should use the base styles background! - , 'background': _getStyle(self.editor, 'background-color') // Try to hide the site below - , 'height': windowInnerHeight + 'px' - }); + // the previewer + _elementStates.previewerIframe = _saveStyleState(self.previewerIframe, 'save', { + 'width': windowOuterWidth / 2 + 'px' + , 'height': windowOuterHeight + 'px' + , 'float': 'right' // Most browsers + , 'cssFloat': 'right' // FF + , 'styleFloat': 'right' // Older IEs + , 'display': 'block' + , 'position': 'static' + , 'left': '' + }); - // The iframe element - _elementStates.iframeElement = _saveStyleState(self.iframeElement, 'save', { - 'width': windowOuterWidth + 'px' - , 'height': windowInnerHeight + 'px' - }); + // Setup the containing element CSS for fullscreen + _elementStates.element = _saveStyleState(self.element, 'save', { + 'position': 'fixed' + , 'top': '0' + , 'left': '0' + , 'width': '100%' + , 'z-index': '9999' // Most browsers + , 'zIndex': '9999' // Firefox + , 'border': 'none' + , 'margin': '0' + // Should use the base styles background! + , 'background': _getStyle(self.editor, 'background-color') // Try to hide the site below + , 'height': windowInnerHeight + 'px' + }); - // ...Oh, and hide the buttons and prevent scrolling - utilBtns.style.visibility = 'hidden'; + // The iframe element + _elementStates.iframeElement = _saveStyleState(self.iframeElement, 'save', { + 'width': windowOuterWidth + 'px' + , 'height': windowInnerHeight + 'px' + }); - if (!nativeFs) { - document.body.style.overflow = 'hidden'; - } + // ...Oh, and hide the buttons and prevent scrolling + utilBtns.style.visibility = 'hidden'; - self.preview(); + if (!nativeFs) { + document.body.style.overflow = 'hidden'; + } - self.focus(); + self.preview(); - self.emit('fullscreenenter'); + self.focus(); + + self.emit('fullscreenenter'); + + callback.call(self); + }, wait); + }; - self._exitFullscreen = function (el) { + self._exitFullscreen = function (el, callback) { + callback = callback || function () {}; this._fixScrollbars(); _saveStyleState(self.element, 'apply', _elementStates.element); _saveStyleState(self.iframeElement, 'apply', _elementStates.iframeElement); _saveStyleState(self.editorIframe, 'apply', _elementStates.editorIframe); @@ -829,10 +862,12 @@ } self.reflow(); self.emit('fullscreenexit'); + + callback.call(self); }; // This setups up live previews by triggering preview() IF in fullscreen on keyup self.editor.addEventListener('keyup', function () { if (keypressTimer) { @@ -924,10 +959,11 @@ // Add keyboard shortcuts for convenience. function shortcutHandler(e) { if (e.keyCode == self.settings.shortcut.modifier) { isMod = true } // check for modifier press(default is alt key), save to var if (e.keyCode == 17) { isCtrl = true } // check for ctrl/cmnd press, in order to catch ctrl/cmnd + s + if (e.keyCode == 18) { isCtrl = false } // Check for alt+p and make sure were not in fullscreen - default shortcut to switch to preview if (isMod === true && e.keyCode == self.settings.shortcut.preview && !self.is('fullscreen')) { e.preventDefault(); if (self.is('edit') && self._previewEnabled) { @@ -1111,11 +1147,10 @@ return this; } EpicEditor.prototype._setupTextareaSync = function () { var self = this - , textareaFileName = self.settings.file.name , _syncTextarea; // Even if autoSave is false, we want to make sure to keep the textarea synced // with the editor's content. One bad thing about this tho is that we're // creating two timers now in some configurations. We keep the textarea synced @@ -1130,11 +1165,14 @@ _syncTextarea = function () { // TODO: Figure out root cause for having to do this ||. // This only happens for draft files. Probably has something to do with // the fact draft files haven't been saved by the time this is called. // TODO: Add test for this case. - self._textareaElement.value = self.exportFile(textareaFileName, 'text', true) || self.settings.file.defaultContent; + // Get the file.name each time as it can change. DO NOT save this to a + // var outside of this closure or the editor will stop syncing when the + // file is changed with importFile or open. + self._textareaElement.value = self.exportFile(self.settings.file.name, 'text', true) || self.settings.file.defaultContent; } if (typeof self.settings.textarea == 'string') { self._textareaElement = document.getElementById(self.settings.textarea); } @@ -1154,11 +1192,11 @@ // code. In this case, the textarea will take precedence. // // If the developer wants drafts to be recoverable they should check if // the local file in localStorage's modified date is newer than the server. if (self._textareaElement.value !== '') { - self.importFile(textareaFileName, self._textareaElement.value); + self.importFile(self.settings.file.name, self._textareaElement.value); // manually save draft after import so there is no delay between the // import and exporting in _syncTextarea. Without this, _syncTextarea // will pull the saved data from localStorage which will be <=100ms old. self.save(true); @@ -1167,10 +1205,12 @@ // Update the textarea on load and pull from drafts _syncTextarea(); // Make sure to keep it updated self.on('__update', _syncTextarea); + self.on('__create', _syncTextarea); + self.on('__save', _syncTextarea); } /** * Will NOT focus the editor if the editor is still starting up AND * focusOnLoad is set to false. This allows you to place this in code that @@ -1206,11 +1246,10 @@ self._eeState.loaded = false; self._eeState.unloaded = true; callback = callback || function () {}; if (self.settings.textarea) { - self._textareaElement.value = ""; self.removeListener('__update'); } if (self._saveIntervalTimer) { window.clearInterval(self._saveIntervalTimer); @@ -1338,23 +1377,31 @@ /** * Puts the editor into fullscreen mode * @returns {object} EpicEditor will be returned */ - EpicEditor.prototype.enterFullscreen = function () { - if (this.is('fullscreen')) { return this; } - this._goFullscreen(this.iframeElement); + EpicEditor.prototype.enterFullscreen = function (callback) { + callback = callback || function () {}; + if (this.is('fullscreen')) { + callback.call(this); + return this; + } + this._goFullscreen(this.iframeElement, callback); return this; } /** * Closes fullscreen mode if opened * @returns {object} EpicEditor will be returned */ - EpicEditor.prototype.exitFullscreen = function () { - if (!this.is('fullscreen')) { return this; } - this._exitFullscreen(this.iframeElement); + EpicEditor.prototype.exitFullscreen = function (callback) { + callback = callback || function () {}; + if (!this.is('fullscreen')) { + callback.call(this); + return this; + } + this._exitFullscreen(this.iframeElement, callback); return this; } /** * Hides the preview and shows the editor again @@ -1459,10 +1506,11 @@ */ EpicEditor.prototype.save = function (_isPreviewDraft, _isAuto) { var self = this , storage , isUpdate = false + , isNew = false , file = self.settings.file.name , previewDraftName = '' , data = this._storage[previewDraftName + self.settings.localStorageName] , content = _getText(this.editor); @@ -1480,10 +1528,11 @@ storage = JSON.parse(this._storage[previewDraftName + self.settings.localStorageName]); // If the file doesn't exist we need to create it if (storage[file] === undefined) { storage[file] = self._defaultFileSchema(); + isNew = true; } // If it does, we need to check if the content is different and // if it is, send the update event and update the timestamp else if (content !== storage[file].content) { @@ -1496,22 +1545,30 @@ } storage[file].content = content; this._storage[previewDraftName + self.settings.localStorageName] = JSON.stringify(storage); - // After the content is actually changed, emit update so it emits the updated content + // If it's a new file, send a create event as well as a private one for + // use internally. + if (isNew) { + self.emit('create'); + self.emit('__create'); + } + + // After the content is actually changed, emit update so it emits the + // updated content. Also send a private event for interal use. if (isUpdate) { self.emit('update'); - // Emit a private update event so it can't get accidentally removed self.emit('__update'); } if (_isAuto) { this.emit('autosave'); } else if (!_isPreviewDraft) { this.emit('save'); + self.emit('__save'); } } return this; } @@ -1561,30 +1618,21 @@ * @param {string} kind The kind of file you want to import (TBI) * @param {object} meta Meta data you want to save with your file. * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.importFile = function (name, content, kind, meta) { - var self = this - , isNew = false; + var self = this; name = name || self.settings.file.name; content = content || ''; kind = kind || 'md'; meta = meta || {}; - if (JSON.parse(this._storage[self.settings.localStorageName])[name] === undefined) { - isNew = true; - } - // Set our current file to the new file and update the content self.settings.file.name = name; _setText(self.editor, content); - if (isNew) { - self.emit('create'); - } - self.save(); if (self.is('fullscreen')) { self.preview(); } @@ -1841,16 +1889,19 @@ EpicEditor.version = '0.2.2'; // Used to store information to be shared across editors EpicEditor._data = {}; - window.EpicEditor = EpicEditor; + if (typeof window.define === 'function' && window.define.amd) { + window.define(function () { return EpicEditor; }); + } else { + window.EpicEditor = EpicEditor; + } })(window); - /** * marked - a markdown parser - * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed) + * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) * https://github.com/chjj/marked */ ;(function() { @@ -1863,17 +1914,17 @@ code: /^( {4}[^\n]+\n*)+/, fences: noop, hr: /^( *[-*_]){3,} *(?:\n+|$)/, heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, nptable: noop, - lheading: /^([^\n]+)\n *(=|-){3,} *\n*/, - blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/, - list: /^( *)(bull) [\s\S]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, - html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/, - def: /^ *\[([^\]]+)\]: *([^\s]+)(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, + lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, + blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, + list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/, + def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, table: noop, - paragraph: /^([^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+\n*/, + paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, text: /^[^\n]+/ }; block.bullet = /(?:[*+-]|\d+\.)/; block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; @@ -1881,17 +1932,22 @@ (/bull/g, block.bullet) (); block.list = replace(block.list) (/bull/g, block.bullet) - ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/) + ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))') + ('def', '\\n+(?=' + block.def.source + ')') (); +block.blockquote = replace(block.blockquote) + ('def', block.def) + (); + block._tag = '(?!(?:' + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' - + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|@)\\b'; + + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b'; block.html = replace(block.html) ('comment', /<!--[\s\S]*?-->/) ('closed', /<(tag)[\s\S]+?<\/\1>/) ('closing', /<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/) @@ -1916,16 +1972,18 @@ /** * GFM Block Grammar */ block.gfm = merge({}, block.normal, { - fences: /^ *(`{3,}|~{3,}) *(\w+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, + fences: /^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, paragraph: /^/ }); block.gfm.paragraph = replace(block.paragraph) - ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|') + ('(?!', '(?!' + + block.gfm.fences.source.replace('\\1', '\\2') + '|' + + block.list.source.replace('\\1', '\\3') + '|') (); /** * GFM + Tables Block Grammar */ @@ -1985,15 +2043,17 @@ /** * Lexing */ -Lexer.prototype.token = function(src, top) { +Lexer.prototype.token = function(src, top, bq) { var src = src.replace(/^ +$/gm, '') , next , loose , cap + , bull + , b , item , space , i , l; @@ -2106,11 +2166,11 @@ cap = cap[0].replace(/^ *> ?/gm, ''); // Pass `top` to keep the current // "toplevel" state. This is exactly // how markdown.pl works. - this.token(cap, top); + this.token(cap, top, true); this.tokens.push({ type: 'blockquote_end' }); @@ -2118,14 +2178,15 @@ } // list if (cap = this.rules.list.exec(src)) { src = src.substring(cap[0].length); + bull = cap[2]; this.tokens.push({ type: 'list_start', - ordered: isFinite(cap[2]) + ordered: bull.length > 1 }); // Get each top-level item. cap = cap[0].match(this.rules.item); @@ -2148,27 +2209,37 @@ item = !this.options.pedantic ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') : item.replace(/^ {1,4}/gm, ''); } + // Determine whether the next list item belongs here. + // Backpedal if it does not belong in this list. + if (this.options.smartLists && i !== l - 1) { + b = block.bullet.exec(cap[i + 1])[0]; + if (bull !== b && !(bull.length > 1 && b.length > 1)) { + src = cap.slice(i + 1).join('\n') + src; + i = l - 1; + } + } + // Determine whether item is loose or not. // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ // for discount behavior. loose = next || /\n\n(?!\s*$)/.test(item); if (i !== l - 1) { - next = item[item.length-1] === '\n'; + next = item.charAt(item.length - 1) === '\n'; if (!loose) loose = next; } this.tokens.push({ type: loose ? 'loose_item_start' : 'list_item_start' }); // Recurse. - this.token(item, false); + this.token(item, false, bq); this.tokens.push({ type: 'list_item_end' }); } @@ -2185,18 +2256,18 @@ src = src.substring(cap[0].length); this.tokens.push({ type: this.options.sanitize ? 'paragraph' : 'html', - pre: cap[1] === 'pre', + pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', text: cap[0] }); continue; } // def - if (top && (cap = this.rules.def.exec(src))) { + if ((!bq && top) && (cap = this.rules.def.exec(src))) { src = src.substring(cap[0].length); this.tokens.links[cap[1].toLowerCase()] = { href: cap[2], title: cap[3] }; @@ -2240,11 +2311,13 @@ // top-level paragraph if (top && (cap = this.rules.paragraph.exec(src))) { src = src.substring(cap[0].length); this.tokens.push({ type: 'paragraph', - text: cap[0] + text: cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1] }); continue; } // text @@ -2270,27 +2343,27 @@ /** * Inline-Level Grammar */ var inline = { - escape: /^\\([\\`*{}\[\]()#+\-.!_>|])/, + escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, url: noop, tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, link: /^!?\[(inside)\]\(href\)/, reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, - code: /^(`+)([\s\S]*?[^`])\1(?!`)/, + code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, br: /^ {2,}\n(?!\s*$)/, del: noop, text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/ }; -inline._inside = /(?:\[[^\]]*\]|[^\]]|\](?=[^\[]*\]))*/; -inline._href = /\s*<?([^\s]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/; +inline._inside = /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/; +inline._href = /\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/; inline.link = replace(inline.link) ('inside', inline._inside) ('href', inline._href) (); @@ -2317,13 +2390,13 @@ /** * GFM Inline Grammar */ inline.gfm = merge({}, inline.normal, { - escape: replace(inline.escape)('])', '~])')(), - url: /^(https?:\/\/[^\s]+[^.,:;"')\]\s])/, - del: /^~{2,}([\s\S]+?)~{2,}/, + escape: replace(inline.escape)('])', '~|])')(), + url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, + del: /^~~(?=\S)([\s\S]*?\S)~~/, text: replace(inline.text) (']|', '~]|') ('|', '|https?://|') () }); @@ -2343,10 +2416,12 @@ function InlineLexer(links, options) { this.options = options || marked.defaults; this.links = links; this.rules = inline.normal; + this.renderer = this.options.renderer || new Renderer; + this.renderer.options = this.options; if (!this.links) { throw new Error('Tokens array requires a `links` property.'); } @@ -2370,12 +2445,12 @@ /** * Static Lexing/Compiling Method */ -InlineLexer.output = function(src, links, opt) { - var inline = new InlineLexer(links, opt); +InlineLexer.output = function(src, links, options) { + var inline = new InlineLexer(links, options); return inline.output(src); }; /** * Lexing/Compiling @@ -2398,120 +2473,113 @@ // autolink if (cap = this.rules.autolink.exec(src)) { src = src.substring(cap[0].length); if (cap[2] === '@') { - text = cap[1][6] === ':' + text = cap[1].charAt(6) === ':' ? this.mangle(cap[1].substring(7)) : this.mangle(cap[1]); href = this.mangle('mailto:') + text; } else { text = escape(cap[1]); href = text; } - out += '<a href="' - + href - + '">' - + text - + '</a>'; + out += this.renderer.link(href, null, text); continue; } // url (gfm) - if (cap = this.rules.url.exec(src)) { + if (!this.inLink && (cap = this.rules.url.exec(src))) { src = src.substring(cap[0].length); text = escape(cap[1]); href = text; - out += '<a href="' - + href - + '">' - + text - + '</a>'; + out += this.renderer.link(href, null, text); continue; } // tag if (cap = this.rules.tag.exec(src)) { + if (!this.inLink && /^<a /i.test(cap[0])) { + this.inLink = true; + } else if (this.inLink && /^<\/a>/i.test(cap[0])) { + this.inLink = false; + } src = src.substring(cap[0].length); out += this.options.sanitize ? escape(cap[0]) : cap[0]; continue; } // link if (cap = this.rules.link.exec(src)) { src = src.substring(cap[0].length); + this.inLink = true; out += this.outputLink(cap, { href: cap[2], title: cap[3] }); + this.inLink = false; continue; } // reflink, nolink if ((cap = this.rules.reflink.exec(src)) || (cap = this.rules.nolink.exec(src))) { src = src.substring(cap[0].length); link = (cap[2] || cap[1]).replace(/\s+/g, ' '); link = this.links[link.toLowerCase()]; if (!link || !link.href) { - out += cap[0][0]; + out += cap[0].charAt(0); src = cap[0].substring(1) + src; continue; } + this.inLink = true; out += this.outputLink(cap, link); + this.inLink = false; continue; } // strong if (cap = this.rules.strong.exec(src)) { src = src.substring(cap[0].length); - out += '<strong>' - + this.output(cap[2] || cap[1]) - + '</strong>'; + out += this.renderer.strong(this.output(cap[2] || cap[1])); continue; } // em if (cap = this.rules.em.exec(src)) { src = src.substring(cap[0].length); - out += '<em>' - + this.output(cap[2] || cap[1]) - + '</em>'; + out += this.renderer.em(this.output(cap[2] || cap[1])); continue; } // code if (cap = this.rules.code.exec(src)) { src = src.substring(cap[0].length); - out += '<code>' - + escape(cap[2], true) - + '</code>'; + out += this.renderer.codespan(escape(cap[2], true)); continue; } // br if (cap = this.rules.br.exec(src)) { src = src.substring(cap[0].length); - out += '<br>'; + out += this.renderer.br(); continue; } // del (gfm) if (cap = this.rules.del.exec(src)) { src = src.substring(cap[0].length); - out += '<del>' - + this.output(cap[1]) - + '</del>'; + out += this.renderer.del(this.output(cap[1])); continue; } // text if (cap = this.rules.text.exec(src)) { src = src.substring(cap[0].length); - out += escape(cap[0]); + out += escape(this.smartypants(cap[0])); continue; } if (src) { throw new @@ -2525,38 +2593,40 @@ /** * Compile Link */ InlineLexer.prototype.outputLink = function(cap, link) { - if (cap[0][0] !== '!') { - return '<a href="' - + escape(link.href) - + '"' - + (link.title - ? ' title="' - + escape(link.title) - + '"' - : '') - + '>' - + this.output(cap[1]) - + '</a>'; - } else { - return '<img src="' - + escape(link.href) - + '" alt="' - + escape(cap[1]) - + '"' - + (link.title - ? ' title="' - + escape(link.title) - + '"' - : '') - + '>'; - } + var href = escape(link.href) + , title = link.title ? escape(link.title) : null; + + return cap[0].charAt(0) !== '!' + ? this.renderer.link(href, title, this.output(cap[1])) + : this.renderer.image(href, title, escape(cap[1])); }; /** + * Smartypants Transformations + */ + +InlineLexer.prototype.smartypants = function(text) { + if (!this.options.smartypants) return text; + return text + // em-dashes + .replace(/--/g, '\u2014') + // opening singles + .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') + // closing singles & apostrophes + .replace(/'/g, '\u2019') + // opening doubles + .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') + // closing doubles + .replace(/"/g, '\u201d') + // ellipses + .replace(/\.{3}/g, '\u2026'); +}; + +/** * Mangle Links */ InlineLexer.prototype.mangle = function(text) { var out = '' @@ -2574,34 +2644,180 @@ return out; }; /** + * Renderer + */ + +function Renderer(options) { + this.options = options || {}; +} + +Renderer.prototype.code = function(code, lang, escaped) { + if (this.options.highlight) { + var out = this.options.highlight(code, lang); + if (out != null && out !== code) { + escaped = true; + code = out; + } + } + + if (!lang) { + return '<pre><code>' + + (escaped ? code : escape(code, true)) + + '\n</code></pre>'; + } + + return '<pre><code class="' + + this.options.langPrefix + + escape(lang, true) + + '">' + + (escaped ? code : escape(code, true)) + + '\n</code></pre>\n'; +}; + +Renderer.prototype.blockquote = function(quote) { + return '<blockquote>\n' + quote + '</blockquote>\n'; +}; + +Renderer.prototype.html = function(html) { + return html; +}; + +Renderer.prototype.heading = function(text, level, raw) { + return '<h' + + level + + ' id="' + + this.options.headerPrefix + + raw.toLowerCase().replace(/[^\w]+/g, '-') + + '">' + + text + + '</h' + + level + + '>\n'; +}; + +Renderer.prototype.hr = function() { + return this.options.xhtml ? '<hr/>\n' : '<hr>\n'; +}; + +Renderer.prototype.list = function(body, ordered) { + var type = ordered ? 'ol' : 'ul'; + return '<' + type + '>\n' + body + '</' + type + '>\n'; +}; + +Renderer.prototype.listitem = function(text) { + return '<li>' + text + '</li>\n'; +}; + +Renderer.prototype.paragraph = function(text) { + return '<p>' + text + '</p>\n'; +}; + +Renderer.prototype.table = function(header, body) { + return '<table>\n' + + '<thead>\n' + + header + + '</thead>\n' + + '<tbody>\n' + + body + + '</tbody>\n' + + '</table>\n'; +}; + +Renderer.prototype.tablerow = function(content) { + return '<tr>\n' + content + '</tr>\n'; +}; + +Renderer.prototype.tablecell = function(content, flags) { + var type = flags.header ? 'th' : 'td'; + var tag = flags.align + ? '<' + type + ' style="text-align:' + flags.align + '">' + : '<' + type + '>'; + return tag + content + '</' + type + '>\n'; +}; + +// span level renderer +Renderer.prototype.strong = function(text) { + return '<strong>' + text + '</strong>'; +}; + +Renderer.prototype.em = function(text) { + return '<em>' + text + '</em>'; +}; + +Renderer.prototype.codespan = function(text) { + return '<code>' + text + '</code>'; +}; + +Renderer.prototype.br = function() { + return this.options.xhtml ? '<br/>' : '<br>'; +}; + +Renderer.prototype.del = function(text) { + return '<del>' + text + '</del>'; +}; + +Renderer.prototype.link = function(href, title, text) { + if (this.options.sanitize) { + try { + var prot = decodeURIComponent(unescape(href)) + .replace(/[^\w:]/g, '') + .toLowerCase(); + } catch (e) { + return ''; + } + if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0) { + return ''; + } + } + var out = '<a href="' + href + '"'; + if (title) { + out += ' title="' + title + '"'; + } + out += '>' + text + '</a>'; + return out; +}; + +Renderer.prototype.image = function(href, title, text) { + var out = '<img src="' + href + '" alt="' + text + '"'; + if (title) { + out += ' title="' + title + '"'; + } + out += this.options.xhtml ? '/>' : '>'; + return out; +}; + +/** * Parsing & Compiling */ function Parser(options) { this.tokens = []; this.token = null; this.options = options || marked.defaults; + this.options.renderer = this.options.renderer || new Renderer; + this.renderer = this.options.renderer; + this.renderer.options = this.options; } /** * Static Parse Method */ -Parser.parse = function(src, options) { - var parser = new Parser(options); +Parser.parse = function(src, options, renderer) { + var parser = new Parser(options, renderer); return parser.parse(src); }; /** * Parse Loop */ Parser.prototype.parse = function(src) { - this.inline = new InlineLexer(src.links, this.options); + this.inline = new InlineLexer(src.links, this.options, this.renderer); this.tokens = src.reverse(); var out = ''; while (this.next()) { out += this.tok(); @@ -2621,11 +2837,11 @@ /** * Preview Next Token */ Parser.prototype.peek = function() { - return this.tokens[this.tokens.length-1] || 0; + return this.tokens[this.tokens.length - 1] || 0; }; /** * Parse Text Tokens */ @@ -2648,146 +2864,108 @@ switch (this.token.type) { case 'space': { return ''; } case 'hr': { - return '<hr>\n'; + return this.renderer.hr(); } case 'heading': { - return '<h' - + this.token.depth - + '>' - + this.inline.output(this.token.text) - + '</h' - + this.token.depth - + '>\n'; + return this.renderer.heading( + this.inline.output(this.token.text), + this.token.depth, + this.token.text); } case 'code': { - if (this.options.highlight) { - var code = this.options.highlight(this.token.text, this.token.lang); - if (code != null && code !== this.token.text) { - this.token.escaped = true; - this.token.text = code; - } - } - - if (!this.token.escaped) { - this.token.text = escape(this.token.text, true); - } - - return '<pre><code' - + (this.token.lang - ? ' class="lang-' - + this.token.lang - + '"' - : '') - + '>' - + this.token.text - + '</code></pre>\n'; + return this.renderer.code(this.token.text, + this.token.lang, + this.token.escaped); } case 'table': { - var body = '' - , heading + var header = '' + , body = '' , i , row , cell + , flags , j; // header - body += '<thead>\n<tr>\n'; + cell = ''; for (i = 0; i < this.token.header.length; i++) { - heading = this.inline.output(this.token.header[i]); - body += this.token.align[i] - ? '<th align="' + this.token.align[i] + '">' + heading + '</th>\n' - : '<th>' + heading + '</th>\n'; + flags = { header: true, align: this.token.align[i] }; + cell += this.renderer.tablecell( + this.inline.output(this.token.header[i]), + { header: true, align: this.token.align[i] } + ); } - body += '</tr>\n</thead>\n'; + header += this.renderer.tablerow(cell); - // body - body += '<tbody>\n' for (i = 0; i < this.token.cells.length; i++) { row = this.token.cells[i]; - body += '<tr>\n'; + + cell = ''; for (j = 0; j < row.length; j++) { - cell = this.inline.output(row[j]); - body += this.token.align[j] - ? '<td align="' + this.token.align[j] + '">' + cell + '</td>\n' - : '<td>' + cell + '</td>\n'; + cell += this.renderer.tablecell( + this.inline.output(row[j]), + { header: false, align: this.token.align[j] } + ); } - body += '</tr>\n'; - } - body += '</tbody>\n'; - return '<table>\n' - + body - + '</table>\n'; + body += this.renderer.tablerow(cell); + } + return this.renderer.table(header, body); } case 'blockquote_start': { var body = ''; while (this.next().type !== 'blockquote_end') { body += this.tok(); } - return '<blockquote>\n' - + body - + '</blockquote>\n'; + return this.renderer.blockquote(body); } case 'list_start': { - var type = this.token.ordered ? 'ol' : 'ul' - , body = ''; + var body = '' + , ordered = this.token.ordered; while (this.next().type !== 'list_end') { body += this.tok(); } - return '<' - + type - + '>\n' - + body - + '</' - + type - + '>\n'; + return this.renderer.list(body, ordered); } case 'list_item_start': { var body = ''; while (this.next().type !== 'list_item_end') { body += this.token.type === 'text' ? this.parseText() : this.tok(); } - return '<li>' - + body - + '</li>\n'; + return this.renderer.listitem(body); } case 'loose_item_start': { var body = ''; while (this.next().type !== 'list_item_end') { body += this.tok(); } - return '<li>' - + body - + '</li>\n'; + return this.renderer.listitem(body); } case 'html': { - return !this.token.pre && !this.options.pedantic + var html = !this.token.pre && !this.options.pedantic ? this.inline.output(this.token.text) : this.token.text; + return this.renderer.html(html); } case 'paragraph': { - return '<p>' - + this.inline.output(this.token.text) - + '</p>\n'; + return this.renderer.paragraph(this.inline.output(this.token.text)); } case 'text': { - return '<p>' - + this.parseText() - + '</p>\n'; + return this.renderer.paragraph(this.parseText()); } } }; /** @@ -2801,10 +2979,23 @@ .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#39;'); } +function unescape(html) { + return html.replace(/&([#\w]+);/g, function(_, n) { + n = n.toLowerCase(); + if (n === 'colon') return ':'; + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' + ? String.fromCharCode(parseInt(n.substring(2), 16)) + : String.fromCharCode(+n.substring(1)); + } + return ''; + }); +} + function replace(regex, opt) { regex = regex.source; opt = opt || ''; return function self(name, val) { if (!name) return new RegExp(regex, opt); @@ -2833,21 +3024,94 @@ } return obj; } + /** * Marked */ -function marked(src, opt) { +function marked(src, opt, callback) { + if (callback || typeof opt === 'function') { + if (!callback) { + callback = opt; + opt = null; + } + + opt = merge({}, marked.defaults, opt || {}); + + var highlight = opt.highlight + , tokens + , pending + , i = 0; + + try { + tokens = Lexer.lex(src, opt) + } catch (e) { + return callback(e); + } + + pending = tokens.length; + + var done = function(err) { + if (err) { + opt.highlight = highlight; + return callback(err); + } + + var out; + + try { + out = Parser.parse(tokens, opt); + } catch (e) { + err = e; + } + + opt.highlight = highlight; + + return err + ? callback(err) + : callback(null, out); + }; + + if (!highlight || highlight.length < 3) { + return done(); + } + + delete opt.highlight; + + if (!pending) return done(); + + for (; i < tokens.length; i++) { + (function(token) { + if (token.type !== 'code') { + return --pending || done(); + } + return highlight(token.text, token.lang, function(err, code) { + if (err) return done(err); + if (code == null || code === token.text) { + return --pending || done(); + } + token.text = code; + token.escaped = true; + --pending || done(); + }); + })(tokens[i]); + } + + return; + } try { + if (opt) opt = merge({}, marked.defaults, opt); return Parser.parse(Lexer.lex(src, opt), opt); } catch (e) { e.message += '\nPlease report this to https://github.com/chjj/marked.'; if ((opt || marked.defaults).silent) { - return 'An error occured:\n' + e.message; + return '<p>An error occured:</p><pre>' + + escape(e.message + '', true) + + '</pre>'; } throw e; } } @@ -2855,39 +3119,47 @@ * Options */ marked.options = marked.setOptions = function(opt) { - marked.defaults = opt; + merge(marked.defaults, opt); return marked; }; marked.defaults = { gfm: true, tables: true, breaks: false, pedantic: false, sanitize: false, + smartLists: false, silent: false, - highlight: null + highlight: null, + langPrefix: 'lang-', + smartypants: false, + headerPrefix: '', + renderer: new Renderer, + xhtml: false }; /** * Expose */ marked.Parser = Parser; marked.parser = Parser.parse; +marked.Renderer = Renderer; + marked.Lexer = Lexer; marked.lexer = Lexer.lex; marked.InlineLexer = InlineLexer; marked.inlineLexer = InlineLexer.output; marked.parse = marked; -if (typeof module !== 'undefined') { +if (typeof module !== 'undefined' && typeof exports === 'object') { module.exports = marked; } else if (typeof define === 'function' && define.amd) { define(function() { return marked; }); } else { this.marked = marked;