1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Desktop, Cli: Allow exporting a note as HTML

This commit is contained in:
Laurent Cozic 2019-12-15 18:41:13 +00:00
parent 37dbb81425
commit 6132cf2128
10 changed files with 185 additions and 24 deletions

View File

@ -32,6 +32,8 @@ class InteropServiceHelper {
const exportOptions = {};
exportOptions.path = path;
exportOptions.format = module.format;
exportOptions.modulePath = module.path;
exportOptions.target = module.target;
if (options.sourceFolderIds) exportOptions.sourceFolderIds = options.sourceFolderIds;
if (options.sourceNoteIds) exportOptions.sourceNoteIds = options.sourceNoteIds;

View File

@ -128,6 +128,7 @@ class NoteListUtils {
for (let i = 0; i < ioModules.length; i++) {
const module = ioModules[i];
if (module.type !== 'exporter') continue;
if (noteIds.length > 1 && module.canDoMultiExport === false) continue;
exportMenu.append(
new MenuItem({

View File

@ -596,14 +596,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -618,14 +616,12 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
@ -747,8 +743,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -760,7 +755,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -775,7 +769,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -783,8 +776,7 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.3.5",
@ -888,8 +880,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -2341,6 +2332,11 @@
}
}
},
"dataurl": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/dataurl/-/dataurl-0.1.0.tgz",
"integrity": "sha1-H0c0/t3sBf/kRXR5eNhnWcSzMZk="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",

View File

@ -90,6 +90,7 @@
"chokidar": "^3.0.0",
"clean-html": "^1.5.0",
"compare-versions": "^3.2.1",
"dataurl": "^0.1.0",
"diacritics": "^1.3.0",
"diff-match-patch": "^1.0.4",
"electron-context-menu": "^0.15.0",

View File

@ -175,4 +175,4 @@ a {
@keyframes rotate {
from {transform: rotate(0deg);}
to {transform: rotate(360deg);}
}
}

View File

@ -10,7 +10,7 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
const src = utils.getAttr(token.attrs, 'src');
const title = utils.getAttr(token.attrs, 'title');
if (!Resource.isResourceUrl(src)) return defaultRender(tokens, idx, options, env, self);
if (!Resource.isResourceUrl(src) || ruleOptions.plainResourceRendering) return defaultRender(tokens, idx, options, env, self);
const r = utils.imageReplacement(src, ruleOptions.resources, ruleOptions.resourceBaseUrl);
if (typeof r === 'string') return r;

View File

@ -27,7 +27,7 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
mime = result.item.mime;
}
if (result && resourceStatus !== 'ready') {
if (result && resourceStatus !== 'ready' && !ruleOptions.plainResourceRendering) {
const icon = utils.resourceStatusFile(resourceStatus);
return `<a class="not-loaded-resource resource-status-${resourceStatus}" data-resource-id="${resourceId}">` + `<img src="data:image/svg+xml;utf8,${htmlentities(icon)}"/>`;
} else {
@ -57,7 +57,12 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
let js = `${ruleOptions.postMessageSyntax}(${JSON.stringify(href)}); return false;`;
if (hrefAttr.indexOf('#') === 0 && href.indexOf('#') === 0) js = ''; // If it's an internal anchor, don't add any JS since the webview is going to handle navigating to the right place
return `<a data-from-md ${resourceIdAttr} title='${htmlentities(title)}' href='${hrefAttr}' onclick='${js}' type='${htmlentities(mime)}'>${icon}`;
if (ruleOptions.plainResourceRendering) {
return `<a data-from-md ${resourceIdAttr} title='${htmlentities(title)}' href='${hrefAttr}' type='${htmlentities(mime)}'>`;
} else {
return `<a data-from-md ${resourceIdAttr} title='${htmlentities(title)}' href='${hrefAttr}' onclick='${js}' type='${htmlentities(mime)}'>${icon}`;
}
};
}

View File

@ -254,6 +254,16 @@ module.exports = function(style, options) {
opacity: 0.5;
}
.exported-note-title {
font-size: 2.2em;
font-weight: bold;
margin-bottom: 1em;
}
.exported-note {
padding: 1em;
}
@media print {
body {
height: auto !important;

View File

@ -61,6 +61,7 @@ class InteropService {
format: 'jex',
fileExtensions: ['jex'],
target: 'file',
canDoMultiExport: true,
description: _('Joplin Export File'),
},
{
@ -78,6 +79,17 @@ class InteropService {
target: 'directory',
description: _('Markdown'),
},
{
format: 'html',
target: 'file',
canDoMultiExport: false,
description: _('HTML File'),
},
{
format: 'html',
target: 'directory',
description: _('HTML Directory'),
},
];
importModules = importModules.map(a => {
@ -127,12 +139,18 @@ class InteropService {
// or exporters, such as ENEX. In this case, the one marked as "isDefault"
// is returned. This is useful to auto-detect the module based on the format.
// For more precise matching, newModuleFromPath_ should be used.
findModuleByFormat_(type, format) {
findModuleByFormat_(type, format, target = null) {
const modules = this.modules();
const matches = [];
for (let i = 0; i < modules.length; i++) {
const m = modules[i];
if (m.format === format && m.type === type) matches.push(modules[i]);
if (m.format === format && m.type === type) {
if (target === null) {
matches.push(m);
} else if (target === m.target) {
matches.push(m);
}
}
}
const output = matches.find(m => !!m.isDefault);
@ -171,7 +189,7 @@ class InteropService {
}
const ModuleClass = require(options.modulePath);
const output = new ModuleClass();
const moduleMetadata = this.findModuleByFormat_(type, options.format);
const moduleMetadata = this.findModuleByFormat_(type, options.format, options.target);
output.setMetadata({options, ...moduleMetadata}); // TODO: Check that this metadata is equivalent to module above
return output;
}
@ -237,10 +255,12 @@ class InteropService {
}
async export(options) {
options = Object.assign({}, options);
if (!options.format) options.format = 'jex';
const exportPath = options.path ? options.path : null;
let sourceFolderIds = options.sourceFolderIds ? options.sourceFolderIds : [];
const sourceNoteIds = options.sourceNoteIds ? options.sourceNoteIds : [];
const exportFormat = options.format ? options.format : 'jex';
const result = { warnings: [] };
const itemsToExport = [];
@ -304,7 +324,7 @@ class InteropService {
await queueExportItem(BaseModel.TYPE_TAG, exportedTagIds[i]);
}
const exporter = this.newModuleByFormat_('exporter', exportFormat);
const exporter = this.newModuleFromPath_('exporter', options);// this.newModuleByFormat_('exporter', exportFormat);
await exporter.init(exportPath);
const typeOrder = [BaseModel.TYPE_FOLDER, BaseModel.TYPE_RESOURCE, BaseModel.TYPE_NOTE, BaseModel.TYPE_TAG, BaseModel.TYPE_NOTE_TAG];
@ -345,6 +365,7 @@ class InteropService {
await exporter.processItem(ItemClass, item);
} catch (error) {
console.error(error);
result.warnings.push(error.message);
}
}

View File

@ -0,0 +1,125 @@
const InteropService_Exporter_Base = require('lib/services/InteropService_Exporter_Base');
const { basename, friendlySafeFilename, rtrimSlashes } = require('lib/path-utils.js');
const BaseModel = require('lib/BaseModel');
const Folder = require('lib/models/Folder');
const Note = require('lib/models/Note');
const Setting = require('lib/models/Setting');
const Resource = require('lib/models/Resource');
const { shim } = require('lib/shim');
const MdToHtml = require('lib/renderers/MdToHtml.js');
const dataurl = require('dataurl');
const { themeStyle } = require('../../theme.js');
const { dirname } = require('lib/path-utils.js');
class InteropService_Exporter_Html extends InteropService_Exporter_Base {
async init(path) {
if (this.metadata().target === 'file') {
this.destDir_ = dirname(path);
this.filePath_ = path;
} else {
this.destDir_ = path;
this.filePath_ = null;
}
this.createdDirs_ = [];
this.resourceDir_ = this.destDir_ ? `${this.destDir_}/_resources` : null;
await shim.fsDriver().mkdir(this.destDir_);
this.mdToHtml_ = new MdToHtml();
this.resources_ = [];
this.style_ = themeStyle(Setting.THEME_LIGHT);
}
async makeDirPath_(item, pathPart = null) {
let output = '';
while (true) {
if (item.type_ === BaseModel.TYPE_FOLDER) {
if (pathPart) {
output = `${pathPart}/${output}`;
} else {
output = `${friendlySafeFilename(item.title, null, true)}/${output}`;
output = await shim.fsDriver().findUniqueFilename(output);
}
}
if (!item.parent_id) return output;
item = await Folder.load(item.parent_id);
}
}
async processNoteResources_(item) {
const target = this.metadata().target;
const linkedResourceIds = await Note.linkedResourceIds(item.body);
const relativePath = target === 'directory' ? rtrimSlashes(await this.makeDirPath_(item, '..')) : '';
const resourcePaths = this.context() && this.context().resourcePaths ? this.context().resourcePaths : {};
let newBody = item.body;
for (let i = 0; i < linkedResourceIds.length; i++) {
const id = linkedResourceIds[i];
const resource = await Resource.load(id);
let resourceContent = '';
const isImage = Resource.isSupportedImageMimeType(resource.mime);
if (!isImage) {
resourceContent = `${relativePath ? `${relativePath}/` : ''}_resources/${basename(resourcePaths[id])}`;
} else {
const buffer = await shim.fsDriver().readFile(resourcePaths[id], 'Buffer');
resourceContent = dataurl.convert({
data: buffer,
mimetype: resource.mime,
});
}
newBody = newBody.replace(new RegExp(`:/${id}`, 'g'), resourceContent);
}
return newBody;
}
async processItem(ItemClass, item) {
if ([BaseModel.TYPE_NOTE, BaseModel.TYPE_FOLDER].indexOf(item.type_) < 0) return;
let dirPath = '';
if (!this.filePath_) {
dirPath = `${this.destDir_}/${await this.makeDirPath_(item)}`;
if (this.createdDirs_.indexOf(dirPath) < 0) {
await shim.fsDriver().mkdir(dirPath);
this.createdDirs_.push(dirPath);
}
}
if (item.type_ === BaseModel.TYPE_NOTE) {
let noteFilePath = '';
if (this.filePath_) {
noteFilePath = this.filePath_;
} else {
noteFilePath = `${dirPath}/${friendlySafeFilename(item.title, null, true)}.html`;
noteFilePath = await shim.fsDriver().findUniqueFilename(noteFilePath);
}
const bodyMd = await this.processNoteResources_(item);
const result = this.mdToHtml_.render(bodyMd, this.style_, { resources: this.resources_, plainResourceRendering: true });
const noteContent = [];
if (item.title) noteContent.push(`<div class="exported-note-title">${item.title}</div>`);
if (result.html) noteContent.push(result.html);
await shim.fsDriver().writeFile(noteFilePath, `<div class="exported-note">${noteContent.join('\n\n')}</div>`, 'utf-8');
}
}
async processResource(resource, filePath) {
if (!Resource.isSupportedImageMimeType(resource.mime)) {
const destResourcePath = `${this.resourceDir_}/${basename(filePath)}`;
await shim.fsDriver().copy(filePath, destResourcePath);
this.resources_.push(resource);
}
}
async close() {}
}
module.exports = InteropService_Exporter_Html;