From 15c6f49854c83f5a616f9771e8c7b8c0dab1be0b Mon Sep 17 00:00:00 2001 From: Thomas Tanghus Date: Wed, 5 Mar 2014 04:59:30 +0100 Subject: [PATCH] Fixes and MarkDown! - Fix #22 - Fix #16 --- Changelog | 7 +- appinfo/info.xml | 2 +- css/rte.css | 6 +- index.php | 21 +- js/Markdown.Converter.js | 1412 ++++++++++++++++++++++++++++++++++ js/Markdown.LICENSE.txt | 32 + js/Markdown.Sanitizer.js | 108 +++ js/html2markdown.LICENSE.txt | 22 + js/html2markdown.js | 568 ++++++++++++++ js/journal.js | 25 +- js/jquery.rte.js | 9 +- js/markdown_dom_parser.js | 431 +++++++++++ lib/app.php | 68 +- lib/hooks.php | 2 +- 14 files changed, 2660 insertions(+), 53 deletions(-) create mode 100644 js/Markdown.Converter.js create mode 100644 js/Markdown.LICENSE.txt create mode 100644 js/Markdown.Sanitizer.js create mode 100644 js/html2markdown.LICENSE.txt create mode 100644 js/html2markdown.js create mode 100644 js/markdown_dom_parser.js diff --git a/Changelog b/Changelog index db6f841..3339224 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,11 @@ # Changelog -### 0.3.3 (current master) +### 0.4 (current master) + - Switching from text to html format removed all text (sorry about that...) + - Problem adding more than one entry possibly caused by updated jquery library. + - When switching from text to html the text is now parsed as MarkDown and vice-versa. + +### 0.3.3 - Update to work both on ownCloud 5 and 6. - Fix wrong link to app installer when there were missing dependencies. diff --git a/appinfo/info.xml b/appinfo/info.xml index bc001b7..96e6a62 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -6,6 +6,6 @@ Thomas Tanghus 4.93 false - 0.3.3.1 + 0.4 Journal view from calendar diff --git a/css/rte.css b/css/rte.css index de07ab5..a09c6d6 100644 --- a/css/rte.css +++ b/css/rte.css @@ -4,4 +4,8 @@ .rte-content { cursor:text;position:absolute; border-color:#000;border-style:solid; white-space: pre-wrap; word-wrap: break-word; padding: 0.2em; } .rte-content ul { list-style: circle inside; } .rte-content li { padding-left: 1em; } - +.rte-content h1 { font-size: 200%; } +.rte-content h2 { font-size: 170%; } +.rte-content h3 { font-size: 140%; } +.rte-content h4 { font-size: 120%; } +.rte-content h5 { font-size: 100%; } diff --git a/index.php b/index.php index 55779b0..8a04648 100644 --- a/index.php +++ b/index.php @@ -19,9 +19,17 @@ * License along with this library. If not, see . * */ + +OCP\User::checkLoggedIn(); + $errors = array(); +$categories = array(); +$calendars = array(); + $l = new OC_L10N('journal'); -OCP\User::checkLoggedIn(); + +// Version check +list($version,) = \OCP\Util::getVersion(); $required_apps = array( array('id' => 'tal', 'name' => 'TAL Page Templates'), @@ -36,7 +44,7 @@ } //http://localhost/owncloud5/settings/apps?appid=tal //http://localhost/owncloud5/index.php/settings/apps?installed -if(count($errors) == 0) { +if(count($errors) === 0) { $categories = OCA\Journal\App::getCategories(); $calendars = array(); $singlecalendar = (bool)OCP\Config::getUserValue(OCP\User::getUser(), 'journal', 'single_calendar', false); @@ -49,9 +57,6 @@ // Load a specific entry? $id = isset($_GET['id']) ? $_GET['id'] : null; - // Version check - list($version,) = \OCP\Util::getVersion(); - OCP\Util::addScript('3rdparty/timepicker', 'jquery.ui.timepicker'); OCP\Util::addScript('contacts','jquery.multi-autocomplete'); if($version < 6) { @@ -59,8 +64,12 @@ } else { OCP\Util::addScript('','tags'); } + OCP\Util::addScript('journal', 'Markdown.Converter'); + OCP\Util::addScript('journal', 'Markdown.Sanitizer'); + OCP\Util::addScript('journal', 'markdown_dom_parser'); + OCP\Util::addScript('journal', 'html2markdown'); OCP\Util::addScript('journal', 'jquery.rte'); - OCP\Util::addScript('journal', 'jquery.textchange'); + //OCP\Util::addScript('journal', 'jquery.textchange'); OCP\Util::addScript('journal', 'journal'); OCP\Util::addscript('tal','modernizr'); OCP\Util::addStyle('3rdparty/timepicker', 'jquery.ui.timepicker'); diff --git a/js/Markdown.Converter.js b/js/Markdown.Converter.js new file mode 100644 index 0000000..f5a0cd2 --- /dev/null +++ b/js/Markdown.Converter.js @@ -0,0 +1,1412 @@ +var Markdown; + +if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module + Markdown = exports; +else + Markdown = {}; + +// The following text is included for historical reasons, but should +// be taken with a pinch of salt; it's not all true anymore. + +// +// Wherever possible, Showdown is a straight, line-by-line port +// of the Perl version of Markdown. +// +// This is not a normal parser design; it's basically just a +// series of string substitutions. It's hard to read and +// maintain this way, but keeping Showdown close to the original +// design makes it easier to port new features. +// +// More importantly, Showdown behaves like markdown.pl in most +// edge cases. So web applications can do client-side preview +// in Javascript, and then build identical HTML on the server. +// +// This port needs the new RegExp functionality of ECMA 262, +// 3rd Edition (i.e. Javascript 1.5). Most modern web browsers +// should do fine. Even with the new regular expression features, +// We do a lot of work to emulate Perl's regex functionality. +// The tricky changes in this file mostly have the "attacklab:" +// label. Major or self-explanatory changes don't. +// +// Smart diff tools like Araxis Merge will be able to match up +// this file with markdown.pl in a useful way. A little tweaking +// helps: in a copy of markdown.pl, replace "#" with "//" and +// replace "$text" with "text". Be sure to ignore whitespace +// and line endings. +// + + +// +// Usage: +// +// var text = "Markdown *rocks*."; +// +// var converter = new Markdown.Converter(); +// var html = converter.makeHtml(text); +// +// alert(html); +// +// Note: move the sample code to the bottom of this +// file before uncommenting it. +// + +(function () { + + function identity(x) { return x; } + function returnFalse(x) { return false; } + + function HookCollection() { } + + HookCollection.prototype = { + + chain: function (hookname, func) { + var original = this[hookname]; + if (!original) + throw new Error("unknown hook " + hookname); + + if (original === identity) + this[hookname] = func; + else + this[hookname] = function (text) { + var args = Array.prototype.slice.call(arguments, 0); + args[0] = original.apply(null, args); + return func.apply(null, args); + }; + }, + set: function (hookname, func) { + if (!this[hookname]) + throw new Error("unknown hook " + hookname); + this[hookname] = func; + }, + addNoop: function (hookname) { + this[hookname] = identity; + }, + addFalse: function (hookname) { + this[hookname] = returnFalse; + } + }; + + Markdown.HookCollection = HookCollection; + + // g_urls and g_titles allow arbitrary user-entered strings as keys. This + // caused an exception (and hence stopped the rendering) when the user entered + // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this + // (since no builtin property starts with "s_"). See + // http://meta.stackoverflow.com/questions/64655/strange-wmd-bug + // (granted, switching from Array() to Object() alone would have left only __proto__ + // to be a problem) + function SaveHash() { } + SaveHash.prototype = { + set: function (key, value) { + this["s_" + key] = value; + }, + get: function (key) { + return this["s_" + key]; + } + }; + + Markdown.Converter = function () { + var pluginHooks = this.hooks = new HookCollection(); + + // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link + pluginHooks.addNoop("plainLinkText"); + + // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked + pluginHooks.addNoop("preConversion"); + + // called with the text once all normalizations have been completed (tabs to spaces, line endings, etc.), but before any conversions have + pluginHooks.addNoop("postNormalization"); + + // Called with the text before / after creating block elements like code blocks and lists. Note that this is called recursively + // with inner content, e.g. it's called with the full text, and then only with the content of a blockquote. The inner + // call will receive outdented text. + pluginHooks.addNoop("preBlockGamut"); + pluginHooks.addNoop("postBlockGamut"); + + // called with the text of a single block element before / after the span-level conversions (bold, code spans, etc.) have been made + pluginHooks.addNoop("preSpanGamut"); + pluginHooks.addNoop("postSpanGamut"); + + // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml + pluginHooks.addNoop("postConversion"); + + // + // Private state of the converter instance: + // + + // Global hashes, used by various utility routines + var g_urls; + var g_titles; + var g_html_blocks; + + // Used to track when we're inside an ordered or unordered list + // (see _ProcessListItems() for details): + var g_list_level; + + this.makeHtml = function (text) { + + // + // Main function. The order in which other subs are called here is + // essential. Link and image substitutions need to happen before + // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the + // and tags get encoded. + // + + // This will only happen if makeHtml on the same converter instance is called from a plugin hook. + // Don't do that. + if (g_urls) + throw new Error("Recursive call to converter.makeHtml"); + + // Create the private state objects. + g_urls = new SaveHash(); + g_titles = new SaveHash(); + g_html_blocks = []; + g_list_level = 0; + + text = pluginHooks.preConversion(text); + + // attacklab: Replace ~ with ~T + // This lets us use tilde as an escape char to avoid md5 hashes + // The choice of character is arbitray; anything that isn't + // magic in Markdown will work. + text = text.replace(/~/g, "~T"); + + // attacklab: Replace $ with ~D + // RegExp interprets $ as a special character + // when it's in a replacement string + text = text.replace(/\$/g, "~D"); + + // Standardize line endings + text = text.replace(/\r\n/g, "\n"); // DOS to Unix + text = text.replace(/\r/g, "\n"); // Mac to Unix + + // Make sure text begins and ends with a couple of newlines: + text = "\n\n" + text + "\n\n"; + + // Convert all tabs to spaces. + text = _Detab(text); + + // Strip any lines consisting only of spaces and tabs. + // This makes subsequent regexen easier to write, because we can + // match consecutive blank lines with /\n+/ instead of something + // contorted like /[ \t]*\n+/ . + text = text.replace(/^[ \t]+$/mg, ""); + + text = pluginHooks.postNormalization(text); + + // Turn block-level HTML blocks into hash entries + text = _HashHTMLBlocks(text); + + // Strip link definitions, store in hashes. + text = _StripLinkDefinitions(text); + + text = _RunBlockGamut(text); + + text = _UnescapeSpecialChars(text); + + // attacklab: Restore dollar signs + text = text.replace(/~D/g, "$$"); + + // attacklab: Restore tildes + text = text.replace(/~T/g, "~"); + + text = pluginHooks.postConversion(text); + + g_html_blocks = g_titles = g_urls = null; + + return text; + }; + + function _StripLinkDefinitions(text) { + // + // Strips link definitions from text, stores the URLs and titles in + // hash references. + // + + // Link defs are in the form: ^[id]: url "optional title" + + /* + text = text.replace(/ + ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 + [ \t]* + \n? // maybe *one* newline + [ \t]* + ? // url = $2 + (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below + [ \t]* + \n? // maybe one newline + [ \t]* + ( // (potential) title = $3 + (\n*) // any lines skipped = $4 attacklab: lookbehind removed + [ \t]+ + ["(] + (.+?) // title = $5 + [")] + [ \t]* + )? // title is optional + (?:\n+|$) + /gm, function(){...}); + */ + + text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm, + function (wholeMatch, m1, m2, m3, m4, m5) { + m1 = m1.toLowerCase(); + g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive + if (m4) { + // Oops, found blank lines, so it's not a title. + // Put back the parenthetical statement we stole. + return m3; + } else if (m5) { + g_titles.set(m1, m5.replace(/"/g, """)); + } + + // Completely remove the definition from the text + return ""; + } + ); + + return text; + } + + function _HashHTMLBlocks(text) { + + // Hashify HTML blocks: + // We only want to do this for block-level HTML tags, such as headers, + // lists, and tables. That's because we still want to wrap

