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:
parent
37dbb81425
commit
6132cf2128
@ -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;
|
||||
|
||||
|
@ -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({
|
||||
|
26
ElectronClient/app/package-lock.json
generated
26
ElectronClient/app/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -175,4 +175,4 @@ a {
|
||||
@keyframes rotate {
|
||||
from {transform: rotate(0deg);}
|
||||
to {transform: rotate(360deg);}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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}`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
125
ReactNativeClient/lib/services/InteropService_Exporter_Html.js
Normal file
125
ReactNativeClient/lib/services/InteropService_Exporter_Html.js
Normal 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;
|
Loading…
Reference in New Issue
Block a user