Merge branch 'dapsi' into staging

pull/1/head
ansuz 3 years ago
commit caa69e73ca

@ -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.

@ -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 () {

@ -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;
}

@ -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,
};
});

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>CryptPad</title>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer" />
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<link href="/customize/src/outer.css?ver=1.3.2" rel="stylesheet" type="text/css">
</head>
<body>
<iframe-placeholder>

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html class="cp-app-noscroll">
<head>
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
<script async data-bootload="/convert/inner.js" data-main="/common/sframe-boot.js?ver=1.7" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
<style>
.loading-hidden { display: none; }
</style>
</head>
<body class="cp-app-convert">
<div id="cp-toolbar" class="cp-toolbar-container"></div>
<div id="cp-sidebarlayout-container"></div>
<noscript>
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
</noscript>
</body>

@ -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 '<m_nFormatFrom>'+id+'</m_nFormatFrom>';
};
var getToId = function (ext) {
var id = getFormatId(ext);
if (!id) { return ''; }
return '<m_nFormatTo>'+id+'</m_nFormatTo>';
};
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 = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<TaskQueueDataConvert xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"
+ "<m_sFileFrom>/working/" + fileName + "</m_sFileFrom>"
+ "<m_sFileTo>/working/" + fileName + "." + outputFormat + "</m_sFileTo>"
+ getFromId(inputFormat)
+ getToId(outputFormat)
+ "<m_bIsNoBase64>false</m_bIsNoBase64>"
+ "</TaskQueueDataConvert>";
// 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 = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);
APP.$rightside = $('<div>', {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();
});
});

@ -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
});
});
});

@ -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;
});

@ -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({

@ -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;
});

@ -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 };
});

@ -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 <lucthevenard@gmail.com>
*
* 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 <head> and <body>.
// Wrapping in a custom element ensures elements are reliably arranged in
// a single element.
'<x-turndown id="turndown-root">' + input + '</x-turndown>',
'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;
})));

@ -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" }));
});

@ -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));
});

Loading…
Cancel
Save