s around + // "paragraphs" that are wrapped in non-block-level tags, such as anchors, + // phrase emphasis, and spans. The list of tags we're looking for is + // hard-coded: + var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" + var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" + + // First, look for nested blocks, e.g.: + //

+ //
+ // tags for inner block must be indented. + //
+ //
+ // + // The outermost tags must start at the left margin for this to match, and + // the inner nested divs must be indented. + // We need to do this before the next, more liberal match, because the next + // match will start at the first `
` and stop at the first `
`. + + // attacklab: This regex can be expensive when it fails. + + /* + text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_a) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*?\n // any number of lines, minimally matching + // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashElement); + + // + // Now match more liberally, simply from `\n` to `\n` + // + + /* + text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_b) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*? // any number of lines, minimally matching + .* // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashElement); + + // Special case just for
. It was easier to make a special case than + // to make the other regex more complicated. + + /* + text = text.replace(/ + \n // Starting after a blank line + [ ]{0,3} + ( // save in $1 + (<(hr) // start tag = $2 + \b // word break + ([^<>])*? + \/?>) // the matching end tag + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashElement); + + // Special case for standalone HTML comments: + + /* + text = text.replace(/ + \n\n // Starting after a blank line + [ ]{0,3} // attacklab: g_tab_width - 1 + ( // save in $1 + -]|-[^>])(?:[^-]|-[^-])*)--) // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackoverflow.com/q/95256 + > + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/\n\n[ ]{0,3}(-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashElement); + + // PHP and ASP-style processor instructions ( and <%...%>) + + /* + text = text.replace(/ + (?: + \n\n // Starting after a blank line + ) + ( // save in $1 + [ ]{0,3} // attacklab: g_tab_width - 1 + (?: + <([?%]) // $2 + [^\r]*? + \2> + ) + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashElement); + + return text; + } + + function hashElement(wholeMatch, m1) { + var blockText = m1; + + // Undo double lines + blockText = blockText.replace(/^\n+/, ""); + + // strip trailing blank lines + blockText = blockText.replace(/\n+$/g, ""); + + // Replace the element text with a marker ("~KxK" where x is its key) + blockText = "\n\n~K" + (g_html_blocks.push(blockText) - 1) + "K\n\n"; + + return blockText; + } + + var blockGamutHookCallback = function (t) { return _RunBlockGamut(t); } + + function _RunBlockGamut(text, doNotUnhash) { + // + // These are all the transformations that form block-level + // tags like paragraphs, headers, and list items. + // + + text = pluginHooks.preBlockGamut(text, blockGamutHookCallback); + + text = _DoHeaders(text); + + // Do Horizontal Rules: + var replacement = "
\n"; + text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement); + text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement); + text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement); + + text = _DoLists(text); + text = _DoCodeBlocks(text); + text = _DoBlockQuotes(text); + + text = pluginHooks.postBlockGamut(text, blockGamutHookCallback); + + // We already ran _HashHTMLBlocks() before, in Markdown(), but that + // was to escape raw HTML in the original Markdown source. This time, + // we're escaping the markup we've just created, so that we don't wrap + //

tags around block-level tags. + text = _HashHTMLBlocks(text); + text = _FormParagraphs(text, doNotUnhash); + + return text; + } + + function _RunSpanGamut(text) { + // + // These are all the transformations that occur *within* block-level + // tags like paragraphs, headers, and list items. + // + + text = pluginHooks.preSpanGamut(text); + + text = _DoCodeSpans(text); + text = _EscapeSpecialCharsWithinTagAttributes(text); + text = _EncodeBackslashEscapes(text); + + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + text = _DoImages(text); + text = _DoAnchors(text); + + // Make links out of things like `` + // Must come after _DoAnchors(), because you can use < and > + // delimiters in inline links like [this](). + text = _DoAutoLinks(text); + + text = text.replace(/~P/g, "://"); // put in place to prevent autolinking; reset now + + text = _EncodeAmpsAndAngles(text); + text = _DoItalicsAndBold(text); + + // Do hard breaks: + text = text.replace(/ +\n/g, "
\n"); + + text = pluginHooks.postSpanGamut(text); + + return text; + } + + function _EscapeSpecialCharsWithinTagAttributes(text) { + // + // Within tags -- meaning between < and > -- encode [\ ` * _] so they + // don't conflict with their use in Markdown for code, italics and strong. + // + + // Build a regex to find HTML tags and comments. See Friedl's + // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. + + // SE: changed the comment part of the regex + + var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi; + + text = text.replace(regex, function (wholeMatch) { + var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`"); + tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackoverflow.com/questions/95987 + return tag; + }); + + return text; + } + + function _DoAnchors(text) { + // + // Turn Markdown link shortcuts into XHTML
tags. + // + // + // First, handle reference-style links: [link text] [id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[] // or anything else + )* + ) + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + ) + ()()()() // pad remaining backreferences + /g, writeAnchorTag); + */ + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag); + + // + // Next, inline-style links: [link text](url "optional title") + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[\]] // or anything else + )* + ) + \] + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // Title = $7 + \6 // matching quote + [ \t]* // ignore any spaces/tabs between closing quote and ) + )? // title is optional + \) + ) + /g, writeAnchorTag); + */ + + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag); + + // + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link test][1] + // or [link test](/foo) + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ([^\[\]]+) // link text = $2; can't contain '[' or ']' + \] + ) + ()()()()() // pad rest of backreferences + /g, writeAnchorTag); + */ + text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); + + return text; + } + + function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) { + if (m7 == undefined) m7 = ""; + var whole_match = m1; + var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs + var link_id = m3.toLowerCase(); + var url = m4; + var title = m7; + + if (url == "") { + if (link_id == "") { + // lower-case and turn embedded newlines into spaces + link_id = link_text.toLowerCase().replace(/ ?\n/g, " "); + } + url = "#" + link_id; + + if (g_urls.get(link_id) != undefined) { + url = g_urls.get(link_id); + if (g_titles.get(link_id) != undefined) { + title = g_titles.get(link_id); + } + } + else { + if (whole_match.search(/\(\s*\)$/m) > -1) { + // Special case for explicit empty url + url = ""; + } else { + return whole_match; + } + } + } + url = encodeProblemUrlChars(url); + url = escapeCharacters(url, "*_"); + var result = ""; + + return result; + } + + function _DoImages(text) { + // + // Turn Markdown image shortcuts into tags. + // + + // + // First, handle reference-style labeled images: ![alt text][id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + ) + ()()()() // pad rest of backreferences + /g, writeImageTag); + */ + text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag); + + // + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + \s? // One optional whitespace character + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? // src url = $4 + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // title = $7 + \6 // matching quote + [ \t]* + )? // title is optional + \) + ) + /g, writeImageTag); + */ + text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag); + + return text; + } + + function attributeEncode(text) { + // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title) + // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it) + return text.replace(/>/g, ">").replace(/" + _RunSpanGamut(m1) + "\n\n"; } + ); + + text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, + function (matchFound, m1) { return "

