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

Merge branch 'clipper'

This commit is contained in:
Laurent Cozic 2018-05-25 13:31:15 +01:00
commit f81dce3321
114 changed files with 25697 additions and 1180 deletions

View File

@ -66,26 +66,6 @@ process.stdout.on('error', function( err ) {
}
});
// async function main() {
// const InteropService = require('lib/services/InteropService');
// const service = new InteropService();
// console.info(service.moduleByFormat('importer', 'enex'));
// //await service.modules();
// }
// main().catch((error) => { console.error(error); });
application.start(process.argv).catch((error) => {
if (error.code == 'flagError') {
console.error(error.message);

View File

@ -4,6 +4,24 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"abab": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
"integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4="
},
"acorn": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz",
"integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ=="
},
"acorn-globals": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz",
"integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==",
"requires": {
"acorn": "^5.0.0"
}
},
"ajv": {
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
@ -33,6 +51,11 @@
"resolved": "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz",
"integrity": "sha1-ZBqlXft9am8KgUHEucCqULbCTdU="
},
"array-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz",
"integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM="
},
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
@ -64,6 +87,11 @@
}
}
},
"async-limiter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
},
"async-mutex": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.1.3.tgz",
@ -122,6 +150,11 @@
"concat-map": "0.0.1"
}
},
"browser-process-hrtime": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz",
"integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44="
},
"camel-case": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz",
@ -277,6 +310,19 @@
}
}
},
"cssom": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz",
"integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs="
},
"cssstyle": {
"version": "0.2.37",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz",
"integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=",
"requires": {
"cssom": "0.3.x"
}
},
"cwise-compiler": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz",
@ -298,6 +344,16 @@
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz",
"integrity": "sha1-GK6XmmoMqZSwYlhTkW0mYruuCxo="
},
"data-urls": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.0.0.tgz",
"integrity": "sha512-ai40PPQR0Fn1lD2PPie79CibnlMN2AYiDhwFX/rZHVsxbs5kNJSjegqXIprhouGXlRdEnfybva7kqRGnB6mypA==",
"requires": {
"abab": "^1.0.4",
"whatwg-mimetype": "^2.0.0",
"whatwg-url": "^6.4.0"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -329,6 +385,14 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-0.2.0.tgz",
"integrity": "sha1-R/31ZzSKF+wl/L8LnkRjSKdvn7U="
},
"domexception": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz",
"integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==",
"requires": {
"webidl-conversions": "^4.0.2"
}
},
"ecc-jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
@ -393,11 +457,51 @@
"iconv-lite": "~0.4.13"
}
},
"es6-promise-pool": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/es6-promise-pool/-/es6-promise-pool-2.5.0.tgz",
"integrity": "sha1-FHxhKza0fxBQJ/nSv1SlmKmdnMs="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"escodegen": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz",
"integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==",
"requires": {
"esprima": "^3.1.3",
"estraverse": "^4.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1",
"source-map": "~0.6.1"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"optional": true
}
}
},
"esprima": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
"integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM="
},
"estraverse": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
},
"esutils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
"integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
},
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@ -424,6 +528,11 @@
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
},
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
},
"fault": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.2.tgz",
@ -432,6 +541,11 @@
"format": "^0.2.2"
}
},
"file-type": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz",
"integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU="
},
"follow-redirects": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.2.5.tgz",
@ -610,6 +724,14 @@
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
},
"html-encoding-sniffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz",
"integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==",
"requires": {
"whatwg-encoding": "^1.0.1"
}
},
"html-entities": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
@ -644,6 +766,14 @@
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
},
"image-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/image-type/-/image-type-3.0.0.tgz",
"integrity": "sha1-FQKvMTX5BuEiyHfDHpSve3qRRsU=",
"requires": {
"file-type": "^4.1.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -773,6 +903,19 @@
"integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=",
"dev": true
},
"joplin-turndown": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.3.tgz",
"integrity": "sha512-WbAXje8wq4/ZLNtPDUFBEtG5zKEbz7Wth5N3vB4Nw7k+PUs3mMF49LVEPP7Kc6H4Ui671qdjpSShvdsmiLY2gA==",
"requires": {
"jsdom": "^11.9.0"
}
},
"joplin-turndown-plugin-gfm": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.2.tgz",
"integrity": "sha512-GRXmjHFrEyUnXOYzOZvUGGtKxPm5LuK98+73ZADqQYdGzMWp/o8Qx22YYAeIBsOV2WtVsRxe2IpUGBG4foSRyQ=="
},
"jpeg-js": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.1.2.tgz",
@ -789,6 +932,39 @@
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
"optional": true
},
"jsdom": {
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.10.0.tgz",
"integrity": "sha512-x5No5FpJgBg3j5aBwA8ka6eGuS5IxbC8FOkmyccKvObtFT0bDMict/LOxINZsZGZSfGdNomLZ/qRV9Bpq/GIBA==",
"requires": {
"abab": "^1.0.4",
"acorn": "^5.3.0",
"acorn-globals": "^4.1.0",
"array-equal": "^1.0.0",
"cssom": ">= 0.3.2 < 0.4.0",
"cssstyle": ">= 0.2.37 < 0.3.0",
"data-urls": "^1.0.0",
"domexception": "^1.0.0",
"escodegen": "^1.9.0",
"html-encoding-sniffer": "^1.0.2",
"left-pad": "^1.2.0",
"nwmatcher": "^1.4.3",
"parse5": "4.0.0",
"pn": "^1.1.0",
"request": "^2.83.0",
"request-promise-native": "^1.0.5",
"sax": "^1.2.4",
"symbol-tree": "^3.2.2",
"tough-cookie": "^2.3.3",
"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.0",
"ws": "^4.0.0",
"xml-name-validator": "^3.0.0"
}
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
@ -828,11 +1004,25 @@
"resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz",
"integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po="
},
"left-pad": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
"integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA=="
},
"levenshtein": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/levenshtein/-/levenshtein-1.0.5.tgz",
"integrity": "sha1-ORFzepy1baNF0Aj1V4LG8TiXm6M="
},
"levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
"integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
"requires": {
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2"
}
},
"lodash": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
@ -843,6 +1033,11 @@
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz",
"integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc="
},
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
},
"lodash.toarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
@ -1028,6 +1223,11 @@
"pify": "^3.0.0"
}
},
"nwmatcher": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz",
"integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ=="
},
"oauth-sign": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
@ -1051,6 +1251,26 @@
"wrappy": "1"
}
},
"optionator": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
"integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
"requires": {
"deep-is": "~0.1.3",
"fast-levenshtein": "~2.0.4",
"levn": "~0.3.0",
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2",
"wordwrap": "~1.0.0"
},
"dependencies": {
"deep-is": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
}
}
},
"param-case": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz",
@ -1067,6 +1287,11 @@
"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-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@ -1083,11 +1308,21 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"pn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz",
"integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA=="
},
"pngjs": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-2.3.1.tgz",
"integrity": "sha1-EdHhK5y2TWPjDBQ6Mw9MH1Z9qF8="
},
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
"integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
},
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
@ -1144,6 +1379,15 @@
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz",
"integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs="
},
"read-chunk": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-2.1.0.tgz",
"integrity": "sha1-agTAkoAF7Z1C4aasVgDhnLx/9lU=",
"requires": {
"pify": "^3.0.0",
"safe-buffer": "^5.1.1"
}
},
"readable-stream": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
@ -1203,6 +1447,24 @@
"uuid": "^3.1.0"
}
},
"request-promise-core": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz",
"integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=",
"requires": {
"lodash": "^4.13.1"
}
},
"request-promise-native": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz",
"integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=",
"requires": {
"request-promise-core": "1.1.1",
"stealthy-require": "^1.1.0",
"tough-cookie": ">=2.3.3"
}
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@ -2037,6 +2299,11 @@
"tweetnacl": "~0.14.0"
}
},
"stealthy-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
},
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
@ -2107,6 +2374,11 @@
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz",
"integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0="
},
"symbol-tree": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz",
"integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY="
},
"tar": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.0.tgz",
@ -2191,6 +2463,21 @@
"punycode": "^1.4.1"
}
},
"tr46": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz",
"integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=",
"requires": {
"punycode": "^2.1.0"
},
"dependencies": {
"punycode": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz",
"integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0="
}
}
},
"tree-kit": {
"version": "0.5.26",
"resolved": "https://registry.npmjs.org/tree-kit/-/tree-kit-0.5.26.tgz",
@ -2210,6 +2497,14 @@
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"optional": true
},
"type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
"integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
"requires": {
"prelude-ls": "~1.1.2"
}
},
"uglify-js": {
"version": "3.3.25",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.25.tgz",
@ -2285,11 +2580,52 @@
"extsprintf": "^1.2.0"
}
},
"w3c-hr-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz",
"integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=",
"requires": {
"browser-process-hrtime": "^0.1.2"
}
},
"webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
"integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="
},
"whatwg-encoding": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz",
"integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==",
"requires": {
"iconv-lite": "0.4.19"
}
},
"whatwg-mimetype": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz",
"integrity": "sha512-FKxhYLytBQiUKjkYteN71fAUA3g6KpNXoho1isLiLSB3N1G4F35Q5vUxWfKFhBwi5IWF27VE6WxhrnnC+m0Mew=="
},
"whatwg-url": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.4.1.tgz",
"integrity": "sha512-FwygsxsXx27x6XXuExA/ox3Ktwcbf+OAvrKmLulotDAiO1Q6ixchPFaHYsis2zZBZSJTR0+dR+JVtf7MlbqZjw==",
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^1.0.1",
"webidl-conversions": "^4.0.2"
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="
},
"wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
},
"wrap-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz",
@ -2304,6 +2640,20 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"ws": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz",
"integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==",
"requires": {
"async-limiter": "~1.0.0",
"safe-buffer": "~5.1.0"
}
},
"xml-name-validator": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw=="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",

