1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Merge branch 'clipper_html_mode'

This commit is contained in:
Laurent Cozic 2019-07-21 13:46:54 +01:00
commit 35b6b3fc46
38 changed files with 1007 additions and 414 deletions

View File

@ -15,9 +15,9 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
}, },
"acorn": { "acorn": {
"version": "5.7.3", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.0.tgz",
"integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" "integrity": "sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw=="
}, },
"acorn-globals": { "acorn-globals": {
"version": "4.3.2", "version": "4.3.2",
@ -26,13 +26,6 @@
"requires": { "requires": {
"acorn": "^6.0.1", "acorn": "^6.0.1",
"acorn-walk": "^6.0.1" "acorn-walk": "^6.0.1"
},
"dependencies": {
"acorn": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.0.tgz",
"integrity": "sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw=="
}
} }
}, },
"acorn-walk": { "acorn-walk": {
@ -518,18 +511,6 @@
"abab": "^2.0.0", "abab": "^2.0.0",
"whatwg-mimetype": "^2.2.0", "whatwg-mimetype": "^2.2.0",
"whatwg-url": "^7.0.0" "whatwg-url": "^7.0.0"
},
"dependencies": {
"whatwg-url": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz",
"integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==",
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
}
} }
}, },
"debug": { "debug": {
@ -1472,62 +1453,13 @@
"css": "^2.2.4", "css": "^2.2.4",
"html-entities": "^1.2.1", "html-entities": "^1.2.1",
"jsdom": "^11.9.0" "jsdom": "^11.9.0"
}
},
"joplin-turndown-plugin-gfm": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.8.tgz",
"integrity": "sha512-uXgq2zGvjiMl/sXG7946EGhh1pyGbZ0L/6z21LBi8D6BJgHQufmXdve/UP3zpgnhiFhfXvzGY10uNaTuDQ99iQ=="
},
"jpeg-js": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.1.2.tgz",
"integrity": "sha1-E1uZLAV1yYXPoPSUoyJ+0jhYPs4="
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"optional": true
},
"jsdom": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
"integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
"requires": {
"abab": "^2.0.0",
"acorn": "^5.5.3",
"acorn-globals": "^4.1.0",
"array-equal": "^1.0.0",
"cssom": ">= 0.3.2 < 0.4.0",
"cssstyle": "^1.0.0",
"data-urls": "^1.0.0",
"domexception": "^1.0.1",
"escodegen": "^1.9.1",
"html-encoding-sniffer": "^1.0.2",
"left-pad": "^1.3.0",
"nwsapi": "^2.0.7",
"parse5": "4.0.0",
"pn": "^1.1.0",
"request": "^2.87.0",
"request-promise-native": "^1.0.5",
"sax": "^1.2.4",
"symbol-tree": "^3.2.2",
"tough-cookie": "^2.3.4",
"w3c-hr-time": "^1.0.1",
"webidl-conversions": "^4.0.2",
"whatwg-encoding": "^1.0.3",
"whatwg-mimetype": "^2.1.0",
"whatwg-url": "^6.4.1",
"ws": "^5.2.0",
"xml-name-validator": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"acorn": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
"integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw=="
},
"ajv": { "ajv": {
"version": "6.10.1", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.1.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.1.tgz",
@ -1581,6 +1513,39 @@
"har-schema": "^2.0.0" "har-schema": "^2.0.0"
} }
}, },
"jsdom": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
"integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
"requires": {
"abab": "^2.0.0",
"acorn": "^5.5.3",
"acorn-globals": "^4.1.0",
"array-equal": "^1.0.0",
"cssom": ">= 0.3.2 < 0.4.0",
"cssstyle": "^1.0.0",
"data-urls": "^1.0.0",
"domexception": "^1.0.1",
"escodegen": "^1.9.1",
"html-encoding-sniffer": "^1.0.2",
"left-pad": "^1.3.0",
"nwsapi": "^2.0.7",
"parse5": "4.0.0",
"pn": "^1.1.0",
"request": "^2.87.0",
"request-promise-native": "^1.0.5",
"sax": "^1.2.4",
"symbol-tree": "^3.2.2",
"tough-cookie": "^2.3.4",
"w3c-hr-time": "^1.0.1",
"webidl-conversions": "^4.0.2",
"whatwg-encoding": "^1.0.3",
"whatwg-mimetype": "^2.1.0",
"whatwg-url": "^6.4.1",
"ws": "^5.2.0",
"xml-name-validator": "^3.0.0"
}
},
"json-schema-traverse": { "json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@ -1604,6 +1569,11 @@
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
}, },
"parse5": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
},
"request": { "request": {
"version": "2.88.0", "version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
@ -1651,9 +1621,48 @@
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"whatwg-url": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
"integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
},
"ws": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"requires": {
"async-limiter": "~1.0.0"
}
} }
} }
}, },
"joplin-turndown-plugin-gfm": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.8.tgz",
"integrity": "sha512-uXgq2zGvjiMl/sXG7946EGhh1pyGbZ0L/6z21LBi8D6BJgHQufmXdve/UP3zpgnhiFhfXvzGY10uNaTuDQ99iQ=="
},
"jpeg-js": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.1.2.tgz",
"integrity": "sha1-E1uZLAV1yYXPoPSUoyJ+0jhYPs4="
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"optional": true
},
"json-schema": { "json-schema": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
@ -2199,11 +2208,6 @@
"data-uri-to-buffer": "0.0.3" "data-uri-to-buffer": "0.0.3"
} }
}, },
"parse5": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
},
"path-exists": { "path-exists": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
@ -3241,9 +3245,9 @@
"integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g=="
}, },
"whatwg-url": { "whatwg-url": {
"version": "6.5.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz",
"integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==",
"requires": { "requires": {
"lodash.sortby": "^4.7.0", "lodash.sortby": "^4.7.0",
"tr46": "^1.0.1", "tr46": "^1.0.1",
@ -3296,14 +3300,6 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}, },
"ws": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"requires": {
"async-limiter": "~1.0.0"
}
},
"xml-name-validator": { "xml-name-validator": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",

View File

@ -0,0 +1,105 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const htmlUtils = require('lib/htmlUtils.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('htmlUtils', function() {
beforeEach(async (done) => {
done();
});
it('should extract image URLs', async (done) => {
const testCases = [
['<img src="http://test.com/img.png"/>', ['http://test.com/img.png']],
['<img src="http://test.com/img.png"/> <img src="http://test.com/img2.png"/>', ['http://test.com/img.png', 'http://test.com/img2.png']],
['<img src="http://test.com/img.png" alt="testing" >', ['http://test.com/img.png']],
['nothing here', []],
['', []],
];
for (let i = 0; i < testCases.length; i++) {
const md = testCases[i][0];
const expected = testCases[i][1];
expect(htmlUtils.extractImageUrls(md).join(' ')).toBe(expected.join(' '));
}
done();
});
it('should replace image URLs', async (done) => {
const testCases = [
['<img src="http://test.com/img.png"/>', ['http://other.com/img2.png'], '<img src="http://other.com/img2.png"/>'],
['<img src="http://test.com/img.png"/> <img src="http://test.com/img2.png"/>', ['http://other.com/img2.png', 'http://other.com/img3.png'], '<img src="http://other.com/img2.png"/> <img src="http://other.com/img3.png"/>'],
['<img src="http://test.com/img.png" alt="testing" >', ['http://other.com/img.png'], '<img src="http://other.com/img.png" alt="testing" >'],
];
const callback = (urls) => {
let i = -1;
return function(src) {
i++;
return urls[i];
}
}
for (let i = 0; i < testCases.length; i++) {
const md = testCases[i][0];
const r = htmlUtils.replaceImageUrls(md, callback(testCases[i][1]));
expect(r.trim()).toBe(testCases[i][2].trim());
}
done();
});
it('should encode attributes', async (done) => {
const testCases = [
[{ a: 'one', b: 'two' }, 'a="one" b="two"'],
[{ a: 'one&two' }, 'a="one&amp;two"'],
];
for (let i = 0; i < testCases.length; i++) {
const attrs = testCases[i][0];
const expected = testCases[i][1];
expect(htmlUtils.attributesHtml(attrs)).toBe(expected);
}
done();
});
it('should prepend a base URL', async (done) => {
const testCases = [
[
'<a href="a.html">Something</a>',
'http://test.com',
'<a href="http://test.com/a.html">Something</a>',
],
[
'<a href="a.html">a</a> <a href="b.html">b</a>',
'http://test.com',
'<a href="http://test.com/a.html">a</a> <a href="http://test.com/b.html">b</a>',
],
[
'<a href="a.html">a</a> <a href="b.html">b</a>',
'http://test.com',
'<a href="http://test.com/a.html">a</a> <a href="http://test.com/b.html">b</a>',
],
];
for (let i = 0; i < testCases.length; i++) {
const html = testCases[i][0];
const baseUrl = testCases[i][1];
const expected = testCases[i][2];
expect(htmlUtils.prependBaseUrl(html, baseUrl)).toBe(expected);
}
done();
});
});

View File

@ -61,13 +61,19 @@
const output = {}; const output = {};
for (let i = 0; i < images.length; i++) { for (let i = 0; i < images.length; i++) {
const img = images[i]; const img = images[i];
const src = forceAbsoluteUrls ? absoluteUrl(img.src) : img.src; if (img.classList && img.classList.contains('joplin-clipper-hidden')) continue;
output[src] = {
let src = imageSrc(img);
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
if (!output[src]) output[src] = [];
output[src].push({
width: img.width, width: img.width,
height: img.height, height: img.height,
naturalWidth: img.naturalWidth, naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight, naturalHeight: img.naturalHeight,
}; });
} }
return output; return output;
} }
@ -86,20 +92,29 @@
return output; return output;
} }
// In general we should use currentSrc because that's the image that's currently displayed,
// especially within <picture> tags or with srcset. In these cases there can be multiple
// sources and the best one is probably the one being displayed, thus currentSrc.
function imageSrc(image) {
if (image.currentSrc) return image.currentSrc;
return image.src;
}
// Cleans up element by removing all its invisible children (which we don't want to render as Markdown) // Cleans up element by removing all its invisible children (which we don't want to render as Markdown)
// And hard-code the image dimensions so that the information can be used by the clipper server to // And hard-code the image dimensions so that the information can be used by the clipper server to
// display them at the right sizes in the notes. // display them at the right sizes in the notes.
function cleanUpElement(element, imageSizes) { function cleanUpElement(convertToMarkup, element, imageSizes, imageIndexes) {
const childNodes = element.childNodes; const childNodes = element.childNodes;
const hiddenNodes = [];
for (let i = childNodes.length - 1; i >= 0; i--) { for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i]; const node = childNodes[i];
const nodeName = node.nodeName.toLowerCase(); const nodeName = node.nodeName.toLowerCase();
const isHidden = node && node.classList && node.classList.contains('joplin-clipper-hidden'); const isHidden = node && node.classList && node.classList.contains('joplin-clipper-hidden');
if (isHidden) { if (isHidden) {
element.removeChild(node); hiddenNodes.push(node);
} else { } else {
// If the data-joplin-clipper-value has been set earlier, create a new DIV element // If the data-joplin-clipper-value has been set earlier, create a new DIV element
@ -112,17 +127,25 @@
} }
if (nodeName === 'img') { if (nodeName === 'img') {
node.src = absoluteUrl(node.src); const src = absoluteUrl(imageSrc(node));
const imageSize = imageSizes[node.src]; node.setAttribute('src', src);
if (imageSize) { if (!(src in imageIndexes)) imageIndexes[src] = 0;
const imageSize = imageSizes[src][imageIndexes[src]];
imageIndexes[src]++;
if (imageSize && convertToMarkup === 'markdown') {
node.width = imageSize.width; node.width = imageSize.width;
node.height = imageSize.height; node.height = imageSize.height;
} }
} }
cleanUpElement(node, imageSizes); cleanUpElement(convertToMarkup, node, imageSizes, imageIndexes);
} }
} }
for (const hiddenNode of hiddenNodes) {
if (!hiddenNode.parentNode) continue;
hiddenNode.parentNode.removeChild(hiddenNode);
}
} }
// When we clone the document before cleaning it, we lose some of the information that might have been set via CSS or // When we clone the document before cleaning it, we lose some of the information that might have been set via CSS or
@ -132,9 +155,11 @@
function preProcessDocument(element) { function preProcessDocument(element) {
const childNodes = element.childNodes; const childNodes = element.childNodes;
for (let i = 0; i < childNodes.length; i++) { for (let i = childNodes.length - 1; i >= 0; i--) {
const node = childNodes[i]; const node = childNodes[i];
const nodeName = node.nodeName.toLowerCase(); const nodeName = node.nodeName.toLowerCase();
const nodeParent = node.parentNode;
const nodeParentName = nodeParent ? nodeParent.nodeName.toLowerCase() : '';
let isVisible = node.nodeType === 1 ? window.getComputedStyle(node).display !== 'none' : true; let isVisible = node.nodeType === 1 ? window.getComputedStyle(node).display !== 'none' : true;
if (isVisible && ['script', 'noscript', 'style', 'select', 'option', 'button'].indexOf(nodeName) >= 0) isVisible = false; if (isVisible && ['script', 'noscript', 'style', 'select', 'option', 'button'].indexOf(nodeName) >= 0) isVisible = false;
@ -153,7 +178,13 @@
if (a && a.toLowerCase().indexOf('math/tex') >= 0) isVisible = true; if (a && a.toLowerCase().indexOf('math/tex') >= 0) isVisible = true;
} }
if (!isVisible) { if (nodeName === 'source' && nodeParentName === 'picture') {
isVisible = false
}
if (node.nodeType === 8) { // Comments are just removed since we can't add a class
node.parentNode.removeChild(node);
} else if (!isVisible) {
node.classList.add('joplin-clipper-hidden'); node.classList.add('joplin-clipper-hidden');
} else { } else {
preProcessDocument(node); preProcessDocument(node);
@ -179,19 +210,25 @@
// Given a document, return a <style> tag that contains all the styles // Given a document, return a <style> tag that contains all the styles
// required to render the page. Not currently used but could be as an // required to render the page. Not currently used but could be as an
// option to clip pages as HTML. // option to clip pages as HTML.
function getStyleTag(doc) { function getStyleSheets(doc) {
const styleText = []; const output = [];
for (var i=0; i<doc.styleSheets.length; i++) { for (var i=0; i<doc.styleSheets.length; i++) {
var sheet = doc.styleSheets[i];
try { try {
var sheet = doc.styleSheets[i];
for (const cssRule of sheet.cssRules) { for (const cssRule of sheet.cssRules) {
styleText.push(cssRule.cssText); output.push({ type: 'text', value: cssRule.cssText });
} }
} catch (error) { } catch (error) {
console.warn(error); // Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
// by the desktop application, since it doesn't have CORS restrictions.
console.info('Could not retrieve stylesheet now:', sheet.href);
console.info('It will downloaded by the main application.');
console.info(error);
output.push({ type: 'url', value: sheet.href });
} }
} }
return '<style>' + styleText.join('\n') + '</style>'; return output;
} }
function documentForReadability() { function documentForReadability() {
@ -223,7 +260,9 @@
async function prepareCommandResponse(command) { async function prepareCommandResponse(command) {
console.info('Got command: ' + command.name); console.info('Got command: ' + command.name);
const clippedContentResponse = (title, html, imageSizes, anchorNames) => { const convertToMarkup = command.preProcessFor ? command.preProcessFor : 'markdown';
const clippedContentResponse = (title, html, imageSizes, anchorNames, stylesheets) => {
return { return {
name: 'clippedContent', name: 'clippedContent',
title: title, title: title,
@ -234,6 +273,9 @@
tags: command.tags || '', tags: command.tags || '',
image_sizes: imageSizes, image_sizes: imageSizes,
anchor_names: anchorNames, anchor_names: anchorNames,
source_command: Object.assign({}, command),
convert_to: convertToMarkup,
stylesheets: stylesheets,
}; };
} }
@ -255,7 +297,6 @@
} else if (command.name === "isProbablyReaderable") { } else if (command.name === "isProbablyReaderable") {
const ok = isProbablyReaderable(documentForReadability()); const ok = isProbablyReaderable(documentForReadability());
console.info('isProbablyReaderable', ok);
return { name: 'isProbablyReaderable', value: ok }; return { name: 'isProbablyReaderable', value: ok };
} else if (command.name === "completePageHtml") { } else if (command.name === "completePageHtml") {
@ -266,8 +307,11 @@
// directly on the document, so we make a copy of it first. // directly on the document, so we make a copy of it first.
const cleanDocument = document.body.cloneNode(true); const cleanDocument = document.body.cloneNode(true);
const imageSizes = getImageSizes(document, true); const imageSizes = getImageSizes(document, true);
cleanUpElement(cleanDocument, imageSizes); const imageIndexes = {};
return clippedContentResponse(pageTitle(), cleanDocument.innerHTML, imageSizes, getAnchorNames(document)); cleanUpElement(convertToMarkup, cleanDocument, imageSizes, imageIndexes);
const stylesheets = convertToMarkup === 'html' ? getStyleSheets(document) : null;
return clippedContentResponse(pageTitle(), cleanDocument.innerHTML, imageSizes, getAnchorNames(document), stylesheets);
} else if (command.name === "selectedHtml") { } else if (command.name === "selectedHtml") {
@ -277,7 +321,8 @@
const container = document.createElement('div'); const container = document.createElement('div');
container.appendChild(range.cloneContents()); container.appendChild(range.cloneContents());
const imageSizes = getImageSizes(document, true); const imageSizes = getImageSizes(document, true);
cleanUpElement(container, imageSizes); const imageIndexes = {};
cleanUpElement(convertToMarkup, container, imageSizes, imageIndexes);
return clippedContentResponse(pageTitle(), container.innerHTML, getImageSizes(document), getAnchorNames(document)); return clippedContentResponse(pageTitle(), container.innerHTML, getImageSizes(document), getAnchorNames(document));
} else if (command.name === 'screenshot') { } else if (command.name === 'screenshot') {

View File

@ -7,6 +7,20 @@ import led_orange from './led_orange.png';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { bridge } = require('./bridge'); const { bridge } = require('./bridge');
function commandUserString(command) {
const s = [];
if (command.name === 'simplifiedPageHtml') s.push('Simplified page');
if (command.name === 'completePageHtml') s.push('Complete page');
if (command.name === 'selectedHtml') s.push('Selection');
if (command.name === 'pageUrl') s.push('URL only');
const p = command.preProcessFor ? command.preProcessFor : 'markdown';
s.push('(' + p + ')');
return s.join(' ');
}
class PreviewComponent extends React.PureComponent { class PreviewComponent extends React.PureComponent {
constructor() { constructor() {
@ -32,6 +46,7 @@ class PreviewComponent extends React.PureComponent {
<div className="Preview"> <div className="Preview">
<h2>Title:</h2> <h2>Title:</h2>
<input className={"Title"} value={this.props.title} onChange={this.props.onTitleChange}/> <input className={"Title"} value={this.props.title} onChange={this.props.onTitleChange}/>
<p><span>Type:</span> {commandUserString(this.props.command)}</p>
<a className={"Confirm Button"} onClick={this.props.onConfirmClick}>Confirm</a> <a className={"Confirm Button"} onClick={this.props.onConfirmClick}>Confirm</a>
</div> </div>
); );
@ -84,6 +99,14 @@ class AppComponent extends Component {
this.clipComplete_click = () => { this.clipComplete_click = () => {
bridge().sendCommandToActiveTab({ bridge().sendCommandToActiveTab({
name: 'completePageHtml', name: 'completePageHtml',
preProcessFor: 'markdown',
});
}
this.clipCompleteHtml_click = () => {
bridge().sendCommandToActiveTab({
name: 'completePageHtml',
preProcessFor: 'html',
}); });
} }
@ -259,6 +282,7 @@ class AppComponent extends Component {
title={content.title} title={content.title}
body_html={content.body_html} body_html={content.body_html}
onTitleChange={this.contentTitle_change} onTitleChange={this.contentTitle_change}
command={content.source_command}
/> />
} }
@ -360,6 +384,7 @@ class AppComponent extends Component {
<ul> <ul>
<li><a className="Button" onClick={this.clipSimplified_click} title={simplifiedPageButtonTooltip}>{simplifiedPageButtonLabel}</a></li> <li><a className="Button" onClick={this.clipSimplified_click} title={simplifiedPageButtonTooltip}>{simplifiedPageButtonLabel}</a></li>
<li><a className="Button" onClick={this.clipComplete_click}>Clip complete page</a></li> <li><a className="Button" onClick={this.clipComplete_click}>Clip complete page</a></li>
<li><a className="Button" onClick={this.clipCompleteHtml_click}>Clip complete page (HTML) (Beta)</a></li>
<li><a className="Button" onClick={this.clipSelection_click}>Clip selection</a></li> <li><a className="Button" onClick={this.clipSelection_click}>Clip selection</a></li>
<li><a className="Button" onClick={this.clipScreenshot_click}>Clip screenshot</a></li> <li><a className="Button" onClick={this.clipScreenshot_click}>Clip screenshot</a></li>
<li><a className="Button" onClick={this.clipUrl_click}>Clip URL</a></li> <li><a className="Button" onClick={this.clipUrl_click}>Clip URL</a></li>

View File

@ -35,6 +35,9 @@ class Bridge {
tags: command.tags || '', tags: command.tags || '',
image_sizes: command.image_sizes || {}, image_sizes: command.image_sizes || {},
anchor_names: command.anchor_names || [], anchor_names: command.anchor_names || [],
source_command: command.source_command,
convert_to: command.convert_to,
stylesheets: command.stylesheets,
}; };
this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content }); this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content });

View File

@ -1020,16 +1020,17 @@ class Application extends BaseApplication {
this.lastMenuScreen_ = screen; this.lastMenuScreen_ = screen;
} }
updateMenuItemStates() { async updateMenuItemStates() {
if (!this.lastMenuScreen_) return; if (!this.lastMenuScreen_) return;
if (!this.store()) return; if (!this.store()) return;
const selectedNoteIds = this.store().getState().selectedNoteIds; const selectedNoteIds = this.store().getState().selectedNoteIds;
const note = selectedNoteIds.length === 1 ? await Note.load(selectedNoteIds[0]) : null;
for (const itemId of ['copy', 'paste', 'cut', 'selectAll', 'bold', 'italic', 'link', 'code', 'insertDateTime', 'commandStartExternalEditing', 'setTags', 'showLocalSearch']) { for (const itemId of ['copy', 'paste', 'cut', 'selectAll', 'bold', 'italic', 'link', 'code', 'insertDateTime', 'commandStartExternalEditing', 'setTags', 'showLocalSearch']) {
const menuItem = Menu.getApplicationMenu().getMenuItemById('edit:' + itemId); const menuItem = Menu.getApplicationMenu().getMenuItemById('edit:' + itemId);
if (!menuItem) continue; if (!menuItem) continue;
menuItem.enabled = selectedNoteIds.length === 1; menuItem.enabled = !!note && note.markup_language === Note.MARKUP_LANGUAGE_MARKDOWN;
} }
} }

View File

@ -47,7 +47,7 @@ convertJsx(__dirname + '/plugins');
const libContent = [ const libContent = [
fs.readFileSync(basePath + '/ReactNativeClient/lib/string-utils-common.js', 'utf8'), fs.readFileSync(basePath + '/ReactNativeClient/lib/string-utils-common.js', 'utf8'),
fs.readFileSync(basePath + '/ReactNativeClient/lib/markJsUtils.js', 'utf8'), fs.readFileSync(basePath + '/ReactNativeClient/lib/markJsUtils.js', 'utf8'),
fs.readFileSync(basePath + '/ReactNativeClient/lib/MdToHtml/webviewLib.js', 'utf8'), fs.readFileSync(basePath + '/ReactNativeClient/lib/renderers/webviewLib.js', 'utf8'),
]; ];
fs.writeFileSync(__dirname + '/gui/note-viewer/lib.js', libContent.join('\n'), 'utf8'); fs.writeFileSync(__dirname + '/gui/note-viewer/lib.js', libContent.join('\n'), 'utf8');

View File

@ -33,6 +33,7 @@ class NotePropertiesDialog extends React.Component {
location: _('Location'), location: _('Location'),
source_url: _('URL'), source_url: _('URL'),
revisionsLink: _('Note History'), revisionsLink: _('Note History'),
markup_language: _('Markup'),
}; };
} }
@ -82,6 +83,7 @@ class NotePropertiesDialog extends React.Component {
} }
formNote.revisionsLink = note.id; formNote.revisionsLink = note.id;
formNote.markup_language = Note.markupLanguageToLabel(note.markup_language);
formNote.id = note.id; formNote.id = note.id;
return formNote; return formNote;
@ -292,7 +294,7 @@ class NotePropertiesDialog extends React.Component {
controlComp = <div style={Object.assign({}, theme.textStyle, {display: 'inline-block'})}>{displayedValue}</div> controlComp = <div style={Object.assign({}, theme.textStyle, {display: 'inline-block'})}>{displayedValue}</div>
} }
if (key !== 'id' && key !== 'revisionsLink') { if (['id', 'revisionsLink', 'markup_language'].indexOf(key) < 0) {
editCompHandler = () => {this.editPropertyButtonClick(key, value)}; editCompHandler = () => {this.editPropertyButtonClick(key, value)};
editCompIcon = 'fa-edit'; editCompIcon = 'fa-edit';
} }

View File

@ -9,7 +9,7 @@ const Revision = require('lib/models/Revision');
const Setting = require('lib/models/Setting'); const Setting = require('lib/models/Setting');
const RevisionService = require('lib/services/RevisionService'); const RevisionService = require('lib/services/RevisionService');
const shared = require('lib/components/shared/note-screen-shared.js'); const shared = require('lib/components/shared/note-screen-shared.js');
const MdToHtml = require('lib/MdToHtml'); const MarkupToHtml = require('lib/renderers/MarkupToHtml');
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
const ReactTooltip = require('react-tooltip'); const ReactTooltip = require('react-tooltip');
const { substrWithEllipsis } = require('lib/string-utils'); const { substrWithEllipsis } = require('lib/string-utils');
@ -92,6 +92,7 @@ class NoteRevisionViewerComponent extends React.PureComponent {
async reloadNote() { async reloadNote() {
let noteBody = ''; let noteBody = '';
let markupLanguage = Note.MARKUP_LANGUAGE_MARKDOWN;
if (!this.state.revisions.length || !this.state.currentRevId) { if (!this.state.revisions.length || !this.state.currentRevId) {
noteBody = _('This note has no history'); noteBody = _('This note has no history');
this.setState({ note: null }); this.setState({ note: null });
@ -100,16 +101,17 @@ class NoteRevisionViewerComponent extends React.PureComponent {
const note = await RevisionService.instance().revisionNote(this.state.revisions, revIndex); const note = await RevisionService.instance().revisionNote(this.state.revisions, revIndex);
if (!note) return; if (!note) return;
noteBody = note.body; noteBody = note.body;
markupLanguage = note.markup_language;
this.setState({ note: note }); this.setState({ note: note });
} }
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.theme);
const mdToHtml = new MdToHtml({ const markupToHtml = new MarkupToHtml({
resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/', resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/',
}); });
const result = mdToHtml.render(noteBody, theme, { const result = markupToHtml.render(markupLanguage, noteBody, theme, {
codeTheme: theme.codeThemeCss, codeTheme: theme.codeThemeCss,
userCss: this.props.customCss ? this.props.customCss : '', userCss: this.props.customCss ? this.props.customCss : '',
resources: await shared.attachedResources(noteBody), resources: await shared.attachedResources(noteBody),

View File

@ -12,7 +12,7 @@ const TagList = require('./TagList.min.js');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const MdToHtml = require('lib/MdToHtml'); const MarkupToHtml = require('lib/renderers/MarkupToHtml');
const shared = require('lib/components/shared/note-screen-shared.js'); const shared = require('lib/components/shared/note-screen-shared.js');
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
const { themeStyle } = require('../theme.js'); const { themeStyle } = require('../theme.js');
@ -72,6 +72,7 @@ class NoteTextComponent extends React.Component {
newNote: null, newNote: null,
noteTags: [], noteTags: [],
showRevisions: false, showRevisions: false,
loading: false,
// If the current note was just created, and the title has never been // If the current note was just created, and the title has never been
// changed by the user, this variable contains that note ID. Used // changed by the user, this variable contains that note ID. Used
@ -96,6 +97,7 @@ class NoteTextComponent extends React.Component {
this.lastSetMarkers_ = ''; this.lastSetMarkers_ = '';
this.lastSetMarkersOptions_ = {}; this.lastSetMarkersOptions_ = {};
this.selectionRange_ = null; this.selectionRange_ = null;
this.lastComponentUpdateNoteId_ = null;
this.noteSearchBar_ = React.createRef(); this.noteSearchBar_ = React.createRef();
// Complicated but reliable method to get editor content height // Complicated but reliable method to get editor content height
@ -326,12 +328,12 @@ class NoteTextComponent extends React.Component {
} }
} }
mdToHtml() { markupToHtml() {
if (this.mdToHtml_) return this.mdToHtml_; if (this.markupToHtml_) return this.markupToHtml_;
this.mdToHtml_ = new MdToHtml({ this.markupToHtml_ = new MarkupToHtml({
resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/', resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/',
}); });
return this.mdToHtml_; return this.markupToHtml_;
} }
async componentWillMount() { async componentWillMount() {
@ -356,7 +358,7 @@ class NoteTextComponent extends React.Component {
this.lastLoadedNoteId_ = note ? note.id : null; this.lastLoadedNoteId_ = note ? note.id : null;
this.updateHtml(note && note.body ? note.body : ''); this.updateHtml(note ? note.markup_language : null, note && note.body ? note.body : '');
eventManager.on('alarmChange', this.onAlarmChange_); eventManager.on('alarmChange', this.onAlarmChange_);
eventManager.on('noteTypeToggle', this.onNoteTypeToggle_); eventManager.on('noteTypeToggle', this.onNoteTypeToggle_);
@ -370,7 +372,7 @@ class NoteTextComponent extends React.Component {
componentWillUnmount() { componentWillUnmount() {
this.saveIfNeeded(); this.saveIfNeeded();
this.mdToHtml_ = null; this.markupToHtml_ = null;
eventManager.removeListener('alarmChange', this.onAlarmChange_); eventManager.removeListener('alarmChange', this.onAlarmChange_);
eventManager.removeListener('noteTypeToggle', this.onNoteTypeToggle_); eventManager.removeListener('noteTypeToggle', this.onNoteTypeToggle_);
@ -389,6 +391,14 @@ class NoteTextComponent extends React.Component {
this.webviewRef().closeDevTools(); this.webviewRef().closeDevTools();
} }
} }
const currentNoteId = this.state.note ? this.state.note.id : null;
if (this.lastComponentUpdateNoteId_ !== currentNoteId && this.editor_) {
const undoManager = this.editor_.editor.getSession().getUndoManager();
undoManager.reset();
this.editor_.editor.getSession().setUndoManager(undoManager);
this.lastComponentUpdateNoteId_ = currentNoteId;
}
} }
webviewRef() { webviewRef() {
@ -398,6 +408,8 @@ class NoteTextComponent extends React.Component {
} }
async saveIfNeeded(saveIfNewNote = false, options = {}) { async saveIfNeeded(saveIfNewNote = false, options = {}) {
if (this.state.loading) return;
const forceSave = saveIfNewNote && (this.state.note && !this.state.note.id); const forceSave = saveIfNewNote && (this.state.note && !this.state.note.id);
if (this.scheduleSaveTimeout_) clearTimeout(this.scheduleSaveTimeout_); if (this.scheduleSaveTimeout_) clearTimeout(this.scheduleSaveTimeout_);
@ -448,6 +460,12 @@ class NoteTextComponent extends React.Component {
await this.saveIfNeeded(); await this.saveIfNeeded();
const defer = () => {
this.setState({ loading: false });
}
this.setState({ loading: true });
const previousNote = this.state.note ? Object.assign({}, this.state.note) : null; const previousNote = this.state.note ? Object.assign({}, this.state.note) : null;
const stateNoteId = this.state.note ? this.state.note.id : null; const stateNoteId = this.state.note ? this.state.note.id : null;
@ -475,18 +493,18 @@ class NoteTextComponent extends React.Component {
noteTags = await Tag.tagsByNoteId(noteId); noteTags = await Tag.tagsByNoteId(noteId);
this.lastLoadedNoteId_ = noteId; this.lastLoadedNoteId_ = noteId;
note = noteId ? await Note.load(noteId) : null; note = noteId ? await Note.load(noteId) : null;
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading if (noteId !== this.lastLoadedNoteId_) return defer(); // Race condition - current note was changed while this one was loading
if (options.noReloadIfLocalChanges && this.isModified()) return; if (options.noReloadIfLocalChanges && this.isModified()) return defer();
// If the note hasn't been changed, exit now // If the note hasn't been changed, exit now
if (this.state.note && note) { if (this.state.note && note) {
let diff = Note.diffObjects(this.state.note, note); let diff = Note.diffObjects(this.state.note, note);
delete diff.type_; delete diff.type_;
if (!Object.getOwnPropertyNames(diff).length) return; if (!Object.getOwnPropertyNames(diff).length) return defer();
} }
} }
this.mdToHtml_ = null; this.markupToHtml_ = null;
// If we are loading nothing (noteId == null), make sure to // If we are loading nothing (noteId == null), make sure to
// set webviewReady to false too because the webview component // set webviewReady to false too because the webview component
@ -520,22 +538,6 @@ class NoteTextComponent extends React.Component {
} }
if (this.editor_) { if (this.editor_) {
// Calling setValue here does two things:
// 1. It sets the initial value as recorded by the undo manager. If we were to set it instead to "" and wait for the render
// phase to set the value, the initial value would still be "", which means pressing "undo" on a note that has just loaded
// would clear it.
// 2. It resets the undo manager - fixes https://github.com/laurent22/joplin/issues/355
// Note: calling undoManager.reset() doesn't work
try {
this.editor_.editor.getSession().setValue(note && note.body? note.body : '');
} catch (error) {
if (error.message === "Cannot read property 'match' of undefined") {
// The internals of Ace Editor throws an exception when creating a new note,
// but that can be ignored.
} else {
console.error(error);
}
}
this.editor_.editor.clearSelection(); this.editor_.editor.clearSelection();
this.editor_.editor.moveCursorTo(0,0); this.editor_.editor.moveCursorTo(0,0);
@ -600,7 +602,9 @@ class NoteTextComponent extends React.Component {
// if (newState.note) await shared.refreshAttachedResources(this, newState.note.body); // if (newState.note) await shared.refreshAttachedResources(this, newState.note.body);
this.updateHtml(newState.note ? newState.note.body : ''); await this.updateHtml(newState.note ? newState.note.markup_language : null, newState.note ? newState.note.body : '');
defer();
} }
async componentWillReceiveProps(nextProps) { async componentWillReceiveProps(nextProps) {
@ -671,7 +675,7 @@ class NoteTextComponent extends React.Component {
const arg0 = args && args.length >= 1 ? args[0] : null; const arg0 = args && args.length >= 1 ? args[0] : null;
const arg1 = args && args.length >= 2 ? args[1] : null; const arg1 = args && args.length >= 2 ? args[1] : null;
console.info('Got ipc-message: ' + msg, args); if (msg !== 'percentScroll') console.info('Got ipc-message: ' + msg, args);
if (msg.indexOf('checkboxclick:') === 0) { if (msg.indexOf('checkboxclick:') === 0) {
// Ugly hack because setting the body here will make the scrollbar // Ugly hack because setting the body here will make the scrollbar
@ -935,12 +939,20 @@ class NoteTextComponent extends React.Component {
} }
} }
async updateHtml(body = null, options = null) { async updateHtml(markupLanguage = null, body = null, options = null) {
if (!options) options = {}; if (!options) options = {};
if (!('useCustomCss' in options)) options.useCustomCss = true; if (!('useCustomCss' in options)) options.useCustomCss = true;
let bodyToRender = body; let bodyToRender = body;
if (bodyToRender === null) bodyToRender = this.state.note && this.state.note.body ? this.state.note.body : '';
if (bodyToRender === null) {
bodyToRender = this.state.note && this.state.note.body ? this.state.note.body : '';
markupLanguage = this.state.note ? this.state.note.markup_language : Note.MARKUP_LANGUAGE_MARKDOWN;
}
if (!markupLanguage) markupLanguage = Note.MARKUP_LANGUAGE_MARKDOWN;
const resources = await shared.attachedResources(bodyToRender);
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.theme);
@ -948,7 +960,7 @@ class NoteTextComponent extends React.Component {
codeTheme: theme.codeThemeCss, codeTheme: theme.codeThemeCss,
postMessageSyntax: 'ipcProxySendToHost', postMessageSyntax: 'ipcProxySendToHost',
userCss: options.useCustomCss ? this.props.customCss : '', userCss: options.useCustomCss ? this.props.customCss : '',
resources: await shared.attachedResources(bodyToRender), resources: resources,
codeHighlightCacheKey: this.state.note ? this.state.note.id : null, codeHighlightCacheKey: this.state.note ? this.state.note.id : null,
}; };
@ -958,10 +970,10 @@ class NoteTextComponent extends React.Component {
if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) { if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) {
// Fixes https://github.com/laurent22/joplin/issues/217 // Fixes https://github.com/laurent22/joplin/issues/217
bodyToRender = '*' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*'; bodyToRender = '<i>' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '</i>';
} }
const result = this.mdToHtml().render(bodyToRender, theme, mdOptions); const result = this.markupToHtml().render(markupLanguage, bodyToRender, theme, mdOptions);
this.setState({ this.setState({
bodyHtml: result.html, bodyHtml: result.html,
@ -1086,7 +1098,7 @@ class NoteTextComponent extends React.Component {
lastSavedNote: Object.assign({}, note), lastSavedNote: Object.assign({}, note),
}); });
this.updateHtml(note.body); this.updateHtml(note.markup_language, note.body);
} catch (error) { } catch (error) {
reg.logger().error(error); reg.logger().error(error);
bridge().showErrorMessageBox(error.message); bridge().showErrorMessageBox(error.message);
@ -1115,13 +1127,13 @@ class NoteTextComponent extends React.Component {
const previousTheme = Setting.value('theme'); const previousTheme = Setting.value('theme');
Setting.setValue('theme', Setting.THEME_LIGHT); Setting.setValue('theme', Setting.THEME_LIGHT);
this.lastSetHtml_ = ''; this.lastSetHtml_ = '';
await this.updateHtml(tempBody, { useCustomCss: false }); await this.updateHtml(this.state.note.markup_language, tempBody, { useCustomCss: false });
this.forceUpdate(); this.forceUpdate();
const restoreSettings = async () => { const restoreSettings = async () => {
Setting.setValue('theme', previousTheme); Setting.setValue('theme', previousTheme);
this.lastSetHtml_ = ''; this.lastSetHtml_ = '';
await this.updateHtml(previousBody); await this.updateHtml(this.state.note.markup_language, previousBody);
this.forceUpdate(); this.forceUpdate();
} }
@ -1419,6 +1431,8 @@ class NoteTextComponent extends React.Component {
} }
createToolbarItems(note) { createToolbarItems(note) {
const markupLanguage = note.markup_language;
const toolbarItems = []; const toolbarItems = [];
if (note && this.state.folder && ['Search', 'Tag'].includes(this.props.notesParentType)) { if (note && this.state.folder && ['Search', 'Tag'].includes(this.props.notesParentType)) {
toolbarItems.push({ toolbarItems.push({
@ -1431,7 +1445,6 @@ class NoteTextComponent extends React.Component {
noteId: note.id, noteId: note.id,
}); });
}, },
// enabled: false,
}); });
} }
@ -1454,83 +1467,85 @@ class NoteTextComponent extends React.Component {
}); });
} }
toolbarItems.push({ if (note.markup_language === Note.MARKUP_LANGUAGE_MARKDOWN) {
tooltip: _('Bold'), toolbarItems.push({
iconName: 'fa-bold', tooltip: _('Bold'),
onClick: () => { return this.commandTextBold(); }, iconName: 'fa-bold',
}); onClick: () => { return this.commandTextBold(); },
});
toolbarItems.push({ toolbarItems.push({
tooltip: _('Italic'), tooltip: _('Italic'),
iconName: 'fa-italic', iconName: 'fa-italic',
onClick: () => { return this.commandTextItalic(); }, onClick: () => { return this.commandTextItalic(); },
}); });
toolbarItems.push({ toolbarItems.push({
type: 'separator', type: 'separator',
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Hyperlink'), tooltip: _('Hyperlink'),
iconName: 'fa-link', iconName: 'fa-link',
onClick: () => { return this.commandTextLink(); }, onClick: () => { return this.commandTextLink(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Code'), tooltip: _('Code'),
iconName: 'fa-code', iconName: 'fa-code',
onClick: () => { return this.commandTextCode(); }, onClick: () => { return this.commandTextCode(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Attach file'), tooltip: _('Attach file'),
iconName: 'fa-paperclip', iconName: 'fa-paperclip',
onClick: () => { return this.commandAttachFile(); }, onClick: () => { return this.commandAttachFile(); },
}); });
toolbarItems.push({ toolbarItems.push({
type: 'separator', type: 'separator',
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Numbered List'), tooltip: _('Numbered List'),
iconName: 'fa-list-ol', iconName: 'fa-list-ol',
onClick: () => { return this.commandTextListOl(); }, onClick: () => { return this.commandTextListOl(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Bulleted List'), tooltip: _('Bulleted List'),
iconName: 'fa-list-ul', iconName: 'fa-list-ul',
onClick: () => { return this.commandTextListUl(); }, onClick: () => { return this.commandTextListUl(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Checkbox'), tooltip: _('Checkbox'),
iconName: 'fa-check-square', iconName: 'fa-check-square',
onClick: () => { return this.commandTextCheckbox(); }, onClick: () => { return this.commandTextCheckbox(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Heading'), tooltip: _('Heading'),
iconName: 'fa-header', iconName: 'fa-header',
onClick: () => { return this.commandTextHeading(); }, onClick: () => { return this.commandTextHeading(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Horizontal Rule'), tooltip: _('Horizontal Rule'),
iconName: 'fa-ellipsis-h', iconName: 'fa-ellipsis-h',
onClick: () => { return this.commandTextHorizontalRule(); }, onClick: () => { return this.commandTextHorizontalRule(); },
}); });
toolbarItems.push({ toolbarItems.push({
tooltip: _('Insert Date Time'), tooltip: _('Insert Date Time'),
iconName: 'fa-calendar-plus-o', iconName: 'fa-calendar-plus-o',
onClick: () => { return this.commandDateTime(); }, onClick: () => { return this.commandDateTime(); },
}); });
toolbarItems.push({ toolbarItems.push({
type: 'separator', type: 'separator',
}); });
}
if (note && this.props.watchedNoteFiles.indexOf(note.id) >= 0) { if (note && this.props.watchedNoteFiles.indexOf(note.id) >= 0) {
toolbarItems.push({ toolbarItems.push({
@ -1646,6 +1661,7 @@ class NoteTextComponent extends React.Component {
const style = this.props.style; const style = this.props.style;
const note = this.state.note; const note = this.state.note;
const body = note && note.body ? note.body : ''; const body = note && note.body ? note.body : '';
const markupLanguage = note ? note.markup_language : Note.MARKUP_LANGUAGE_MARKDOWN;
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.theme);
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer']; const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
const isTodo = note && !!note.is_todo; const isTodo = note && !!note.is_todo;
@ -1855,7 +1871,7 @@ class NoteTextComponent extends React.Component {
delete editorRootStyle.fontSize; delete editorRootStyle.fontSize;
const editor = <AceEditor const editor = <AceEditor
value={body} value={body}
mode="markdown" mode={markupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'text' : 'markdown'}
theme={editorRootStyle.editorTheme} theme={editorRootStyle.editorTheme}
style={editorRootStyle} style={editorRootStyle}
width={editorStyle.width + 'px'} width={editorStyle.width + 'px'}

View File

@ -9,10 +9,15 @@
} }
#content { #content {
/* Needs this in case the content contains elements with absolute positioning */
/* Without this they would just stay at a fixed position when scrolling */
position: relative;
overflow-y: auto; overflow-y: auto;
height: 100%;
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
/* Note: the height is set via updateBodyHeight(). Setting it here to 100% */
/* won't work with some pages due to the position: relative */
} }
mark { mark {
@ -253,6 +258,7 @@
// The body element needs to have a fixed height for the content to be scrollable // The body element needs to have a fixed height for the content to be scrollable
function updateBodyHeight() { function updateBodyHeight() {
document.getElementById('body').style.height = window.innerHeight + 'px'; document.getElementById('body').style.height = window.innerHeight + 'px';
document.getElementById('content').style.height = window.innerHeight + 'px';
} }
contentElement.addEventListener('scroll', webviewLib.logEnabledEventHandler(e => { contentElement.addEventListener('scroll', webviewLib.logEnabledEventHandler(e => {

View File

@ -154,9 +154,9 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
}, },
"acorn": { "acorn": {
"version": "5.7.3", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.0.tgz",
"integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" "integrity": "sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw=="
}, },
"acorn-globals": { "acorn-globals": {
"version": "4.3.2", "version": "4.3.2",
@ -165,13 +165,6 @@
"requires": { "requires": {
"acorn": "^6.0.1", "acorn": "^6.0.1",
"acorn-walk": "^6.0.1" "acorn-walk": "^6.0.1"
},
"dependencies": {
"acorn": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.0.tgz",
"integrity": "sha512-8oe72N3WPMjA+2zVG71Ia0nXZ8DpQH+QyyHO+p06jT8eg8FGG3FbcUIi8KziHlAfheJQZeoqbvq1mQSQHXKYLw=="
}
} }
}, },
"acorn-walk": { "acorn-walk": {
@ -1260,6 +1253,15 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
"integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA="
}, },
"camel-case": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
"integrity": "sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=",
"requires": {
"no-case": "^2.2.0",
"upper-case": "^1.1.1"
}
},
"camelcase": { "camelcase": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
@ -1382,6 +1384,21 @@
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz",
"integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q=="
}, },
"clean-css": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz",
"integrity": "sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==",
"requires": {
"source-map": "~0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"cli-boxes": { "cli-boxes": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
@ -1712,18 +1729,6 @@
"abab": "^2.0.0", "abab": "^2.0.0",
"whatwg-mimetype": "^2.2.0", "whatwg-mimetype": "^2.2.0",
"whatwg-url": "^7.0.0" "whatwg-url": "^7.0.0"
},
"dependencies": {
"whatwg-url": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz",
"integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==",
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
}
} }
}, },
"debug": { "debug": {
@ -3434,6 +3439,11 @@
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
}, },
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"highlight.js": { "highlight.js": {
"version": "9.15.6", "version": "9.15.6",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.6.tgz", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.6.tgz",
@ -3473,6 +3483,27 @@
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
"integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8="
}, },
"html-minifier": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz",
"integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==",
"requires": {
"camel-case": "^3.0.0",
"clean-css": "^4.2.1",
"commander": "^2.19.0",
"he": "^1.2.0",
"param-case": "^2.1.1",
"relateurl": "^0.2.7",
"uglify-js": "^3.5.1"
},
"dependencies": {
"commander": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
}
}
},
"http-errors": { "http-errors": {
"version": "1.7.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.1.tgz",
@ -3805,6 +3836,69 @@
"css": "^2.2.4", "css": "^2.2.4",
"html-entities": "^1.2.1", "html-entities": "^1.2.1",
"jsdom": "^11.9.0" "jsdom": "^11.9.0"
},
"dependencies": {
"acorn": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
"integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw=="
},
"jsdom": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
"integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
"requires": {
"abab": "^2.0.0",
"acorn": "^5.5.3",
"acorn-globals": "^4.1.0",
"array-equal": "^1.0.0",
"cssom": ">= 0.3.2 < 0.4.0",
"cssstyle": "^1.0.0",
"data-urls": "^1.0.0",
"domexception": "^1.0.1",
"escodegen": "^1.9.1",
"html-encoding-sniffer": "^1.0.2",
"left-pad": "^1.3.0",
"nwsapi": "^2.0.7",
"parse5": "4.0.0",
"pn": "^1.1.0",
"request": "^2.87.0",
"request-promise-native": "^1.0.5",
"sax": "^1.2.4",
"symbol-tree": "^3.2.2",
"tough-cookie": "^2.3.4",
"w3c-hr-time": "^1.0.1",
"webidl-conversions": "^4.0.2",
"whatwg-encoding": "^1.0.3",
"whatwg-mimetype": "^2.1.0",
"whatwg-url": "^6.4.1",
"ws": "^5.2.0",
"xml-name-validator": "^3.0.0"
}
},
"parse5": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
},
"whatwg-url": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz",
"integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==",
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
},
"ws": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"requires": {
"async-limiter": "~1.0.0"
}
}
} }
}, },
"joplin-turndown-plugin-gfm": { "joplin-turndown-plugin-gfm": {
@ -3833,39 +3927,6 @@
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"optional": true "optional": true
}, },
"jsdom": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz",
"integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==",
"requires": {
"abab": "^2.0.0",
"acorn": "^5.5.3",
"acorn-globals": "^4.1.0",
"array-equal": "^1.0.0",
"cssom": ">= 0.3.2 < 0.4.0",
"cssstyle": "^1.0.0",
"data-urls": "^1.0.0",
"domexception": "^1.0.1",
"escodegen": "^1.9.1",
"html-encoding-sniffer": "^1.0.2",
"left-pad": "^1.3.0",
"nwsapi": "^2.0.7",
"parse5": "4.0.0",
"pn": "^1.1.0",
"request": "^2.87.0",
"request-promise-native": "^1.0.5",
"sax": "^1.2.4",
"symbol-tree": "^3.2.2",
"tough-cookie": "^2.3.4",
"w3c-hr-time": "^1.0.1",
"webidl-conversions": "^4.0.2",
"whatwg-encoding": "^1.0.3",
"whatwg-mimetype": "^2.1.0",
"whatwg-url": "^6.4.1",
"ws": "^5.2.0",
"xml-name-validator": "^3.0.0"
}
},
"jsesc": { "jsesc": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz",
@ -4115,6 +4176,11 @@
"signal-exit": "^3.0.0" "signal-exit": "^3.0.0"
} }
}, },
"lower-case": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
"integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw="
},
"lowercase-keys": { "lowercase-keys": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
@ -4479,6 +4545,14 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true "dev": true
}, },
"no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
"integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==",
"requires": {
"lower-case": "^1.1.1"
}
},
"node-abi": { "node-abi": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.7.1.tgz", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.7.1.tgz",
@ -4929,6 +5003,14 @@
"semver": "^5.1.0" "semver": "^5.1.0"
} }
}, },
"param-case": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
"integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=",
"requires": {
"no-case": "^2.2.0"
}
},
"parse-color": { "parse-color": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz",
@ -4987,11 +5069,6 @@
"error-ex": "^1.2.0" "error-ex": "^1.2.0"
} }
}, },
"parse5": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
},
"path-exists": { "path-exists": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
@ -5600,6 +5677,11 @@
"rc": "^1.0.1" "rc": "^1.0.1"
} }
}, },
"relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk="
},
"remove-trailing-separator": { "remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@ -6376,6 +6458,32 @@
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz",
"integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg==" "integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg=="
}, },
"uglify-js": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
"integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==",
"requires": {
"commander": "~2.20.0",
"source-map": "~0.6.1"
},
"dependencies": {
"commander": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
"integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ=="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"uglifycss": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/uglifycss/-/uglifycss-0.0.29.tgz",
"integrity": "sha512-J2SQ2QLjiknNGbNdScaNZsXgmMGI0kYNrXaDlr4obnPW9ni1jljb1NeEVWAiTgZ8z+EBWP2ozfT9vpy03rjlMQ=="
},
"uid-safe": { "uid-safe": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
@ -6477,6 +6585,11 @@
} }
} }
}, },
"upper-case": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz",
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg="
},
"uri-js": { "uri-js": {
"version": "4.2.2", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
@ -6616,9 +6729,9 @@
"integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g=="
}, },
"whatwg-url": { "whatwg-url": {
"version": "6.5.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz",
"integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==",
"requires": { "requires": {
"lodash.sortby": "^4.7.0", "lodash.sortby": "^4.7.0",
"tr46": "^1.0.1", "tr46": "^1.0.1",
@ -6720,14 +6833,6 @@
"signal-exit": "^3.0.2" "signal-exit": "^3.0.2"
} }
}, },
"ws": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz",
"integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==",
"requires": {
"async-limiter": "~1.0.0"
}
},
"xdg-basedir": { "xdg-basedir": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz",

View File

@ -100,6 +100,7 @@
"fs-extra": "^5.0.0", "fs-extra": "^5.0.0",
"highlight.js": "^9.15.6", "highlight.js": "^9.15.6",
"html-entities": "^1.2.1", "html-entities": "^1.2.1",
"html-minifier": "^4.0.0",
"image-type": "^3.0.0", "image-type": "^3.0.0",
"joplin-turndown": "^4.0.17", "joplin-turndown": "^4.0.17",
"joplin-turndown-plugin-gfm": "^1.0.8", "joplin-turndown-plugin-gfm": "^1.0.8",
@ -149,6 +150,7 @@
"syswide-cas": "^5.1.0", "syswide-cas": "^5.1.0",
"tar": "^4.4.4", "tar": "^4.4.4",
"tcp-port-used": "^0.1.2", "tcp-port-used": "^0.1.2",
"uglifycss": "0.0.29",
"url-parse": "^1.4.3", "url-parse": "^1.4.3",
"uuid": "^3.2.1", "uuid": "^3.2.1",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",

View File

@ -1,42 +0,0 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../utils');
function installRule(markdownIt, mdOptions, ruleOptions) {
const defaultRender = markdownIt.renderer.rules.image;
markdownIt.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx];
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);
const resourceId = Resource.urlToId(src);
const result = ruleOptions.resources[resourceId];
const resource = result ? result.item : null;
const resourceStatus = utils.resourceStatus(result);
if (resourceStatus !== 'ready') {
const icon = utils.resourceStatusImage(resourceStatus);
return '<div class="not-loaded-resource resource-status-' + resourceStatus + '" data-resource-id="' + resourceId + '">' + '<img src="data:image/svg+xml;utf8,' + htmlentities(icon) + '"/>' + '</div>';
}
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {
let realSrc = './' + Resource.filename(resource);
if (ruleOptions.resourceBaseUrl) realSrc = ruleOptions.resourceBaseUrl + realSrc;
let output = '<img data-from-md data-resource-id="' + resource.id + '" title="' + htmlentities(title) + '" src="' + realSrc + '"/>';
return output;
}
return defaultRender(tokens, idx, options, env, self);
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@ -6,7 +6,7 @@ const Resource = require('lib/models/Resource.js');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const { shim } = require('lib/shim'); const { shim } = require('lib/shim');
const MdToHtml = require('lib/MdToHtml.js'); const MdToHtml = require('lib/renderers/MdToHtml.js');
const shared = require('lib/components/shared/note-screen-shared.js'); const shared = require('lib/components/shared/note-screen-shared.js');
class NoteBodyViewer extends Component { class NoteBodyViewer extends Component {

View File

@ -0,0 +1,92 @@
const urlUtils = require('lib/urlUtils.js');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
// [\s\S] instead of . for multiline matching
// https://stackoverflow.com/a/16119722/561309
const imageRegex = /<img([\s\S]*?)src=["']([\s\S]*?)["']([\s\S]*?)>/gi
const anchorRegex = /<a([\s\S]*?)href=["']([\s\S]*?)["']([\s\S]*?)>/gi
class HtmlUtils {
headAndBodyHtml(doc) {
const output = [];
if (doc.head) output.push(doc.head.innerHTML);
if (doc.body) output.push(doc.body.innerHTML);
return output.join('\n');
}
extractImageUrls(html) {
if (!html) return [];
const output = [];
let matches;
while (matches = imageRegex.exec(html)) {
output.push(matches[2]);
}
return output;
}
replaceImageUrls(html, callback) {
return this.processImageTags(html, data => {
const newSrc = callback(data.src);
return {
type: 'replaceSource',
src: newSrc,
};
});
}
processImageTags(html, callback) {
if (!html) return '';
return html.replace(imageRegex, (v, before, src, after) => {
const action = callback({ src: src });
console.info('src', src);
if (!action) return '<img' + before + 'src="' + src + '"' + after + '>';
if (action.type === 'replaceElement') {
return action.html;
}
if (action.type === 'replaceSource') {
return '<img' + before + 'src="' + action.src + '"' + after + '>';
}
if (action.type === 'setAttributes') {
const attrHtml = this.attributesHtml(action.attrs);
return '<img' + before + attrHtml + after + '>';
}
throw new Error('Invalid action: ' + action.type);
});
}
prependBaseUrl(html, baseUrl) {
if (!html) return '';
return html.replace(anchorRegex, (v, before, href, after) => {
const newHref = urlUtils.prependBaseUrl(href, baseUrl);
return '<a' + before + 'href="' + newHref + '"' + after + '>';
});
}
attributesHtml(attr) {
const output = [];
for (const n in attr) {
if (!attr.hasOwnProperty(n)) continue;
output.push(n + '="' + htmlentities(attr[n]) + '"');
}
return output.join(' ');
}
}
const htmlUtils = new HtmlUtils();
module.exports = htmlUtils;

View File

@ -303,7 +303,7 @@ class JoplinDatabase extends Database {
// must be set in the synchronizer too. // must be set in the synchronizer too.
// Note: v16 and v17 don't do anything. They were used to debug an issue. // Note: v16 and v17 don't do anything. They were used to debug an issue.
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]; const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24];
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion); let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
@ -634,6 +634,10 @@ class JoplinDatabase extends Database {
queries.push('CREATE UNIQUE INDEX key_values_key ON key_values (key)'); queries.push('CREATE UNIQUE INDEX key_values_key ON key_values (key)');
} }
if (targetVersion == 24) {
queries.push('ALTER TABLE notes ADD COLUMN `markup_language` INT NOT NULL DEFAULT 1'); // 1: Markdown, 2: HTML
}
queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] }); queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] });
try { try {

View File

@ -1,7 +1,7 @@
const stringPadding = require('string-padding'); const stringPadding = require('string-padding');
const urlUtils = require('lib/urlUtils'); const urlUtils = require('lib/urlUtils');
const MarkdownIt = require('markdown-it'); const MarkdownIt = require('markdown-it');
const setupLinkify = require('lib/MdToHtml/setupLinkify'); const setupLinkify = require('lib/renderers/MdToHtml/setupLinkify');
const markdownUtils = { const markdownUtils = {

View File

@ -0,0 +1,21 @@
const markdownUtils = require('lib/markdownUtils');
const htmlUtils = require('lib/htmlUtils');
const Note = require('lib/models/Note');
class MarkupLanguageUtils {
lib_(language) {
if (language === Note.MARKUP_LANGUAGE_HTML) return htmlUtils;
if (language === Note.MARKUP_LANGUAGE_MARKDOWN) return markdownUtils;
throw new Error('Unsupported markup language: ' + language);
}
extractImageUrls(language, text) {
return this.lib_(language).extractImageUrls(text);
}
}
const markupLanguageUtils = new MarkupLanguageUtils();
module.exports = markupLanguageUtils;

View File

@ -149,6 +149,18 @@ class BaseItem extends BaseModel {
return null; return null;
} }
static async loadItemsByIds(ids) {
const classes = this.syncItemClassNames();
let output = [];
for (let i = 0; i < classes.length; i++) {
const ItemClass = this.getClass(classes[i]);
const sql = 'SELECT * FROM ' + ItemClass.tableName() + ' WHERE id IN ("' + ids.join('","') + '")';
const models = await ItemClass.modelSelectAll(sql);
output = output.concat(models);
}
return output;
}
static loadItemByField(itemType, field, value) { static loadItemByField(itemType, field, value) {
let ItemClass = this.itemClass(itemType); let ItemClass = this.itemClass(itemType);
return ItemClass.loadByField(field, value); return ItemClass.loadByField(field, value);

View File

@ -129,7 +129,7 @@ class Note extends BaseItem {
matches = matches.concat(matches2) matches = matches.concat(matches2)
// For example: <img src=":/fcca2938a96a22570e8eae2565bc6b0b"/> // For example: <img src=":/fcca2938a96a22570e8eae2565bc6b0b"/>
const imgRegex = /<img.*?src=["']:\/([a-zA-Z0-9]{32})["']/g const imgRegex = /<img[\s\S]*?src=["']:\/([a-zA-Z0-9]{32})["'][\s\S]*?>/gi
const imgMatches = []; const imgMatches = [];
while (true) { while (true) {
const m = imgRegex.exec(body); const m = imgRegex.exec(body);
@ -142,15 +142,8 @@ class Note extends BaseItem {
static async linkedItems(body) { static async linkedItems(body) {
const itemIds = this.linkedItemIds(body); const itemIds = this.linkedItemIds(body);
const output = []; const r = await BaseItem.loadItemsByIds(itemIds);
return r;
for (let i = 0; i < itemIds.length; i++) {
const item = await BaseItem.loadItemById(itemIds[i]);
if (!item) continue;
output.push(item);
}
return output;
} }
static async linkedItemIdsByType(type, body) { static async linkedItemIdsByType(type, body) {
@ -166,7 +159,7 @@ class Note extends BaseItem {
} }
static async linkedResourceIds(body) { static async linkedResourceIds(body) {
return await this.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, body); return this.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, body);
} }
static async replaceResourceInternalToExternalLinks(body) { static async replaceResourceInternalToExternalLinks(body) {
@ -629,9 +622,18 @@ class Note extends BaseItem {
return false; return false;
} }
static markupLanguageToLabel(markupLanguageId) {
if (markupLanguageId === Note.MARKUP_LANGUAGE_MARKDOWN) return 'Markdown';
if (markupLanguageId === Note.MARKUP_LANGUAGE_HTML) return 'HTML';
throw new Error('Invalid markup language ID: ' + markupLanguageId);
}
} }
Note.updateGeolocationEnabled_ = true; Note.updateGeolocationEnabled_ = true;
Note.geolocationUpdating_ = false; Note.geolocationUpdating_ = false;
Note.MARKUP_LANGUAGE_MARKDOWN = 1;
Note.MARKUP_LANGUAGE_HTML = 2;
module.exports = Note; module.exports = Note;

View File

@ -0,0 +1,39 @@
const Resource = require('lib/models/Resource');
const htmlUtils = require('lib/htmlUtils');
const utils = require('./utils');
class HtmlToHtml {
constructor(options) {
this.resourceBaseUrl_ = 'resourceBaseUrl' in options ? options.resourceBaseUrl : null;
}
render(markup, theme, options) {
const html = htmlUtils.processImageTags(markup, data => {
if (!data.src) return null;
const r = utils.imageReplacement(data.src, options.resources, this.resourceBaseUrl_);
if (!r) return null;
if (typeof r === 'string') {
return {
type: 'replaceElement',
html: r,
};
} else {
return {
type: 'setAttributes',
attrs: r,
};
}
});
return {
html: html,
cssFiles: [],
}
}
}
module.exports = HtmlToHtml;

View File

@ -0,0 +1,39 @@
const MdToHtml = require('./MdToHtml');
const HtmlToHtml = require('./HtmlToHtml');
const Note = require('lib/models/Note');
class MarkupToHtml {
constructor(options) {
this.options_ = options;
this.renderers_ = {};
}
renderer(markupLanguage) {
if (this.renderers_[markupLanguage]) return this.renderers_[markupLanguage];
let RendererClass = null;
if (markupLanguage === Note.MARKUP_LANGUAGE_MARKDOWN) {
RendererClass = MdToHtml;
} else if (markupLanguage === Note.MARKUP_LANGUAGE_HTML) {
RendererClass = HtmlToHtml;
} else {
throw new Error('Invalid markup language: ' + markupLanguage);
}
this.renderers_[markupLanguage] = new RendererClass(this.options_);
return this.renderers_[markupLanguage];
}
injectedJavaScript() {
return '';
}
render(markupLanguage, markup, theme, options) {
return this.renderer(markupLanguage).render(markup, theme, options);
}
}
module.exports = MarkupToHtml;

View File

@ -6,8 +6,8 @@ const { shim } = require('lib/shim.js');
const { _ } = require('lib/locale'); const { _ } = require('lib/locale');
const md5 = require('md5'); const md5 = require('md5');
const StringUtils = require('lib/string-utils.js'); const StringUtils = require('lib/string-utils.js');
const noteStyle = require('./MdToHtml/noteStyle'); const noteStyle = require('./noteStyle');
const Setting = require('./models/Setting.js'); const Setting = require('lib/models/Setting.js');
const rules = { const rules = {
image: require('./MdToHtml/rules/image'), image: require('./MdToHtml/rules/image'),
checkbox: require('./MdToHtml/rules/checkbox'), checkbox: require('./MdToHtml/rules/checkbox'),

View File

@ -1,7 +1,7 @@
const Entities = require('html-entities').AllHtmlEntities; const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode; const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
const utils = require('../utils'); const utils = require('../../utils');
let checkboxIndex_ = -1; let checkboxIndex_ = -1;

View File

@ -1,7 +1,7 @@
const Entities = require('html-entities').AllHtmlEntities; const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode; const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
const utils = require('../utils'); const utils = require('../../utils');
const StringUtils = require('lib/string-utils.js'); const StringUtils = require('lib/string-utils.js');
const md5 = require('md5'); const md5 = require('md5');

View File

@ -1,26 +1,13 @@
const Entities = require('html-entities').AllHtmlEntities; const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode; const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
const utils = require('../utils'); const htmlUtils = require('lib/htmlUtils.js');
const utils = require('../../utils');
function renderImageHtml(before, src, after, ruleOptions) { function renderImageHtml(before, src, after, ruleOptions) {
const resourceId = Resource.urlToId(src); const r = utils.imageReplacement(src, ruleOptions.resources, ruleOptions.resourceBaseUrl);
const result = ruleOptions.resources[resourceId]; if (typeof r === 'string') return r;
const resource = result ? result.item : null; if (r) return '<img ' + before + ' ' + htmlUtils.attributesHtml(r) + ' ' + after + '/>';
const resourceStatus = utils.resourceStatus(result);
if (resourceStatus !== 'ready') {
const icon = utils.resourceStatusImage(resourceStatus);
return '<div class="not-loaded-resource resource-status-' + resourceStatus + '" data-resource-id="' + resourceId + '">' + '<img src="data:image/svg+xml;utf8,' + htmlentities(icon) + '"/>' + '</div>';
}
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {
let newSrc = './' + Resource.filename(resource);
if (ruleOptions.resourceBaseUrl) newSrc = ruleOptions.resourceBaseUrl + newSrc;
return '<img ' + before + ' data-resource-id="' + resource.id + '" src="' + newSrc + '" ' + after + '/>';
}
return '[Image: ' + htmlentities(resource.title) + ' (' + htmlentities(mime) + ')]'; return '[Image: ' + htmlentities(resource.title) + ' (' + htmlentities(mime) + ')]';
} }
@ -33,7 +20,7 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
return self.renderToken(tokens, idx, options); return self.renderToken(tokens, idx, options);
}; };
const imageRegex = /<img(.*?)src=["'](.*?)["'](.*?)\/>/ const imageRegex = /<img(.*?)src=["'](.*?)["'](.*?)>/gi
const handleImageTags = function(defaultRender) { const handleImageTags = function(defaultRender) {
return function(tokens, idx, options, env, self) { return function(tokens, idx, options, env, self) {

View File

@ -0,0 +1,29 @@
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js');
const utils = require('../../utils');
const htmlUtils = require('lib/htmlUtils.js');
function installRule(markdownIt, mdOptions, ruleOptions) {
const defaultRender = markdownIt.renderer.rules.image;
markdownIt.renderer.rules.image = (tokens, idx, options, env, self) => {
const token = tokens[idx];
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);
const r = utils.imageReplacement(src, ruleOptions.resources, ruleOptions.resourceBaseUrl);
if (typeof r === 'string') return r;
if (r) return '<img data-from-md ' + htmlUtils.attributesHtml(Object.assign({}, r, { title: title })) + '/>';
return defaultRender(tokens, idx, options, env, self);
};
}
module.exports = function(context, ruleOptions) {
return function(md, mdOptions) {
installRule(md, mdOptions, ruleOptions);
};
};

View File

@ -1,7 +1,7 @@
const Entities = require('html-entities').AllHtmlEntities; const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode; const htmlentities = (new Entities()).encode;
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
const utils = require('../utils'); const utils = require('../../utils');
const loaderImage = '<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="16px" height="16px" viewBox="0 0 128 128" xml:space="preserve"><g><circle cx="16" cy="64" r="16" fill="#000000" fill-opacity="1"/><circle cx="16" cy="64" r="16" fill="#555555" fill-opacity="0.67" transform="rotate(45,64,64)"/><circle cx="16" cy="64" r="16" fill="#949494" fill-opacity="0.42" transform="rotate(90,64,64)"/><circle cx="16" cy="64" r="16" fill="#cccccc" fill-opacity="0.2" transform="rotate(135,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(180,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(225,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(270,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(315,64,64)"/><animateTransform attributeName="transform" type="rotate" values="0 64 64;315 64 64;270 64 64;225 64 64;180 64 64;135 64 64;90 64 64;45 64 64" calcMode="discrete" dur="720ms" repeatCount="indefinite"></animateTransform></g></svg>'; const loaderImage = '<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="16px" height="16px" viewBox="0 0 128 128" xml:space="preserve"><g><circle cx="16" cy="64" r="16" fill="#000000" fill-opacity="1"/><circle cx="16" cy="64" r="16" fill="#555555" fill-opacity="0.67" transform="rotate(45,64,64)"/><circle cx="16" cy="64" r="16" fill="#949494" fill-opacity="0.42" transform="rotate(90,64,64)"/><circle cx="16" cy="64" r="16" fill="#cccccc" fill-opacity="0.2" transform="rotate(135,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(180,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(225,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(270,64,64)"/><circle cx="16" cy="64" r="16" fill="#e1e1e1" fill-opacity="0.12" transform="rotate(315,64,64)"/><animateTransform attributeName="transform" type="rotate" values="0 64 64;315 64 64;270 64 64;225 64 64;180 64 64;135 64 64;90 64 64;45 64 64" calcMode="discrete" dur="720ms" repeatCount="indefinite"></animateTransform></g></svg>';

View File

@ -1,4 +1,6 @@
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const utils = {}; const utils = {};
@ -68,7 +70,7 @@ utils.resourceStatusFile = function(state) {
throw new Error('Unknown state: ' + state); throw new Error('Unknown state: ' + state);
} }
utils.resourceStatus = function(resourceInfo, localState) { utils.resourceStatus = function(resourceInfo) {
let resourceStatus = 'ready'; let resourceStatus = 'ready';
if (resourceInfo) { if (resourceInfo) {
@ -91,4 +93,30 @@ utils.resourceStatus = function(resourceInfo, localState) {
return resourceStatus; return resourceStatus;
} }
utils.imageReplacement = function(src, resources, resourceBaseUrl) {
if (!Resource.isResourceUrl(src)) return null;
const resourceId = Resource.urlToId(src);
const result = resources[resourceId];
const resource = result ? result.item : null;
const resourceStatus = utils.resourceStatus(result);
if (resourceStatus !== 'ready') {
const icon = utils.resourceStatusImage(resourceStatus);
return '<div class="not-loaded-resource resource-status-' + resourceStatus + '" data-resource-id="' + resourceId + '">' + '<img src="data:image/svg+xml;utf8,' + htmlentities(icon) + '"/>' + '</div>';
}
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (Resource.isSupportedImageMimeType(mime)) {
let newSrc = './' + Resource.filename(resource);
if (resourceBaseUrl) newSrc = resourceBaseUrl + newSrc;
return {
'data-resource-id': resource.id,
'src': newSrc,
};
}
return null;
}
module.exports = utils; module.exports = utils;

View File

@ -7,12 +7,15 @@ const BaseItem = require('lib/models/BaseItem');
const BaseModel = require('lib/BaseModel'); const BaseModel = require('lib/BaseModel');
const Setting = require('lib/models/Setting'); const Setting = require('lib/models/Setting');
const markdownUtils = require('lib/markdownUtils'); const markdownUtils = require('lib/markdownUtils');
const htmlUtils = require('lib/htmlUtils');
const markupLanguageUtils = require('lib/markupLanguageUtils');
const mimeUtils = require('lib/mime-utils.js').mime; const mimeUtils = require('lib/mime-utils.js').mime;
const { Logger } = require('lib/logger.js'); const { Logger } = require('lib/logger.js');
const md5 = require('md5'); const md5 = require('md5');
const { shim } = require('lib/shim'); const { shim } = require('lib/shim');
const HtmlToMd = require('lib/HtmlToMd'); const HtmlToMd = require('lib/HtmlToMd');
const urlUtils = require('lib/urlUtils.js'); const urlUtils = require('lib/urlUtils.js');
const ArrayUtils = require('lib/ArrayUtils.js');
const { netUtils } = require('lib/net-utils'); const { netUtils } = require('lib/net-utils');
const { fileExtension, safeFileExtension, safeFilename, filename } = require('lib/path-utils'); const { fileExtension, safeFileExtension, safeFilename, filename } = require('lib/path-utils');
const ApiResponse = require('lib/services/rest/ApiResponse'); const ApiResponse = require('lib/services/rest/ApiResponse');
@ -369,7 +372,7 @@ class Api {
let note = await this.requestNoteToNote_(requestNote); let note = await this.requestNoteToNote_(requestNote);
const imageUrls = markdownUtils.extractImageUrls(note.body); const imageUrls = ArrayUtils.unique(markupLanguageUtils.extractImageUrls(note.markup_language, note.body));
this.logger().info('Request (' + requestId + '): Downloading images: ' + imageUrls.length); this.logger().info('Request (' + requestId + '): Downloading images: ' + imageUrls.length);
@ -379,7 +382,7 @@ class Api {
result = await this.createResourcesFromPaths_(result); result = await this.createResourcesFromPaths_(result);
await this.removeTempFiles_(result); await this.removeTempFiles_(result);
note.body = this.replaceImageUrlsByResources_(note.body, result, imageSizes); note.body = this.replaceImageUrlsByResources_(note.markup_language, note.body, result, imageSizes);
this.logger().info('Request (' + requestId + '): Saving note...'); this.logger().info('Request (' + requestId + '): Saving note...');
@ -430,18 +433,45 @@ class Api {
if (requestNote.id) output.id = requestNote.id; if (requestNote.id) output.id = requestNote.id;
if (requestNote.body_html) { const baseUrl = requestNote.base_url ? requestNote.base_url : '';
// Parsing will not work if the HTML is not wrapped in a top level tag, which is not guaranteed
// when getting the content from elsewhere. So here wrap it - it won't change anything to the final
// rendering but it makes sure everything will be parsed.
output.body = await this.htmlToMdParser().parse('<div>' + requestNote.body_html + '</div>', {
baseUrl: requestNote.base_url ? requestNote.base_url : '',
anchorNames: requestNote.anchor_names ? requestNote.anchor_names : [],
});
// Note: to save the note as HTML, use the code below: if (requestNote.body_html) {
// const minify = require('html-minifier').minify; if (requestNote.convert_to === 'html') {
// output.body = minify(requestNote.body_html, { collapseWhitespace: true }); const style = await this.buildNoteStyleSheet_(requestNote.stylesheets);
const minify = require('html-minifier').minify;
const minifyOptions = {
// Remove all spaces and, especially, newlines from tag attributes, as that would
// break the rendering.
customAttrCollapse: /.*/,
// Need to remove all whitespaces because whitespace at a beginning of a line
// means a code block in Markdown.
collapseWhitespace: true,
minifyCSS: true,
maxLineLength: 300,
};
const uglifycss = require('uglifycss');
const styleString = uglifycss.processString(style.join('\n'), {
// Need to set a max length because Ace Editor takes forever
// to display notes with long lines.
maxLineLen: 200,
});
const styleTag = style.length ? '<style>' + styleString + '</style>' + '\n' : '';
output.body = styleTag + minify(requestNote.body_html, minifyOptions);
output.body = htmlUtils.prependBaseUrl(output.body, baseUrl);
output.markup_language = Note.MARKUP_LANGUAGE_HTML;
} else { // Convert to Markdown
// Parsing will not work if the HTML is not wrapped in a top level tag, which is not guaranteed
// when getting the content from elsewhere. So here wrap it - it won't change anything to the final
// rendering but it makes sure everything will be parsed.
output.body = await this.htmlToMdParser().parse('<div>' + requestNote.body_html + '</div>', {
baseUrl: baseUrl,
anchorNames: requestNote.anchor_names ? requestNote.anchor_names : [],
});
output.markup_language = Note.MARKUP_LANGUAGE_MARKDOWN;
}
} }
if (requestNote.parent_id) { if (requestNote.parent_id) {
@ -457,6 +487,9 @@ class Api {
if ('user_updated_time' in requestNote) output.user_updated_time = Database.formatValue(Database.TYPE_INT, requestNote.user_updated_time); if ('user_updated_time' in requestNote) output.user_updated_time = Database.formatValue(Database.TYPE_INT, requestNote.user_updated_time);
if ('user_created_time' in requestNote) output.user_created_time = Database.formatValue(Database.TYPE_INT, requestNote.user_created_time); if ('user_created_time' in requestNote) output.user_created_time = Database.formatValue(Database.TYPE_INT, requestNote.user_created_time);
if ('is_todo' in requestNote) output.is_todo = Database.formatValue(Database.TYPE_INT, requestNote.is_todo); if ('is_todo' in requestNote) output.is_todo = Database.formatValue(Database.TYPE_INT, requestNote.is_todo);
if ('markup_language' in requestNote) output.markup_language = Database.formatValue(Database.TYPE_INT, requestNote.markup_language);
if (!output.markup_language) output.markup_language = Note.MARKUP_LANGUAGE_MARKDOWN;
return output; return output;
} }
@ -486,6 +519,32 @@ class Api {
return newImagePath; return newImagePath;
} }
async buildNoteStyleSheet_(stylesheets) {
if (!stylesheets) return [];
const output = [];
for (const stylesheet of stylesheets) {
if (stylesheet.type === 'text') {
output.push(stylesheet.value);
} else if (stylesheet.type === 'url') {
try {
const tempPath = Setting.value('tempDir') + '/' + md5(Math.random() + '_' + Date.now()) + '.css';
await shim.fetchBlob(stylesheet.value, { path: tempPath, maxRetry: 1 });
const text = await shim.fsDriver().readFile(tempPath);
output.push(text);
await shim.fsDriver().remove(tempPath);
} catch (error) {
this.logger().warn('Cannot download stylesheet at ' + stylesheet.value, error);
}
} else {
throw new Error('Invalid stylesheet type: ' + stylesheet.type);
}
}
return output;
}
async downloadImage_(url, allowFileProtocolImages) { async downloadImage_(url, allowFileProtocolImages) {
const tempDir = Setting.value('tempDir'); const tempDir = Setting.value('tempDir');
@ -525,12 +584,7 @@ class Api {
const output = {}; const output = {};
let urlIndex = 0; const downloadOne = url => {
const promiseProducer = () => {
if (urlIndex >= urls.length) return null;
const url = urls[urlIndex++];
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const imagePath = await this.downloadImage_(url, allowFileProtocolImages); const imagePath = await this.downloadImage_(url, allowFileProtocolImages);
if (imagePath) output[url] = { path: imagePath, originalUrl: url }; if (imagePath) output[url] = { path: imagePath, originalUrl: url };
@ -538,7 +592,15 @@ class Api {
}); });
} }
const concurrency = 3 let urlIndex = 0;
const promiseProducer = () => {
if (urlIndex >= urls.length) return null;
const url = urls[urlIndex++];
return downloadOne(url);
}
const concurrency = 10;
const pool = new PromisePool(promiseProducer, concurrency) const pool = new PromisePool(promiseProducer, concurrency)
await pool.start() await pool.start()
@ -571,21 +633,33 @@ class Api {
} }
} }
replaceImageUrlsByResources_(md, urls, imageSizes) { replaceImageUrlsByResources_(markupLanguage, md, urls, imageSizes) {
let output = md.replace(/(!\[.*?\]\()([^\s\)]+)(.*?\))/g, (match, before, imageUrl, after) => { const imageSizesIndexes = {};
const urlInfo = urls[imageUrl];
if (!urlInfo || !urlInfo.resource) return before + imageUrl + after;
const imageSize = imageSizes[urlInfo.originalUrl];
const resourceUrl = Resource.internalUrl(urlInfo.resource);
if (imageSize && (imageSize.naturalWidth !== imageSize.width || imageSize.naturalHeight !== imageSize.height)) { if (markupLanguage === Note.MARKUP_LANGUAGE_HTML) {
return '<img width="' + imageSize.width + '" height="' + imageSize.height + '" src="' + resourceUrl + '"/>'; return htmlUtils.replaceImageUrls(md, imageUrl => {
} else { const urlInfo = urls[imageUrl];
return before + resourceUrl + after; if (!urlInfo || !urlInfo.resource) return imageUrl;
} return Resource.internalUrl(urlInfo.resource);
}); });
} else {
let output = md.replace(/(!\[.*?\]\()([^\s\)]+)(.*?\))/g, (match, before, imageUrl, after) => {
const urlInfo = urls[imageUrl];
if (!urlInfo || !urlInfo.resource) return before + imageUrl + after;
if (!(urlInfo.originalUrl in imageSizesIndexes)) imageSizesIndexes[urlInfo.originalUrl] = 0;
const imageSize = imageSizes[urlInfo.originalUrl][imageSizesIndexes[urlInfo.originalUrl]];
imageSizesIndexes[urlInfo.originalUrl]++;
const resourceUrl = Resource.internalUrl(urlInfo.resource);
return output; if (imageSize && (imageSize.naturalWidth !== imageSize.width || imageSize.naturalHeight !== imageSize.height)) {
return '<img width="' + imageSize.width + '" height="' + imageSize.height + '" src="' + resourceUrl + '"/>';
} else {
return before + resourceUrl + after;
}
});
return output;
}
} }

View File

@ -422,7 +422,7 @@ async function initialize(dispatch) {
if (Setting.value('env') == 'prod') { if (Setting.value('env') == 'prod') {
await db.open({ name: 'joplin.sqlite' }) await db.open({ name: 'joplin.sqlite' })
} else { } else {
await db.open({ name: 'joplin-68.sqlite' }); await db.open({ name: 'joplin-70.sqlite' });
// await db.clearForTesting(); // await db.clearForTesting();
} }

View File

@ -18,7 +18,7 @@ async function copyJs(name, filePath) {
async function main(argv) { async function main(argv) {
await fs.mkdirp(outputDir); await fs.mkdirp(outputDir);
await copyJs('webviewLib', rnDir + '/lib/MdToHtml/webviewLib.js'); await copyJs('webviewLib', rnDir + '/lib/renderers/webviewLib.js');
} }
main(process.argv).catch((error) => { main(process.argv).catch((error) => {