" + _RunSpanGamut(m1) + "

\n\n"; } + ); + + // atx-style headers: + // # Header 1 + // ## Header 2 + // ## Header 2 with closing hashes ## + // ... + // ###### Header 6 + // + + /* + text = text.replace(/ + ^(\#{1,6}) // $1 = string of #'s + [ \t]* + (.+?) // $2 = Header text + [ \t]* + \#* // optional closing #'s (not counted) + \n+ + /gm, function() {...}); + */ + + text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, + function (wholeMatch, m1, m2) { + var h_level = m1.length; + return "" + _RunSpanGamut(m2) + "\n\n"; + } + ); + + return text; + } + + function _DoLists(text, isInsideParagraphlessListItem) { + // + // Form HTML ordered (numbered) and unordered (bulleted) lists. + // + + // attacklab: add sentinel to hack around khtml/safari bug: + // http://bugs.webkit.org/show_bug.cgi?id=11231 + text += "~0"; + + // Re-usable pattern to match any entirel ul or ol list: + + /* + var whole_list = / + ( // $1 = whole list + ( // $2 + [ ]{0,3} // attacklab: g_tab_width - 1 + ([*+-]|\d+[.]) // $3 = first list item marker + [ \t]+ + ) + [^\r]+? + ( // $4 + ~0 // sentinel for workaround; should be $ + | + \n{2,} + (?=\S) + (?! // Negative lookahead for another list item marker + [ \t]* + (?:[*+-]|\d+[.])[ \t]+ + ) + ) + ) + /g + */ + var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; + + if (g_list_level) { + text = text.replace(whole_list, function (wholeMatch, m1, m2) { + var list = m1; + var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol"; + + var result = _ProcessListItems(list, list_type, isInsideParagraphlessListItem); + + // Trim any trailing whitespace, to put the closing `` + // up on the preceding line, to get it past the current stupid + // HTML block parser. This is a hack to work around the terrible + // hack that is the HTML block parser. + result = result.replace(/\s+$/, ""); + result = "<" + list_type + ">" + result + "\n"; + return result; + }); + } else { + whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; + text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) { + var runup = m1; + var list = m2; + + var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol"; + var result = _ProcessListItems(list, list_type); + result = runup + "<" + list_type + ">\n" + result + "\n"; + return result; + }); + } + + // attacklab: strip sentinel + text = text.replace(/~0/, ""); + + return text; + } + + var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" }; + + function _ProcessListItems(list_str, list_type, isInsideParagraphlessListItem) { + // + // Process the contents of a single ordered or unordered list, splitting it + // into individual list items. + // + // list_type is either "ul" or "ol". + + // The $g_list_level global keeps track of when we're inside a list. + // Each time we enter a list, we increment it; when we leave a list, + // we decrement. If it's zero, we're not in a list anymore. + // + // We do this because when we're not inside a list, we want to treat + // something like this: + // + // I recommend upgrading to version + // 8. Oops, now this line is treated + // as a sub-list. + // + // As a single paragraph, despite the fact that the second line starts + // with a digit-period-space sequence. + // + // Whereas when we're inside a list (or sub-list), that line will be + // treated as the start of a sub-list. What a kludge, huh? This is + // an aspect of Markdown's syntax that's hard to parse perfectly + // without resorting to mind-reading. Perhaps the solution is to + // change the syntax rules such that sub-lists must start with a + // starting cardinal number; e.g. "1." or "a.". + + g_list_level++; + + // trim trailing blank lines: + list_str = list_str.replace(/\n{2,}$/, "\n"); + + // attacklab: add sentinel to emulate \z + list_str += "~0"; + + // In the original attacklab showdown, list_type was not given to this function, and anything + // that matched /[*+-]|\d+[.]/ would just create the next
  • , causing this mismatch: + // + // Markdown rendered by WMD rendered by MarkdownSharp + // ------------------------------------------------------------------ + // 1. first 1. first 1. first + // 2. second 2. second 2. second + // - third 3. third * third + // + // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx, + // with {MARKER} being one of \d+[.] or [*+-], depending on list_type: + + /* + list_str = list_str.replace(/ + (^[ \t]*) // leading whitespace = $1 + ({MARKER}) [ \t]+ // list marker = $2 + ([^\r]+? // list item text = $3 + (\n+) + ) + (?= + (~0 | \2 ({MARKER}) [ \t]+) + ) + /gm, function(){...}); + */ + + var marker = _listItemMarkers[list_type]; + var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm"); + var last_item_had_a_double_newline = false; + list_str = list_str.replace(re, + function (wholeMatch, m1, m2, m3) { + var item = m3; + var leading_space = m1; + var ends_with_double_newline = /\n\n$/.test(item); + var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1; + + if (contains_double_newline || last_item_had_a_double_newline) { + item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true); + } + else { + // Recursion for sub-lists: + item = _DoLists(_Outdent(item), /* isInsideParagraphlessListItem= */ true); + item = item.replace(/\n$/, ""); // chomp(item) + if (!isInsideParagraphlessListItem) // only the outer-most item should run this, otherwise it's run multiple times for the inner ones + item = _RunSpanGamut(item); + } + last_item_had_a_double_newline = ends_with_double_newline; + return "
  • " + item + "
  • \n"; + } + ); + + // attacklab: strip sentinel + list_str = list_str.replace(/~0/g, ""); + + g_list_level--; + return list_str; + } + + function _DoCodeBlocks(text) { + // + // Process Markdown `
    ` blocks.
    +            //  
    +
    +            /*
    +            text = text.replace(/
    +                (?:\n\n|^)
    +                (                               // $1 = the code block -- one or more lines, starting with a space/tab
    +                    (?:
    +                        (?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
    +                        .*\n+
    +                    )+
    +                )
    +                (\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width
    +            /g ,function(){...});
    +            */
    +
    +            // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
    +            text += "~0";
    +
    +            text = text.replace(/(?:\n\n|^\n?)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
    +                function (wholeMatch, m1, m2) {
    +                    var codeblock = m1;
    +                    var nextChar = m2;
    +
    +                    codeblock = _EncodeCode(_Outdent(codeblock));
    +                    codeblock = _Detab(codeblock);
    +                    codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
    +                    codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
    +
    +                    codeblock = "
    " + codeblock + "\n
    "; + + return "\n\n" + codeblock + "\n\n" + nextChar; + } + ); + + // attacklab: strip sentinel + text = text.replace(/~0/, ""); + + return text; + } + + function hashBlock(text) { + text = text.replace(/(^\n+|\n+$)/g, ""); + return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n"; + } + + function _DoCodeSpans(text) { + // + // * Backtick quotes are used for spans. + // + // * You can use multiple backticks as the delimiters if you want to + // include literal backticks in the code span. So, this input: + // + // Just type ``foo `bar` baz`` at the prompt. + // + // Will translate to: + // + //

    Just type foo `bar` baz at the prompt.

    + // + // There's no arbitrary limit to the number of backticks you + // can use as delimters. If you need three consecutive backticks + // in your code, use four for delimiters, etc. + // + // * You can use spaces to get literal backticks at the edges: + // + // ... type `` `bar` `` ... + // + // Turns to: + // + // ... type `bar` ... + // + + /* + text = text.replace(/ + (^|[^\\]) // Character before opening ` can't be a backslash + (`+) // $2 = Opening run of ` + ( // $3 = The code block + [^\r]*? + [^`] // attacklab: work around lack of lookbehind + ) + \2 // Matching closer + (?!`) + /gm, function(){...}); + */ + + text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, + function (wholeMatch, m1, m2, m3, m4) { + var c = m3; + c = c.replace(/^([ \t]*)/g, ""); // leading whitespace + c = c.replace(/[ \t]*$/g, ""); // trailing whitespace + c = _EncodeCode(c); + c = c.replace(/:\/\//g, "~P"); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. Will be converted back after the auto-linker runs. + return m1 + "" + c + ""; + } + ); + + return text; + } + + function _EncodeCode(text) { + // + // Encode/escape certain characters inside Markdown code runs. + // The point is that in code, these characters are literals, + // and lose their special Markdown meanings. + // + // Encode all ampersands; HTML entities are not + // entities within a Markdown code span. + text = text.replace(/&/g, "&"); + + // Do the angle bracket song and dance: + text = text.replace(//g, ">"); + + // Now, escape characters that are magic in Markdown: + text = escapeCharacters(text, "\*_{}[]\\", false); + + // jj the line above breaks this: + //--- + + //* Item + + // 1. Subitem + + // special char: * + //--- + + return text; + } + + function _DoItalicsAndBold(text) { + + // must go first: + text = text.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g, + "$1$3$4"); + + text = text.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g, + "$1$3$4"); + + return text; + } + + function _DoBlockQuotes(text) { + + /* + text = text.replace(/ + ( // Wrap whole match in $1 + ( + ^[ \t]*>[ \t]? // '>' at the start of a line + .+\n // rest of the first line + (.+\n)* // subsequent consecutive lines + \n* // blanks + )+ + ) + /gm, function(){...}); + */ + + text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, + function (wholeMatch, m1) { + var bq = m1; + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting + + // attacklab: clean up hack + bq = bq.replace(/~0/g, ""); + + bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines + bq = _RunBlockGamut(bq); // recurse + + bq = bq.replace(/(^|\n)/g, "$1 "); + // These leading spaces screw with
     content, so we need to fix that:
    +                    bq = bq.replace(
    +                            /(\s*
    [^\r]+?<\/pre>)/gm,
    +                        function (wholeMatch, m1) {
    +                            var pre = m1;
    +                            // attacklab: hack around Konqueror 3.5.4 bug:
    +                            pre = pre.replace(/^  /mg, "~0");
    +                            pre = pre.replace(/~0/g, "");
    +                            return pre;
    +                        });
    +
    +                    return hashBlock("
    \n" + bq + "\n
    "); + } + ); + return text; + } + + function _FormParagraphs(text, doNotUnhash) { + // + // Params: + // $text - string to process with html

    tags + // + + // Strip leading and trailing lines: + text = text.replace(/^\n+/g, ""); + text = text.replace(/\n+$/g, ""); + + var grafs = text.split(/\n{2,}/g); + var grafsOut = []; + + var markerRe = /~K(\d+)K/; + + // + // Wrap

    tags. + // + var end = grafs.length; + for (var i = 0; i < end; i++) { + var str = grafs[i]; + + // if this is an HTML marker, copy it + if (markerRe.test(str)) { + grafsOut.push(str); + } + else if (/\S/.test(str)) { + str = _RunSpanGamut(str); + str = str.replace(/^([ \t]*)/g, "

    "); + str += "

    " + grafsOut.push(str); + } + + } + // + // Unhashify HTML blocks + // + if (!doNotUnhash) { + end = grafsOut.length; + for (var i = 0; i < end; i++) { + var foundAny = true; + while (foundAny) { // we may need several runs, since the data may be nested + foundAny = false; + grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) { + foundAny = true; + return g_html_blocks[id]; + }); + } + } + } + return grafsOut.join("\n\n"); + } + + function _EncodeAmpsAndAngles(text) { + // Smart processing for ampersands and angle brackets that need to be encoded. + + // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: + // http://bumppo.net/projects/amputator/ + text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&"); + + // Encode naked <'s + text = text.replace(/<(?![a-z\/?!]|~D)/gi, "<"); + + return text; + } + + function _EncodeBackslashEscapes(text) { + // + // Parameter: String. + // Returns: The string, with after processing the following backslash + // escape sequences. + // + + // attacklab: The polite way to do this is with the new + // escapeCharacters() function: + // + // text = escapeCharacters(text,"\\",true); + // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); + // + // ...but we're sidestepping its use of the (slow) RegExp constructor + // as an optimization for Firefox. This function gets called a LOT. + + text = text.replace(/\\(\\)/g, escapeCharacters_callback); + text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback); + return text; + } + + var charInsideUrl = "[-A-Z0-9+&@#/%?=~_|[\\]()!:,.;]", + charEndingUrl = "[-A-Z0-9+&@#/%=~_|[\\])]", + autoLinkRegex = new RegExp("(=\"|<)?\\b(https?|ftp)(://" + charInsideUrl + "*" + charEndingUrl + ")(?=$|\\W)", "gi"), + endCharRegex = new RegExp(charEndingUrl, "i"); + + function handleTrailingParens(wholeMatch, lookbehind, protocol, link) { + if (lookbehind) + return wholeMatch; + if (link.charAt(link.length - 1) !== ")") + return "<" + protocol + link + ">"; + var parens = link.match(/[()]/g); + var level = 0; + for (var i = 0; i < parens.length; i++) { + if (parens[i] === "(") { + if (level <= 0) + level = 1; + else + level++; + } + else { + level--; + } + } + var tail = ""; + if (level < 0) { + var re = new RegExp("\\){1," + (-level) + "}$"); + link = link.replace(re, function (trailingParens) { + tail = trailingParens; + return ""; + }); + } + if (tail) { + var lastChar = link.charAt(link.length - 1); + if (!endCharRegex.test(lastChar)) { + tail = lastChar + tail; + link = link.substr(0, link.length - 1); + } + } + return "<" + protocol + link + ">" + tail; + } + + function _DoAutoLinks(text) { + + // note that at this point, all other URL in the text are already hyperlinked as
    + // *except* for the case + + // automatically add < and > around unadorned raw hyperlinks + // must be preceded by a non-word character (and not by =" or <) and followed by non-word/EOF character + // simulating the lookbehind in a consuming way is okay here, since a URL can neither and with a " nor + // with a <, so there is no risk of overlapping matches. + text = text.replace(autoLinkRegex, handleTrailingParens); + + // autolink anything like + + var replacer = function (wholematch, m1) { return "" + pluginHooks.plainLinkText(m1) + ""; } + text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer); + + // Email addresses: + /* + text = text.replace(/ + < + (?:mailto:)? + ( + [-.\w]+ + \@ + [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ + ) + > + /gi, _DoAutoLinks_callback()); + */ + + /* disabling email autolinking, since we don't do that on the server, either + text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, + function(wholeMatch,m1) { + return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); + } + ); + */ + return text; + } + + function _UnescapeSpecialChars(text) { + // + // Swap back in all the special characters we've hidden. + // + text = text.replace(/~E(\d+)E/g, + function (wholeMatch, m1) { + var charCodeToReplace = parseInt(m1); + return String.fromCharCode(charCodeToReplace); + } + ); + return text; + } + + function _Outdent(text) { + // + // Remove one level of line-leading tabs or spaces + // + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width + + // attacklab: clean up hack + text = text.replace(/~0/g, "") + + return text; + } + + function _Detab(text) { + if (!/\t/.test(text)) + return text; + + var spaces = [" ", " ", " ", " "], + skew = 0, + v; + + return text.replace(/[\n\t]/g, function (match, offset) { + if (match === "\n") { + skew = offset + 1; + return match; + } + v = (offset - skew) % 4; + skew = offset + 1; + return spaces[v]; + }); + } + + // + // attacklab: Utility functions + // + + var _problemUrlChars = /(?:["'*()[\]:]|~D)/g; + + // hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems + function encodeProblemUrlChars(url) { + if (!url) + return ""; + + var len = url.length; + + return url.replace(_problemUrlChars, function (match, offset) { + if (match == "~D") // escape for dollar + return "%24"; + if (match == ":") { + if (offset == len - 1 || /[0-9\/]/.test(url.charAt(offset + 1))) + return ":" + } + return "%" + match.charCodeAt(0).toString(16); + }); + } + + + function escapeCharacters(text, charsToEscape, afterBackslash) { + // First we have to escape the escape characters so that + // we can build a character class out of them + var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])"; + + if (afterBackslash) { + regexString = "\\\\" + regexString; + } + + var regex = new RegExp(regexString, "g"); + text = text.replace(regex, escapeCharacters_callback); + + return text; + } + + + function escapeCharacters_callback(wholeMatch, m1) { + var charCodeToEscape = m1.charCodeAt(0); + return "~E" + charCodeToEscape + "E"; + } + + }; // end of the Markdown.Converter constructor + +})(); diff --git a/js/Markdown.LICENSE.txt b/js/Markdown.LICENSE.txt new file mode 100644 index 0000000..b938687 --- /dev/null +++ b/js/Markdown.LICENSE.txt @@ -0,0 +1,32 @@ +A javascript port of Markdown, as used on Stack Overflow +and the rest of Stack Exchange network. + +Largely based on showdown.js by John Fraser (Attacklab). + +Original Markdown Copyright (c) 2004-2005 John Gruber + + + +Original Showdown code copyright (c) 2007 John Fraser + +Modifications and bugfixes (c) 2009 Dana Robinson +Modifications and bugfixes (c) 2009-2013 Stack Exchange Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/js/Markdown.Sanitizer.js b/js/Markdown.Sanitizer.js new file mode 100644 index 0000000..cc5826f --- /dev/null +++ b/js/Markdown.Sanitizer.js @@ -0,0 +1,108 @@ +(function () { + var output, Converter; + if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module + output = exports; + Converter = require("./Markdown.Converter").Converter; + } else { + output = window.Markdown; + Converter = output.Converter; + } + + output.getSanitizingConverter = function () { + var converter = new Converter(); + converter.hooks.chain("postConversion", sanitizeHtml); + converter.hooks.chain("postConversion", balanceTags); + return converter; + } + + function sanitizeHtml(html) { + return html.replace(/<[^>]*>?/gi, sanitizeTag); + } + + // (tags that can be opened/closed) | (tags that stand alone) + var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i; + // | + var a_white = /^(]+")?\s?>|<\/a>)$/i; + + // ]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i; + + function sanitizeTag(tag) { + if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white)) + return tag; + else + return ""; + } + + /// + /// attempt to balance HTML tags in the html string + /// by removing any unmatched opening or closing tags + /// IMPORTANT: we *assume* HTML has *already* been + /// sanitized and is safe/sane before balancing! + /// + /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593 + /// + function balanceTags(html) { + + if (html == "") + return ""; + + var re = /<\/?\w+[^>]*(\s|$|>)/g; + // convert everything to lower case; this makes + // our case insensitive comparisons easier + var tags = html.toLowerCase().match(re); + + // no HTML tags present? nothing to do; exit now + var tagcount = (tags || []).length; + if (tagcount == 0) + return html; + + var tagname, tag; + var ignoredtags = "



  • "; + var match; + var tagpaired = []; + var tagremove = []; + var needsRemoval = false; + + // loop through matched tags in forward order + for (var ctag = 0; ctag < tagcount; ctag++) { + tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1"); + // skip any already paired tags + // and skip tags in our ignore list; assume they're self-closed + if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1) + continue; + + tag = tags[ctag]; + match = -1; + + if (!/^<\//.test(tag)) { + // this is an opening tag + // search forwards (next tags), look for closing tags + for (var ntag = ctag + 1; ntag < tagcount; ntag++) { + if (!tagpaired[ntag] && tags[ntag] == "") { + match = ntag; + break; + } + } + } + + if (match == -1) + needsRemoval = tagremove[ctag] = true; // mark for removal + else + tagpaired[match] = true; // mark paired + } + + if (!needsRemoval) + return html; + + // delete all orphaned tags from the string + + var ctag = 0; + html = html.replace(re, function (match) { + var res = tagremove[ctag] ? "" : match; + ctag++; + return res; + }); + return html; + } +})(); diff --git a/js/html2markdown.LICENSE.txt b/js/html2markdown.LICENSE.txt new file mode 100644 index 0000000..7324765 --- /dev/null +++ b/js/html2markdown.LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2013 Kates Gasis, Himanshu Gilani + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/js/html2markdown.js b/js/html2markdown.js new file mode 100644 index 0000000..518d75e --- /dev/null +++ b/js/html2markdown.js @@ -0,0 +1,568 @@ +/** + * html2markdown - An HTML to Markdown converter. + * + * This implementation uses HTML or DOM parsing for conversion. Parsing code was + * abstracted out in a parsing function which should be easy to remove in favor + * of other parsing libraries. + * + * Converted MarkDown was tested with ShowDown library for HTML rendering. And + * it tries to create MarkDown that does not confuse ShowDown when certain + * combination of HTML tags come together. + * + * @author Himanshu Gilani + * @author Kates Gasis (original author) + * + */ + +/** + * html2markdown + * @param html - html string to convert + * @return converted markdown text + */ + +/* + Universal JavaScript Module, supports AMD (RequireJS), Node.js, and the browser. + https://gist.github.com/kirel/1268753 +*/ + +(function (name, definition) { + if (typeof define === 'function') { // AMD + define(definition); + } else if (typeof module !== 'undefined' && module.exports) { // Node.js + module.exports = definition(); + } else { // Browser + var theModule = definition(), global = this, old = global[name]; + theModule.noConflict = function () { + global[name] = old; + return theModule; + }; + global[name] = theModule; + } +})('html2markdown', function() { + +function trim(value) { + return value.replace(/^\s+|\s+$/g,""); +} + +function endsWith(value, suffix) { + return value.match(suffix+"$") == suffix; +} + +function startsWith(value, str) { + return value.indexOf(str) == 0; +} + +function html2markdown(html, opts) { + opts = opts || {}; + + var nodeList = []; + var listTagStack = []; + var linkAttrStack = []; + var blockquoteStack = []; + var preStack = []; + var codeStack = []; + var links = []; + var inlineStyle = opts['inlineStyle'] || false; + var parser = opts['parser']; + var markdownTags = { + "hr": "- - -\n\n", + "br": " \n", + "title": "# ", + "h1": "# ", + "h2": "## ", + "h3": "### ", + "h4": "#### ", + "h5": "##### ", + "h6": "###### ", + "b": "**", + "strong": "**", + "i": "_", + "em": "_", + "dfn": "_", + "var": "_", + "cite": "_", + "span": " ", + "ul": "* ", + "ol": "1. ", + "dl": "- ", + "blockquote": "> " + }; + + if (!parser && typeof markdownDOMParser !== 'undefined') { + parser = markdownDOMParser; + } + + function getListMarkdownTag() { + var listItem = ""; + if (listTagStack) { + for (var i = 0; i < listTagStack.length - 1; i++) { + listItem += " "; + } + } + listItem += peek(listTagStack); + return listItem; + } + + function convertAttrs(attrs) { + var attributes = {}; + for (var k in attrs) { + var attr = attrs[k]; + attributes[attr.name] = attr; + } + return attributes; + } + + function peek(list) { + if (list && list.length > 0) { + return list.slice(-1)[0]; + } + return ""; + } + + function peekTillNotEmpty(list) { + if (!list) { + return ""; + } + + for (var i = list.length - 1; i >= 0; i--){ + if (list[i] != "") { + return list[i]; + } + } + return ""; + } + + function removeIfEmptyTag(start) { + var cleaned = false; + if (start == peekTillNotEmpty(nodeList)) { + while (peek(nodeList) != start) { + nodeList.pop(); + } + nodeList.pop(); + cleaned = true; + } + return cleaned; + } + + function sliceText(start) { + var text = []; + while (nodeList.length > 0 && peek(nodeList) != start) { + var t = nodeList.pop(); + text.unshift(t); + } + return text.join(""); + } + + function block(isEndBlock) { + var lastItem = nodeList.pop(); + if (!lastItem) { + return; + } + + if (!isEndBlock) { + var block; + if (/\s*\n\n\s*$/.test(lastItem)) { + lastItem = lastItem.replace(/\s*\n\n\s*$/, "\n\n"); + block = ""; + } else if (/\s*\n\s*$/.test(lastItem)) { + lastItem = lastItem.replace(/\s*\n\s*$/, "\n"); + block = "\n"; + } else if (/\s+$/.test(lastItem)) { + block = "\n\n"; + } else { + block = "\n\n"; + } + + nodeList.push(lastItem); + nodeList.push(block); + } else { + nodeList.push(lastItem); + if (!endsWith(lastItem, "\n")) { + nodeList.push("\n\n"); + } + } + } + + function listBlock() { + if (nodeList.length > 0) { + var li = peek(nodeList); + + if (!endsWith(li, "\n")) { + nodeList.push("\n"); + } + } else { + nodeList.push("\n"); + } + } + + parser(html, { + start: function(tag, attrs, unary) { + tag = tag.toLowerCase(); + + if (unary && (tag != "br" && tag != "hr" && tag != "img")) { + return; + } + + switch (tag) { + case "br": + nodeList.push(markdownTags[tag]); + break; + case "hr": + block(); + nodeList.push(markdownTags[tag]); + break; + case "title": + case "h1": + case "h2": + case "h3": + case "h4": + case "h5": + case "h6": + block(); + nodeList.push(markdownTags[tag]); + break; + case "b": + case "strong": + case "i": + case "em": + case "dfn": + case "var": + case "cite": + nodeList.push(markdownTags[tag]); + break; + case "code": + case "span": + if (preStack.length > 0) { + break; + } else if (!/\s+$/.test(peek(nodeList))) { + nodeList.push(markdownTags[tag]); + } + break; + case "p": + case "div": + case "table": + case "tbody": + case "tr": + case "td": + block(); + break; + case "ul": + case "ol": + case "dl": + listTagStack.push(markdownTags[tag]); + // lists are block elements + if (listTagStack.length > 1) { + listBlock(); + } else { + block(); + } + break; + case "li": + case "dt": + var li = getListMarkdownTag(); + nodeList.push(li); + break; + case "a": + var attribs = convertAttrs(attrs); + linkAttrStack.push(attribs); + nodeList.push("["); + break; + case "img": + var attribs = convertAttrs(attrs); + var alt, title, url; + + attribs["src"] ? url = attribs["src"].value : url = ""; + if (!url) { + break; + } + + attribs['alt'] ? alt = trim(attribs['alt'].value) : alt = ""; + attribs['title'] ? title = trim(attribs['title'].value) : title = ""; + + // if parent of image tag is nested in anchor tag use inline style + if (!inlineStyle && !startsWith(peekTillNotEmpty(nodeList), "[")) { + var l = links.indexOf(url); + if (l == -1) { + links.push(url); + l=links.length-1; + } + + block(); + nodeList.push("!["); + if (alt!= "") { + nodeList.push(alt); + } else if (title != null) { + nodeList.push(title); + } + + nodeList.push("][" + l + "]"); + block(); + } else { + //if image is not a link image then treat images as block elements + if (!startsWith(peekTillNotEmpty(nodeList), "[")) { + block(); + } + + nodeList.push("![" + alt + "](" + url + (title ? " \"" + title + "\"" : "") + ")"); + + if (!startsWith(peekTillNotEmpty(nodeList), "[")) { + block(true); + } + } + break; + case "blockquote": + //listBlock(); + block(); + blockquoteStack.push(markdownTags[tag]); + break; + case "pre": + block(); + preStack.push(true); + nodeList.push(" "); + break; + case "table": + nodeList.push(""); + break; + case "thead": + nodeList.push(""); + break; + case "tbody": + nodeList.push(""); + break; + case "tr": + nodeList.push(""); + break; + case "td": + nodeList.push("
    "); + break; + } + }, + chars: function(text) { + if (preStack.length > 0) { + text = text.replace(/\n/g,"\n "); + } else if (trim(text) != "") { + text = text.replace(/\s+/g, " "); + + var prevText = peekTillNotEmpty(nodeList); + if (/\s+$/.test(prevText)) { + text = text.replace(/^\s+/g, ""); + } + } else { + nodeList.push(""); + return; + } + + //if(blockquoteStack.length > 0 && peekTillNotEmpty(nodeList).endsWith("\n")) { + if (blockquoteStack.length > 0) { + nodeList.push(blockquoteStack.join("")); + } + + nodeList.push(text); + }, + end: function(tag) { + tag = tag.toLowerCase(); + + switch (tag) { + case "title": + case "h1": + case "h2": + case "h3": + case "h4": + case "h5": + case "h6": + if(!removeIfEmptyTag(markdownTags[tag])) { + block(true); + } + break; + case "p": + case "div": + case "table": + case "tbody": + case "tr": + case "td": + while(nodeList.length > 0 && trim(peek(nodeList)) == "") { + nodeList.pop(); + } + block(true); + break; + case "b": + case "strong": + case "i": + case "em": + case "dfn": + case "var": + case "cite": + if (!removeIfEmptyTag(markdownTags[tag])) { + nodeList.push(trim(sliceText(markdownTags[tag]))); + nodeList.push(markdownTags[tag]); + } + break; + case "a": + var text = sliceText("["); + text = text.replace(/\s+/g, " "); + text = trim(text); + + if (text == "") { + nodeList.pop(); + break; + } + + var attrs = linkAttrStack.pop(); + var url; + attrs["href"] && attrs["href"].value != "" ? url = attrs["href"].value : url = ""; + + if (url == "") { + nodeList.pop(); + nodeList.push(text); + break; + } + + nodeList.push(text); + + if (!inlineStyle && !startsWith(peek(nodeList), "!")){ + var l = links.indexOf(url); + if (l == -1) { + links.push(url); + l=links.length-1; + } + nodeList.push("][" + l + "]"); + } else { + if(startsWith(peek(nodeList), "!")){ + var text = nodeList.pop(); + text = nodeList.pop() + text; + block(); + nodeList.push(text); + } + + var title = attrs["title"]; + nodeList.push("](" + url + (title ? " \"" + trim(title.value).replace(/\s+/g, " ") + "\"" : "") + ")"); + + if(startsWith(peek(nodeList), "!")){ + block(true); + } + } + break; + case "ul": + case "ol": + case "dl": + listBlock(); + listTagStack.pop(); + break; + case "li": + case "dt": + var li = getListMarkdownTag(); + if (!removeIfEmptyTag(li)) { + var text = trim(sliceText(li)); + + if (startsWith(text, "[![")) { + nodeList.pop(); + block(); + nodeList.push(text); + block(true); + } else { + nodeList.push(text); + listBlock(); + } + } + break; + case "blockquote": + blockquoteStack.pop(); + break; + case "pre": + //uncomment following experimental code to discard line numbers when syntax highlighters are used + //notes this code thorough testing before production user + /* + var p=[]; + var flag = true; + var count = 0, whiteSpace = 0, line = 0; + console.log(">> " + peek(nodeList)); + while(peek(nodeList).startsWith(" ") || flag == true) + { + //console.log('inside'); + var text = nodeList.pop(); + p.push(text); + + if(flag == true && !text.startsWith(" ")) { + continue; + } else { + flag = false; + } + + //var result = parseInt(text.trim()); + if(!isNaN(text.trim())) { + count++; + } else if(text.trim() == ""){ + whiteSpace++; + } else { + line++; + } + flag = false; + } + + console.log(line); + if(line != 0) + { + while(p.length != 0) { + nodeList.push(p.pop()); + } + } + */ + block(true); + preStack.pop(); + break; + case "code": + case "span": + if (preStack.length > 0) { + break; + } else if (trim(peek(nodeList)) == "") { + nodeList.pop(); + nodeList.push(markdownTags[tag]); + } else { + var text = nodeList.pop(); + nodeList.push(trim(text)); + nodeList.push(markdownTags[tag]); + } + break; + case "table": + nodeList.push("
    "); + break; + case "thead": + nodeList.push(""); + break; + case "tbody": + nodeList.push(""); + break; + case "tr": + nodeList.push(""); + break; + case "td": + nodeList.push(""); + break; + case "br": + case "hr": + case "img": + break; + } + + } + }, {"nodesToIgnore": ["script", "noscript", "object", "iframe", "frame", "head", "style", "label"]}); + + if (!inlineStyle) { + for (var i = 0; i < links.length; i++) { + if (i == 0) { + var lastItem = nodeList.pop(); + nodeList.push(lastItem.replace(/\s+$/g, "")); + nodeList.push("\n\n[" + i + "]: " + links[i]); + } else { + nodeList.push("\n[" + i + "]: " + links[i]); + } + } + } + + return nodeList.join(""); + +} + +return html2markdown; + +}); diff --git a/js/journal.js b/js/journal.js index 5ebd5a0..08255b8 100644 --- a/js/journal.js +++ b/js/journal.js @@ -73,6 +73,7 @@ String.prototype.zeroPad = function(digits) { OC.Journal = { categories:undefined, version:undefined, + enabled:false, init:function() { var self = this; this.version = $('#journal-content').data('version'); @@ -149,7 +150,7 @@ OC.Journal = { } return $(obj).parents('.propertycontainer').first(); }, - required:function(event){ // eventhandler for required elements + required:function(event) { // eventhandler for required elements // FIXME: This doesn't seem to work. console.log('blur on required'); var obj = $(event.target); @@ -162,8 +163,12 @@ OC.Journal = { $(obj).off('blur', OC.Journal.required); } }, + isEnabled: function() { + return this.enabled; + }, setEnabled:function(state) { if(typeof state == 'undefined') { state = true; } + this.enabled = state; console.log('OC.Journal.setEnabled: ' + state); if(state) { $('#description').rte('setEnabled', true); @@ -198,6 +203,7 @@ OC.Journal = { id:'', data:undefined, add:function() { + OC.Journal.setEnabled(false); // TODO: wrap a DIV around the summary field with a suggestion(?) to fill out this field first. See OC.Journal.required // Remember to reenable all controls. $('#entry,#metadata').show(); @@ -206,10 +212,11 @@ OC.Journal = { OC.Journal.setEnabled(false); $('#summary').prop('disabled', false); $('#summary').addClass('editable'); - $('#entries lidata-id="'+this.id+'"').removeClass('active'); + $('#entries li[data-id="' + this.id + '"]').removeClass('active'); this.id = 'new'; this.data = undefined; $('.property').each(function () { + console.log('Emptying properties', this, $(this).get(0).nodeName); switch($(this).get(0).nodeName) { case 'DIV': $(this).html(''); @@ -227,8 +234,10 @@ OC.Journal = { $('#editortoolbar li.richtext').hide(); $('#editable').attr('checked', true); $('#actions').hide(); + OC.Journal.setEnabled(true); }, createEntry:function(data) { + console.log('createEntry', data); var sharedindicator = data.owner == OC.currentUser ? '' : '' var date = new Date(parseInt(data.dtstart)*1000); @@ -237,6 +246,7 @@ OC.Journal = { + sharedindicator + '
    '+date.toDateString()+timestring+'
  • ').data('entry', data); }, loadEntry:function(id, data) { + OC.Journal.setEnabled(false); console.log('loadEntry:', id, data.summary, data.description.format); this.permissions = parseInt(data.permissions); this.readonly = !(this.permissions & OC.PERMISSION_UPDATE); @@ -286,11 +296,14 @@ OC.Journal = { $('#also_time').attr('checked', true); //$('#also_time').get(0).checked = true; } - console.log('dtstart: '+date); + console.log('dtstart: ' + date); OC.Share.loadIcons('journal'); }, saveproperty:function(obj) { + if(!OC.Journal.isEnabled()) { + return; + } console.log('saveproperty, this.id', this.id) if(!this.id || this.id == 'new') { // we are adding an entry and want a response back from the server. this.id = 'new'; @@ -561,11 +574,11 @@ OC.Journal = { $('#entry,#metadata').show(); $('#firstrun').hide(); } else { - try { + /*try { $('#description').rte('destroy'); } catch(e) { console.warn(e); - } + }*/ $('#entry,#metadata').hide(); $('#firstrun').show(); } @@ -589,6 +602,7 @@ OC.Journal = { $(document).ready(function(){ // Initialize controls. + $('#description').rte({classes: ['property','content']}); $('#categories').multiple_autocomplete({source: OC.Journal.categories}); //$('#categories').multiple_autocomplete('option', 'source', categories); if(!Modernizr.inputtypes.date) { @@ -597,7 +611,6 @@ $(document).ready(function(){ if(!Modernizr.inputtypes.time) { $('#dtstarttime').timepicker({timeFormat: 'hh:mm', showPeriodLabels:false}); } - $('#description').rte({classes: ['property','content']}); $('.tip').tipsy(); $('#actions a,.cog').tipsy({gravity: 'e'}); diff --git a/js/jquery.rte.js b/js/jquery.rte.js index 67b25df..d023ad2 100644 --- a/js/jquery.rte.js +++ b/js/jquery.rte.js @@ -17,6 +17,7 @@ $.widget( 'ui.rte', { textarea = this.element; //.hide(), pos = textarea.position(); //this.element.text = this.text; + this.converter = new Markdown.Converter(); this.mirror = $('
    ').css({ top:pos.top, left: pos.left, @@ -209,14 +210,18 @@ $.widget( 'ui.rte', { case 'mode': switch(value) { case 'html': - this.mirror.html(this.element.val()); + console.log('converting to html:', this.element.val()); + this.mirror.html(this.converter.makeHtml(this.element.val())); this.mirror.show(); this.element.hide(); this.mirror.trigger('change'); break; case 'text': + console.log('converting to text:', $(this.mirror).html()); try { - this.element.val($(this.mirror.html().replace(/
    /g, "\n")).text()); + //this.element.val($(this.mirror).html().replace(/
    /g, "\n")); + this.element.val(html2markdown($(this.mirror).html())); + //this.element.val(this.converter.makeMarkdown($(this.mirror).html())); } catch(e) { console.warn('Exception:', e); } diff --git a/js/markdown_dom_parser.js b/js/markdown_dom_parser.js new file mode 100644 index 0000000..442526c --- /dev/null +++ b/js/markdown_dom_parser.js @@ -0,0 +1,431 @@ +/* + * HTMLParser - This implementation of parser assumes we are parsing HTML in browser + * and user DOM methods available in browser for parsing HTML. + * + * @author Himanshu Gilani + * + */ + +/* + Universal JavaScript Module, supports AMD (RequireJS), Node.js, and the browser. + https://gist.github.com/kirel/1268753 +*/ + +(function (name, definition) { + if (typeof define === 'function') { // AMD + define(definition); + } else if (typeof module !== 'undefined' && module.exports) { // Node.js + module.exports = definition(); + } else { // Browser + var theModule = definition(), global = this, old = global[name]; + theModule.noConflict = function () { + global[name] = old; + return theModule; + }; + global[name] = theModule; + } +})('markdownDOMParser', function() { + +var HTMLParser = function (html, handler, opts) { + opts = opts || {}; + + var e = document.createElement('div'); + e.innerHTML = html; + var node = e; + var nodesToIgnore = opts['nodesToIgnore'] || []; + var parseHiddenNodes = opts['parseHiddenNodes'] || 'false'; + + var c = node.childNodes; + for (var i = 0; i < c.length; i++) { + try { + var ignore = false; + for (var k=0; k< nodesToIgnore.length; k++) { + if (c[i].nodeName.toLowerCase() == nodesToIgnore[k]) { + ignore= true; + break; + } + } + + //NOTE hidden node testing is expensive in FF. + if (ignore || (!parseHiddenNodes && isHiddenNode(c[i])) ){ + continue; + } + + if (c[i].nodeName.toLowerCase() != "#text" && c[i].nodeName.toLowerCase() != "#comment") { + var attrs = []; + + if (c[i].hasAttributes()) { + var attributes = c[i].attributes; + for ( var a = 0; a < attributes.length; a++) { + var attribute = attributes.item(a); + + attrs.push({ + name : attribute.nodeName, + value : attribute.nodeValue, + }); + } + } + + if (handler.start) { + if (c[i].hasChildNodes()) { + handler.start(c[i].nodeName, attrs, false); + + //if (c[i].nodeName.toLowerCase() == "pre" || c[i].nodeName.toLowerCase() == "code") { + // handler.chars(c[i].innerHTML); + //} else + if (c[i].nodeName.toLowerCase() == "iframe" || c[i].nodeName.toLowerCase() == "frame") { + if (c[i].contentDocument && c[i].contentDocument.documentElement) { + return HTMLParser(c[i].contentDocument.documentElement, handler, opts); + } + } else { + HTMLParser(c[i].innerHTML, handler, opts); + } + + if (handler.end) { + handler.end(c[i].nodeName); + } + } else { + handler.start(c[i].nodeName, attrs, true); + } + } + } else if (c[i].nodeName.toLowerCase() == "#text") { + if (handler.chars) { + handler.chars(c[i].nodeValue); + } + } else if (c[i].nodeName.toLowerCase() == "#comment") { + if (handler.comment) { + handler.comment(c[i].nodeValue); + } + } + + } catch (e) { + //properly log error + console.error(e); + console.log("error while parsing node: " + c[i].nodeName.toLowerCase()); + } + } +}; + +function isHiddenNode(node) { + if (node.nodeName.toLowerCase() == "title"){ + return false; + } + + if (window.getComputedStyle) { + try { + var style = window.getComputedStyle(node, null); + if (style.getPropertyValue && style.getPropertyValue('display') == 'none') { + return true; + } + } catch (e) { + // consume and ignore. node styles are not accessible + } + return false; + } +} + +//http://blogs.msdn.com/b/aoakley/archive/2003/11/12/49645.aspx +function HTMLDecode(str) { + var div = document.createElement('div'); + div.style.display="none"; + div.innerHTML = str; + var decoded = div.firstChild.nodeValue; + div.parentNode.removeChild(div); + return decoded; +} + +// HTMLEncode (@author Ulrich Jensen, http://www.htmlescape.net) +var hex = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'); + +function HTMLEncode(originalText) { + var preescape = "" + originalText; + var escaped = ""; + + var i = 0; + for (i = 0; i < preescape.length; i++) { + var p = preescape.charAt(i); + + p = "" + escapeCharOther(p); + p = "" + escapeTags(p); + p = "" + escapeBR(p); + + escaped = escaped + p; + } + return escaped; +} + +function escapeHtmlTextArea(originalText) { + var preescape = "" + originalText; + var escaped = ""; + + var i = 0; + for (i = 0; i < preescape.length; i++) { + var p = preescape.charAt(i); + + p = "" + escapeCharOther(p); + p = "" + escapeTags(p); + + escaped = escaped + p; + } + + return escaped; +} + +function escapeBR(original) { + var thechar = original.charCodeAt(0); + + switch (thechar) { + case 10: + return "
    "; + break; // newline + case '\r': + break; + } + return original; +} + +function escapeNBSP(original) { + var thechar = original.charCodeAt(0); + switch (thechar) { + case 32: + return " "; + break; // space + } + return original; +} + +function escapeTags(original) { + var thechar = original.charCodeAt(0); + switch (thechar) { + case 60: + return "<"; + break; // < + case 62: + return ">"; + break; // > + case 34: + return """; + break; // " + } + return original; +} + +function escapeCharOther(original) { + var found = true; + var thechar = original.charCodeAt(0); + switch (thechar) { + case 38: + return "&"; + break; // & + case 198: + return "Æ"; + break; // Æ + case 193: + return "Á"; + break; // Á + case 194: + return "Â"; + break; // Â + case 192: + return "À"; + break; // À + case 197: + return "Å"; + break; // Å + case 195: + return "Ã"; + break; // Ã + case 196: + return "Ä"; + break; // Ä + case 199: + return "Ç"; + break; // Ç + case 208: + return "Ð"; + break; // Ð + case 201: + return "É"; + break; // É + case 202: + return "Ê"; + break; // Ê + case 200: + return "È"; + break; // È + case 203: + return "Ë"; + break; // Ë + case 205: + return "Í"; + break; // Í + case 206: + return "Î"; + break; // Î + case 204: + return "Ì"; + break; // Ì + case 207: + return "Ï"; + break; // Ï + case 209: + return "Ñ"; + break; // Ñ + case 211: + return "Ó"; + break; // Ó + case 212: + return "Ô"; + break; // Ô + case 210: + return "Ò"; + break; // Ò + case 216: + return "Ø"; + break; // Ø + case 213: + return "Õ"; + break; // Õ + case 214: + return "Ö"; + break; // Ö + case 222: + return "Þ"; + break; // Þ + case 218: + return "Ú"; + break; // Ú + case 219: + return "Û"; + break; // Û + case 217: + return "Ù"; + break; // Ù + case 220: + return "Ü"; + break; // Ü + case 221: + return "Ý"; + break; // Ý + case 225: + return "á"; + break; // á + case 226: + return "â"; + break; // â + case 230: + return "æ"; + break; // æ + case 224: + return "à"; + break; // à + case 229: + return "å"; + break; // å + case 227: + return "ã"; + break; // ã + case 228: + return "ä"; + break; // ä + case 231: + return "ç"; + break; // ç + case 233: + return "é"; + break; // é + case 234: + return "ê"; + break; // ê + case 232: + return "è"; + break; // è + case 240: + return "ð"; + break; // ð + case 235: + return "ë"; + break; // ë + case 237: + return "í"; + break; // í + case 238: + return "î"; + break; // î + case 236: + return "ì"; + break; // ì + case 239: + return "ï"; + break; // ï + case 241: + return "ñ"; + break; // ñ + case 243: + return "ó"; + break; // ó + case 244: + return "ô"; + break; // ô + case 242: + return "ò"; + break; // ò + case 248: + return "ø"; + break; // ø + case 245: + return "õ"; + break; // õ + case 246: + return "ö"; + break; // ö + case 223: + return "ß"; + break; // ß + case 254: + return "þ"; + break; // þ + case 250: + return "ú"; + break; // ú + case 251: + return "û"; + break; // û + case 249: + return "ù"; + break; // ù + case 252: + return "ü"; + break; // ü + case 253: + return "ý"; + break; // ý + case 255: + return "ÿ"; + break; // ÿ + case 162: + return "¢"; + break; // ¢ + default: + found = false; + break; + } + + if (!found) { + if (thechar > 127) { + var c = thechar; + var a4 = c % 16; + c = Math.floor(c / 16); + var a3 = c % 16; + c = Math.floor(c / 16); + var a2 = c % 16; + c = Math.floor(c / 16); + var a1 = c % 16; + return "&#x" + hex[a1] + hex[a2] + hex[a3] + hex[a4] + ";"; + } else { + return original; + } + } +} +return HTMLParser; +}); diff --git a/lib/app.php b/lib/app.php index 6b42518..a200931 100644 --- a/lib/app.php +++ b/lib/app.php @@ -271,42 +271,40 @@ public static function getCategories(){ * @param $vevents VJOURNALs to scan. null to check all journals for the current user. * @returns bool */ - public static function scanCategories($vevents = null) { - if (is_null($vevents)) { - $vevents = array(); - $calendars = array(); - $singlecalendar = (bool)\OCP\Config::getUserValue( - \OCP\User::getUser(), 'journal', 'single_calendar', false); - if($singlecalendar) { - $cid = \OCP\Config::getUserValue( - \OCP\User::getUser(), 'journal', 'default_calendar', null); - $calendar = \OC_Calendar_App::getCalendar($cid, true); - if(!$calendar) { - \OCP\Util::writeLog('journal', - 'The default calendar ' . $cid . ' is either not owned by ' - . \OCP\User::getUser() . ' or doesn\'t exist.', - \OCP\Util::WARN - ); - return false; - } - $calendars[] = $calendar; - } else { - $calendars = \OC_Calendar_Calendar::allCalendars(\OCP\User::getUser(), true); + public static function scanCategories() { + $calendars = array(); + $singlecalendar = (bool)\OCP\Config::getUserValue( + \OCP\User::getUser(), 'journal', 'single_calendar', false + ); + if($singlecalendar) { + $cid = \OCP\Config::getUserValue( + \OCP\User::getUser(), 'journal', 'default_calendar', null); + $calendar = \OC_Calendar_App::getCalendar($cid, true); + if(!$calendar) { + \OCP\Util::writeLog('journal', + 'The default calendar ' . $cid . ' is either not owned by ' + . \OCP\User::getUser() . ' or doesn\'t exist.', + \OCP\Util::WARN + ); + return false; } - \OCP\Util::writeLog('journal', __METHOD__ . ', calendars: ' - . count($calendars), \OCP\Util::DEBUG); - if(count($calendars) > 0) { - foreach($calendars as $calendar) { - foreach(VJournal::all($calendar['id']) as $vevent) { - $vobject = \OC_VObject::parse($vevent['calendardata']); - try { - self::getVCategories()->loadFromVObject($vobject->VJOURNAL, true); - } catch(Exception $e) { - \OCP\Util::writeLog('journal', - __METHOD__.', exception: ' . $e->getMessage(), - \OCP\Util::ERROR - ); - } + $calendars[] = $calendar; + } else { + $calendars = \OC_Calendar_Calendar::allCalendars(\OCP\User::getUser(), true); + } + \OCP\Util::writeLog('journal', __METHOD__ . ', calendars: ' + . count($calendars), \OCP\Util::DEBUG); + if(count($calendars) > 0) { + foreach($calendars as $calendar) { + foreach(VJournal::all($calendar['id']) as $vevent) { + $vobject = \OC_VObject::parse($vevent['calendardata']); + try { + self::getVCategories()->loadFromVObject($vobject->VJOURNAL, true); + } catch(\Exception $e) { + \OCP\Util::writeLog('journal', + __METHOD__.', exception: ' . $e->getMessage(), + \OCP\Util::ERROR + ); } } } diff --git a/lib/hooks.php b/lib/hooks.php index e3a1776..e22fad7 100644 --- a/lib/hooks.php +++ b/lib/hooks.php @@ -64,7 +64,7 @@ public static function taskToJournalEntry($vtodo) { } try { $id = \OC_Calendar_Object::add($cid, $vcalendar->serialize()); - } catch (Exception $e) { + } catch (\Exception $e) { \OCP\Util::writeLog('journal', __METHOD__ . ', Error adding completed Task to calendar: "' . $cid . '" ' . $e->getMessage(), \OCP\Util::ERROR);