View File

@ -31,11 +31,15 @@
"async-mutex": "^0.1.3",
"base-64": "^0.1.0",
"compare-version": "^0.1.2",
"es6-promise-pool": "^2.5.0",
"follow-redirects": "^1.2.4",
"form-data": "^2.1.4",
"fs-extra": "^5.0.0",
"html-entities": "^1.2.1",
"html-minifier": "^3.5.15",
"image-type": "^3.0.0",
"joplin-turndown": "^4.0.3",
"joplin-turndown-plugin-gfm": "^1.0.2",
"jssha": "^2.3.0",
"levenshtein": "^1.0.5",
"lodash": "^4.17.4",
@ -48,6 +52,7 @@
"promise": "^7.1.1",
"proper-lockfile": "^2.0.1",
"query-string": "4.3.4",
"read-chunk": "^2.1.0",
"redux": "^3.7.2",
"sax": "^1.2.2",
"server-destroy": "^1.0.1",

View File

@ -13,11 +13,15 @@ if [[ $TEST_FILE != "" ]]; then
exit
fi
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
(cd "$ROOT_DIR" && npm test tests-build/encryption.js)
(cd "$ROOT_DIR" && npm test tests-build/ArrayUtils.js)
(cd "$ROOT_DIR" && npm test tests-build/models_Setting.js)
(cd "$ROOT_DIR" && npm test tests-build/models_Note.js)
(cd "$ROOT_DIR" && npm test tests-build/models_Folder.js)
(cd "$ROOT_DIR" && npm test tests-build/services_InteropService.js)
(cd "$ROOT_DIR" && npm test tests-build/encryption.js
(cd "$ROOT_DIR" && npm test tests-build/EnexToMd.js)
(cd "$ROOT_DIR" && npm test tests-build/HtmlToMd.js)
(cd "$ROOT_DIR" && npm test tests-build/markdownUtils.js)
(cd "$ROOT_DIR" && npm test tests-build/models_Folder.js)
(cd "$ROOT_DIR" && npm test tests-build/models_Note.js)
(cd "$ROOT_DIR" && npm test tests-build/models_Setting.js)
(cd "$ROOT_DIR" && npm test tests-build/services_InteropService.js)
(cd "$ROOT_DIR" && npm test tests-build/services_ResourceService.js)
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
(cd "$ROOT_DIR" && npm test tests-build/urlUtils.js)

View File

@ -0,0 +1,62 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { filename } = require('lib/path-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim');
const { enexXmlToMd } = require('lib/import-enex-md-gen.js');
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('EnexToMd', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should convert from Enex to Markdown', asyncTest(async () => {
const basePath = __dirname + '/enex_to_md';
const files = await shim.fsDriver().readDirStats(basePath);
for (let i = 0; i < files.length; i++) {
const htmlFilename = files[i].path;
if (htmlFilename.indexOf('.html') < 0) continue;
const htmlPath = basePath + '/' + htmlFilename;
const mdPath = basePath + '/' + filename(htmlFilename) + '.md';
// if (htmlFilename !== 'text2.html') continue;
const html = await shim.fsDriver().readFile(htmlPath);
const expectedMd = await shim.fsDriver().readFile(mdPath);
const actualMd = await enexXmlToMd('<div>' + html + '</div>', []);
if (actualMd !== expectedMd) {
console.info('');
console.info('Error converting file: ' + htmlFilename);
console.info('--------------------------------- Got:');
console.info(actualMd.split('\n'));
console.info('--------------------------------- Expected:');
console.info(expectedMd.split('\n'));
console.info('--------------------------------------------');
console.info('');
expect(false).toBe(true);
// return;
} else {
expect(true).toBe(true)
}
}
}));
});

View File

