diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index ad5f726f8..68ba4ed06 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -12,7 +12,7 @@ define(function() { * You should never remove the drive from this list. */ AppConfig.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard', - /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'form']; + /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'form', 'convert']; /* The registered only types are apps restricted to registered users. * You should never remove apps from this list unless you know what you're doing. The apps * listed here by default can't work without a user account. diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index f4c5f40cd..1919f0670 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -173,12 +173,16 @@ define([ return h('div.cp-md-toc', content).outerHTML; }; - DiffMd.render = function (md, sanitize, restrictedMd) { + var noHeadingId = false; + DiffMd.render = function (md, sanitize, restrictedMd, noId) { Marked.setOptions({ renderer: restrictedMd ? restrictedRenderer : renderer, }); + noHeadingId = noId; var r = Marked(md, { - sanitize: sanitize + sanitize: sanitize, + headerIds: !noId, + gfm: true, }); // Add Table of Content @@ -208,7 +212,11 @@ define([ }; restrictedRenderer.code = renderer.code; + var _heading = renderer.heading; renderer.heading = function (text, level) { + if (noHeadingId) { + return _heading.apply(this, arguments); + } var i = 0; var safeText = text.toLowerCase().replace(/[^\w]+/g, '-'); var getId = function () { diff --git a/www/convert/app-convert.less b/www/convert/app-convert.less new file mode 100644 index 000000000..c5813641a --- /dev/null +++ b/www/convert/app-convert.less @@ -0,0 +1,16 @@ +@import (reference) '../../customize/src/less2/include/framework.less'; +@import (reference) '../../customize/src/less2/include/sidebar-layout.less'; + +&.cp-app-convert { + + .framework_min_main( + @bg-color: @colortheme_apps[default], + ); + .sidebar-layout_main(); + + // body + display: flex; + flex-flow: column; + background-color: @cp_app-bg; + +} diff --git a/www/convert/file-crypto.js b/www/convert/file-crypto.js new file mode 100644 index 000000000..6a0c08816 --- /dev/null +++ b/www/convert/file-crypto.js @@ -0,0 +1,209 @@ +define([ + '/bower_components/tweetnacl/nacl-fast.min.js', +], function () { + var Nacl = window.nacl; + //var PARANOIA = true; + + var plainChunkLength = 128 * 1024; + var cypherChunkLength = 131088; + + var computeEncryptedSize = function (bytes, meta) { + var metasize = Nacl.util.decodeUTF8(JSON.stringify(meta)).length; + var chunks = Math.ceil(bytes / plainChunkLength); + return metasize + 18 + (chunks * 16) + bytes; + }; + + var encodePrefix = function (p) { + return [ + 65280, // 255 << 8 + 255, + ].map(function (n, i) { + return (p & n) >> ((1 - i) * 8); + }); + }; + var decodePrefix = function (A) { + return (A[0] << 8) | A[1]; + }; + + var slice = function (A) { + return Array.prototype.slice.call(A); + }; + + var createNonce = function () { + return new Uint8Array(new Array(24).fill(0)); + }; + + var increment = function (N) { + var l = N.length; + while (l-- > 1) { + /* our linter suspects this is unsafe because we lack types + but as long as this is only used on nonces, it should be safe */ + if (N[l] !== 255) { return void N[l]++; } // jshint ignore:line + if (l === 0) { throw new Error('E_NONCE_TOO_LARGE'); } + N[l] = 0; + } + }; + + var joinChunks = function (chunks) { + return new Blob(chunks); + }; + + var decrypt = function (u8, key, done, progress) { + var MAX = u8.length; + var _progress = function (offset) { + if (typeof(progress) !== 'function') { return; } + progress(Math.min(1, offset / MAX)); + }; + + var nonce = createNonce(); + var i = 0; + + var prefix = u8.subarray(0, 2); + var metadataLength = decodePrefix(prefix); + + var res = { + metadata: undefined, + }; + + var cancelled = false; + var cancel = function () { + cancelled = true; + }; + + var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength)); + + var metaChunk = Nacl.secretbox.open(metaBox, nonce, key); + increment(nonce); + + try { + res.metadata = JSON.parse(Nacl.util.encodeUTF8(metaChunk)); + } catch (e) { + return window.setTimeout(function () { + done('E_METADATA_DECRYPTION'); + }); + } + + if (!res.metadata) { + return void setTimeout(function () { + done('NO_METADATA'); + }); + } + + var takeChunk = function (cb) { + setTimeout(function () { + var start = i * cypherChunkLength + 2 + metadataLength; + var end = start + cypherChunkLength; + i++; + var box = new Uint8Array(u8.subarray(start, end)); + + // decrypt the chunk + var plaintext = Nacl.secretbox.open(box, nonce, key); + increment(nonce); + + if (!plaintext) { return cb('DECRYPTION_ERROR'); } + + _progress(end); + cb(void 0, plaintext); + }); + }; + + var chunks = []; + + var again = function () { + if (cancelled) { return; } + takeChunk(function (e, plaintext) { + if (e) { + return setTimeout(function () { + done(e); + }); + } + if (plaintext) { + if ((2 + metadataLength + i * cypherChunkLength) < u8.length) { // not done + chunks.push(plaintext); + return setTimeout(again); + } + chunks.push(plaintext); + res.content = joinChunks(chunks); + return done(void 0, res); + } + done('UNEXPECTED_ENDING'); + }); + }; + + again(); + + return { + cancel: cancel + }; + }; + + // metadata + /* { filename: 'raccoon.jpg', type: 'image/jpeg' } */ + var encrypt = function (u8, metadata, key) { + var nonce = createNonce(); + + // encode metadata + var plaintext = Nacl.util.decodeUTF8(JSON.stringify(metadata)); + + // if metadata is too large, drop the thumbnail. + if (plaintext.length > 65535) { + var temp = JSON.parse(JSON.stringify(metadata)); + delete metadata.thumbnail; + plaintext = Nacl.util.decodeUTF8(JSON.stringify(temp)); + } + + var i = 0; + + var state = 0; + var next = function (cb) { + if (state === 2) { return void setTimeout(cb); } + + var start; + var end; + var part; + var box; + + if (state === 0) { // metadata... + part = new Uint8Array(plaintext); + box = Nacl.secretbox(part, nonce, key); + increment(nonce); + + if (box.length > 65535) { + return void cb('METADATA_TOO_LARGE'); + } + var prefixed = new Uint8Array(encodePrefix(box.length) + .concat(slice(box))); + state++; + + return void setTimeout(function () { + cb(void 0, prefixed); + }); + } + + // encrypt the rest of the file... + start = i * plainChunkLength; + end = start + plainChunkLength; + + part = u8.subarray(start, end); + box = Nacl.secretbox(part, nonce, key); + increment(nonce); + i++; + + // regular data is done + if (i * plainChunkLength >= u8.length) { state = 2; } + + setTimeout(function () { + cb(void 0, box); + }); + }; + + return next; + }; + + return { + decrypt: decrypt, + encrypt: encrypt, + joinChunks: joinChunks, + computeEncryptedSize: computeEncryptedSize, + }; +}); diff --git a/www/convert/index.html b/www/convert/index.html new file mode 100644 index 000000000..96a3cce86 --- /dev/null +++ b/www/convert/index.html @@ -0,0 +1,12 @@ + + + + CryptPad + + + + + + + + diff --git a/www/convert/inner.html b/www/convert/inner.html new file mode 100644 index 000000000..206b85722 --- /dev/null +++ b/www/convert/inner.html @@ -0,0 +1,18 @@ + + + + + + + + +
+
+ + + diff --git a/www/convert/inner.js b/www/convert/inner.js new file mode 100644 index 000000000..d8eca9af2 --- /dev/null +++ b/www/convert/inner.js @@ -0,0 +1,259 @@ +define([ + 'jquery', + '/api/config', + '/bower_components/chainpad-crypto/crypto.js', + '/common/toolbar.js', + '/bower_components/nthen/index.js', + '/common/sframe-common.js', + '/common/hyperscript.js', + '/customize/messages.js', + '/common/common-interface.js', + '/common/common-util.js', + + '/bower_components/file-saver/FileSaver.min.js', + 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', + 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', + 'less!/convert/app-convert.less', +], function ( + $, + ApiConfig, + Crypto, + Toolbar, + nThen, + SFCommon, + h, + Messages, + UI, + Util + ) +{ + var APP = {}; + + var common; + var sFrameChan; + + var debug = console.debug; + + var x2tReady = Util.mkEvent(true); + var x2tInitialized = false; + var x2tInit = function(x2t) { + debug("x2t mount"); + // x2t.FS.mount(x2t.MEMFS, {} , '/'); + x2t.FS.mkdir('/working'); + x2t.FS.mkdir('/working/media'); + x2t.FS.mkdir('/working/fonts'); + x2tInitialized = true; + x2tReady.fire(); + //fetchFonts(x2t); + debug("x2t mount done"); + }; + var getX2t = function (cb) { + // XXX require http headers on firefox... + require(['/common/onlyoffice/x2t/x2t.js'], function() { // FIXME why does this fail without an access-control-allow-origin header? + var x2t = window.Module; + x2t.run(); + if (x2tInitialized) { + debug("x2t runtime already initialized"); + return void x2tReady.reg(function () { + cb(x2t); + }); + } + + x2t.onRuntimeInitialized = function() { + debug("x2t in runtime initialized"); + // Init x2t js module + x2tInit(x2t); + x2tReady.reg(function () { + cb(x2t); + }); + }; + }); + }; + /* + Converting Data + + This function converts a data in a specific format to the outputformat + The filename extension needs to represent the input format + Example: fileName=cryptpad.bin outputFormat=xlsx + */ + var getFormatId = function (ext) { + // Sheets + if (ext === 'xlsx') { return 257; } + if (ext === 'xls') { return 258; } + if (ext === 'ods') { return 259; } + if (ext === 'csv') { return 260; } + if (ext === 'pdf') { return 513; } + // Docs + if (ext === 'docx') { return 65; } + if (ext === 'doc') { return 66; } + if (ext === 'odt') { return 67; } + if (ext === 'txt') { return 69; } + if (ext === 'html') { return 70; } + + // Slides + if (ext === 'pptx') { return 129; } + if (ext === 'ppt') { return 130; } + if (ext === 'odp') { return 131; } + + return; + }; + var getFromId = function (ext) { + var id = getFormatId(ext); + if (!id) { return ''; } + return ''+id+''; + }; + var getToId = function (ext) { + var id = getFormatId(ext); + if (!id) { return ''; } + return ''+id+''; + }; + var x2tConvertDataInternal = function(x2t, data, fileName, outputFormat) { + debug("Converting Data for " + fileName + " to " + outputFormat); + + var inputFormat = fileName.split('.').pop(); + + x2t.FS.writeFile('/working/' + fileName, data); + var params = "" + + "" + + "/working/" + fileName + "" + + "/working/" + fileName + "." + outputFormat + "" + + getFromId(inputFormat) + + getToId(outputFormat) + + "false" + + ""; + // writing params file to mounted working disk (in memory) + x2t.FS.writeFile('/working/params.xml', params); + // running conversion + x2t.ccall("runX2T", ["number"], ["string"], ["/working/params.xml"]); + // reading output file from working disk (in memory) + var result; + try { + result = x2t.FS.readFile('/working/' + fileName + "." + outputFormat); + } catch (e) { + console.error(e, x2t.FS); + debug("Failed reading converted file"); + UI.warn(Messages.error); + return ""; + } + return result; + }; + var x2tConverter = function (typeSrc, typeTarget) { + return function (data, name, cb) { + getX2t(function (x2t) { + if (typeSrc === 'ods') { + data = x2tConvertDataInternal(x2t, data, name, 'xlsx'); + name += '.xlsx'; + } + if (typeSrc === 'odt') { + data = x2tConvertDataInternal(x2t, data, name, 'docx'); + name += '.docx'; + } + if (typeSrc === 'odp') { + data = x2tConvertDataInternal(x2t, data, name, 'pptx'); + name += '.pptx'; + } + cb(x2tConvertDataInternal(x2t, data, name, typeTarget)); + }); + }; + }; + + var CONVERTERS = { + xlsx: { + //pdf: x2tConverter('xlsx', 'pdf'), + ods: x2tConverter('xlsx', 'ods'), + bin: x2tConverter('xlsx', 'bin'), + }, + ods: { + //pdf: x2tConverter('ods', 'pdf'), + xlsx: x2tConverter('ods', 'xlsx'), + bin: x2tConverter('ods', 'bin'), + }, + odt: { + docx: x2tConverter('odt', 'docx'), + txt: x2tConverter('odt', 'txt'), + bin: x2tConverter('odt', 'bin'), + }, + docx: { + odt: x2tConverter('docx', 'odt'), + txt: x2tConverter('docx', 'txt'), + bin: x2tConverter('docx', 'bin'), + }, + txt: { + odt: x2tConverter('txt', 'odt'), + docx: x2tConverter('txt', 'docx'), + bin: x2tConverter('txt', 'bin'), + }, + odp: { + pptx: x2tConverter('odp', 'pptx'), + bin: x2tConverter('odp', 'bin'), + }, + pptx: { + odp: x2tConverter('pptx', 'odp'), + bin: x2tConverter('pptx', 'bin'), + }, + }; + + Messages.convertPage = "Convert"; // XXX + Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward."; + + var createToolbar = function () { + var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications']; + var configTb = { + displayed: displayed, + sfCommon: common, + $container: APP.$toolbar, + pageTitle: Messages.convertPage, + metadataMgr: common.getMetadataMgr(), + }; + APP.toolbar = Toolbar.create(configTb); + APP.toolbar.$rightside.hide(); + }; + + nThen(function (waitFor) { + $(waitFor(UI.addLoadingScreen)); + SFCommon.create(waitFor(function (c) { APP.common = common = c; })); + }).nThen(function (waitFor) { + APP.$container = $('#cp-sidebarlayout-container'); + APP.$toolbar = $('#cp-toolbar'); + APP.$leftside = $('
', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container); + APP.$rightside = $('
', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container); + sFrameChan = common.getSframeChannel(); + sFrameChan.onReady(waitFor()); + }).nThen(function (/*waitFor*/) { + createToolbar(); + + var hint = h('p.cp-convert-hint', Messages.convert_hint); + + var picker = h('input', { + type: 'file' + }); + APP.$rightside.append([hint, picker]); + + $(picker).on('change', function () { + var file = picker.files[0]; + var name = file && file.name; + var reader = new FileReader(); + var parsed = file && file.name && /.+\.([^.]+)$/.exec(file.name); + var ext = parsed && parsed[1]; + reader.onload = function (e) { + if (CONVERTERS[ext]) { + Object.keys(CONVERTERS[ext]).forEach(function (to) { + var button = h('button.btn', to); + $(button).click(function () { + CONVERTERS[ext][to](new Uint8Array(e.target.result), name, function (a) { + var n = name.slice(0, -ext.length) + to; + var blob = new Blob([a], {type: "application/bin;charset=utf-8"}); + window.saveAs(blob, n); + }); + + }).appendTo(APP.$rightside); + }); + } + }; + reader.readAsArrayBuffer(file, 'application/octet-stream'); + }); + + UI.removeLoadingScreen(); + + }); +}); diff --git a/www/convert/main.js b/www/convert/main.js new file mode 100644 index 000000000..236290b13 --- /dev/null +++ b/www/convert/main.js @@ -0,0 +1,28 @@ +// Load #1, load as little as possible because we are in a race to get the loading screen up. +define([ + '/bower_components/nthen/index.js', + '/api/config', + '/common/dom-ready.js', + '/common/sframe-common-outer.js' +], function (nThen, ApiConfig, DomReady, SFCommonO) { + + // Loaded in load #2 + nThen(function (waitFor) { + DomReady.onReady(waitFor()); + }).nThen(function (waitFor) { + SFCommonO.initIframe(waitFor, true); + }).nThen(function (/*waitFor*/) { + var category; + if (window.location.hash) { + category = window.location.hash.slice(1); + window.location.hash = ''; + } + var addData = function (obj) { + if (category) { obj.category = category; } + }; + SFCommonO.start({ + noRealtime: true, + addData: addData + }); + }); +}); diff --git a/www/form/export.js b/www/form/export.js new file mode 100644 index 000000000..e29c6dd87 --- /dev/null +++ b/www/form/export.js @@ -0,0 +1,60 @@ +define([ + '/common/common-util.js', + '/customize/messages.js' +], function (Util, Messages) { + var Export = {}; + + var escapeCSV = function (v) { + if (!/("|,)/.test(v)) { + return v || ''; + } + var value = ''; + var vv = (v || '').replaceAll('"', '""'); + value += '"' + vv + '"'; + return value; + }; + Export.results = function (content, answers, TYPES) { + console.log(content, answers, TYPES); + if (!content || !content.form) { return; } + var csv = ""; + var form = content.form; + + var questions = Object.keys(form).map(function (key) { + var obj = form[key]; + if (!obj) { return; } + return obj.q || Messages.form_default; + }).filter(Boolean); + questions.unshift(Messages.form_poll_time); // "Time" + questions.unshift(Messages.share_formView); // "Participant" + + questions.forEach(function (v, i) { + if (i) { csv += ','; } + csv += escapeCSV(v); + }); + + Object.keys(answers || {}).forEach(function (key) { + var obj = answers[key]; + csv += '\n'; + var time = new Date(obj.time).toISOString(); + var msg = obj.msg || {}; + var user = msg._userdata; + csv += escapeCSV(time); + csv += ',' + escapeCSV(user.name || Messages.anonymous); + Object.keys(form).forEach(function (key) { + csv += ',' + escapeCSV(String(msg[key])); + }); + }); + console.log(csv); + return csv; + }; + + Export.main = function (content, cb) { + var json = Util.clone(content || {}); + delete json.answers; + cb(new Blob([JSON.stringify(json, 0, 2)], { + type: 'application/json;charset=utf-8' + })); + }; + + return Export; +}); diff --git a/www/form/inner.js b/www/form/inner.js index 80c50a0d6..123e86d5b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -4,6 +4,7 @@ define([ '/bower_components/chainpad-crypto/crypto.js', '/common/sframe-app-framework.js', '/common/toolbar.js', + '/form/export.js', '/bower_components/nthen/index.js', '/common/sframe-common.js', '/common/common-util.js', @@ -30,6 +31,8 @@ define([ 'cm/mode/gfm/gfm', 'css!cm/lib/codemirror.css', + '/bower_components/file-saver/FileSaver.min.js', + 'css!/bower_components/codemirror/lib/codemirror.css', 'css!/bower_components/codemirror/addon/dialog/dialog.css', 'css!/bower_components/codemirror/addon/fold/foldgutter.css', @@ -42,6 +45,7 @@ define([ Crypto, Framework, Toolbar, + Exporter, nThen, SFCommon, Util, @@ -1656,9 +1660,22 @@ define([ var controls = h('div.cp-form-creator-results-controls'); var $controls = $(controls).appendTo($container); + Messages.form_exportCSV = "Export results as CSV"; + var exportButton = h('button.btn.btn-secondary', Messages.form_exportCSV); + var exportCSV = h('div.cp-form-creator-results-export', exportButton); + $(exportCSV).appendTo($container); var results = h('div.cp-form-creator-results-content'); var $results = $(results).appendTo($container); + $(exportButton).click(function () { + var csv = Exporter.results(content, answers, TYPES); + if (!csv) { return void UI.warn(Messages.error); } + var suggestion = APP.framework._.title.suggestTitle('cryptpad-document'); + var title = Util.fixFileName(suggestion) + '.csv'; + window.saveAs(new Blob([csv], { + type: 'text/csv' + }), title); + }); var summary = true; var form = content.form; @@ -2315,6 +2332,7 @@ define([ var andThen = function (framework) { framework.start(); + APP.framework = framework; var evOnChange = Util.mkEvent(); var content = {}; @@ -2739,6 +2757,17 @@ define([ return content; }); + framework.setFileImporter({ accept: ['.json'] }, function (newContent) { + var parsed = JSON.parse(newContent || {}); + parsed.answers = content.answers; + return parsed; + }); + + framework.setFileExporter(['.json'], function(cb, ext) { + Exporter.main(content, cb, ext); + }, true); + + }; Framework.create({ diff --git a/www/kanban/export.js b/www/kanban/export.js index 9ee770ac7..694724cf4 100644 --- a/www/kanban/export.js +++ b/www/kanban/export.js @@ -13,6 +13,73 @@ define([ })); }; + module.import = function (content) { + // Import from Trello + + var c = { + data: {}, + items: {}, + list: [] + }; + + var colorMap = { + red: 'color1', + orange: 'color2', + yellow: 'color3', + lime: 'color4', + green: 'color5', + sky: 'color6', + blue: 'color7', + purple: 'color8', + pink: 'color9', + black: 'nocolor' + }; + content.cards.forEach(function (obj, i) { + var tags; + var color; + if (Array.isArray(obj.labels)) { + obj.labels.forEach(function (l) { + if (!color) { + color = colorMap[l.color] || ''; + } + if (l.name) { + tags = tags || []; + var n = l.name.toLowerCase().trim(); + if (tags.indexOf(n) === -1) { tags.push(n); } + } + }); + } + c.items[(i+1)] = { + id: (i+1), + title: obj.name, + body: obj.desc, + color: color, + tags: tags + }; + }); + + var id = 1; + content.lists.forEach(function (obj) { + var _id = obj.id; + var cards = []; + content.cards.forEach(function (card, i) { + if (card.idList === _id) { + cards.push(i+1); + } + }); + c.data[id] = { + id: id, + title: obj.name, + item: cards + }; + c.list.push(id); + + id++; + }); + + return c; + }; + return module; }); diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 76a9fe1cc..8eb090ffc 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -18,6 +18,7 @@ define([ '/bower_components/marked/marked.min.js', 'cm/lib/codemirror', '/kanban/jkanban_cp.js', + '/kanban/export.js', 'cm/mode/gfm/gfm', 'cm/addon/edit/closebrackets', @@ -50,7 +51,8 @@ define([ ChainPad, Marked, CodeMirror, - jKanban) + jKanban, + Export) { var verbose = function (x) { console.log(x); }; @@ -1060,6 +1062,11 @@ define([ var parsed; try { parsed = JSON.parse(content); } catch (e) { return void console.error(e); } + + if (parsed && parsed.id && parsed.lists && parsed.cards) { + return { content: Export.import(parsed) }; + } + return { content: parsed }; }); diff --git a/www/lib/turndown.browser.umd.js b/www/lib/turndown.browser.umd.js new file mode 100644 index 000000000..9b8c40d13 --- /dev/null +++ b/www/lib/turndown.browser.umd.js @@ -0,0 +1,959 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.TurndownService = factory()); +}(this, (function () { 'use strict'; + + function extend (destination) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + for (var key in source) { + if (source.hasOwnProperty(key)) destination[key] = source[key]; + } + } + return destination + } + + function repeat (character, count) { + return Array(count + 1).join(character) + } + + var blockElements = [ + 'ADDRESS', 'ARTICLE', 'ASIDE', 'AUDIO', 'BLOCKQUOTE', 'BODY', 'CANVAS', + 'CENTER', 'DD', 'DIR', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE', + 'FOOTER', 'FORM', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', + 'HGROUP', 'HR', 'HTML', 'ISINDEX', 'LI', 'MAIN', 'MENU', 'NAV', 'NOFRAMES', + 'NOSCRIPT', 'OL', 'OUTPUT', 'P', 'PRE', 'SECTION', 'TABLE', 'TBODY', 'TD', + 'TFOOT', 'TH', 'THEAD', 'TR', 'UL' + ]; + + function isBlock (node) { + return is(node, blockElements) + } + + var voidElements = [ + 'AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', + 'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR' + ]; + + function isVoid (node) { + return is(node, voidElements) + } + + function hasVoid (node) { + return has(node, voidElements) + } + + var meaningfulWhenBlankElements = [ + 'A', 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TH', 'TD', 'IFRAME', 'SCRIPT', + 'AUDIO', 'VIDEO' + ]; + + function isMeaningfulWhenBlank (node) { + return is(node, meaningfulWhenBlankElements) + } + + function hasMeaningfulWhenBlank (node) { + return has(node, meaningfulWhenBlankElements) + } + + function is (node, tagNames) { + return tagNames.indexOf(node.nodeName) >= 0 + } + + function has (node, tagNames) { + return ( + node.getElementsByTagName && + tagNames.some(function (tagName) { + return node.getElementsByTagName(tagName).length + }) + ) + } + + var rules = {}; + + rules.paragraph = { + filter: 'p', + + replacement: function (content) { + return '\n\n' + content + '\n\n' + } + }; + + rules.lineBreak = { + filter: 'br', + + replacement: function (content, node, options) { + return options.br + '\n' + } + }; + + rules.heading = { + filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + + replacement: function (content, node, options) { + var hLevel = Number(node.nodeName.charAt(1)); + + if (options.headingStyle === 'setext' && hLevel < 3) { + var underline = repeat((hLevel === 1 ? '=' : '-'), content.length); + return ( + '\n\n' + content + '\n' + underline + '\n\n' + ) + } else { + return '\n\n' + repeat('#', hLevel) + ' ' + content + '\n\n' + } + } + }; + + rules.blockquote = { + filter: 'blockquote', + + replacement: function (content) { + content = content.replace(/^\n+|\n+$/g, ''); + content = content.replace(/^/gm, '> '); + return '\n\n' + content + '\n\n' + } + }; + + rules.list = { + filter: ['ul', 'ol'], + + replacement: function (content, node) { + var parent = node.parentNode; + if (parent.nodeName === 'LI' && parent.lastElementChild === node) { + return '\n' + content + } else { + return '\n\n' + content + '\n\n' + } + } + }; + + rules.listItem = { + filter: 'li', + + replacement: function (content, node, options) { + content = content + .replace(/^\n+/, '') // remove leading newlines + .replace(/\n+$/, '\n') // replace trailing newlines with just a single one + .replace(/\n/gm, '\n '); // indent + var prefix = options.bulletListMarker + ' '; + var parent = node.parentNode; + if (parent.nodeName === 'OL') { + var start = parent.getAttribute('start'); + var index = Array.prototype.indexOf.call(parent.children, node); + prefix = (start ? Number(start) + index : index + 1) + '. '; + } + return ( + prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '') + ) + } + }; + + rules.indentedCodeBlock = { + filter: function (node, options) { + return ( + options.codeBlockStyle === 'indented' && + node.nodeName === 'PRE' && + node.firstChild && + node.firstChild.nodeName === 'CODE' + ) + }, + + replacement: function (content, node, options) { + return ( + '\n\n ' + + node.firstChild.textContent.replace(/\n/g, '\n ') + + '\n\n' + ) + } + }; + + rules.fencedCodeBlock = { + filter: function (node, options) { + return ( + options.codeBlockStyle === 'fenced' && + node.nodeName === 'PRE' && + node.firstChild && + node.firstChild.nodeName === 'CODE' + ) + }, + + replacement: function (content, node, options) { + var className = node.firstChild.getAttribute('class') || ''; + var language = (className.match(/language-(\S+)/) || [null, ''])[1]; + var code = node.firstChild.textContent; + + var fenceChar = options.fence.charAt(0); + var fenceSize = 3; + var fenceInCodeRegex = new RegExp('^' + fenceChar + '{3,}', 'gm'); + + var match; + while ((match = fenceInCodeRegex.exec(code))) { + if (match[0].length >= fenceSize) { + fenceSize = match[0].length + 1; + } + } + + var fence = repeat(fenceChar, fenceSize); + + return ( + '\n\n' + fence + language + '\n' + + code.replace(/\n$/, '') + + '\n' + fence + '\n\n' + ) + } + }; + + rules.horizontalRule = { + filter: 'hr', + + replacement: function (content, node, options) { + return '\n\n' + options.hr + '\n\n' + } + }; + + rules.inlineLink = { + filter: function (node, options) { + return ( + options.linkStyle === 'inlined' && + node.nodeName === 'A' && + node.getAttribute('href') + ) + }, + + replacement: function (content, node) { + var href = node.getAttribute('href'); + var title = cleanAttribute(node.getAttribute('title')); + if (title) title = ' "' + title + '"'; + return '[' + content + '](' + href + title + ')' + } + }; + + rules.referenceLink = { + filter: function (node, options) { + return ( + options.linkStyle === 'referenced' && + node.nodeName === 'A' && + node.getAttribute('href') + ) + }, + + replacement: function (content, node, options) { + var href = node.getAttribute('href'); + var title = cleanAttribute(node.getAttribute('title')); + if (title) title = ' "' + title + '"'; + var replacement; + var reference; + + switch (options.linkReferenceStyle) { + case 'collapsed': + replacement = '[' + content + '][]'; + reference = '[' + content + ']: ' + href + title; + break + case 'shortcut': + replacement = '[' + content + ']'; + reference = '[' + content + ']: ' + href + title; + break + default: + var id = this.references.length + 1; + replacement = '[' + content + '][' + id + ']'; + reference = '[' + id + ']: ' + href + title; + } + + this.references.push(reference); + return replacement + }, + + references: [], + + append: function (options) { + var references = ''; + if (this.references.length) { + references = '\n\n' + this.references.join('\n') + '\n\n'; + this.references = []; // Reset references + } + return references + } + }; + + rules.emphasis = { + filter: ['em', 'i'], + + replacement: function (content, node, options) { + if (!content.trim()) return '' + return options.emDelimiter + content + options.emDelimiter + } + }; + + rules.strong = { + filter: ['strong', 'b'], + + replacement: function (content, node, options) { + if (!content.trim()) return '' + return options.strongDelimiter + content + options.strongDelimiter + } + }; + + rules.code = { + filter: function (node) { + var hasSiblings = node.previousSibling || node.nextSibling; + var isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings; + + return node.nodeName === 'CODE' && !isCodeBlock + }, + + replacement: function (content) { + if (!content.trim()) return '' + + var delimiter = '`'; + var leadingSpace = ''; + var trailingSpace = ''; + var matches = content.match(/`+/gm); + if (matches) { + if (/^`/.test(content)) leadingSpace = ' '; + if (/`$/.test(content)) trailingSpace = ' '; + while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + '`'; + } + + return delimiter + leadingSpace + content + trailingSpace + delimiter + } + }; + + rules.image = { + filter: 'img', + + replacement: function (content, node) { + var alt = cleanAttribute(node.getAttribute('alt')); + var src = node.getAttribute('src') || ''; + var title = cleanAttribute(node.getAttribute('title')); + var titlePart = title ? ' "' + title + '"' : ''; + return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : '' + } + }; + + function cleanAttribute (attribute) { + return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '' + } + + /** + * Manages a collection of rules used to convert HTML to Markdown + */ + + function Rules (options) { + this.options = options; + this._keep = []; + this._remove = []; + + this.blankRule = { + replacement: options.blankReplacement + }; + + this.keepReplacement = options.keepReplacement; + + this.defaultRule = { + replacement: options.defaultReplacement + }; + + this.array = []; + for (var key in options.rules) this.array.push(options.rules[key]); + } + + Rules.prototype = { + add: function (key, rule) { + this.array.unshift(rule); + }, + + keep: function (filter) { + this._keep.unshift({ + filter: filter, + replacement: this.keepReplacement + }); + }, + + remove: function (filter) { + this._remove.unshift({ + filter: filter, + replacement: function () { + return '' + } + }); + }, + + forNode: function (node) { + if (node.isBlank) return this.blankRule + var rule; + + if ((rule = findRule(this.array, node, this.options))) return rule + if ((rule = findRule(this._keep, node, this.options))) return rule + if ((rule = findRule(this._remove, node, this.options))) return rule + + return this.defaultRule + }, + + forEach: function (fn) { + for (var i = 0; i < this.array.length; i++) fn(this.array[i], i); + } + }; + + function findRule (rules, node, options) { + for (var i = 0; i < rules.length; i++) { + var rule = rules[i]; + if (filterValue(rule, node, options)) return rule + } + return void 0 + } + + function filterValue (rule, node, options) { + var filter = rule.filter; + if (typeof filter === 'string') { + if (filter === node.nodeName.toLowerCase()) return true + } else if (Array.isArray(filter)) { + if (filter.indexOf(node.nodeName.toLowerCase()) > -1) return true + } else if (typeof filter === 'function') { + if (filter.call(rule, node, options)) return true + } else { + throw new TypeError('`filter` needs to be a string, array, or function') + } + } + + /** + * The collapseWhitespace function is adapted from collapse-whitespace + * by Luc Thevenard. + * + * The MIT License (MIT) + * + * Copyright (c) 2014 Luc Thevenard + * + * 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. + */ + + /** + * collapseWhitespace(options) removes extraneous whitespace from an the given element. + * + * @param {Object} options + */ + function collapseWhitespace (options) { + var element = options.element; + var isBlock = options.isBlock; + var isVoid = options.isVoid; + var isPre = options.isPre || function (node) { + return node.nodeName === 'PRE' + }; + + if (!element.firstChild || isPre(element)) return + + var prevText = null; + var prevVoid = false; + + var prev = null; + var node = next(prev, element, isPre); + + while (node !== element) { + if (node.nodeType === 3 || node.nodeType === 4) { // Node.TEXT_NODE or Node.CDATA_SECTION_NODE + var text = node.data.replace(/[ \r\n\t]+/g, ' '); + + if ((!prevText || / $/.test(prevText.data)) && + !prevVoid && text[0] === ' ') { + text = text.substr(1); + } + + // `text` might be empty at this point. + if (!text) { + node = remove(node); + continue + } + + node.data = text; + + prevText = node; + } else if (node.nodeType === 1) { // Node.ELEMENT_NODE + if (isBlock(node) || node.nodeName === 'BR') { + if (prevText) { + prevText.data = prevText.data.replace(/ $/, ''); + } + + prevText = null; + prevVoid = false; + } else if (isVoid(node)) { + // Avoid trimming space around non-block, non-BR void elements. + prevText = null; + prevVoid = true; + } + } else { + node = remove(node); + continue + } + + var nextNode = next(prev, node, isPre); + prev = node; + node = nextNode; + } + + if (prevText) { + prevText.data = prevText.data.replace(/ $/, ''); + if (!prevText.data) { + remove(prevText); + } + } + } + + /** + * remove(node) removes the given node from the DOM and returns the + * next node in the sequence. + * + * @param {Node} node + * @return {Node} node + */ + function remove (node) { + var next = node.nextSibling || node.parentNode; + + node.parentNode.removeChild(node); + + return next + } + + /** + * next(prev, current, isPre) returns the next node in the sequence, given the + * current and previous nodes. + * + * @param {Node} prev + * @param {Node} current + * @param {Function} isPre + * @return {Node} + */ + function next (prev, current, isPre) { + if ((prev && prev.parentNode === current) || isPre(current)) { + return current.nextSibling || current.parentNode + } + + return current.firstChild || current.nextSibling || current.parentNode + } + + /* + * Set up window for Node.js + */ + + var root = (typeof window !== 'undefined' ? window : {}); + + /* + * Parsing HTML strings + */ + + function canParseHTMLNatively () { + var Parser = root.DOMParser; + var canParse = false; + + // Adapted from https://gist.github.com/1129031 + // Firefox/Opera/IE throw errors on unsupported types + try { + // WebKit returns null on unsupported types + if (new Parser().parseFromString('', 'text/html')) { + canParse = true; + } + } catch (e) {} + + return canParse + } + + function createHTMLParser () { + var Parser = function () {}; + + { + if (shouldUseActiveX()) { + Parser.prototype.parseFromString = function (string) { + var doc = new window.ActiveXObject('htmlfile'); + doc.designMode = 'on'; // disable on-page scripts + doc.open(); + doc.write(string); + doc.close(); + return doc + }; + } else { + Parser.prototype.parseFromString = function (string) { + var doc = document.implementation.createHTMLDocument(''); + doc.open(); + doc.write(string); + doc.close(); + return doc + }; + } + } + return Parser + } + + function shouldUseActiveX () { + var useActiveX = false; + try { + document.implementation.createHTMLDocument('').open(); + } catch (e) { + if (window.ActiveXObject) useActiveX = true; + } + return useActiveX + } + + var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser(); + + function RootNode (input) { + var root; + if (typeof input === 'string') { + var doc = htmlParser().parseFromString( + // DOM parsers arrange elements in the and . + // Wrapping in a custom element ensures elements are reliably arranged in + // a single element. + '' + input + '', + 'text/html' + ); + root = doc.getElementById('turndown-root'); + } else { + root = input.cloneNode(true); + } + collapseWhitespace({ + element: root, + isBlock: isBlock, + isVoid: isVoid + }); + + return root + } + + var _htmlParser; + function htmlParser () { + _htmlParser = _htmlParser || new HTMLParser(); + return _htmlParser + } + + function Node (node) { + node.isBlock = isBlock(node); + node.isCode = node.nodeName.toLowerCase() === 'code' || node.parentNode.isCode; + node.isBlank = isBlank(node); + node.flankingWhitespace = flankingWhitespace(node); + return node + } + + function isBlank (node) { + return ( + !isVoid(node) && + !isMeaningfulWhenBlank(node) && + /^\s*$/i.test(node.textContent) && + !hasVoid(node) && + !hasMeaningfulWhenBlank(node) + ) + } + + function flankingWhitespace (node) { + var leading = ''; + var trailing = ''; + + if (!node.isBlock) { + var hasLeading = /^\s/.test(node.textContent); + var hasTrailing = /\s$/.test(node.textContent); + var blankWithSpaces = node.isBlank && hasLeading && hasTrailing; + + if (hasLeading && !isFlankedByWhitespace('left', node)) { + leading = ' '; + } + + if (!blankWithSpaces && hasTrailing && !isFlankedByWhitespace('right', node)) { + trailing = ' '; + } + } + + return { leading: leading, trailing: trailing } + } + + function isFlankedByWhitespace (side, node) { + var sibling; + var regExp; + var isFlanked; + + if (side === 'left') { + sibling = node.previousSibling; + regExp = / $/; + } else { + sibling = node.nextSibling; + regExp = /^ /; + } + + if (sibling) { + if (sibling.nodeType === 3) { + isFlanked = regExp.test(sibling.nodeValue); + } else if (sibling.nodeType === 1 && !isBlock(sibling)) { + isFlanked = regExp.test(sibling.textContent); + } + } + return isFlanked + } + + var reduce = Array.prototype.reduce; + var leadingNewLinesRegExp = /^\n*/; + var trailingNewLinesRegExp = /\n*$/; + var escapes = [ + [/\\/g, '\\\\'], + [/\*/g, '\\*'], + [/^-/g, '\\-'], + [/^\+ /g, '\\+ '], + [/^(=+)/g, '\\$1'], + [/^(#{1,6}) /g, '\\$1 '], + [/`/g, '\\`'], + [/^~~~/g, '\\~~~'], + [/\[/g, '\\['], + [/\]/g, '\\]'], + [/^>/g, '\\>'], + [/_/g, '\\_'], + [/^(\d+)\. /g, '$1\\. '] + ]; + + function TurndownService (options) { + if (!(this instanceof TurndownService)) return new TurndownService(options) + + var defaults = { + rules: rules, + headingStyle: 'setext', + hr: '* * *', + bulletListMarker: '*', + codeBlockStyle: 'indented', + fence: '```', + emDelimiter: '_', + strongDelimiter: '**', + linkStyle: 'inlined', + linkReferenceStyle: 'full', + br: ' ', + blankReplacement: function (content, node) { + return node.isBlock ? '\n\n' : '' + }, + keepReplacement: function (content, node) { + return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML + }, + defaultReplacement: function (content, node) { + return node.isBlock ? '\n\n' + content + '\n\n' : content + } + }; + this.options = extend({}, defaults, options); + this.rules = new Rules(this.options); + } + + TurndownService.prototype = { + /** + * The entry point for converting a string or DOM node to Markdown + * @public + * @param {String|HTMLElement} input The string or DOM node to convert + * @returns A Markdown representation of the input + * @type String + */ + + turndown: function (input) { + if (!canConvert(input)) { + throw new TypeError( + input + ' is not a string, or an element/document/fragment node.' + ) + } + + if (input === '') return '' + + var output = process.call(this, new RootNode(input)); + return postProcess.call(this, output) + }, + + /** + * Add one or more plugins + * @public + * @param {Function|Array} plugin The plugin or array of plugins to add + * @returns The Turndown instance for chaining + * @type Object + */ + + use: function (plugin) { + if (Array.isArray(plugin)) { + for (var i = 0; i < plugin.length; i++) this.use(plugin[i]); + } else if (typeof plugin === 'function') { + plugin(this); + } else { + throw new TypeError('plugin must be a Function or an Array of Functions') + } + return this + }, + + /** + * Adds a rule + * @public + * @param {String} key The unique key of the rule + * @param {Object} rule The rule + * @returns The Turndown instance for chaining + * @type Object + */ + + addRule: function (key, rule) { + this.rules.add(key, rule); + return this + }, + + /** + * Keep a node (as HTML) that matches the filter + * @public + * @param {String|Array|Function} filter The unique key of the rule + * @returns The Turndown instance for chaining + * @type Object + */ + + keep: function (filter) { + this.rules.keep(filter); + return this + }, + + /** + * Remove a node that matches the filter + * @public + * @param {String|Array|Function} filter The unique key of the rule + * @returns The Turndown instance for chaining + * @type Object + */ + + remove: function (filter) { + this.rules.remove(filter); + return this + }, + + /** + * Escapes Markdown syntax + * @public + * @param {String} string The string to escape + * @returns A string with Markdown syntax escaped + * @type String + */ + + escape: function (string) { + return escapes.reduce(function (accumulator, escape) { + return accumulator.replace(escape[0], escape[1]) + }, string) + } + }; + + /** + * Reduces a DOM node down to its Markdown string equivalent + * @private + * @param {HTMLElement} parentNode The node to convert + * @returns A Markdown representation of the node + * @type String + */ + + function process (parentNode) { + var self = this; + return reduce.call(parentNode.childNodes, function (output, node) { + node = new Node(node); + + var replacement = ''; + if (node.nodeType === 3) { + replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue); + } else if (node.nodeType === 1) { + replacement = replacementForNode.call(self, node); + } + + return join(output, replacement) + }, '') + } + + /** + * Appends strings as each rule requires and trims the output + * @private + * @param {String} output The conversion output + * @returns A trimmed version of the ouput + * @type String + */ + + function postProcess (output) { + var self = this; + this.rules.forEach(function (rule) { + if (typeof rule.append === 'function') { + output = join(output, rule.append(self.options)); + } + }); + + return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '') + } + + /** + * Converts an element node to its Markdown equivalent + * @private + * @param {HTMLElement} node The node to convert + * @returns A Markdown representation of the node + * @type String + */ + + function replacementForNode (node) { + var rule = this.rules.forNode(node); + var content = process.call(this, node); + var whitespace = node.flankingWhitespace; + if (whitespace.leading || whitespace.trailing) content = content.trim(); + return ( + whitespace.leading + + rule.replacement(content, node, this.options) + + whitespace.trailing + ) + } + + /** + * Determines the new lines between the current output and the replacement + * @private + * @param {String} output The current conversion output + * @param {String} replacement The string to append to the output + * @returns The whitespace to separate the current output and the replacement + * @type String + */ + + function separatingNewlines (output, replacement) { + var newlines = [ + output.match(trailingNewLinesRegExp)[0], + replacement.match(leadingNewLinesRegExp)[0] + ].sort(); + var maxNewlines = newlines[newlines.length - 1]; + return maxNewlines.length < 2 ? maxNewlines : '\n\n' + } + + function join (string1, string2) { + var separator = separatingNewlines(string1, string2); + + // Remove trailing/leading newlines and replace with separator + string1 = string1.replace(trailingNewLinesRegExp, ''); + string2 = string2.replace(leadingNewLinesRegExp, ''); + + return string1 + separator + string2 + } + + /** + * Determines whether an input can be converted + * @private + * @param {String|HTMLElement} input Describe this parameter + * @returns Describe what it returns + * @type String|Object|Array|Boolean|Number + */ + + function canConvert (input) { + return ( + input != null && ( + typeof input === 'string' || + (input.nodeType && ( + input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11 + )) + ) + ) + } + + return TurndownService; + +}))); diff --git a/www/pad/export.js b/www/pad/export.js index f1e3497c9..01ac27d7e 100644 --- a/www/pad/export.js +++ b/www/pad/export.js @@ -1,12 +1,24 @@ define([ 'jquery', '/common/common-util.js', + '/common/diffMarked.js', + '/common/hyperscript.js', '/bower_components/hyperjson/hyperjson.js', '/bower_components/nthen/index.js', -], function ($, Util, Hyperjson, nThen) { + '/lib/turndown.browser.umd.js' +], function ($, Util, DiffMd, h, Hyperjson, nThen, Turndown) { var module = { ext: '.html', // default - exts: ['.html', '.doc'] + exts: ['.html', '.md', '.doc'] + }; + + module.importMd = function (md, common) { + var html = DiffMd.render(md, true, false, true); + var div = h('div#cp-temp'); + DiffMd.apply(html, $(div), common); + var body = h('body'); + body.innerHTML = div.innerHTML; + return body; }; var exportMediaTags = function (inner, cb) { @@ -77,6 +89,15 @@ define([ }); return void cb(blob); } + if (ext === ".md") { + var md = Turndown({ + headingStyle: 'atx' + }).turndown(toExport); + var mdBlob = new Blob([md], { + type: 'text/markdown;charset=utf-8' + }); + return void cb(mdBlob); + } var html = module.getHTML(toExport); cb(new Blob([ html ], { type: "text/html;charset=utf-8" })); }); diff --git a/www/pad/inner.js b/www/pad/inner.js index 47ab5dbe3..ae22af23c 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -1115,7 +1115,7 @@ define([ framework._.sfCommon.isPadStored(function(err, val) { if (!val) { return; } - var b64images = $inner.find('img[src^="data:image"]:not(.cke_reset)'); + var b64images = $inner.find('img[src^="data:image"]:not(.cke_reset), img[src^="data:application/octet-stream"]:not(.cke_reset)'); if (b64images.length && framework._.sfCommon.isLoggedIn()) { var no = h('button.cp-corner-cancel', Messages.cancel); var yes = h('button.cp-corner-primary', Messages.ok); @@ -1169,7 +1169,14 @@ define([ }); cb($dom[0]); }; - framework.setFileImporter({ accept: 'text/html' }, function(content, f, cb) { + framework.setFileImporter({ accept: ['.md', 'text/html'] }, function(content, f, cb) { + if (!f) { return; } + if (/\.md$/.test(f.name)) { + var mdDom = Exporter.importMd(content, framework._.sfCommon); + return importMediaTags(mdDom, function(dom) { + cb(Hyperjson.fromDOM(dom)); + }); + } importMediaTags(domFromHTML(content).body, function(dom) { cb(Hyperjson.fromDOM(dom)); });