From 7c3a80d9cf42611ed3b5b5b06464ef05b736f060 Mon Sep 17 00:00:00 2001 From: rylon Date: Thu, 3 Oct 2013 15:31:45 +0200 Subject: [PATCH] saimod documentation proto, workin markdown viewer --- autoload.inc.php | 6 +- docu/docu.php | 25 + docu/docu_categories.php | 6 + docu/register_sys_docu.php | 2 + docu/system/1.system.md | 3 + docu/system/2.license.md | 21 + docu/system/3.getting started.md | 0 docu/system/system.api.md | 0 docu/system/system.cache.md | 0 docu/system/system.config.md | 0 docu/system/system.db.md | 0 docu/system/system.docu.md | 0 docu/system/system.log.md | 0 docu/system/system.page.md | 0 docu/system/system.sai.md | 0 docu/system/system.security.md | 0 docu/system/system.system.md | 0 img/img.php | 16 + lib/EpicEditor/js/epiceditor.js | 2899 +++++++++++++++ lib/EpicEditor/js/epiceditor.min.js | 5 + lib/EpicEditor/themes/base/epiceditor.css | 70 + lib/EpicEditor/themes/editor/epic-dark.css | 13 + lib/EpicEditor/themes/editor/epic-light.css | 12 + lib/EpicEditor/themes/preview/bartik.css | 167 + lib/EpicEditor/themes/preview/github.css | 368 ++ .../themes/preview/preview-dark.css | 121 + lib/Michelf/Markdown.php | 3096 +++++++++++++++++ lib/Michelf/MarkdownExtra.php | 40 + lib/Michelf/extra/License.md | 36 + lib/Michelf/extra/Readme.md | 259 ++ lib/Michelf/extra/Readme.php | 31 + lib/Michelf/extra/composer.json | 31 + lib/autoload.inc.php | 2 + .../saimod_sys_docu/saimod_sys_docu.js | 16 + .../saimod_sys_docu/saimod_sys_docu.php | 42 +- sai/modules/saimod_sys_docu/tab.tpl | 12 + sai/modules/saimod_sys_docu/tab2.tpl | 8 + sai/modules/saimod_sys_docu/tabopt.tpl | 1 + sai/modules/saimod_sys_docu/tabs.tpl | 10 + 39 files changed, 7315 insertions(+), 3 deletions(-) create mode 100644 docu/docu.php create mode 100644 docu/docu_categories.php create mode 100644 docu/register_sys_docu.php create mode 100644 docu/system/1.system.md create mode 100644 docu/system/2.license.md create mode 100644 docu/system/3.getting started.md create mode 100644 docu/system/system.api.md create mode 100644 docu/system/system.cache.md create mode 100644 docu/system/system.config.md create mode 100644 docu/system/system.db.md create mode 100644 docu/system/system.docu.md create mode 100644 docu/system/system.log.md create mode 100644 docu/system/system.page.md create mode 100644 docu/system/system.sai.md create mode 100644 docu/system/system.security.md create mode 100644 docu/system/system.system.md create mode 100644 img/img.php create mode 100644 lib/EpicEditor/js/epiceditor.js create mode 100644 lib/EpicEditor/js/epiceditor.min.js create mode 100644 lib/EpicEditor/themes/base/epiceditor.css create mode 100644 lib/EpicEditor/themes/editor/epic-dark.css create mode 100644 lib/EpicEditor/themes/editor/epic-light.css create mode 100644 lib/EpicEditor/themes/preview/bartik.css create mode 100644 lib/EpicEditor/themes/preview/github.css create mode 100644 lib/EpicEditor/themes/preview/preview-dark.css create mode 100644 lib/Michelf/Markdown.php create mode 100644 lib/Michelf/MarkdownExtra.php create mode 100644 lib/Michelf/extra/License.md create mode 100644 lib/Michelf/extra/Readme.md create mode 100644 lib/Michelf/extra/Readme.php create mode 100644 lib/Michelf/extra/composer.json create mode 100644 lib/autoload.inc.php create mode 100644 sai/modules/saimod_sys_docu/saimod_sys_docu.js create mode 100644 sai/modules/saimod_sys_docu/tab.tpl create mode 100644 sai/modules/saimod_sys_docu/tab2.tpl create mode 100644 sai/modules/saimod_sys_docu/tabopt.tpl create mode 100644 sai/modules/saimod_sys_docu/tabs.tpl diff --git a/autoload.inc.php b/autoload.inc.php index 10c2b79..92fdd14 100644 --- a/autoload.inc.php +++ b/autoload.inc.php @@ -34,4 +34,8 @@ spl_autoload_register('__autoload_system'); \SYSTEM\autoload::registerFolder(dirname(__FILE__).'/cache','SYSTEM\CACHE'); -\SYSTEM\autoload::registerFolder(dirname(__FILE__).'/sai','SYSTEM\SAI'); \ No newline at end of file +\SYSTEM\autoload::registerFolder(dirname(__FILE__).'/sai','SYSTEM\SAI'); +\SYSTEM\autoload::registerFolder(dirname(__FILE__).'/docu','SYSTEM\DOCU'); + +require_once dirname(__FILE__).'/lib/autoload.inc.php'; +require_once dirname(__FILE__).'/docu/register_sys_docu.php'; \ No newline at end of file diff --git a/docu/docu.php b/docu/docu.php new file mode 100644 index 0000000..e073f91 --- /dev/null +++ b/docu/docu.php @@ -0,0 +1,25 @@ + tag specifically for CSS + * @param {string} path The path to the CSS file + * @param {object} context In what context you want to apply this to (document, iframe, etc) + * @param {string} id An id for you to reference later for changing properties of the + * @returns {undefined} + */ + function _insertCSSLink(path, context, id) { + id = id || ''; + var headID = context.getElementsByTagName("head")[0] + , cssNode = context.createElement('link'); + + _applyAttrs(cssNode, { + type: 'text/css' + , id: id + , rel: 'stylesheet' + , href: path + , name: path + , media: 'screen' + }); + + headID.appendChild(cssNode); + } + + // Simply replaces a class (o), to a new class (n) on an element provided (e) + function _replaceClass(e, o, n) { + e.className = e.className.replace(o, n); + } + + // Feature detects an iframe to get the inner document for writing to + function _getIframeInnards(el) { + return el.contentDocument || el.contentWindow.document; + } + + // Grabs the text from an element and preserves whitespace + function _getText(el) { + var theText; + // Make sure to check for type of string because if the body of the page + // doesn't have any text it'll be "" which is falsey and will go into + // the else which is meant for Firefox and shit will break + if (typeof document.body.innerText == 'string') { + theText = el.innerText; + } + else { + // First replace
s before replacing the rest of the HTML + theText = el.innerHTML.replace(/
/gi, "\n"); + // Now we can clean the HTML + theText = theText.replace(/<(?:.|\n)*?>/gm, ''); + // Now fix HTML entities + theText = theText.replace(/</gi, '<'); + theText = theText.replace(/>/gi, '>'); + } + return theText; + } + + function _setText(el, content) { + // Don't convert lt/gt characters as HTML when viewing the editor window + // TODO: Write a test to catch regressions for this + content = content.replace(//g, '>'); + content = content.replace(/\n/g, '
'); + + // Make sure to there aren't two spaces in a row (replace one with  ) + // If you find and replace every space with a   text will not wrap. + // Hence the name (Non-Breaking-SPace). + // TODO: Probably need to test this somehow... + content = content.replace(/
\s/g, '
 ') + content = content.replace(/\s\s\s/g, '   ') + content = content.replace(/\s\s/g, '  ') + content = content.replace(/^ /, ' ') + + el.innerHTML = content; + return true; + } + + /** + * Converts the 'raw' format of a file's contents into plaintext + * @param {string} content Contents of the file + * @returns {string} the sanitized content + */ + function _sanitizeRawContent(content) { + // Get this, 2 spaces in a content editable actually converts to: + // 0020 00a0, meaning, "space no-break space". So, manually convert + // no-break spaces to spaces again before handing to marked. + // Also, WebKit converts no-break to unicode equivalent and FF HTML. + return content.replace(/\u00a0/g, ' ').replace(/ /g, ' '); + } + + /** + * Will return the version number if the browser is IE. If not will return -1 + * TRY NEVER TO USE THIS AND USE FEATURE DETECTION IF POSSIBLE + * @returns {Number} -1 if false or the version number if true + */ + function _isIE() { + var rv = -1 // Return value assumes failure. + , ua = navigator.userAgent + , re; + if (navigator.appName == 'Microsoft Internet Explorer') { + re = /MSIE ([0-9]{1,}[\.0-9]{0,})/; + if (re.exec(ua) != null) { + rv = parseFloat(RegExp.$1, 10); + } + } + return rv; + } + + /** + * Same as the isIE(), but simply returns a boolean + * THIS IS TERRIBLE AND IS ONLY USED BECAUSE FULLSCREEN IN SAFARI IS BORKED + * If some other engine uses WebKit and has support for fullscreen they + * probably wont get native fullscreen until Safari's fullscreen is fixed + * @returns {Boolean} true if Safari + */ + function _isSafari() { + var n = window.navigator; + return n.userAgent.indexOf('Safari') > -1 && n.userAgent.indexOf('Chrome') == -1; + } + + /** + * Same as the isIE(), but simply returns a boolean + * THIS IS TERRIBLE ONLY USE IF ABSOLUTELY NEEDED + * @returns {Boolean} true if Safari + */ + function _isFirefox() { + var n = window.navigator; + return n.userAgent.indexOf('Firefox') > -1 && n.userAgent.indexOf('Seamonkey') == -1; + } + + /** + * Determines if supplied value is a function + * @param {object} object to determine type + */ + function _isFunction(functionToCheck) { + var getType = {}; + return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; + } + + /** + * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1 + * @param {boolean} [deepMerge=false] If true, will deep merge meaning it will merge sub-objects like {obj:obj2{foo:'bar'}} + * @param {object} first object + * @param {object} second object + * @returnss {object} a new object based on obj1 and obj2 + */ + function _mergeObjs() { + // copy reference to target object + var target = arguments[0] || {} + , i = 1 + , length = arguments.length + , deep = false + , options + , name + , src + , copy + + // Handle a deep copy situation + if (typeof target === "boolean") { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if (typeof target !== "object" && !_isFunction(target)) { + target = {}; + } + // extend jQuery itself if only one argument is passed + if (length === i) { + target = this; + --i; + } + + for (; i < length; i++) { + // Only deal with non-null/undefined values + if ((options = arguments[i]) != null) { + // Extend the base object + for (name in options) { + // @NOTE: added hasOwnProperty check + if (options.hasOwnProperty(name)) { + src = target[name]; + copy = options[name]; + // Prevent never-ending loop + if (target === copy) { + continue; + } + // Recurse if we're merging object values + if (deep && copy && typeof copy === "object" && !copy.nodeType) { + target[name] = _mergeObjs(deep, + // Never move original objects, clone them + src || (copy.length != null ? [] : {}) + , copy); + } else if (copy !== undefined) { // Don't bring in undefined values + target[name] = copy; + } + } + } + } + } + + // Return the modified object + return target; + } + + /** + * Initiates the EpicEditor object and sets up offline storage as well + * @class Represents an EpicEditor instance + * @param {object} options An optional customization object + * @returns {object} EpicEditor will be returned + */ + function EpicEditor(options) { + // Default settings will be overwritten/extended by options arg + var self = this + , opts = options || {} + , _defaultFileSchema + , _defaultFile + , defaults = { container: 'epiceditor' + , basePath: 'epiceditor' + , textarea: undefined + , clientSideStorage: true + , localStorageName: 'epiceditor' + , useNativeFullscreen: true + , file: { name: null + , defaultContent: '' + , autoSave: 100 // Set to false for no auto saving + } + , theme: { base: '/themes/base/epiceditor.css' + , preview: '/themes/preview/github.css' + , editor: '/themes/editor/epic-dark.css' + } + , focusOnLoad: false + , shortcut: { modifier: 18 // alt keycode + , fullscreen: 70 // f keycode + , preview: 80 // p keycode + } + , string: { togglePreview: 'Toggle Preview Mode' + , toggleEdit: 'Toggle Edit Mode' + , toggleFullscreen: 'Enter Fullscreen' + } + , parser: typeof marked == 'function' ? marked : null + , autogrow: false + , button: { fullscreen: true + , preview: true + , bar: "auto" + } + } + , defaultStorage + , autogrowDefaults = { minHeight: 80 + , maxHeight: false + , scroll: true + }; + + self.settings = _mergeObjs(true, defaults, opts); + + var buttons = self.settings.button; + self._fullscreenEnabled = typeof(buttons) === 'object' ? typeof buttons.fullscreen === 'undefined' || buttons.fullscreen : buttons === true; + self._editEnabled = typeof(buttons) === 'object' ? typeof buttons.edit === 'undefined' || buttons.edit : buttons === true; + self._previewEnabled = typeof(buttons) === 'object' ? typeof buttons.preview === 'undefined' || buttons.preview : buttons === true; + + if (!(typeof self.settings.parser == 'function' && typeof self.settings.parser('TEST') == 'string')) { + self.settings.parser = function (str) { + return str; + } + } + + if (self.settings.autogrow) { + if (self.settings.autogrow === true) { + self.settings.autogrow = autogrowDefaults; + } + else { + self.settings.autogrow = _mergeObjs(true, autogrowDefaults, self.settings.autogrow); + } + self._oldHeight = -1; + } + + // If you put an absolute link as the path of any of the themes ignore the basePath + // preview theme + if (!self.settings.theme.preview.match(/^https?:\/\//)) { + self.settings.theme.preview = self.settings.basePath + self.settings.theme.preview; + } + // editor theme + if (!self.settings.theme.editor.match(/^https?:\/\//)) { + self.settings.theme.editor = self.settings.basePath + self.settings.theme.editor; + } + // base theme + if (!self.settings.theme.base.match(/^https?:\/\//)) { + self.settings.theme.base = self.settings.basePath + self.settings.theme.base; + } + + // Grab the container element and save it to self.element + // if it's a string assume it's an ID and if it's an object + // assume it's a DOM element + if (typeof self.settings.container == 'string') { + self.element = document.getElementById(self.settings.container); + } + else if (typeof self.settings.container == 'object') { + self.element = self.settings.container; + } + + // 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) { + if (typeof self.settings.container == 'string') { + self.settings.file.name = self.settings.container; + } + else if (typeof self.settings.container == 'object') { + if (self.element.id) { + self.settings.file.name = self.element.id; + } + else { + if (!EpicEditor._data.unnamedEditors) { + EpicEditor._data.unnamedEditors = []; + } + EpicEditor._data.unnamedEditors.push(self); + self.settings.file.name = '__epiceditor-untitled-' + EpicEditor._data.unnamedEditors.length; + } + } + } + + if (self.settings.button.bar === "show") { + self.settings.button.bar = true; + } + + if (self.settings.button.bar === "hide") { + self.settings.button.bar = false; + } + + // Protect the id and overwrite if passed in as an option + // TODO: Put underscrore to denote that this is private + self._instanceId = 'epiceditor-' + Math.round(Math.random() * 100000); + self._storage = {}; + self._canSave = true; + + // Setup local storage of files + self._defaultFileSchema = function () { + return { + content: self.settings.file.defaultContent + , created: new Date() + , modified: new Date() + } + } + + if (localStorage && self.settings.clientSideStorage) { + this._storage = localStorage; + if (this._storage[self.settings.localStorageName] && self.getFiles(self.settings.file.name) === undefined) { + _defaultFile = self._defaultFileSchema(); + _defaultFile.content = self.settings.file.defaultContent; + } + } + + if (!this._storage[self.settings.localStorageName]) { + defaultStorage = {}; + defaultStorage[self.settings.file.name] = self._defaultFileSchema(); + defaultStorage = JSON.stringify(defaultStorage); + this._storage[self.settings.localStorageName] = defaultStorage; + } + + // A string to prepend files with to save draft versions of files + // and reset all preview drafts on each load! + self._previewDraftLocation = '__draft-'; + self._storage[self._previewDraftLocation + self.settings.localStorageName] = self._storage[self.settings.localStorageName]; + + // This needs to replace the use of classes to check the state of EE + self._eeState = { + fullscreen: false + , preview: false + , edit: false + , loaded: false + , unloaded: false + } + + // Now that it exists, allow binding of events if it doesn't exist yet + if (!self.events) { + self.events = {}; + } + + return this; + } + + /** + * Inserts the EpicEditor into the DOM via an iframe and gets it ready for editing and previewing + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.load = function (callback) { + + // Get out early if it's already loaded + if (this.is('loaded')) { return this; } + + // TODO: Gotta get the privates with underscores! + // TODO: Gotta document what these are for... + var self = this + , _HtmlTemplates + , iframeElement + , baseTag + , utilBtns + , utilBar + , utilBarTimer + , keypressTimer + , mousePos = { y: -1, x: -1 } + , _elementStates + , _isInEdit + , nativeFs = false + , nativeFsWebkit = false + , nativeFsMoz = false + , nativeFsW3C = false + , fsElement + , isMod = false + , isCtrl = false + , eventableIframes + , i // i is reused for loops + , boundAutogrow; + + // Startup is a way to check if this EpicEditor is starting up. Useful for + // checking and doing certain things before EpicEditor emits a load event. + self._eeState.startup = true; + + if (self.settings.useNativeFullscreen) { + nativeFsWebkit = document.body.webkitRequestFullScreen ? true : false; + nativeFsMoz = document.body.mozRequestFullScreen ? true : false; + nativeFsW3C = document.body.requestFullscreen ? true : false; + nativeFs = nativeFsWebkit || nativeFsMoz || nativeFsW3C; + } + + // Fucking Safari's native fullscreen works terribly + // REMOVE THIS IF SAFARI 7 WORKS BETTER + if (_isSafari()) { + nativeFs = false; + nativeFsWebkit = false; + } + + // It opens edit mode by default (for now); + if (!self.is('edit') && !self.is('preview')) { + self._eeState.edit = true; + } + + callback = callback || function () {}; + + // The editor HTML + // TODO: edit-mode class should be dynamically added + _HtmlTemplates = { + // This is wrapping iframe element. It contains the other two iframes and the utilbar + chrome: '
' + + '' + + '' + + '
' + + (self._previewEnabled ? ' ' : '') + + (self._editEnabled ? ' ' : '') + + (self._fullscreenEnabled ? '' : '') + + '
' + + '
' + + // The previewer is just an empty box for the generated HTML to go into + , previewer: '
' + , editor: '' + }; + + // Write an iframe and then select it for the editor + self.element.innerHTML = ''; + + // 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) + // TODO: Change self.iframe to self.iframeDocument + self.iframe = _getIframeInnards(iframeElement); + self.iframe.open(); + self.iframe.write(_HtmlTemplates.chrome); + + // Now that we got the innards of the iframe, we can grab the other iframes + self.editorIframe = self.iframe.getElementById('epiceditor-editor-frame') + self.previewerIframe = self.iframe.getElementById('epiceditor-previewer-frame'); + + // Setup the editor iframe + self.editorIframeDocument = _getIframeInnards(self.editorIframe); + self.editorIframeDocument.open(); + // Need something for... you guessed it, Firefox + self.editorIframeDocument.write(_HtmlTemplates.editor); + self.editorIframeDocument.close(); + + // Setup the previewer iframe + self.previewerIframeDocument = _getIframeInnards(self.previewerIframe); + self.previewerIframeDocument.open(); + self.previewerIframeDocument.write(_HtmlTemplates.previewer); + + // Base tag is added so that links will open a new tab and not inside of the iframes + baseTag = self.previewerIframeDocument.createElement('base'); + baseTag.target = '_blank'; + self.previewerIframeDocument.getElementsByTagName('head')[0].appendChild(baseTag); + + self.previewerIframeDocument.close(); + + self.reflow(); + + // Insert Base Stylesheet + _insertCSSLink(self.settings.theme.base, self.iframe, 'theme'); + + // Insert Editor Stylesheet + _insertCSSLink(self.settings.theme.editor, self.editorIframeDocument, 'theme'); + + // Insert Previewer Stylesheet + _insertCSSLink(self.settings.theme.preview, self.previewerIframeDocument, 'theme'); + + // Add a relative style to the overall wrapper to keep CSS relative to the editor + self.iframe.getElementById('epiceditor-wrapper').style.position = 'relative'; + + // Set the position to relative so we hide them with left: -999999px + self.editorIframe.style.position = 'absolute'; + self.previewerIframe.style.position = 'absolute'; + + // Now grab the editor and previewer for later use + self.editor = self.editorIframeDocument.body; + self.previewer = self.previewerIframeDocument.getElementById('epiceditor-preview'); + + self.editor.contentEditable = true; + + // Firefox's gets all fucked up so, to be sure, we need to hardcode it + self.iframe.body.style.height = this.element.offsetHeight + 'px'; + + // Should actually check what mode it's in! + self.previewerIframe.style.left = '-999999px'; + + // Keep long lines from being longer than the editor + this.editorIframeDocument.body.style.wordWrap = 'break-word'; + + // FIXME figure out why it needs +2 px + if (_isIE() > -1) { + this.previewer.style.height = parseInt(_getStyle(this.previewer, 'height'), 10) + 2; + } + + // If there is a file to be opened with that filename and it has content... + this.open(self.settings.file.name); + + if (self.settings.focusOnLoad) { + // We need to wait until all three iframes are done loading by waiting until the parent + // iframe's ready state == complete, then we can focus on the contenteditable + self.iframe.addEventListener('readystatechange', function () { + if (self.iframe.readyState == 'complete') { + self.focus(); + } + }); + } + + // Because IE scrolls the whole window to hash links, we need our own + // method of scrolling the iframe to an ID from clicking a hash + self.previewerIframeDocument.addEventListener('click', function (e) { + var el = e.target + , body = self.previewerIframeDocument.body; + if (el.nodeName == 'A') { + // Make sure the link is a hash and the link is local to the iframe + if (el.hash && el.hostname == window.location.hostname) { + // Prevent the whole window from scrolling + e.preventDefault(); + // Prevent opening a new window + el.target = '_self'; + // Scroll to the matching element, if an element exists + if (body.querySelector(el.hash)) { + body.scrollTop = body.querySelector(el.hash).offsetTop; + } + } + } + }); + + utilBtns = self.iframe.getElementById('epiceditor-utilbar'); + + // TODO: Move into fullscreen setup function (_setupFullscreen) + _elementStates = {} + self._goFullscreen = function (el) { + this._fixScrollbars('auto'); + + if (self.is('fullscreen')) { + self._exitFullscreen(el); + return; + } + + if (nativeFs) { + if (nativeFsWebkit) { + el.webkitRequestFullScreen(); + } + else if (nativeFsMoz) { + el.mozRequestFullScreen(); + } + else if (nativeFsW3C) { + el.requestFullscreen(); + } + } + + _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; + + // Without this the scrollbars will get hidden when scrolled to the bottom in faux fullscreen (see #66) + if (!nativeFs) { + windowOuterHeight = window.innerHeight; + } + + // 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': '' + }); + + // 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': '' + }); + + // 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 iframe element + _elementStates.iframeElement = _saveStyleState(self.iframeElement, 'save', { + 'width': windowOuterWidth + 'px' + , 'height': windowInnerHeight + 'px' + }); + + // ...Oh, and hide the buttons and prevent scrolling + utilBtns.style.visibility = 'hidden'; + + if (!nativeFs) { + document.body.style.overflow = 'hidden'; + } + + self.preview(); + + self.focus(); + + self.emit('fullscreenenter'); + }; + + self._exitFullscreen = function (el) { + this._fixScrollbars(); + + _saveStyleState(self.element, 'apply', _elementStates.element); + _saveStyleState(self.iframeElement, 'apply', _elementStates.iframeElement); + _saveStyleState(self.editorIframe, 'apply', _elementStates.editorIframe); + _saveStyleState(self.previewerIframe, 'apply', _elementStates.previewerIframe); + + // We want to always revert back to the original styles in the CSS so, + // if it's a fluid width container it will expand on resize and not get + // stuck at a specific width after closing fullscreen. + self.element.style.width = self._eeState.reflowWidth ? self._eeState.reflowWidth : ''; + self.element.style.height = self._eeState.reflowHeight ? self._eeState.reflowHeight : ''; + + utilBtns.style.visibility = 'visible'; + + // Put the editor back in the right state + // TODO: This is ugly... how do we make this nicer? + // setting fullscreen to false here prevents the + // native fs callback from calling this function again + self._eeState.fullscreen = false; + + if (!nativeFs) { + document.body.style.overflow = 'auto'; + } + else { + if (nativeFsWebkit) { + document.webkitCancelFullScreen(); + } + else if (nativeFsMoz) { + document.mozCancelFullScreen(); + } + else if (nativeFsW3C) { + document.exitFullscreen(); + } + } + + if (_isInEdit) { + self.edit(); + } + else { + self.preview(); + } + + self.reflow(); + + self.emit('fullscreenexit'); + }; + + // This setups up live previews by triggering preview() IF in fullscreen on keyup + self.editor.addEventListener('keyup', function () { + if (keypressTimer) { + window.clearTimeout(keypressTimer); + } + keypressTimer = window.setTimeout(function () { + if (self.is('fullscreen')) { + self.preview(); + } + }, 250); + }); + + fsElement = self.iframeElement; + + // Sets up the onclick event on utility buttons + utilBtns.addEventListener('click', function (e) { + var targetClass = e.target.className; + if (targetClass.indexOf('epiceditor-toggle-preview-btn') > -1) { + self.preview(); + } + else if (targetClass.indexOf('epiceditor-toggle-edit-btn') > -1) { + self.edit(); + } + else if (targetClass.indexOf('epiceditor-fullscreen-btn') > -1) { + self._goFullscreen(fsElement); + } + }); + + // Sets up the NATIVE fullscreen editor/previewer for WebKit + if (nativeFsWebkit) { + document.addEventListener('webkitfullscreenchange', function () { + if (!document.webkitIsFullScreen && self._eeState.fullscreen) { + self._exitFullscreen(fsElement); + } + }, false); + } + else if (nativeFsMoz) { + document.addEventListener('mozfullscreenchange', function () { + if (!document.mozFullScreen && self._eeState.fullscreen) { + self._exitFullscreen(fsElement); + } + }, false); + } + else if (nativeFsW3C) { + document.addEventListener('fullscreenchange', function () { + if (document.fullscreenElement == null && self._eeState.fullscreen) { + self._exitFullscreen(fsElement); + } + }, false); + } + + // TODO: Move utilBar stuff into a utilBar setup function (_setupUtilBar) + utilBar = self.iframe.getElementById('epiceditor-utilbar'); + + // Hide it at first until they move their mouse + if (self.settings.button.bar !== true) { + utilBar.style.display = 'none'; + } + + utilBar.addEventListener('mouseover', function () { + if (utilBarTimer) { + clearTimeout(utilBarTimer); + } + }); + + function utilBarHandler(e) { + if (self.settings.button.bar !== "auto") { + return; + } + // Here we check if the mouse has moves more than 5px in any direction before triggering the mousemove code + // we do this for 2 reasons: + // 1. On Mac OS X lion when you scroll and it does the iOS like "jump" when it hits the top/bottom of the page itll fire off + // a mousemove of a few pixels depending on how hard you scroll + // 2. We give a slight buffer to the user in case he barely touches his touchpad or mouse and not trigger the UI + if (Math.abs(mousePos.y - e.pageY) >= 5 || Math.abs(mousePos.x - e.pageX) >= 5) { + utilBar.style.display = 'block'; + // if we have a timer already running, kill it out + if (utilBarTimer) { + clearTimeout(utilBarTimer); + } + + // begin a new timer that hides our object after 1000 ms + utilBarTimer = window.setTimeout(function () { + utilBar.style.display = 'none'; + }, 1000); + } + mousePos = { y: e.pageY, x: e.pageX }; + } + + // 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 + + // 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) { + self.preview(); + } + else if (self._editEnabled) { + self.edit(); + } + } + // Check for alt+f - default shortcut to make editor fullscreen + if (isMod === true && e.keyCode == self.settings.shortcut.fullscreen && self._fullscreenEnabled) { + e.preventDefault(); + self._goFullscreen(fsElement); + } + + // Set the modifier key to false once *any* key combo is completed + // or else, on Windows, hitting the alt key will lock the isMod state to true (ticket #133) + if (isMod === true && e.keyCode !== self.settings.shortcut.modifier) { + isMod = false; + } + + // When a user presses "esc", revert everything! + if (e.keyCode == 27 && self.is('fullscreen')) { + self._exitFullscreen(fsElement); + } + + // Check for ctrl + s (since a lot of people do it out of habit) and make it do nothing + if (isCtrl === true && e.keyCode == 83) { + self.save(); + e.preventDefault(); + isCtrl = false; + } + + // Do the same for Mac now (metaKey == cmd). + if (e.metaKey && e.keyCode == 83) { + self.save(); + e.preventDefault(); + } + + } + + function shortcutUpHandler(e) { + if (e.keyCode == self.settings.shortcut.modifier) { isMod = false } + if (e.keyCode == 17) { isCtrl = false } + } + + function pasteHandler(e) { + var content; + if (e.clipboardData) { + //FF 22, Webkit, "standards" + e.preventDefault(); + content = e.clipboardData.getData("text/plain"); + self.editorIframeDocument.execCommand("insertText", false, content); + } + else if (window.clipboardData) { + //IE, "nasty" + e.preventDefault(); + content = window.clipboardData.getData("Text"); + content = content.replace(//g, '>'); + content = content.replace(/\n/g, '
'); + content = content.replace(/\r/g, ''); //fuck you, ie! + content = content.replace(/
\s/g, '
 ') + content = content.replace(/\s\s\s/g, '   ') + content = content.replace(/\s\s/g, '  ') + self.editorIframeDocument.selection.createRange().pasteHTML(content); + } + } + + // Hide and show the util bar based on mouse movements + eventableIframes = [self.previewerIframeDocument, self.editorIframeDocument]; + + for (i = 0; i < eventableIframes.length; i++) { + eventableIframes[i].addEventListener('mousemove', function (e) { + utilBarHandler(e); + }); + eventableIframes[i].addEventListener('scroll', function (e) { + utilBarHandler(e); + }); + eventableIframes[i].addEventListener('keyup', function (e) { + shortcutUpHandler(e); + }); + eventableIframes[i].addEventListener('keydown', function (e) { + shortcutHandler(e); + }); + eventableIframes[i].addEventListener('paste', function (e) { + pasteHandler(e); + }); + } + + // Save the document every 100ms by default + // TODO: Move into autosave setup function (_setupAutoSave) + if (self.settings.file.autoSave) { + self._saveIntervalTimer = window.setInterval(function () { + if (!self._canSave) { + return; + } + self.save(false, true); + }, self.settings.file.autoSave); + } + + // Update a textarea automatically if a textarea is given so you don't need + // AJAX to submit a form and instead fall back to normal form behavior + if (self.settings.textarea) { + self._setupTextareaSync(); + } + + window.addEventListener('resize', function () { + // If NOT webkit, and in fullscreen, we need to account for browser resizing + // we don't care about webkit because you can't resize in webkit's fullscreen + if (self.is('fullscreen')) { + _applyStyles(self.iframeElement, { + 'width': window.outerWidth + 'px' + , 'height': window.innerHeight + 'px' + }); + + _applyStyles(self.element, { + 'height': window.innerHeight + 'px' + }); + + _applyStyles(self.previewerIframe, { + 'width': window.outerWidth / 2 + 'px' + , 'height': window.innerHeight + 'px' + }); + + _applyStyles(self.editorIframe, { + 'width': window.outerWidth / 2 + 'px' + , 'height': window.innerHeight + 'px' + }); + } + // Makes the editor support fluid width when not in fullscreen mode + else if (!self.is('fullscreen')) { + self.reflow(); + } + }); + + // Set states before flipping edit and preview modes + self._eeState.loaded = true; + self._eeState.unloaded = false; + + if (self.is('preview')) { + self.preview(); + } + else { + self.edit(); + } + + self.iframe.close(); + self._eeState.startup = false; + + if (self.settings.autogrow) { + self._fixScrollbars(); + + boundAutogrow = function () { + setTimeout(function () { + self._autogrow(); + }, 1); + }; + + //for if autosave is disabled or very slow + ['keydown', 'keyup', 'paste', 'cut'].forEach(function (ev) { + self.getElement('editor').addEventListener(ev, boundAutogrow); + }); + + self.on('__update', boundAutogrow); + self.on('edit', function () { + setTimeout(boundAutogrow, 50) + }); + self.on('preview', function () { + setTimeout(boundAutogrow, 50) + }); + + //for browsers that have rendering delays + setTimeout(boundAutogrow, 50); + boundAutogrow(); + } + + // The callback and call are the same thing, but different ways to access them + callback.call(this); + this.emit('load'); + 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 + // by saving and opening the textarea content from the draft file storage. + self._textareaSaveTimer = window.setInterval(function () { + if (!self._canSave) { + return; + } + self.save(true); + }, 100); + + _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; + } + + if (typeof self.settings.textarea == 'string') { + self._textareaElement = document.getElementById(self.settings.textarea); + } + else if (typeof self.settings.textarea == 'object') { + self._textareaElement = self.settings.textarea; + } + + // On page load, if there's content in the textarea that means one of two + // different things: + // + // 1. The editor didn't load and the user was writing in the textarea and + // now he refreshed the page or the JS loaded and the textarea now has + // content. If this is the case the user probably expects his content is + // moved into the editor and not lose what he typed. + // + // 2. The developer put content in the textarea from some server side + // 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); + + // 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); + } + + // Update the textarea on load and pull from drafts + _syncTextarea(); + + // Make sure to keep it updated + self.on('__update', _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 + * gets fired during .load() without worrying about it overriding the user's + * option. For example use cases see preview() and edit(). + * @returns {undefined} + */ + + // Prevent focus when the user sets focusOnLoad to false by checking if the + // editor is starting up AND if focusOnLoad is true + EpicEditor.prototype._focusExceptOnLoad = function () { + var self = this; + if ((self._eeState.startup && self.settings.focusOnLoad) || !self._eeState.startup) { + self.focus(); + } + } + + /** + * Will remove the editor, but not offline files + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.unload = function (callback) { + + // Make sure the editor isn't already unloaded. + if (this.is('unloaded')) { + throw new Error('Editor isn\'t loaded'); + } + + var self = this + , editor = window.parent.document.getElementById(self._instanceId); + + editor.parentNode.removeChild(editor); + 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); + } + if (self._textareaSaveTimer) { + window.clearInterval(self._textareaSaveTimer); + } + + callback.call(this); + self.emit('unload'); + return self; + } + + /** + * reflow allows you to dynamically re-fit the editor in the parent without + * having to unload and then reload the editor again. + * + * reflow will also emit a `reflow` event and will return the new dimensions. + * If it's called without params it'll return the new width and height and if + * it's called with just width or just height it'll just return the width or + * height. It's returned as an object like: { width: '100px', height: '1px' } + * + * @param {string|null} kind Can either be 'width' or 'height' or null + * if null, both the height and width will be resized + * @param {function} callback A function to fire after the reflow is finished. + * Will return the width / height in an obj as the first param of the callback. + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.reflow = function (kind, callback) { + var self = this + , widthDiff = _outerWidth(self.element) - self.element.offsetWidth + , heightDiff = _outerHeight(self.element) - self.element.offsetHeight + , elements = [self.iframeElement, self.editorIframe, self.previewerIframe] + , eventData = {} + , newWidth + , newHeight; + + if (typeof kind == 'function') { + callback = kind; + kind = null; + } + + if (!callback) { + callback = function () {}; + } + + for (var x = 0; x < elements.length; x++) { + if (!kind || kind == 'width') { + newWidth = self.element.offsetWidth - widthDiff + 'px'; + elements[x].style.width = newWidth; + self._eeState.reflowWidth = newWidth; + eventData.width = newWidth; + } + if (!kind || kind == 'height') { + newHeight = self.element.offsetHeight - heightDiff + 'px'; + elements[x].style.height = newHeight; + self._eeState.reflowHeight = newHeight + eventData.height = newHeight; + } + } + + self.emit('reflow', eventData); + callback.call(this, eventData); + return self; + } + + /** + * Will take the markdown and generate a preview view based on the theme + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.preview = function () { + var self = this + , x + , theme = self.settings.theme.preview + , anchors; + + _replaceClass(self.getElement('wrapper'), 'epiceditor-edit-mode', 'epiceditor-preview-mode'); + + // Check if no CSS theme link exists + if (!self.previewerIframeDocument.getElementById('theme')) { + _insertCSSLink(theme, self.previewerIframeDocument, 'theme'); + } + else if (self.previewerIframeDocument.getElementById('theme').name !== theme) { + self.previewerIframeDocument.getElementById('theme').href = theme; + } + + // Save a preview draft since it might not be saved to the real file yet + self.save(true); + + // Add the generated draft HTML into the previewer + self.previewer.innerHTML = self.exportFile(null, 'html', true); + + // Hide the editor and display the previewer + if (!self.is('fullscreen')) { + self.editorIframe.style.left = '-999999px'; + self.previewerIframe.style.left = ''; + self._eeState.preview = true; + self._eeState.edit = false; + self._focusExceptOnLoad(); + } + + self.emit('preview'); + return self; + } + + /** + * Helper to focus on the editor iframe. Will figure out which iframe to + * focus on based on which one is active and will handle the cross browser + * issues with focusing on the iframe vs the document body. + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.focus = function (pageload) { + var self = this + , isPreview = self.is('preview') + , focusElement = isPreview ? self.previewerIframeDocument.body + : self.editorIframeDocument.body; + + if (_isFirefox() && isPreview) { + focusElement = self.previewerIframe; + } + + focusElement.focus(); + return this; + } + + /** + * 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); + 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); + return this; + } + + /** + * Hides the preview and shows the editor again + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.edit = function () { + var self = this; + _replaceClass(self.getElement('wrapper'), 'epiceditor-preview-mode', 'epiceditor-edit-mode'); + self._eeState.preview = false; + self._eeState.edit = true; + self.editorIframe.style.left = ''; + self.previewerIframe.style.left = '-999999px'; + self._focusExceptOnLoad(); + self.emit('edit'); + return this; + } + + /** + * Grabs a specificed HTML node. Use it as a shortcut to getting the iframe contents + * @param {String} name The name of the node (can be document, body, editor, previewer, or wrapper) + * @returns {Object|Null} + */ + EpicEditor.prototype.getElement = function (name) { + var available = { + "container": this.element + , "wrapper": this.iframe.getElementById('epiceditor-wrapper') + , "wrapperIframe": this.iframeElement + , "editor": this.editorIframeDocument + , "editorIframe": this.editorIframe + , "previewer": this.previewerIframeDocument + , "previewerIframe": this.previewerIframe + } + + // Check that the given string is a possible option and verify the editor isn't unloaded + // without this, you'd be given a reference to an object that no longer exists in the DOM + if (!available[name] || this.is('unloaded')) { + return null; + } + else { + return available[name]; + } + } + + /** + * Returns a boolean of each "state" of the editor. For example "editor.is('loaded')" // returns true/false + * @param {String} what the state you want to check for + * @returns {Boolean} + */ + EpicEditor.prototype.is = function (what) { + var self = this; + switch (what) { + case 'loaded': + return self._eeState.loaded; + case 'unloaded': + return self._eeState.unloaded + case 'preview': + return self._eeState.preview + case 'edit': + return self._eeState.edit; + case 'fullscreen': + return self._eeState.fullscreen; + // TODO: This "works", but the tests are saying otherwise. Come back to this + // and figure out how to fix it. + // case 'focused': + // return document.activeElement == self.iframeElement; + default: + return false; + } + } + + /** + * Opens a file + * @param {string} name The name of the file you want to open + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.open = function (name) { + var self = this + , defaultContent = self.settings.file.defaultContent + , fileObj; + name = name || self.settings.file.name; + self.settings.file.name = name; + if (this._storage[self.settings.localStorageName]) { + fileObj = self.exportFile(name); + if (fileObj !== undefined) { + _setText(self.editor, fileObj); + self.emit('read'); + } + else { + _setText(self.editor, defaultContent); + self.save(); // ensure a save + self.emit('create'); + } + self.previewer.innerHTML = self.exportFile(null, 'html'); + self.emit('open'); + } + return this; + } + + /** + * Saves content for offline use + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.save = function (_isPreviewDraft, _isAuto) { + var self = this + , storage + , isUpdate = false + , file = self.settings.file.name + , previewDraftName = '' + , data = this._storage[previewDraftName + self.settings.localStorageName] + , content = _getText(this.editor); + + if (_isPreviewDraft) { + previewDraftName = self._previewDraftLocation; + } + + // This could have been false but since we're manually saving + // we know it's save to start autoSaving again + this._canSave = true; + + // Guard against storage being wiped out without EpicEditor knowing + // TODO: Emit saving error - storage seems to have been wiped + if (data) { + 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(); + } + + // 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) { + storage[file].modified = new Date(); + isUpdate = true; + } + //don't bother autosaving if the content hasn't actually changed + else if (_isAuto) { + return; + } + + 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 (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'); + } + } + + return this; + } + + /** + * Removes a page + * @param {string} name The name of the file you want to remove from localStorage + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.remove = function (name) { + var self = this + , s; + name = name || self.settings.file.name; + + // If you're trying to delete a page you have open, block saving + if (name == self.settings.file.name) { + self._canSave = false; + } + + s = JSON.parse(this._storage[self.settings.localStorageName]); + delete s[name]; + this._storage[self.settings.localStorageName] = JSON.stringify(s); + this.emit('remove'); + return this; + }; + + /** + * Renames a file + * @param {string} oldName The old file name + * @param {string} newName The new file name + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.rename = function (oldName, newName) { + var self = this + , s = JSON.parse(this._storage[self.settings.localStorageName]); + s[newName] = s[oldName]; + delete s[oldName]; + this._storage[self.settings.localStorageName] = JSON.stringify(s); + self.open(newName); + return this; + }; + + /** + * Imports a file and it's contents and opens it + * @param {string} name The name of the file you want to import (will overwrite existing files!) + * @param {string} content Content of the file you want to import + * @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; + + 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(); + } + + //firefox has trouble with importing and working out the size right away + if (self.settings.autogrow) { + setTimeout(function () { + self._autogrow(); + }, 50); + } + + return this; + }; + + /** + * Gets the local filestore + * @param {string} name Name of the file in the store + * @returns {object|undefined} the local filestore, or a specific file in the store, if a name is given + */ + EpicEditor.prototype._getFileStore = function (name, _isPreviewDraft) { + var previewDraftName = '' + , store; + if (_isPreviewDraft) { + previewDraftName = this._previewDraftLocation; + } + store = JSON.parse(this._storage[previewDraftName + this.settings.localStorageName]); + if (name) { + return store[name]; + } + else { + return store; + } + } + + /** + * Exports a file as a string in a supported format + * @param {string} name Name of the file you want to export (case sensitive) + * @param {string} kind Kind of file you want the content in (currently supports html and text, default is the format the browser "wants") + * @returns {string|undefined} The content of the file in the content given or undefined if it doesn't exist + */ + EpicEditor.prototype.exportFile = function (name, kind, _isPreviewDraft) { + var self = this + , file + , content; + + name = name || self.settings.file.name; + kind = kind || 'text'; + + file = self._getFileStore(name, _isPreviewDraft); + + // If the file doesn't exist just return early with undefined + if (file === undefined) { + return; + } + + content = file.content; + + switch (kind) { + case 'html': + content = _sanitizeRawContent(content); + return self.settings.parser(content); + case 'text': + return _sanitizeRawContent(content); + case 'json': + file.content = _sanitizeRawContent(file.content); + return JSON.stringify(file); + case 'raw': + return content; + default: + return content; + } + } + + /** + * Gets the contents and metadata for files + * @param {string} name Name of the file whose data you want (case sensitive) + * @param {boolean} excludeContent whether the contents of files should be excluded + * @returns {object} An object with the names and data of every file, or just the data of one file if a name was given + */ + EpicEditor.prototype.getFiles = function (name, excludeContent) { + var file + , data = this._getFileStore(name); + + if (name) { + if (data !== undefined) { + if (excludeContent) { + delete data.content; + } + else { + data.content = _sanitizeRawContent(data.content); + } + } + return data; + } + else { + for (file in data) { + if (data.hasOwnProperty(file)) { + if (excludeContent) { + delete data[file].content; + } + else { + data[file].content = _sanitizeRawContent(data[file].content); + } + } + } + return data; + } + } + + // EVENTS + // TODO: Support for namespacing events like "preview.foo" + /** + * Sets up an event handler for a specified event + * @param {string} ev The event name + * @param {function} handler The callback to run when the event fires + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.on = function (ev, handler) { + var self = this; + if (!this.events[ev]) { + this.events[ev] = []; + } + this.events[ev].push(handler); + return self; + }; + + /** + * This will emit or "trigger" an event specified + * @param {string} ev The event name + * @param {any} data Any data you want to pass into the callback + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.emit = function (ev, data) { + var self = this + , x; + + data = data || self.getFiles(self.settings.file.name); + + if (!this.events[ev]) { + return; + } + + function invokeHandler(handler) { + handler.call(self, data); + } + + for (x = 0; x < self.events[ev].length; x++) { + invokeHandler(self.events[ev][x]); + } + + return self; + }; + + /** + * Will remove any listeners added from EpicEditor.on() + * @param {string} ev The event name + * @param {function} handler Handler to remove + * @returns {object} EpicEditor will be returned + */ + EpicEditor.prototype.removeListener = function (ev, handler) { + var self = this; + if (!handler) { + this.events[ev] = []; + return self; + } + if (!this.events[ev]) { + return self; + } + // Otherwise a handler and event exist, so take care of it + this.events[ev].splice(this.events[ev].indexOf(handler), 1); + return self; + } + + /** + * Handles autogrowing the editor + */ + EpicEditor.prototype._autogrow = function () { + var editorHeight + , newHeight + , minHeight + , maxHeight + , el + , style + , maxedOut = false; + + //autogrow in fullscreen is nonsensical + if (!this.is('fullscreen')) { + if (this.is('edit')) { + el = this.getElement('editor').documentElement; + } + else { + el = this.getElement('previewer').documentElement; + } + + editorHeight = _outerHeight(el); + newHeight = editorHeight; + + //handle minimum + minHeight = this.settings.autogrow.minHeight; + if (typeof minHeight === 'function') { + minHeight = minHeight(this); + } + + if (minHeight && newHeight < minHeight) { + newHeight = minHeight; + } + + //handle maximum + maxHeight = this.settings.autogrow.maxHeight; + if (typeof maxHeight === 'function') { + maxHeight = maxHeight(this); + } + + if (maxHeight && newHeight > maxHeight) { + newHeight = maxHeight; + maxedOut = true; + } + + if (maxedOut) { + this._fixScrollbars('auto'); + } else { + this._fixScrollbars('hidden'); + } + + //actual resize + if (newHeight != this.oldHeight) { + this.getElement('container').style.height = newHeight + 'px'; + this.reflow(); + if (this.settings.autogrow.scroll) { + window.scrollBy(0, newHeight - this.oldHeight); + } + this.oldHeight = newHeight; + } + } + } + + /** + * Shows or hides scrollbars based on the autogrow setting + * @param {string} forceSetting a value to force the overflow to + */ + EpicEditor.prototype._fixScrollbars = function (forceSetting) { + var setting; + if (this.settings.autogrow) { + setting = 'hidden'; + } + else { + setting = 'auto'; + } + setting = forceSetting || setting; + this.getElement('editor').documentElement.style.overflow = setting; + this.getElement('previewer').documentElement.style.overflow = setting; + } + + EpicEditor.version = '0.2.2'; + + // Used to store information to be shared across editors + EpicEditor._data = {}; + + window.EpicEditor = EpicEditor; +})(window); + +/** + * marked - a markdown parser + * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed) + * https://github.com/chjj/marked + */ + +;(function() { + +/** + * Block-Level Grammar + */ + +var block = { + newline: /^\n+/, + 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+|$)/, + table: noop, + paragraph: /^([^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+\n*/, + text: /^[^\n]+/ +}; + +block.bullet = /(?:[*+-]|\d+\.)/; +block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; +block.item = replace(block.item, 'gm') + (/bull/g, block.bullet) + (); + +block.list = replace(block.list) + (/bull/g, block.bullet) + ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/) + (); + +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'; + +block.html = replace(block.html) + ('comment', //) + ('closed', /<(tag)[\s\S]+?<\/\1>/) + ('closing', /])*?>/) + (/tag/g, block._tag) + (); + +block.paragraph = replace(block.paragraph) + ('hr', block.hr) + ('heading', block.heading) + ('lheading', block.lheading) + ('blockquote', block.blockquote) + ('tag', '<' + block._tag) + ('def', block.def) + (); + +/** + * Normal Block Grammar + */ + +block.normal = merge({}, block); + +/** + * GFM Block Grammar + */ + +block.gfm = merge({}, block.normal, { + fences: /^ *(`{3,}|~{3,}) *(\w+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, + paragraph: /^/ +}); + +block.gfm.paragraph = replace(block.paragraph) + ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|') + (); + +/** + * GFM + Tables Block Grammar + */ + +block.tables = merge({}, block.gfm, { + nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, + table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ +}); + +/** + * Block Lexer + */ + +function Lexer(options) { + this.tokens = []; + this.tokens.links = {}; + this.options = options || marked.defaults; + this.rules = block.normal; + + if (this.options.gfm) { + if (this.options.tables) { + this.rules = block.tables; + } else { + this.rules = block.gfm; + } + } +} + +/** + * Expose Block Rules + */ + +Lexer.rules = block; + +/** + * Static Lex Method + */ + +Lexer.lex = function(src, options) { + var lexer = new Lexer(options); + return lexer.lex(src); +}; + +/** + * Preprocessing + */ + +Lexer.prototype.lex = function(src) { + src = src + .replace(/\r\n|\r/g, '\n') + .replace(/\t/g, ' ') + .replace(/\u00a0/g, ' ') + .replace(/\u2424/g, '\n'); + + return this.token(src, true); +}; + +/** + * Lexing + */ + +Lexer.prototype.token = function(src, top) { + var src = src.replace(/^ +$/gm, '') + , next + , loose + , cap + , item + , space + , i + , l; + + while (src) { + // newline + if (cap = this.rules.newline.exec(src)) { + src = src.substring(cap[0].length); + if (cap[0].length > 1) { + this.tokens.push({ + type: 'space' + }); + } + } + + // code + if (cap = this.rules.code.exec(src)) { + src = src.substring(cap[0].length); + cap = cap[0].replace(/^ {4}/gm, ''); + this.tokens.push({ + type: 'code', + text: !this.options.pedantic + ? cap.replace(/\n+$/, '') + : cap + }); + continue; + } + + // fences (gfm) + if (cap = this.rules.fences.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'code', + lang: cap[2], + text: cap[3] + }); + continue; + } + + // heading + if (cap = this.rules.heading.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'heading', + depth: cap[1].length, + text: cap[2] + }); + continue; + } + + // table no leading pipe (gfm) + if (top && (cap = this.rules.nptable.exec(src))) { + src = src.substring(cap[0].length); + + item = { + type: 'table', + header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3].replace(/\n$/, '').split('\n') + }; + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = item.cells[i].split(/ *\| */); + } + + this.tokens.push(item); + + continue; + } + + // lheading + if (cap = this.rules.lheading.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'heading', + depth: cap[2] === '=' ? 1 : 2, + text: cap[1] + }); + continue; + } + + // hr + if (cap = this.rules.hr.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'hr' + }); + continue; + } + + // blockquote + if (cap = this.rules.blockquote.exec(src)) { + src = src.substring(cap[0].length); + + this.tokens.push({ + type: 'blockquote_start' + }); + + 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.tokens.push({ + type: 'blockquote_end' + }); + + continue; + } + + // list + if (cap = this.rules.list.exec(src)) { + src = src.substring(cap[0].length); + + this.tokens.push({ + type: 'list_start', + ordered: isFinite(cap[2]) + }); + + // Get each top-level item. + cap = cap[0].match(this.rules.item); + + next = false; + l = cap.length; + i = 0; + + for (; i < l; i++) { + item = cap[i]; + + // Remove the list item's bullet + // so it is seen as the next token. + space = item.length; + item = item.replace(/^ *([*+-]|\d+\.) +/, ''); + + // Outdent whatever the + // list item contains. Hacky. + if (~item.indexOf('\n ')) { + space -= item.length; + item = !this.options.pedantic + ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') + : item.replace(/^ {1,4}/gm, ''); + } + + // 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'; + if (!loose) loose = next; + } + + this.tokens.push({ + type: loose + ? 'loose_item_start' + : 'list_item_start' + }); + + // Recurse. + this.token(item, false); + + this.tokens.push({ + type: 'list_item_end' + }); + } + + this.tokens.push({ + type: 'list_end' + }); + + continue; + } + + // html + if (cap = this.rules.html.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: this.options.sanitize + ? 'paragraph' + : 'html', + pre: cap[1] === 'pre', + text: cap[0] + }); + continue; + } + + // def + if (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] + }; + continue; + } + + // table (gfm) + if (top && (cap = this.rules.table.exec(src))) { + src = src.substring(cap[0].length); + + item = { + type: 'table', + header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') + }; + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = item.cells[i] + .replace(/^ *\| *| *\| *$/g, '') + .split(/ *\| */); + } + + this.tokens.push(item); + + continue; + } + + // 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] + }); + continue; + } + + // text + if (cap = this.rules.text.exec(src)) { + // Top-level should never reach here. + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'text', + text: cap[0] + }); + continue; + } + + if (src) { + throw new + Error('Infinite loop on byte: ' + src.charCodeAt(0)); + } + } + + return this.tokens; +}; + +/** + * Inline-Level Grammar + */ + +var inline = { + escape: /^\\([\\`*{}\[\]()#+\-.!_>|])/, + autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, + url: noop, + tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, + link: /^!?\[(inside)\]\(href\)/, + reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, + nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, + strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, + em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, + code: /^(`+)([\s\S]*?[^`])\1(?!`)/, + br: /^ {2,}\n(?!\s*$)/, + del: noop, + text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/; + +inline.link = replace(inline.link) + ('inside', inline._inside) + ('href', inline._href) + (); + +inline.reflink = replace(inline.reflink) + ('inside', inline._inside) + (); + +/** + * Normal Inline Grammar + */ + +inline.normal = merge({}, inline); + +/** + * Pedantic Inline Grammar + */ + +inline.pedantic = merge({}, inline.normal, { + strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, + em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/ +}); + +/** + * GFM Inline Grammar + */ + +inline.gfm = merge({}, inline.normal, { + escape: replace(inline.escape)('])', '~])')(), + url: /^(https?:\/\/[^\s]+[^.,:;"')\]\s])/, + del: /^~{2,}([\s\S]+?)~{2,}/, + text: replace(inline.text) + (']|', '~]|') + ('|', '|https?://|') + () +}); + +/** + * GFM + Line Breaks Inline Grammar + */ + +inline.breaks = merge({}, inline.gfm, { + br: replace(inline.br)('{2,}', '*')(), + text: replace(inline.gfm.text)('{2,}', '*')() +}); + +/** + * Inline Lexer & Compiler + */ + +function InlineLexer(links, options) { + this.options = options || marked.defaults; + this.links = links; + this.rules = inline.normal; + + if (!this.links) { + throw new + Error('Tokens array requires a `links` property.'); + } + + if (this.options.gfm) { + if (this.options.breaks) { + this.rules = inline.breaks; + } else { + this.rules = inline.gfm; + } + } else if (this.options.pedantic) { + this.rules = inline.pedantic; + } +} + +/** + * Expose Inline Rules + */ + +InlineLexer.rules = inline; + +/** + * Static Lexing/Compiling Method + */ + +InlineLexer.output = function(src, links, opt) { + var inline = new InlineLexer(links, opt); + return inline.output(src); +}; + +/** + * Lexing/Compiling + */ + +InlineLexer.prototype.output = function(src) { + var out = '' + , link + , text + , href + , cap; + + while (src) { + // escape + if (cap = this.rules.escape.exec(src)) { + src = src.substring(cap[0].length); + out += cap[1]; + continue; + } + + // autolink + if (cap = this.rules.autolink.exec(src)) { + src = src.substring(cap[0].length); + if (cap[2] === '@') { + text = cap[1][6] === ':' + ? this.mangle(cap[1].substring(7)) + : this.mangle(cap[1]); + href = this.mangle('mailto:') + text; + } else { + text = escape(cap[1]); + href = text; + } + out += '' + + text + + ''; + continue; + } + + // url (gfm) + if (cap = this.rules.url.exec(src)) { + src = src.substring(cap[0].length); + text = escape(cap[1]); + href = text; + out += '' + + text + + ''; + continue; + } + + // tag + if (cap = this.rules.tag.exec(src)) { + 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); + out += this.outputLink(cap, { + href: cap[2], + title: cap[3] + }); + 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]; + src = cap[0].substring(1) + src; + continue; + } + out += this.outputLink(cap, link); + continue; + } + + // strong + if (cap = this.rules.strong.exec(src)) { + src = src.substring(cap[0].length); + out += '' + + this.output(cap[2] || cap[1]) + + ''; + continue; + } + + // em + if (cap = this.rules.em.exec(src)) { + src = src.substring(cap[0].length); + out += '' + + this.output(cap[2] || cap[1]) + + ''; + continue; + } + + // code + if (cap = this.rules.code.exec(src)) { + src = src.substring(cap[0].length); + out += '' + + escape(cap[2], true) + + ''; + continue; + } + + // br + if (cap = this.rules.br.exec(src)) { + src = src.substring(cap[0].length); + out += '
'; + continue; + } + + // del (gfm) + if (cap = this.rules.del.exec(src)) { + src = src.substring(cap[0].length); + out += '' + + this.output(cap[1]) + + ''; + continue; + } + + // text + if (cap = this.rules.text.exec(src)) { + src = src.substring(cap[0].length); + out += escape(cap[0]); + continue; + } + + if (src) { + throw new + Error('Infinite loop on byte: ' + src.charCodeAt(0)); + } + } + + return out; +}; + +/** + * Compile Link + */ + +InlineLexer.prototype.outputLink = function(cap, link) { + if (cap[0][0] !== '!') { + return '' + + this.output(cap[1]) + + ''; + } else { + return ''
+      + escape(cap[1])
+      + ''; + } +}; + +/** + * Mangle Links + */ + +InlineLexer.prototype.mangle = function(text) { + var out = '' + , l = text.length + , i = 0 + , ch; + + for (; i < l; i++) { + ch = text.charCodeAt(i); + if (Math.random() > 0.5) { + ch = 'x' + ch.toString(16); + } + out += '&#' + ch + ';'; + } + + return out; +}; + +/** + * Parsing & Compiling + */ + +function Parser(options) { + this.tokens = []; + this.token = null; + this.options = options || marked.defaults; +} + +/** + * Static Parse Method + */ + +Parser.parse = function(src, options) { + var parser = new Parser(options); + return parser.parse(src); +}; + +/** + * Parse Loop + */ + +Parser.prototype.parse = function(src) { + this.inline = new InlineLexer(src.links, this.options); + this.tokens = src.reverse(); + + var out = ''; + while (this.next()) { + out += this.tok(); + } + + return out; +}; + +/** + * Next Token + */ + +Parser.prototype.next = function() { + return this.token = this.tokens.pop(); +}; + +/** + * Preview Next Token + */ + +Parser.prototype.peek = function() { + return this.tokens[this.tokens.length-1] || 0; +}; + +/** + * Parse Text Tokens + */ + +Parser.prototype.parseText = function() { + var body = this.token.text; + + while (this.peek().type === 'text') { + body += '\n' + this.next().text; + } + + return this.inline.output(body); +}; + +/** + * Parse Current Token + */ + +Parser.prototype.tok = function() { + switch (this.token.type) { + case 'space': { + return ''; + } + case 'hr': { + return '
\n'; + } + case 'heading': { + return '' + + this.inline.output(this.token.text) + + '\n'; + } + 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 '
'
+        + this.token.text
+        + '
\n'; + } + case 'table': { + var body = '' + , heading + , i + , row + , cell + , j; + + // header + body += '\n\n'; + for (i = 0; i < this.token.header.length; i++) { + heading = this.inline.output(this.token.header[i]); + body += this.token.align[i] + ? '' + heading + '\n' + : '' + heading + '\n'; + } + body += '\n\n'; + + // body + body += '\n' + for (i = 0; i < this.token.cells.length; i++) { + row = this.token.cells[i]; + body += '\n'; + for (j = 0; j < row.length; j++) { + cell = this.inline.output(row[j]); + body += this.token.align[j] + ? '' + cell + '\n' + : '' + cell + '\n'; + } + body += '\n'; + } + body += '\n'; + + return '\n' + + body + + '
\n'; + } + case 'blockquote_start': { + var body = ''; + + while (this.next().type !== 'blockquote_end') { + body += this.tok(); + } + + return '
\n' + + body + + '
\n'; + } + case 'list_start': { + var type = this.token.ordered ? 'ol' : 'ul' + , body = ''; + + while (this.next().type !== 'list_end') { + body += this.tok(); + } + + return '<' + + type + + '>\n' + + body + + '\n'; + } + case 'list_item_start': { + var body = ''; + + while (this.next().type !== 'list_item_end') { + body += this.token.type === 'text' + ? this.parseText() + : this.tok(); + } + + return '
  • ' + + body + + '
  • \n'; + } + case 'loose_item_start': { + var body = ''; + + while (this.next().type !== 'list_item_end') { + body += this.tok(); + } + + return '
  • ' + + body + + '
  • \n'; + } + case 'html': { + return !this.token.pre && !this.options.pedantic + ? this.inline.output(this.token.text) + : this.token.text; + } + case 'paragraph': { + return '

    ' + + this.inline.output(this.token.text) + + '

    \n'; + } + case 'text': { + return '

    ' + + this.parseText() + + '

    \n'; + } + } +}; + +/** + * Helpers + */ + +function escape(html, encode) { + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function replace(regex, opt) { + regex = regex.source; + opt = opt || ''; + return function self(name, val) { + if (!name) return new RegExp(regex, opt); + val = val.source || val; + val = val.replace(/(^|[^\[])\^/g, '$1'); + regex = regex.replace(name, val); + return self; + }; +} + +function noop() {} +noop.exec = noop; + +function merge(obj) { + var i = 1 + , target + , key; + + for (; i < arguments.length; i++) { + target = arguments[i]; + for (key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + obj[key] = target[key]; + } + } + } + + return obj; +} + +/** + * Marked + */ + +function marked(src, opt) { + try { + 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; + } + throw e; + } +} + +/** + * Options + */ + +marked.options = +marked.setOptions = function(opt) { + marked.defaults = opt; + return marked; +}; + +marked.defaults = { + gfm: true, + tables: true, + breaks: false, + pedantic: false, + sanitize: false, + silent: false, + highlight: null +}; + +/** + * Expose + */ + +marked.Parser = Parser; +marked.parser = Parser.parse; + +marked.Lexer = Lexer; +marked.lexer = Lexer.lex; + +marked.InlineLexer = InlineLexer; +marked.inlineLexer = InlineLexer.output; + +marked.parse = marked; + +if (typeof module !== 'undefined') { + module.exports = marked; +} else if (typeof define === 'function' && define.amd) { + define(function() { return marked; }); +} else { + this.marked = marked; +} + +}).call(function() { + return this || (typeof window !== 'undefined' ? window : global); +}()); diff --git a/lib/EpicEditor/js/epiceditor.min.js b/lib/EpicEditor/js/epiceditor.min.js new file mode 100644 index 0000000..e664025 --- /dev/null +++ b/lib/EpicEditor/js/epiceditor.min.js @@ -0,0 +1,5 @@ +/** + * EpicEditor - An Embeddable JavaScript Markdown Editor (https://github.com/OscarGodson/EpicEditor) + * Copyright (c) 2011-2012, Oscar Godson. (MIT Licensed) + */(function(e,t){function n(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])}function r(e,t){for(var n in t)t.hasOwnProperty(n)&&(e.style[n]=t[n])}function i(t,n){var r=t,i=null;return e.getComputedStyle?i=document.defaultView.getComputedStyle(r,null).getPropertyValue(n):r.currentStyle&&(i=r.currentStyle[n]),i}function s(e,t,n){var s={},o;if(t==="save"){for(o in n)n.hasOwnProperty(o)&&(s[o]=i(e,o));r(e,n)}else t==="apply"&&r(e,n);return s}function o(e){var t=parseInt(i(e,"border-left-width"),10)+parseInt(i(e,"border-right-width"),10),n=parseInt(i(e,"padding-left"),10)+parseInt(i(e,"padding-right"),10),r=e.offsetWidth,s;return isNaN(t)&&(t=0),s=t+n+r,s}function u(e){var t=parseInt(i(e,"border-top-width"),10)+parseInt(i(e,"border-bottom-width"),10),n=parseInt(i(e,"padding-top"),10)+parseInt(i(e,"padding-bottom"),10),r=parseInt(i(e,"height"),10),s;return isNaN(t)&&(t=0),s=t+n+r,s}function a(e,t,r){r=r||"";var i=t.getElementsByTagName("head")[0],s=t.createElement("link");n(s,{type:"text/css",id:r,rel:"stylesheet",href:e,name:e,media:"screen"}),i.appendChild(s)}function f(e,t,n){e.className=e.className.replace(t,n)}function l(e){return e.contentDocument||e.contentWindow.document}function c(e){var t;return typeof document.body.innerText=="string"?t=e.innerText:(t=e.innerHTML.replace(/
    /gi,"\n"),t=t.replace(/<(?:.|\n)*?>/gm,""),t=t.replace(/</gi,"<"),t=t.replace(/>/gi,">")),t}function h(e,t){return t=t.replace(//g,">"),t=t.replace(/\n/g,"
    "),t=t.replace(/
    \s/g,"
     "),t=t.replace(/\s\s\s/g,"   "),t=t.replace(/\s\s/g,"  "),t=t.replace(/^ /," "),e.innerHTML=t,!0}function p(e){return e.replace(/\u00a0/g," ").replace(/ /g," ")}function d(){var e=-1,t=navigator.userAgent,n;return navigator.appName=="Microsoft Internet Explorer"&&(n=/MSIE ([0-9]{1,}[\.0-9]{0,})/,n.exec(t)!=null&&(e=parseFloat(RegExp.$1,10))),e}function v(){var t=e.navigator;return t.userAgent.indexOf("Safari")>-1&&t.userAgent.indexOf("Chrome")==-1}function m(){var t=e.navigator;return t.userAgent.indexOf("Firefox")>-1&&t.userAgent.indexOf("Seamonkey")==-1}function g(e){var t={};return e&&t.toString.call(e)==="[object Function]"}function y(){var e=arguments[0]||{},n=1,r=arguments.length,i=!1,s,o,u,a;typeof e=="boolean"&&(i=e,e=arguments[1]||{},n=2),typeof e!="object"&&!g(e)&&(e={}),r===n&&(e=this,--n);for(;n=5||Math.abs(g.x-t.pageX)>=5)h.style.display="block",p&&clearTimeout(p),p=e.setTimeout(function(){h.style.display="none"},1e3);g={y:t.pageY,x:t.pageX}}function M(e){e.keyCode==n.settings.shortcut.modifier&&(N=!0),e.keyCode==17&&(C=!0),N===!0&&e.keyCode==n.settings.shortcut.preview&&!n.is("fullscreen")&&(e.preventDefault(),n.is("edit")&&n._previewEnabled?n.preview():n._editEnabled&&n.edit()),N===!0&&e.keyCode==n.settings.shortcut.fullscreen&&n._fullscreenEnabled&&(e.preventDefault(),n._goFullscreen(T)),N===!0&&e.keyCode!==n.settings.shortcut.modifier&&(N=!1),e.keyCode==27&&n.is("fullscreen")&&n._exitFullscreen(T),C===!0&&e.keyCode==83&&(n.save(),e.preventDefault(),C=!1),e.metaKey&&e.keyCode==83&&(n.save(),e.preventDefault())}function _(e){e.keyCode==n.settings.shortcut.modifier&&(N=!1),e.keyCode==17&&(C=!1)}function D(t){var r;t.clipboardData?(t.preventDefault(),r=t.clipboardData.getData("text/plain"),n.editorIframeDocument.execCommand("insertText",!1,r)):e.clipboardData&&(t.preventDefault(),r=e.clipboardData.getData("Text"),r=r.replace(//g,">"),r=r.replace(/\n/g,"
    "),r=r.replace(/\r/g,""),r=r.replace(/
    \s/g,"
     "),r=r.replace(/\s\s\s/g,"   "),r=r.replace(/\s\s/g,"  "),n.editorIframeDocument.selection.createRange().pasteHTML(r))}if(this.is("loaded"))return this;var n=this,o,u,f,c,h,p,m,g={y:-1,x:-1},y,b,w=!1,E=!1,S=!1,x=!1,T,N=!1,C=!1,k,L,A;n._eeState.startup=!0,n.settings.useNativeFullscreen&&(E=document.body.webkitRequestFullScreen?!0:!1,S=document.body.mozRequestFullScreen?!0:!1,x=document.body.requestFullscreen?!0:!1,w=E||S||x),v()&&(w=!1,E=!1),!n.is("edit")&&!n.is("preview")&&(n._eeState.edit=!0),t=t||function(){},o={chrome:'
    '+(n._previewEnabled?' ':"")+(n._editEnabled?' ':"")+(n._fullscreenEnabled?'':"")+"
    "+"
    ",previewer:'
    ',editor:""},n.element.innerHTML='',n.element.style.height=n.element.offsetHeight+"px",u=document.getElementById(n._instanceId),n.iframeElement=u,n.iframe=l(u),n.iframe.open(),n.iframe.write(o.chrome),n.editorIframe=n.iframe.getElementById("epiceditor-editor-frame"),n.previewerIframe=n.iframe.getElementById("epiceditor-previewer-frame"),n.editorIframeDocument=l(n.editorIframe),n.editorIframeDocument.open(),n.editorIframeDocument.write(o.editor),n.editorIframeDocument.close(),n.previewerIframeDocument=l(n.previewerIframe),n.previewerIframeDocument.open(),n.previewerIframeDocument.write(o.previewer),f=n.previewerIframeDocument.createElement("base"),f.target="_blank",n.previewerIframeDocument.getElementsByTagName("head")[0].appendChild(f),n.previewerIframeDocument.close(),n.reflow(),a(n.settings.theme.base,n.iframe,"theme"),a(n.settings.theme.editor,n.editorIframeDocument,"theme"),a(n.settings.theme.preview,n.previewerIframeDocument,"theme"),n.iframe.getElementById("epiceditor-wrapper").style.position="relative",n.editorIframe.style.position="absolute",n.previewerIframe.style.position="absolute",n.editor=n.editorIframeDocument.body,n.previewer=n.previewerIframeDocument.getElementById("epiceditor-preview"),n.editor.contentEditable=!0,n.iframe.body.style.height=this.element.offsetHeight+"px",n.previewerIframe.style.left="-999999px",this.editorIframeDocument.body.style.wordWrap="break-word",d()>-1&&(this.previewer.style.height=parseInt(i(this.previewer,"height"),10)+2),this.open(n.settings.file.name),n.settings.focusOnLoad&&n.iframe.addEventListener("readystatechange",function(){n.iframe.readyState=="complete"&&n.focus()}),n.previewerIframeDocument.addEventListener("click",function(t){var r=t.target,i=n.previewerIframeDocument.body;r.nodeName=="A"&&r.hash&&r.hostname==e.location.hostname&&(t.preventDefault(),r.target="_self",i.querySelector(r.hash)&&(i.scrollTop=i.querySelector(r.hash).offsetTop))}),c=n.iframe.getElementById("epiceditor-utilbar"),y={},n._goFullscreen=function(t){this._fixScrollbars("auto");if(n.is("fullscreen")){n._exitFullscreen(t);return}w&&(E?t.webkitRequestFullScreen():S?t.mozRequestFullScreen():x&&t.requestFullscreen()),b=n.is("edit"),n._eeState.fullscreen=!0,n._eeState.edit=!0,n._eeState.preview=!0;var r=e.innerWidth,o=e.innerHeight,u=e.outerWidth,a=e.outerHeight;w||(a=e.innerHeight),y.editorIframe=s(n.editorIframe,"save",{width:u/2+"px",height:a+"px","float":"left",cssFloat:"left",styleFloat:"left",display:"block",position:"static",left:""}),y.previewerIframe=s(n.previewerIframe,"save",{width:u/2+"px",height:a+"px","float":"right",cssFloat:"right",styleFloat:"right",display:"block",position:"static",left:""}),y.element=s(n.element,"save",{position:"fixed",top:"0",left:"0",width:"100%","z-index":"9999",zIndex:"9999",border:"none",margin:"0",background:i(n.editor,"background-color"),height:o+"px"}),y.iframeElement=s(n.iframeElement,"save",{width:u+"px",height:o+"px"}),c.style.visibility="hidden",w||(document.body.style.overflow="hidden"),n.preview(),n.focus(),n.emit("fullscreenenter")},n._exitFullscreen=function(e){this._fixScrollbars(),s(n.element,"apply",y.element),s(n.iframeElement,"apply",y.iframeElement),s(n.editorIframe,"apply",y.editorIframe),s(n.previewerIframe,"apply",y.previewerIframe),n.element.style.width=n._eeState.reflowWidth?n._eeState.reflowWidth:"",n.element.style.height=n._eeState.reflowHeight?n._eeState.reflowHeight:"",c.style.visibility="visible",n._eeState.fullscreen=!1,w?E?document.webkitCancelFullScreen():S?document.mozCancelFullScreen():x&&document.exitFullscreen():document.body.style.overflow="auto",b?n.edit():n.preview(),n.reflow(),n.emit("fullscreenexit")},n.editor.addEventListener("keyup",function(){m&&e.clearTimeout(m),m=e.setTimeout(function(){n.is("fullscreen")&&n.preview()},250)}),T=n.iframeElement,c.addEventListener("click",function(e){var t=e.target.className;t.indexOf("epiceditor-toggle-preview-btn")>-1?n.preview():t.indexOf("epiceditor-toggle-edit-btn")>-1?n.edit():t.indexOf("epiceditor-fullscreen-btn")>-1&&n._goFullscreen(T)}),E?document.addEventListener("webkitfullscreenchange",function(){!document.webkitIsFullScreen&&n._eeState.fullscreen&&n._exitFullscreen(T)},!1):S?document.addEventListener("mozfullscreenchange",function(){!document.mozFullScreen&&n._eeState.fullscreen&&n._exitFullscreen(T)},!1):x&&document.addEventListener("fullscreenchange",function(){document.fullscreenElement==null&&n._eeState.fullscreen&&n._exitFullscreen(T)},!1),h=n.iframe.getElementById("epiceditor-utilbar"),n.settings.button.bar!==!0&&(h.style.display="none"),h.addEventListener("mouseover",function(){p&&clearTimeout(p)}),k=[n.previewerIframeDocument,n.editorIframeDocument];for(L=0;Li&&(n=i,a=!0),a?this._fixScrollbars("auto"):this._fixScrollbars("hidden"),n!=this.oldHeight&&(this.getElement("container").style.height=n+"px",this.reflow(),this.settings.autogrow.scroll&&e.scrollBy(0,n-this.oldHeight),this.oldHeight=n))},b.prototype._fixScrollbars=function(e){var t;this.settings.autogrow?t="hidden":t="auto",t=e||t,this.getElement("editor").documentElement.style.overflow=t,this.getElement("previewer").documentElement.style.overflow=t},b.version="0.2.2",b._data={},e.EpicEditor=b})(window),function(){function t(t){this.tokens=[],this.tokens.links={},this.options=t||f.defaults,this.rules=e.normal,this.options.gfm&&(this.options.tables?this.rules=e.tables:this.rules=e.gfm)}function r(e,t){this.options=t||f.defaults,this.links=e,this.rules=n.normal;if(!this.links)throw new Error("Tokens array requires a `links` property.");this.options.gfm?this.options.breaks?this.rules=n.breaks:this.rules=n.gfm:this.options.pedantic&&(this.rules=n.pedantic)}function i(e){this.tokens=[],this.token=null,this.options=e||f.defaults}function s(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function o(e,t){return e=e.source,t=t||"",function n(r,i){return r?(i=i.source||i,i=i.replace(/(^|[^\[])\^/g,"$1"),e=e.replace(r,i),n):new RegExp(e,t)}}function u(){}function a(e){var t=1,n,r;for(;t[^\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+|$)/,table:u,paragraph:/^([^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+\n*/,text:/^[^\n]+/};e.bullet=/(?:[*+-]|\d+\.)/,e.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,e.item=o(e.item,"gm")(/bull/g,e.bullet)(),e.list=o(e.list)(/bull/g,e.bullet)("hr",/\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/)(),e._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",e.html=o(e.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,e._tag)(),e.paragraph=o(e.paragraph)("hr",e.hr)("heading",e.heading)("lheading",e.lheading)("blockquote",e.blockquote)("tag","<"+e._tag)("def",e.def)(),e.normal=a({},e),e.gfm=a({},e.normal,{fences:/^ *(`{3,}|~{3,}) *(\w+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,paragraph:/^/}),e.gfm.paragraph=o(e.paragraph)("(?!","(?!"+e.gfm.fences.source.replace("\\1","\\2")+"|")(),e.tables=a({},e.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),t.rules=e,t.lex=function(e,n){var r=new t(n);return r.lex(e)},t.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},t.prototype.token=function(e,t){var e=e.replace(/^ +$/gm,""),n,r,i,s,o,u,a;while(e){if(i=this.rules.newline.exec(e))e=e.substring(i[0].length),i[0].length>1&&this.tokens.push({type:"space"});if(i=this.rules.code.exec(e)){e=e.substring(i[0].length),i=i[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?i:i.replace(/\n+$/,"")});continue}if(i=this.rules.fences.exec(e)){e=e.substring(i[0].length),this.tokens.push({type:"code",lang:i[2],text:i[3]});continue}if(i=this.rules.heading.exec(e)){e=e.substring(i[0].length),this.tokens.push({type:"heading",depth:i[1].length,text:i[2]});continue}if(t&&(i=this.rules.nptable.exec(e))){e=e.substring(i[0].length),s={type:"table",header:i[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:i[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:i[3].replace(/\n$/,"").split("\n")};for(u=0;u ?/gm,""),this.token(i,t),this.tokens.push({type:"blockquote_end"});continue}if(i=this.rules.list.exec(e)){e=e.substring(i[0].length),this.tokens.push({type:"list_start",ordered:isFinite(i[2])}),i=i[0].match(this.rules.item),n=!1,a=i.length,u=0;for(;u|])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:u,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)([\s\S]*?[^`])\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:u,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/,n.link=o(n.link)("inside",n._inside)("href",n._href)(),n.reflink=o(n.reflink)("inside",n._inside)(),n.normal=a({},n),n.pedantic=a({},n.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),n.gfm=a({},n.normal,{escape:o(n.escape)("])","~])")(),url:/^(https?:\/\/[^\s]+[^.,:;"')\]\s])/,del:/^~{2,}([\s\S]+?)~{2,}/,text:o(n.text)("]|","~]|")("|","|https?://|")()}),n.breaks=a({},n.gfm,{br:o(n.br)("{2,}","*")(),text:o(n.gfm.text)("{2,}","*")()}),r.rules=n,r.output=function(e,t,n){var i=new r(t,n);return i.output(e)},r.prototype.output=function(e){var t="",n,r,i,o;while(e){if(o=this.rules.escape.exec(e)){e=e.substring(o[0].length),t+=o[1];continue}if(o=this.rules.autolink.exec(e)){e=e.substring(o[0].length),o[2]==="@"?(r=o[1][6]===":"?this.mangle(o[1].substring(7)):this.mangle(o[1]),i=this.mangle("mailto:")+r):(r=s(o[1]),i=r),t+=''+r+"";continue}if(o=this.rules.url.exec(e)){e=e.substring(o[0].length),r=s(o[1]),i=r,t+=''+r+"";continue}if(o=this.rules.tag.exec(e)){e=e.substring(o[0].length),t+=this.options.sanitize?s(o[0]):o[0];continue}if(o=this.rules.link.exec(e)){e=e.substring(o[0].length),t+=this.outputLink(o,{href:o[2],title:o[3]});continue}if((o=this.rules.reflink.exec(e))||(o=this.rules.nolink.exec(e))){e=e.substring(o[0].length),n=(o[2]||o[1]).replace(/\s+/g," "),n=this.links[n.toLowerCase()];if(!n||!n.href){t+=o[0][0],e=o[0].substring(1)+e;continue}t+=this.outputLink(o,n);continue}if(o=this.rules.strong.exec(e)){e=e.substring(o[0].length),t+=""+this.output(o[2]||o[1])+"";continue}if(o=this.rules.em.exec(e)){e=e.substring(o[0].length),t+=""+this.output(o[2]||o[1])+"";continue}if(o=this.rules.code.exec(e)){e=e.substring(o[0].length),t+=""+s(o[2],!0)+"";continue}if(o=this.rules.br.exec(e)){e=e.substring(o[0].length),t+="
    ";continue}if(o=this.rules.del.exec(e)){e=e.substring(o[0].length),t+=""+this.output(o[1])+"";continue}if(o=this.rules.text.exec(e)){e=e.substring(o[0].length),t+=s(o[0]);continue}if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}return t},r.prototype.outputLink=function(e,t){return e[0][0]!=="!"?'"+this.output(e[1])+"":''+s(e[1])+'"},r.prototype.mangle=function(e){var t="",n=e.length,r=0,i;for(;r.5&&(i="x"+i.toString(16)),t+="&#"+i+";";return t},i.parse=function(e,t){var n=new i(t);return n.parse(e)},i.prototype.parse=function(e){this.inline=new r(e.links,this.options),this.tokens=e.reverse();var t="";while(this.next())t+=this.tok();return t},i.prototype.next=function(){return this.token=this.tokens.pop()},i.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},i.prototype.parseText=function(){var e=this.token.text;while(this.peek().type==="text")e+="\n"+this.next().text;return this.inline.output(e)},i.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return"
    \n";case"heading":return""+this.inline.output(this.token.text)+"\n";case"code":if(this.options.highlight){var e=this.options.highlight(this.token.text,this.token.lang);e!=null&&e!==this.token.text&&(this.token.escaped=!0,this.token.text=e)}return this.token.escaped||(this.token.text=s(this.token.text,!0)),"
    "+this.token.text+"
    \n";case"table":var t="",n,r,i,o,u;t+="\n\n";for(r=0;r'+n+"\n":""+n+"\n";t+="\n\n",t+="\n";for(r=0;r\n";for(u=0;u'+o+"\n":""+o+"\n";t+="\n"}return t+="\n","\n"+t+"
    \n";case"blockquote_start":var t="";while(this.next().type!=="blockquote_end" +)t+=this.tok();return"
    \n"+t+"
    \n";case"list_start":var a=this.token.ordered?"ol":"ul",t="";while(this.next().type!=="list_end")t+=this.tok();return"<"+a+">\n"+t+"\n";case"list_item_start":var t="";while(this.next().type!=="list_item_end")t+=this.token.type==="text"?this.parseText():this.tok();return"
  • "+t+"
  • \n";case"loose_item_start":var t="";while(this.next().type!=="list_item_end")t+=this.tok();return"
  • "+t+"
  • \n";case"html":return!this.token.pre&&!this.options.pedantic?this.inline.output(this.token.text):this.token.text;case"paragraph":return"

    "+this.inline.output(this.token.text)+"

    \n";case"text":return"

    "+this.parseText()+"

    \n"}},u.exec=u,f.options=f.setOptions=function(e){return f.defaults=e,f},f.defaults={gfm:!0,tables:!0,breaks:!1,pedantic:!1,sanitize:!1,silent:!1,highlight:null},f.Parser=i,f.parser=i.parse,f.Lexer=t,f.lexer=t.lex,f.InlineLexer=r,f.inlineLexer=r.output,f.parse=f,typeof module!="undefined"?module.exports=f:typeof define=="function"&&define.amd?define(function(){return f}):this.marked=f}.call(function(){return this||(typeof window!="undefined"?window:global)}()); \ No newline at end of file diff --git a/lib/EpicEditor/themes/base/epiceditor.css b/lib/EpicEditor/themes/base/epiceditor.css new file mode 100644 index 0000000..76e58fc --- /dev/null +++ b/lib/EpicEditor/themes/base/epiceditor.css @@ -0,0 +1,70 @@ +html, body, iframe, div { + margin:0; + padding:0; +} + +#epiceditor-utilbar { + position:fixed; + bottom:10px; + right:10px; +} + +#epiceditor-utilbar button { + display:block; + float:left; + width:30px; + height:30px; + border:none; + background:none; +} + +#epiceditor-utilbar button.epiceditor-toggle-preview-btn { + background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAf9JREFUeNrsVu1twjAQLZmAEcIGYYKGDWACkgkSkPiQ+CifAoEgyQTQDegEpROQDZoRMgH0HToj1yIglKr90Vg62T6f/e4d9gu50+n09BctlwFnwP8TOAgCA10VRr2ZELaHhbBXx3HCVMC+71voXmD6g4Qi2NB13e1DwAAkZhtmmKYRcxsJhHeBPc8bMMufbDU0PxF4vV4TSyth8477csI6lTV/a71er9tioonBarXaIAmLErliNWyqoM8nrJPpHFNLWLcI4xvj5XKpMo2ZgcvzIvs+75S0wKwPPB/CnpWXsG00Gra2WCwshekOVoC9Sb6IGN1ge2HNsWK+B0iJqxAL5oSpYeDJJW02mxVYLAWSGfDtebylA68Bc4wh+ahK5PcxLh6PR5GUpym/iTOfz89PqNVqhRI4iQf1/o174HNMVYDSGeTDmXQ3znogCGrtdpsYVBhER1aH2Wzm8iE7UR74DMTWGNxUQWmNYqTEzq8APoo9sJ8wKoR5eU7T6VQVjZAvx4YvDJWt1Ol0QsTqkppF8EW8/12OhTnSpT2LCe2/KiCTyUQVkJgPuwgb6XG32w05Xui4q0imLLNDxA/uSuZ4PNaZqZlSsejDYfd6veihj8RoNDK5XOUHAen3Dfr9/j7VZxEJ6AwuxCCvhMTM7oNAARhl/0Ay4Az419qXAAMAfBdK7281j6YAAAAASUVORK5CYII=); +} + +#epiceditor-utilbar button.epiceditor-toggle-edit-btn { + background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnVJREFUeNrsV+lt4lAQNtEWAB2QCtZ0ABWsqYB1BU5AgDgkQOISl9cV4FQQtgLYCuKtIN4K4gogM9H3pMmLTTh24c+O9An7+XnOb8aP1G63M64hN8aV5GqGvxyyyfO8Y3S6hDtc2wTfcZyLRPwdvxEbPSviIwjIRtO49s9ONSRLMIGvwkBI+EPYEEqyQmcZdl2Xo3BgcJ90xPUGDh1veLFYWCBK9oQ6P4BgiXVO6fUjg5zCJcE6kVxsLEN4QTlWzO5yuRwlGp7P55zOxz1RRqK2SfKD8BvOG4IHxUqlEnxop9lsZpITa0KWndGwIeQIXswzHbynpK2xzjXbeBfxdDrlhbUWjYyuiJS9fBJxgL3PItKsprNQrVaD1GQyYUVP2oYelDziPoPnT5+k2QajleEC3usI/exM7oYiXor0hpxS8qhLv5FIVVrcBwkp5ueclbS27qNMvkj7kg3nZX1qtVqAaSUN5OGUwn2M4RUb3263plh700E6I7wTKn1suCAW3PF4zL1r1Ov1SBhXZLEJFqGTQCq5N1BZIp3s+DOi5fXcG7lGo1Ea5DIFSWzcq7a4R6uYqJmlkSqHoeGKeq+w905MtGKj0Yje9TE5ID9pqictQQwmXTfh82cIJ0NML0d0QY8MdhMn13A4zENB0hAJEYn6EnGL3MZ0hsyG3Ww2g70jU8lgMOhqHicJz+KfovVkz3J5/FardfhBgDZzS90SeoJ8cXjQJlUAEmZUCx30kYiTfr9vgFRc72+ChCHSzNH+Qgk+fA7b7fZZJ5AAiIRhT4zUv3/Y07LiaPW9yPE2L5jrI/p/d7wVEZe0U8bJkvr/F+ZS8irAAIorRozUvI0gAAAAAElFTkSuQmCC); +} + +#epiceditor-utilbar button.epiceditor-fullscreen-btn { + background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAY5JREFUeNrsV8tthDAQhYgCtoV04BKghK2En5AQBw4IITjwq2RLgBJcwpawHZAxMpGDYGwTFPaQkUbG1sybj58NmNM0GVfIh3GRXBbYEid932N9d1zXHWWAGAb4m+9VsUA0H5SubKkKIGA4qyUC2qKBxSCe541HKln7dV2nVbHRtq0Nw7CXGNtz/jzwqjZ5odtqTOagQRC82KRpGkcS/L2Ok7lXZV3Xp7Q6DMNR2mqNthMhKXLqzcUCc6Wgn3wU1wlX1PboHs8tjaLoyVtLT7JFK2ZZM6CZvWyE+X1Voaj3la3DMA63epGqqm4wfyCBH8xmz1+Z1WVZ2vyqU2HvHtv9OI4PsRpjL91YV2a7pcB8onmOifYFUhSFLQCTnQtkDpokyYv73JBtcAQsA3zGTXJBEgNXgrF3CcrBZGwnC+4uqxHnH+zN8/ybvexZwvZNhlsHbrt5CyCgDtu1XosUe58K4kuOF9EKnKYp20eVrxDUJssyreM0bDg4kIw0EfCbftvqQ6KKYf7/wvyVfAkwAMjqoFSaWbrBAAAAAElFTkSuQmCC); +} + +@media +only screen and (-webkit-min-device-pixel-ratio: 2), +only screen and ( min--moz-device-pixel-ratio: 2), +only screen and ( -o-min-device-pixel-ratio: 2/1), +only screen and ( min-device-pixel-ratio: 2), +only screen and ( min-resolution: 192dpi), +only screen and ( min-resolution: 2dppx) { + #epiceditor-utilbar button.epiceditor-toggle-preview-btn { + background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAABrVBMVEWIiIiHh4eIiIiJiYmEhISEhISEhISMjIyNjY2MjIyNjY2MjIyFhYWDg4OFhYWKioqKioqBgYGJiYmJiYmJiYmKioqKioqFhYWIiIiJiYmIiIiJiYmIiIiGhoaIiIiGhoaIiIiHh4eHh4eGhoaFhYWKioqFhYWEhISFhYWEhISDg4ONjY2NjY2MjIyHh4eMjIyLi4uCgoKCgoKCgoKBgYGBgYGOjo6Ojo6BgYGPj4+Ojo6Hh4eNjY2Pj4+BgYGNjY2BgYGHh4eQkJCNjY2FhYWHh4eDg4OQkJCQkJCAgICCgoKAgICAgICQkJCQkJCQkJCAgICQkJCAgICQkJCFhYWHh4eHh4eGhoaKioqGhoaMjIyDg4OEhISDg4OEhISDg4OEhISKioqBgYGJiYmKioqKioqJiYmMjIyEhISCgoKGhoaLi4uPj4+Pj4+BgYGKioqDg4OGhoaGhoaMjIyHh4eEhISEhISDg4OIiIiJiYmGhoaHh4eJiYkAAACAgICFhYWQkJCIiIiEhISLi4uDg4OHh4eGhoaCgoKBgYGJiYmMjIyOjo6KioqNjY2Pj4+o6Lr0AAAAfnRSTlPfvxDPEL8gv5/Pr3AQ3++vn8+/cCCA34/vj8/vMJ+vgJ+vYN/fMIBAv4/v38/v39+PgBBg74DPgGAwMHDvrzBQj1AggJ8wIHC/IM9gjxBQgO+vv89Qz0C/70Bgz89ggIDvYN/fII8wIHDvMJ+/QBBAMBDPnxBgML9AEI/vQACT8cYwAAACXElEQVR42u2WV1caURRGJ73H9J7YW+y99957ARREURAFQRFEOFGMkybxN+eec2cWUwBnrTwli/1yvvnm7PvEuoMAf0FGzsj/nDwxOTUzdyoxNzM1OWFU7h0aPtUxPNRrQPZ0XKagw3OF7Nm9TMOuJ43cQmpavSWV3HRhgKakcmvjhSEaW/VyV/tvg7R3aeU+9ULZ/fLEQ/ndMvXbPrV88EvBvQdOMCsLMzjNd5TFgSTr3SpWOCuUTYWTVVV6m+Sdr0qqqVGxw6pqXUPyZlziFaU9Vi3HVSyzag+D/YlcbXLZLm+85AsOgMK4hkIABz/YkSVVdpS3fnKevQCIYgCcGqKslGd0g3dbIIR5fP8cZCkMdOANWeSLEJZklt5StxEWcmLIuw+AHGE+YiEWE67jGxnlO8xv8OGTEBGRRSACmNtYyBUjgcCCKJPLqjYMAeD04ENEGIjQ7AGikuUFNhdF8Vogj0T5bDyqEjh55AwI4N7/hmRTe4zRxEM+ZuKYFSYeEP9HzPtuEFjm9pKf9W5KQLbKhSVMbkymfHL90i+s/wR5PM9iCaYiLOcLTogCrKEIYwkLD19T25/4bR+unSG3bkOQwiG1QZfV6gryBaqL5c01XCCZ9lbOCOvNUpouUOGishSK+dpKUHMZ2M6Jz7ZHNEO+hOoLUWVZZROx6a8hn+VcRWh1EOtBUuhcPiy+pBdg3fZ3DaOj2ma7LtXVW1uj0XVqTW2aS9/bUP8jJfUN3is+N97mp8nV9Wavka9kZ/e6zuzuNP59Hhkbn53+QkzPjo+NZP6TZOSM/L/LfwAJ69Ba3fXmtwAAAABJRU5ErkJggg==); + background-size: 30px 30px; + } + + #epiceditor-utilbar button.epiceditor-toggle-edit-btn { + background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAACNFBMVEWIiIiHh4eIiIiGhoaEhISDg4OCgoKDg4OMjIyNjY2MjIyMjIyLi4uMjIyKioqCgoKNjY2Ojo6Dg4ONjY2FhYWFhYWCgoKKioqKioqLi4uLi4uMjIyLi4uFhYWKioqKioqEhISJiYmJiYmEhISEhISFhYWEhISGhoaHh4eJiYmNjY2Hh4eKioqDg4ODg4OCgoKOjo6Ojo6Dg4OOjo6BgYGOjo6AgICCgoKNjY2Dg4OPj4+BgYGBgYGAgICOjo6BgYGOjo6AgICOjo6BgYGBgYGAgICPj4+QkJCQkJCQkJCQkJCPj4+NjY2IiIiIiIiFhYWFhYWEhISEhISGhoaDg4OGhoaDg4OHh4eDg4OCgoKMjIyLi4uLi4uKioqNjY2KioqNjY2NjY2NjY2Dg4OKioqGhoaJiYmIiIiGhoaFhYWEhISFhYWFhYWOjo6Pj4+FhYWGhoaPj4+Hh4eMjIyMjIyMjIyJiYmIiIiIiIiOjo6Ojo6CgoKBgYGPj4+QkJCHh4eHh4eEhISIiIiDg4OEhISDg4ODg4OLi4uHh4eEhISMjIyFhYWGhoaHh4eOjo6IiIiKioqKioqMjIyGhoaIiIiJiYmIiIiFhYWGhoaGhoaHh4eKioqJiYmLi4uIiIiGhoaJiYmHh4eGhoaGhoaJiYmFhYWKioqPj4+IiIiGhoYAAACAgICCgoKQkJCPj4+BgYGDg4OFhYWHh4eOjo6JiYmGhoaLi4uEhISIiIiNjY2MjIyKioqRGRgVAAAAq3RSTlNQz7+Pz4AQr4/vIK+Av99QICBQzyCv37/PcM+f378QgHAwgIBgnxBQMK+PgFCf34Bwn0BQv2Ag7zBwEFDvj4CAv1DvYK/f398gUICA39+fYHAwQHAQn78gz0DfYO8wgO+/YECPIK9gz89Av8/f31AwgK9A72AwIGCAMM+PICCvrxCfr+/fYCAgYO9QgGBQjyCPYM8QcM8wEEAgn58Qn+8w7+/fv0DvQEBA7wCxres+AAAC50lEQVR42u2XVUMbQRSFU3d3d3d3d8EpUKRoKbS40+LuDsEtQOFQKA2FEvhz3XsnS5IlJLB9a/s95J49M99sXpJsNPgD/st/oZwxJJGhTn4+RLxQJ78k9xnUyU+HJV6pk6OGiXR1ciy5sZizfPMrwzl9mIiak5x6/sLnUZmH9+9eqqAQCftyakXkqFUq7MpX6JbW2WBHDnxtmJUAIMiGfD3AYINArDdsCppNfmewxQqx4aRVObFm0ibLAW+aNYkz5ZL4SdusRIkI8SVKOcFyp/cu5ftYA6ySc4Kl3DZuxs4dhfAZV+CDQtNFm0lWuLsBFPoqXF9g9bjSZrlqypwqAC1TClqAtYprIVfLjT+f0gfAXyn7oY9GNZ/KiWVumX17OYYAIUp3O+AnDu7bZqxOk5zU+ZOpPwD0UABNBaEAaPYACBVCZ5Ik14vlg5ClVjFpC0NZ6lplGa0nxN2gSZkg2vtB9FOmKDUNCyemcTStMXXcpmh4yGUl5ToAORMOaGiflhtkoRKCfq41yTw+GFsHKeeIRUfwwbwKOo/eIATi3GQNivREVxyIZsqeADL1+swuvZEiAJ4UmsEUsVEODRAnNp2iulzekrVAbyJLPrYctJTJ7nGQjI7uMULXBIBjIyQWUWLeAGik0E39sQGKYU0QMmp1vGnADSjj0EFtk5uOjuKzOtgok8r34rxasMzEjDFhjdDJNsE7u2VXh9oYDgNllp/n8IgfzJbwXhq9zlRu5soZWtFFl/Zy8SkaljK0R6inJTEinLQo5aSFE889kkqUWvsCdM37ZcnHYnrNBhably5QyoJDtFuJK1xMF3mHgVlkHM2e4eYB02Xxfts/NwXBuSMWLIG7sTmbb/+Hzj3fy1wuQD6N3DMX5/g0VHDDQ3aXAR4jXsEb5/co9fbLN2KdlJbO/zmM5a0qH+KukXxOnfzoO5GmTr5M7mOoktP4xrfUyffIvQ118pNBiTvq5AeDxNV//W+CFX4Doxg1lHC8FNUAAAAASUVORK5CYII=); + background-size: 30px 30px; + } + + #epiceditor-utilbar button.epiceditor-fullscreen-btn { + background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAMAAAANIilAAAAA5FBMVEWPj4+JiYmJiYmCgoKGhoaPj4+Li4uEhISAgICNjY2Li4uNjY2QkJCFhYWEhISLi4uHh4eHh4eIiIiHh4eGhoaHh4eEhISOjo6EhISJiYmFhYWJiYmFhYWNjY2NjY2Hh4eOjo6CgoKGhoaKioqKioqJiYmKioqDg4ODg4OBgYGGhoaKioqKioqLi4uMjIyMjIyIiIiIiIiMjIyOjo6GhoaDg4ONjY2EhISPj4+CgoIAAACJiYmHh4eIiIiFhYWCgoKPj4+Li4uAgICKioqEhISGhoaBgYGOjo6MjIyQkJCNjY2Dg4O2Jy/OAAAAO3RSTlMQz0DPEHDPz0DvMDBAQBBAQBAQz88wMM/v7zAw788Q7xAQQM9AEBDPEEAwMO/v7zDvMEBA70BAQEBAAAENmbYAAAG1SURBVHja7dftM0JREMdxD5GkEEqKpBJRSc8rQq7Q////2Dnm3t85jtkTmmGG75t7Z3c/t9fN0Tf6x1/Dz0ZrZGZf/BJ8a9QjM/vCwks9Px5aBcslC+P3nPVmi8dcczrcHHM2flug1CHRYcoYNd0YFlrAL1yHqPOC9g9IdbAfjHAjYFjoT+FI1MfRiAM/cZdEl0+oVidVvRaMcAOMgJWGBUYSVhrWjdfvjKqrq1Vzsu7Cy1Vo7XXZhYuj0ahwfHY+sjo/Oy7woyhitkTQsESsZawstG6VlvDCfIlUmfSVVjpDqtL8goBLbKFhsRcwalxcB100CDkxLLQbJxKwpvb3At7Y2iRuJzd4V26HuM2tDQkPWMOamu1Awkeetx2qtDy/lvZaCW173pGIWetA/xBbF+ZgiVgjGcdutLJ7xO1l9VnMiWGhJdzl4vx4CNpN+ifJXUy7RPEuZ2C1AIY1NG5kHI4Dx8MynnBtovYkqHzi25OyP8ONiKFh3djQsCIecn2i/lBvMU+UXxwi3HyE832jvD0RsLuP8CN3Oh0+feRmixE+g7CdLb43WiEz++Jb+M//x/gB/ArCl0G4nsFuEQAAAABJRU5ErkJggg==); + background-size: 30px 30px; + } +} + +#epiceditor-utilbar button:last-child { + margin-left:15px; +} + +#epiceditor-utilbar button:hover { + cursor:pointer; +} + +.epiceditor-edit-mode #epiceditor-utilbar button.epiceditor-toggle-edit-btn { + display:none; +} + +.epiceditor-preview-mode #epiceditor-utilbar button.epiceditor-toggle-preview-btn { + display:none; +} diff --git a/lib/EpicEditor/themes/editor/epic-dark.css b/lib/EpicEditor/themes/editor/epic-dark.css new file mode 100644 index 0000000..058ace6 --- /dev/null +++ b/lib/EpicEditor/themes/editor/epic-dark.css @@ -0,0 +1,13 @@ +html { padding:10px; } + +body { + border:0; + background:rgb(41,41,41); + font-family:monospace; + font-size:14px; + padding:10px; + color:#ddd; + line-height:1.35em; + margin:0; + padding:0; +} diff --git a/lib/EpicEditor/themes/editor/epic-light.css b/lib/EpicEditor/themes/editor/epic-light.css new file mode 100644 index 0000000..9411cec --- /dev/null +++ b/lib/EpicEditor/themes/editor/epic-light.css @@ -0,0 +1,12 @@ +html { padding:10px; } + +body { + border:0; + background:#fcfcfc; + font-family:monospace; + font-size:14px; + padding:10px; + line-height:1.35em; + margin:0; + padding:0; +} diff --git a/lib/EpicEditor/themes/preview/bartik.css b/lib/EpicEditor/themes/preview/bartik.css new file mode 100644 index 0000000..2ffb6d5 --- /dev/null +++ b/lib/EpicEditor/themes/preview/bartik.css @@ -0,0 +1,167 @@ +body { + font-family: Georgia, "Times New Roman", Times, serif; + line-height: 1.5; + font-size: 87.5%; + word-wrap: break-word; + margin: 2em; + padding: 0; + border: 0; + outline: 0; + background: #fff; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.0em 0 0.5em; + font-weight: inherit; +} + +h1 { + font-size: 1.357em; + color: #000; +} + +h2 { + font-size: 1.143em; +} + +p { + margin: 0 0 1.2em; +} + +del { + text-decoration: line-through; +} + +tr:nth-child(odd) { + background-color: #dddddd; +} + +img { + outline: 0; +} + +code { + background-color: #f2f2f2; + background-color: rgba(40, 40, 0, 0.06); +} + +pre { + background-color: #f2f2f2; + background-color: rgba(40, 40, 0, 0.06); + margin: 10px 0; + overflow: hidden; + padding: 15px; + white-space: pre-wrap; +} + +pre code { + font-size: 100%; + background-color: transparent; +} + +blockquote { + background: #f7f7f7; + border-left: 1px solid #bbb; + font-style: italic; + margin: 1.5em 10px; + padding: 0.5em 10px; +} + +blockquote:before { + color: #bbb; + content: "\201C"; + font-size: 3em; + line-height: 0.1em; + margin-right: 0.2em; + vertical-align: -.4em; +} + +blockquote:after { + color: #bbb; + content: "\201D"; + font-size: 3em; + line-height: 0.1em; + vertical-align: -.45em; +} + +blockquote > p:first-child { + display: inline; +} + +table { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + border: 0; + border-spacing: 0; + font-size: 0.857em; + margin: 10px 0; + width: 100%; +} + +table table { + font-size: 1em; +} + +table tr th { + background: #757575; + background: rgba(0, 0, 0, 0.51); + border-bottom-style: none; +} + +table tr th, +table tr th a, +table tr th a:hover { + color: #FFF; + font-weight: bold; +} + +table tbody tr th { + vertical-align: top; +} + +tr td, +tr th { + padding: 4px 9px; + border: 1px solid #fff; + text-align: left; /* LTR */ +} + +tr:nth-child(odd) { + background: #e4e4e4; + background: rgba(0, 0, 0, 0.105); +} + +tr, +tr:nth-child(even) { + background: #efefef; + background: rgba(0, 0, 0, 0.063); +} + +a { + color: #0071B3; +} + +a:hover, +a:focus { + color: #018fe2; +} + +a:active { + color: #23aeff; +} + +a:link, +a:visited { + text-decoration: none; +} + +a:hover, +a:active, +a:focus { + text-decoration: underline; +} + diff --git a/lib/EpicEditor/themes/preview/github.css b/lib/EpicEditor/themes/preview/github.css new file mode 100644 index 0000000..4c78db4 --- /dev/null +++ b/lib/EpicEditor/themes/preview/github.css @@ -0,0 +1,368 @@ +html { padding:0 10px; } + +body { + margin:0; + padding:0; + background:#fff; +} + +#epiceditor-wrapper{ + background:white; +} + +#epiceditor-preview{ + padding-top:10px; + padding-bottom:10px; + font-family: Helvetica,arial,freesans,clean,sans-serif; + font-size:13px; + line-height:1.6; +} + +#epiceditor-preview>*:first-child{ + margin-top:0!important; +} + +#epiceditor-preview>*:last-child{ + margin-bottom:0!important; +} + +#epiceditor-preview a{ + color:#4183C4; + text-decoration:none; +} + +#epiceditor-preview a:hover{ + text-decoration:underline; +} + +#epiceditor-preview h1, +#epiceditor-preview h2, +#epiceditor-preview h3, +#epiceditor-preview h4, +#epiceditor-preview h5, +#epiceditor-preview h6{ + margin:20px 0 10px; + padding:0; + font-weight:bold; + -webkit-font-smoothing:antialiased; +} + +#epiceditor-preview h1 tt, +#epiceditor-preview h1 code, +#epiceditor-preview h2 tt, +#epiceditor-preview h2 code, +#epiceditor-preview h3 tt, +#epiceditor-preview h3 code, +#epiceditor-preview h4 tt, +#epiceditor-preview h4 code, +#epiceditor-preview h5 tt, +#epiceditor-preview h5 code, +#epiceditor-preview h6 tt, +#epiceditor-preview h6 code{ + font-size:inherit; +} + +#epiceditor-preview h1{ + font-size:28px; + color:#000; +} + +#epiceditor-preview h2{ + font-size:24px; + border-bottom:1px solid #ccc; + color:#000; +} + +#epiceditor-preview h3{ + font-size:18px; +} + +#epiceditor-preview h4{ + font-size:16px; +} + +#epiceditor-preview h5{ + font-size:14px; +} + +#epiceditor-preview h6{ + color:#777; + font-size:14px; +} + +#epiceditor-preview p, +#epiceditor-preview blockquote, +#epiceditor-preview ul, +#epiceditor-preview ol, +#epiceditor-preview dl, +#epiceditor-preview li, +#epiceditor-preview table, +#epiceditor-preview pre{ + margin:15px 0; +} + +#epiceditor-preview hr{ + background:transparent url('../../images/modules/pulls/dirty-shade.png') repeat-x 0 0; + border:0 none; + color:#ccc; + height:4px; + padding:0; +} + +#epiceditor-preview>h2:first-child, +#epiceditor-preview>h1:first-child, +#epiceditor-preview>h1:first-child+h2, +#epiceditor-preview>h3:first-child, +#epiceditor-preview>h4:first-child, +#epiceditor-preview>h5:first-child, +#epiceditor-preview>h6:first-child{ + margin-top:0; + padding-top:0; +} + +#epiceditor-preview h1+p, +#epiceditor-preview h2+p, +#epiceditor-preview h3+p, +#epiceditor-preview h4+p, +#epiceditor-preview h5+p, +#epiceditor-preview h6+p{ + margin-top:0; +} + +#epiceditor-preview li p.first{ + display:inline-block; +} + +#epiceditor-preview ul, +#epiceditor-preview ol{ + padding-left:30px; +} + +#epiceditor-preview ul li>:first-child, +#epiceditor-preview ol li>:first-child{ + margin-top:0; +} + +#epiceditor-preview ul li>:last-child, +#epiceditor-preview ol li>:last-child{ + margin-bottom:0; +} + +#epiceditor-preview dl{ + padding:0; +} + +#epiceditor-preview dl dt{ + font-size:14px; + font-weight:bold; + font-style:italic; + padding:0; + margin:15px 0 5px; +} + +#epiceditor-preview dl dt:first-child{ + padding:0; +} + +#epiceditor-preview dl dt>:first-child{ + margin-top:0; +} + +#epiceditor-preview dl dt>:last-child{ + margin-bottom:0; +} + +#epiceditor-preview dl dd{ + margin:0 0 15px; + padding:0 15px; +} + +#epiceditor-preview dl dd>:first-child{ + margin-top:0; +} + +#epiceditor-preview dl dd>:last-child{ + margin-bottom:0; +} + +#epiceditor-preview blockquote{ + border-left:4px solid #DDD; + padding:0 15px; + color:#777; +} + +#epiceditor-preview blockquote>:first-child{ + margin-top:0; +} + +#epiceditor-preview blockquote>:last-child{ + margin-bottom:0; +} + +#epiceditor-preview table{ + padding:0; + border-collapse: collapse; + border-spacing: 0; + font-size: 100%; + font: inherit; +} + +#epiceditor-preview table tr{ + border-top:1px solid #ccc; + background-color:#fff; + margin:0; + padding:0; +} + +#epiceditor-preview table tr:nth-child(2n){ + background-color:#f8f8f8; +} + +#epiceditor-preview table tr th{ + font-weight:bold; +} + +#epiceditor-preview table tr th, +#epiceditor-preview table tr td{ + border:1px solid #ccc; + text-align:left; + margin:0; + padding:6px 13px; +} + +#epiceditor-preview table tr th>:first-child, +#epiceditor-preview table tr td>:first-child{ + margin-top:0; +} + +#epiceditor-preview table tr th>:last-child, +#epiceditor-preview table tr td>:last-child{ + margin-bottom:0; +} + +#epiceditor-preview img{ + max-width:100%; +} + +#epiceditor-preview span.frame{ + display:block; + overflow:hidden; +} + +#epiceditor-preview span.frame>span{ + border:1px solid #ddd; + display:block; + float:left; + overflow:hidden; + margin:13px 0 0; + padding:7px; + width:auto; +} + +#epiceditor-preview span.frame span img{ + display:block; + float:left; +} + +#epiceditor-preview span.frame span span{ + clear:both; + color:#333; + display:block; + padding:5px 0 0; +} + +#epiceditor-preview span.align-center{ + display:block; + overflow:hidden; + clear:both; +} + +#epiceditor-preview span.align-center>span{ + display:block; + overflow:hidden; + margin:13px auto 0; + text-align:center; +} + +#epiceditor-preview span.align-center span img{ + margin:0 auto; + text-align:center; +} + +#epiceditor-preview span.align-right{ + display:block; + overflow:hidden; + clear:both; +} + +#epiceditor-preview span.align-right>span{ + display:block; + overflow:hidden; + margin:13px 0 0; + text-align:right; +} + +#epiceditor-preview span.align-right span img{ + margin:0; + text-align:right; +} + +#epiceditor-preview span.float-left{ + display:block; + margin-right:13px; + overflow:hidden; + float:left; +} + +#epiceditor-preview span.float-left span{ + margin:13px 0 0; +} + +#epiceditor-preview span.float-right{ + display:block; + margin-left:13px; + overflow:hidden; + float:right; +} + +#epiceditor-preview span.float-right>span{ + display:block; + overflow:hidden; + margin:13px auto 0; + text-align:right; +} + +#epiceditor-preview code, +#epiceditor-preview tt{ + margin:0 2px; + padding:0 5px; + white-space:nowrap; + border:1px solid #eaeaea; + background-color:#f8f8f8; + border-radius:3px; +} + +#epiceditor-preview pre>code{ + margin:0; + padding:0; + white-space:pre; + border:none; + background:transparent; +} + +#epiceditor-preview .highlight pre, +#epiceditor-preview pre{ + background-color:#f8f8f8; + border:1px solid #ccc; + font-size:13px; + line-height:19px; + overflow:auto; + padding:6px 10px; + border-radius:3px; +} + +#epiceditor-preview pre code, +#epiceditor-preview pre tt{ + background-color:transparent; + border:none; +} diff --git a/lib/EpicEditor/themes/preview/preview-dark.css b/lib/EpicEditor/themes/preview/preview-dark.css new file mode 100644 index 0000000..620c193 --- /dev/null +++ b/lib/EpicEditor/themes/preview/preview-dark.css @@ -0,0 +1,121 @@ +html { padding:0 10px; } + +body { + margin:0; + padding:10px 0; + background:#000; +} + +#epiceditor-preview h1, +#epiceditor-preview h2, +#epiceditor-preview h3, +#epiceditor-preview h4, +#epiceditor-preview h5, +#epiceditor-preview h6, +#epiceditor-preview p, +#epiceditor-preview blockquote { + margin: 0; + padding: 0; +} +#epiceditor-preview { + background:#000; + font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #ccc; +} +#epiceditor-preview a { + color: #fff; +} +#epiceditor-preview a:hover { + color: #00ff00; + text-decoration: none; +} +#epiceditor-preview a img { + border: none; +} +#epiceditor-preview p { + margin-bottom: 9px; +} +#epiceditor-preview h1, +#epiceditor-preview h2, +#epiceditor-preview h3, +#epiceditor-preview h4, +#epiceditor-preview h5, +#epiceditor-preview h6 { + color: #cdcdcd; + line-height: 36px; +} +#epiceditor-preview h1 { + margin-bottom: 18px; + font-size: 30px; +} +#epiceditor-preview h2 { + font-size: 24px; +} +#epiceditor-preview h3 { + font-size: 18px; +} +#epiceditor-preview h4 { + font-size: 16px; +} +#epiceditor-preview h5 { + font-size: 14px; +} +#epiceditor-preview h6 { + font-size: 13px; +} +#epiceditor-preview hr { + margin: 0 0 19px; + border: 0; + border-bottom: 1px solid #ccc; +} +#epiceditor-preview blockquote { + padding: 13px 13px 21px 15px; + margin-bottom: 18px; + font-family:georgia,serif; + font-style: italic; +} +#epiceditor-preview blockquote:before { + content:"\201C"; + font-size:40px; + margin-left:-10px; + font-family:georgia,serif; + color:#eee; +} +#epiceditor-preview blockquote p { + font-size: 14px; + font-weight: 300; + line-height: 18px; + margin-bottom: 0; + font-style: italic; +} +#epiceditor-preview code, #epiceditor-preview pre { + font-family: Monaco, Andale Mono, Courier New, monospace; +} +#epiceditor-preview code { + background-color: #000; + color: #f92672; + padding: 1px 3px; + font-size: 12px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +#epiceditor-preview pre { + display: block; + padding: 14px; + color:#66d9ef; + margin: 0 0 18px; + line-height: 16px; + font-size: 11px; + border: 1px solid #d9d9d9; + white-space: pre-wrap; + word-wrap: break-word; +} +#epiceditor-preview pre code { + background-color: #000; + color:#ccc; + font-size: 11px; + padding: 0; +} diff --git a/lib/Michelf/Markdown.php b/lib/Michelf/Markdown.php new file mode 100644 index 0000000..094b0ee --- /dev/null +++ b/lib/Michelf/Markdown.php @@ -0,0 +1,3096 @@ + +# +# Original Markdown +# Copyright (c) 2004-2006 John Gruber +# +# +namespace Michelf; + + +# +# Markdown Parser Class +# + +class Markdown { + + ### Version ### + + const MARKDOWNLIB_VERSION = "1.3"; + + ### Simple Function Interface ### + + public static function defaultTransform($text) { + # + # Initialize the parser and return the result of its transform method. + # This will work fine for derived classes too. + # + # Take parser class on which this function was called. + $parser_class = \get_called_class(); + + # try to take parser from the static parser list + static $parser_list; + $parser =& $parser_list[$parser_class]; + + # create the parser it not already set + if (!$parser) + $parser = new $parser_class; + + # Transform text using parser. + return $parser->transform($text); + } + + ### Configuration Variables ### + + # Change to ">" for HTML output. + public $empty_element_suffix = " />"; + public $tab_width = 4; + + # Change to `true` to disallow markup or entities. + public $no_markup = false; + public $no_entities = false; + + # Predefined urls and titles for reference links and images. + public $predef_urls = array(); + public $predef_titles = array(); + + + ### Parser Implementation ### + + # Regex to match balanced [brackets]. + # Needed to insert a maximum bracked depth while converting to PHP. + protected $nested_brackets_depth = 6; + protected $nested_brackets_re; + + protected $nested_url_parenthesis_depth = 4; + protected $nested_url_parenthesis_re; + + # Table of hash values for escaped characters: + protected $escape_chars = '\`*_{}[]()>#+-.!'; + protected $escape_chars_re; + + + public function __construct() { + # + # Constructor function. Initialize appropriate member variables. + # + $this->_initDetab(); + $this->prepareItalicsAndBold(); + + $this->nested_brackets_re = + str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). + str_repeat('\])*', $this->nested_brackets_depth); + + $this->nested_url_parenthesis_re = + str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). + str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + + $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + + # Sort document, block, and span gamut in ascendent priority order. + asort($this->document_gamut); + asort($this->block_gamut); + asort($this->span_gamut); + } + + + # Internal hashes used during transformation. + protected $urls = array(); + protected $titles = array(); + protected $html_hashes = array(); + + # Status flag to avoid invalid nesting. + protected $in_anchor = false; + + + protected function setup() { + # + # Called before the transformation process starts to setup parser + # states. + # + # Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); + + $this->in_anchor = false; + } + + protected function teardown() { + # + # Called after the transformation process to clear any variable + # which may be taking up memory unnecessarly. + # + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } + + + public function transform($text) { + # + # Main function. Performs some preprocessing on the input text + # and pass it through the document gamut. + # + $this->setup(); + + # Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + + # Standardize line endings: + # DOS to Unix and Mac to Unix + $text = preg_replace('{\r\n?}', "\n", $text); + + # Make sure $text ends with a couple of newlines: + $text .= "\n\n"; + + # Convert all tabs to spaces. + $text = $this->detab($text); + + # Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks($text); + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ ]*\n+/ . + $text = preg_replace('/^[ ]+$/m', '', $text); + + # Run document gamut methods. + foreach ($this->document_gamut as $method => $priority) { + $text = $this->$method($text); + } + + $this->teardown(); + + return $text . "\n"; + } + + protected $document_gamut = array( + # Strip link definitions, store in hashes. + "stripLinkDefinitions" => 20, + + "runBasicBlockGamut" => 30, + ); + + + protected function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:\n+|\Z) + }xm', + array(&$this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + return ''; # String that will replace the block + } + + + protected function hashHTMLBlocks($text) { + if ($this->no_markup) return $text; + + $less_than_tab = $this->tab_width - 1; + + # Hashify HTML blocks: + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap

    s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded: + # + # * List "a" is made of tags which can be both inline or block-level. + # These will be treated block-level when the start tag is alone on + # its line, otherwise they're not matched here and will be taken as + # inline later. + # * List "b" is made of tags which are always block-level; + # + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. + 'script|noscript|form|fieldset|iframe|math|svg|'. + 'article|section|nav|aside|hgroup|header|footer|'. + 'figure'; + + # Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' + (?> # optional tag attributes + \s # starts with whitespace + (?> + [^>"/]+ # text outside quotes + | + /+(?!>) # slash not followed by ">" + | + "[^"]*" # text inside double quotes (tolerate ">") + | + \'[^\']*\' # text inside single quotes (tolerate ">") + )* + )? + '; + $content = + str_repeat(' + (?> + [^<]+ # content without tag + | + <\2 # nested opening tag + '.$attr.' # attributes + (?> + /> + | + >', $nested_tags_level). # end of opening tag + '.*?'. # last level nested tag content + str_repeat(' + # closing nested tag + ) + | + <(?!/\2\s*> # other tags with a different name + ) + )*', + $nested_tags_level); + $content2 = str_replace('\2', '\3', $content); + + # First, look for nested blocks, e.g.: + #

    + #
    + # tags for inner block must be indented. + #
    + #
    + # + # The outermost tags must start at the left margin for this to match, and + # the inner nested divs must be indented. + # We need to do this before the next, more liberal match, because the next + # match will start at the first `
    ` and stop at the first `
    `. + $text = preg_replace_callback('{(?> + (?> + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + + # Match from `\n` to `\n`, handling nested tags + # in between. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_b_re.')# start tag = $2 + '.$attr.'> # attributes followed by > and \n + '.$content.' # content, support nesting + # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special version for tags of group a. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_a_re.')# start tag = $3 + '.$attr.'>[ ]*\n # attributes followed by > + '.$content2.' # content, support nesting + # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for
    . It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,'.$less_than_tab.'} + <(hr) # start tag = $2 + '.$attr.' # attributes + /?> # the matching end tag + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # Special case for standalone HTML comments: + + [ ]{0,'.$less_than_tab.'} + (?s: + + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # PHP and ASP-style processor instructions ( + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + ) + )}Sxmi', + array(&$this, '_hashHTMLBlocks_callback'), + $text); + + return $text; + } + protected function _hashHTMLBlocks_callback($matches) { + $text = $matches[1]; + $key = $this->hashBlock($text); + return "\n\n$key\n\n"; + } + + + protected function hashPart($text, $boundary = 'X') { + # + # Called whenever a tag must be hashed when a function insert an atomic + # element in the text stream. Passing $text to through this function gives + # a unique text-token which will be reverted back when calling unhash. + # + # The $boundary argument specify what character should be used to surround + # the token. By convension, "B" is used for block elements that needs not + # to be wrapped into paragraph tags at the end, ":" is used for elements + # that are word separators and "X" is used in the general case. + # + # Swap back any tag hash found in $text so we do not have to `unhash` + # multiple times at the end. + $text = $this->unhash($text); + + # Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++$i . $boundary; + $this->html_hashes[$key] = $text; + return $key; # String that will replace the tag. + } + + + protected function hashBlock($text) { + # + # Shortcut function for hashPart with block-level boundaries. + # + return $this->hashPart($text, 'B'); + } + + + protected $block_gamut = array( + # + # These are all the transformations that form block-level + # tags like paragraphs, headers, and list items. + # + "doHeaders" => 10, + "doHorizontalRules" => 20, + + "doLists" => 40, + "doCodeBlocks" => 50, + "doBlockQuotes" => 60, + ); + + protected function runBlockGamut($text) { + # + # Run block gamut tranformations. + # + # We need to escape raw HTML in Markdown source before doing anything + # else. This need to be done for each block, and not only at the + # begining in the Markdown function since hashed blocks can be part of + # list items and could have been indented. Indented blocks would have + # been seen as a code block in a previous pass of hashHTMLBlocks. + $text = $this->hashHTMLBlocks($text); + + return $this->runBasicBlockGamut($text); + } + + protected function runBasicBlockGamut($text) { + # + # Run block gamut tranformations, without hashing HTML blocks. This is + # useful when HTML blocks are known to be already hashed, like in the first + # whole-document pass. + # + foreach ($this->block_gamut as $method => $priority) { + $text = $this->$method($text); + } + + # Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs($text); + + return $text; + } + + + protected function doHorizontalRules($text) { + # Do Horizontal Rules: + return preg_replace( + '{ + ^[ ]{0,3} # Leading space + ([-*_]) # $1: First marker + (?> # Repeated marker group + [ ]{0,2} # Zero, one, or two spaces. + \1 # Marker character + ){2,} # Group repeated at least twice + [ ]* # Tailing spaces + $ # End of line. + }mx', + "\n".$this->hashBlock("empty_element_suffix")."\n", + $text); + } + + + protected $span_gamut = array( + # + # These are all the transformations that occur *within* block-level + # tags like paragraphs, headers, and list items. + # + # Process character escapes, code spans, and inline HTML + # in one shot. + "parseSpan" => -30, + + # Process anchor and image tags. Images must come first, + # because ![foo][f] looks like an anchor. + "doImages" => 10, + "doAnchors" => 20, + + # Make links out of things like `` + # Must come after doAnchors, because you can use < and > + # delimiters in inline links like [this](). + "doAutoLinks" => 30, + "encodeAmpsAndAngles" => 40, + + "doItalicsAndBold" => 50, + "doHardBreaks" => 60, + ); + + protected function runSpanGamut($text) { + # + # Run span gamut tranformations. + # + foreach ($this->span_gamut as $method => $priority) { + $text = $this->$method($text); + } + + return $text; + } + + + protected function doHardBreaks($text) { + # Do hard breaks: + return preg_replace_callback('/ {2,}\n/', + array(&$this, '_doHardBreaks_callback'), $text); + } + protected function _doHardBreaks_callback($matches) { + return $this->hashPart("empty_element_suffix\n"); + } + + + protected function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + ) + }xs', + array(&$this, '_doAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link text][1] + # or [link text](/foo) + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeAttribute($url); + + $result = "titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + protected function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $url = $this->encodeAttribute($url); + + $result = "encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + + return $this->hashPart($result); + } + + + protected function doImages($text) { + # + # Turn Markdown image shortcuts into tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array(&$this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + ) + }xs', + array(&$this, '_doImages_inline_callback'), $text); + + return $text; + } + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeAttribute($this->urls[$link_id]); + $result = "\"$alt_text\"";titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeAttribute($url); + $result = "\"$alt_text\"";encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + + protected function doHeaders($text) { + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + # + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 + # ## Header 2 + # ## Header 2 with closing hashes ## + # ... + # ###### Header 6 + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + protected function _doHeaders_callback_setext($matches) { + # Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) + return $matches[0]; + + $level = $matches[2]{0} == '=' ? 1 : 2; + $block = "".$this->runSpanGamut($matches[1]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + protected function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $block = "".$this->runSpanGamut($matches[2]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + protected function doLists($text) { + # + # Form HTML ordered (numbered) and unordered (bulleted) lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $markers_relist = array( + $marker_ul_re => $marker_ol_re, + $marker_ol_re => $marker_ul_re, + ); + + foreach ($markers_relist as $marker_re => $other_marker_re) { + # Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + ([ ]{0,'.$less_than_tab.'}) # $3 = number of spaces + ('.$marker_re.') # $4 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $5 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + | + (?= # Lookahead for another kind of list + \n + \3 # Must have the same indentation + '.$other_marker_re.'[ ]+ + ) + ) + ) + '; // mx + + # We use a different prefix before nested lists than top-level lists. + # See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + } + + return $text; + } + protected function _doLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[\.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[4]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $result = $this->hashBlock("<$list_type>\n" . $result . ""); + return "\n". $result ."\n\n"; + } + + protected $list_level = 0; + + protected function processListItems($list_str, $marker_any_re) { + # + # Process the contents of a single ordered or unordered list, splitting it + # into individual list items. + # + # The $this->list_level global keeps track of when we're inside a list. + # Each time we enter a list, we increment it; when we leave a list, + # we decrement. If it's zero, we're not in a list anymore. + # + # We do this because when we're not inside a list, we want to treat + # something like this: + # + # I recommend upgrading to version + # 8. Oops, now this line is treated + # as a sub-list. + # + # As a single paragraph, despite the fact that the second line starts + # with a digit-period-space sequence. + # + # Whereas when we're inside a list (or sub-list), that line will be + # treated as the start of a sub-list. What a kludge, huh? This is + # an aspect of Markdown's syntax that's hard to parse perfectly + # without resorting to mind-reading. Perhaps the solution is to + # change the syntax rules such that sub-lists must start with a + # starting cardinal number; e.g. "1." or "a.". + + $this->list_level++; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array(&$this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + protected function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + # Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } + else { + # Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = preg_replace('/\n+$/', '', $item); + $item = $this->runSpanGamut($item); + } + + return "
  • " . $item . "
  • \n"; + } + + + protected function doCodeBlocks($text) { + # + # Process Markdown `
    ` blocks.
    +	#
    +		$text = preg_replace_callback('{
    +				(?:\n\n|\A\n?)
    +				(	            # $1 = the code block -- one or more lines, starting with a space/tab
    +				  (?>
    +					[ ]{'.$this->tab_width.'}  # Lines must start with a tab or a tab-width of spaces
    +					.*\n+
    +				  )+
    +				)
    +				((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
    +			}xm',
    +			array(&$this, '_doCodeBlocks_callback'), $text);
    +
    +		return $text;
    +	}
    +	protected function _doCodeBlocks_callback($matches) {
    +		$codeblock = $matches[1];
    +
    +		$codeblock = $this->outdent($codeblock);
    +		$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
    +
    +		# trim leading newlines and trailing newlines
    +		$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
    +
    +		$codeblock = "
    $codeblock\n
    "; + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + + + protected function makeCodeSpan($code) { + # + # Create a code span markup for $code. Called from handleSpanToken. + # + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + return $this->hashPart("$code"); + } + + + protected $em_relist = array( + '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(?em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + # Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + # Construct master expression from list. + $token_re = '{('. implode('|', $token_relist) .')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + protected function doItalicsAndBold($text) { + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + # + # Get prepared regular expression for seraching emphasis tokens + # in current context. + # + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + # + # Each loop iteration search for the next emphasis token. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + # Reached end of text span: empty stack without emitting. + # any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + # Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + # Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + # Other closing marker: close one em or strong and + # change current token state to match the other + $token_stack[0] = str_repeat($token{0}, 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + # Reached closing marker for both em and strong. + # Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + } else { + # Reached opening three-char emphasis marker. Push on token + # stack; will be handled by the special condition above. + $em = $token{0}; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + # Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + # Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + # Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + # Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + return $text_stack[0]; + } + + + protected function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array(&$this, '_doBlockQuotes_callback'), $text); + + return $text; + } + protected function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + # trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); # recurse + + $bq = preg_replace('/^/m', " ", $bq); + # These leading spaces cause problem with
     content, 
    +		# so we need to fix that:
    +		$bq = preg_replace_callback('{(\s*
    .+?
    )}sx', + array(&$this, '_doBlockQuotes_callback2'), $bq); + + return "\n". $this->hashBlock("
    \n$bq\n
    ")."\n\n"; + } + protected function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + + protected function formParagraphs($text) { + # + # Params: + # $text - string to process with html

    tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap

    tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + # Is a paragraph. + $value = $this->runSpanGamut($value); + $value = preg_replace('/^([ ]*)/', "

    ", $value); + $value .= "

    "; + $grafs[$key] = $this->unhash($value); + } + else { + # Is a block. + # Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 =
    tag +//
    ]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (
    ) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// # We can't call Markdown(), because that resets the hash; +// # that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// # Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + + protected function encodeAttribute($text) { + # + # Encode text for a double-quoted HTML attribute. This function + # is *not* suitable for attributes enclosed in single quotes. + # + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '"', $text); + return $text; + } + + + protected function encodeAmpsAndAngles($text) { + # + # Smart processing for ampersands and angle brackets that need to + # be encoded. Valid character entities are left alone unless the + # no-entities mode is set. + # + if ($this->no_entities) { + $text = str_replace('&', '&', $text); + } else { + # Ampersand-encoding based entirely on Nat Irons's Amputator + # MT plugin: + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&', $text);; + } + # Encode remaining <'s + $text = str_replace('<', '<', $text); + + return $text; + } + + + protected function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i', + array(&$this, '_doAutoLinks_url_callback'), $text); + + # Email addresses: + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + (?: + [-!#$%&\'*+/=?^_`.{|}~\w\x80-\xFF]+ + | + ".*?" + ) + \@ + (?: + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + | + \[[\d.a-fA-F:]+\] # IPv4 & IPv6 + ) + ) + > + }xi', + array(&$this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + protected function _doAutoLinks_url_callback($matches) { + $url = $this->encodeAttribute($matches[1]); + $link = "$url"; + return $this->hashPart($link); + } + protected function _doAutoLinks_email_callback($matches) { + $address = $matches[1]; + $link = $this->encodeEmailAddress($address); + return $this->hashPart($link); + } + + + protected function encodeEmailAddress($addr) { + # + # Input: an email address, e.g. "foo@example.com" + # + # Output: the email address as a mailto link, with each character + # of the address encoded as either a decimal or hex entity, in + # the hopes of foiling most address harvesting spam bots. E.g.: + # + #

    foo@exampl + # e.com

    + # + # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + # With some optimizations by Milian Wolff. + # + $addr = "mailto:" . $addr; + $chars = preg_split('/(? $char) { + $ord = ord($char); + # Ignore non-ascii chars. + if ($ord < 128) { + $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. + # roughly 10% raw, 45% hex, 45% dec + # '@' *must* be encoded. I insist. + if ($r > 90 && $char != '@') /* do nothing */; + else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; + else $chars[$key] = '&#'.$ord.';'; + } + } + + $addr = implode('', $chars); + $text = implode('', array_slice($chars, 7)); # text without `mailto:` + $addr = "$text"; + + return $addr; + } + + + protected function parseSpan($str) { + # + # Take the string $str and parse it into tokens, hashing embeded HTML, + # escaped characters and handling code spans. + # + $output = ''; + + $span_re = '{ + ( + \\\\'.$this->escape_chars_re.' + | + (?no_markup ? '' : ' + | + # comment + | + <\?.*?\?> | <%.*?%> # processing instruction + | + <[!$]?[-a-zA-Z0-9:_]+ # regular tags + (?> + \s + (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* + )? + > + | + <[-a-zA-Z0-9:_]+\s*/> # xml-style empty tag + | + # closing tag + ').' + ) + }xs'; + + while (1) { + # + # Each loop iteration seach for either the next tag, the next + # openning code span marker, or the next escaped character. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); + + # Create token from text preceding tag. + if ($parts[0] != "") { + $output .= $parts[0]; + } + + # Check if we reach the end. + if (isset($parts[1])) { + $output .= $this->handleSpanToken($parts[1], $parts[2]); + $str = $parts[2]; + } + else { + break; + } + } + + return $output; + } + + + protected function handleSpanToken($token, &$str) { + # + # Handle $token provided by parseSpan by determining its nature and + # returning the corresponding value that should replace it. + # + switch ($token{0}) { + case "\\": + return $this->hashPart("&#". ord($token{1}). ";"); + case "`": + # Search for end marker in remaining text. + if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', + $str, $matches)) + { + $str = $matches[2]; + $codespan = $this->makeCodeSpan($matches[1]); + return $this->hashPart($codespan); + } + return $token; // return as text since no ending marker found. + default: + return $this->hashPart($token); + } + } + + + protected function outdent($text) { + # + # Remove one level of line-leading tabs or spaces + # + return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); + } + + + # String length function for detab. `_initDetab` will create a function to + # hanlde UTF-8 if the default function does not exist. + protected $utf8_strlen = 'mb_strlen'; + + protected function detab($text) { + # + # Replace tabs with the appropriate amount of space. + # + # For each line we separate the line in blocks delemited by + # tab characters. Then we reconstruct every line by adding the + # appropriate number of space between each blocks. + + $text = preg_replace_callback('/^.*\t.*$/m', + array(&$this, '_detab_callback'), $text); + + return $text; + } + protected function _detab_callback($matches) { + $line = $matches[0]; + $strlen = $this->utf8_strlen; # strlen function for UTF-8. + + # Split in blocks. + $blocks = explode("\t", $line); + # Add each blocks to the line. + $line = $blocks[0]; + unset($blocks[0]); # Do not add first block twice. + foreach ($blocks as $block) { + # Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - + $strlen($line, 'UTF-8') % $this->tab_width; + $line .= str_repeat(" ", $amount) . $block; + } + return $line; + } + protected function _initDetab() { + # + # Check for the availability of the function in the `utf8_strlen` property + # (initially `mb_strlen`). If the function is not available, create a + # function that will loosely count the number of UTF-8 characters with a + # regular expression. + # + if (function_exists($this->utf8_strlen)) return; + $this->utf8_strlen = create_function('$text', 'return preg_match_all( + "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", + $text, $m);'); + } + + + protected function unhash($text) { + # + # Swap back in all the tags hashed by _HashHTMLBlocks. + # + return preg_replace_callback('/(.)\x1A[0-9]+\1/', + array(&$this, '_unhash_callback'), $text); + } + protected function _unhash_callback($matches) { + return $this->html_hashes[$matches[0]]; + } + +} + + +# +# Temporary Markdown Extra Parser Implementation Class +# +# NOTE: DON'T USE THIS CLASS +# Currently the implementation of of Extra resides here in this temporary class. +# This makes it easier to propagate the changes between the three different +# packaging styles of PHP Markdown. When this issue is resolved, this +# MarkdownExtra_TmpImpl class here will disappear and \Michelf\MarkdownExtra +# will contain the code. So please use \Michelf\MarkdownExtra and ignore this +# one. +# + +class _MarkdownExtra_TmpImpl extends \Michelf\Markdown { + + ### Configuration Variables ### + + # Prefix for footnote ids. + public $fn_id_prefix = ""; + + # Optional title attribute for footnote links and backlinks. + public $fn_link_title = ""; + public $fn_backlink_title = ""; + + # Optional class attribute for footnote links and backlinks. + public $fn_link_class = "footnote-ref"; + public $fn_backlink_class = "footnote-backref"; + + # Class name for table cell alignment (%% replaced left/center/right) + # For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center' + # If empty, the align attribute is used instead of a class name. + public $table_align_class_tmpl = ''; + + # Optional class prefix for fenced code block. + public $code_class_prefix = ""; + # Class attribute for code blocks goes on the `code` tag; + # setting this to true will put attributes on the `pre` tag instead. + public $code_attr_on_pre = false; + + # Predefined abbreviations. + public $predef_abbr = array(); + + + ### Parser Implementation ### + + public function __construct() { + # + # Constructor function. Initialize the parser object. + # + # Add extra escapable characters before parent constructor + # initialize the table. + $this->escape_chars .= ':|'; + + # Insert extra document, block, and span transformations. + # Parent constructor will do the sorting. + $this->document_gamut += array( + "doFencedCodeBlocks" => 5, + "stripFootnotes" => 15, + "stripAbbreviations" => 25, + "appendFootnotes" => 50, + ); + $this->block_gamut += array( + "doFencedCodeBlocks" => 5, + "doTables" => 15, + "doDefLists" => 45, + ); + $this->span_gamut += array( + "doFootnotes" => 5, + "doAbbreviations" => 70, + ); + + parent::__construct(); + } + + + # Extra variables used during extra transformations. + protected $footnotes = array(); + protected $footnotes_ordered = array(); + protected $footnotes_ref_count = array(); + protected $footnotes_numbers = array(); + protected $abbr_desciptions = array(); + protected $abbr_word_re = ''; + + # Give the current footnote number. + protected $footnote_counter = 1; + + + protected function setup() { + # + # Setting up Extra-specific variables. + # + parent::setup(); + + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + $this->footnote_counter = 1; + + foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + } + } + + protected function teardown() { + # + # Clearing Extra-specific variables. + # + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->footnotes_ref_count = array(); + $this->footnotes_numbers = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + + parent::teardown(); + } + + + ### Extra Attribute Parser ### + + # Expression to use to catch attributes (includes the braces) + protected $id_class_attr_catch_re = '\{((?:[ ]*[#.][-_:a-zA-Z0-9]+){1,})[ ]*\}'; + # Expression to use when parsing in a context when no capture is desired + protected $id_class_attr_nocatch_re = '\{(?:[ ]*[#.][-_:a-zA-Z0-9]+){1,}[ ]*\}'; + + protected function doExtraAttributes($tag_name, $attr) { + # + # Parse attributes caught by the $this->id_class_attr_catch_re expression + # and return the HTML-formatted list of attributes. + # + # Currently supported attributes are .class and #id. + # + if (empty($attr)) return ""; + + # Split on components + preg_match_all('/[#.][-_:a-zA-Z0-9]+/', $attr, $matches); + $elements = $matches[0]; + + # handle classes and ids (only first id taken into account) + $classes = array(); + $id = false; + foreach ($elements as $element) { + if ($element{0} == '.') { + $classes[] = substr($element, 1); + } else if ($element{0} == '#') { + if ($id === false) $id = substr($element, 1); + } + } + + # compose attributes as string + $attr_str = ""; + if (!empty($id)) { + $attr_str .= ' id="'.$id.'"'; + } + if (!empty($classes)) { + $attr_str .= ' class="'.implode(" ", $classes).'"'; + } + return $attr_str; + } + + + protected function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + (?: + <(.+?)> # url = $2 + | + (\S+?) # url = $3 + ) + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $4 + [")] + [ ]* + )? # title is optional + (?:[ ]* '.$this->id_class_attr_catch_re.' )? # $5 = extra id & class attr + (?:\n+|\Z) + }xm', + array(&$this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + protected function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $url = $matches[2] == '' ? $matches[3] : $matches[2]; + $this->urls[$link_id] = $url; + $this->titles[$link_id] =& $matches[4]; + $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]); + return ''; # String that will replace the block + } + + + ### HTML Block Parser ### + + # Tags that are always treated as block tags: + protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption'; + + # Tags treated as block tags only if the opening tag is alone on its line: + protected $context_block_tags_re = 'script|noscript|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video'; + + # Tags where markdown="1" default to span mode: + protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; + + # Tags which must not have their contents modified, no matter where + # they appear: + protected $clean_tags_re = 'script|math|svg'; + + # Tags that do not need to be closed. + protected $auto_close_tags_re = 'hr|img|param|source|track'; + + + protected function hashHTMLBlocks($text) { + # + # Hashify HTML Blocks and "clean tags". + # + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap

    s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded. + # + # This works by calling _HashHTMLBlocks_InMarkdown, which then calls + # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" + # attribute is found within a tag, _HashHTMLBlocks_InHTML calls back + # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. + # These two functions are calling each other. It's recursive! + # + if ($this->no_markup) return $text; + + # + # Call the HTML-in-Markdown hasher. + # + list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); + + return $text; + } + protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0, + $enclosing_tag_re = '', $span = false) + { + # + # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. + # + # * $indent is the number of space to be ignored when checking for code + # blocks. This is important because if we don't take the indent into + # account, something like this (which looks right) won't work as expected: + # + #

    + #
    + # Hello World. <-- Is this a Markdown code block or text? + #
    <-- Is this a Markdown code block or a real tag? + #
    + # + # If you don't like this, just don't indent the tag on which + # you apply the markdown="1" attribute. + # + # * If $enclosing_tag_re is not empty, stops at the first unmatched closing + # tag with that name. Nested tags supported. + # + # * If $span is true, text inside must treated as span. So any double + # newline will be replaced by a single newline so that it does not create + # paragraphs. + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to check for the presense of newlines around a block tag. + $newline_before_re = '/(?:^\n?|\n\n)*$/'; + $newline_after_re = + '{ + ^ # Start of text following the tag. + (?>[ ]*)? # Optional comment. + [ ]*\n # Must be followed by newline. + }xs'; + + # Regex to match any tag. + $block_tag_re = + '{ + ( # $2: Capture whole tag. + # Tag name. + '.$this->block_tags_re.' | + '.$this->context_block_tags_re.' | + '.$this->clean_tags_re.' | + (?!\s)'.$enclosing_tag_re.' + ) + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + # CData Block + | + # Code span marker + `+ + '. ( !$span ? ' # If not in span. + | + # Indented code block + (?: ^[ ]*\n | ^ | \n[ ]*\n ) + [ ]{'.($indent+4).'}[^\n]* \n + (?> + (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n + )* + | + # Fenced code block marker + (?<= ^ | \n ) + [ ]{0,'.($indent+3).'}~{3,} + [ ]* + (?: + \.?[-_:a-zA-Z0-9]+ # standalone class name + | + '.$this->id_class_attr_nocatch_re.' # extra attributes + )? + [ ]* + \n + ' : '' ). ' # End (if not is span). + ) + }xs'; + + + $depth = 0; # Current depth inside the tag tree. + $parsed = ""; # Parsed text that will be returned. + + # + # Loop through every tag until we find the closing tag of the parent + # or loop until reaching the end of text if no parent tag specified. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($block_tag_re, $text, 2, + PREG_SPLIT_DELIM_CAPTURE); + + # If in Markdown span mode, add a empty-string span-level hash + # after each newline to prevent triggering any block element. + if ($span) { + $void = $this->hashPart("", ':'); + $newline = "$void\n"; + $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; + } + + $parsed .= $parts[0]; # Text before current tag. + + # If end of $text has been reached. Stop loop. + if (count($parts) < 3) { + $text = ""; + break; + } + + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + $tag_re = preg_quote($tag); # For use in a regular expression. + + # + # Check for: Code span marker + # + if ($tag{0} == "`") { + # Find corresponding end marker. + $tag_re = preg_quote($tag); + if (preg_match('{^(?>.+?|\n(?!\n))*?(?.*\n)*?[ ]{'.($fence_indent).'}'.$fence_re.'[ ]*(?:\n|$)}', $text, + $matches)) + { + # End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + # No end marker: just skip it. + $parsed .= $tag; + } + } + # + # Check for: Indented code block. + # + else if ($tag{0} == "\n" || $tag{0} == " ") { + # Indented code block: pass it unchanged, will be handled + # later. + $parsed .= $tag; + } + # + # Check for: Opening Block level tag or + # Opening Context Block tag (like ins and del) + # used as a block tag (tag is alone on it's line). + # + else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) || + ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) && + preg_match($newline_before_re, $parsed) && + preg_match($newline_after_re, $text) ) + ) + { + # Need to parse tag and following text using the HTML parser. + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); + + # Make sure it stays outside of any paragraph by adding newlines. + $parsed .= "\n\n$block_text\n\n"; + } + # + # Check for: Clean tag (like script, math) + # HTML Comments, processing instructions. + # + else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Need to parse tag and following text using the HTML parser. + # (don't check for markdown attribute) + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); + + $parsed .= $block_text; + } + # + # Check for: Tag with same name as enclosing tag. + # + else if ($enclosing_tag_re !== '' && + # Same name as enclosing tag. + preg_match('{^= 0); + + return array($parsed, $text); + } + protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { + # + # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. + # + # * Calls $hash_method to convert any blocks. + # * Stops when the first opening tag closes. + # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. + # (it is not inside clean tags) + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to match `markdown` attribute inside of a tag. + $markdown_attr_re = ' + { + \s* # Eat whitespace before the `markdown` attribute + markdown + \s*=\s* + (?> + (["\']) # $1: quote delimiter + (.*?) # $2: attribute value + \1 # matching delimiter + | + ([^\s>]*) # $3: unquoted attribute value + ) + () # $4: make $3 always defined (avoid warnings) + }xs'; + + # Regex to match any tag. + $tag_re = '{ + ( # $2: Capture whole tag. + + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + # CData Block + ) + }xs'; + + $original_text = $text; # Save original text in case of faliure. + + $depth = 0; # Current depth inside the tag tree. + $block_text = ""; # Temporary text holder for current text. + $parsed = ""; # Parsed text that will be returned. + + # + # Get the name of the starting tag. + # (This pattern makes $base_tag_name_re safe without quoting.) + # + if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) + $base_tag_name_re = $matches[1]; + + # + # Loop through every tag until we find the corresponding closing tag. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (count($parts) < 3) { + # + # End of $text reached with unbalenced tag(s). + # In that case, we return original text unchanged and pass the + # first character as filtered to prevent an infinite loop in the + # parent function. + # + return array($original_text{0}, substr($original_text, 1)); + } + + $block_text .= $parts[0]; # Text before current tag. + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + + # + # Check for: Auto-close tag (like
    ) + # Comments and Processing Instructions. + # + if (preg_match('{^auto_close_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Just add the tag to the block as if it was text. + $block_text .= $tag; + } + else { + # + # Increase/decrease nested tag count. Only do so if + # the tag's name match base tag's. + # + if (preg_match('{^mode = $attr_m[2] . $attr_m[3]; + $span_mode = $this->mode == 'span' || $this->mode != 'block' && + preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag); + + # Calculate indent before tag. + if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { + $strlen = $this->utf8_strlen; + $indent = $strlen($matches[1], 'UTF-8'); + } else { + $indent = 0; + } + + # End preceding block with this tag. + $block_text .= $tag; + $parsed .= $this->$hash_method($block_text); + + # Get enclosing tag name for the ParseMarkdown function. + # (This pattern makes $tag_name_re safe without quoting.) + preg_match('/^<([\w:$]*)\b/', $tag, $matches); + $tag_name_re = $matches[1]; + + # Parse the content using the HTML-in-Markdown parser. + list ($block_text, $text) + = $this->_hashHTMLBlocks_inMarkdown($text, $indent, + $tag_name_re, $span_mode); + + # Outdent markdown text. + if ($indent > 0) { + $block_text = preg_replace("/^[ ]{1,$indent}/m", "", + $block_text); + } + + # Append tag content to parsed text. + if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; + else $parsed .= "$block_text"; + + # Start over with a new block. + $block_text = ""; + } + else $block_text .= $tag; + } + + } while ($depth > 0); + + # + # Hash last block text that wasn't processed inside the loop. + # + $parsed .= $this->$hash_method($block_text); + + return array($parsed, $text); + } + + + protected function hashClean($text) { + # + # Called whenever a tag must be hashed when a function inserts a "clean" tag + # in $text, it passes through this function and is automaticaly escaped, + # blocking invalid nested overlap. + # + return $this->hashPart($text, 'C'); + } + + + protected function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ \n]* + (?: + <(.+?)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ \n]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes + ) + }xs', + array(&$this, '_doAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link text][1] + # or [link text](/foo) + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ([^\[\]]+) # link text = $2; can\'t contain [ or ] + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + protected function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeAttribute($url); + + $result = "titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + protected function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("a", $dummy =& $matches[8]); + + + $url = $this->encodeAttribute($url); + + $result = "encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $attr; + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + + return $this->hashPart($result); + } + + + protected function doImages($text) { + # + # Turn Markdown image shortcuts into tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array(&$this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ \n]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ \n]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ \n]* + )? # title is optional + \) + (?:[ ]? '.$this->id_class_attr_catch_re.' )? # $8 = id/class attributes + ) + }xs', + array(&$this, '_doImages_inline_callback'), $text); + + return $text; + } + protected function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeAttribute($this->urls[$link_id]); + $result = "\"$alt_text\"";titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + if (isset($this->ref_attr[$link_id])) + $result .= $this->ref_attr[$link_id]; + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + protected function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + $attr = $this->doExtraAttributes("img", $dummy =& $matches[8]); + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeAttribute($url); + $result = "\"$alt_text\"";encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $attr; + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + + protected function doHeaders($text) { + # + # Redefined to add id and class attribute support. + # + # Setext-style headers: + # Header 1 {#header1} + # ======== + # + # Header 2 {#header2 .class1 .class2} + # -------- + # + $text = preg_replace_callback( + '{ + (^.+?) # $1: Header text + (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes + [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer + }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 {#header1} + # ## Header 2 {#header2} + # ## Header 2 with closing hashes ## {#header3.class1.class2} + # ... + # ###### Header 6 {.class2} + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + (?:[ ]+ '.$this->id_class_attr_catch_re.' )? # $3 = id/class attributes + [ ]* + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + protected function _doHeaders_callback_setext($matches) { + if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) + return $matches[0]; + $level = $matches[3]{0} == '=' ? 1 : 2; + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[2]); + $block = "".$this->runSpanGamut($matches[1]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + protected function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $attr = $this->doExtraAttributes("h$level", $dummy =& $matches[3]); + $block = "".$this->runSpanGamut($matches[2]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + protected function doTables($text) { + # + # Form HTML tables. + # + $less_than_tab = $this->tab_width - 1; + # + # Find tables with leading pipe. + # + # | Header 1 | Header 2 + # | -------- | -------- + # | Cell 1 | Cell 2 + # | Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] # Optional leading pipe (present) + (.+) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + [ ]* # Allowed whitespace. + [|] .* \n # Row content. + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array(&$this, '_doTable_leadingPipe_callback'), $text); + + # + # Find tables without leading pipe. + # + # Header 1 | Header 2 + # -------- | -------- + # Cell 1 | Cell 2 + # Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + (\S.*[|].*) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + .* [|] .* \n # Row content + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array(&$this, '_DoTable_callback'), $text); + + return $text; + } + protected function _doTable_leadingPipe_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove leading pipe for each row. + $content = preg_replace('/^ *[|]/m', '', $content); + + return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); + } + protected function _doTable_makeAlignAttr($alignname) + { + if (empty($this->table_align_class_tmpl)) + return " align=\"$alignname\""; + + $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl); + return " class=\"$classname\""; + } + protected function _doTable_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove any tailing pipes for each line. + $head = preg_replace('/[|] *$/m', '', $head); + $underline = preg_replace('/[|] *$/m', '', $underline); + $content = preg_replace('/[|] *$/m', '', $content); + + # Reading alignement from header underline. + $separators = preg_split('/ *[|] */', $underline); + foreach ($separators as $n => $s) { + if (preg_match('/^ *-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('right'); + else if (preg_match('/^ *:-+: *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('center'); + else if (preg_match('/^ *:-+ *$/', $s)) + $attr[$n] = $this->_doTable_makeAlignAttr('left'); + else + $attr[$n] = ''; + } + + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $head = $this->parseSpan($head); + $headers = preg_split('/ *[|] */', $head); + $col_count = count($headers); + $attr = array_pad($attr, $col_count, ''); + + # Write column headers. + $text = "\n"; + $text .= "\n"; + $text .= "\n"; + foreach ($headers as $n => $header) + $text .= " ".$this->runSpanGamut(trim($header))."\n"; + $text .= "\n"; + $text .= "\n"; + + # Split content by row. + $rows = explode("\n", trim($content, "\n")); + + $text .= "\n"; + foreach ($rows as $row) { + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $row = $this->parseSpan($row); + + # Split row by cell. + $row_cells = preg_split('/ *[|] */', $row, $col_count); + $row_cells = array_pad($row_cells, $col_count, ''); + + $text .= "\n"; + foreach ($row_cells as $n => $cell) + $text .= " ".$this->runSpanGamut(trim($cell))."\n"; + $text .= "\n"; + } + $text .= "\n"; + $text .= "
    "; + + return $this->hashBlock($text) . "\n"; + } + + + protected function doDefLists($text) { + # + # Form HTML definition lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable pattern to match any entire dl list: + $whole_list_re = '(?> + ( # $1 = whole list + ( # $2 + [ ]{0,'.$less_than_tab.'} + ((?>.*\S.*\n)+) # $3 = defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another term + [ ]{0,'.$less_than_tab.'} + (?: \S.*\n )+? # defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?! # Negative lookahead for another definition + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + ) + ) + )'; // mx + + $text = preg_replace_callback('{ + (?>\A\n?|(?<=\n\n)) + '.$whole_list_re.' + }mx', + array(&$this, '_doDefLists_callback'), $text); + + return $text; + } + protected function _doDefLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $list = $matches[1]; + + # Turn double returns into triple returns, so that we can make a + # paragraph for the last item in a list, if necessary: + $result = trim($this->processDefListItems($list)); + $result = "
    \n" . $result . "\n
    "; + return $this->hashBlock($result) . "\n\n"; + } + + + protected function processDefListItems($list_str) { + # + # Process the contents of a single definition list, splitting it + # into individual term and definition list items. + # + $less_than_tab = $this->tab_width - 1; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + # Process definition terms. + $list_str = preg_replace_callback('{ + (?>\A\n?|\n\n+) # leading line + ( # definition terms = $1 + [ ]{0,'.$less_than_tab.'} # leading whitespace + (?!\:[ ]|[ ]) # negative lookahead for a definition + # mark (colon) or more whitespace. + (?> \S.* \n)+? # actual term (not whitespace). + ) + (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed + # with a definition mark. + }xm', + array(&$this, '_processDefListItems_callback_dt'), $list_str); + + # Process actual definitions. + $list_str = preg_replace_callback('{ + \n(\n+)? # leading line = $1 + ( # marker space = $2 + [ ]{0,'.$less_than_tab.'} # whitespace before colon + \:[ ]+ # definition mark (colon) + ) + ((?s:.+?)) # definition text = $3 + (?= \n+ # stop at next definition mark, + (?: # next term or end of text + [ ]{0,'.$less_than_tab.'} \:[ ] | +
    | \z + ) + ) + }xm', + array(&$this, '_processDefListItems_callback_dd'), $list_str); + + return $list_str; + } + protected function _processDefListItems_callback_dt($matches) { + $terms = explode("\n", trim($matches[1])); + $text = ''; + foreach ($terms as $term) { + $term = $this->runSpanGamut(trim($term)); + $text .= "\n
    " . $term . "
    "; + } + return $text . "\n"; + } + protected function _processDefListItems_callback_dd($matches) { + $leading_line = $matches[1]; + $marker_space = $matches[2]; + $def = $matches[3]; + + if ($leading_line || preg_match('/\n{2,}/', $def)) { + # Replace marker with the appropriate whitespace indentation + $def = str_repeat(' ', strlen($marker_space)) . $def; + $def = $this->runBlockGamut($this->outdent($def . "\n\n")); + $def = "\n". $def ."\n"; + } + else { + $def = rtrim($def); + $def = $this->runSpanGamut($this->outdent($def)); + } + + return "\n
    " . $def . "
    \n"; + } + + + protected function doFencedCodeBlocks($text) { + # + # Adding the fenced code block syntax to regular Markdown: + # + # ~~~ + # Code block + # ~~~ + # + $less_than_tab = $this->tab_width; + + $text = preg_replace_callback('{ + (?:\n|\A) + # 1: Opening marker + ( + ~{3,} # Marker: three tilde or more. + ) + [ ]* + (?: + \.?([-_:a-zA-Z0-9]+) # 2: standalone class name + | + '.$this->id_class_attr_catch_re.' # 3: Extra attributes + )? + [ ]* \n # Whitespace and newline following marker. + + # 4: Content + ( + (?> + (?!\1 [ ]* \n) # Not a closing marker. + .*\n+ + )+ + ) + + # Closing marker. + \1 [ ]* \n + }xm', + array(&$this, '_doFencedCodeBlocks_callback'), $text); + + return $text; + } + protected function _doFencedCodeBlocks_callback($matches) { + $classname =& $matches[2]; + $attrs =& $matches[3]; + $codeblock = $matches[4]; + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + $codeblock = preg_replace_callback('/^\n+/', + array(&$this, '_doFencedCodeBlocks_newlines'), $codeblock); + + if ($classname != "") { + if ($classname{0} == '.') + $classname = substr($classname, 1); + $attr_str = ' class="'.$this->code_class_prefix.$classname.'"'; + } else { + $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs); + } + $pre_attr_str = $this->code_attr_on_pre ? $attr_str : ''; + $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str; + $codeblock = "$codeblock
    "; + + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + protected function _doFencedCodeBlocks_newlines($matches) { + return str_repeat("empty_element_suffix", + strlen($matches[0])); + } + + + # + # Redefining emphasis markers so that emphasis by underscore does not + # work in the middle of a word. + # + protected $em_relist = array( + '' => '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? '(?:(? '(?<=\S|^)(? '(?<=\S|^)(? tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap

    tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + $value = trim($this->runSpanGamut($value)); + + # Check if this should be enclosed in a paragraph. + # Clean tag hashes & block tag hashes are left alone. + $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); + + if ($is_p) { + $value = "

    $value

    "; + } + $grafs[$key] = $value; + } + + # Join grafs in one text, then unhash HTML tags. + $text = implode("\n\n", $grafs); + + # Finish by removing any tag hashes still present in $text. + $text = $this->unhash($text); + + return $text; + } + + + ### Footnotes + + protected function stripFootnotes($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [^id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1 + [ ]* + \n? # maybe *one* newline + ( # text = $2 (no blank lines allowed) + (?: + .+ # actual text + | + \n # newlines but + (?!\[\^.+?\]:\s)# negative lookahead for footnote marker. + (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed + # by non-indented content + )* + ) + }xm', + array(&$this, '_stripFootnotes_callback'), + $text); + return $text; + } + protected function _stripFootnotes_callback($matches) { + $note_id = $this->fn_id_prefix . $matches[1]; + $this->footnotes[$note_id] = $this->outdent($matches[2]); + return ''; # String that will replace the block + } + + + protected function doFootnotes($text) { + # + # Replace footnote references in $text [^id] with a special text-token + # which will be replaced by the actual footnote marker in appendFootnotes. + # + if (!$this->in_anchor) { + $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); + } + return $text; + } + + + protected function appendFootnotes($text) { + # + # Append footnote list to text. + # + $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array(&$this, '_appendFootnotes_callback'), $text); + + if (!empty($this->footnotes_ordered)) { + $text .= "\n\n"; + $text .= "
    \n"; + $text .= "empty_element_suffix ."\n"; + $text .= "
      \n\n"; + + $attr = " rev=\"footnote\""; + if ($this->fn_backlink_class != "") { + $class = $this->fn_backlink_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_backlink_title != "") { + $title = $this->fn_backlink_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + $num = 0; + + while (!empty($this->footnotes_ordered)) { + $footnote = reset($this->footnotes_ordered); + $note_id = key($this->footnotes_ordered); + unset($this->footnotes_ordered[$note_id]); + $ref_count = $this->footnotes_ref_count[$note_id]; + unset($this->footnotes_ref_count[$note_id]); + unset($this->footnotes[$note_id]); + + $footnote .= "\n"; # Need to append newline before parsing. + $footnote = $this->runBlockGamut("$footnote\n"); + $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array(&$this, '_appendFootnotes_callback'), $footnote); + + $attr = str_replace("%%", ++$num, $attr); + $note_id = $this->encodeAttribute($note_id); + + # Prepare backlink, multiple backlinks if multiple references + $backlink = ""; + for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) { + $backlink .= " "; + } + # Add backlink to last paragraph; create new paragraph if needed. + if (preg_match('{

      $}', $footnote)) { + $footnote = substr($footnote, 0, -4) . " $backlink

      "; + } else { + $footnote .= "\n\n

      $backlink

      "; + } + + $text .= "
    1. \n"; + $text .= $footnote . "\n"; + $text .= "
    2. \n\n"; + } + + $text .= "
    \n"; + $text .= "
    "; + } + return $text; + } + protected function _appendFootnotes_callback($matches) { + $node_id = $this->fn_id_prefix . $matches[1]; + + # Create footnote marker only if it has a corresponding footnote *and* + # the footnote hasn't been used by another marker. + if (isset($this->footnotes[$node_id])) { + $num =& $this->footnotes_numbers[$node_id]; + if (!isset($num)) { + # Transfer footnote content to the ordered list and give it its + # number + $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; + $this->footnotes_ref_count[$node_id] = 1; + $num = $this->footnote_counter++; + $ref_count_mark = ''; + } else { + $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1; + } + + $attr = ""; + if ($this->fn_link_class != "") { + $class = $this->fn_link_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_link_title != "") { + $title = $this->fn_link_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + + $attr = str_replace("%%", $num, $attr); + $node_id = $this->encodeAttribute($node_id); + + return + "". + "$num". + ""; + } + + return "[^".$matches[1]."]"; + } + + + ### Abbreviations ### + + protected function stripAbbreviations($text) { + # + # Strips abbreviations from text, stores titles in hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [id]*: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 + (.*) # text = $2 (no blank lines allowed) + }xm', + array(&$this, '_stripAbbreviations_callback'), + $text); + return $text; + } + protected function _stripAbbreviations_callback($matches) { + $abbr_word = $matches[1]; + $abbr_desc = $matches[2]; + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + return ''; # String that will replace the block + } + + + protected function doAbbreviations($text) { + # + # Find defined abbreviations in text and wrap them in elements. + # + if ($this->abbr_word_re) { + // cannot use the /x modifier because abbr_word_re may + // contain significant spaces: + $text = preg_replace_callback('{'. + '(?abbr_word_re.')'. + '(?![\w\x1A])'. + '}', + array(&$this, '_doAbbreviations_callback'), $text); + } + return $text; + } + protected function _doAbbreviations_callback($matches) { + $abbr = $matches[0]; + if (isset($this->abbr_desciptions[$abbr])) { + $desc = $this->abbr_desciptions[$abbr]; + if (empty($desc)) { + return $this->hashPart("$abbr"); + } else { + $desc = $this->encodeAttribute($desc); + return $this->hashPart("$abbr"); + } + } else { + return $matches[0]; + } + } + +} + + +?> \ No newline at end of file diff --git a/lib/Michelf/MarkdownExtra.php b/lib/Michelf/MarkdownExtra.php new file mode 100644 index 0000000..267bf16 --- /dev/null +++ b/lib/Michelf/MarkdownExtra.php @@ -0,0 +1,40 @@ + +# +# Original Markdown +# Copyright (c) 2004-2006 John Gruber +# +# +namespace Michelf; + + +# Just force Michelf/Markdown.php to load. This is needed to load +# the temporary implementation class. See below for details. +\Michelf\Markdown::MARKDOWNLIB_VERSION; + +# +# Markdown Extra Parser Class +# +# Note: Currently the implementation resides in the temporary class +# \Michelf\MarkdownExtra_TmpImpl (in the same file as \Michelf\Markdown). +# This makes it easier to propagate the changes between the three different +# packaging styles of PHP Markdown. Once this issue is resolved, the +# _MarkdownExtra_TmpImpl will disappear and this one will contain the code. +# + +class MarkdownExtra extends \Michelf\_MarkdownExtra_TmpImpl { + + ### Parser Implementation ### + + # Temporarily, the implemenation is in the _MarkdownExtra_TmpImpl class. + # See note above. + +} + + +?> \ No newline at end of file diff --git a/lib/Michelf/extra/License.md b/lib/Michelf/extra/License.md new file mode 100644 index 0000000..027becb --- /dev/null +++ b/lib/Michelf/extra/License.md @@ -0,0 +1,36 @@ +PHP Markdown Lib +Copyright (c) 2004-2013 Michel Fortin + +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2006 John Gruber + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. diff --git a/lib/Michelf/extra/Readme.md b/lib/Michelf/extra/Readme.md new file mode 100644 index 0000000..8ee5fae --- /dev/null +++ b/lib/Michelf/extra/Readme.md @@ -0,0 +1,259 @@ +PHP Markdown +============ + +PHP Markdown Lib 1.3 - 11 Apr 2013 + +by Michel Fortin + + +based on Markdown by John Gruber + + + +Introduction +------------ + +This is a library package that includes the PHP Markdown parser and its +sibling PHP Markdown Extra which additional features. + +Markdown is a text-to-HTML conversion tool for web writers. Markdown +allows you to write using an easy-to-read, easy-to-write plain text +format, then convert it to structurally valid XHTML (or HTML). + +"Markdown" is two things: a plain text markup syntax, and a software +tool, written in Perl, that converts the plain text markup to HTML. +PHP Markdown is a port to PHP of the original Markdown program by +John Gruber. + +PHP Markdown can work as a plug-in for WordPress, as a modifier for +the Smarty templating engine, or as a replacement for Textile +formatting in any software that supports Textile. + +Full documentation of Markdown's syntax is available on John's +Markdown page: + + +Requirement +----------- + +This library package requires PHP 5.3 or later. + +Note: The older plugin/library hybrid package for PHP Markdown and +PHP Markdown Extra is still maintained and will work with PHP 4.0.5 and later. + +Before PHP 5.3.7, pcre.backtrack_limit defaults to 100 000, which is too small +in many situations. You might need to set it to higher values. Later PHP +releases defaults to 1 000 000, which is usually fine. + + +Usage +----- + +This library package is meant to be used with class autoloading. For autoloading +to work, your project needs have setup a PSR-0-compatible autoloader. See the +included Readme.php file for a minimal autoloader setup. (If you don't want to +use autoloading you can do a classic `require_once` to manually include the +files prior use instead.) + +With class autoloading in place, putting the 'Michelf' folder in your +include path should be enough for this to work: + + use \Michelf\Markdown; + $my_html = Markdown::defaultTransform($my_text); + +Markdown Extra syntax is also available the same way: + + use \Michelf\MarkdownExtra; + $my_html = MarkdownExtra::defaultTransform($my_text); + +If you wish to use PHP Markdown with another text filter function +built to parse HTML, you should filter the text *after* the `transform` +function call. This is an example with [PHP SmartyPants][psp]: + + use \Michelf\Markdown, \Michelf\SmartyPants; + $my_html = Markdown::defaultTransform($my_text); + $my_html = SmartyPants::defaultTransform($my_html); + +All these examples are using the static `defaultTransform` static function +found inside the parser class. If you want to customize the parser +configuration, you can also instantiate it directly and change some +configuration variables: + + use \Michelf\MarkdownExtra; + $parser = new MarkdownExtra; + $parser->fn_id_prefix = "post22-"; + $my_html = $parser->transform($my_text); + + +Usage +----- + +This library package is meant to be used with class autoloading. For autoloading +to work, your project needs have setup a PSR-0-compatible autoloader. See the +included Readme.php file for a minimal autoloader setup. (If you don't want to +use autoloading you can do a classic `require_once` to manually include the +files prior use instead.) + +With class autoloading in place, putting the 'Michelf' folder in your +include path should be enough for this to work: + + use \Michelf\Markdown; + $my_html = Markdown::defaultTransform($my_text); + +Markdown Extra syntax is also available the same way: + + use \Michelf\MarkdownExtra; + $my_html = MarkdownExtra::defaultTransform($my_text); + +If you wish to use PHP Markdown with another text filter function +built to parse HTML, you should filter the text *after* the `transform` +function call. This is an example with [PHP SmartyPants][psp]: + + use \Michelf\Markdown, \Michelf\SmartyPants; + $my_html = Markdown::defaultTransform($my_text); + $my_html = SmartyPants::defaultTransform($my_html); + +All these examples are using the static `defaultTransform` static function +found inside the parser class. If you want to customize the parser +configuration, you can also instantiate it directly and change some +configuration variables: + + use \Michelf\MarkdownExtra; + $parser = new MarkdownExtra; + $parser->fn_id_prefix = "post22-"; + $my_html = $parser->transform($my_text); + +To learn more, see the full list of [configuration variables]. + + [configuration variables]: http://michelf.ca/project/php-markdown/configuration/ + + +Public API and Versionning Policy +--------------------------------- + +Version numbers are of the form *major*.*minor*.*patch*. + +The public API of PHP Markdown consist of the two parser classes `Markdown` +and `MarkdownExtra`, their constructors, the `transform` and `defaultTransform` +functions and their configuration variables. The public API is stable for +a given major version number. It might get additions when the minor version +number increments. + +**Protected members are not considered public API.** This is unconventionnal +and deserves an explanation. Incrementing the major version number every time +the underlying implementation of something changes is going to give nonsential +version numbers for the vast majority of people who just use the parser. +Protected members are meant to create parser subclasses that behave in +different ways. Very few people create parser subclasses. I don't want to +discourage it by making everything private, but at the same time I can't +guarenty any stable hook between versions if you use protected members. + +**Syntax changes** will increment the minor number for new features, and the +patch number for small corrections. A *new feature* is something that needs a +change in the syntax documentation. Note that since PHP Markdown Lib includes +two parsers, a syntax change for either of them will increment the minor +number. Also note that there is nothigng perfectly backward-compatible with the +Markdown syntax: all inputs are always valid, so new features always replace +something that was previously legal, although generally non-sensial to do. + + +Bugs +---- + +To file bug reports please send email to: + + +Please include with your report: (1) the example input; (2) the output you +expected; (3) the output PHP Markdown actually produced. + +If you have a problem where Markdown gives you an empty result, first check +that the backtrack limit is not too low by running `php --info | grep pcre`. +See Installation and Requirement above for details. + + +Version History +--------------- + +PHP Markdown Lib 1.3 (11 Apr 2013): + +This is the first release of PHP Markdown Lib. This package requires PHP +version 4.3 or later and is designed to work with PSR-0 autoloading and, +optionally with Composer. Here is a list of the changes since +PHP Markdown Extra 1.2.6: + +* Plugin interface for Wordpress and other systems is no longer present in + the Lib package. The classic package is still available if you need it: + + +* Added `public` and `protected` protection attributes, plus a section about + what is "public API" and what isn't in the Readme file. + +* Changed HTML output for footnotes: now instead of adding `rel` and `rev` + attributes, footnotes links have the class name `footnote-ref` and + backlinks `footnote-backref`. + +* Fixed some regular expressions to make PCRE not shout warnings about POSIX + collation classes (dependent on your version of PCRE). + +* Added optional class and id attributes to images and links using the same + syntax as for headers: + + [link](url){#id .class} + ![img](url){#id .class} + + It work too for reference-style links and images. In this case you need + to put those attributes at the reference definition: + + [link][linkref] or [linkref] + ![img][linkref] + + [linkref]: url "optional title" {#id .class} + +* Fixed a PHP notice message triggered when some table column separator + markers are missing on the separator line below column headers. + +* Fixed a small mistake that could cause the parser to retain an invalid + state related to parsing links across multiple runs. This was never + observed (that I know of), but it's still worth fixing. + + +Copyright and License +--------------------- + +PHP Markdown Lib +Copyright (c) 2004-2013 Michel Fortin + +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2005 John Gruber + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. diff --git a/lib/Michelf/extra/Readme.php b/lib/Michelf/extra/Readme.php new file mode 100644 index 0000000..75e23e0 --- /dev/null +++ b/lib/Michelf/extra/Readme.php @@ -0,0 +1,31 @@ + + + + + PHP Markdown Lib - Readme + + + + + diff --git a/lib/Michelf/extra/composer.json b/lib/Michelf/extra/composer.json new file mode 100644 index 0000000..670fc96 --- /dev/null +++ b/lib/Michelf/extra/composer.json @@ -0,0 +1,31 @@ +{ + "name": "michelf/php-markdown", + "type": "library", + "description": "PHP Markdown", + "homepage": "http://michelf.ca/projects/php-markdown/", + "keywords": ["markdown"], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "http://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "http://daringfireball.net/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-0": { "Michelf": "" } + }, + "extra": { + "branch-alias": { + "dev-lib": "1.3.x-dev" + } + } +} diff --git a/lib/autoload.inc.php b/lib/autoload.inc.php new file mode 100644 index 0000000..0057f5e --- /dev/null +++ b/lib/autoload.inc.php @@ -0,0 +1,2 @@ + $docs){ + $vars2 = array( 'active' => ($first ? 'active' : ''), + 'tab_id' => str_replace(' ', '_', $cat), + 'tab_id_pretty' => $cat); + $first = false; + $vars['tabopts'] .= \SYSTEM\PAGE\replace::replaceFile(\SYSTEM\SERVERPATH(new \SYSTEM\PSAI(),'modules/saimod_sys_docu/tabopt.tpl'), $vars2); + + $first2 = true; + foreach($docs as $doc){ + $tabs[$cat]['tab_id'] = str_replace(' ', '_', $cat); + $tabs[$cat]['content'] = isset($tabs[$cat]['content']) ? $tabs[$cat]['content'] : ''; + $tabs[$cat]['menu'] = isset($tabs[$cat]['menu']) ? $tabs[$cat]['menu'] : ''; + //$tabs[$cat]['content'] .= \Michelf\Markdown::defaultTransform(file_get_contents($doc)); + $vars3 = array( 'active' => ($first2 ? 'active' : ''), + 'content' => \Michelf\Markdown::defaultTransform(file_get_contents($doc)), + 'tab_id' => str_replace(array('.',' '), '_', basename($doc))); + $tabs[$cat]['content'] .= \SYSTEM\PAGE\replace::replaceFile(\SYSTEM\SERVERPATH(new \SYSTEM\PSAI(),'modules/saimod_sys_docu/tab2.tpl'), $vars3); + $vars3 = array( 'active' => ($first2 ? 'active' : ''), + 'tab_id' => str_replace(array('.',' '), '_', basename($doc)), + 'tab_id_pretty' => basename($doc)); + $tabs[$cat]['menu'] .= \SYSTEM\PAGE\replace::replaceFile(\SYSTEM\SERVERPATH(new \SYSTEM\PSAI(),'modules/saimod_sys_docu/tabopt.tpl'), $vars3); + $first2 = false; + } + + $vars['tabs'] = ''; + $first = true; + foreach($tabs as $tab){ + $tab['active'] = ($first ? 'active' : ''); + $first = false; + $vars['tabs'] .= \SYSTEM\PAGE\replace::replaceFile(\SYSTEM\SERVERPATH(new \SYSTEM\PSAI(),'modules/saimod_sys_docu/tab.tpl'), $tab);} + } + return \SYSTEM\PAGE\replace::replaceFile(\SYSTEM\SERVERPATH(new \SYSTEM\PSAI(),'modules/saimod_sys_docu/tabs.tpl'), $vars); } public static function html_li_menu(){return '
  • Docu
  • ';} @@ -11,5 +46,8 @@ class saimod_sys_docu extends \SYSTEM\SAI\SaiModule { public static function right_right(){return \SYSTEM\SECURITY\Security::check(\SYSTEM\SECURITY\RIGHTS::SYS_SAI);} public static function sai_mod__SYSTEM_SAI_saimod_sys_docu_flag_css(){} - public static function sai_mod__SYSTEM_SAI_saimod_sys_docu_flag_js(){} + public static function sai_mod__SYSTEM_SAI_saimod_sys_docu_flag_js(){ + return \SYSTEM\LOG\JsonResult::toString( + array( \SYSTEM\WEBPATH(new \SYSTEM\PSYSTEM(),'lib/EpicEditor/js/epiceditor.min.js'), + \SYSTEM\WEBPATH(new \SYSTEM\PSAI(),'modules/saimod_sys_docu/saimod_sys_docu.js')));} } \ No newline at end of file diff --git a/sai/modules/saimod_sys_docu/tab.tpl b/sai/modules/saimod_sys_docu/tab.tpl new file mode 100644 index 0000000..804cb3d --- /dev/null +++ b/sai/modules/saimod_sys_docu/tab.tpl @@ -0,0 +1,12 @@ +
    +
    + +
    + ${content} +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/sai/modules/saimod_sys_docu/tab2.tpl b/sai/modules/saimod_sys_docu/tab2.tpl new file mode 100644 index 0000000..53a7e4c --- /dev/null +++ b/sai/modules/saimod_sys_docu/tab2.tpl @@ -0,0 +1,8 @@ +
    +
    +
    + ${content} + +
    +
    +
    \ No newline at end of file diff --git a/sai/modules/saimod_sys_docu/tabopt.tpl b/sai/modules/saimod_sys_docu/tabopt.tpl new file mode 100644 index 0000000..de4373f --- /dev/null +++ b/sai/modules/saimod_sys_docu/tabopt.tpl @@ -0,0 +1 @@ +
  • ${tab_id_pretty}
  • \ No newline at end of file diff --git a/sai/modules/saimod_sys_docu/tabs.tpl b/sai/modules/saimod_sys_docu/tabs.tpl new file mode 100644 index 0000000..dc06e28 --- /dev/null +++ b/sai/modules/saimod_sys_docu/tabs.tpl @@ -0,0 +1,10 @@ +

    Documentation

    +
    +
    + +
    + ${tabs} +
    +
    \ No newline at end of file