@ -7,6 +7,7 @@ const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim');
const HtmlToMd = require('lib/HtmlToMd');
const { enexXmlToMd } = require('lib/import-enex-md-gen.js');
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit
@ -23,9 +24,10 @@ describe('HtmlToMd', function() {
done();
});
it('should convert from HTML to Markdown', asyncTest(async () => {
it('should convert from Html to Markdown', asyncTest(async () => {
const basePath = __dirname + '/html_to_md';
const files = await shim.fsDriver().readDirStats(basePath);
const htmlToMd = new HtmlToMd();
for (let i = 0; i < files.length; i++) {
const htmlFilename = files[i].path;
@ -34,17 +36,19 @@ describe('HtmlToMd', function() {
const htmlPath = basePath + '/' + htmlFilename;
const mdPath = basePath + '/' + filename(htmlFilename) + '.md';
// if (htmlFilename !== 'text2.html') continue;
// if (htmlFilename !== 'table_with_colspan.html') continue;
const html = await shim.fsDriver().readFile(htmlPath);
const expectedMd = await shim.fsDriver().readFile(mdPath);
const actualMd = await enexXmlToMd('<div>' + html + '</div>', []);
const actualMd = await htmlToMd.parse('<div>' + html + '</div>', []);
if (actualMd !== expectedMd) {
console.info('');
console.info('Error converting file: ' + htmlFilename);
console.info('--------------------------------- Got:');
console.info(actualMd);
console.info('--------------------------------- Raw:');
console.info(actualMd.split('\n'));
console.info('--------------------------------- Expected:');
console.info(expectedMd.split('\n'));

View File

@ -0,0 +1 @@
<a href="https://joplin.cozic.net"><h1 id="joplin"><img class="title-icon" src="https://joplin.cozic.net/images/Icon512.png">oplin</h1></a>

View File

@ -0,0 +1 @@
[# ![](https://joplin.cozic.net/images/Icon512.png)oplin](https://joplin.cozic.net)

View File

@ -0,0 +1 @@
<a href="javascript:alert('js')">Some text</a>

View File

@ -0,0 +1 @@
[Some text]()

View File

@ -0,0 +1 @@
<a href="http://example.com"><p>That</p><p>Shouldn't be allowed</p></a>

View File

@ -0,0 +1 @@
[That<br>Shouldn't be allowed](http://example.com)

View File

@ -0,0 +1,13 @@
<!-- Make sure in particular that indentation is correct after the 9th item -->
<ol>
<li><p>One</p><p>Two</p></li>
<li><p>One</p><p>Two</p></li>
<li><p>One</p><p>Two</p></li>
<li><p>One</p><p>Two</p></li>
<li><p>One</p><p>Two</p></li>
<li><p>One</p><p>Two</p></li>
<li><p>One</p><p>Two</p></li>
<li><p>One</p><p>Two</p></li>
<li><p>One</p><p>Two</p></li>
<li><p>One</p><p>Two</p></li>
</ol>

View File

@ -0,0 +1,39 @@
1. One
Two
2. One
Two
3. One
Two
4. One
Two
5. One
Two
6. One
Two
7. One
Two
8. One
Two
9. One
Two
10. One
Two

View File

@ -0,0 +1 @@
<script id="appnexus-adload" data-reactid="7">window.apntag=window.apntag||{};window.apntag.anq=window.apntag.anq||[];</script>

File diff suppressed because one or more lines are too long

View File

View File

@ -0,0 +1,4 @@
<table>
<tr><td>No</td><td>header</td></tr>
<tr><td>And no</td><td>suprises</td></tr>
</table>

View File

@ -0,0 +1,4 @@
| | |
| --- | --- |
| No | header |
| And no | suprises |

View File

@ -0,0 +1,15 @@
<table>
<tr>
<td colspan="2">
Something that was originally spanning two columns
</td>
</tr>
<tr>
<td>
One
</td>
<td>
Two
</td>
</tr>
</table>

View File

@ -0,0 +1,4 @@
| | |
| --- | --- |
| Something that was originally spanning two columns | |
| One | Two |

View File

@ -0,0 +1,10 @@
<table>
<tr>
<td></td>
<td>Previous is empty</td>
</tr>
<tr>
<td>Next is empty</td>
<td></td>
</tr>
</table>

View File

@ -0,0 +1,4 @@
| | |
| --- | --- |
| | Previous is empty |
| Next is empty | |

View File

@ -0,0 +1,13 @@
<table>
<tr>
<td>One</td><td>Two</td>
</tr>
<tr></tr>
<tr>
<td>One</td><td>Two</td>
</tr>
<tr></tr>
<tr>
<td>One</td><td>Two</td>
</tr>
</table>

View File

@ -0,0 +1,5 @@
| | |
| --- | --- |
| One | Two |
| One | Two |
| One | Two |

View File

@ -0,0 +1,6 @@
<table>
<tr>
<td><p>Some paragraph</p><p>inside a table cell</p></td>
<td>Second column</td>
</tr>
</table>

View File

@ -0,0 +1,3 @@
| | |
| --- | --- |
| Some paragraph<br><br>inside a table cell | Second column |

View File

@ -0,0 +1,16 @@
<!--
The inner table is rendered but not the outer one.
Basically if any table contains another table, it is rendered as plain text
-->
<table>
<tr><td>
First column, and an inner table:
<table>
<tr><td>One</td><td>Two</td></tr>
<tr><td>One</td><td>Two</td></tr>
</table>
</td>
<td>Second column</td>
</tr>
</table>

View File

@ -0,0 +1,8 @@
First column, and an inner table:
| | |
| --- | --- |
| One | Two |
| One | Two |
Second column

View File

@ -0,0 +1,54 @@
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 markdownUtils = require('lib/markdownUtils.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('markdownUtils', function() {
beforeEach(async (done) => {
done();
});
it('should prepend a base URL', async (done) => {
const baseUrl = 'https://test.com/site';
const testCases = [
['[something](testing.html)', '[something](https://test.com/site/testing.html)'],
['![something](/img/test.png)', '![something](https://test.com/img/test.png)'],
['[![something](/img/test.png)](/index.html "Home page")', '[![something](https://test.com/img/test.png)](https://test.com/index.html "Home page")'],
['[onelink.com](/jmp/?id=123&u=http://something.com/test)', '[onelink.com](https://test.com/jmp/?id=123&u=http://something.com/test)'],
['[![some text](/img/test.png)](/jmp/?s=80&l=related&u=http://example.com "some decription")', '[![some text](https://test.com/img/test.png)](https://test.com/jmp/?s=80&l=related&u=http://example.com "some decription")'],
];
for (let i = 0; i < testCases.length; i++) {
const md = testCases[i][0];
const expected = testCases[i][1];
expect(markdownUtils.prependBaseUrl(md, baseUrl)).toBe(expected);
}
done();
});
it('should extract image URLs', async (done) => {
const testCases = [
['![something](http://test.com/img.png)', ['http://test.com/img.png']],
['![something](http://test.com/img.png) ![something2](http://test.com/img2.png)', ['http://test.com/img.png', 'http://test.com/img2.png']],
['![something](http://test.com/img.png "Some description")', ['http://test.com/img.png']],
];
for (let i = 0; i < testCases.length; i++) {
const md = testCases[i][0];
const expected = testCases[i][1];
expect(markdownUtils.extractImageUrls(md).join('')).toBe(expected.join(''));
}
done();
});
});

View File

@ -0,0 +1,37 @@
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 urlUtils = require('lib/urlUtils.js');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('urlUtils', function() {
beforeEach(async (done) => {
done();
});
it('should prepend a base URL', async (done) => {
expect(urlUtils.prependBaseUrl('testing.html', 'http://example.com')).toBe('http://example.com/testing.html');
expect(urlUtils.prependBaseUrl('testing.html', 'http://example.com/')).toBe('http://example.com/testing.html');
expect(urlUtils.prependBaseUrl('/jmp/?id=123&u=http://something.com/test', 'http://example.com/')).toBe('http://example.com/jmp/?id=123&u=http://something.com/test');
expect(urlUtils.prependBaseUrl('/testing.html', 'http://example.com/')).toBe('http://example.com/testing.html');
expect(urlUtils.prependBaseUrl('/testing.html', 'http://example.com/something')).toBe('http://example.com/testing.html');
expect(urlUtils.prependBaseUrl('/testing.html', 'https://example.com/something')).toBe('https://example.com/testing.html');
expect(urlUtils.prependBaseUrl('//somewhereelse.com/testing.html', 'https://example.com/something')).toBe('https://somewhereelse.com/testing.html');
expect(urlUtils.prependBaseUrl('//somewhereelse.com/testing.html', 'http://example.com/something')).toBe('http://somewhereelse.com/testing.html');
expect(urlUtils.prependBaseUrl('', 'http://example.com/something')).toBe('http://example.com/something');
expect(urlUtils.prependBaseUrl('testing.html', '')).toBe('testing.html');
// It shouldn't prepend anyting for these:
expect(urlUtils.prependBaseUrl('mailto:emailme@example.com', 'http://example.com')).toBe('mailto:emailme@example.com');
expect(urlUtils.prependBaseUrl('javascript:var%20testing=true', 'http://example.com')).toBe('javascript:var%20testing=true');
expect(urlUtils.prependBaseUrl('http://alreadyabsolute.com', 'http://example.com')).toBe('http://alreadyabsolute.com');
done();
});
});

1
Clipper/joplin-webclipper/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist/

View File

@ -0,0 +1,40 @@
let browser_ = null;
if (typeof browser !== 'undefined') {
browser_ = browser;
browserSupportsPromises_ = true;
} else if (typeof chrome !== 'undefined') {
browser_ = chrome;
browserSupportsPromises_ = false;
}
async function browserCaptureVisibleTabs(windowId, options) {
if (browserSupportsPromises_) return browser_.tabs.captureVisibleTab(windowId, { format: 'jpeg' });
return new Promise((resolve, reject) => {
browser_.tabs.captureVisibleTab(windowId, { format: 'jpeg' }, (image) => {
resolve(image);
});
});
}
chrome.runtime.onInstalled.addListener(function() {
});
browser_.runtime.onMessage.addListener((command) => {
if (command.name === 'screenshotArea') {
browserCaptureVisibleTabs(null, { format: 'jpeg' }).then((imageDataUrl) => {
content = Object.assign({}, command.content);
content.imageDataUrl = imageDataUrl;
fetch(command.apiBaseUrl + "/notes", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(content)
});
});
}
});

View File

@ -0,0 +1 @@
*.bundle.js

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,238 @@
(function() {
if (window.jopext_hasRun) return;
window.jopext_hasRun = true;
console.info('jopext: Loading content script');
let browser_ = null;
if (typeof browser !== 'undefined') {
browser_ = browser;
browserSupportsPromises_ = true;
} else if (typeof chrome !== 'undefined') {
browser_ = chrome;
browserSupportsPromises_ = false;
}
function pageTitle() {
const titleElements = document.getElementsByTagName("title");
if (titleElements.length) return titleElements[0].text.trim();
return document.title.trim();
}
function baseUrl() {
let output = location.origin + location.pathname;
if (output[output.length - 1] !== '/') {
output = output.split('/');
output.pop();
output = output.join('/');
}
return output;
}
// Cleans up element by removing all its invisible children (which we don't want to render as Markdown)
function cleanUpElement(element) {
const childNodes = element.childNodes;
for (let i = 0; i < childNodes.length; i++) {
const node = childNodes[i];
let isVisible = node.nodeType === 1 ? window.getComputedStyle(node).display !== 'none' : true;
if (isVisible && ['input', 'textarea', 'script', 'style', 'select', 'option', 'button'].indexOf(node.nodeName.toLowerCase()) >= 0) isVisible = false;
if (!isVisible) {
element.removeChild(node);
} else {
cleanUpElement(node);
}
}
}
function readabilityProcess() {
var uri = {
spec: location.href,
host: location.host,
prePath: location.protocol + "//" + location.host,
scheme: location.protocol.substr(0, location.protocol.indexOf(":")),
pathBase: location.protocol + "//" + location.host + location.pathname.substr(0, location.pathname.lastIndexOf("/") + 1)
};
// Readability directly change the passed document so clone it so as
// to preserve the original web page.
const documentClone = document.cloneNode(true);
const readability = new Readability(documentClone); // new window.Readability(uri, documentClone);
const article = readability.parse();
if (!article) throw new Error('Could not parse HTML document with Readability');
return {
title: article.title,
body: article.content,
}
}
async function prepareCommandResponse(command) {
console.info('Got command: ' + command.name);
if (command.name === "simplifiedPageHtml") {
let article = null;
try {
article = readabilityProcess();
} catch (error) {
console.warn(error);
console.warn('Sending full page HTML instead');
const newCommand = Object.assign({}, command, { name: 'completePageHtml' });
const response = await prepareCommandResponse(newCommand);
response.warning = 'Could not retrieve simplified version of page - full page has been saved instead.';
return response;
}
return {
name: 'clippedContent',
html: article.body,
title: article.title,
baseUrl: baseUrl(),
url: location.origin + location.pathname,
};
} else if (command.name === "completePageHtml") {
const cleanDocument = document.body.cloneNode(true);
cleanUpElement(cleanDocument);
return {
name: 'clippedContent',
html: cleanDocument.innerHTML,
title: pageTitle(),
baseUrl: baseUrl(),
url: location.origin + location.pathname,
};
} else if (command.name === 'screenshot') {
const overlay = document.createElement('div');
overlay.style.opacity = '0.4';
overlay.style.background = 'black';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.zIndex = 99999999;
overlay.style.top = 0;
overlay.style.left = 0;
overlay.style.position = 'fixed';
document.body.appendChild(overlay);
const messageComp = document.createElement('div');
const messageCompWidth = 300;
messageComp.style.position = 'fixed'
messageComp.style.opacity = '0.9'
messageComp.style.width = messageCompWidth + 'px'
messageComp.style.maxWidth = messageCompWidth + 'px'
messageComp.style.border = '1px solid black'
messageComp.style.background = 'white'
messageComp.style.top = '10px'
messageComp.style.textAlign = 'center';
messageComp.style.padding = '6px'
messageComp.style.left = Math.round(document.body.clientWidth / 2 - messageCompWidth / 2) + 'px'
messageComp.style.zIndex = overlay.style.zIndex + 1
messageComp.textContent = 'Drag and release to capture a screenshot';
document.body.appendChild(messageComp);
const selection = document.createElement('div');
selection.style.opacity = '0.4';
selection.style.border = '1px solid red';
selection.style.background = 'white';
selection.style.border = '2px solid black';
selection.style.zIndex = overlay.style.zIndex - 1;
selection.style.top = 0;
selection.style.left = 0;
selection.style.position = 'fixed';
document.body.appendChild(selection);
let isDragging = false;
let draggingStartPos = null;
let selectionArea = {};
function updateSelection() {
selection.style.left = selectionArea.x + 'px';
selection.style.top = selectionArea.y + 'px';
selection.style.width = selectionArea.width + 'px';
selection.style.height = selectionArea.height + 'px';
}
function setSelectionSizeFromMouse(event) {
selectionArea.width = Math.max(1, event.clientX - draggingStartPos.x);
selectionArea.height = Math.max(1, event.clientY - draggingStartPos.y);
updateSelection();
}
function selection_mouseDown(event) {
selectionArea = { x: event.clientX, y: event.clientY, width: 0, height: 0 }
draggingStartPos = { x: event.clientX, y: event.clientY };
isDragging = true;
updateSelection();
}
function selection_mouseMove(event) {
if (!isDragging) return;
setSelectionSizeFromMouse(event);
}
function selection_mouseUp(event) {
setSelectionSizeFromMouse(event);
isDragging = false;
overlay.removeEventListener('mousedown', selection_mouseDown);
overlay.removeEventListener('mousemove', selection_mouseMove);
overlay.removeEventListener('mouseup', selection_mouseUp);
document.body.removeChild(overlay);
document.body.removeChild(selection);
document.body.removeChild(messageComp);
if (!selectionArea || !selectionArea.width || !selectionArea.height) return;
setTimeout(() => {
const content = {
title: pageTitle(),
cropRect: selectionArea,
url: location.origin + location.pathname,
};
browser_.runtime.sendMessage({
name: 'screenshotArea',
content: content,
apiBaseUrl: command.apiBaseUrl,
});
}, 10);
}
overlay.addEventListener('mousedown', selection_mouseDown);
overlay.addEventListener('mousemove', selection_mouseMove);
overlay.addEventListener('mouseup', selection_mouseUp);
return {};
} else {
throw new Error('Unknown command: ' + JSON.stringify(command));
}
}
async function execCommand(command) {
const response = await prepareCommandResponse(command);
browser_.runtime.sendMessage(response);
}
browser_.runtime.onMessage.addListener((command) => {
console.info('jopext: Got command:', command);
execCommand(command);
});
})();

View File

@ -0,0 +1,3 @@
// Add here all the external scripts that the content script might need
// and run browserify on it to create vendor.bundle.js
const Readability = require('readability-node').Readability;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

View File

@ -0,0 +1,46 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper",
"version": "1.0.1",
"description": "Gets and saves content from your browser to Joplin.",
"homepage_url": "https://joplin.cozic.net",
"icons": {
"48": "icons/48.png",
"96": "icons/96.png"
},
"permissions": [
"activeTab",
"tabs",
"http://*/",
"https://*/",
"<all_urls>"
],
"browser_action": {
"default_icon": "icons/32.png",
"default_title": "Joplin Web Clipper",
"default_popup": "popup/build/index.html"
},
"content_scripts": [
{
"matches": ["*://*/"],
"js": ["main.js"]
}
],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"applications": {
"gecko": {
"id": "{8419486a-54e9-11e8-9401-ac9e17909436}"
}
}
}

View File

@ -0,0 +1,45 @@
{
"name": "joplin-webclipper",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"fs-extra": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz",
"integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"graceful-fs": {
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
"dev": true
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6"
}
},
"readability-node": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/readability-node/-/readability-node-0.1.0.tgz",
"integrity": "sha1-DUBacMLCFZRKf0qbX3UGzQWpsao="
},
"universalify": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=",
"dev": true
}
}
}

View File

@ -0,0 +1,17 @@
{
"name": "joplin-webclipper",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"readability-node": "^0.1.0"
},
"devDependencies": {
"fs-extra": "^6.0.1"
}
}

View File

@ -0,0 +1,21 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
node_modules
# testing
coverage
# production
build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<div id="popup-content">
<div class="button" data-action="getCompletePageHtml">Complete HTMLxx</div>
<div class="button beast">Turtle</div>
<div class="button beast">Snake</div>
<div class="button reset">Reset</div>
</div>
<div id="error-content" class="hidden">
<p>Can't beastify this web page.</p><p>Try a different page.</p>
</div>
<script src="index.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
{
"name": "joplin-webclipper-popup",
"version": "0.1.0",
"private": true,
"homepage": ".",
"dependencies": {
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-redux": "^5.0.7",
"react-scripts": "1.1.4",
"redux": "^4.0.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"watch": "cra-build-watch",
"postinstall": "node scripts/postinstall.js"
},
"devDependencies": {
"cra-build-watch": "^1.0.1",
"fs-extra": "^6.0.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
const fs = require('fs-extra');
fs.copySync(__dirname + '/../../../../ReactNativeClient/lib/randomClipperPort.js', __dirname + '/../src/randomClipperPort.js');

View File

@ -0,0 +1,104 @@
.App {
width: 400px;
height: 600px;
overflow-x: hidden;
overflow-y: hidden;
display: flex;
flex-direction: column;
background-color: #162b3d;
font-size: 16px;
}
.App h2 {
font-size: 1em;
color: #5A95C7;
padding-left: 10px;
margin-top: .5em;
margin-bottom: .5em;
font-weight: normal;
}
.App .Disabled {
opacity: .5;
}
.App .Controls {
flex: 0;
padding-top: 10px;
}
.App .Controls ul {
flex: 0;
list-style-type: none;
padding: 0 10px;
margin: 0;
display: flex;
flex-direction: column;
align-items: stretch;
}
.App a.Button {
background-color: #0269c2;
padding: 0 14px;
display: flex;
flex: 1;
color: #eee;
font-weight: normal;
font-size: .8em;
cursor: pointer;
align-items: center;
min-height: 31px;
}
.App a.Button:hover {
background-color: #1E89E6;
}
.App .Controls a.Button {
margin-bottom: 10px;
}
.App .Preview {
min-height: 0;
flex: 1;
align-items: stretch;
margin: 0 10px 10px 10px;
display: flex;
flex-direction: column;
/*border: 2px solid red;*/
}
.App .Preview .Info {
color: #38668D;
}
.App .Preview .Title {
flex: 0;
margin-bottom: 10px;
}
.App .Preview .BodyWrapper {
flex: 1;
overflow: hidden;
flex-shrink: 1;
min-width: auto;
}
.App .Preview .Body {
/*flex: 1;*/
font-size: .5em;
overflow-x: hidden;
overflow-y: scroll;
overflow-wrap: break-word;
background-color: #ffffff;
/*flex-shrink: 1;*/
/*min-width: auto;*/
/*padding: 10px;*/
/*margin-bottom: 10px;*/
width: 100%;
height: 100%;
}
.App .Preview .Confirm {
flex: 0;
}

View File

@ -0,0 +1,146 @@
import React, { Component } from 'react';
import './App.css';
const { connect } = require('react-redux');
const { bridge } = require('./bridge');
class AppComponent extends Component {
constructor() {
super();
this.state = ({
contentScriptLoaded: false,
});
this.confirm_click = () => {
bridge().sendContentToJoplin(this.props.clippedContent);
}
this.contentTitle_change = (event) => {
this.props.dispatch({
type: 'CLIPPED_CONTENT_TITLE_SET',
text: event.currentTarget.value
});
}
this.clipScreenshot_click = async () => {
try {
const baseUrl = await bridge().clipperServerBaseUrl();
bridge().sendCommandToActiveTab({
name: 'screenshot',
apiBaseUrl: baseUrl,
});
window.close();
} catch (error) {
this.props.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: false, errorMessage: error.message } });
}
}
}
clipSimplified_click() {
bridge().sendCommandToActiveTab({
name: 'simplifiedPageHtml',
});
}
clipComplete_click() {
bridge().sendCommandToActiveTab({
name: 'completePageHtml',
});
}
async loadContentScripts() {
await bridge().tabsExecuteScript({file: "/content_scripts/JSDOMParser.js"});
await bridge().tabsExecuteScript({file: "/content_scripts/Readability.js"});
await bridge().tabsExecuteScript({file: "/content_scripts/index.js"});
}
async componentDidMount() {
await this.loadContentScripts();
this.setState({
contentScriptLoaded: true,
});
}
render() {
if (!this.state.contentScriptLoaded) return 'Loading...';
const warningComponent = !this.props.warning ? null : <div className="Warning">{ this.props.warning }</div>
const hasContent = !!this.props.clippedContent;
const content = this.props.clippedContent;
let previewComponent = null;
const operation = this.props.contentUploadOperation;
if (operation) {
let msg = '';
if (operation.searchingClipperServer) {
msg = 'Searching clipper service... Please make sure that Joplin is running.';
} else if (operation.uploading) {
msg = 'Processing note... The note will be available in Joplin as soon as the web page and images have been downloaded and converted. In the meantime you may close this popup.';
} else if (operation.success) {
msg = 'Note was successfully created!';
} else {
msg = 'There was some error creating the note: ' + operation.errorMessage;
}
previewComponent = (
<div className="Preview">
<p className="Info">{ msg }</p>
</div>
);
} else {
if (hasContent) {
previewComponent = (
<div className="Preview">
<input className={"Title"} value={content.title} onChange={this.contentTitle_change}/>
<div className={"BodyWrapper"}>
<div className={"Body"} dangerouslySetInnerHTML={{__html: content.bodyHtml}}></div>
</div>
<a className={"Confirm Button"} onClick={this.confirm_click}>Confirm</a>
</div>
);
} else {
previewComponent = (
<div className="Preview">
<p className="Info">(No preview yet)</p>
</div>
);
}
}
return (
<div className="App">
<div className="Controls">
<ul>
<li><a className="Button" onClick={this.clipSimplified_click}>Clip simplified page</a></li>
<li><a className="Button" onClick={this.clipComplete_click}>Clip complete page</a></li>
<li><a className="Button" onClick={this.clipScreenshot_click}>Clip screenshot</a></li>
</ul>
</div>
{ warningComponent }
<h2>Preview:</h2>
{ previewComponent }
</div>
);
}
}
const mapStateToProps = (state) => {
return {
warning: state.warning,
clippedContent: state.clippedContent,
contentUploadOperation: state.contentUploadOperation,
};
};
const App = connect(mapStateToProps)(AppComponent);
export default App;

View File

@ -0,0 +1,7 @@
const Global = {}
Global.browser = function() {
return window.browser;
}
module.exports = Global;

View File

@ -0,0 +1,185 @@
const randomClipperPort = require('./randomClipperPort');
class Bridge {
init(browser, browserSupportsPromises, dispatch) {
console.info('Popup: Init bridge');
this.browser_ = browser;
this.dispatch_ = dispatch;
this.browserSupportsPromises_ = browserSupportsPromises;
this.clipperServerPort_ = null;
this.clipperServerPortStatus_ = 'searching';
this.browser_notify = async (command) => {
console.info('Popup: Got command: ' + command.name);
if (command.warning) {
console.warn('Popup: Got warning: ' + command.warning);
this.dispatch({ type: 'WARNING_SET', text: command.warning });
} else {
this.dispatch({ type: 'WARNING_SET', text: '' });
}
if (command.name === 'clippedContent') {
const content = {
title: command.title,
bodyHtml: command.html,
baseUrl: command.baseUrl,
url: command.url,
};
this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content });
}
}
this.browser_.runtime.onMessage.addListener(this.browser_notify);
this.findClipperServerPort();
}
browser() {
return this.browser_;
}
dispatch(action) {
return this.dispatch_(action);
}
async findClipperServerPort() {
let state = null;
for (let i = 0; i < 10; i++) {
state = randomClipperPort(state);
try {
console.info('findClipperServerPort: Trying ' + state.port);
const response = await fetch('http://127.0.0.1:' + state.port + '/ping');
const text = await response.text();
console.info('findClipperServerPort: Got response: ' + text);
if (text.trim() === 'JoplinClipperServer') {
this.clipperServerPortStatus_ = 'found';
this.clipperServerPort_ = state.port;
return;
}
} catch (error) {
// continue
}
}
this.clipperServerPortStatus_ = 'not_found';
return null;
}
async clipperServerPort() {
return new Promise((resolve, reject) => {
const checkStatus = () => {
if (this.clipperServerPortStatus_ === 'not_found') {
reject(new Error('Could not find clipper service. Please make sure that Joplin is running and that the clipper server is enabled.'));
return true;
} else if (this.clipperServerPortStatus_ === 'found') {
resolve(this.clipperServerPort_);
return true;
}
return false;
}
if (checkStatus()) return;
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { searchingClipperServer: true } });
const waitIID = setInterval(() => {
if (!checkStatus()) return;
this.dispatch({ type: 'CONTENT_UPLOAD', operation: null });
clearInterval(waitIID);
}, 1000);
});
}
async clipperServerBaseUrl() {
const port = await this.clipperServerPort();
return 'http://127.0.0.1:' + port;
}
async tabsExecuteScript(options) {
if (this.browserSupportsPromises_) return this.browser().tabs.executeScript(options);
return new Promise((resolve, reject) => {
this.browser().tabs.executeScript(options, () => {
resolve();
});
})
}
async tabsQuery(options) {
if (this.browserSupportsPromises_) return this.browser().tabs.query(options);
return new Promise((resolve, reject) => {
this.browser().tabs.query(options, (tabs) => {
resolve(tabs);
});
});
}
async tabsSendMessage(tabId, command) {
if (this.browserSupportsPromises_) return this.browser().tabs.sendMessage(tabId, command);
return new Promise((resolve, reject) => {
this.browser().tabs.sendMessage(tabId, command, (result) => {
resolve(result);
});
});
}
async sendCommandToActiveTab(command) {
const tabs = await this.tabsQuery({ active: true, currentWindow: true });
if (!tabs.length) {
console.warn('No valid tab');
return;
}
this.dispatch({ type: 'CONTENT_UPLOAD', operation: null });
console.info('Sending message ', command);
await this.tabsSendMessage(tabs[0].id, command);
}
async sendContentToJoplin(content) {
console.info('Popup: Sending to Joplin...');
try {
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: true } });
if (!content) throw new Error('Cannot send empty content');
const baseUrl = await this.clipperServerBaseUrl();
const response = await fetch(baseUrl + "/notes", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(content)
})
if (!response.ok) {
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: false, errorMessage: response.text() } });
} else {
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: true } });
}
} catch (error) {
this.dispatch({ type: 'CONTENT_UPLOAD', operation: { uploading: false, success: false, errorMessage: error.message } });
}
}
}
const bridge_ = new Bridge();
const bridge = function() {
return bridge_;
}
export { bridge }

