From 539f39a2d9353286480a7ffec10bf73c3aa1a7ed Mon Sep 17 00:00:00 2001 From: benweet Date: Sun, 28 Jul 2013 18:14:42 +0100 Subject: [PATCH] New partialRendering extension --- js/extensionMgr.js | 19 +++-- js/extensions/buttonHtmlCode.js | 4 +- js/extensions/buttonStat.js | 25 +++++-- js/extensions/dialogAbout.js | 3 + js/extensions/markdownExtra.js | 10 +-- js/extensions/partialRendering.js | 111 ++++++++++++++++++------------ js/extensions/scrollLink.js | 27 +++++--- js/extensions/toc.js | 23 ++++--- js/libs/Markdown.Extra.js | 5 ++ js/main.js | 1 + 10 files changed, 149 insertions(+), 79 deletions(-) diff --git a/js/extensionMgr.js b/js/extensionMgr.js index 8d784588..047280a2 100644 --- a/js/extensionMgr.js +++ b/js/extensionMgr.js @@ -153,22 +153,26 @@ define([ var onAsyncPreviewCallbackList = getExtensionCallbackList("onAsyncPreview"); // The number of times we expect tryFinished to be called var nbAsyncPreviewCallback = onAsyncPreviewCallbackList.length + 1; + var previewContentsElt = undefined; + var previewContentsJQElt = undefined; extensionMgr["onAsyncPreview"] = function() { logger.log("onAsyncPreview"); // Call onPreviewFinished callbacks when all async preview are finished var counter = 0; function tryFinished() { if(++counter === nbAsyncPreviewCallback) { - var html = ""; - $("#preview-contents > .preview-content").each(function() { - html += $(this).html(); + _.defer(function() { + var html = ""; + _.each(previewContentsElt.children, function(elt) { + html += elt.innerHTML; + }); + onPreviewFinished(utils.trim(html)); + logger.log("Preview time: " + (new Date() - extensionMgr.previewStartTime)); }); - onPreviewFinished(utils.trim(html)); - logger.log("Preview time: " + (new Date() - extensionMgr.previewStartTime)); } } // We assume images are loading in the preview - $("#preview-contents").waitForImages(tryFinished); + previewContentsJQElt.waitForImages(tryFinished); _.each(onAsyncPreviewCallbackList, function(asyncPreviewCallback) { asyncPreviewCallback(tryFinished); }); @@ -184,6 +188,9 @@ define([ } extensionMgr["onReady"] = function() { + previewContentsElt = document.getElementById('preview-contents'); + previewContentsJQElt = $(previewContentsElt); + // Create accordion in settings dialog _.chain(extensionList).sortBy(function(extension) { return extension.extensionName.toLowerCase(); diff --git a/js/extensions/buttonHtmlCode.js b/js/extensions/buttonHtmlCode.js index 7828fdb4..c8505eca 100644 --- a/js/extensions/buttonHtmlCode.js +++ b/js/extensions/buttonHtmlCode.js @@ -30,6 +30,7 @@ define([ selectedFileDesc = fileDesc; }; + var textareaElt = undefined; buttonHtmlCode.onPreviewFinished = function(html) { try { var htmlCode = _.template(buttonHtmlCode.config.template, { @@ -37,7 +38,7 @@ define([ documentMarkdown: selectedFileDesc.content, documentHTML: html }); - $("#input-html-code").val(htmlCode); + textareaElt.value = htmlCode; } catch(e) { extensionMgr.onError(e); @@ -46,6 +47,7 @@ define([ }; buttonHtmlCode.onReady = function() { + textareaElt = document.getElementById('input-html-code'); $(".action-html-code").click(function() { _.defer(function() { $("#input-html-code").each(function() { diff --git a/js/extensions/buttonStat.js b/js/extensions/buttonStat.js index 5c4d5f2e..524adbaa 100644 --- a/js/extensions/buttonStat.js +++ b/js/extensions/buttonStat.js @@ -43,12 +43,29 @@ define([ buttonStat.onCreatePreviewButton = function() { return $(_.template(buttonStatHTML, buttonStat.config)); }; + + var previewContentsElt = undefined; + var value1Elt = undefined; + var value2Elt = undefined; + var value3Elt = undefined; + buttonStat.onReady = function() { + previewContentsElt = document.getElementById('preview-contents'); + value1Elt = document.getElementById('span-stat-value1'); + value2Elt = document.getElementById('span-stat-value2'); + value3Elt = document.getElementById('span-stat-value3'); + }; buttonStat.onPreviewFinished = function() { - var text = $("#preview-contents").clone().find("script").remove().end().text(); - $("#span-stat-value1").text((text.match(new RegExp(buttonStat.config.value1, "g")) || []).length); - $("#span-stat-value2").text((text.match(new RegExp(buttonStat.config.value2, "g")) || []).length); - $("#span-stat-value3").text((text.match(new RegExp(buttonStat.config.value3, "g")) || []).length); + var previewContentsEltClone = previewContentsElt.cloneNode(true); + var scriptEltList = previewContentsEltClone.getElementsByTagName('script'); + for(var i = scriptEltList.length-1; i >= 0; i--) { + var scriptElt = scriptEltList[i]; + scriptElt.parentNode.removeChild(scriptElt); + } + var text = previewContentsEltClone.textContent; + value1Elt.textContent = (text.match(new RegExp(buttonStat.config.value1, "g")) || []).length; + value2Elt.textContent = (text.match(new RegExp(buttonStat.config.value2, "g")) || []).length; + value3Elt.textContent = (text.match(new RegExp(buttonStat.config.value3, "g")) || []).length; }; return buttonStat; diff --git a/js/extensions/dialogAbout.js b/js/extensions/dialogAbout.js index f476f5e8..8bc6e978 100644 --- a/js/extensions/dialogAbout.js +++ b/js/extensions/dialogAbout.js @@ -10,12 +10,14 @@ define([ var libraries = { "Bootstrap": "http://twitter.github.io/bootstrap/", + "crel": "https://github.com/KoryNunn/crel", "CSS Browser Selector": "https://github.com/rafaelp/css_browser_selector/", "Dropbox-js": "https://github.com/dropbox/dropbox-js", "FileSaver.js": "https://github.com/eligrey/FileSaver.js/", "Gatekeeper": "https://github.com/prose/gatekeeper", "Github.js": "https://github.com/michael/github", "Glyphicons": "http://glyphicons.com/", + "Highlight.js": "http://softwaremaniacs.org/soft/highlight/en/", "jGrowl": "https://github.com/stanlemon/jGrowl/", "jQuery": "http://jquery.com/", "jQuery Mouse Wheel Plugin": "https://github.com/brandonaaron/jquery-mousewheel", @@ -34,6 +36,7 @@ define([ var projects = { "StackEdit Download Proxy": "https://github.com/benweet/stackedit-download-proxy", + "StackEdit Picasa Proxy": "https://github.com/benweet/stackedit-picasa-proxy", "StackEdit SSH Proxy": "https://github.com/benweet/stackedit-ssh-proxy", "StackEdit Tumblr Proxy": "https://github.com/benweet/stackedit-tumblr-proxy", "StackEdit WordPress Proxy": "https://github.com/benweet/stackedit-wordpress-proxy", diff --git a/js/extensions/markdownExtra.js b/js/extensions/markdownExtra.js index 5c7369d5..1c8b79ed 100644 --- a/js/extensions/markdownExtra.js +++ b/js/extensions/markdownExtra.js @@ -1,10 +1,11 @@ define([ "jquery", + "underscore", "utils", "classes/Extension", "text!html/markdownExtraSettingsBlock.html", "libs/Markdown.Extra", -], function($, utils, Extension, markdownExtraSettingsBlockHTML) { +], function($, _, utils, Extension, markdownExtraSettingsBlockHTML) { var markdownExtra = new Extension("markdownExtra", "Markdown Extra", true); markdownExtra.settingsBlock = markdownExtraSettingsBlockHTML; @@ -50,9 +51,10 @@ define([ }; if(markdownExtra.config.highlighter == "highlight") { options.highlighter = "prettify"; + var previewContentsElt = document.getElementById('preview-contents'); editor.hooks.chain("onPreviewRefresh", function() { - $('.prettyprint').each(function(i, e) { - hljs.highlightBlock(e); + _.each(previewContentsElt.querySelectorAll('.prettyprint'), function(elt) { + hljs.highlightBlock(elt); }); }); } @@ -61,7 +63,7 @@ define([ editor.hooks.chain("onPreviewRefresh", prettyPrint); } Markdown.Extra.init(converter, options); - + // Store extensions list in converter for partialRendering converter.extraExtensions = markdownExtra.config.extensions; }; diff --git a/js/extensions/partialRendering.js b/js/extensions/partialRendering.js index 8978638d..51226c54 100644 --- a/js/extensions/partialRendering.js +++ b/js/extensions/partialRendering.js @@ -1,9 +1,9 @@ define([ - "jquery", "underscore", + "crel", "classes/Extension", "text!html/partialRenderingSettingsBlock.html", -], function($, _, Extension, partialRenderingSettingsBlockHTML) { +], function(_, crel, Extension, partialRenderingSettingsBlockHTML) { var partialRendering = new Extension("partialRendering", "Partial Rendering", true); partialRendering.settingsBlock = partialRenderingSettingsBlockHTML; @@ -14,12 +14,12 @@ define([ var linkDefinition = undefined; var sectionsToRemove = []; var modifiedSections = []; - var insertAfterSection = undefined; + var insertBeforeSection = undefined; var fileChanged = false; function updateSectionList(newSectionList, newLinkDefinition) { modifiedSections = []; sectionsToRemove = []; - insertAfterSection = undefined; + insertBeforeSection = undefined; // Render everything if file or linkDefinition changed if(fileChanged === true || linkDefinition != newLinkDefinition) { @@ -59,7 +59,7 @@ define([ var leftSections = sectionList.slice(0, leftIndex); modifiedSections = newSectionList.slice(leftIndex, newSectionList.length + rightIndex); var rightSections = sectionList.slice(sectionList.length + rightIndex, sectionList.length); - insertAfterSection = _.last(leftSections); + insertBeforeSection = _.first(rightSections); sectionsToRemove = sectionList.slice(leftIndex, sectionList.length + rightIndex); sectionList = leftSections.concat(modifiedSections).concat(rightSections); } @@ -72,17 +72,8 @@ define([ hasFootnotes = false; _.each(sectionListParam, function(text) { text += "\n\n"; - - // Strip link definitions - text = text.replace(/^```.*\n[\s\S]*?\n```|^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm, function(wholeMatch, link) { - if(link) { - newLinkDefinition += wholeMatch; - return ""; - } - return wholeMatch; - }); - // And footnotes eventually + // Strip footnotes if(doFootnotes) { text = text.replace(/^```.*\n[\s\S]*?\n```|\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g, function(wholeMatch, footnote) { if(footnote) { @@ -94,6 +85,15 @@ define([ }); } + // Strip link definitions + text = text.replace(/^```.*\n[\s\S]*?\n```|^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm, function(wholeMatch, link) { + if(link) { + newLinkDefinition += wholeMatch; + return ""; + } + return wholeMatch; + }); + // Skip space only sections if(/\S/.test(text)) { // Add section to the newSectionList @@ -107,56 +107,73 @@ define([ updateSectionList(newSectionList, newLinkDefinition); }; - var footnoteContainerElt = $('
'); - var footnoteList = {}; + var footnoteContainerElt = undefined; + var previewContentsElt = undefined; + var footnoteMap = {}; function refreshSections() { // Remove outdated sections _.each(sectionsToRemove, function(section) { - $("#wmd-preview-section-" + section.id).remove(); + var sectionElt = document.getElementById("wmd-preview-section-" + section.id); + previewContentsElt.removeChild(sectionElt); }); - var wmdPreviewElt = $("#wmd-preview"); - var insertAfterSectionElt = insertAfterSection === undefined ? wmdPreviewElt : $("#wmd-preview-section-" + insertAfterSection.id); + var wmdPreviewElt = document.getElementById("wmd-preview"); + var markdownEltList = Array.prototype.slice.call(wmdPreviewElt.childNodes); + wmdPreviewElt.innerHTML = ''; + var newSectionEltList = document.createDocumentFragment(); _.each(modifiedSections, function(section) { - var sectionElt = $('
'); - _.some(wmdPreviewElt.contents(), function(elt, index) { - elt = $(elt); - if(index !== 0 && elt.is(".wmd-title")) { - return true; + var sectionElt = crel('div', { + id: 'wmd-preview-section-' + section.id, + class: 'wmd-preview-section preview-content' + }); + var isFirst = true; + while(markdownEltList.length !== 0) { + var elt = markdownEltList[0]; + if(isFirst === false && /(^| )wmd-title($| )/.test(elt.className)) { + // Stop when encountered the next wmd-title + break; } - else if(elt.is("div.footnotes")) { - elt.find("ol > li").each(function() { - var footnoteElt = $(this).clone(); - var id = footnoteElt.attr("id").substring(3); - footnoteList[id] = footnoteElt; + isFirst = false; + if(elt.tagName == 'DIV' && elt.className == 'footnotes') { + _.each(elt.querySelectorAll("ol > li"), function(footnoteElt) { + // Store each footnote in our footnote map + var id = footnoteElt.id.substring(3); + footnoteMap[id] = footnoteElt; }); - elt.remove(); } else { - sectionElt.append(elt); + sectionElt.appendChild(elt); } - }); - insertAfterSectionElt.after(sectionElt); - insertAfterSectionElt = sectionElt; + markdownEltList.shift(); + }; + newSectionEltList.appendChild(sectionElt); }); + var insertBeforeElt = footnoteContainerElt; + if(insertBeforeSection !== undefined) { + insertBeforeElt = document.getElementById("wmd-preview-section-" + insertBeforeSection.id); + } + previewContentsElt.insertBefore(newSectionEltList, insertBeforeElt); // Rewrite footnotes in the footer and update footnote numbers - footnoteContainerElt.empty(); + footnoteContainerElt.innerHTML = ''; var usedFootnoteIds = []; if(hasFootnotes === true) { - var footnoteElts = $("
    "); - $("#preview-contents a.footnote").each(function(index) { - var id=$(this).text(index + 1).attr("id").substring(6); + var footnoteElts = crel('ol'); + _.each(previewContentsElt.querySelectorAll('a.footnote'), function(elt, index) { + elt.textContent = index + 1; + var id = elt.id.substring(6); usedFootnoteIds.push(id); - footnoteElts.append(footnoteList[id].clone()); + footnoteElts.appendChild(footnoteMap[id].cloneNode(true)); }); if(usedFootnoteIds.length > 0) { // Append the whole footnotes at the end of the document - footnoteContainerElt.html($('
    ').append("
    ").append(footnoteElts)); + footnoteContainerElt.appendChild(crel('div', { + class: 'footnotes' + }, crel('hr'), footnoteElts)); } // Keep used footnotes only in our map - footnoteList = _.pick(footnoteList, usedFootnoteIds); + footnoteMap = _.pick(footnoteMap, usedFootnoteIds); } } @@ -175,12 +192,18 @@ define([ }; partialRendering.onReady = function() { - $("#preview-contents").append(footnoteContainerElt); - $("#wmd-preview").hide(); + footnoteContainerElt = crel('div', { + id: 'wmd-preview-section-footnotes', + class: 'preview-content' + }); + previewContentsElt = document.getElementById("preview-contents"); + previewContentsElt.appendChild(footnoteContainerElt); }; + partialRendering.onFileSelected = function() { fileChanged = true; }; + partialRendering.onFileOpen = function() { if(converter.extraExtensions) { doFootnotes = _.some(converter.extraExtensions, function(extension) { diff --git a/js/extensions/scrollLink.js b/js/extensions/scrollLink.js index 85244449..2d897d66 100644 --- a/js/extensions/scrollLink.js +++ b/js/extensions/scrollLink.js @@ -15,6 +15,9 @@ define([ sectionList = sectionListParam; }; + var editorElt = undefined; + var previewElt = undefined; + var textareaElt = undefined; var mdSectionList = []; var htmlSectionList = []; function pxToFloat(px) { @@ -25,10 +28,7 @@ define([ var buildSections = _.debounce(function() { // Try to find Markdown sections by looking for titles - var editorElt = $("#wmd-input"); mdSectionList = []; - // This textarea is used to measure sections height - var textareaElt = $("#md-section-helper"); // It has to be the same width as wmd-input textareaElt.width(editorElt.width()); // Consider wmd-input top padding (will be used for 1st and last @@ -65,14 +65,14 @@ define([ }); // Try to find corresponding sections in the preview - var previewElt = $(".preview-container"); htmlSectionList = []; var htmlSectionOffset = 0; var previewScrollTop = previewElt.scrollTop(); // Each title element is a section separator - $("#preview-contents > .preview-content").children(".wmd-title").each(function() { + previewElt.find(".preview-content > .wmd-title").each(function() { + var titleElt = $(this); // Consider div scroll position and header element top margin - var newSectionOffset = $(this).position().top + previewScrollTop + pxToFloat($(this).css('margin-top')); + var newSectionOffset = titleElt.position().top + previewScrollTop + pxToFloat(titleElt.css('margin-top')); htmlSectionList.push({ startOffset: htmlSectionOffset, endOffset: newSectionOffset, @@ -100,9 +100,7 @@ define([ if(mdSectionList.length === 0 || mdSectionList.length !== htmlSectionList.length) { return; } - var editorElt = $("#wmd-input"); var editorScrollTop = editorElt.scrollTop(); - var previewElt = $(".preview-container"); var previewScrollTop = previewElt.scrollTop(); function animate(srcScrollTop, srcSectionList, destElt, destSectionList, currentDestScrollTop, callback) { // Find the section corresponding to the offset @@ -160,6 +158,12 @@ define([ }; scrollLink.onLayoutCreated = function() { + editorElt = $("#wmd-input"); + previewElt = $(".preview-container"); + + // This textarea is used to measure sections height + textareaElt = $("#md-section-helper"); + $(".preview-container").bind("keyup mouseup mousewheel", function() { isScrollPreview = true; isScrollEditor = false; @@ -172,19 +176,20 @@ define([ }); }; + var previewContentsElt = undefined; scrollLink.onEditorConfigure = function(editor) { + previewContentsElt = $("#preview-contents"); editor.getConverter().hooks.chain("postConversion", function(text) { // To avoid losing scrolling position before elements are fully // loaded - var previewElt = $("#preview-contents"); - previewElt.height(previewElt.height()); + previewContentsElt.height(previewContentsElt.height()); return text; }); }; scrollLink.onPreviewFinished = function() { // Now set the correct height - $("#preview-contents").height("auto"); + previewContentsElt.height("auto"); isScrollEditor = true; buildSections(); }; diff --git a/js/extensions/toc.js b/js/extensions/toc.js index 3de123d2..bbd0ede4 100644 --- a/js/extensions/toc.js +++ b/js/extensions/toc.js @@ -90,10 +90,11 @@ define([ } // Build the TOC + var previewContentsElt = undefined; function buildToc() { var anchorList = {}; function createAnchor(element) { - var id = element.prop("id") || utils.slugify(element.text()); + var id = element.id || utils.slugify(element.textContent); var anchor = id; var index = 0; while (_.has(anchorList, anchor)) { @@ -101,31 +102,35 @@ define([ } anchorList[anchor] = true; // Update the id of the element - element.prop("id", anchor); + element.id = anchor; return anchor; } var elementList = []; - $("#preview-contents > .preview-content").children("h1, h2, h3, h4, h5, h6").each(function() { - elementList.push(new TocElement($(this).prop("tagName"), createAnchor($(this)), $(this).text())); + _.each(previewContentsElt.querySelectorAll('.preview-content > .wmd-title'), function(elt) { + elementList.push(new TocElement(elt.tagName, createAnchor(elt), elt.textContent)); }); elementList = groupTags(elementList); return '
    \n
      \n' + elementList.join("") + '
    \n
    \n'; } toc.onEditorConfigure = function(editor) { - var tocExp = new RegExp("^" + toc.config.marker + "$", "g") + previewContentsElt = document.getElementById('preview-contents'); + var tocEltList = document.querySelectorAll('.table-of-contents'); + var tocExp = new RegExp("^" + toc.config.marker + "$", "g"); // Run TOC generation when conversion is finished directly on HTML editor.hooks.chain("onPreviewRefresh", function() { var htmlToc = buildToc(); // Replace toc paragraphs - $("#preview-contents p").each(function() { - if(tocExp.test($(this).html())) { - $(this).html(htmlToc); + _.each(previewContentsElt.getElementsByTagName('p'), function(elt) { + if(tocExp.test(elt.innerHTML)) { + elt.innerHTML = htmlToc; } }); // Add toc in the TOC button - $(".table-of-contents").html(htmlToc); + _.each(tocEltList, function(elt) { + elt.innerHTML = htmlToc; + }); }); }; diff --git a/js/libs/Markdown.Extra.js b/js/libs/Markdown.Extra.js index c85683e3..42123fc0 100644 --- a/js/libs/Markdown.Extra.js +++ b/js/libs/Markdown.Extra.js @@ -482,6 +482,9 @@ // Find and convert footnotes references. Markdown.Extra.prototype.doFootnotes = function(text) { var self = this; + if(self.isConvertingFootnote === true) { + return text; + } var footnoteCounter = 0; text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) { @@ -512,7 +515,9 @@ for(var i=0; i' diff --git a/js/main.js b/js/main.js index 38c6d2b6..0c8d349c 100644 --- a/js/main.js +++ b/js/main.js @@ -4,6 +4,7 @@ requirejs.config({ paths: { "jquery": "libs/jquery", "underscore": "libs/underscore", + "crel": "libs/crel", "jgrowl": "libs/jgrowl", "mousetrap": "libs/mousetrap", "toMarkdown": "libs/to-markdown",