mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
Merge branch 'master' of github.com:laurent22/joplin
This commit is contained in:
commit
6e64b950ca
180
CliClient/package-lock.json
generated
180
CliClient/package-lock.json
generated
@ -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",
|
||||||
|
105
CliClient/tests/htmlUtils.js
Normal file
105
CliClient/tests/htmlUtils.js
Normal 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&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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -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') {
|
||||||
|
@ -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>
|
||||||
|
@ -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 });
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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');
|
||||||
|
@ -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';
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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'}
|
||||||
|
@ -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 => {
|
||||||
|
247
ElectronClient/app/package-lock.json
generated
247
ElectronClient/app/package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -28,7 +28,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/
|
|||||||
|
|
||||||
Operating System | Download | Alt. Download
|
Operating System | Download | Alt. Download
|
||||||
-----------------|----------|----------------
|
-----------------|----------|----------------
|
||||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.281/joplin-v1.0.281.apk)
|
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.282/joplin-v1.0.282.apk)
|
||||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
|
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
|
||||||
|
|
||||||
## Terminal application
|
## Terminal application
|
||||||
|
@ -94,8 +94,8 @@ android {
|
|||||||
applicationId "net.cozic.joplin"
|
applicationId "net.cozic.joplin"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 2097517
|
versionCode 2097518
|
||||||
versionName "1.0.281"
|
versionName "1.0.282"
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,8 @@
|
|||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:largeHeap="true"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- ============================= -->
|
<!-- ============================= -->
|
||||||
|
@ -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);
|
|
||||||
};
|
|
||||||
};
|
|
@ -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 {
|
||||||
|
@ -288,11 +288,18 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
ResourceFetcher.instance().markForDownload(event.resourceId);
|
ResourceFetcher.instance().markForDownload(event.resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.doFocusUpdate_) {
|
if (this.doFocusUpdate_) {
|
||||||
this.doFocusUpdate_ = false;
|
this.doFocusUpdate_ = false;
|
||||||
this.focusUpdate();
|
this.focusUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prevProps.showSideMenu !== this.props.showSideMenu && this.props.showSideMenu) {
|
||||||
|
this.props.dispatch({
|
||||||
|
type: 'NOTE_SIDE_MENU_OPTIONS_SET',
|
||||||
|
options: this.sideMenuOptions(),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@ -433,7 +440,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localFilePath = pickerResponse.uri;
|
const localFilePath = Platform.select({
|
||||||
|
android: pickerResponse.uri,
|
||||||
|
ios: decodeURI(pickerResponse.uri),
|
||||||
|
});
|
||||||
|
|
||||||
let mimeType = pickerResponse.type;
|
let mimeType = pickerResponse.type;
|
||||||
|
|
||||||
if (!mimeType) {
|
if (!mimeType) {
|
||||||
@ -554,11 +565,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
properties_onPress() {
|
properties_onPress() {
|
||||||
this.props.dispatch({
|
|
||||||
type: 'NOTE_SIDE_MENU_OPTIONS_SET',
|
|
||||||
options: this.sideMenuOptions(),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.dispatch({ type: 'SIDE_MENU_OPEN' });
|
this.props.dispatch({ type: 'SIDE_MENU_OPEN' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -891,6 +897,7 @@ const NoteScreen = connect(
|
|||||||
theme: state.settings.theme,
|
theme: state.settings.theme,
|
||||||
ftsEnabled: state.settings['db.ftsEnabled'],
|
ftsEnabled: state.settings['db.ftsEnabled'],
|
||||||
sharedData: state.sharedData,
|
sharedData: state.sharedData,
|
||||||
|
showSideMenu: state.showSideMenu,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)(NoteScreenComponent)
|
)(NoteScreenComponent)
|
||||||
|
@ -144,8 +144,6 @@ class NotesScreenComponent extends BaseScreenComponent {
|
|||||||
notes: notes,
|
notes: notes,
|
||||||
notesSource: source,
|
notesSource: source,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.info('Done', Date.now() - startTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFolder_onPress(folderId) {
|
deleteFolder_onPress(folderId) {
|
||||||
|
92
ReactNativeClient/lib/htmlUtils.js
Normal file
92
ReactNativeClient/lib/htmlUtils.js
Normal 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;
|
@ -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 {
|
||||||
|
@ -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 = {
|
||||||
|
|
||||||
|
21
ReactNativeClient/lib/markupLanguageUtils.js
Normal file
21
ReactNativeClient/lib/markupLanguageUtils.js
Normal 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;
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
@ -239,8 +232,12 @@ class Note extends BaseItem {
|
|||||||
|
|
||||||
for (let i = 0; i < orders.length; i++) {
|
for (let i = 0; i < orders.length; i++) {
|
||||||
const order = orders[i];
|
const order = orders[i];
|
||||||
if (a[order.by] < b[order.by]) r = +1;
|
let aProp = a[order.by];
|
||||||
if (a[order.by] > b[order.by]) r = -1;
|
let bProp = b[order.by];
|
||||||
|
if (typeof aProp === 'string') aProp = aProp.toLowerCase();
|
||||||
|
if (typeof bProp === 'string') bProp = bProp.toLowerCase();
|
||||||
|
if (aProp < bProp) r = +1;
|
||||||
|
if (aProp > bProp) r = -1;
|
||||||
if (order.dir == 'ASC') r = -r;
|
if (order.dir == 'ASC') r = -r;
|
||||||
if (r !== 0) return r;
|
if (r !== 0) return r;
|
||||||
}
|
}
|
||||||
@ -629,9 +626,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;
|
39
ReactNativeClient/lib/renderers/HtmlToHtml.js
Normal file
39
ReactNativeClient/lib/renderers/HtmlToHtml.js
Normal 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;
|
39
ReactNativeClient/lib/renderers/MarkupToHtml.js
Normal file
39
ReactNativeClient/lib/renderers/MarkupToHtml.js
Normal 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;
|
@ -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'),
|
@ -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;
|
||||||
|
|
@ -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');
|
||||||
|
|
@ -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) {
|
29
ReactNativeClient/lib/renderers/MdToHtml/rules/image.js
Normal file
29
ReactNativeClient/lib/renderers/MdToHtml/rules/image.js
Normal 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);
|
||||||
|
};
|
||||||
|
};
|
@ -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>';
|
||||||
|
|
@ -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;
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
18
ReactNativeClient/package-lock.json
generated
18
ReactNativeClient/package-lock.json
generated
@ -3922,9 +3922,9 @@
|
|||||||
"integrity": "sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g="
|
"integrity": "sha1-1mtTZFIcuz3Yqlna37ovtoZcj9g="
|
||||||
},
|
},
|
||||||
"markdown-it-anchor": {
|
"markdown-it-anchor": {
|
||||||
"version": "5.0.2",
|
"version": "5.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.2.4.tgz",
|
||||||
"integrity": "sha512-AFM/woBI8QDJMS/9+MmsBMT5/AR+ImfOsunQZTZhzcTmna3rIzAzbOh5E0l6mlFM/i9666BpUtkqQ9bS7WApCg=="
|
"integrity": "sha512-n8zCGjxA3T+Mx1pG8HEgbJbkB8JFUuRkeTZQuIM8iPY6oQ8sWOPRZJDFC9a/pNg2QkHEjjGkhBEl/RSyzaDZ3A=="
|
||||||
},
|
},
|
||||||
"markdown-it-deflist": {
|
"markdown-it-deflist": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
@ -3937,9 +3937,9 @@
|
|||||||
"integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw="
|
"integrity": "sha1-m+4OmpkKljupbfaYDE/dsF37Tcw="
|
||||||
},
|
},
|
||||||
"markdown-it-footnote": {
|
"markdown-it-footnote": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-it-footnote/-/markdown-it-footnote-3.0.2.tgz",
|
||||||
"integrity": "sha1-fzcwdHysyG4v4L+KF6cQ80eRUXo="
|
"integrity": "sha512-JVW6fCmZWjvMdDQSbOT3nnOQtd9iAXmw7hTSh26+v42BnvXeVyGMDBm5b/EZocMed2MbCAHiTX632vY0FyGB8A=="
|
||||||
},
|
},
|
||||||
"markdown-it-ins": {
|
"markdown-it-ins": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -4007,9 +4007,9 @@
|
|||||||
"integrity": "sha1-y5yf+RpSVawI8/09YyhuFd8KH8M="
|
"integrity": "sha1-y5yf+RpSVawI8/09YyhuFd8KH8M="
|
||||||
},
|
},
|
||||||
"markdown-it-toc-done-right": {
|
"markdown-it-toc-done-right": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it-toc-done-right/-/markdown-it-toc-done-right-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-it-toc-done-right/-/markdown-it-toc-done-right-4.0.2.tgz",
|
||||||
"integrity": "sha512-jaoIC02ehqUrT6RpbNZMe+ug7BaW3V6G1qTi/hTtcswuFWunP2H2GzmVobWIyOb3nQ4HNBXHqdbsv8fEsMCKWA=="
|
"integrity": "sha512-LIdwU8coEdBa6JxhMmcst5Qc9/ptVDmUB6dJuJzlxaV75Dry+AziKpQRgZ42SMQ5m7hpb7z6b0erIRoYk+bMWA=="
|
||||||
},
|
},
|
||||||
"match-at": {
|
"match-at": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user