View File

@ -0,0 +1,5 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}

View File

@ -0,0 +1,52 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
const { Provider } = require('react-redux');
const { bridge } = require('./bridge');
const { createStore } = require('redux');
const defaultState = {
warning: '',
clippedContent: null,
contentUploadOperation: null,
};
function reducer(state = defaultState, action) {
let newState = state;
if (action.type === 'WARNING_SET') {
newState = Object.assign({}, state);
newState.warning = action.text;
} else if (action.type === 'CLIPPED_CONTENT_SET') {
newState = Object.assign({}, state);
newState.clippedContent = action.content;
} else if (action.type === 'CLIPPED_CONTENT_TITLE_SET') {
newState = Object.assign({}, state);
const newContent = newState.clippedContent ? Object.assign({}, newState.clippedContent) : {};
newContent.title = action.text;
newState.clippedContent = newContent;
} else if (action.type === 'CONTENT_UPLOAD') {
newState = Object.assign({}, state);
newState.contentUploadOperation = action.operation;
}
return newState;
}
const store = createStore(reducer);
bridge().init(window.browser ? window.browser : window.chrome, !!window.browser, store.dispatch);
console.info('Popup: Creating React app...');
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));

View File

@ -0,0 +1,164 @@
const reservedPorts = [1024, 1027, 1028, 1029, 1058, 1059, 1080, 1085, 1098, 1099, 1109, 1119, 1167, 1194, 1198, 1214, 1220, 1234, 1241, 1270, 1293, 1311, 1314, 1337, 1341, 1344,
1352, 1360, 1414, 1417, 1418, 1419, 1420, 1431, 1433, 1434, 1492, 1494, 1500, 1501, 1503, 1512, 1513, 1521, 1524, 1527, 1533, 1547, 1550, 1581, 1582, 1583, 1589, 1604, 1626, 1627,
1628, 1629, 1645, 1646, 1666, 1677, 1688, 1701, 1707, 1716, 1719, 1720, 1723, 1755, 1761, 1783, 1801, 1812, 1813, 1863, 1880, 1883, 1900, 1935, 1967, 1970, 1972, 1984, 1985, 1998,
2000, 2010, 2033, 2049, 2056, 2080, 2082, 2083, 2086, 2087, 2095, 2096, 2100, 2101, 2102, 2103, 2104, 2123, 2142, 2152, 2159, 2181, 2195, 2196, 2210, 2211, 2221, 2222, 2261, 2262,
2266, 2305, 2351, 2368, 2369, 2370, 2372, 2375, 2376, 2377, 2379, 2380, 2399, 2401, 2404, 2424, 2427, 2447, 2480, 2483, 2484, 2535, 2541, 2546, 2593, 2598, 2599, 2638, 2710, 2727,
2809, 2811, 2827, 2944, 2945, 2947, 2948, 2949, 2967, 3000, 3004, 3020, 3050, 3052, 3074, 3101, 3128, 3225, 3233, 3260, 3268, 3269, 3283, 3290, 3305, 3306, 3313, 3316, 3323, 3332,
3333, 3351, 3386, 3389, 3396, 3412, 3423, 3424, 3455, 3478, 3479, 3480, 3483, 3493, 3516, 3527, 3535, 3544, 3632, 3645, 3659, 3667, 3689, 3690, 3702, 3724, 3725, 3768, 3784, 3785,
3799, 3804, 3825, 3826, 3830, 3835, 3856, 3868, 3872, 3880, 3900, 3960, 3962, 3978, 3979, 3999, 4000, 4001, 4018, 4035, 4045, 4050, 4069, 4089, 4090, 4093, 4096, 4105, 4111, 4116,
4125, 4172, 4190, 4198, 4201, 4222, 4226, 4242, 4243, 4244, 4303, 4321, 4352, 4444, 4486, 4488, 4500, 4502, 4505, 4534, 4560, 4567, 4569, 4604, 4605, 4610, 4662, 4664, 4672, 4711,
4713, 4728, 4730, 4739, 4747, 4750, 4753, 4840, 4843, 4847, 4848, 4894, 4949, 4950, 5000, 5001, 5002, 5003, 5004, 5005, 5010, 5011, 5031, 5037, 5048, 5050, 5051, 5060, 5061, 5062,
5064, 5065, 5070, 5084, 5085, 5093, 5099, 5104, 5121, 5124, 5125, 5150, 5151, 5154, 5190, 5201, 5222, 5223, 5228, 5242, 5243, 5246, 5247, 5269, 5280, 5281, 5298, 5310, 5349, 5351,
5353, 5355, 5357, 5358, 5394, 5402, 5405, 5412, 5413, 5417, 5421, 5432, 5433, 5445, 5480, 5481, 5495, 5498, 5499, 5500, 5501, 5517, 5550, 5554, 5555, 5556, 5568, 5601, 5631, 5632,
5656, 5666, 5667, 5670, 5672, 5678, 5683, 5701, 5718, 5719, 5722, 5723, 5724, 5741, 5742, 5800, 5900, 5931, 5938, 5984, 5985, 5986, 5988, 6000, 6001, 6002, 6003, 6004, 6005, 6006,
6007, 6008, 6009, 6010, 6011, 6012, 6013, 6014, 6015, 6016, 6017, 6018, 6019, 6020, 6021, 6022, 6023, 6024, 6025, 6026, 6027, 6028, 6029, 6030, 6031, 6032, 6033, 6034, 6035, 6036,
6037, 6038, 6039, 6040, 6041, 6042, 6043, 6044, 6045, 6046, 6047, 6048, 6049, 6050, 6051, 6052, 6053, 6054, 6055, 6056, 6057, 6058, 6059, 6060, 6061, 6062, 6063, 6086, 6100, 6101,
6110, 6111, 6112, 6113, 6136, 6159, 6200, 6201, 6225, 6227, 6240, 6244, 6255, 6257, 6260, 6262, 6343, 6346, 6347, 6350, 6379, 6389, 6432, 6436, 6437, 6444, 6445, 6463, 6502, 6513,
6514, 6515, 6543, 6556, 6560, 6566, 6571, 6600, 6601, 6602, 6619, 6622, 6653, 6660, 6665, 6679, 6690, 6697, 6699, 6715, 6771, 6783, 6789, 6869, 6881, 6888, 6889, 6891, 6901, 6902,
6969, 6970, 7000, 7001, 7002, 7005, 7006, 7010, 7022, 7023, 7025, 7047, 7070, 7133, 7144, 7145, 7171, 7262, 7272, 7306, 7307, 7312, 7396, 7400, 7401, 7402, 7471, 7473, 7474, 7478,
7542, 7547, 7575, 7624, 7631, 7634, 7652, 7655, 7656, 7670, 7687, 7707, 7717, 7777, 7831, 7880, 7890, 7915, 7935, 7946, 7990, 8000, 8005, 8006, 8007, 8008, 8009, 8042, 8069, 8070,
8074, 8075, 8080, 8088, 8089, 8090, 8091, 8092, 8111, 8112, 8116, 8118, 8123, 8139, 8140, 8172, 8184, 8194, 8200, 8222, 8243, 8245, 8280, 8281, 8291, 8303, 8332, 8333, 8337, 8384,
8388, 8443, 8444, 8484, 8500, 8530, 8531, 8580, 8629, 8642, 8691, 8767, 8834, 8840, 8880, 8883, 8887, 8888, 8889, 8983, 8997, 8998, 8999, 9000, 9001, 9002, 9006, 9030, 9042, 9043,
9050, 9060, 9080, 9090, 9091, 9092, 9100, 9101, 9102, 9103, 9119, 9150, 9191, 9199, 9200, 9217, 9293, 9300, 9303, 9306, 9309, 9312, 9332, 9333, 9339, 9389, 9418, 9419, 9420, 9421,
9422, 9425, 9443, 9535, 9536, 9600, 9675, 9676, 9695, 9785, 9800, 9875, 9898, 9899, 9981, 9982, 9987, 9993, 9997, 9999, 10000, 10001, 10009, 10010, 10024, 10025, 10042, 10050, 10051,
10080, 10110, 10172, 10200, 10201, 10212, 10308, 10480, 10505, 10514, 10823, 10891, 10933, 11001, 11111, 11112, 11211, 11214, 11215, 11235, 11311, 11371, 11753, 12012, 12013, 12035,
12043, 12046, 12201, 12222, 12223, 12345, 12443, 12489, 12975, 13000, 13008, 13075, 13720, 13721, 13724, 13782, 13783, 13785, 13786, 14550, 14567, 15000, 15345, 15441, 15567, 15672,
16000, 16080, 16200, 16225, 16250, 16261, 16300, 16384, 16385, 16386, 16387, 16393, 16394, 16395, 16396, 16397, 16398, 16399, 16400, 16401, 16402, 16403, 16404, 16405, 16406, 16407,
16408, 16409, 16410, 16411, 16412, 16413, 16414, 16415, 16416, 16417, 16418, 16419, 16420, 16421, 16422, 16423, 16424, 16425, 16426, 16427, 16428, 16429, 16430, 16431, 16432, 16433,
16434, 16435, 16436, 16437, 16438, 16439, 16440, 16441, 16442, 16443, 16444, 16445, 16446, 16447, 16448, 16449, 16450, 16451, 16452, 16453, 16454, 16455, 16456, 16457, 16458, 16459,
16460, 16461, 16462, 16463, 16464, 16465, 16466, 16467, 16468, 16469, 16470, 16471, 16472, 16482, 16567, 17011, 17500, 18091, 18092, 18104, 18200, 18201, 18206, 18300, 18301, 18306,
18333, 18400, 18401, 18505, 18506, 18605, 18606, 19000, 19001, 19132, 19150, 19226, 19294, 19295, 19302, 19812, 19813, 19814, 19999, 20000, 20560, 20595, 20808, 21025, 22000, 22136,
22222, 23073, 23399, 23513, 24441, 24444, 24465, 24554, 24800, 24842, 25565, 25575, 25826, 26000, 26900, 27000, 27016, 27017, 27031, 27036, 27037, 27374, 27500, 27888, 27901, 27950,
27960, 28001, 28015, 28770, 28785, 28852, 28910, 28960, 29000, 29070, 29900, 29920, 30564, 31337, 31416, 31438, 31457, 32137, 32400, 32764, 32887, 32976, 33434, 33848, 34000, 34197,
35357, 37008, 40000, 43110, 43594, 44405, 44818, 47001, 47808, 49151];
// From https://github.com/coverslide/node-alea
const AleaModule = function () {
// importState to sync generator states
Alea.importState = function(i){
var random = new Alea();
random.importState(i);
return random;
};
return Alea;
function Alea() {
return (function(args) {
// Johannes Baagøe <baagoe@baagoe.com>, 2010
var s0 = 0;
var s1 = 0;
var s2 = 0;
var c = 1;
if (args.length === 0) {
args = [+new Date()];
}
var mash = Mash();
s0 = mash(' ');
s1 = mash(' ');
s2 = mash(' ');
for (var i = 0; i < args.length; i++) {
s0 -= mash(args[i]);
if (s0 < 0) {
s0 += 1;
}
s1 -= mash(args[i]);
if (s1 < 0) {
s1 += 1;
}
s2 -= mash(args[i]);
if (s2 < 0) {
s2 += 1;
}
}
mash = null;
var random = function() {
var t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32
s0 = s1;
s1 = s2;
return s2 = t - (c = t | 0);
};
random.uint32 = function() {
return random() * 0x100000000; // 2^32
};
random.fract53 = function() {
return random() +
(random() * 0x200000 | 0) * 1.1102230246251565e-16; // 2^-53
};
random.version = 'Alea 0.9';
random.args = args;
// my own additions to sync state between two generators
random.exportState = function(){
return [s0, s1, s2, c];
};
random.importState = function(i){
s0 = +i[0] || 0;
s1 = +i[1] || 0;
s2 = +i[2] || 0;
c = +i[3] || 0;
};
return random;
} (Array.prototype.slice.call(arguments)));
}
function Mash() {
var n = 0xefc8249d;
var mash = function(data) {
data = data.toString();
for (var i = 0; i < data.length; i++) {
n += data.charCodeAt(i);
var h = 0.02519603282416938 * n;
n = h >>> 0;
h -= n;
h *= n;
n = h >>> 0;
h -= n;
n += h * 0x100000000; // 2^32
}
return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
};
mash.version = 'Mash 0.9';
return mash;
}
}
const Alea = AleaModule()
function findClipperPort(state) {
const minPort = 1024
const maxPort = 49151
let prng = null;
if (!state) {
prng = new Alea(1867)
state = { prng: prng }
} else {
prng = state.prng;
}
const randomPort = () => {
return minPort + Math.floor(prng() * ((maxPort + 1) - minPort));
}
let port = null;
for (let i = 0; i < maxPort; i++) {
port = randomPort();
if (reservedPorts.indexOf(port) < 0) break;
}
if (!port) throw new Error('Cannot find a non-reserved port');
state.port = port;
return state;
}
module.exports = findClipperPort;

