diff --git a/Vagrantfile b/Vagrantfile index 0afe04a..6a97d6a 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -13,7 +13,6 @@ Vagrant.configure(2) do |config| config.vm.synced_folder "scripts", "/home/vagrant/scripts" config.vm.synced_folder "web", "/srv/http/ctf" - config.vm.provision :shell, path: "/vagrant/deploy", run: "always", privileged: false config.vm.provision :shell, :path => "scripts/setup.sh" config.ssh.forward_agent = true diff --git a/ctf.nginx b/ctf.nginx index ad45bfa..c6dfbd8 100644 --- a/ctf.nginx +++ b/ctf.nginx @@ -27,6 +27,10 @@ server { try_files /index.html /index.html; } + location ~ /static { + root /srv/http/static; + } + location ~ /api { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/problems/misc/survey/README.md b/problems/misc/survey/README.md new file mode 100644 index 0000000..85f40fc --- /dev/null +++ b/problems/misc/survey/README.md @@ -0,0 +1,61 @@ +# Problem Format + +This survey problem serves as an example for future problems. Each problem must be placed in its own directory, under the directory of its category (for organization). For the name of the folder, please use the ID of the problem (`pid`), which should be the name of the problem represented by lowercase words separated by dashes; don't include problem value. The program will automatically load the problems into the database. + +The directory *must* contain a `problem.json`; this information will be loaded into the database. Refer to the following example: + +```javascript +{ + "pid": "survey", // required + "title": "Survey", // required + "description": "Take our survey.", // required - can use HTML + "hint": "No hint!", // optional - defaults to "" + "category": "Miscellaneous", // required + "autogen": false, // optional - defaults to false + "programming": false, // optional - defaults to false + "value": 20, // required - integer out of 800 + "bonus": 0, // optional - defaults to 0; see below for details + "threshold": 0, // recommended - defaults to 0; see below for details + "weightmap": { } // recommended - defaults to {} +} +``` + +## Bonus Points + +Bonus points encourage teams to finish solving a problem first. Rather than an array of three values like last year, we're going to have bonus-point templates. Each of the following integers is a code for a certain "template", and bonus points will be assigned accordingly. + +| Code | 3rd | 2nd | 1st | +|------|-----|-----|-----| +| 0 | 0% | 0% | 0% | +| 1 | 1% | 2% | 3% | +| 2 | 1% | 3% | 5% | +| 3 | 3% | 5% | 8% | +| 4 | 6% | 8% | 10% | +| 5 | 8% | 12% | 20% | + +The table indicates how many percent bonus a team should receive if they solve a problem first, second, or third. Low problems such as the survey should not yield bonus points; only high-valued points should have bonus points in order to encourage teams t o solve them first. + +## Problem Unlocking + +Problem unlocking is managed through a mechanism that involves a threshold and weightmap. The weightmap is a dictionary mapping problem IDs to weight. The threshold is the minimum amount needed in order to unlock that particular problem. Take, for example, an abridged version of last year's Launch Code problem's JSON: + +```javascript +{ + "pid": "launch-code", + "threshold": 5, + "weightmap": { + "php3": 1, + "faster-math": 1, + "biggerisbetter": 1, + "cave-johnson": 1, + "blackmesa": 1, + "rsa3": 1, + "yandere": 1, + "rsi": 1, + "adoughbee": 1, + "infinity_star": 1 + } +} +``` + +That means in order to unlock Launch Code, a team would have to have solved five of the problems listed, since each problem has weight 1. It's also possible to add higher weights to a particular problem to promote solving that problem. \ No newline at end of file diff --git a/server/api/admin.py b/server/api/admin.py index cd7983e..0114e95 100644 --- a/server/api/admin.py +++ b/server/api/admin.py @@ -1,6 +1,7 @@ from flask import Blueprint, jsonify from decorators import admins_only, api_wrapper from models import db, Problems, Files +from schemas import verify_to_schema, check import json @@ -23,4 +24,21 @@ def problem_data(): "threshold": problem.threshold, "weightmap": json.loads(problem.weightmap) }) - return { "success": 1, "problems": problems_return } \ No newline at end of file + return { "success": 1, "problems": problems_return } + +""" +@blueprint.route("/problems/submit", methods=["POST"]) +@api_wrapper +@admins_only +def problem_submit(): + params = utils.flat_multi(request.form) + verify_to_schema(UserSchema, params) + + title = params.get("title") + +ProblemSubmissionSchema = Schema({ + Required("title"): check( + ([str, Length(min=4, max=64)], "The title should be between 4 and 64 characters long."), + ), +}, extra=True) +""" \ No newline at end of file diff --git a/web/index.html b/web/index.html index d66c7b5..04adfc5 100644 --- a/web/index.html +++ b/web/index.html @@ -20,8 +20,8 @@ - + diff --git a/web/js/epiceditor.js b/web/js/epiceditor.js new file mode 100644 index 0000000..befaf54 --- /dev/null +++ b/web/js/epiceditor.js @@ -0,0 +1,2899 @@ +/** + * EpicEditor - An Embeddable JavaScript Markdown Editor (https://github.com/OscarGodson/EpicEditor) + * Copyright (c) 2011-2012, Oscar Godson. (MIT Licensed) + */ + +(function (window, undefined) { + /** + * Applies attributes to a DOM object + * @param {object} context The DOM obj you want to apply the attributes to + * @param {object} attrs A key/value pair of attributes you want to apply + * @returns {undefined} + */ + function _applyAttrs(context, attrs) { + for (var attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + context[attr] = attrs[attr]; + } + } + } + + /** + * Applies styles to a DOM object + * @param {object} context The DOM obj you want to apply the attributes to + * @param {object} attrs A key/value pair of attributes you want to apply + * @returns {undefined} + */ + function _applyStyles(context, attrs) { + for (var attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + context.style[attr] = attrs[attr]; + } + } + } + + /** + * Returns a DOM objects computed style + * @param {object} el The element you want to get the style from + * @param {string} styleProp The property you want to get from the element + * @returns {string} Returns a string of the value. If property is not set it will return a blank string + */ + function _getStyle(el, styleProp) { + var x = el + , y = null; + if (window.getComputedStyle) { + y = document.defaultView.getComputedStyle(x, null).getPropertyValue(styleProp); + } + else if (x.currentStyle) { + y = x.currentStyle[styleProp]; + } + return y; + } + + /** + * Saves the current style state for the styles requested, then applies styles + * to overwrite the existing one. The old styles are returned as an object so + * you can pass it back in when you want to revert back to the old style + * @param {object} el The element to get the styles of + * @param {string} type Can be "save" or "apply". apply will just apply styles you give it. Save will write styles + * @param {object} styles Key/value style/property pairs + * @returns {object} + */ + function _saveStyleState(el, type, styles) { + var returnState = {} + , style; + if (type === 'save') { + for (style in styles) { + if (styles.hasOwnProperty(style)) { + returnState[style] = _getStyle(el, style); + } + } + // After it's all done saving all the previous states, change the styles + _applyStyles(el, styles); + } + else if (type === 'apply') { + _applyStyles(el, styles); + } + return returnState; + } + + /** + * Gets an elements total width including it's borders and padding + * @param {object} el The element to get the total width of + * @returns {int} + */ + function _outerWidth(el) { + var b = parseInt(_getStyle(el, 'border-left-width'), 10) + parseInt(_getStyle(el, 'border-right-width'), 10) + , p = parseInt(_getStyle(el, 'padding-left'), 10) + parseInt(_getStyle(el, 'padding-right'), 10) + , w = el.offsetWidth + , t; + // For IE in case no border is set and it defaults to "medium" + if (isNaN(b)) { b = 0; } + t = b + p + w; + return t; + } + + /** + * Gets an elements total height including it's borders and padding + * @param {object} el The element to get the total width of + * @returns {int} + */ + function _outerHeight(el) { + var b = parseInt(_getStyle(el, 'border-top-width'), 10) + parseInt(_getStyle(el, 'border-bottom-width'), 10) + , p = parseInt(_getStyle(el, 'padding-top'), 10) + parseInt(_getStyle(el, 'padding-bottom'), 10) + , w = parseInt(_getStyle(el, 'height'), 10) + , t; + // For IE in case no border is set and it defaults to "medium" + if (isNaN(b)) { b = 0; } + t = b + p + w; + return t; + } + + /** + * Inserts a 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/web/js/epiceditor.min.js b/web/js/epiceditor.min.js new file mode 100644 index 0000000..e664025 --- /dev/null +++ b/web/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/web/pages/admin/problems.html b/web/pages/admin/problems.html index ca0798e..a444e9e 100644 --- a/web/pages/admin/problems.html +++ b/web/pages/admin/problems.html @@ -7,6 +7,9 @@ width: 100%; height: 200px; } +.epiceditor-edit-mode { + border: 1px solid #999 !important; +}