View File

@ -0,0 +1,31 @@
html, body {
width: 100px;
}
.hidden {
display: none;
}
.button {
margin: 3% auto;
padding: 4px;
text-align: center;
font-size: 1.5em;
cursor: pointer;
}
.beast:hover {
background-color: #CFF2F2;
}
.beast {
background-color: #E5F2F2;
}
.reset {
background-color: #FBFBC9;
}
.reset:hover {
background-color: #EAEA9D;
}

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@ const DecryptionWorker = require('lib/services/DecryptionWorker');
const InteropService = require('lib/services/InteropService');
const InteropServiceHelper = require('./InteropServiceHelper.js');
const ResourceService = require('lib/services/ResourceService');
const ClipperServer = require('lib/ClipperServer');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
@ -462,6 +463,14 @@ class Application extends BaseApplication {
}, {
type: 'separator',
screens: ['Main'],
},{
label: _('Web clipper options'),
click: () => {
this.dispatch({
type: 'NAV_GO',
routeName: 'ClipperConfig',
});
}
},{
label: _('Encryption options'),
click: () => {
@ -680,6 +689,17 @@ class Application extends BaseApplication {
DecryptionWorker.instance().scheduleStart();
});
}
const clipperLogger = new Logger();
clipperLogger.addTarget('file', { path: Setting.value('profileDir') + '/log-clipper.txt' });
clipperLogger.addTarget('console');
ClipperServer.instance().setLogger(clipperLogger);
ClipperServer.instance().setDispatch(this.store().dispatch);
if (Setting.value('clipperServer.autoStart')) {
ClipperServer.instance().start();
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -13,6 +13,7 @@ const { StatusScreen } = require('./StatusScreen.min.js');
const { ImportScreen } = require('./ImportScreen.min.js');
const { ConfigScreen } = require('./ConfigScreen.min.js');
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min.js');
const { ClipperConfigScreen } = require('./ClipperConfigScreen.min.js');
const { Navigator } = require('./Navigator.min.js');
const { app } = require('../app');
@ -86,6 +87,7 @@ class RootComponent extends React.Component {
Config: { screen: ConfigScreen, title: () => _('Options') },
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
EncryptionConfig: { screen: EncryptionConfigScreen, title: () => _('Encryption Options') },
ClipperConfig: { screen: ClipperConfigScreen, title: () => _('Clipper Options') },
};
return (

File diff suppressed because it is too large Load Diff

View File

@ -82,11 +82,15 @@
"electron-context-menu": "^0.9.1",
"electron-is-dev": "^0.3.0",
"electron-window-state": "^4.1.1",
"es6-promise-pool": "^2.5.0",
"follow-redirects": "^1.2.5",
"form-data": "^2.3.1",
"fs-extra": "^5.0.0",
"highlight.js": "^9.12.0",
"html-entities": "^1.2.1",
"image-type": "^3.0.0",
"joplin-turndown": "^4.0.5",
"joplin-turndown-plugin-gfm": "^1.0.6",
"jssha": "^2.3.1",
"katex": "^0.9.0-beta1",
"levenshtein": "^1.0.5",
@ -107,7 +111,10 @@
"react-datetime": "^2.11.0",
"react-dom": "^16.0.0",
"react-redux": "^5.0.6",
"read-chunk": "^2.1.0",
"readability-node": "^0.1.0",
"redux": "^3.7.2",
"server-destroy": "^1.0.1",
"smalltalk": "^2.5.1",
"sprintf-js": "^1.1.1",
"sqlite3": "^3.1.13",

View File

@ -385,6 +385,30 @@ class BaseApplication {
return os.homedir() + '/.config/' + Setting.value('appName');
}
async testing() {
const markdownUtils = require('lib/markdownUtils');
const ClipperServer = require('lib/ClipperServer');
const server = new ClipperServer();
const HtmlToMd = require('lib/HtmlToMd');
const service = new HtmlToMd();
const html = await shim.fsDriver().readFile('/mnt/d/test.html');
let markdown = service.parse(html, { baseUrl: 'https://duckduckgo.com/' });
console.info(markdown);
console.info('--------------------------------------------------');
const imageUrls = markdownUtils.extractImageUrls(markdown);
let result = await server.downloadImages_(imageUrls);
result = await server.createResourcesFromPaths_(result);
console.info(result);
markdown = server.replaceImageUrlsByResources_(markdown, result);
console.info('--------------------------------------------------');
console.info(markdown);
console.info('--------------------------------------------------');
}
async start(argv) {
let startFlags = await this.handleStartFlags_(argv);
@ -466,6 +490,8 @@ class BaseApplication {
if (!currentFolder) currentFolder = await Folder.defaultFolder();
Setting.setValue('activeFolderId', currentFolder ? currentFolder.id : '');
// await this.testing();process.exit();
return argv;
}

View File

@ -0,0 +1,327 @@
const { netUtils } = require('lib/net-utils');
const urlParser = require("url");
const Note = require('lib/models/Note');
const Folder = require('lib/models/Folder');
const Resource = require('lib/models/Resource');
const Setting = require('lib/models/Setting');
const { shim } = require('lib/shim');
const md5 = require('md5');
const { fileExtension, safeFileExtension, safeFilename, filename } = require('lib/path-utils');
const HtmlToMd = require('lib/HtmlToMd');
const { Logger } = require('lib/logger.js');
const markdownUtils = require('lib/markdownUtils');
const mimeUtils = require('lib/mime-utils.js').mime;
const randomClipperPort = require('lib/randomClipperPort');
const enableServerDestroy = require('server-destroy');
class ClipperServer {
constructor() {
this.logger_ = new Logger();
this.startState_ = 'idle';
this.server_ = null;
this.port_ = null;
}
static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new ClipperServer();
return this.instance_;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
setDispatch(d) {
this.dispatch_ = d;
}
dispatch(action) {
if (!this.dispatch_) throw new Error('dispatch not set!');
this.dispatch_(action);
}
setStartState(v) {
if (this.startState_ === v) return;
this.startState_ = v;
this.dispatch({
type: 'CLIPPER_SERVER_SET',
startState: v,
});
}
setPort(v) {
if (this.port_ === v) return;
this.port_ = v;
this.dispatch({
type: 'CLIPPER_SERVER_SET',
port: v,
});
}
// startState() {
// return this.startState_;
// }
// port() {
// return this.port_;
// }
htmlToMdParser() {
if (this.htmlToMdParser_) return this.htmlToMdParser_;
this.htmlToMdParser_ = new HtmlToMd();
return this.htmlToMdParser_;
}
async requestNoteToNote(requestNote) {
const output = {
title: requestNote.title ? requestNote.title : '',
body: requestNote.body ? requestNote.body : '',
};
if (requestNote.bodyHtml) {
// 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.bodyHtml + '</div>', {
baseUrl: requestNote.baseUrl ? requestNote.baseUrl : '',
});
}
if (requestNote.parent_id) {
output.parent_id = requestNote.parent_id;
} else {
const folder = await Folder.defaultFolder();
if (!folder) throw new Error('Cannot find folder for note');
output.parent_id = folder.id;
}
if (requestNote.url) output.source_url = requestNote.url;
return output;
}
// Note must have been saved first
async attachImageFromDataUrl_(note, imageDataUrl, cropRect) {
const tempDir = Setting.value('tempDir');
const mime = mimeUtils.fromDataUrl(imageDataUrl);
let ext = mimeUtils.toFileExtension(mime) || '';
if (ext) ext = '.' + ext;
const tempFilePath = tempDir + '/' + md5(Math.random() + '_' + Date.now()) + ext;
const imageConvOptions = {};
if (cropRect) imageConvOptions.cropRect = cropRect;
await shim.imageFromDataUrl(imageDataUrl, tempFilePath, imageConvOptions);
return await shim.attachFileToNote(note, tempFilePath);
}
async downloadImage_(url) {
const tempDir = Setting.value('tempDir');
const name = filename(url);
let fileExt = safeFileExtension(fileExtension(url).toLowerCase());
if (fileExt) fileExt = '.' + fileExt;
let imagePath = tempDir + '/' + safeFilename(name) + fileExt;
if (await shim.fsDriver().exists(imagePath)) imagePath = tempDir + '/' + safeFilename(name) + '_' + md5(Math.random() + '_' + Date.now()).substr(0,10) + fileExt;
try {
const result = await shim.fetchBlob(url, { path: imagePath });
return imagePath;
} catch (error) {
this.logger().warn('Cannot download image at ' + url, error);
return '';
}
}
async downloadImages_(urls) {
const PromisePool = require('es6-promise-pool')
const output = {};
let urlIndex = 0;
const promiseProducer = () => {
if (urlIndex >= urls.length) return null;
const url = urls[urlIndex++];
return new Promise(async (resolve, reject) => {
const imagePath = await this.downloadImage_(url);
if (imagePath) output[url] = { path: imagePath };
resolve();
});
}
const concurrency = 3
const pool = new PromisePool(promiseProducer, concurrency)
await pool.start()
return output;
}
async createResourcesFromPaths_(urls) {
for (let url in urls) {
if (!urls.hasOwnProperty(url)) continue;
const urlInfo = urls[url];
try {
const resource = await shim.createResourceFromPath(urlInfo.path);
urlInfo.resource = resource;
} catch (error) {
this.logger().warn('Cannot create resource for ' + url, error);
}
}
return urls;
}
async removeTempFiles_(urls) {
for (let url in urls) {
if (!urls.hasOwnProperty(url)) continue;
const urlInfo = urls[url];
try {
await shim.fsDriver().remove(urlInfo.path);
} catch (error) {
this.logger().warn('Cannot remove ' + urlInfo.path, error);
}
}
}
replaceImageUrlsByResources_(md, urls) {
let output = md.replace(/(!\[.*?\]\()([^\s\)]+)(.*?\))/g, (match, before, imageUrl, after) => {
const urlInfo = urls[imageUrl];
if (!urlInfo || !urlInfo.resource) return before + imageUrl + after;
const resourceUrl = Resource.internalUrl(urlInfo.resource);
return before + resourceUrl + after;
});
return output;
}
async findAvailablePort() {
const tcpPortUsed = require('tcp-port-used');
let state = null;
for (let i = 0; i < 10000; i++) {
state = randomClipperPort(state);
const inUse = await tcpPortUsed.check(state.port);
if (!inUse) return state.port;
}
throw new Error('All potential ports are in use or not available.')
}
async start() {
this.setPort(null);
this.setStartState('starting');
try {
const p = await this.findAvailablePort();
this.setPort(p);
} catch (error) {
this.setStartState('idle');
this.logger().error(error);
return;
}
this.server_ = require('http').createServer();
this.server_.on('request', (request, response) => {
const writeCorsHeaders = (code) => {
response.writeHead(code, {
"Content-Type": "application/json",
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
});
}
const writeResponseJson = (code, object) => {
writeCorsHeaders(code);
response.write(JSON.stringify(object));
response.end();
}
const writeResponseText = (code, text) => {
writeCorsHeaders(code);
response.write(text);
response.end();
}
const requestId = Date.now();
this.logger().info('Request (' + requestId + '): ' + request.method + ' ' + request.url);
const url = urlParser.parse(request.url, true);
if (request.method === 'GET') {
if (url.pathname === '/ping') {
return writeResponseText(200, 'JoplinClipperServer');
}
} else if (request.method === 'POST') {
if (url.pathname === '/notes') {
let body = '';
request.on('data', (data) => {
body += data;
});
request.on('end', async () => {
try {
const requestNote = JSON.parse(body);
let note = await this.requestNoteToNote(requestNote);
const imageUrls = markdownUtils.extractImageUrls(note.body);
let result = await this.downloadImages_(imageUrls);
result = await this.createResourcesFromPaths_(result);
await this.removeTempFiles_(result);
note.body = this.replaceImageUrlsByResources_(note.body, result);
note = await Note.save(note);
if (requestNote.imageDataUrl) {
await this.attachImageFromDataUrl_(note, requestNote.imageDataUrl, requestNote.cropRect);
}
this.logger().info('Request (' + requestId + '): Created note ' + note.id);
return writeResponseJson(200, note);
} catch (error) {
this.logger().error(error);
return writeResponseJson(400, { errorCode: 'exception', errorMessage: error.message });
}
});
} else {
return writeResponseJson(404, { errorCode: 'not_found' });
}
} else if (request.method === 'OPTIONS') {
writeCorsHeaders(200);
response.end();
} else {
return writeResponseJson(405, { errorCode: 'method_not_allowed' });
}
});
this.server_.on('close', () => {
});
enableServerDestroy(this.server_);
this.logger().info('Starting Clipper server on port ' + this.port_);
this.server_.listen(this.port_);
this.setStartState('started');
}
async stop() {
this.server_.destroy();
this.server_ = null;
this.setStartState('idle');
this.setPort(null);
}
}
module.exports = ClipperServer;

View File

@ -1,14 +0,0 @@
const { enexXmlToMd } = require('lib/import-enex-md-gen.js');
class HtmlToMarkdownParser {
async parse(html, options = {}) {
if (!options.baseUrl) options.baseUrl = '';
const markdown = await enexXmlToMd(html, [], options);
return markdown;
}
}
module.exports = HtmlToMarkdownParser;

View File

@ -0,0 +1,21 @@
const TurndownService = require('joplin-turndown')
const markdownUtils = require('lib/markdownUtils');
class HtmlToMd {
parse(html, options = {}) {
const turndownPluginGfm = require('joplin-turndown-plugin-gfm').gfm
const turndown = new TurndownService({
headingStyle: 'atx',
})
turndown.use(turndownPluginGfm)
turndown.remove('script');
turndown.remove('style');
let md = turndown.turndown(html)
if (options.baseUrl) md = markdownUtils.prependBaseUrl(md, options.baseUrl);
return md;
}
}
module.exports = HtmlToMd;

View File

@ -7,7 +7,7 @@ class Logger {
constructor() {
this.targets_ = [];
this.level_ = Logger.LEVEL_ERROR;
this.level_ = Logger.LEVEL_INFO;
this.fileAppendQueue_ = []
this.lastDbCleanup_ = time.unixMs();
}

View File

@ -1,16 +0,0 @@
const markdownUtils = {
// Not really escaping because that's not supported by marked.js
escapeLinkText(text) {
return text.replace(/(\[|\]|\(|\))/g, '_');
},
escapeLinkUrl(url) {
url = url.replace(/\(/g, '%28');
url = url.replace(/\)/g, '%29');
return url;
},
};
module.exports = { markdownUtils };

View File

@ -0,0 +1,37 @@
const urlUtils = require('lib/urlUtils');
const markdownUtils = {
// Not really escaping because that's not supported by marked.js
escapeLinkText(text) {
return text.replace(/(\[|\]|\(|\))/g, '_');
},
escapeLinkUrl(url) {
url = url.replace(/\(/g, '%28');
url = url.replace(/\)/g, '%29');
return url;
},
prependBaseUrl(md, baseUrl) {
return md.replace(/(\]\()([^\s\)]+)(.*?\))/g, (match, before, url, after) => {
return before + urlUtils.prependBaseUrl(url, baseUrl) + after;
});
},
extractImageUrls(md) {
// ![some text](http://path/to/image)
const regex = new RegExp(/!\[.*?\]\(([^\s\)]+).*?\)/, 'g')
let match = regex.exec(md);
const output = [];
while (match) {
const url = match[1];
if (output.indexOf(url) < 0) output.push(url);
match = regex.exec(md);
}
return output;
},
};
module.exports = markdownUtils;

View File

@ -29,6 +29,17 @@ const mime = {
return null;
},
fromDataUrl(dataUrl) {
// Example: data:image/jpeg;base64,/9j/4AAQSkZJR.....
const defaultMime = 'text/plain';
let p = dataUrl.substr(0, dataUrl.indexOf(',')).split(';');
let s = p[0];
s = s.split(':');
if (s.length <= 1) return defaultMime;
s = s[1];
return s.indexOf('/') >= 0 ? s : defaultMime; // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
},
}
module.exports = { mime };

Some files were not shown because too many files have changed in this diff Show More