From 71efff682785a771fd8757f2d0f726505bcbf3de Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Tue, 30 Jul 2019 09:35:42 +0200 Subject: [PATCH] Linter update (#1777) * Update eslint config * Applied linter to lib * Applied eslint config to CliClient/app * Removed prettier due to https://github.com/prettier/prettier/pull/4765 * First pass on test units * Applied linter config to test units * Applied eslint config to clipper * Applied to plugin dir * Applied to root of ElectronClient * Applied on RN root * Applied on CLI root * Applied on Clipper root * Applied config to tools * test hook * test hook * test hook * Added pre-commit hook * Applied rule no-trailing-spaces * Make sure root packages are installed when installing sub-dir * Added doc --- .eslintignore | 39 +- .eslintrc.js | 36 +- .prettierignore | 3 - .prettierrc.json | 8 - CONTRIBUTING.md | 5 +- CliClient/app/ResourceServer.js | 20 +- CliClient/app/app-gui.js | 79 +- CliClient/app/app.js | 103 +- CliClient/app/autocompletion.js | 63 +- CliClient/app/base-command.js | 4 +- CliClient/app/build-doc.js | 30 +- CliClient/app/cli-integration-tests.js | 41 +- CliClient/app/cli-utils.js | 51 +- CliClient/app/command-apidoc.js | 34 +- CliClient/app/command-attach.js | 5 +- CliClient/app/command-cat.js | 17 +- CliClient/app/command-config.js | 30 +- CliClient/app/command-cp.js | 5 +- CliClient/app/command-done.js | 5 +- CliClient/app/command-dump.js | 8 +- CliClient/app/command-e2ee.js | 18 +- CliClient/app/command-edit.js | 43 +- CliClient/app/command-exit.js | 4 +- CliClient/app/command-export-sync-status.js | 16 +- CliClient/app/command-export.js | 28 +- CliClient/app/command-geoloc.js | 9 +- CliClient/app/command-help.js | 33 +- CliClient/app/command-import.js | 31 +- CliClient/app/command-ls.js | 14 +- CliClient/app/command-mkbook.js | 7 +- CliClient/app/command-mknote.js | 4 +- CliClient/app/command-mktodo.js | 4 +- CliClient/app/command-mv.js | 6 +- CliClient/app/command-ren.js | 4 +- CliClient/app/command-rmbook.js | 11 +- CliClient/app/command-rmnote.js | 13 +- CliClient/app/command-search.js | 8 +- CliClient/app/command-set.js | 6 +- CliClient/app/command-status.js | 13 +- CliClient/app/command-sync.js | 39 +- CliClient/app/command-tag.js | 16 +- CliClient/app/command-todo.js | 7 +- CliClient/app/command-undone.js | 9 +- CliClient/app/command-use.js | 9 +- CliClient/app/command-version.js | 4 +- CliClient/app/fuzzing.js | 2148 ++++++++++++- CliClient/app/gui/ConsoleWidget.js | 8 +- CliClient/app/gui/FolderListWidget.js | 31 +- CliClient/app/gui/NoteListWidget.js | 6 +- CliClient/app/gui/NoteMetadataWidget.js | 4 +- CliClient/app/gui/NoteWidget.js | 8 +- CliClient/app/gui/StatusBarWidget.js | 14 +- CliClient/app/help-utils.js | 25 +- CliClient/app/main.js | 18 +- CliClient/app/onedrive-api-node-utils.js | 44 +- CliClient/package.json | 3 +- CliClient/tests/ArrayUtils.js | 4 +- CliClient/tests/EnexToMd.js | 14 +- CliClient/tests/HtmlToMd.js | 16 +- CliClient/tests/StringUtils.js | 4 +- CliClient/tests/TaskQueue.js | 4 +- CliClient/tests/encryption.js | 10 +- CliClient/tests/htmlUtils.js | 8 +- CliClient/tests/markdownUtils.js | 4 +- CliClient/tests/models_BaseItem.js | 36 +- CliClient/tests/models_Folder.js | 34 +- CliClient/tests/models_ItemChange.js | 6 +- CliClient/tests/models_Note.js | 24 +- CliClient/tests/models_Resource.js | 14 +- CliClient/tests/models_Revision.js | 18 +- CliClient/tests/models_Setting.js | 6 +- CliClient/tests/models_Tag.js | 10 +- CliClient/tests/pathUtils.js | 14 +- CliClient/tests/services_InteropService.js | 44 +- CliClient/tests/services_KvStore.js | 4 +- CliClient/tests/services_ResourceService.js | 26 +- CliClient/tests/services_Revision.js | 10 +- CliClient/tests/services_SearchEngine.js | 64 +- CliClient/tests/services_rest_Api.js | 84 +- CliClient/tests/synchronizer.js | 227 +- CliClient/tests/test-utils.js | 23 +- CliClient/tests/urlUtils.js | 4 +- Clipper/joplin-webclipper/background.js | 15 +- .../content_scripts/JSDOMParser.js | 2168 ++++++------- .../content_scripts/Readability-readerable.js | 100 +- .../content_scripts/Readability.js | 2694 ++++++++--------- .../content_scripts/index.js | 80 +- .../content_scripts/vendor.js | 4 +- Clipper/joplin-webclipper/package.json | 3 - .../popup/scripts/postinstall.js | 2 +- Clipper/joplin-webclipper/popup/src/App.js | 80 +- Clipper/joplin-webclipper/popup/src/Global.js | 6 +- Clipper/joplin-webclipper/popup/src/bridge.js | 28 +- Clipper/joplin-webclipper/popup/src/index.js | 6 +- .../popup/src/randomClipperPort.js | 2 +- ElectronClient/app/ElectronAppWrapper.js | 40 +- ElectronClient/app/InteropServiceHelper.js | 4 +- ElectronClient/app/app.js | 253 +- ElectronClient/app/bridge.js | 12 +- ElectronClient/app/checkForUpdates.js | 20 +- ElectronClient/app/compile-package-info.js | 10 +- ElectronClient/app/electronRebuild.js | 20 +- ElectronClient/app/eventManager.js | 2 +- ElectronClient/app/main-html.js | 3 +- ElectronClient/app/main.js | 2 +- ElectronClient/app/package.json | 2 +- ElectronClient/app/plugins/GotoAnything.jsx | 21 +- ElectronClient/app/theme.js | 187 +- ReactNativeClient/App.js | 49 - ReactNativeClient/index.android.js | 2 +- ReactNativeClient/index.ios.js | 2 +- ReactNativeClient/index.js | 2 +- ReactNativeClient/lib/BaseModel.js | 5 + ReactNativeClient/lib/DropboxApi.js | 4 +- ReactNativeClient/lib/SyncTargetNextcloud.js | 1 - .../lib/SyncTargetOneDriveDev.js | 2 - ReactNativeClient/lib/TaskQueue.js | 1 - ReactNativeClient/lib/TemplateUtils.js | 1 - ReactNativeClient/lib/WebDavApi.js | 3 +- ReactNativeClient/lib/WelcomeUtils.js | 1 - .../lib/components/CameraView.js | 53 +- .../lib/components/note-body-viewer.js | 1 - ReactNativeClient/lib/components/note-list.js | 2 - .../lib/components/screen-header.js | 1 - .../lib/components/screens/NoteTagsDialog.js | 3 - .../lib/components/screens/config.js | 204 +- .../components/screens/encryption-config.js | 2 - .../lib/components/screens/note.js | 315 +- .../lib/components/screens/notes.js | 3 +- .../lib/components/screens/onedrive-login.js | 1 - .../lib/components/screens/status.js | 3 - .../lib/components/screens/tag.js | 72 - .../lib/components/screens/tags.js | 4 - .../lib/components/select-date-time-dialog.js | 18 +- .../components/shared/note-screen-shared.js | 2 + .../lib/components/shared/side-menu-shared.js | 4 - .../lib/components/side-menu-content-note.js | 8 - .../lib/components/side-menu-content.js | 7 - ReactNativeClient/lib/components/side-menu.js | 2 - .../lib/file-api-driver-local.js | 2 +- .../lib/file-api-driver-onedrive.js | 26 +- .../lib/file-api-driver-webdav.js | 5 +- ReactNativeClient/lib/fs-driver-node.js | 2 +- ReactNativeClient/lib/fs-driver-rn.js | 1 + ReactNativeClient/lib/import-enex-md-gen.js | 5 - ReactNativeClient/lib/import-enex.js | 9 - ReactNativeClient/lib/joplin-database.js | 2 +- ReactNativeClient/lib/logger.js | 3 - ReactNativeClient/lib/markdownUtils.js | 2 +- ReactNativeClient/lib/models/BaseItem.js | 11 +- ReactNativeClient/lib/models/Folder.js | 2 - ReactNativeClient/lib/models/Note.js | 4 +- ReactNativeClient/lib/models/Resource.js | 1 - ReactNativeClient/lib/models/Revision.js | 2 - ReactNativeClient/lib/models/Search.js | 1 - ReactNativeClient/lib/onedrive-api.js | 1 + ReactNativeClient/lib/parseUri.js | 2 + ReactNativeClient/lib/path-utils.js | 4 +- ReactNativeClient/lib/promise-utils.js | 29 +- ReactNativeClient/lib/reducer.js | 355 ++- ReactNativeClient/lib/renderers/HtmlToHtml.js | 1 - ReactNativeClient/lib/renderers/MdToHtml.js | 4 - .../lib/renderers/MdToHtml/rules/checkbox.js | 5 - .../MdToHtml/rules/highlight_keywords.js | 4 - .../renderers/MdToHtml/rules/html_image.js | 4 +- .../lib/renderers/MdToHtml/rules/image.js | 2 - .../lib/renderers/MdToHtml/rules/katex.js | 3 +- .../lib/renderers/MdToHtml/rules/link_open.js | 6 +- ReactNativeClient/lib/renderers/noteStyle.js | 2 +- .../lib/services/DecryptionWorker.js | 1 - .../lib/services/EncryptionService.js | 39 +- .../lib/services/ExternalEditWatcher.js | 10 +- .../lib/services/InteropService.js | 4 +- .../services/InteropService_Exporter_Jex.js | 8 - .../services/InteropService_Exporter_Md.js | 1 - .../services/InteropService_Importer_Enex.js | 8 - .../services/InteropService_Importer_Jex.js | 8 - .../services/InteropService_Importer_Md.js | 7 - .../services/InteropService_Importer_Raw.js | 4 +- .../lib/services/RevisionService.js | 1 - .../lib/services/SearchEngine.js | 2 - ReactNativeClient/lib/services/rest/Api.js | 16 +- ReactNativeClient/lib/shim-init-node.js | 5 +- ReactNativeClient/lib/shim.js | 14 +- ReactNativeClient/lib/string-utils.js | 4 +- ReactNativeClient/lib/synchronizer.js | 11 +- ReactNativeClient/main.js | 2 +- ReactNativeClient/metro.config.js | 18 +- ReactNativeClient/package.json | 2 +- ReactNativeClient/root.js | 163 +- Tools/build-release-stats.js | 8 +- Tools/build-translation.js | 20 +- Tools/build-website.js | 12 +- Tools/build-welcome.js | 10 +- Tools/buildReactNativeInjectedJs.js | 4 +- Tools/copycss.js | 11 +- Tools/git-changelog.js | 61 +- Tools/package.json | 2 +- Tools/release-android.js | 14 +- Tools/release-cli.js | 11 +- Tools/release-clipper.js | 6 +- Tools/release-electron.js | 5 +- Tools/tool-utils.js | 42 +- Tools/update-homebrew.js | 6 +- Tools/update-readme-contributors.js | 10 +- Tools/update-readme-download.js | 9 +- package-lock.json | 1154 ++++++- package.json | 16 +- 208 files changed, 7715 insertions(+), 5019 deletions(-) delete mode 100644 .prettierignore delete mode 100644 .prettierrc.json delete mode 100644 ReactNativeClient/App.js delete mode 100644 ReactNativeClient/lib/components/screens/tag.js diff --git a/.eslintignore b/.eslintignore index 9f7712124..fcba99d7a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,40 @@ *.min.js +.git/ +.github/ +_mydocs/ +_releases/ +Assets/ +CliClient/build +CliClient/locales +CliClient/node_modules +CliClient/tests-build +CliClient/tests/enex_to_md +CliClient/tests/html_to_md +CliClient/tests/logs +CliClient/tests/support +CliClient/tests/sync +CliClient/tests/tmp +Clipper/joplin-webclipper/content_scripts/JSDOMParser.js +Clipper/joplin-webclipper/content_scripts/Readability-readerable.js +Clipper/joplin-webclipper/content_scripts/Readability.js +Clipper/joplin-webclipper/dist +Clipper/joplin-webclipper/icons +Clipper/joplin-webclipper/popup/build +Clipper/joplin-webclipper/popup/node_modules +docs/ +ElectronClient/app/dist +ElectronClient/app/lib +ElectronClient/app/lib/vendor/sjcl-rn.js +ElectronClient/app/lib/vendor/sjcl.js +ElectronClient/app/locales +ElectronClient/app/node_modules highlight.pack.js -ReactNativeClient/lib/vendor/ \ No newline at end of file +node_modules/ +ReactNativeClient/android +ReactNativeClient/ios +ReactNativeClient/lib/vendor/ +ReactNativeClient/locales +ReactNativeClient/node_modules +readme/ +Tools/node_modules +Tools/PortableAppsLauncher \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 352709e43..30c87e4c8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,30 +4,50 @@ module.exports = { 'es6': true, 'node': true, }, - 'extends': ['eslint:recommended', 'prettier'], + 'extends': ['eslint:recommended'], 'globals': { 'Atomics': 'readonly', - 'SharedArrayBuffer': 'readonly' + 'SharedArrayBuffer': 'readonly', + + // Jasmine variables + 'expect': 'readonly', + 'describe': 'readonly', + 'it': 'readonly', + 'beforeEach': 'readonly', + 'jasmine': 'readonly', + + // React Native variables + '__DEV__': 'readonly', + + // Clipper variables + 'browserSupportsPromises_': true, + 'chrome': 'readonly', + 'browser': 'readonly', }, 'parserOptions': { 'ecmaVersion': 2018, "ecmaFeatures": { "jsx": true, }, + "sourceType": "module", }, 'rules': { "react/jsx-uses-react": "error", "react/jsx-uses-vars": "error", - "no-unused-vars": ["error", { "argsIgnorePattern": "event|reject|resolve|prevState|snapshot|prevProps" }], + // Ignore all unused function arguments, because in some + // case they are kept to indicate the function signature. + "no-unused-vars": ["error", { "argsIgnorePattern": ".*" }], "no-constant-condition": 0, "no-prototype-builtins": 0, - "prettier/prettier": "error", - // Uncomment this to automatically remove unused requires: - // "autofix/no-unused-vars": "error", + "space-in-parens": ["error", "never"], + "semi": ["error", "always"], + "eol-last": ["error", "always"], + "quotes": ["error", "single"], + "indent": ["error", "tab"], + "comma-dangle": ["error", "always-multiline"], + "no-trailing-spaces": "error", }, "plugins": [ "react", - "prettier", - "autofix", ], }; \ No newline at end of file diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 9f7712124..000000000 --- a/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -*.min.js -highlight.pack.js -ReactNativeClient/lib/vendor/ \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index a3c93e608..000000000 --- a/.prettierrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "useTabs": true, - "singleQuote": true, - "printWidth": 1000, - "semi": true, - "trailingComma": "es5", - "endOfLine": "auto" -} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e96a7a0c3..5b1a8a657 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,10 +40,7 @@ Building the apps is relatively easy - please [see the build instructions](https ## Coding style -There are only two rules, but not following them means the pull request will not be accepted (it can be accepted once the issues are fixed): - -- **Please use tabs, NOT spaces.** -- **Please do not add or remove optional characters, such as spaces or colons.** Please setup your editor so that it only changes what you are working on and is not making automated changes elsewhere. The reason for this is that small white space changes make diff hard to read and can cause needless conflicts. +Coding style is enforced by a pre-commit hook that runs eslint. This hook is installed whenever running `npm install` on any of the application directory. If for some reason the pre-commit hook didn't get installed, you can manually install it by running `npm install` at the root of the repository. ## Unit tests diff --git a/CliClient/app/ResourceServer.js b/CliClient/app/ResourceServer.js index c7425d039..c17d6e6c0 100644 --- a/CliClient/app/ResourceServer.js +++ b/CliClient/app/ResourceServer.js @@ -1,14 +1,11 @@ -const { _ } = require('lib/locale.js'); const { Logger } = require('lib/logger.js'); -const Resource = require('lib/models/Resource.js'); const { netUtils } = require('lib/net-utils.js'); -const http = require("http"); -const urlParser = require("url"); +const http = require('http'); +const urlParser = require('url'); const enableServerDestroy = require('server-destroy'); class ResourceServer { - constructor() { this.server_ = null; this.logger_ = new Logger(); @@ -40,7 +37,7 @@ class ResourceServer { async start() { this.port_ = await netUtils.findAvailablePort([9167, 9267, 8167, 8267]); - if (!this.port_) { + if (!this.port_) { this.logger().error('Could not find available port to start resource server. Please report the error at https://github.com/laurent22/joplin'); return; } @@ -48,11 +45,10 @@ class ResourceServer { this.server_ = http.createServer(); this.server_.on('request', async (request, response) => { - - const writeResponse = (message) => { + const writeResponse = message => { response.write(message); response.end(); - } + }; const url = urlParser.parse(request.url, true); let resourceId = url.pathname.split('/'); @@ -69,6 +65,7 @@ class ResourceServer { if (!done) throw new Error('Unhandled resource: ' + resourceId); } catch (error) { response.setHeader('Content-Type', 'text/plain'); + // eslint-disable-next-line require-atomic-updates response.statusCode = 400; response.write(error.message); } @@ -76,7 +73,7 @@ class ResourceServer { response.end(); }); - this.server_.on('error', (error) => { + this.server_.on('error', error => { this.logger().error('Resource server:', error); }); @@ -91,7 +88,6 @@ class ResourceServer { if (this.server_) this.server_.destroy(); this.server_ = null; } - } -module.exports = ResourceServer; \ No newline at end of file +module.exports = ResourceServer; diff --git a/CliClient/app/app-gui.js b/CliClient/app/app-gui.js index f11b43eaf..d1d245359 100644 --- a/CliClient/app/app-gui.js +++ b/CliClient/app/app-gui.js @@ -5,13 +5,12 @@ const Tag = require('lib/models/Tag.js'); const BaseModel = require('lib/BaseModel.js'); const Note = require('lib/models/Note.js'); const Resource = require('lib/models/Resource.js'); -const { cliUtils } = require('./cli-utils.js'); const { reducer, defaultState } = require('lib/reducer.js'); const { splitCommandString } = require('lib/string-utils.js'); const { reg } = require('lib/registry.js'); const { _ } = require('lib/locale.js'); const Entities = require('html-entities').AllHtmlEntities; -const htmlentities = (new Entities()).encode; +const htmlentities = new Entities().encode; const chalk = require('chalk'); const tk = require('terminal-kit'); @@ -20,12 +19,10 @@ const Renderer = require('tkwidgets/framework/Renderer.js'); const DecryptionWorker = require('lib/services/DecryptionWorker'); const BaseWidget = require('tkwidgets/BaseWidget.js'); -const ListWidget = require('tkwidgets/ListWidget.js'); const TextWidget = require('tkwidgets/TextWidget.js'); const HLayoutWidget = require('tkwidgets/HLayoutWidget.js'); const VLayoutWidget = require('tkwidgets/VLayoutWidget.js'); const ReduxRootWidget = require('tkwidgets/ReduxRootWidget.js'); -const RootWidget = require('tkwidgets/RootWidget.js'); const WindowWidget = require('tkwidgets/WindowWidget.js'); const NoteWidget = require('./gui/NoteWidget.js'); @@ -37,7 +34,6 @@ const StatusBarWidget = require('./gui/StatusBarWidget.js'); const ConsoleWidget = require('./gui/ConsoleWidget.js'); class AppGui { - constructor(app, store, keymap) { try { this.app_ = app; @@ -50,12 +46,12 @@ class AppGui { // Some keys are directly handled by the tkwidget framework // so they need to be remapped in a different way. this.tkWidgetKeys_ = { - 'focus_next': 'TAB', - 'focus_previous': 'SHIFT_TAB', - 'move_up': 'UP', - 'move_down': 'DOWN', - 'page_down': 'PAGE_DOWN', - 'page_up': 'PAGE_UP', + focus_next: 'TAB', + focus_previous: 'SHIFT_TAB', + move_up: 'UP', + move_down: 'DOWN', + page_down: 'PAGE_DOWN', + page_up: 'PAGE_UP', }; this.renderer_ = null; @@ -64,7 +60,7 @@ class AppGui { this.renderer_ = new Renderer(this.term(), this.rootWidget_); - this.app_.on('modelAction', async (event) => { + this.app_.on('modelAction', async event => { await this.handleModelAction(event.action); }); @@ -134,7 +130,7 @@ class AppGui { }; folderList.name = 'folderList'; folderList.vStretch = true; - folderList.on('currentItemChange', async (event) => { + folderList.on('currentItemChange', async event => { const item = folderList.currentItem; if (item === '-') { @@ -169,7 +165,7 @@ class AppGui { }); } }); - this.rootWidget_.connect(folderList, (state) => { + this.rootWidget_.connect(folderList, state => { return { selectedFolderId: state.selectedFolderId, selectedTagId: state.selectedTagId, @@ -196,7 +192,7 @@ class AppGui { id: note ? note.id : null, }); }); - this.rootWidget_.connect(noteList, (state) => { + this.rootWidget_.connect(noteList, state => { return { selectedNoteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null, items: state.notes, @@ -210,7 +206,7 @@ class AppGui { borderBottomWidth: 1, borderLeftWidth: 1, }; - this.rootWidget_.connect(noteText, (state) => { + this.rootWidget_.connect(noteText, state => { return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null, notes: state.notes, @@ -225,7 +221,7 @@ class AppGui { borderLeftWidth: 1, borderRightWidth: 1, }; - this.rootWidget_.connect(noteMetadata, (state) => { + this.rootWidget_.connect(noteMetadata, state => { return { noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null }; }); noteMetadata.hide(); @@ -292,7 +288,7 @@ class AppGui { if (!cmd) return; const isConfigPassword = cmd.indexOf('config ') >= 0 && cmd.indexOf('password') >= 0; if (isConfigPassword) return; - this.stdout(chalk.cyan.bold('> ' + cmd)); + this.stdout(chalk.cyan.bold('> ' + cmd)); } setupKeymap(keymap) { @@ -408,7 +404,7 @@ class AppGui { activeListItem() { const widget = this.widget('mainWindow').focusedWidget; if (!widget) return null; - + if (widget.name == 'noteList' || widget.name == 'folderList') { return widget.currentItem; } @@ -430,18 +426,14 @@ class AppGui { } async processFunctionCommand(cmd) { - if (cmd === 'activate') { - const w = this.widget('mainWindow').focusedWidget; if (w.name === 'folderList') { this.widget('noteList').focus(); } else if (w.name === 'noteList' || w.name === 'noteText') { this.processPromptCommand('edit $n'); } - } else if (cmd === 'delete') { - if (this.widget('folderList').hasFocus) { const item = this.widget('folderList').selectedJoplinItem; @@ -462,9 +454,7 @@ class AppGui { } else { this.stdout(_('Please select the note or notebook to be deleted first.')); } - } else if (cmd === 'toggle_console') { - if (!this.consoleIsShown()) { this.showConsole(); this.minimizeConsole(); @@ -475,22 +465,15 @@ class AppGui { this.maximizeConsole(); } } - } else if (cmd === 'toggle_metadata') { - this.toggleNoteMetadata(); - } else if (cmd === 'enter_command_line_mode') { - const cmd = await this.widget('statusBar').prompt(); if (!cmd) return; this.addCommandToConsole(cmd); - await this.processPromptCommand(cmd); - + await this.processPromptCommand(cmd); } else { - throw new Error('Unknown command: ' + cmd); - } } @@ -501,7 +484,7 @@ class AppGui { // this.logger().debug('Got command: ' + cmd); - try { + try { let note = this.widget('noteList').currentItem; let folder = this.widget('folderList').currentItem; let args = splitCommandString(cmd); @@ -511,7 +494,7 @@ class AppGui { args[i] = note ? note.id : ''; } else if (args[i] == '$b') { args[i] = folder ? folder.id : ''; - } else if (args[i] == '$c') { + } else if (args[i] == '$c') { const item = this.activeListItem(); args[i] = item ? item.id : ''; } @@ -523,7 +506,7 @@ class AppGui { } this.widget('console').scrollBottom(); - + // Invalidate so that the screen is redrawn in case inputting a command has moved // the GUI up (in particular due to autocompletion), it's moved back to the right position. this.widget('root').invalidate(); @@ -603,7 +586,7 @@ class AppGui { async setupResourceServer() { const linkStyle = chalk.blue.underline; const noteTextWidget = this.widget('noteText'); - const resourceIdRegex = /^:\/[a-f0-9]+$/i + const resourceIdRegex = /^:\/[a-f0-9]+$/i; const noteLinks = {}; const hasProtocol = function(s, protocols) { @@ -613,7 +596,7 @@ class AppGui { if (s.indexOf(protocols[i] + '://') === 0) return true; } return false; - } + }; // By default, before the server is started, only the regular // URLs appear in blue. @@ -637,7 +620,7 @@ class AppGui { const link = noteLinks[path]; if (link.type === 'url') { - response.writeHead(302, { 'Location': link.url }); + response.writeHead(302, { Location: link.url }); return true; } @@ -650,11 +633,13 @@ class AppGui { if (item.mime) response.setHeader('Content-Type', item.mime); response.write(await Resource.content(item)); } else if (item.type_ === BaseModel.TYPE_NOTE) { - const html = [` + const html = [ + ` - `]; + `, + ]; html.push('
' + htmlentities(item.title) + '\n\n' + htmlentities(item.body) + '
'); html.push(''); response.write(html.join('')); @@ -679,7 +664,7 @@ class AppGui { noteLinks[index] = { type: 'item', id: url.substr(2), - }; + }; } else if (hasProtocol(url, ['http', 'https', 'file', 'ftp'])) { noteLinks[index] = { type: 'url', @@ -711,7 +696,6 @@ class AppGui { term.grabInput(); term.on('key', async (name, matches, data) => { - // ------------------------------------------------------------------------- // Handle special shortcuts // ------------------------------------------------------------------------- @@ -729,13 +713,13 @@ class AppGui { return; } - if (name === 'CTRL_C' ) { + if (name === 'CTRL_C') { const cmd = this.app().currentCommand(); if (!cmd || !cmd.cancellable() || this.commandCancelCalled_) { this.stdout(_('Press Ctrl+D or type "exit" to exit the application')); } else { this.commandCancelCalled_ = true; - await cmd.cancel() + await cmd.cancel(); this.commandCancelCalled_ = false; } return; @@ -744,8 +728,8 @@ class AppGui { // ------------------------------------------------------------------------- // Build up current shortcut // ------------------------------------------------------------------------- - - const now = (new Date()).getTime(); + + const now = new Date().getTime(); if (now - this.lastShortcutKeyTime_ > 800 || this.isSpecialKey(name)) { this.currentShortcutKeys_ = [name]; @@ -813,7 +797,6 @@ class AppGui { process.exit(1); }); } - } AppGui.INPUT_MODE_NORMAL = 1; diff --git a/CliClient/app/app.js b/CliClient/app/app.js index bc5134638..8883029b3 100644 --- a/CliClient/app/app.js +++ b/CliClient/app/app.js @@ -1,10 +1,5 @@ const { BaseApplication } = require('lib/BaseApplication'); -const { createStore, applyMiddleware } = require('redux'); -const { reducer, defaultState } = require('lib/reducer.js'); -const { JoplinDatabase } = require('lib/joplin-database.js'); -const { Database } = require('lib/database.js'); const { FoldersScreenUtils } = require('lib/folders-screen-utils.js'); -const { DatabaseDriverNode } = require('lib/database-driver-node.js'); const ResourceService = require('lib/services/ResourceService'); const BaseModel = require('lib/BaseModel.js'); const Folder = require('lib/models/Folder.js'); @@ -12,21 +7,15 @@ const BaseItem = require('lib/models/BaseItem.js'); const Note = require('lib/models/Note.js'); const Tag = require('lib/models/Tag.js'); const Setting = require('lib/models/Setting.js'); -const { Logger } = require('lib/logger.js'); -const { sprintf } = require('sprintf-js'); const { reg } = require('lib/registry.js'); const { fileExtension } = require('lib/path-utils.js'); -const { shim } = require('lib/shim.js'); -const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js'); -const os = require('os'); +const { _ } = require('lib/locale.js'); const fs = require('fs-extra'); const { cliUtils } = require('./cli-utils.js'); const Cache = require('lib/Cache'); -const WelcomeUtils = require('lib/WelcomeUtils'); const RevisionService = require('lib/services/RevisionService'); class Application extends BaseApplication { - constructor() { super(); @@ -75,7 +64,7 @@ class Application extends BaseApplication { // const response = await cliUtils.promptMcq(msg, answers); // if (!response) return null; - return output[response - 1]; + // return output[response - 1]; } else { return output.length ? output[0] : null; } @@ -97,10 +86,12 @@ class Application extends BaseApplication { const parent = options.parent ? options.parent : app().currentFolder(); const ItemClass = BaseItem.itemClass(type); - if (type == BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) { // Handle it as pattern + if (type == BaseModel.TYPE_NOTE && pattern.indexOf('*') >= 0) { + // Handle it as pattern if (!parent) throw new Error(_('No notebook selected.')); return await Note.previews(parent.id, { titlePattern: pattern }); - } else { // Single item + } else { + // Single item let item = null; if (type == BaseModel.TYPE_NOTE) { if (!parent) throw new Error(_('No notebook has been specified.')); @@ -126,15 +117,15 @@ class Application extends BaseApplication { } setupCommand(cmd) { - cmd.setStdout((text) => { + cmd.setStdout(text => { return this.stdout(text); }); - cmd.setDispatcher((action) => { + cmd.setDispatcher(action => { if (this.store()) { return this.store().dispatch(action); } else { - return (action) => {}; + return action => {}; } }); @@ -185,9 +176,9 @@ class Application extends BaseApplication { commands(uiType = null) { if (!this.allCommandsLoaded_) { - fs.readdirSync(__dirname).forEach((path) => { + fs.readdirSync(__dirname).forEach(path => { if (path.indexOf('command-') !== 0) return; - const ext = fileExtension(path) + const ext = fileExtension(path); if (ext != 'js') return; let CommandClass = require('./' + path); @@ -276,19 +267,27 @@ class Application extends BaseApplication { dummyGui() { return { - isDummy: () => { return true; }, - prompt: (initialText = '', promptString = '', options = null) => { return cliUtils.prompt(initialText, promptString, options); }, + isDummy: () => { + return true; + }, + prompt: (initialText = '', promptString = '', options = null) => { + return cliUtils.prompt(initialText, promptString, options); + }, showConsole: () => {}, maximizeConsole: () => {}, - stdout: (text) => { console.info(text); }, - fullScreen: (b=true) => {}, + stdout: text => { + console.info(text); + }, + fullScreen: (b = true) => {}, exit: () => {}, - showModalOverlay: (text) => {}, + showModalOverlay: text => {}, hideModalOverlay: () => {}, - stdoutMaxWidth: () => { return 100; }, + stdoutMaxWidth: () => { + return 100; + }, forceRender: () => {}, termSaveState: () => {}, - termRestoreState: (state) => {}, + termRestoreState: state => {}, }; } @@ -300,7 +299,7 @@ class Application extends BaseApplication { let outException = null; try { - if (this.gui().isDummy() && !this.activeCommand_.supportsUi('cli')) throw new Error(_('The command "%s" is only available in GUI mode', this.activeCommand_.name())); + if (this.gui().isDummy() && !this.activeCommand_.supportsUi('cli')) throw new Error(_('The command "%s" is only available in GUI mode', this.activeCommand_.name())); const cmdArgs = cliUtils.makeCommandArgs(this.activeCommand_, argv); await this.activeCommand_.action(cmdArgs); } catch (error) { @@ -316,24 +315,24 @@ class Application extends BaseApplication { async loadKeymaps() { const defaultKeyMap = [ - { "keys": [":"], "type": "function", "command": "enter_command_line_mode" }, - { "keys": ["TAB"], "type": "function", "command": "focus_next" }, - { "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" }, - { "keys": ["UP"], "type": "function", "command": "move_up" }, - { "keys": ["DOWN"], "type": "function", "command": "move_down" }, - { "keys": ["PAGE_UP"], "type": "function", "command": "page_up" }, - { "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" }, - { "keys": ["ENTER"], "type": "function", "command": "activate" }, - { "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" }, - { "keys": [" "], "command": "todo toggle $n" }, - { "keys": ["tc"], "type": "function", "command": "toggle_console" }, - { "keys": ["tm"], "type": "function", "command": "toggle_metadata" }, - { "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 }, - { "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 }, - { "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 }, - { "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 }, - { "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 }, - { "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 } + { keys: [':'], type: 'function', command: 'enter_command_line_mode' }, + { keys: ['TAB'], type: 'function', command: 'focus_next' }, + { keys: ['SHIFT_TAB'], type: 'function', command: 'focus_previous' }, + { keys: ['UP'], type: 'function', command: 'move_up' }, + { keys: ['DOWN'], type: 'function', command: 'move_down' }, + { keys: ['PAGE_UP'], type: 'function', command: 'page_up' }, + { keys: ['PAGE_DOWN'], type: 'function', command: 'page_down' }, + { keys: ['ENTER'], type: 'function', command: 'activate' }, + { keys: ['DELETE', 'BACKSPACE'], type: 'function', command: 'delete' }, + { keys: [' '], command: 'todo toggle $n' }, + { keys: ['tc'], type: 'function', command: 'toggle_console' }, + { keys: ['tm'], type: 'function', command: 'toggle_metadata' }, + { keys: ['/'], type: 'prompt', command: 'search ""', cursorPosition: -2 }, + { keys: ['mn'], type: 'prompt', command: 'mknote ""', cursorPosition: -2 }, + { keys: ['mt'], type: 'prompt', command: 'mktodo ""', cursorPosition: -2 }, + { keys: ['mb'], type: 'prompt', command: 'mkbook ""', cursorPosition: -2 }, + { keys: ['yn'], type: 'prompt', command: 'cp $n ""', cursorPosition: -2 }, + { keys: ['dn'], type: 'prompt', command: 'mv $n ""', cursorPosition: -2 }, ]; // Filter the keymap item by command so that items in keymap.json can override @@ -341,7 +340,7 @@ class Application extends BaseApplication { const itemsByCommand = {}; for (let i = 0; i < defaultKeyMap.length; i++) { - itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i] + itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i]; } const filePath = Setting.value('profileDir') + '/keymap.json'; @@ -374,7 +373,7 @@ class Application extends BaseApplication { async start(argv) { argv = await super.start(argv); - cliUtils.setStdout((object) => { + cliUtils.setStdout(object => { return this.stdout(object); }); @@ -401,7 +400,8 @@ class Application extends BaseApplication { // Need to call exit() explicitely, otherwise Node wait for any timeout to complete // https://stackoverflow.com/questions/18050095 process.exit(0); - } else { // Otherwise open the GUI + } else { + // Otherwise open the GUI this.initRedux(); const keymap = await this.loadKeymaps(); @@ -421,7 +421,7 @@ class Application extends BaseApplication { const tags = await Tag.allWithNotes(); ResourceService.runInBackground(); - + RevisionService.instance().runInBackground(); this.dispatch({ @@ -435,7 +435,6 @@ class Application extends BaseApplication { }); } } - } let application_ = null; @@ -446,4 +445,4 @@ function app() { return application_; } -module.exports = { app }; \ No newline at end of file +module.exports = { app }; diff --git a/CliClient/app/autocompletion.js b/CliClient/app/autocompletion.js index 20464127e..99edb885b 100644 --- a/CliClient/app/autocompletion.js +++ b/CliClient/app/autocompletion.js @@ -14,11 +14,11 @@ async function handleAutocompletionPromise(line) { //should look for commmands it could be if (words.length == 1) { if (names.indexOf(words[0]) === -1) { - let x = names.filter((n) => n.indexOf(words[0]) === 0); + let x = names.filter(n => n.indexOf(words[0]) === 0); if (x.length === 1) { return x[0] + ' '; } - return x.length > 0 ? x.map((a) => a + ' ') : line; + return x.length > 0 ? x.map(a => a + ' ') : line; } else { return line; } @@ -34,9 +34,9 @@ async function handleAutocompletionPromise(line) { let next = words.length > 1 ? words[words.length - 1] : ''; let l = []; if (next[0] === '-') { - for (let i = 0; itoCommandLine(a)); + let ret = l.map(a => toCommandLine(a)); ret.prefix = toCommandLine(words.slice(0, -1)) + ' '; return ret; } //Complete an argument //Determine the number of positional arguments by counting the number of //words that don't start with a - less one for the command name - const positionalArgs = words.filter((a)=>a.indexOf('-') !== 0).length - 1; + const positionalArgs = words.filter(a => a.indexOf('-') !== 0).length - 1; let cmdUsage = yargParser(metadata.usage)['_']; cmdUsage.splice(0, 1); if (cmdUsage.length >= positionalArgs) { - let argName = cmdUsage[positionalArgs - 1]; argName = cliUtils.parseCommandArg(argName).name; @@ -76,23 +75,23 @@ async function handleAutocompletionPromise(line) { if (argName == 'note' || argName == 'note-pattern') { const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : []; - l.push(...notes.map((n) => n.title)); + l.push(...notes.map(n => n.title)); } if (argName == 'notebook') { const folders = await Folder.search({ titlePattern: next + '*' }); - l.push(...folders.map((n) => n.title)); + l.push(...folders.map(n => n.title)); } if (argName == 'item') { const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : []; const folders = await Folder.search({ titlePattern: next + '*' }); - l.push(...notes.map((n) => n.title), folders.map((n) => n.title)); + l.push(...notes.map(n => n.title), folders.map(n => n.title)); } if (argName == 'tag') { let tags = await Tag.search({ titlePattern: next + '*' }); - l.push(...tags.map((n) => n.title)); + l.push(...tags.map(n => n.title)); } if (argName == 'file') { @@ -113,12 +112,11 @@ async function handleAutocompletionPromise(line) { if (l.length === 1) { return toCommandLine([...words.slice(0, -1), l[0]]); } else if (l.length > 1) { - let ret = l.map(a=>toCommandLine(a)); + let ret = l.map(a => toCommandLine(a)); ret.prefix = toCommandLine(words.slice(0, -1)) + ' '; return ret; } return line; - } function handleAutocompletion(str, callback) { handleAutocompletionPromise(str).then(function(res) { @@ -127,19 +125,21 @@ function handleAutocompletion(str, callback) { } function toCommandLine(args) { if (Array.isArray(args)) { - return args.map(function(a) { - if(a.indexOf('"') !== -1 || a.indexOf(' ') !== -1) { - return "'" + a + "'"; - } else if (a.indexOf("'") !== -1) { - return '"' + a + '"'; - } else { - return a; - } - }).join(' '); + return args + .map(function(a) { + if (a.indexOf('"') !== -1 || a.indexOf(' ') !== -1) { + return '\'' + a + '\''; + } else if (a.indexOf('\'') !== -1) { + return '"' + a + '"'; + } else { + return a; + } + }) + .join(' '); } else { - if(args.indexOf('"') !== -1 || args.indexOf(' ') !== -1) { - return "'" + args + "' "; - } else if (args.indexOf("'") !== -1) { + if (args.indexOf('"') !== -1 || args.indexOf(' ') !== -1) { + return '\'' + args + '\' '; + } else if (args.indexOf('\'') !== -1) { return '"' + args + '" '; } else { return args + ' '; @@ -151,9 +151,9 @@ function getArguments(line) { let inDoubleQuotes = false; let currentWord = ''; let parsed = []; - for(let i = 0; i { + fs.readdirSync(__dirname).forEach(path => { if (path.indexOf('command-') !== 0) return; - const ext = fileExtension(path) + const ext = fileExtension(path); if (ext != 'js') return; let CommandClass = require('./' + path); @@ -87,14 +87,14 @@ function getHeader() { let description = []; description.push('Joplin is a note taking and to-do application, which can handle a large number of notes organised into notebooks.'); description.push('The notes are searchable, can be copied, tagged and modified with your own text editor.'); - description.push("\n\n"); + description.push('\n\n'); description.push('The notes can be synchronised with various target including the file system (for example with a network directory) or with Microsoft OneDrive.'); - description.push("\n\n"); + description.push('\n\n'); description.push('Notes exported from Evenotes via .enex files can be imported into Joplin, including the formatted content, resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.).'); output.push(wrap(description.join(''), INDENT)); - return output.join("\n"); + return output.join('\n'); } function getFooter() { @@ -113,7 +113,7 @@ function getFooter() { const licenseText = fs.readFileSync(filePath, 'utf8'); output.push(wrap(licenseText, INDENT)); - return output.join("\n"); + return output.join('\n'); } async function main() { @@ -128,12 +128,12 @@ async function main() { } const headerText = getHeader(); - const commandsText = commandBlocks.join("\n\n"); + const commandsText = commandBlocks.join('\n\n'); const footerText = getFooter(); - console.info(headerText + "\n\n" + 'USAGE' + "\n\n" + commandsText + "\n\n" + footerText); + console.info(headerText + '\n\n' + 'USAGE' + '\n\n' + commandsText + '\n\n' + footerText); } -main().catch((error) => { +main().catch(error => { console.error(error); -}); \ No newline at end of file +}); diff --git a/CliClient/app/cli-integration-tests.js b/CliClient/app/cli-integration-tests.js index b38510af0..814daff00 100644 --- a/CliClient/app/cli-integration-tests.js +++ b/CliClient/app/cli-integration-tests.js @@ -1,4 +1,4 @@ -"use strict" +'use strict'; const fs = require('fs-extra'); const { Logger } = require('lib/logger.js'); @@ -10,7 +10,7 @@ const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); const Setting = require('lib/models/Setting.js'); const { sprintf } = require('sprintf-js'); -const exec = require('child_process').exec +const exec = require('child_process').exec; process.on('unhandledRejection', (reason, p) => { console.error('Unhandled promise rejection', p, 'reason:', reason); @@ -32,8 +32,8 @@ db.setLogger(dbLogger); function createClient(id) { return { - 'id': id, - 'profileDir': baseDir + '/client' + id, + id: id, + profileDir: baseDir + '/client' + id, }; } @@ -72,14 +72,7 @@ function assertEquals(expected, real) { } async function clearDatabase() { - await db.transactionExecBatch([ - 'DELETE FROM folders', - 'DELETE FROM notes', - 'DELETE FROM tags', - 'DELETE FROM note_tags', - 'DELETE FROM resources', - 'DELETE FROM deleted_items', - ]); + await db.transactionExecBatch(['DELETE FROM folders', 'DELETE FROM notes', 'DELETE FROM tags', 'DELETE FROM note_tags', 'DELETE FROM resources', 'DELETE FROM deleted_items']); } const testUnits = {}; @@ -101,7 +94,7 @@ testUnits.testFolders = async () => { folders = await Folder.all(); assertEquals(0, folders.length); -} +}; testUnits.testNotes = async () => { await execCommand(client, 'mkbook nb1'); @@ -121,16 +114,16 @@ testUnits.testNotes = async () => { notes = await Note.all(); assertEquals(2, notes.length); - await execCommand(client, "rm -f 'blabla*'"); + await execCommand(client, 'rm -f \'blabla*\''); notes = await Note.all(); assertEquals(2, notes.length); - await execCommand(client, "rm -f 'n*'"); + await execCommand(client, 'rm -f \'n*\''); notes = await Note.all(); assertEquals(0, notes.length); -} +}; testUnits.testCat = async () => { await execCommand(client, 'mkbook nb1'); @@ -145,7 +138,7 @@ testUnits.testCat = async () => { r = await execCommand(client, 'cat -v mynote'); assertTrue(r.indexOf(note.id) >= 0); -} +}; testUnits.testConfig = async () => { await execCommand(client, 'config editor vim'); @@ -159,7 +152,7 @@ testUnits.testConfig = async () => { let r = await execCommand(client, 'config'); assertTrue(r.indexOf('editor') >= 0); assertTrue(r.indexOf('subl') >= 0); -} +}; testUnits.testCp = async () => { await execCommand(client, 'mkbook nb2'); @@ -180,7 +173,7 @@ testUnits.testCp = async () => { notes = await Note.previews(f2.id); assertEquals(1, notes.length); assertEquals(notesF1[0].title, notes[0].title); -} +}; testUnits.testLs = async () => { await execCommand(client, 'mkbook nb1'); @@ -190,7 +183,7 @@ testUnits.testLs = async () => { assertTrue(r.indexOf('note1') >= 0); assertTrue(r.indexOf('note2') >= 0); -} +}; testUnits.testMv = async () => { await execCommand(client, 'mkbook nb2'); @@ -210,14 +203,14 @@ testUnits.testMv = async () => { await execCommand(client, 'mknote note2'); await execCommand(client, 'mknote note3'); await execCommand(client, 'mknote blabla'); - await execCommand(client, "mv 'note*' nb2"); + await execCommand(client, 'mv \'note*\' nb2'); notes1 = await Note.previews(f1.id); notes2 = await Note.previews(f2.id); assertEquals(1, notes1.length); assertEquals(4, notes2.length); -} +}; async function main(argv) { await fs.remove(baseDir); @@ -243,7 +236,7 @@ async function main(argv) { } } -main(process.argv).catch((error) => { +main(process.argv).catch(error => { console.info(''); logger.error(error); -}); \ No newline at end of file +}); diff --git a/CliClient/app/cli-utils.js b/CliClient/app/cli-utils.js index 895549b72..4fd511366 100644 --- a/CliClient/app/cli-utils.js +++ b/CliClient/app/cli-utils.js @@ -16,7 +16,7 @@ cliUtils.printArray = function(logFunction, rows, headers = null) { for (let i = 0; i < rows.length; i++) { let row = rows[i]; - + for (let j = 0; j < row.length; j++) { let item = row[j]; let width = item ? item.toString().length : 0; @@ -26,7 +26,6 @@ cliUtils.printArray = function(logFunction, rows, headers = null) { } } - let lines = []; for (let row = 0; row < rows.length; row++) { let line = []; for (let col = 0; col < colWidths.length; col++) { @@ -37,7 +36,7 @@ cliUtils.printArray = function(logFunction, rows, headers = null) { } logFunction(line.join(' ')); } -} +}; cliUtils.parseFlags = function(flags) { let output = {}; @@ -56,7 +55,7 @@ cliUtils.parseFlags = function(flags) { } } return output; -} +}; cliUtils.parseCommandArg = function(arg) { if (arg.length <= 2) throw new Error('Invalid command arg: ' + arg); @@ -72,7 +71,7 @@ cliUtils.parseCommandArg = function(arg) { } else { throw new Error('Invalid command arg: ' + arg); } -} +}; cliUtils.makeCommandArgs = function(cmd, argv) { let cmdUsage = cmd.usage(); @@ -85,7 +84,6 @@ cliUtils.makeCommandArgs = function(cmd, argv) { for (let i = 0; i < options.length; i++) { if (options[i].length != 2) throw new Error('Invalid options: ' + options[i]); let flags = options[i][0]; - let text = options[i][1]; flags = cliUtils.parseFlags(flags); @@ -125,27 +123,27 @@ cliUtils.makeCommandArgs = function(cmd, argv) { output.options = argOptions; return output; -} +}; cliUtils.promptMcq = function(message, answers) { const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, }); - message += "\n\n"; + message += '\n\n'; for (let n in answers) { if (!answers.hasOwnProperty(n)) continue; - message += _('%s: %s', n, answers[n]) + "\n"; + message += _('%s: %s', n, answers[n]) + '\n'; } - message += "\n"; + message += '\n'; message += _('Your choice: '); return new Promise((resolve, reject) => { - rl.question(message, (answer) => { + rl.question(message, answer => { rl.close(); if (!(answer in answers)) { @@ -156,7 +154,7 @@ cliUtils.promptMcq = function(message, answers) { resolve(answer); }); }); -} +}; cliUtils.promptConfirm = function(message, answers = null) { if (!answers) answers = [_('Y'), _('n')]; @@ -164,19 +162,19 @@ cliUtils.promptConfirm = function(message, answers = null) { const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, }); message += ' (' + answers.join('/') + ')'; return new Promise((resolve, reject) => { - rl.question(message + ' ', (answer) => { + rl.question(message + ' ', answer => { const ok = !answer || answer.toLowerCase() == answers[0].toLowerCase(); rl.close(); resolve(ok); }); }); -} +}; // Note: initialText is there to have the same signature as statusBar.prompt() so that // it can be a drop-in replacement, however initialText is not used (and cannot be @@ -189,10 +187,9 @@ cliUtils.prompt = function(initialText = '', promptString = ':', options = null) const mutableStdout = new Writable({ write: function(chunk, encoding, callback) { - if (!this.muted) - process.stdout.write(chunk, encoding); + if (!this.muted) process.stdout.write(chunk, encoding); callback(); - } + }, }); const rl = readline.createInterface({ @@ -204,15 +201,15 @@ cliUtils.prompt = function(initialText = '', promptString = ':', options = null) return new Promise((resolve, reject) => { mutableStdout.muted = false; - rl.question(promptString, (answer) => { + rl.question(promptString, answer => { rl.close(); - if (!!options.secure) this.stdout_(''); + if (options.secure) this.stdout_(''); resolve(answer); }); mutableStdout.muted = !!options.secure; }); -} +}; let redrawStarted_ = false; let redrawLastLog_ = null; @@ -220,7 +217,7 @@ let redrawLastUpdateTime_ = 0; cliUtils.setStdout = function(v) { this.stdout_ = v; -} +}; cliUtils.redraw = function(s) { const now = time.unixMs(); @@ -233,8 +230,8 @@ cliUtils.redraw = function(s) { redrawLastLog_ = s; } - redrawStarted_ = true; -} + redrawStarted_ = true; +}; cliUtils.redrawDone = function() { if (!redrawStarted_) return; @@ -245,6 +242,6 @@ cliUtils.redrawDone = function() { redrawLastLog_ = null; redrawStarted_ = false; -} +}; -module.exports = { cliUtils }; \ No newline at end of file +module.exports = { cliUtils }; diff --git a/CliClient/app/command-apidoc.js b/CliClient/app/command-apidoc.js index 7b4bd530f..d5348d06f 100644 --- a/CliClient/app/command-apidoc.js +++ b/CliClient/app/command-apidoc.js @@ -1,19 +1,12 @@ const { BaseCommand } = require('./base-command.js'); -const { _ } = require('lib/locale.js'); -const { cliUtils } = require('./cli-utils.js'); -const EncryptionService = require('lib/services/EncryptionService'); -const DecryptionWorker = require('lib/services/DecryptionWorker'); -const MasterKey = require('lib/models/MasterKey'); const BaseItem = require('lib/models/BaseItem'); const BaseModel = require('lib/BaseModel'); -const Setting = require('lib/models/Setting.js'); const { toTitleCase } = require('lib/string-utils.js'); const { reg } = require('lib/registry.js'); const markdownUtils = require('lib/markdownUtils'); const { Database } = require('lib/database.js'); class Command extends BaseCommand { - usage() { return 'apidoc'; } @@ -23,15 +16,19 @@ class Command extends BaseCommand { } createPropertiesTable(tableFields) { - const headers = [ - { name: 'name', label: 'Name' }, - { name: 'type', label: 'Type', filter: (value) => { - return Database.enumName('fieldType', value); - }}, - { name: 'description', label: 'Description' }, - ]; - - return markdownUtils.createMarkdownTable(headers, tableFields); + const headers = [ + { name: 'name', label: 'Name' }, + { + name: 'type', + label: 'Type', + filter: value => { + return Database.enumName('fieldType', value); + }, + }, + { name: 'description', label: 'Description' }, + ]; + + return markdownUtils.createMarkdownTable(headers, tableFields); } async action(args) { @@ -70,8 +67,8 @@ class Command extends BaseCommand { lines.push('}'); lines.push('```'); lines.push(''); - - lines.push('# Authorisation') + + lines.push('# Authorisation'); lines.push(''); lines.push('To prevent unauthorised applications from accessing the API, the calls must be authentified. To do so, you must provide a token as a query parameter for each API call. You can get this token from the Joplin desktop application, on the Web Clipper Options screen.'); lines.push(''); @@ -293,7 +290,6 @@ class Command extends BaseCommand { this.stdout(lines.join('\n')); } - } module.exports = Command; diff --git a/CliClient/app/command-attach.js b/CliClient/app/command-attach.js index 0b22ef4cd..7d92737bf 100644 --- a/CliClient/app/command-attach.js +++ b/CliClient/app/command-attach.js @@ -3,10 +3,8 @@ const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const BaseModel = require('lib/BaseModel.js'); const { shim } = require('lib/shim.js'); -const fs = require('fs-extra'); class Command extends BaseCommand { - usage() { return 'attach '; } @@ -26,7 +24,6 @@ class Command extends BaseCommand { await shim.attachFileToNote(note, localFilePath); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-cat.js b/CliClient/app/command-cat.js index 04237b681..fd64a5435 100644 --- a/CliClient/app/command-cat.js +++ b/CliClient/app/command-cat.js @@ -2,11 +2,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const BaseModel = require('lib/BaseModel.js'); -const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); class Command extends BaseCommand { - usage() { return 'cat '; } @@ -16,9 +14,7 @@ class Command extends BaseCommand { } options() { - return [ - ['-v, --verbose', _('Displays the complete information about note.')], - ]; + return [['-v, --verbose', _('Displays the complete information about note.')]]; } async action(args) { @@ -30,10 +26,13 @@ class Command extends BaseCommand { const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item); this.stdout(content); - app().gui().showConsole(); - app().gui().maximizeConsole(); + app() + .gui() + .showConsole(); + app() + .gui() + .maximizeConsole(); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-config.js b/CliClient/app/command-config.js index 51833dcec..e8a16e0d0 100644 --- a/CliClient/app/command-config.js +++ b/CliClient/app/command-config.js @@ -4,25 +4,22 @@ const { app } = require('./app.js'); const Setting = require('lib/models/Setting.js'); class Command extends BaseCommand { - usage() { return 'config [name] [value]'; } description() { - return _("Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration."); + return _('Gets or sets a config value. If [value] is not provided, it will show the value of [name]. If neither [name] nor [value] is provided, it will list the current configuration.'); } options() { - return [ - ['-v, --verbose', _('Also displays unset and hidden config variables.')], - ]; + return [['-v, --verbose', _('Also displays unset and hidden config variables.')]]; } async action(args) { const verbose = args.options.verbose; - const renderKeyValue = (name) => { + const renderKeyValue = name => { const md = Setting.settingMetadata(name); let value = Setting.value(name); if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value); @@ -33,7 +30,7 @@ class Command extends BaseCommand { } else { return _('%s = %s', name, value); } - } + }; if (!args.name && !args.value) { let keys = Setting.keys(!verbose, 'cli'); @@ -43,15 +40,23 @@ class Command extends BaseCommand { if (!verbose && !value) continue; this.stdout(renderKeyValue(keys[i])); } - app().gui().showConsole(); - app().gui().maximizeConsole(); + app() + .gui() + .showConsole(); + app() + .gui() + .maximizeConsole(); return; } if (args.name && !args.value) { this.stdout(renderKeyValue(args.name)); - app().gui().showConsole(); - app().gui().maximizeConsole(); + app() + .gui() + .showConsole(); + app() + .gui() + .maximizeConsole(); return; } @@ -64,7 +69,6 @@ class Command extends BaseCommand { await Setting.saveAll(); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-cp.js b/CliClient/app/command-cp.js index 05d2938e0..f5073d8ec 100644 --- a/CliClient/app/command-cp.js +++ b/CliClient/app/command-cp.js @@ -2,11 +2,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const BaseModel = require('lib/BaseModel.js'); -const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); class Command extends BaseCommand { - usage() { return 'cp [notebook]'; } @@ -33,7 +31,6 @@ class Command extends BaseCommand { Note.updateGeolocation(newNote.id); } } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-done.js b/CliClient/app/command-done.js index b05f3f55b..fa8c75746 100644 --- a/CliClient/app/command-done.js +++ b/CliClient/app/command-done.js @@ -2,12 +2,10 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const BaseModel = require('lib/BaseModel.js'); -const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); const { time } = require('lib/time-utils.js'); class Command extends BaseCommand { - usage() { return 'done '; } @@ -35,7 +33,6 @@ class Command extends BaseCommand { async action(args) { await Command.handleAction(this, args, true); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-dump.js b/CliClient/app/command-dump.js index 0672c2632..01e26124b 100644 --- a/CliClient/app/command-dump.js +++ b/CliClient/app/command-dump.js @@ -1,12 +1,9 @@ const { BaseCommand } = require('./base-command.js'); -const { app } = require('./app.js'); -const { _ } = require('lib/locale.js'); const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); const Tag = require('lib/models/Tag.js'); class Command extends BaseCommand { - usage() { return 'dump'; } @@ -35,10 +32,9 @@ class Command extends BaseCommand { } items = items.concat(tags); - + this.stdout(JSON.stringify(items)); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-e2ee.js b/CliClient/app/command-e2ee.js index c6e6a5650..e3d761c41 100644 --- a/CliClient/app/command-e2ee.js +++ b/CliClient/app/command-e2ee.js @@ -1,9 +1,7 @@ const { BaseCommand } = require('./base-command.js'); const { _ } = require('lib/locale.js'); -const { cliUtils } = require('./cli-utils.js'); const EncryptionService = require('lib/services/EncryptionService'); const DecryptionWorker = require('lib/services/DecryptionWorker'); -const MasterKey = require('lib/models/MasterKey'); const BaseItem = require('lib/models/BaseItem'); const Setting = require('lib/models/Setting.js'); const { shim } = require('lib/shim'); @@ -12,7 +10,6 @@ const imageType = require('image-type'); const readChunk = require('read-chunk'); class Command extends BaseCommand { - usage() { return 'e2ee [path]'; } @@ -35,7 +32,7 @@ class Command extends BaseCommand { const options = args.options; - const askForMasterKey = async (error) => { + const askForMasterKey = async error => { const masterKeyId = error.masterKeyId; const password = await this.prompt(_('Enter master password:'), { type: 'string', secure: true }); if (!password) { @@ -45,7 +42,7 @@ class Command extends BaseCommand { Setting.setObjectKey('encryption.passwordCache', masterKeyId, password); await EncryptionService.instance().loadMasterKeysFromSettings(); return true; - } + }; if (args.command === 'enable') { const password = options.password ? options.password.toString() : await this.prompt(_('Enter master password:'), { type: 'string', secure: true }); @@ -100,7 +97,7 @@ class Command extends BaseCommand { while (true) { try { const outputDir = options.output ? options.output : require('os').tmpdir(); - let outFile = outputDir + '/' + pathUtils.filename(args.path) + '.' + Date.now() + '.bin'; + let outFile = outputDir + '/' + pathUtils.filename(args.path) + '.' + Date.now() + '.bin'; await EncryptionService.instance().decryptFile(args.path, outFile); const buffer = await readChunk(outFile, 0, 64); const detectedType = imageType(buffer); @@ -128,19 +125,17 @@ class Command extends BaseCommand { if (args.command === 'target-status') { const fs = require('fs-extra'); - const pathUtils = require('lib/path-utils.js'); - const fsDriver = new (require('lib/fs-driver-node.js').FsDriverNode)(); const targetPath = args.path; if (!targetPath) throw new Error('Please specify the sync target path.'); const dirPaths = function(targetPath) { let paths = []; - fs.readdirSync(targetPath).forEach((path) => { + fs.readdirSync(targetPath).forEach(path => { paths.push(path); }); return paths; - } + }; let itemCount = 0; let resourceCount = 0; @@ -224,7 +219,6 @@ class Command extends BaseCommand { return; } } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-edit.js b/CliClient/app/command-edit.js index 9fb39ec45..9dc6b367a 100644 --- a/CliClient/app/command-edit.js +++ b/CliClient/app/command-edit.js @@ -3,15 +3,11 @@ const { BaseCommand } = require('./base-command.js'); const { uuid } = require('lib/uuid.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); const Setting = require('lib/models/Setting.js'); const BaseModel = require('lib/BaseModel.js'); -const { cliUtils } = require('./cli-utils.js'); -const { time } = require('lib/time-utils.js'); class Command extends BaseCommand { - usage() { return 'edit '; } @@ -21,20 +17,19 @@ class Command extends BaseCommand { } async action(args) { - let watcher = null; let tempFilePath = null; const onFinishedEditing = async () => { if (tempFilePath) fs.removeSync(tempFilePath); - } + }; const textEditorPath = () => { if (Setting.value('editor')) return Setting.value('editor'); if (process.env.EDITOR) return process.env.EDITOR; throw new Error(_('No text editor is defined. Please set it using `config editor `')); - } + }; - try { + try { // ------------------------------------------------------------------------- // Load note or create it if it doesn't exist // ------------------------------------------------------------------------- @@ -76,18 +71,30 @@ class Command extends BaseCommand { this.logger().info('Disabling fullscreen...'); - app().gui().showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.')); - await app().gui().forceRender(); - const termState = app().gui().termSaveState(); + app() + .gui() + .showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.')); + await app() + .gui() + .forceRender(); + const termState = app() + .gui() + .termSaveState(); - const spawnSync = require('child_process').spawnSync; + const spawnSync = require('child_process').spawnSync; const result = spawnSync(editorPath, editorArgs, { stdio: 'inherit' }); if (result.error) this.stdout(_('Error opening note in editor: %s', result.error.message)); - app().gui().termRestoreState(termState); - app().gui().hideModalOverlay(); - app().gui().forceRender(); + app() + .gui() + .termRestoreState(termState); + app() + .gui() + .hideModalOverlay(); + app() + .gui() + .forceRender(); // ------------------------------------------------------------------------- // Save the note and clean up @@ -107,13 +114,11 @@ class Command extends BaseCommand { }); await onFinishedEditing(); - - } catch(error) { + } catch (error) { await onFinishedEditing(); throw error; } } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-exit.js b/CliClient/app/command-exit.js index 32be81aff..55fb7538b 100644 --- a/CliClient/app/command-exit.js +++ b/CliClient/app/command-exit.js @@ -3,7 +3,6 @@ const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); class Command extends BaseCommand { - usage() { return 'exit'; } @@ -19,7 +18,6 @@ class Command extends BaseCommand { async action(args) { await app().exit(); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-export-sync-status.js b/CliClient/app/command-export-sync-status.js index 5b900c414..e4f7343bc 100644 --- a/CliClient/app/command-export-sync-status.js +++ b/CliClient/app/command-export-sync-status.js @@ -1,13 +1,10 @@ const { BaseCommand } = require('./base-command.js'); -const { Database } = require('lib/database.js'); const { app } = require('./app.js'); const Setting = require('lib/models/Setting.js'); -const { _ } = require('lib/locale.js'); const { ReportService } = require('lib/services/report.js'); const fs = require('fs-extra'); class Command extends BaseCommand { - usage() { return 'export-sync-status'; } @@ -23,14 +20,17 @@ class Command extends BaseCommand { async action(args) { const service = new ReportService(); const csv = await service.basicItemList({ format: 'csv' }); - const filePath = Setting.value('profileDir') + '/syncReport-' + (new Date()).getTime() + '.csv'; + const filePath = Setting.value('profileDir') + '/syncReport-' + new Date().getTime() + '.csv'; await fs.writeFileSync(filePath, csv); this.stdout('Sync status exported to ' + filePath); - app().gui().showConsole(); - app().gui().maximizeConsole(); + app() + .gui() + .showConsole(); + app() + .gui() + .maximizeConsole(); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-export.js b/CliClient/app/command-export.js index 624bce752..e7acb002c 100644 --- a/CliClient/app/command-export.js +++ b/CliClient/app/command-export.js @@ -1,14 +1,10 @@ const { BaseCommand } = require('./base-command.js'); const InteropService = require('lib/services/InteropService.js'); const BaseModel = require('lib/BaseModel.js'); -const Note = require('lib/models/Note.js'); -const { reg } = require('lib/registry.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const fs = require('fs-extra'); class Command extends BaseCommand { - usage() { return 'export '; } @@ -19,17 +15,14 @@ class Command extends BaseCommand { options() { const service = new InteropService(); - const formats = service.modules() + const formats = service + .modules() .filter(m => m.type === 'exporter') .map(m => m.format + (m.description ? ' (' + m.description + ')' : '')); - return [ - ['--format ', _('Destination format: %s', formats.join(', '))], - ['--note ', _('Exports only the given note.')], - ['--notebook ', _('Exports only the given notebook.')], - ]; + return [['--format ', _('Destination format: %s', formats.join(', '))], ['--note ', _('Exports only the given note.')], ['--notebook ', _('Exports only the given notebook.')]]; } - + async action(args) { let exportOptions = {}; exportOptions.path = args.path; @@ -37,25 +30,20 @@ class Command extends BaseCommand { exportOptions.format = args.options.format ? args.options.format : 'jex'; if (args.options.note) { - const notes = await app().loadItems(BaseModel.TYPE_NOTE, args.options.note, { parent: app().currentFolder() }); if (!notes.length) throw new Error(_('Cannot find "%s".', args.options.note)); - exportOptions.sourceNoteIds = notes.map((n) => n.id); - + exportOptions.sourceNoteIds = notes.map(n => n.id); } else if (args.options.notebook) { - const folders = await app().loadItems(BaseModel.TYPE_FOLDER, args.options.notebook); if (!folders.length) throw new Error(_('Cannot find "%s".', args.options.notebook)); - exportOptions.sourceFolderIds = folders.map((n) => n.id); - + exportOptions.sourceFolderIds = folders.map(n => n.id); } const service = new InteropService(); const result = await service.export(exportOptions); - result.warnings.map((w) => this.stdout(w)); + result.warnings.map(w => this.stdout(w)); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-geoloc.js b/CliClient/app/command-geoloc.js index eddb3bf69..aaf248054 100644 --- a/CliClient/app/command-geoloc.js +++ b/CliClient/app/command-geoloc.js @@ -2,11 +2,9 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const BaseModel = require('lib/BaseModel.js'); -const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); class Command extends BaseCommand { - usage() { return 'geoloc '; } @@ -23,9 +21,10 @@ class Command extends BaseCommand { const url = Note.geolocationUrl(item); this.stdout(url); - app().gui().showConsole(); + app() + .gui() + .showConsole(); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-help.js b/CliClient/app/command-help.js index 016e08d78..e26a88b1b 100644 --- a/CliClient/app/command-help.js +++ b/CliClient/app/command-help.js @@ -1,14 +1,10 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { renderCommandHelp } = require('./help-utils.js'); -const { Database } = require('lib/database.js'); -const Setting = require('lib/models/Setting.js'); -const { wrap } = require('lib/string-utils.js'); const { _ } = require('lib/locale.js'); const { cliUtils } = require('./cli-utils.js'); class Command extends BaseCommand { - usage() { return 'help [command]'; } @@ -28,7 +24,7 @@ class Command extends BaseCommand { output.push(command); } - output.sort((a, b) => a.name() < b.name() ? -1 : +1); + output.sort((a, b) => (a.name() < b.name() ? -1 : +1)); return output; } @@ -40,31 +36,37 @@ class Command extends BaseCommand { this.stdout(_('For information on how to customise the shortcuts please visit %s', 'https://joplinapp.org/terminal/#shortcuts')); this.stdout(''); - if (app().gui().isDummy()) { + if ( + app() + .gui() + .isDummy() + ) { throw new Error(_('Shortcuts are not available in CLI mode.')); } - const keymap = app().gui().keymap(); + const keymap = app() + .gui() + .keymap(); let rows = []; for (let i = 0; i < keymap.length; i++) { const item = keymap[i]; - const keys = item.keys.map((k) => k === ' ' ? '(SPACE)' : k); + const keys = item.keys.map(k => (k === ' ' ? '(SPACE)' : k)); rows.push([keys.join(', '), item.command]); } cliUtils.printArray(this.stdout.bind(this), rows); } else if (args.command === 'all') { const commands = this.allCommands(); - const output = commands.map((c) => renderCommandHelp(c)); + const output = commands.map(c => renderCommandHelp(c)); this.stdout(output.join('\n\n')); } else if (args.command) { const command = app().findCommandByName(args['command']); if (!command) throw new Error(_('Cannot find "%s".', args.command)); this.stdout(renderCommandHelp(command, stdoutWidth)); } else { - const commandNames = this.allCommands().map((a) => a.name()); + const commandNames = this.allCommands().map(a => a.name()); this.stdout(_('Type `help [command]` for more information about a command; or type `help all` for the complete usage information.')); this.stdout(''); @@ -82,10 +84,13 @@ class Command extends BaseCommand { this.stdout(_('For the list of keyboard shortcuts and config options, type `help keymap`')); } - app().gui().showConsole(); - app().gui().maximizeConsole(); + app() + .gui() + .showConsole(); + app() + .gui() + .maximizeConsole(); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-import.js b/CliClient/app/command-import.js index 0f44aef62..4b559e897 100644 --- a/CliClient/app/command-import.js +++ b/CliClient/app/command-import.js @@ -1,17 +1,11 @@ const { BaseCommand } = require('./base-command.js'); const InteropService = require('lib/services/InteropService.js'); const BaseModel = require('lib/BaseModel.js'); -const Note = require('lib/models/Note.js'); -const { filename, basename, fileExtension } = require('lib/path-utils.js'); -const { importEnex } = require('lib/import-enex'); const { cliUtils } = require('./cli-utils.js'); -const { reg } = require('lib/registry.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const fs = require('fs-extra'); class Command extends BaseCommand { - usage() { return 'import [notebook]'; } @@ -22,14 +16,14 @@ class Command extends BaseCommand { options() { const service = new InteropService(); - const formats = service.modules().filter(m => m.type === 'importer').map(m => m.format); + const formats = service + .modules() + .filter(m => m.type === 'importer') + .map(m => m.format); - return [ - ['--format ', _('Source format: %s', (['auto'].concat(formats)).join(', '))], - ['-f, --force', _('Do not ask for confirmation.')], - ]; + return [['--format ', _('Source format: %s', ['auto'].concat(formats).join(', '))], ['-f, --force', _('Do not ask for confirmation.')]]; } - + async action(args) { let folder = await app().loadItem(BaseModel.TYPE_FOLDER, args.notebook); @@ -44,7 +38,7 @@ class Command extends BaseCommand { // onProgress/onError supported by Enex import only - importOptions.onProgress = (progressState) => { + importOptions.onProgress = progressState => { let line = []; line.push(_('Found: %d.', progressState.loaded)); line.push(_('Created: %d.', progressState.created)); @@ -56,20 +50,21 @@ class Command extends BaseCommand { cliUtils.redraw(lastProgress); }; - importOptions.onError = (error) => { + importOptions.onError = error => { let s = error.trace ? error.trace : error.toString(); this.stdout(s); }; - app().gui().showConsole(); + app() + .gui() + .showConsole(); this.stdout(_('Importing notes...')); const service = new InteropService(); const result = await service.import(importOptions); - result.warnings.map((w) => this.stdout(w)); + result.warnings.map(w => this.stdout(w)); cliUtils.redrawDone(); if (lastProgress) this.stdout(_('The notes have been imported: %s', lastProgress)); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-ls.js b/CliClient/app/command-ls.js index 029681a11..54a1b8f1c 100644 --- a/CliClient/app/command-ls.js +++ b/CliClient/app/command-ls.js @@ -10,7 +10,6 @@ const { time } = require('lib/time-utils.js'); const { cliUtils } = require('./cli-utils.js'); class Command extends BaseCommand { - usage() { return 'ls [note-pattern]'; } @@ -24,14 +23,7 @@ class Command extends BaseCommand { } options() { - return [ - ['-n, --limit ', _('Displays only the first top notes.')], - ['-s, --sort ', _('Sorts the item by (eg. title, updated_time, created_time).')], - ['-r, --reverse', _('Reverses the sorting order.')], - ['-t, --type ', _('Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.')], - ['-f, --format ', _('Either "text" or "json"')], - ['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')], - ]; + return [['-n, --limit ', _('Displays only the first top notes.')], ['-s, --sort ', _('Sorts the item by (eg. title, updated_time, created_time).')], ['-r, --reverse', _('Reverses the sorting order.')], ['-t, --type ', _('Displays only the items of the specific type(s). Can be `n` for notes, `t` for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the to-dos, while `-ttd` would display notes and to-dos.')], ['-f, --format ', _('Either "text" or "json"')], ['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')]]; } async action(args) { @@ -105,7 +97,7 @@ class Command extends BaseCommand { if (hasTodos) { if (item.is_todo) { - row.push(sprintf('[%s]', !!item.todo_completed ? 'X' : ' ')); + row.push(sprintf('[%s]', item.todo_completed ? 'X' : ' ')); } else { row.push(' '); } @@ -118,9 +110,7 @@ class Command extends BaseCommand { cliUtils.printArray(this.stdout.bind(this), rows); } - } - } module.exports = Command; diff --git a/CliClient/app/command-mkbook.js b/CliClient/app/command-mkbook.js index cc668b49a..032a89285 100644 --- a/CliClient/app/command-mkbook.js +++ b/CliClient/app/command-mkbook.js @@ -2,10 +2,8 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const Folder = require('lib/models/Folder.js'); -const { reg } = require('lib/registry.js'); class Command extends BaseCommand { - usage() { return 'mkbook '; } @@ -15,10 +13,9 @@ class Command extends BaseCommand { } async action(args) { - let folder = await Folder.save({ title: args['new-notebook'] }, { userSideValidation: true }); + let folder = await Folder.save({ title: args['new-notebook'] }, { userSideValidation: true }); app().switchCurrentFolder(folder); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-mknote.js b/CliClient/app/command-mknote.js index 79f4c19a7..ffecfcf87 100644 --- a/CliClient/app/command-mknote.js +++ b/CliClient/app/command-mknote.js @@ -4,7 +4,6 @@ const { _ } = require('lib/locale.js'); const Note = require('lib/models/Note.js'); class Command extends BaseCommand { - usage() { return 'mknote '; } @@ -26,7 +25,6 @@ class Command extends BaseCommand { app().switchCurrentFolder(app().currentFolder()); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-mktodo.js b/CliClient/app/command-mktodo.js index d96fb5275..ca4bc57d9 100644 --- a/CliClient/app/command-mktodo.js +++ b/CliClient/app/command-mktodo.js @@ -4,7 +4,6 @@ const { _ } = require('lib/locale.js'); const Note = require('lib/models/Note.js'); class Command extends BaseCommand { - usage() { return 'mktodo '; } @@ -27,7 +26,6 @@ class Command extends BaseCommand { app().switchCurrentFolder(app().currentFolder()); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-mv.js b/CliClient/app/command-mv.js index 729bab96e..3747d6c16 100644 --- a/CliClient/app/command-mv.js +++ b/CliClient/app/command-mv.js @@ -6,7 +6,6 @@ const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); class Command extends BaseCommand { - usage() { return 'mv [notebook]'; } @@ -18,7 +17,7 @@ class Command extends BaseCommand { async action(args) { const pattern = args['note']; const destination = args['notebook']; - + const folder = await Folder.loadByField('title', destination); if (!folder) throw new Error(_('Cannot find "%s".', destination)); @@ -29,7 +28,6 @@ class Command extends BaseCommand { await Note.moveToFolder(notes[i].id, folder.id); } } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-ren.js b/CliClient/app/command-ren.js index 45b60671c..28f8d4bbe 100644 --- a/CliClient/app/command-ren.js +++ b/CliClient/app/command-ren.js @@ -6,7 +6,6 @@ const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); class Command extends BaseCommand { - usage() { return 'ren '; } @@ -35,7 +34,6 @@ class Command extends BaseCommand { await Note.save(newItem); } } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-rmbook.js b/CliClient/app/command-rmbook.js index b141be41a..4cf729c4e 100644 --- a/CliClient/app/command-rmbook.js +++ b/CliClient/app/command-rmbook.js @@ -1,14 +1,10 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const BaseItem = require('lib/models/BaseItem.js'); const Folder = require('lib/models/Folder.js'); -const Note = require('lib/models/Note.js'); const BaseModel = require('lib/BaseModel.js'); -const { cliUtils } = require('./cli-utils.js'); class Command extends BaseCommand { - usage() { return 'rmbook '; } @@ -18,9 +14,7 @@ class Command extends BaseCommand { } options() { - return [ - ['-f, --force', _('Deletes the notebook without asking for confirmation.')], - ]; + return [['-f, --force', _('Deletes the notebook without asking for confirmation.')]]; } async action(args) { @@ -34,7 +28,6 @@ class Command extends BaseCommand { await Folder.delete(folder.id); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-rmnote.js b/CliClient/app/command-rmnote.js index 8e1ecea2f..9d8c18d11 100644 --- a/CliClient/app/command-rmnote.js +++ b/CliClient/app/command-rmnote.js @@ -1,14 +1,10 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const BaseItem = require('lib/models/BaseItem.js'); -const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); const BaseModel = require('lib/BaseModel.js'); -const { cliUtils } = require('./cli-utils.js'); class Command extends BaseCommand { - usage() { return 'rmnote '; } @@ -18,9 +14,7 @@ class Command extends BaseCommand { } options() { - return [ - ['-f, --force', _('Deletes the notes without asking for confirmation.')], - ]; + return [['-f, --force', _('Deletes the notes without asking for confirmation.')]]; } async action(args) { @@ -32,10 +26,9 @@ class Command extends BaseCommand { const ok = force ? true : await this.prompt(notes.length > 1 ? _('%d notes match this pattern. Delete them?', notes.length) : _('Delete note?'), { booleanAnswerDefault: 'n' }); if (!ok) return; - let ids = notes.map((n) => n.id); + let ids = notes.map(n => n.id); await Note.batchDelete(ids); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-search.js b/CliClient/app/command-search.js index 3f14157d4..fc1385f3f 100644 --- a/CliClient/app/command-search.js +++ b/CliClient/app/command-search.js @@ -1,15 +1,10 @@ const { BaseCommand } = require('./base-command.js'); -const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const BaseModel = require('lib/BaseModel.js'); const Folder = require('lib/models/Folder.js'); -const Note = require('lib/models/Note.js'); -const { sprintf } = require('sprintf-js'); -const { time } = require('lib/time-utils.js'); const { uuid } = require('lib/uuid.js'); class Command extends BaseCommand { - usage() { return 'search [notebook]'; } @@ -50,7 +45,6 @@ class Command extends BaseCommand { id: searchId, }); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-set.js b/CliClient/app/command-set.js index 3a01ae5b4..de28bbce8 100644 --- a/CliClient/app/command-set.js +++ b/CliClient/app/command-set.js @@ -3,12 +3,9 @@ const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const BaseModel = require('lib/BaseModel.js'); const { Database } = require('lib/database.js'); -const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); -const BaseItem = require('lib/models/BaseItem.js'); class Command extends BaseCommand { - usage() { return 'set [value]'; } @@ -45,7 +42,6 @@ class Command extends BaseCommand { await Note.save(newNote); } } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-status.js b/CliClient/app/command-status.js index 0bda410bf..fd150b749 100644 --- a/CliClient/app/command-status.js +++ b/CliClient/app/command-status.js @@ -1,12 +1,10 @@ const { BaseCommand } = require('./base-command.js'); -const { Database } = require('lib/database.js'); const { app } = require('./app.js'); const Setting = require('lib/models/Setting.js'); const { _ } = require('lib/locale.js'); const { ReportService } = require('lib/services/report.js'); class Command extends BaseCommand { - usage() { return 'status'; } @@ -34,10 +32,13 @@ class Command extends BaseCommand { } } - app().gui().showConsole(); - app().gui().maximizeConsole(); + app() + .gui() + .showConsole(); + app() + .gui() + .maximizeConsole(); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-sync.js b/CliClient/app/command-sync.js index 1fb41b26d..81a39991f 100644 --- a/CliClient/app/command-sync.js +++ b/CliClient/app/command-sync.js @@ -3,7 +3,6 @@ const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js'); const Setting = require('lib/models/Setting.js'); -const BaseItem = require('lib/models/BaseItem.js'); const ResourceFetcher = require('lib/services/ResourceFetcher'); const { Synchronizer } = require('lib/synchronizer.js'); const { reg } = require('lib/registry.js'); @@ -14,7 +13,6 @@ const fs = require('fs-extra'); const SyncTargetRegistry = require('lib/SyncTargetRegistry'); class Command extends BaseCommand { - constructor() { super(); this.syncTargetId_ = null; @@ -31,9 +29,7 @@ class Command extends BaseCommand { } options() { - return [ - ['--target ', _('Sync to provided target (defaults to sync.target config value)')], - ]; + return [['--target ', _('Sync to provided target (defaults to sync.target config value)')]]; } static lockFile(filePath) { @@ -66,13 +62,16 @@ class Command extends BaseCommand { const syncTarget = reg.syncTarget(this.syncTargetId_); const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_); - if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) { // OneDrive + if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) { + // OneDrive this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api()); const auth = await this.oneDriveApiUtils_.oauthDance({ - log: (...s) => { return this.stdout(...s); } + log: (...s) => { + return this.stdout(...s); + }, }); this.oneDriveApiUtils_ = null; - + Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null); if (!auth) { this.stdout(_('Authentication was not completed (did not receive an authentication token).')); @@ -80,7 +79,8 @@ class Command extends BaseCommand { } return true; - } else if (syncTargetMd.name === 'dropbox') { // Dropbox + } else if (syncTargetMd.name === 'dropbox') { + // Dropbox const api = await syncTarget.api(); const loginUrl = api.loginUrl(); this.stdout(_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')); @@ -118,7 +118,7 @@ class Command extends BaseCommand { // Lock is unique per profile/database const lockFilePath = require('os').tmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41 - if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock'); + if (!(await fs.pathExists(lockFilePath))) await fs.writeFile(lockFilePath, 'synclock'); try { if (await Command.isLocked(lockFilePath)) throw new Error(_('Synchronisation is already in progress.')); @@ -147,22 +147,26 @@ class Command extends BaseCommand { const syncTarget = reg.syncTarget(this.syncTargetId_); - if (!await syncTarget.isAuthenticated()) { - app().gui().showConsole(); - app().gui().maximizeConsole(); + if (!(await syncTarget.isAuthenticated())) { + app() + .gui() + .showConsole(); + app() + .gui() + .maximizeConsole(); const authDone = await this.doAuth(); if (!authDone) return cleanUp(); } - + const sync = await syncTarget.synchronizer(); let options = { - onProgress: (report) => { + onProgress: report => { let lines = Synchronizer.reportToLines(report); if (lines.length) cliUtils.redraw(lines.join(' ')); }, - onMessage: (msg) => { + onMessage: msg => { cliUtils.redrawDone(); this.stdout(msg); }, @@ -237,7 +241,6 @@ class Command extends BaseCommand { cancellable() { return true; } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-tag.js b/CliClient/app/command-tag.js index eedfa58f7..eb5d2dbf6 100644 --- a/CliClient/app/command-tag.js +++ b/CliClient/app/command-tag.js @@ -6,7 +6,6 @@ const BaseModel = require('lib/BaseModel.js'); const { time } = require('lib/time-utils.js'); class Command extends BaseCommand { - usage() { return 'tag [tag] [note]'; } @@ -16,9 +15,7 @@ class Command extends BaseCommand { } options() { - return [ - ['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')], - ]; + return [['-l, --long', _('Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE')]]; } async action(args) { @@ -50,7 +47,7 @@ class Command extends BaseCommand { } else if (command == 'list') { if (tag) { let notes = await Tag.notes(tag.id); - notes.map((note) => { + notes.map(note => { let line = ''; if (options.long) { line += BaseModel.shortId(note.id); @@ -61,7 +58,7 @@ class Command extends BaseCommand { if (note.is_todo) { line += '['; if (note.todo_completed) { - line += 'X'; + line += 'X'; } else { line += ' '; } @@ -74,13 +71,14 @@ class Command extends BaseCommand { }); } else { let tags = await Tag.all(); - tags.map((tag) => { this.stdout(tag.title); }); + tags.map(tag => { + this.stdout(tag.title); + }); } } else { throw new Error(_('Invalid command: "%s"', command)); } } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-todo.js b/CliClient/app/command-todo.js index 1f83e2cda..b84076ce2 100644 --- a/CliClient/app/command-todo.js +++ b/CliClient/app/command-todo.js @@ -2,12 +2,10 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const BaseModel = require('lib/BaseModel.js'); -const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); const { time } = require('lib/time-utils.js'); class Command extends BaseCommand { - usage() { return 'todo '; } @@ -39,12 +37,11 @@ class Command extends BaseCommand { } } else if (action == 'clear') { toSave.is_todo = 0; - } + } await Note.save(toSave); } } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-undone.js b/CliClient/app/command-undone.js index 3373c0ee3..2e750f3d9 100644 --- a/CliClient/app/command-undone.js +++ b/CliClient/app/command-undone.js @@ -1,15 +1,9 @@ const { BaseCommand } = require('./base-command.js'); -const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); -const BaseModel = require('lib/BaseModel.js'); -const Folder = require('lib/models/Folder.js'); -const Note = require('lib/models/Note.js'); -const { time } = require('lib/time-utils.js'); const CommandDone = require('./command-done.js'); class Command extends BaseCommand { - usage() { return 'undone '; } @@ -21,7 +15,6 @@ class Command extends BaseCommand { async action(args) { await CommandDone.handleAction(this, args, false); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-use.js b/CliClient/app/command-use.js index c25a635c2..bcdfc3a94 100644 --- a/CliClient/app/command-use.js +++ b/CliClient/app/command-use.js @@ -2,10 +2,8 @@ const { BaseCommand } = require('./base-command.js'); const { app } = require('./app.js'); const { _ } = require('lib/locale.js'); const BaseModel = require('lib/BaseModel.js'); -const Folder = require('lib/models/Folder.js'); class Command extends BaseCommand { - usage() { return 'use '; } @@ -14,10 +12,6 @@ class Command extends BaseCommand { return _('Switches to [notebook] - all further operations will happen within this notebook.'); } - autocomplete() { - return { data: autocompleteFolders }; - } - compatibleUis() { return ['cli']; } @@ -27,7 +21,6 @@ class Command extends BaseCommand { if (!folder) throw new Error(_('Cannot find "%s".', args['notebook'])); app().switchCurrentFolder(folder); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/command-version.js b/CliClient/app/command-version.js index 9a3603b24..8a02ae676 100644 --- a/CliClient/app/command-version.js +++ b/CliClient/app/command-version.js @@ -3,7 +3,6 @@ const Setting = require('lib/models/Setting.js'); const { _ } = require('lib/locale.js'); class Command extends BaseCommand { - usage() { return 'version'; } @@ -16,7 +15,6 @@ class Command extends BaseCommand { const p = require('./package.json'); this.stdout(_('%s %s (%s)', p.name, p.version, Setting.value('env'))); } - } -module.exports = Command; \ No newline at end of file +module.exports = Command; diff --git a/CliClient/app/fuzzing.js b/CliClient/app/fuzzing.js index f28fb0c06..80bd8d8d3 100644 --- a/CliClient/app/fuzzing.js +++ b/CliClient/app/fuzzing.js @@ -1,4 +1,4 @@ -"use strict" +'use strict'; const { time } = require('lib/time-utils.js'); const { Logger } = require('lib/logger.js'); @@ -6,7 +6,7 @@ const Resource = require('lib/models/Resource.js'); const { dirname } = require('lib/path-utils.js'); const { FsDriverNode } = require('./fs-driver-node.js'); const lodash = require('lodash'); -const exec = require('child_process').exec +const exec = require('child_process').exec; const fs = require('fs-extra'); const baseDir = dirname(__dirname) + '/tests/fuzzing'; @@ -28,8 +28,8 @@ process.on('unhandledRejection', (reason, p) => { function createClient(id) { return { - 'id': id, - 'profileDir': baseDir + '/client' + id, + id: id, + profileDir: baseDir + '/client' + id, }; } @@ -39,7 +39,11 @@ async function createClients() { for (let clientId = 0; clientId < 2; clientId++) { let client = createClient(clientId); promises.push(fs.remove(client.profileDir)); - promises.push(execCommand(client, 'config sync.target 2').then(() => { return execCommand(client, 'config sync.2.path ' + syncDir); })); + promises.push( + execCommand(client, 'config sync.target 2').then(() => { + return execCommand(client, 'config sync.2.path ' + syncDir); + }) + ); output.push(client); } @@ -54,7 +58,2008 @@ function randomElement(array) { } function randomWord() { - const words = ['belief','scandalous','flawless','wrestle','sort','moldy','carve','incompetent','cruel','awful','fang','holistic','makeshift','synonymous','questionable','soft','drop','boot','whimsical','stir','idea','adhesive','present','hilarious','unusual','divergent','probable','depend','suck','belong','advise','straight','encouraging','wing','clam','serve','fill','nostalgic','dysfunctional','aggressive','floor','baby','grease','sisters','print','switch','control','victorious','cracker','dream','wistful','adaptable','reminiscent','inquisitive','pushy','unaccountable','receive','guttural','two','protect','skin','unbiased','plastic','loutish','zip','used','divide','communicate','dear','muddled','dinosaurs','grip','trees','well-off','calendar','chickens','irate','deranged','trip','stream','white','poison','attack','obtain','theory','laborer','omniscient','brake','maniacal','curvy','smoke','babies','punch','hammer','toothbrush','same','crown','jagged','peep','difficult','reject','merciful','useless','doctor','mix','wicked','plant','quickest','roll','suffer','curly','brother','frighten','cold','tremendous','move','knot','lame','imaginary','capricious','raspy','aunt','loving','wink','wooden','hop','free','drab','fire','instrument','border','frame','silent','glue','decorate','distance','powerful','pig','admit','fix','pour','flesh','profuse','skinny','learn','filthy','dress','bloody','produce','innocent','meaty','pray','slimy','sun','kindhearted','dime','exclusive','boast','neat','ruthless','recess','grieving','daily','hateful','ignorant','fence','spring','slim','education','overflow','plastic','gaping','chew','detect','right','lunch','gainful','argue','cloistered','horses','orange','shame','bitter','able','sail','magical','exist','force','wheel','best','suit','spurious','partner','request','dog','gusty','money','gaze','lonely','company','pale','tempt','rat','flame','wobble','superficial','stop','protective','stare','tongue','heal','railway','idiotic','roll','puffy','turn','meeting','new','frightening','sophisticated','poke','elderly','room','stimulating','increase','moor','secret','lean','occur','country','damp','evanescent','alluring','oafish','join','thundering','cars','awesome','advice','unruly','ray','wind','anxious','fly','hammer','adventurous','shop','cook','trucks','nonchalant','addition','base','abashed','excuse','giants','dramatic','piquant','coach','possess','poor','finger','wide-eyed','aquatic','welcome','instruct','expert','evasive','hug','cute','return','mice','damage','turkey','quiet','bewildered','tidy','pointless','outrageous','medical','foolish','curve','grandiose','gullible','hapless','gleaming','third','grin','pipe','egg','act','physical','eager','side','milk','tearful','fertile','average','glamorous','strange','yak','terrific','thin','near','snails','flowery','authority','fish','curious','perpetual','healthy','health','match','fade','chemical','economic','drawer','avoid','lying','minister','lick','powder','decay','desire','furry','faint','beam','sordid','fax','tail','bawdy','cherry','letter','clover','ladybug','teeth','behavior','black','amazing','pink','waste','island','forgetful','needless','lock','waves','boundary','receipt','handy','religion','hypnotic','aftermath','explain','sense','mundane','rambunctious','second','preserve','alarm','dusty','event','blow','weigh','value','glorious','jail','sigh','cemetery','serious','yummy','cattle','understood','limit','alert','fear','lucky','tested','surround','dolls','pleasant','disillusioned','discover','tray','night','seemly','liquid','worry','pen','bent','gruesome','war','teeny-tiny','common','judge','symptomatic','bed','trot','unequaled','flowers','friends','damaged','peel','skip','show','twist','worthless','brush','look','behave','imperfect','week','petite','direction','soda','lively','coal','coil','release','berserk','books','impossible','replace','cough','chunky','torpid','discreet','material','bomb','soothe','crack','hope','license','frightened','breathe','maddening','calculator','committee','paltry','green','subsequent','arrest','gigantic','tasty','metal','willing','man','stem','nonstop','route','impulse','government','comfortable','include','literate','multiply','test','vast','exercise','addicted','agreeable','lace','toes','young','water','end','wash','glossy','round','staking','sink','open','spot','trip','fierce','robust','pastoral','drown','dress','machine','calculating','holiday','crabby','disgusting','plan','sleet','sleepy','typical','borrow','possible','curtain','airplane','industry','nut','rough','wacky','rock','enormous','uninterested','sugar','rake','consist','wrist','basket','chop','wet','street','known','settle','bless','cluttered','wild','expand','angle','snake','yawn','hate','flood','rabid','spiteful','anger','market','bizarre','force','majestic','scissors','beg','rifle','foregoing','cactus','funny','eggnog','wish','high-pitched','drop','camp','scarf','car','groan','wonderful','wealthy','cup','lock','available','previous','jam','political','vacation','three','desk','fry','aspiring','productive','clear','bored','flashy','plug','precede','abhorrent','muddle','flimsy','paste','need','reward','frail','obnoxious','creature','whip','unbecoming','lake','unused','chin','tour','zephyr','experience','building','scrub','correct','hover','panicky','scorch','diligent','hulking','ubiquitous','tedious','aberrant','file','accidental','mist','blue-eyed','trite','nondescript','cows','wait','test','snotty','amuck','jump','lackadaisical','grey','tawdry','strong','land','kind','star','ludicrous','stupid','telling','use','bruise','whirl','cream','harsh','aboriginal','substantial','brawny','tease','pollution','weather','degree','dry','film','obey','closed','dependent','want','undesirable','stamp','relax','foot','obscene','successful','wriggle','drain','greasy','escape','cross','odd','boring','absorbed','houses','suppose','suit','moon','ceaseless','explode','clap','pop','courageous','miss','notebook','delirious','form','pretty','sock','grotesque','noxious','record','stop','saw','thing','dislike','cloth','six','jar','unnatural','spiffy','itchy','secretary','move','certain','unkempt','sassy','queue','shrug','crow','heavenly','desert','screw','vessel','mug','encourage','icy','enthusiastic','throat','whistle','ignore','miniature','squeak','scarecrow','fluttering','hang','icicle','lie','juicy','empty','baseball','various','promise','abortive','descriptive','high','spy','faded','talk','air','messup','decorous','sneaky','mark','sack','ultra','chivalrous','lethal','expect','disgusted','reaction','fireman','private','ritzy','manage','actor','rely','uppity','thread','bat','space','underwear','blood','nine','maid','shelf','hanging','shop','prick','wound','sloppy','offer','increase','clear','slap','rude','poised','wretched','cause','quince','tame','remarkable','abject','sail','guide','subdued','spiky','debonair','chicken','tired','hum','land','scared','splendid','guess','cast','rub','magnificent','ants','overwrought','interfere','gorgeous','office','trade','sniff','melted','bore','point','pet','purple','brake','flavor','toe','prickly','zinc','homely','modern','kindly','whisper','bare','annoyed','glass','noisy','null','thoughtless','skirt','dock','rings','mind','neck','macho','wave','history','play','road','profit','word','opposite','dreary','governor','horse','trust','elbow','kiss','crayon','stitch','excited','needy','arrange','easy','alcoholic','safe','lumpy','monkey','smile','capable','untidy','extra-small','memory','selective','reproduce','old-fashioned','overrated','texture','knit','downtown','risk','pot','sofa','righteous','wren','pull','carry','aboard','listen','classy','thank','shocking','condition','root','attempt','swim','frog','hurt','army','title','handsomely','town','guiltless','thaw','spell','selfish','disturbed','tramp','girls','utopian','noiseless','trail','bashful','business','rhetorical','snail','sign','plausible','left','design','tall','violent','wasteful','beautiful','breezy','tap','murder','talented','needle','creator','imagine','flippant','dead','bone','coherent','relation','aromatic','mountainous','face','ask','picture','pedal','colour','obese','group','top','bubble','pinch','optimal','school','bathe','flagrant','check','deliver','pass','tan','crate','hose','debt','faulty','longing','hollow','invincible','afford','lovely','ticket','changeable','subtract','fumbling','responsible','confused','woman','touch','watch','zesty','library','jail','wrap','terrify','brick','popcorn','cooperative','peck','pocket','property','buzz','tiresome','digestion','exciting','nation','juvenile','shade','copper','wanting','deer','waste','man','join','spotty','amused','mountain','waggish','bushes','tense','river','heartbreaking','help','mine','narrow','smash','scrawny','tame','rain','playground','airport','astonishing','level','befitting','animal','heat','painful','cellar','ski','sedate','knowing','vigorous','change','eight','ship','work','strip','robin','tank','challenge','vacuous','representative','regret','tightfisted','erratic','club','imported','therapeutic','rainstorm','luxuriant','relieved','day','system','apologise','male','prepare','malicious','naive','whistle','curl','hobbies','trousers','stereotyped','dad','endurable','grass','hot','bomb','morning','guide','keen','plot','accept','disastrous','macabre','year','spicy','absorbing','sticks','efficient','drain','warm','rice','utter','fact','marked','ratty','chalk','towering','treat','nest','annoy','jealous','stamp','effect','cautious','jelly','feigned','gabby','corn','volleyball','pan','psychedelic','fairies','silent','zonked','bump','trouble','mass','queen','things','bury','sister','quiet','colossal','puncture','four','attend','love','wiry','vegetable','destruction','note','pies','resolute','load','fancy','tacky','periodic','abandoned','vivacious','blush','wrathful','miscreant','call','striped','wiggly','supreme','hand','impolite','rule','deserted','concern','cover','harbor','waiting','soggy','psychotic','ancient','sponge','domineering','elegant','impartial','unlock','abrasive','count','flight','neighborly','roof','bulb','auspicious','automatic','magic','sign','amusing','orange','branch','sulky','attack','fetch','number','jellyfish','start','alike','touch','sour','wary','minor','punish','connect','protest','pie','kaput','doubtful','friendly','simplistic','smart','vanish','applaud','jumbled','ready','yell','support','squash','raise','parallel','super','jazzy','crush','apathetic','water','food','thrill','permit','heady','last','mine','signal','smoke','preach','x-ray','name','birth','minute','steel','bedroom','female','acrid','riddle','attractive','earth','crack','muscle','alive','guarded','sweater','donkey','doubt','lettuce','magenta','live','farm','glib','bow','fascinated','friend','practise','remember','bleach','hungry','voiceless','pin','sparkling','report','arm','sad','shaggy','parcel','wail','flash','territory','functional','wise','screeching','appliance','future','appear','team','rabbit','porter','paint','flat','amusement','ocean','head','geese','wash','embarrassed','tub','boundless','freezing','mushy','surprise','temporary','marble','recondite','telephone','zipper','pine','reign','pump','tangy','reply','toys','purpose','songs','form','delicious','wood','horn','nutty','fruit','lumber','potato','cheat','cloudy','visit','reduce','destroy','deafening','full','warlike','mitten','cover','earthy','seashore','yarn','tenuous','pause','boil','dogs','tough','knife','shy','naughty','existence','fire','eminent','remove','juice','sleep','voyage','balance','unsightly','plough','ill-fated','pumped','motionless','allow','trade','warm','toad','wave','wall','pigs','circle','rejoice','ear','drink','found','taboo','object','old','temper','plant','public','picayune','blot','delight','carpenter','dispensable','tire','cow','furniture','rightful','mute','gentle','gifted','ragged','stiff','retire','compare','sable','hole','judicious','chilly','sparkle','futuristic','love','bubble','travel','name','numberless','succeed','acoustic','lowly','society','injure','agree','reason','party','wool','careful','hook','bell','ball','attach','scream','development','happy','appreciate','disagree','request','march','rampant','scrape','sack','hair','measure','owe','grubby','vein','boy','punishment','smoggy','wry','immense','shoe','pack','brash','cave','sincere','adorable','fantastic','attraction','racial','jittery','defiant','honey','paper','weight','bee','blind','birthday','toothsome','trick','guard','fog','handle','dirty','salt','rinse','nippy','observe','suggestion','weak','instinctive','frequent','detail','verse','quirky','scattered','toy','aware','distribution','repulsive','draconian','bucket','harm','radiate','bang','shrill','living','rhythm','obsequious','drum','inject','skate','beds','smash','order','stitch','ground','nosy','kick','dusty','home','rot','frame','jam','sky','soap','rescue','energetic','grape','massive','deeply','dazzling','park','pull','number','abundant','barbarous','drag','ajar','close','moan','haircut','shade','married','cats','thirsty','dirt','vagabond','fearful','squealing','squalid','zebra','murky','sheet','fat','follow','bikes','unpack','materialistic','surprise','arch','selection','acoustics','helpless','thoughtful','cry','quarrelsome','arrogant','illegal','sudden','elite','tomatoes','spoil','flower','shivering','front','caption','volcano','ugliest','ambitious','pickle','interrupt','nervous','approve','messy','dust','oceanic','brass','tremble','fine','nerve','lunchroom','hard','engine','erect','flower','cynical','irritating','tight','cobweb','gray','invention','snatch','account','sharp','spooky','squeamish','eatable','share','need','moaning','suspect','rush','rural','false','float','bite','careless','sidewalk','cowardly','stroke','educated','ugly','type','wandering','bolt','mint','fit','large','extra-large','defeated','kitty','tacit','abiding','grandfather','trains','lamp','habitual','fast','offbeat','accurate','many','fortunate','lyrical','charge','illustrious','transport','wakeful','cable','ordinary','string','question','train','fancy','kick','enchanting','jobless','ahead','comparison','loose','dance','add','wonder','stale','earn','reflective','bright','true','statuesque','amount','matter','repair','care','ruin','terrible','elastic','spiders','craven','lamentable','decision','swing','connection','gaudy','knowledge','cheap','lazy','step','dinner','rod','agreement','comb','mean','past','knotty','busy','quicksand','match','early','long','onerous','ambiguous','worried','spade','happen','crook','dapper','grate','announce','plate','haunt','friction','actually','chance','example','rapid','zealous','necessary','ink','mere','shock','huge','jaded','spill','store','fuzzy','table','bottle','halting','spark','end','remain','transport','seat','leg','long-term','clip','grumpy','shake','walk','try','action','soup','short','hurry','square','belligerent','thankful','beginner','small','bumpy','silly','badge','marvelous','wealth','open','unequal','scatter','pest','fool','step','groovy','childlike','door','bouncy','believe','incredible','box','unhealthy','swanky','abrupt','depressed','flaky','famous','detailed','regret','envious','natural','apparel','spare','mark','ten','power','glistening','arrive','animated','slip','heap','shaky','unfasten','contain','inexpensive','introduce','shallow','rule','gather','pump','humorous','acceptable','womanly','giddy','silk','yoke','straw','invite','one','red','growth','unadvised','measly','flap','puzzled','regular','painstaking','little','plain','tumble','rest','fabulous','melt','label','truculent','internal','passenger','zippy','bright','earsplitting','tooth','veil','grip','square','stuff','gate','level','stone','observation','time','workable','bird','realise','spotted','coast','quiver','rebel','entertain','rotten','loss','collect','meal','satisfy','military','bake','cagey','redundant','treatment','knock','blink','scale','board','fair','nebulous','sip','homeless','place','complain','joke','bat','winter','choke','frantic','chubby','highfalutin','troubled','whole','rose','delightful','loaf','afraid','sturdy','class','cultured','yielding','broken','kittens','absurd','discovery','next','disarm','dangerous','lively','reflect','chief','teeny','pencil','ban','grade','size','dashing','thought','breath','empty','hellish','shock','sea','weary','payment','limping','premium','grateful','somber','tax','coach','vulgar','stretch','tow','branch','insurance','yam','stormy','wish','snow','cute','milky','battle','far','roasted','slip','adamant','awake','employ','tangible','tickle','jog','hysterical','meddle','parsimonious','judge','educate','respect','sound','oven','gratis','station','train','purring','steady','carriage','humdrum','voracious','jolly','brainy','proud','elfin','cure','motion','record','quizzical','pail','bike','faithful','approval','vague','fall','store','normal','rock','bear','bounce','giant','satisfying','crooked','lopsided','vest','separate','sneeze','teaching','general','meat','festive','historical','line','north','tip','son','damaging','nimble','broad','list','confuse','first','deserve','steep','last','rich','oval','thick','glow','great','clammy','cheer','untidy','scientific','noise','stomach','undress','big','bite-sized','enter','cake','aloof','abnormal','month','grab','well-groomed','silver','art','berry','giraffe','complete','billowy','thumb','squeal','crib','discussion','even','stretch','mellow','angry','grouchy','absent','snow','middle','stingy','mourn','deep','honorable','nice','verdant','reach','lavish','sin','interest','whine','tug','vengeful','rhyme','stay','upset','hesitant','tent','wire','gold','momentous','yellow','cap','delicate','youthful','twig','burly','devilish','chess','wide','misty','useful','memorise','madly','plants','spectacular','accessible','collar','truck','harmony','uncovered','beef','low','channel','abusive','analyse','observant','snobbish','duck','excellent','intend','wreck','testy','care','shoes','charming','demonic','can','wipe','acidic','watch','decisive','brave','greet','imminent','influence','oranges','seal','eggs','knowledgeable','ashamed','shiny','inconclusive','remind','house','solid','quixotic','describe','support']; + const words = [ + 'belief', + 'scandalous', + 'flawless', + 'wrestle', + 'sort', + 'moldy', + 'carve', + 'incompetent', + 'cruel', + 'awful', + 'fang', + 'holistic', + 'makeshift', + 'synonymous', + 'questionable', + 'soft', + 'drop', + 'boot', + 'whimsical', + 'stir', + 'idea', + 'adhesive', + 'present', + 'hilarious', + 'unusual', + 'divergent', + 'probable', + 'depend', + 'suck', + 'belong', + 'advise', + 'straight', + 'encouraging', + 'wing', + 'clam', + 'serve', + 'fill', + 'nostalgic', + 'dysfunctional', + 'aggressive', + 'floor', + 'baby', + 'grease', + 'sisters', + 'print', + 'switch', + 'control', + 'victorious', + 'cracker', + 'dream', + 'wistful', + 'adaptable', + 'reminiscent', + 'inquisitive', + 'pushy', + 'unaccountable', + 'receive', + 'guttural', + 'two', + 'protect', + 'skin', + 'unbiased', + 'plastic', + 'loutish', + 'zip', + 'used', + 'divide', + 'communicate', + 'dear', + 'muddled', + 'dinosaurs', + 'grip', + 'trees', + 'well-off', + 'calendar', + 'chickens', + 'irate', + 'deranged', + 'trip', + 'stream', + 'white', + 'poison', + 'attack', + 'obtain', + 'theory', + 'laborer', + 'omniscient', + 'brake', + 'maniacal', + 'curvy', + 'smoke', + 'babies', + 'punch', + 'hammer', + 'toothbrush', + 'same', + 'crown', + 'jagged', + 'peep', + 'difficult', + 'reject', + 'merciful', + 'useless', + 'doctor', + 'mix', + 'wicked', + 'plant', + 'quickest', + 'roll', + 'suffer', + 'curly', + 'brother', + 'frighten', + 'cold', + 'tremendous', + 'move', + 'knot', + 'lame', + 'imaginary', + 'capricious', + 'raspy', + 'aunt', + 'loving', + 'wink', + 'wooden', + 'hop', + 'free', + 'drab', + 'fire', + 'instrument', + 'border', + 'frame', + 'silent', + 'glue', + 'decorate', + 'distance', + 'powerful', + 'pig', + 'admit', + 'fix', + 'pour', + 'flesh', + 'profuse', + 'skinny', + 'learn', + 'filthy', + 'dress', + 'bloody', + 'produce', + 'innocent', + 'meaty', + 'pray', + 'slimy', + 'sun', + 'kindhearted', + 'dime', + 'exclusive', + 'boast', + 'neat', + 'ruthless', + 'recess', + 'grieving', + 'daily', + 'hateful', + 'ignorant', + 'fence', + 'spring', + 'slim', + 'education', + 'overflow', + 'plastic', + 'gaping', + 'chew', + 'detect', + 'right', + 'lunch', + 'gainful', + 'argue', + 'cloistered', + 'horses', + 'orange', + 'shame', + 'bitter', + 'able', + 'sail', + 'magical', + 'exist', + 'force', + 'wheel', + 'best', + 'suit', + 'spurious', + 'partner', + 'request', + 'dog', + 'gusty', + 'money', + 'gaze', + 'lonely', + 'company', + 'pale', + 'tempt', + 'rat', + 'flame', + 'wobble', + 'superficial', + 'stop', + 'protective', + 'stare', + 'tongue', + 'heal', + 'railway', + 'idiotic', + 'roll', + 'puffy', + 'turn', + 'meeting', + 'new', + 'frightening', + 'sophisticated', + 'poke', + 'elderly', + 'room', + 'stimulating', + 'increase', + 'moor', + 'secret', + 'lean', + 'occur', + 'country', + 'damp', + 'evanescent', + 'alluring', + 'oafish', + 'join', + 'thundering', + 'cars', + 'awesome', + 'advice', + 'unruly', + 'ray', + 'wind', + 'anxious', + 'fly', + 'hammer', + 'adventurous', + 'shop', + 'cook', + 'trucks', + 'nonchalant', + 'addition', + 'base', + 'abashed', + 'excuse', + 'giants', + 'dramatic', + 'piquant', + 'coach', + 'possess', + 'poor', + 'finger', + 'wide-eyed', + 'aquatic', + 'welcome', + 'instruct', + 'expert', + 'evasive', + 'hug', + 'cute', + 'return', + 'mice', + 'damage', + 'turkey', + 'quiet', + 'bewildered', + 'tidy', + 'pointless', + 'outrageous', + 'medical', + 'foolish', + 'curve', + 'grandiose', + 'gullible', + 'hapless', + 'gleaming', + 'third', + 'grin', + 'pipe', + 'egg', + 'act', + 'physical', + 'eager', + 'side', + 'milk', + 'tearful', + 'fertile', + 'average', + 'glamorous', + 'strange', + 'yak', + 'terrific', + 'thin', + 'near', + 'snails', + 'flowery', + 'authority', + 'fish', + 'curious', + 'perpetual', + 'healthy', + 'health', + 'match', + 'fade', + 'chemical', + 'economic', + 'drawer', + 'avoid', + 'lying', + 'minister', + 'lick', + 'powder', + 'decay', + 'desire', + 'furry', + 'faint', + 'beam', + 'sordid', + 'fax', + 'tail', + 'bawdy', + 'cherry', + 'letter', + 'clover', + 'ladybug', + 'teeth', + 'behavior', + 'black', + 'amazing', + 'pink', + 'waste', + 'island', + 'forgetful', + 'needless', + 'lock', + 'waves', + 'boundary', + 'receipt', + 'handy', + 'religion', + 'hypnotic', + 'aftermath', + 'explain', + 'sense', + 'mundane', + 'rambunctious', + 'second', + 'preserve', + 'alarm', + 'dusty', + 'event', + 'blow', + 'weigh', + 'value', + 'glorious', + 'jail', + 'sigh', + 'cemetery', + 'serious', + 'yummy', + 'cattle', + 'understood', + 'limit', + 'alert', + 'fear', + 'lucky', + 'tested', + 'surround', + 'dolls', + 'pleasant', + 'disillusioned', + 'discover', + 'tray', + 'night', + 'seemly', + 'liquid', + 'worry', + 'pen', + 'bent', + 'gruesome', + 'war', + 'teeny-tiny', + 'common', + 'judge', + 'symptomatic', + 'bed', + 'trot', + 'unequaled', + 'flowers', + 'friends', + 'damaged', + 'peel', + 'skip', + 'show', + 'twist', + 'worthless', + 'brush', + 'look', + 'behave', + 'imperfect', + 'week', + 'petite', + 'direction', + 'soda', + 'lively', + 'coal', + 'coil', + 'release', + 'berserk', + 'books', + 'impossible', + 'replace', + 'cough', + 'chunky', + 'torpid', + 'discreet', + 'material', + 'bomb', + 'soothe', + 'crack', + 'hope', + 'license', + 'frightened', + 'breathe', + 'maddening', + 'calculator', + 'committee', + 'paltry', + 'green', + 'subsequent', + 'arrest', + 'gigantic', + 'tasty', + 'metal', + 'willing', + 'man', + 'stem', + 'nonstop', + 'route', + 'impulse', + 'government', + 'comfortable', + 'include', + 'literate', + 'multiply', + 'test', + 'vast', + 'exercise', + 'addicted', + 'agreeable', + 'lace', + 'toes', + 'young', + 'water', + 'end', + 'wash', + 'glossy', + 'round', + 'staking', + 'sink', + 'open', + 'spot', + 'trip', + 'fierce', + 'robust', + 'pastoral', + 'drown', + 'dress', + 'machine', + 'calculating', + 'holiday', + 'crabby', + 'disgusting', + 'plan', + 'sleet', + 'sleepy', + 'typical', + 'borrow', + 'possible', + 'curtain', + 'airplane', + 'industry', + 'nut', + 'rough', + 'wacky', + 'rock', + 'enormous', + 'uninterested', + 'sugar', + 'rake', + 'consist', + 'wrist', + 'basket', + 'chop', + 'wet', + 'street', + 'known', + 'settle', + 'bless', + 'cluttered', + 'wild', + 'expand', + 'angle', + 'snake', + 'yawn', + 'hate', + 'flood', + 'rabid', + 'spiteful', + 'anger', + 'market', + 'bizarre', + 'force', + 'majestic', + 'scissors', + 'beg', + 'rifle', + 'foregoing', + 'cactus', + 'funny', + 'eggnog', + 'wish', + 'high-pitched', + 'drop', + 'camp', + 'scarf', + 'car', + 'groan', + 'wonderful', + 'wealthy', + 'cup', + 'lock', + 'available', + 'previous', + 'jam', + 'political', + 'vacation', + 'three', + 'desk', + 'fry', + 'aspiring', + 'productive', + 'clear', + 'bored', + 'flashy', + 'plug', + 'precede', + 'abhorrent', + 'muddle', + 'flimsy', + 'paste', + 'need', + 'reward', + 'frail', + 'obnoxious', + 'creature', + 'whip', + 'unbecoming', + 'lake', + 'unused', + 'chin', + 'tour', + 'zephyr', + 'experience', + 'building', + 'scrub', + 'correct', + 'hover', + 'panicky', + 'scorch', + 'diligent', + 'hulking', + 'ubiquitous', + 'tedious', + 'aberrant', + 'file', + 'accidental', + 'mist', + 'blue-eyed', + 'trite', + 'nondescript', + 'cows', + 'wait', + 'test', + 'snotty', + 'amuck', + 'jump', + 'lackadaisical', + 'grey', + 'tawdry', + 'strong', + 'land', + 'kind', + 'star', + 'ludicrous', + 'stupid', + 'telling', + 'use', + 'bruise', + 'whirl', + 'cream', + 'harsh', + 'aboriginal', + 'substantial', + 'brawny', + 'tease', + 'pollution', + 'weather', + 'degree', + 'dry', + 'film', + 'obey', + 'closed', + 'dependent', + 'want', + 'undesirable', + 'stamp', + 'relax', + 'foot', + 'obscene', + 'successful', + 'wriggle', + 'drain', + 'greasy', + 'escape', + 'cross', + 'odd', + 'boring', + 'absorbed', + 'houses', + 'suppose', + 'suit', + 'moon', + 'ceaseless', + 'explode', + 'clap', + 'pop', + 'courageous', + 'miss', + 'notebook', + 'delirious', + 'form', + 'pretty', + 'sock', + 'grotesque', + 'noxious', + 'record', + 'stop', + 'saw', + 'thing', + 'dislike', + 'cloth', + 'six', + 'jar', + 'unnatural', + 'spiffy', + 'itchy', + 'secretary', + 'move', + 'certain', + 'unkempt', + 'sassy', + 'queue', + 'shrug', + 'crow', + 'heavenly', + 'desert', + 'screw', + 'vessel', + 'mug', + 'encourage', + 'icy', + 'enthusiastic', + 'throat', + 'whistle', + 'ignore', + 'miniature', + 'squeak', + 'scarecrow', + 'fluttering', + 'hang', + 'icicle', + 'lie', + 'juicy', + 'empty', + 'baseball', + 'various', + 'promise', + 'abortive', + 'descriptive', + 'high', + 'spy', + 'faded', + 'talk', + 'air', + 'messup', + 'decorous', + 'sneaky', + 'mark', + 'sack', + 'ultra', + 'chivalrous', + 'lethal', + 'expect', + 'disgusted', + 'reaction', + 'fireman', + 'private', + 'ritzy', + 'manage', + 'actor', + 'rely', + 'uppity', + 'thread', + 'bat', + 'space', + 'underwear', + 'blood', + 'nine', + 'maid', + 'shelf', + 'hanging', + 'shop', + 'prick', + 'wound', + 'sloppy', + 'offer', + 'increase', + 'clear', + 'slap', + 'rude', + 'poised', + 'wretched', + 'cause', + 'quince', + 'tame', + 'remarkable', + 'abject', + 'sail', + 'guide', + 'subdued', + 'spiky', + 'debonair', + 'chicken', + 'tired', + 'hum', + 'land', + 'scared', + 'splendid', + 'guess', + 'cast', + 'rub', + 'magnificent', + 'ants', + 'overwrought', + 'interfere', + 'gorgeous', + 'office', + 'trade', + 'sniff', + 'melted', + 'bore', + 'point', + 'pet', + 'purple', + 'brake', + 'flavor', + 'toe', + 'prickly', + 'zinc', + 'homely', + 'modern', + 'kindly', + 'whisper', + 'bare', + 'annoyed', + 'glass', + 'noisy', + 'null', + 'thoughtless', + 'skirt', + 'dock', + 'rings', + 'mind', + 'neck', + 'macho', + 'wave', + 'history', + 'play', + 'road', + 'profit', + 'word', + 'opposite', + 'dreary', + 'governor', + 'horse', + 'trust', + 'elbow', + 'kiss', + 'crayon', + 'stitch', + 'excited', + 'needy', + 'arrange', + 'easy', + 'alcoholic', + 'safe', + 'lumpy', + 'monkey', + 'smile', + 'capable', + 'untidy', + 'extra-small', + 'memory', + 'selective', + 'reproduce', + 'old-fashioned', + 'overrated', + 'texture', + 'knit', + 'downtown', + 'risk', + 'pot', + 'sofa', + 'righteous', + 'wren', + 'pull', + 'carry', + 'aboard', + 'listen', + 'classy', + 'thank', + 'shocking', + 'condition', + 'root', + 'attempt', + 'swim', + 'frog', + 'hurt', + 'army', + 'title', + 'handsomely', + 'town', + 'guiltless', + 'thaw', + 'spell', + 'selfish', + 'disturbed', + 'tramp', + 'girls', + 'utopian', + 'noiseless', + 'trail', + 'bashful', + 'business', + 'rhetorical', + 'snail', + 'sign', + 'plausible', + 'left', + 'design', + 'tall', + 'violent', + 'wasteful', + 'beautiful', + 'breezy', + 'tap', + 'murder', + 'talented', + 'needle', + 'creator', + 'imagine', + 'flippant', + 'dead', + 'bone', + 'coherent', + 'relation', + 'aromatic', + 'mountainous', + 'face', + 'ask', + 'picture', + 'pedal', + 'colour', + 'obese', + 'group', + 'top', + 'bubble', + 'pinch', + 'optimal', + 'school', + 'bathe', + 'flagrant', + 'check', + 'deliver', + 'pass', + 'tan', + 'crate', + 'hose', + 'debt', + 'faulty', + 'longing', + 'hollow', + 'invincible', + 'afford', + 'lovely', + 'ticket', + 'changeable', + 'subtract', + 'fumbling', + 'responsible', + 'confused', + 'woman', + 'touch', + 'watch', + 'zesty', + 'library', + 'jail', + 'wrap', + 'terrify', + 'brick', + 'popcorn', + 'cooperative', + 'peck', + 'pocket', + 'property', + 'buzz', + 'tiresome', + 'digestion', + 'exciting', + 'nation', + 'juvenile', + 'shade', + 'copper', + 'wanting', + 'deer', + 'waste', + 'man', + 'join', + 'spotty', + 'amused', + 'mountain', + 'waggish', + 'bushes', + 'tense', + 'river', + 'heartbreaking', + 'help', + 'mine', + 'narrow', + 'smash', + 'scrawny', + 'tame', + 'rain', + 'playground', + 'airport', + 'astonishing', + 'level', + 'befitting', + 'animal', + 'heat', + 'painful', + 'cellar', + 'ski', + 'sedate', + 'knowing', + 'vigorous', + 'change', + 'eight', + 'ship', + 'work', + 'strip', + 'robin', + 'tank', + 'challenge', + 'vacuous', + 'representative', + 'regret', + 'tightfisted', + 'erratic', + 'club', + 'imported', + 'therapeutic', + 'rainstorm', + 'luxuriant', + 'relieved', + 'day', + 'system', + 'apologise', + 'male', + 'prepare', + 'malicious', + 'naive', + 'whistle', + 'curl', + 'hobbies', + 'trousers', + 'stereotyped', + 'dad', + 'endurable', + 'grass', + 'hot', + 'bomb', + 'morning', + 'guide', + 'keen', + 'plot', + 'accept', + 'disastrous', + 'macabre', + 'year', + 'spicy', + 'absorbing', + 'sticks', + 'efficient', + 'drain', + 'warm', + 'rice', + 'utter', + 'fact', + 'marked', + 'ratty', + 'chalk', + 'towering', + 'treat', + 'nest', + 'annoy', + 'jealous', + 'stamp', + 'effect', + 'cautious', + 'jelly', + 'feigned', + 'gabby', + 'corn', + 'volleyball', + 'pan', + 'psychedelic', + 'fairies', + 'silent', + 'zonked', + 'bump', + 'trouble', + 'mass', + 'queen', + 'things', + 'bury', + 'sister', + 'quiet', + 'colossal', + 'puncture', + 'four', + 'attend', + 'love', + 'wiry', + 'vegetable', + 'destruction', + 'note', + 'pies', + 'resolute', + 'load', + 'fancy', + 'tacky', + 'periodic', + 'abandoned', + 'vivacious', + 'blush', + 'wrathful', + 'miscreant', + 'call', + 'striped', + 'wiggly', + 'supreme', + 'hand', + 'impolite', + 'rule', + 'deserted', + 'concern', + 'cover', + 'harbor', + 'waiting', + 'soggy', + 'psychotic', + 'ancient', + 'sponge', + 'domineering', + 'elegant', + 'impartial', + 'unlock', + 'abrasive', + 'count', + 'flight', + 'neighborly', + 'roof', + 'bulb', + 'auspicious', + 'automatic', + 'magic', + 'sign', + 'amusing', + 'orange', + 'branch', + 'sulky', + 'attack', + 'fetch', + 'number', + 'jellyfish', + 'start', + 'alike', + 'touch', + 'sour', + 'wary', + 'minor', + 'punish', + 'connect', + 'protest', + 'pie', + 'kaput', + 'doubtful', + 'friendly', + 'simplistic', + 'smart', + 'vanish', + 'applaud', + 'jumbled', + 'ready', + 'yell', + 'support', + 'squash', + 'raise', + 'parallel', + 'super', + 'jazzy', + 'crush', + 'apathetic', + 'water', + 'food', + 'thrill', + 'permit', + 'heady', + 'last', + 'mine', + 'signal', + 'smoke', + 'preach', + 'x-ray', + 'name', + 'birth', + 'minute', + 'steel', + 'bedroom', + 'female', + 'acrid', + 'riddle', + 'attractive', + 'earth', + 'crack', + 'muscle', + 'alive', + 'guarded', + 'sweater', + 'donkey', + 'doubt', + 'lettuce', + 'magenta', + 'live', + 'farm', + 'glib', + 'bow', + 'fascinated', + 'friend', + 'practise', + 'remember', + 'bleach', + 'hungry', + 'voiceless', + 'pin', + 'sparkling', + 'report', + 'arm', + 'sad', + 'shaggy', + 'parcel', + 'wail', + 'flash', + 'territory', + 'functional', + 'wise', + 'screeching', + 'appliance', + 'future', + 'appear', + 'team', + 'rabbit', + 'porter', + 'paint', + 'flat', + 'amusement', + 'ocean', + 'head', + 'geese', + 'wash', + 'embarrassed', + 'tub', + 'boundless', + 'freezing', + 'mushy', + 'surprise', + 'temporary', + 'marble', + 'recondite', + 'telephone', + 'zipper', + 'pine', + 'reign', + 'pump', + 'tangy', + 'reply', + 'toys', + 'purpose', + 'songs', + 'form', + 'delicious', + 'wood', + 'horn', + 'nutty', + 'fruit', + 'lumber', + 'potato', + 'cheat', + 'cloudy', + 'visit', + 'reduce', + 'destroy', + 'deafening', + 'full', + 'warlike', + 'mitten', + 'cover', + 'earthy', + 'seashore', + 'yarn', + 'tenuous', + 'pause', + 'boil', + 'dogs', + 'tough', + 'knife', + 'shy', + 'naughty', + 'existence', + 'fire', + 'eminent', + 'remove', + 'juice', + 'sleep', + 'voyage', + 'balance', + 'unsightly', + 'plough', + 'ill-fated', + 'pumped', + 'motionless', + 'allow', + 'trade', + 'warm', + 'toad', + 'wave', + 'wall', + 'pigs', + 'circle', + 'rejoice', + 'ear', + 'drink', + 'found', + 'taboo', + 'object', + 'old', + 'temper', + 'plant', + 'public', + 'picayune', + 'blot', + 'delight', + 'carpenter', + 'dispensable', + 'tire', + 'cow', + 'furniture', + 'rightful', + 'mute', + 'gentle', + 'gifted', + 'ragged', + 'stiff', + 'retire', + 'compare', + 'sable', + 'hole', + 'judicious', + 'chilly', + 'sparkle', + 'futuristic', + 'love', + 'bubble', + 'travel', + 'name', + 'numberless', + 'succeed', + 'acoustic', + 'lowly', + 'society', + 'injure', + 'agree', + 'reason', + 'party', + 'wool', + 'careful', + 'hook', + 'bell', + 'ball', + 'attach', + 'scream', + 'development', + 'happy', + 'appreciate', + 'disagree', + 'request', + 'march', + 'rampant', + 'scrape', + 'sack', + 'hair', + 'measure', + 'owe', + 'grubby', + 'vein', + 'boy', + 'punishment', + 'smoggy', + 'wry', + 'immense', + 'shoe', + 'pack', + 'brash', + 'cave', + 'sincere', + 'adorable', + 'fantastic', + 'attraction', + 'racial', + 'jittery', + 'defiant', + 'honey', + 'paper', + 'weight', + 'bee', + 'blind', + 'birthday', + 'toothsome', + 'trick', + 'guard', + 'fog', + 'handle', + 'dirty', + 'salt', + 'rinse', + 'nippy', + 'observe', + 'suggestion', + 'weak', + 'instinctive', + 'frequent', + 'detail', + 'verse', + 'quirky', + 'scattered', + 'toy', + 'aware', + 'distribution', + 'repulsive', + 'draconian', + 'bucket', + 'harm', + 'radiate', + 'bang', + 'shrill', + 'living', + 'rhythm', + 'obsequious', + 'drum', + 'inject', + 'skate', + 'beds', + 'smash', + 'order', + 'stitch', + 'ground', + 'nosy', + 'kick', + 'dusty', + 'home', + 'rot', + 'frame', + 'jam', + 'sky', + 'soap', + 'rescue', + 'energetic', + 'grape', + 'massive', + 'deeply', + 'dazzling', + 'park', + 'pull', + 'number', + 'abundant', + 'barbarous', + 'drag', + 'ajar', + 'close', + 'moan', + 'haircut', + 'shade', + 'married', + 'cats', + 'thirsty', + 'dirt', + 'vagabond', + 'fearful', + 'squealing', + 'squalid', + 'zebra', + 'murky', + 'sheet', + 'fat', + 'follow', + 'bikes', + 'unpack', + 'materialistic', + 'surprise', + 'arch', + 'selection', + 'acoustics', + 'helpless', + 'thoughtful', + 'cry', + 'quarrelsome', + 'arrogant', + 'illegal', + 'sudden', + 'elite', + 'tomatoes', + 'spoil', + 'flower', + 'shivering', + 'front', + 'caption', + 'volcano', + 'ugliest', + 'ambitious', + 'pickle', + 'interrupt', + 'nervous', + 'approve', + 'messy', + 'dust', + 'oceanic', + 'brass', + 'tremble', + 'fine', + 'nerve', + 'lunchroom', + 'hard', + 'engine', + 'erect', + 'flower', + 'cynical', + 'irritating', + 'tight', + 'cobweb', + 'gray', + 'invention', + 'snatch', + 'account', + 'sharp', + 'spooky', + 'squeamish', + 'eatable', + 'share', + 'need', + 'moaning', + 'suspect', + 'rush', + 'rural', + 'false', + 'float', + 'bite', + 'careless', + 'sidewalk', + 'cowardly', + 'stroke', + 'educated', + 'ugly', + 'type', + 'wandering', + 'bolt', + 'mint', + 'fit', + 'large', + 'extra-large', + 'defeated', + 'kitty', + 'tacit', + 'abiding', + 'grandfather', + 'trains', + 'lamp', + 'habitual', + 'fast', + 'offbeat', + 'accurate', + 'many', + 'fortunate', + 'lyrical', + 'charge', + 'illustrious', + 'transport', + 'wakeful', + 'cable', + 'ordinary', + 'string', + 'question', + 'train', + 'fancy', + 'kick', + 'enchanting', + 'jobless', + 'ahead', + 'comparison', + 'loose', + 'dance', + 'add', + 'wonder', + 'stale', + 'earn', + 'reflective', + 'bright', + 'true', + 'statuesque', + 'amount', + 'matter', + 'repair', + 'care', + 'ruin', + 'terrible', + 'elastic', + 'spiders', + 'craven', + 'lamentable', + 'decision', + 'swing', + 'connection', + 'gaudy', + 'knowledge', + 'cheap', + 'lazy', + 'step', + 'dinner', + 'rod', + 'agreement', + 'comb', + 'mean', + 'past', + 'knotty', + 'busy', + 'quicksand', + 'match', + 'early', + 'long', + 'onerous', + 'ambiguous', + 'worried', + 'spade', + 'happen', + 'crook', + 'dapper', + 'grate', + 'announce', + 'plate', + 'haunt', + 'friction', + 'actually', + 'chance', + 'example', + 'rapid', + 'zealous', + 'necessary', + 'ink', + 'mere', + 'shock', + 'huge', + 'jaded', + 'spill', + 'store', + 'fuzzy', + 'table', + 'bottle', + 'halting', + 'spark', + 'end', + 'remain', + 'transport', + 'seat', + 'leg', + 'long-term', + 'clip', + 'grumpy', + 'shake', + 'walk', + 'try', + 'action', + 'soup', + 'short', + 'hurry', + 'square', + 'belligerent', + 'thankful', + 'beginner', + 'small', + 'bumpy', + 'silly', + 'badge', + 'marvelous', + 'wealth', + 'open', + 'unequal', + 'scatter', + 'pest', + 'fool', + 'step', + 'groovy', + 'childlike', + 'door', + 'bouncy', + 'believe', + 'incredible', + 'box', + 'unhealthy', + 'swanky', + 'abrupt', + 'depressed', + 'flaky', + 'famous', + 'detailed', + 'regret', + 'envious', + 'natural', + 'apparel', + 'spare', + 'mark', + 'ten', + 'power', + 'glistening', + 'arrive', + 'animated', + 'slip', + 'heap', + 'shaky', + 'unfasten', + 'contain', + 'inexpensive', + 'introduce', + 'shallow', + 'rule', + 'gather', + 'pump', + 'humorous', + 'acceptable', + 'womanly', + 'giddy', + 'silk', + 'yoke', + 'straw', + 'invite', + 'one', + 'red', + 'growth', + 'unadvised', + 'measly', + 'flap', + 'puzzled', + 'regular', + 'painstaking', + 'little', + 'plain', + 'tumble', + 'rest', + 'fabulous', + 'melt', + 'label', + 'truculent', + 'internal', + 'passenger', + 'zippy', + 'bright', + 'earsplitting', + 'tooth', + 'veil', + 'grip', + 'square', + 'stuff', + 'gate', + 'level', + 'stone', + 'observation', + 'time', + 'workable', + 'bird', + 'realise', + 'spotted', + 'coast', + 'quiver', + 'rebel', + 'entertain', + 'rotten', + 'loss', + 'collect', + 'meal', + 'satisfy', + 'military', + 'bake', + 'cagey', + 'redundant', + 'treatment', + 'knock', + 'blink', + 'scale', + 'board', + 'fair', + 'nebulous', + 'sip', + 'homeless', + 'place', + 'complain', + 'joke', + 'bat', + 'winter', + 'choke', + 'frantic', + 'chubby', + 'highfalutin', + 'troubled', + 'whole', + 'rose', + 'delightful', + 'loaf', + 'afraid', + 'sturdy', + 'class', + 'cultured', + 'yielding', + 'broken', + 'kittens', + 'absurd', + 'discovery', + 'next', + 'disarm', + 'dangerous', + 'lively', + 'reflect', + 'chief', + 'teeny', + 'pencil', + 'ban', + 'grade', + 'size', + 'dashing', + 'thought', + 'breath', + 'empty', + 'hellish', + 'shock', + 'sea', + 'weary', + 'payment', + 'limping', + 'premium', + 'grateful', + 'somber', + 'tax', + 'coach', + 'vulgar', + 'stretch', + 'tow', + 'branch', + 'insurance', + 'yam', + 'stormy', + 'wish', + 'snow', + 'cute', + 'milky', + 'battle', + 'far', + 'roasted', + 'slip', + 'adamant', + 'awake', + 'employ', + 'tangible', + 'tickle', + 'jog', + 'hysterical', + 'meddle', + 'parsimonious', + 'judge', + 'educate', + 'respect', + 'sound', + 'oven', + 'gratis', + 'station', + 'train', + 'purring', + 'steady', + 'carriage', + 'humdrum', + 'voracious', + 'jolly', + 'brainy', + 'proud', + 'elfin', + 'cure', + 'motion', + 'record', + 'quizzical', + 'pail', + 'bike', + 'faithful', + 'approval', + 'vague', + 'fall', + 'store', + 'normal', + 'rock', + 'bear', + 'bounce', + 'giant', + 'satisfying', + 'crooked', + 'lopsided', + 'vest', + 'separate', + 'sneeze', + 'teaching', + 'general', + 'meat', + 'festive', + 'historical', + 'line', + 'north', + 'tip', + 'son', + 'damaging', + 'nimble', + 'broad', + 'list', + 'confuse', + 'first', + 'deserve', + 'steep', + 'last', + 'rich', + 'oval', + 'thick', + 'glow', + 'great', + 'clammy', + 'cheer', + 'untidy', + 'scientific', + 'noise', + 'stomach', + 'undress', + 'big', + 'bite-sized', + 'enter', + 'cake', + 'aloof', + 'abnormal', + 'month', + 'grab', + 'well-groomed', + 'silver', + 'art', + 'berry', + 'giraffe', + 'complete', + 'billowy', + 'thumb', + 'squeal', + 'crib', + 'discussion', + 'even', + 'stretch', + 'mellow', + 'angry', + 'grouchy', + 'absent', + 'snow', + 'middle', + 'stingy', + 'mourn', + 'deep', + 'honorable', + 'nice', + 'verdant', + 'reach', + 'lavish', + 'sin', + 'interest', + 'whine', + 'tug', + 'vengeful', + 'rhyme', + 'stay', + 'upset', + 'hesitant', + 'tent', + 'wire', + 'gold', + 'momentous', + 'yellow', + 'cap', + 'delicate', + 'youthful', + 'twig', + 'burly', + 'devilish', + 'chess', + 'wide', + 'misty', + 'useful', + 'memorise', + 'madly', + 'plants', + 'spectacular', + 'accessible', + 'collar', + 'truck', + 'harmony', + 'uncovered', + 'beef', + 'low', + 'channel', + 'abusive', + 'analyse', + 'observant', + 'snobbish', + 'duck', + 'excellent', + 'intend', + 'wreck', + 'testy', + 'care', + 'shoes', + 'charming', + 'demonic', + 'can', + 'wipe', + 'acidic', + 'watch', + 'decisive', + 'brave', + 'greet', + 'imminent', + 'influence', + 'oranges', + 'seal', + 'eggs', + 'knowledgeable', + 'ashamed', + 'shiny', + 'inconclusive', + 'remind', + 'house', + 'solid', + 'quixotic', + 'describe', + 'support', + ]; return randomElement(words); } @@ -123,48 +2128,64 @@ async function execRandomCommand(client) { let possibleCommands = [ ['mkbook {word}', 40], // CREATE FOLDER ['mknote {word}', 70], // CREATE NOTE - [async () => { // DELETE RANDOM ITEM - let items = await clientItems(client); - let item = randomElement(items); - if (!item) return; + [ + async () => { + // DELETE RANDOM ITEM + let items = await clientItems(client); + let item = randomElement(items); + if (!item) return; - if (item.type_ == 1) { - return execCommand(client, 'rm -f ' + item.id); - } else if (item.type_ == 2) { - return execCommand(client, 'rm -r -f ' + item.id); - } else if (item.type_ == 5) { - // tag - } else { - throw new Error('Unknown type: ' + item.type_); - } - }, 30], - [async () => { // SYNC - let avgSyncDuration = averageSyncDuration(); - let options = {}; - if (!isNaN(avgSyncDuration)) { - if (Math.random() >= 0.5) { - options.killAfter = avgSyncDuration * Math.random(); + if (item.type_ == 1) { + return execCommand(client, 'rm -f ' + item.id); + } else if (item.type_ == 2) { + return execCommand(client, 'rm -r -f ' + item.id); + } else if (item.type_ == 5) { + // tag + } else { + throw new Error('Unknown type: ' + item.type_); } - } - return execCommand(client, 'sync --random-failures', options); - }, 30], - [async () => { // UPDATE RANDOM ITEM - let items = await clientItems(client); - let item = randomNote(items); - if (!item) return; + }, + 30, + ], + [ + async () => { + // SYNC + let avgSyncDuration = averageSyncDuration(); + let options = {}; + if (!isNaN(avgSyncDuration)) { + if (Math.random() >= 0.5) { + options.killAfter = avgSyncDuration * Math.random(); + } + } + return execCommand(client, 'sync --random-failures', options); + }, + 30, + ], + [ + async () => { + // UPDATE RANDOM ITEM + let items = await clientItems(client); + let item = randomNote(items); + if (!item) return; - return execCommand(client, 'set ' + item.id + ' title "' + randomWord() + '"'); - }, 50], - [async () => { // ADD TAG - let items = await clientItems(client); - let note = randomNote(items); - if (!note) return; + return execCommand(client, 'set ' + item.id + ' title "' + randomWord() + '"'); + }, + 50, + ], + [ + async () => { + // ADD TAG + let items = await clientItems(client); + let note = randomNote(items); + if (!note) return; - let tag = randomTag(items); - let tagTitle = !tag || Math.random() >= 0.9 ? 'tag-' + randomWord() : tag.title; - - return execCommand(client, 'tag add ' + tagTitle + ' ' + note.id); - }, 50], + let tag = randomTag(items); + let tagTitle = !tag || Math.random() >= 0.9 ? 'tag-' + randomWord() : tag.title; + + return execCommand(client, 'tag add ' + tagTitle + ' ' + note.id); + }, + 50, + ], ]; let cmd = null; @@ -190,12 +2211,12 @@ function averageSyncDuration() { function randomNextCheckTime() { let output = time.unixMs() + 1000 + Math.random() * 1000 * 120; - logger.info('Next sync check: ' + time.unixMsToIso(output) + ' (' + (Math.round((output - time.unixMs()) / 1000)) + ' sec.)'); + logger.info('Next sync check: ' + time.unixMsToIso(output) + ' (' + Math.round((output - time.unixMs()) / 1000) + ' sec.)'); return output; } function findItem(items, itemId) { - for (let i = 0; i < items.length; i++) { + for (let i = 0; i < items.length; i++) { if (items[i].id == itemId) return items[i]; } return null; @@ -244,10 +2265,7 @@ function findMissingItems_(items1, items2) { } function findMissingItems(items1, items2) { - return [ - findMissingItems_(items1, items2), - findMissingItems_(items2, items1), - ]; + return [findMissingItems_(items1, items2), findMissingItems_(items2, items1)]; } async function compareClientItems(clientItems) { @@ -257,7 +2275,7 @@ async function compareClientItems(clientItems) { itemCounts.push(items.length); } logger.info('Item count: ' + itemCounts.join(', ')); - + let missingItems = findMissingItems(clientItems[0], clientItems[1]); if (missingItems[0].length || missingItems[1].length) { logger.error('Items are different'); @@ -296,9 +2314,8 @@ async function compareClientItems(clientItems) { async function main(argv) { await fs.remove(syncDir); - + let clients = await createClients(); - let activeCommandCounts = []; let clientId = 0; for (let i = 0; i < clients.length; i++) { @@ -310,15 +2327,17 @@ async function main(argv) { clients[clientId].activeCommandCount++; - execRandomCommand(clients[clientId]).catch((error) => { - logger.info('Client ' + clientId + ':'); - logger.error(error); - }).then((r) => { - if (r) { - logger.info('Client ' + clientId + ":\n" + r.trim()) - } - clients[clientId].activeCommandCount--; - }); + execRandomCommand(clients[clientId]) + .catch(error => { + logger.info('Client ' + clientId + ':'); + logger.error(error); + }) + .then(r => { + if (r) { + logger.info('Client ' + clientId + ':\n' + r.trim()); + } + clients[clientId].activeCommandCount--; + }); } let nextSyncCheckTime = randomNextCheckTime(); @@ -351,6 +2370,7 @@ async function main(argv) { await compareClientItems(clientItems); nextSyncCheckTime = randomNextCheckTime(); + // eslint-disable-next-line require-atomic-updates state = 'commands'; return; } @@ -377,6 +2397,6 @@ async function main(argv) { }, 100); } -main(process.argv).catch((error) => { +main(process.argv).catch(error => { logger.error(error); -}); \ No newline at end of file +}); diff --git a/CliClient/app/gui/ConsoleWidget.js b/CliClient/app/gui/ConsoleWidget.js index e6bc30bd1..5cf6e5f9d 100644 --- a/CliClient/app/gui/ConsoleWidget.js +++ b/CliClient/app/gui/ConsoleWidget.js @@ -1,7 +1,6 @@ const TextWidget = require('tkwidgets/TextWidget.js'); class ConsoleWidget extends TextWidget { - constructor() { super(); this.lines_ = []; @@ -16,7 +15,7 @@ class ConsoleWidget extends TextWidget { } get lastLine() { - return this.lines_.length ? this.lines_[this.lines_.length-1] : ''; + return this.lines_.length ? this.lines_[this.lines_.length - 1] : ''; } addLine(line) { @@ -40,13 +39,12 @@ class ConsoleWidget extends TextWidget { if (this.lines_.length > this.maxLines_) { this.lines_.splice(0, this.lines_.length - this.maxLines_); } - this.text = this.lines_.join("\n"); + this.text = this.lines_.join('\n'); this.updateText_ = false; } super.render(); } - } -module.exports = ConsoleWidget; \ No newline at end of file +module.exports = ConsoleWidget; diff --git a/CliClient/app/gui/FolderListWidget.js b/CliClient/app/gui/FolderListWidget.js index ad1f43eaa..10f709191 100644 --- a/CliClient/app/gui/FolderListWidget.js +++ b/CliClient/app/gui/FolderListWidget.js @@ -5,7 +5,6 @@ const ListWidget = require('tkwidgets/ListWidget.js'); const _ = require('lib/locale.js')._; class FolderListWidget extends ListWidget { - constructor() { super(); @@ -20,7 +19,7 @@ class FolderListWidget extends ListWidget { this.updateItems_ = false; this.trimItemTitle = false; - this.itemRenderer = (item) => { + this.itemRenderer = item => { let output = []; if (item === '-') { output.push('-'.repeat(this.innerWidth)); @@ -32,7 +31,7 @@ class FolderListWidget extends ListWidget { output.push(_('Search:')); output.push(item.title); } - + return output.join(' '); }; } @@ -45,7 +44,6 @@ class FolderListWidget extends ListWidget { output++; folderId = folder.parent_id; } - throw new Error('unreachable'); } get selectedFolderId() { @@ -54,7 +52,7 @@ class FolderListWidget extends ListWidget { set selectedFolderId(v) { this.selectedFolderId_ = v; - this.updateIndexFromSelectedItemId() + this.updateIndexFromSelectedItemId(); this.invalidate(); } @@ -64,7 +62,7 @@ class FolderListWidget extends ListWidget { set selectedSearchId(v) { this.selectedSearchId_ = v; - this.updateIndexFromSelectedItemId() + this.updateIndexFromSelectedItemId(); this.invalidate(); } @@ -74,7 +72,7 @@ class FolderListWidget extends ListWidget { set selectedTagId(v) { this.selectedTagId_ = v; - this.updateIndexFromSelectedItemId() + this.updateIndexFromSelectedItemId(); this.invalidate(); } @@ -84,7 +82,7 @@ class FolderListWidget extends ListWidget { set notesParentType(v) { this.notesParentType_ = v; - this.updateIndexFromSelectedItemId() + this.updateIndexFromSelectedItemId(); this.invalidate(); } @@ -95,7 +93,7 @@ class FolderListWidget extends ListWidget { set searches(v) { this.searches_ = v; this.updateItems_ = true; - this.updateIndexFromSelectedItemId() + this.updateIndexFromSelectedItemId(); this.invalidate(); } @@ -106,7 +104,7 @@ class FolderListWidget extends ListWidget { set tags(v) { this.tags_ = v; this.updateItems_ = true; - this.updateIndexFromSelectedItemId() + this.updateIndexFromSelectedItemId(); this.invalidate(); } @@ -117,7 +115,7 @@ class FolderListWidget extends ListWidget { set folders(v) { this.folders_ = v; this.updateItems_ = true; - this.updateIndexFromSelectedItemId() + this.updateIndexFromSelectedItemId(); this.invalidate(); } @@ -128,7 +126,7 @@ class FolderListWidget extends ListWidget { } return false; } - + render() { if (this.updateItems_) { this.logger().debug('Rebuilding items...', this.notesParentType, this.selectedJoplinItemId, this.selectedSearchId); @@ -136,7 +134,7 @@ class FolderListWidget extends ListWidget { const previousParentType = this.notesParentType; let newItems = []; - const orderFolders = (parentId) => { + const orderFolders = parentId => { for (let i = 0; i < this.folders.length; i++) { const f = this.folders[i]; const folderParentId = f.parent_id ? f.parent_id : ''; @@ -145,7 +143,7 @@ class FolderListWidget extends ListWidget { if (this.folderHasChildren_(this.folders, f.id)) orderFolders(f.id); } } - } + }; orderFolders(''); @@ -162,7 +160,7 @@ class FolderListWidget extends ListWidget { this.items = newItems; this.notesParentType = previousParentType; - this.updateIndexFromSelectedItemId(wasSelectedItemId) + this.updateIndexFromSelectedItemId(wasSelectedItemId); this.updateItems_ = false; } @@ -188,7 +186,6 @@ class FolderListWidget extends ListWidget { const index = this.itemIndexByKey('id', itemId); this.currentIndex = index >= 0 ? index : 0; } - } -module.exports = FolderListWidget; \ No newline at end of file +module.exports = FolderListWidget; diff --git a/CliClient/app/gui/NoteListWidget.js b/CliClient/app/gui/NoteListWidget.js index 30d44df44..34a3029eb 100644 --- a/CliClient/app/gui/NoteListWidget.js +++ b/CliClient/app/gui/NoteListWidget.js @@ -2,14 +2,13 @@ const Note = require('lib/models/Note.js'); const ListWidget = require('tkwidgets/ListWidget.js'); class NoteListWidget extends ListWidget { - constructor() { super(); this.selectedNoteId_ = 0; this.updateIndexFromSelectedNoteId_ = false; - this.itemRenderer = (note) => { + this.itemRenderer = note => { let label = Note.displayTitle(note); // + ' ' + note.id; if (note.is_todo) { label = '[' + (note.todo_completed ? 'X' : ' ') + '] ' + label; @@ -32,7 +31,6 @@ class NoteListWidget extends ListWidget { super.render(); } - } -module.exports = NoteListWidget; \ No newline at end of file +module.exports = NoteListWidget; diff --git a/CliClient/app/gui/NoteMetadataWidget.js b/CliClient/app/gui/NoteMetadataWidget.js index ff68e189d..1ba0702e3 100644 --- a/CliClient/app/gui/NoteMetadataWidget.js +++ b/CliClient/app/gui/NoteMetadataWidget.js @@ -2,7 +2,6 @@ const Note = require('lib/models/Note.js'); const TextWidget = require('tkwidgets/TextWidget.js'); class NoteMetadataWidget extends TextWidget { - constructor() { super(); this.noteId_ = 0; @@ -30,7 +29,6 @@ class NoteMetadataWidget extends TextWidget { this.text = this.note_ ? await Note.minimalSerializeForDisplay(this.note_) : ''; } } - } -module.exports = NoteMetadataWidget; \ No newline at end of file +module.exports = NoteMetadataWidget; diff --git a/CliClient/app/gui/NoteWidget.js b/CliClient/app/gui/NoteWidget.js index d0a29538e..fe2b1f3ba 100644 --- a/CliClient/app/gui/NoteWidget.js +++ b/CliClient/app/gui/NoteWidget.js @@ -3,7 +3,6 @@ const TextWidget = require('tkwidgets/TextWidget.js'); const { _ } = require('lib/locale.js'); class NoteWidget extends TextWidget { - constructor() { super(); this.noteId_ = 0; @@ -44,11 +43,11 @@ class NoteWidget extends TextWidget { } else if (this.noteId_) { this.doAsync('loadNote', async () => { this.note_ = await Note.load(this.noteId_); - + if (this.note_ && this.note_.encryption_applied) { this.text = _('One or more items are currently encrypted and you may need to supply a master password. To do so please type `e2ee decrypt`. If you have already supplied the password, the encrypted items are being decrypted in the background and will be available soon.'); } else { - this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : ''; + this.text = this.note_ ? this.note_.title + '\n\n' + this.note_.body : ''; } if (this.lastLoadedNoteId_ !== this.noteId_) this.scrollTop = 0; @@ -59,7 +58,6 @@ class NoteWidget extends TextWidget { this.scrollTop = 0; } } - } -module.exports = NoteWidget; \ No newline at end of file +module.exports = NoteWidget; diff --git a/CliClient/app/gui/StatusBarWidget.js b/CliClient/app/gui/StatusBarWidget.js index ae911da77..a676f76c6 100644 --- a/CliClient/app/gui/StatusBarWidget.js +++ b/CliClient/app/gui/StatusBarWidget.js @@ -5,7 +5,6 @@ const stripAnsi = require('strip-ansi'); const { handleAutocompletion } = require('../autocompletion.js'); class StatusBarWidget extends BaseWidget { - constructor() { super(); @@ -75,7 +74,7 @@ class StatusBarWidget extends BaseWidget { super.render(); const doSaveCursor = !this.promptActive; - + if (doSaveCursor) this.term.saveCursor(); this.innerClear(); @@ -87,14 +86,13 @@ class StatusBarWidget extends BaseWidget { //const textStyle = this.promptActive ? (s) => s : chalk.bgBlueBright.white; //const textStyle = (s) => s; - const textStyle = this.promptActive ? (s) => s : chalk.gray; + const textStyle = this.promptActive ? s => s : chalk.gray; this.term.drawHLine(this.absoluteInnerX, this.absoluteInnerY, this.innerWidth, textStyle(' ')); this.term.moveTo(this.absoluteInnerX, this.absoluteInnerY); if (this.promptActive) { - this.term.write(textStyle(this.promptState_.promptString)); if (this.inputEventEmitter_) { @@ -113,8 +111,8 @@ class StatusBarWidget extends BaseWidget { history: this.history, default: this.promptState_.initialText, autoComplete: handleAutocompletion, - autoCompleteHint : true, - autoCompleteMenu : true, + autoCompleteHint: true, + autoCompleteMenu: true, }; if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition; @@ -153,19 +151,15 @@ class StatusBarWidget extends BaseWidget { // Only callback once everything has been cleaned up and reset resolveFn(resolveResult); }); - } else { - for (let i = 0; i < this.items_.length; i++) { const s = this.items_[i].substr(0, this.innerWidth - 1); this.term.write(textStyle(s)); } - } if (doSaveCursor) this.term.restoreCursor(); } - } module.exports = StatusBarWidget; diff --git a/CliClient/app/help-utils.js b/CliClient/app/help-utils.js index a56359fc7..fde9421ce 100644 --- a/CliClient/app/help-utils.js +++ b/CliClient/app/help-utils.js @@ -1,10 +1,7 @@ -const fs = require('fs-extra'); const { wrap } = require('lib/string-utils.js'); const Setting = require('lib/models/Setting.js'); -const { fileExtension, basename, dirname } = require('lib/path-utils.js'); -const { _, setLocale, languageCode } = require('lib/locale.js'); +const { _ } = require('lib/locale.js'); -const rootDir = dirname(dirname(__dirname)); const MAX_WIDTH = 78; const INDENT = ' '; @@ -16,14 +13,14 @@ function renderTwoColumnData(options, baseIndent, width) { let option = options[i]; const flag = option[0]; const indent = baseIndent + INDENT + ' '.repeat(optionColWidth + 2); - + let r = wrap(option[1], indent, width); r = r.substr(flag.length + (baseIndent + INDENT).length); r = baseIndent + INDENT + flag + r; output.push(r); } - return output.join("\n"); + return output.join('\n'); } function renderCommandHelp(cmd, width = null) { @@ -44,7 +41,7 @@ function renderCommandHelp(cmd, width = null) { } if (cmd.name() === 'config') { - const renderMetadata = (md) => { + const renderMetadata = md => { let desc = []; if (md.label) { @@ -67,13 +64,13 @@ function renderCommandHelp(cmd, width = null) { } else if (md.type === Setting.TYPE_INT) { defaultString = (md.value ? md.value : 0).toString(); } else if (md.type === Setting.TYPE_BOOL) { - defaultString = (md.value === true ? 'true' : 'false'); + defaultString = md.value === true ? 'true' : 'false'; } } - + if (defaultString !== null) desc.push(_('Default: %s', defaultString)); - return [md.key, desc.join("\n")]; + return [md.key, desc.join('\n')]; }; output.push(''); @@ -83,7 +80,7 @@ function renderCommandHelp(cmd, width = null) { let keysValues = []; const keys = Setting.keys(true, 'cli'); for (let i = 0; i < keys.length; i++) { - if (keysValues.length) keysValues.push(['','']); + if (keysValues.length) keysValues.push(['', '']); const md = Setting.settingMetadata(keys[i]); if (!md.label) continue; keysValues.push(renderMetadata(md)); @@ -91,8 +88,8 @@ function renderCommandHelp(cmd, width = null) { output.push(renderTwoColumnData(keysValues, baseIndent, width)); } - - return output.join("\n"); + + return output.join('\n'); } function getOptionColWidth(options) { @@ -104,4 +101,4 @@ function getOptionColWidth(options) { return output; } -module.exports = { renderCommandHelp }; \ No newline at end of file +module.exports = { renderCommandHelp }; diff --git a/CliClient/app/main.js b/CliClient/app/main.js index 7ee9c1784..d0d0b28b5 100644 --- a/CliClient/app/main.js +++ b/CliClient/app/main.js @@ -53,25 +53,25 @@ shimInit(); const application = app(); -if (process.platform === "win32") { - var rl = require("readline").createInterface({ +if (process.platform === 'win32') { + var rl = require('readline').createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, }); - rl.on("SIGINT", function () { - process.emit("SIGINT"); + rl.on('SIGINT', function() { + process.emit('SIGINT'); }); } -process.stdout.on('error', function( err ) { +process.stdout.on('error', function(err) { // https://stackoverflow.com/questions/12329816/error-write-epipe-when-piping-node-output-to-head#15884508 - if (err.code == "EPIPE") { + if (err.code == 'EPIPE') { process.exit(0); } }); -application.start(process.argv).catch((error) => { +application.start(process.argv).catch(error => { if (error.code == 'flagError') { console.error(error.message); console.error(_('Type `joplin help` for usage information.')); @@ -81,4 +81,4 @@ application.start(process.argv).catch((error) => { } process.exit(1); -}); \ No newline at end of file +}); diff --git a/CliClient/app/onedrive-api-node-utils.js b/CliClient/app/onedrive-api-node-utils.js index b9ba88183..301381f98 100644 --- a/CliClient/app/onedrive-api-node-utils.js +++ b/CliClient/app/onedrive-api-node-utils.js @@ -1,13 +1,11 @@ const { _ } = require('lib/locale.js'); const { netUtils } = require('lib/net-utils.js'); -const http = require("http"); -const urlParser = require("url"); -const FormData = require('form-data'); +const http = require('http'); +const urlParser = require('url'); const enableServerDestroy = require('server-destroy'); class OneDriveApiNodeUtils { - constructor(api) { this.api_ = api; this.oauthServer_ = null; @@ -48,7 +46,7 @@ class OneDriveApiNodeUtils { let authCodeUrl = this.api().authCodeUrl('http://localhost:' + port); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.oauthServer_ = http.createServer(); let errorMessage = null; @@ -56,7 +54,7 @@ class OneDriveApiNodeUtils { const url = urlParser.parse(request.url, true); if (url.pathname === '/auth') { - response.writeHead(302, { 'Location': authCodeUrl }); + response.writeHead(302, { Location: authCodeUrl }); response.end(); return; } @@ -64,10 +62,10 @@ class OneDriveApiNodeUtils { const query = url.query; const writeResponse = (code, message) => { - response.writeHead(code, {"Content-Type": "text/html"}); + response.writeHead(code, { 'Content-Type': 'text/html' }); response.write(this.makePage(message)); response.end(); - } + }; // After the response has been received, don't destroy the server right // away or the browser might display a connection reset error (even @@ -77,21 +75,24 @@ class OneDriveApiNodeUtils { this.oauthServer_.destroy(); this.oauthServer_ = null; }, 1000); - } + }; if (!query.code) return writeResponse(400, '"code" query parameter is missing'); - this.api().execTokenRequest(query.code, 'http://localhost:' + port.toString()).then(() => { - writeResponse(200, _('The application has been authorised - you may now close this browser tab.')); - targetConsole.log(''); - targetConsole.log(_('The application has been successfully authorised.')); - waitAndDestroy(); - }).catch((error) => { - writeResponse(400, error.message); - targetConsole.log(''); - targetConsole.log(error.message); - waitAndDestroy(); - }); + this.api() + .execTokenRequest(query.code, 'http://localhost:' + port.toString()) + .then(() => { + writeResponse(200, _('The application has been authorised - you may now close this browser tab.')); + targetConsole.log(''); + targetConsole.log(_('The application has been successfully authorised.')); + waitAndDestroy(); + }) + .catch(error => { + writeResponse(400, error.message); + targetConsole.log(''); + targetConsole.log(error.message); + waitAndDestroy(); + }); }); this.oauthServer_.on('close', () => { @@ -116,7 +117,6 @@ class OneDriveApiNodeUtils { targetConsole.log('http://127.0.0.1:' + port + '/auth'); }); } - } -module.exports = { OneDriveApiNodeUtils }; \ No newline at end of file +module.exports = { OneDriveApiNodeUtils }; diff --git a/CliClient/package.json b/CliClient/package.json index be64ec1aa..6fe353431 100644 --- a/CliClient/package.json +++ b/CliClient/package.json @@ -82,6 +82,7 @@ "jasmine": "^2.6.0" }, "scripts": { - "test": "jasmine" + "test": "jasmine", + "postinstall": "cd .. && npm i" } } diff --git a/CliClient/tests/ArrayUtils.js b/CliClient/tests/ArrayUtils.js index fd2a5fbde..a77056e4d 100644 --- a/CliClient/tests/ArrayUtils.js +++ b/CliClient/tests/ArrayUtils.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -53,4 +55,4 @@ describe('ArrayUtils', function() { done(); }); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/EnexToMd.js b/CliClient/tests/EnexToMd.js index 9174a0a06..b12c84fc2 100644 --- a/CliClient/tests/EnexToMd.js +++ b/CliClient/tests/EnexToMd.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const os = require('os'); @@ -27,7 +29,7 @@ describe('EnexToMd', function() { 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; @@ -41,10 +43,10 @@ describe('EnexToMd', function() { let expectedMd = await shim.fsDriver().readFile(mdPath); let actualMd = await enexXmlToMd('
' + html + '
', []); - + if (os.EOL === '\r\n') { - expectedMd = expectedMd.replace(/\r\n/g, '\n') - actualMd = actualMd.replace(/\r\n/g, '\n') + expectedMd = expectedMd.replace(/\r\n/g, '\n'); + actualMd = actualMd.replace(/\r\n/g, '\n'); } if (actualMd !== expectedMd) { @@ -60,9 +62,9 @@ describe('EnexToMd', function() { expect(false).toBe(true); // return; } else { - expect(true).toBe(true) + expect(true).toBe(true); } } })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/HtmlToMd.js b/CliClient/tests/HtmlToMd.js index 8e48e4989..156697085 100644 --- a/CliClient/tests/HtmlToMd.js +++ b/CliClient/tests/HtmlToMd.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const os = require('os'); @@ -29,7 +31,7 @@ describe('HtmlToMd', function() { 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; if (htmlFilename.indexOf('.html') < 0) continue; @@ -39,14 +41,14 @@ describe('HtmlToMd', function() { // if (htmlFilename !== 'mathjax_block.html') continue; - const htmlToMdOptions = {} + const htmlToMdOptions = {}; if (htmlFilename === 'anchor_local.html') { // Normally the list of anchor names in the document are retrieved from the HTML code // This is straightforward when the document is still in DOM format, as with the clipper, // but otherwise it would need to be somehow parsed out from the HTML. Here we just // hard code the anchors that we know are in the file. - htmlToMdOptions.anchorNames = ['first', 'second'] + htmlToMdOptions.anchorNames = ['first', 'second']; } const html = await shim.fsDriver().readFile(htmlPath); @@ -55,8 +57,8 @@ describe('HtmlToMd', function() { let actualMd = await htmlToMd.parse('
' + html + '
', htmlToMdOptions); if (os.EOL === '\r\n') { - expectedMd = expectedMd.replace(/\r\n/g, '\n') - actualMd = actualMd.replace(/\r\n/g, '\n') + expectedMd = expectedMd.replace(/\r\n/g, '\n'); + actualMd = actualMd.replace(/\r\n/g, '\n'); } if (actualMd !== expectedMd) { @@ -74,9 +76,9 @@ describe('HtmlToMd', function() { expect(false).toBe(true); // return; } else { - expect(true).toBe(true) + expect(true).toBe(true); } } })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/StringUtils.js b/CliClient/tests/StringUtils.js index a4a477a3a..0e37dbd4d 100644 --- a/CliClient/tests/StringUtils.js +++ b/CliClient/tests/StringUtils.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -42,4 +44,4 @@ describe('StringUtils', function() { done(); }); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/TaskQueue.js b/CliClient/tests/TaskQueue.js index f2ddd6af2..a1fad3bea 100644 --- a/CliClient/tests/TaskQueue.js +++ b/CliClient/tests/TaskQueue.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { asyncTest, fileContentEqual, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js'); @@ -54,4 +56,4 @@ describe('TaskQueue', function() { expect(results[1].error.message).toBe('e'); })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/encryption.js b/CliClient/tests/encryption.js index 1603abdbb..e914e6afa 100644 --- a/CliClient/tests/encryption.js +++ b/CliClient/tests/encryption.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -116,7 +118,7 @@ describe('Encryption', function() { await service.loadMasterKey(masterKey, '123456', true); let cipherText = await service.encryptString('some secret'); - cipherText += "ABCDEFGHIJ"; + cipherText += 'ABCDEFGHIJ'; let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText)); @@ -147,8 +149,8 @@ describe('Encryption', function() { // Check that encrypted data is there expect(!!deserialized.encryption_cipher_text).toBe(true); - encryptedNote = await Note.save(deserialized); - decryptedNote = await Note.decrypt(encryptedNote); + const encryptedNote = await Note.save(deserialized); + const decryptedNote = await Note.decrypt(encryptedNote); expect(decryptedNote.title).toBe(note.title); expect(decryptedNote.body).toBe(note.body); @@ -176,4 +178,4 @@ describe('Encryption', function() { done(); }); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/htmlUtils.js b/CliClient/tests/htmlUtils.js index 10be8dd62..145b76963 100644 --- a/CliClient/tests/htmlUtils.js +++ b/CliClient/tests/htmlUtils.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -46,8 +48,8 @@ describe('htmlUtils', function() { return function(src) { i++; return urls[i]; - } - } + }; + }; for (let i = 0; i < testCases.length; i++) { const md = testCases[i][0]; @@ -102,4 +104,4 @@ describe('htmlUtils', function() { done(); }); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/markdownUtils.js b/CliClient/tests/markdownUtils.js index 9203b19ed..ef9c07360 100644 --- a/CliClient/tests/markdownUtils.js +++ b/CliClient/tests/markdownUtils.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -52,4 +54,4 @@ describe('markdownUtils', function() { done(); }); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/models_BaseItem.js b/CliClient/tests/models_BaseItem.js index 9c274e0ff..9f0cc84c5 100644 --- a/CliClient/tests/models_BaseItem.js +++ b/CliClient/tests/models_BaseItem.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -38,29 +40,29 @@ describe('models_BaseItem', function() { // This is to handle the case where a property is removed from a BaseItem table - in that case files in // the sync target will still have the old property but we don't need it locally. it('should ignore properties that are present in sync file but not in database when serialising', asyncTest(async () => { - let folder = await Folder.save({ title: "folder1" }); - + let folder = await Folder.save({ title: 'folder1' }); + let serialized = await Folder.serialize(folder); - serialized += "\nignore_me: true" + serialized += '\nignore_me: true'; let unserialized = await Folder.unserialize(serialized); expect('ignore_me' in unserialized).toBe(false); })); - + it('should not modify title when unserializing', asyncTest(async () => { - let folder1 = await Folder.save({ title: "" }); - let folder2 = await Folder.save({ title: "folder1" }); - - let serialized1 = await Folder.serialize(folder1); - let unserialized1 = await Folder.unserialize(serialized1); - - expect(unserialized1.title).toBe(folder1.title); - - let serialized2 = await Folder.serialize(folder2); - let unserialized2 = await Folder.unserialize(serialized2); - - expect(unserialized2.title).toBe(folder2.title); + let folder1 = await Folder.save({ title: '' }); + let folder2 = await Folder.save({ title: 'folder1' }); + + let serialized1 = await Folder.serialize(folder1); + let unserialized1 = await Folder.unserialize(serialized1); + + expect(unserialized1.title).toBe(folder1.title); + + let serialized2 = await Folder.serialize(folder2); + let unserialized2 = await Folder.unserialize(serialized2); + + expect(unserialized2.title).toBe(folder2.title); })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/models_Folder.js b/CliClient/tests/models_Folder.js index c43cdee11..846fe6d88 100644 --- a/CliClient/tests/models_Folder.js +++ b/CliClient/tests/models_Folder.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -26,10 +28,10 @@ describe('models_Folder', function() { }); it('should tell if a notebook can be nested under another one', asyncTest(async () => { - let f1 = await Folder.save({ title: "folder1" }); - let f2 = await Folder.save({ title: "folder2", parent_id: f1.id }); - let f3 = await Folder.save({ title: "folder3", parent_id: f2.id }); - let f4 = await Folder.save({ title: "folder4" }); + let f1 = await Folder.save({ title: 'folder1' }); + let f2 = await Folder.save({ title: 'folder2', parent_id: f1.id }); + let f3 = await Folder.save({ title: 'folder3', parent_id: f2.id }); + let f4 = await Folder.save({ title: 'folder4' }); expect(await Folder.canNestUnder(f1.id, f2.id)).toBe(false); expect(await Folder.canNestUnder(f2.id, f2.id)).toBe(false); @@ -42,8 +44,8 @@ describe('models_Folder', function() { })); it('should recursively delete notes and sub-notebooks', asyncTest(async () => { - let f1 = await Folder.save({ title: "folder1" }); - let f2 = await Folder.save({ title: "folder2", parent_id: f1.id }); + let f1 = await Folder.save({ title: 'folder1' }); + let f2 = await Folder.save({ title: 'folder2', parent_id: f1.id }); let n1 = await Note.save({ title: 'note1', parent_id: f2.id }); await Folder.delete(f1.id); @@ -55,9 +57,9 @@ describe('models_Folder', function() { it('should sort by last modified, based on content', asyncTest(async () => { let folders; - let f1 = await Folder.save({ title: "folder1" }); await sleep(0.1); - let f2 = await Folder.save({ title: "folder2" }); await sleep(0.1); - let f3 = await Folder.save({ title: "folder3" }); await sleep(0.1); + let f1 = await Folder.save({ title: 'folder1' }); await sleep(0.1); + let f2 = await Folder.save({ title: 'folder2' }); await sleep(0.1); + let f3 = await Folder.save({ title: 'folder3' }); await sleep(0.1); let n1 = await Note.save({ title: 'note1', parent_id: f2.id }); folders = await Folder.orderByLastModified(await Folder.all(), 'desc'); @@ -89,9 +91,9 @@ describe('models_Folder', function() { it('should sort by last modified, based on content (sub-folders too)', asyncTest(async () => { let folders; - let f1 = await Folder.save({ title: "folder1" }); await sleep(0.1); - let f2 = await Folder.save({ title: "folder2" }); await sleep(0.1); - let f3 = await Folder.save({ title: "folder3", parent_id: f1.id }); await sleep(0.1); + let f1 = await Folder.save({ title: 'folder1' }); await sleep(0.1); + let f2 = await Folder.save({ title: 'folder2' }); await sleep(0.1); + let f3 = await Folder.save({ title: 'folder3', parent_id: f1.id }); await sleep(0.1); let n1 = await Note.save({ title: 'note1', parent_id: f3.id }); folders = await Folder.orderByLastModified(await Folder.all(), 'desc'); @@ -100,7 +102,7 @@ describe('models_Folder', function() { expect(folders[1].id).toBe(f3.id); expect(folders[2].id).toBe(f2.id); - let n2 = await Note.save({ title: 'note2', parent_id: f2.id }); + let n2 = await Note.save({ title: 'note2', parent_id: f2.id }); folders = await Folder.orderByLastModified(await Folder.all(), 'desc'); expect(folders[0].id).toBe(f2.id); @@ -108,13 +110,13 @@ describe('models_Folder', function() { expect(folders[2].id).toBe(f3.id); await Note.save({ id: n1.id, title: 'note1 MOD' }); - + folders = await Folder.orderByLastModified(await Folder.all(), 'desc'); expect(folders[0].id).toBe(f1.id); expect(folders[1].id).toBe(f3.id); expect(folders[2].id).toBe(f2.id); - let f4 = await Folder.save({ title: "folder4", parent_id: f1.id }); await sleep(0.1); + let f4 = await Folder.save({ title: 'folder4', parent_id: f1.id }); await sleep(0.1); let n3 = await Note.save({ title: 'note3', parent_id: f4.id }); folders = await Folder.orderByLastModified(await Folder.all(), 'desc'); @@ -125,4 +127,4 @@ describe('models_Folder', function() { expect(folders[3].id).toBe(f2.id); })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/models_ItemChange.js b/CliClient/tests/models_ItemChange.js index fbcf1b72a..760097209 100644 --- a/CliClient/tests/models_ItemChange.js +++ b/CliClient/tests/models_ItemChange.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -25,7 +27,7 @@ describe('models_ItemChange', function() { }); it('should delete old changes that have been processed', asyncTest(async () => { - const n1 = await Note.save({ title: "abcd efgh" }); // 3 + const n1 = await Note.save({ title: 'abcd efgh' }); // 3 await ItemChange.waitForAllSaved(); @@ -48,4 +50,4 @@ describe('models_ItemChange', function() { expect(await ItemChange.lastChangeId()).toBe(0); })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/models_Note.js b/CliClient/tests/models_Note.js index 876b337b3..247a55d5d 100644 --- a/CliClient/tests/models_Note.js +++ b/CliClient/tests/models_Note.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -21,7 +23,7 @@ describe('models_Note', function() { }); it('should find resource and note IDs', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); let note2 = await Note.save({ title: 'ma deuxième note', body: 'Lien vers première note : ' + Note.markdownTag(note1), parent_id: folder1.id }); @@ -67,7 +69,7 @@ describe('models_Note', function() { })); it('should change the type of notes', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); note1 = await Note.load(note1.id); @@ -86,9 +88,9 @@ describe('models_Note', function() { expect(changedNote === note1).toBe(false); expect(!!changedNote.is_todo).toBe(false); })); - + it('should serialize and unserialize without modifying data', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1"}); + let folder1 = await Folder.save({ title: 'folder1'}); const testCases = [ [ {title: '', body:'Body and no title\nSecond line\nThird Line', parent_id: folder1.id}, '', 'Body and no title\nSecond line\nThird Line'], @@ -96,22 +98,22 @@ describe('models_Note', function() { 'Note title', 'Body and title'], [ {title: 'Title and no body', body:'', parent_id: folder1.id}, 'Title and no body', ''], - ] - + ]; + for (let i = 0; i < testCases.length; i++) { const t = testCases[i]; - + const input = t[0]; const expectedTitle = t[1]; const expectedBody = t[1]; - + let note1 = await Note.save(input); let serialized = await Note.serialize(note1); - let unserialized = await Note.unserialize( serialized); - + let unserialized = await Note.unserialize(serialized); + expect(unserialized.title).toBe(input.title); expect(unserialized.body).toBe(input.body); } })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/models_Resource.js b/CliClient/tests/models_Resource.js index 64ae22305..a344eb130 100644 --- a/CliClient/tests/models_Resource.js +++ b/CliClient/tests/models_Resource.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars, require-atomic-updates */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -25,7 +27,7 @@ describe('models_Resource', function() { }); it('should have a "done" fetch_status when created locally', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, testImagePath); let resource1 = (await Resource.all())[0]; @@ -34,7 +36,7 @@ describe('models_Resource', function() { })); it('should have a default local state', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, testImagePath); let resource1 = (await Resource.all())[0]; @@ -45,7 +47,7 @@ describe('models_Resource', function() { })); it('should save and delete local state', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, testImagePath); let resource1 = (await Resource.all())[0]; @@ -61,7 +63,7 @@ describe('models_Resource', function() { })); it('should resize the resource if the image is below the required dimensions', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); const previousMax = Resource.IMAGE_MAX_DIMENSION; Resource.IMAGE_MAX_DIMENSION = 5; @@ -76,7 +78,7 @@ describe('models_Resource', function() { })); it('should not resize the resource if the image is below the required dimensions', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, testImagePath); let resource1 = (await Resource.all())[0]; @@ -87,4 +89,4 @@ describe('models_Resource', function() { expect(originalStat.size).toBe(newStat.size); })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/models_Revision.js b/CliClient/tests/models_Revision.js index 9a45ef95c..7bb293765 100644 --- a/CliClient/tests/models_Revision.js +++ b/CliClient/tests/models_Revision.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -41,7 +43,7 @@ describe('models_Revision', function() { const newObject = { one: '123', three: '999', - } + }; const patch = Revision.createObjectPatch(oldObject, newObject); const merged = Revision.applyObjectPatch(oldObject, patch); @@ -82,16 +84,16 @@ describe('models_Revision', function() { - +x %5D Check `, - expected: [-1, +1], - }, - { - patch: `@@ -1022,56 +1022,415 @@ + expected: [-1, +1], + }, + { + patch: `@@ -1022,56 +1022,415 @@ .%0A%0A# - How to view a note history%0A%0AWhile all the apps +%C2%A0How does it work?%0A%0AAll the apps save a version of the modified notes every 10 minutes. %0A%0A# `, - expected: [-(19+27+2), 17+67+4], - }, + expected: [-(19+27+2), 17+67+4], + }, ]; for (const test of tests) { @@ -101,4 +103,4 @@ describe('models_Revision', function() { } })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/models_Setting.js b/CliClient/tests/models_Setting.js index 53cce0439..a1a7de0c2 100644 --- a/CliClient/tests/models_Setting.js +++ b/CliClient/tests/models_Setting.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -18,7 +20,7 @@ describe('models_Setting', function() { const settings = { 'sync.5.path': 'http://example.com', 'sync.5.username': 'testing', - } + }; let output = Setting.subValues('sync.5', settings); expect(output['path']).toBe('http://example.com'); @@ -29,4 +31,4 @@ describe('models_Setting', function() { expect('username' in output).toBe(false); })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/models_Tag.js b/CliClient/tests/models_Tag.js index cf136e631..a959d1674 100644 --- a/CliClient/tests/models_Tag.js +++ b/CliClient/tests/models_Tag.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -22,7 +24,7 @@ describe('models_Tag', function() { }); it('should add tags by title', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await Tag.setNoteTagsByTitles(note1.id, ['un', 'deux']); @@ -32,7 +34,7 @@ describe('models_Tag', function() { })); it('should not allow renaming tag to existing tag names', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await Tag.setNoteTagsByTitles(note1.id, ['un', 'deux']); @@ -44,7 +46,7 @@ describe('models_Tag', function() { })); it('should not return tags without notes', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await Tag.setNoteTagsByTitles(note1.id, ['un']); @@ -57,4 +59,4 @@ describe('models_Tag', function() { expect(tags.length).toBe(0); })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/pathUtils.js b/CliClient/tests/pathUtils.js index dba746026..fce59209a 100644 --- a/CliClient/tests/pathUtils.js +++ b/CliClient/tests/pathUtils.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { extractExecutablePath, quotePath, unquotePath, friendlySafeFilename, toFileProtocolPath} = require('lib/path-utils.js'); @@ -50,7 +52,7 @@ describe('pathUtils', function() { const t = testCases[i]; expect(quotePath(t[0])).toBe(t[1]); expect(unquotePath(quotePath(t[0]))).toBe(t[0]); - } + } done(); }); @@ -68,7 +70,7 @@ describe('pathUtils', function() { for (let i = 0; i < testCases.length; i++) { const t = testCases[i]; expect(extractExecutablePath(t[0])).toBe(t[1]); - } + } done(); }); @@ -77,7 +79,7 @@ describe('pathUtils', function() { const testCases_win32 = [ ['C:\\handle\\space test', 'file:///C:/handle/space+test'], ['C:\\escapeplus\\+', 'file:///C:/escapeplus/%2B'], - ['C:\\handle\\single quote\'', 'file:///C:/handle/single+quote%27'], + ['C:\\handle\\single quote\'', 'file:///C:/handle/single+quote%27'], ]; const testCases_unixlike = [ ['/handle/space test', 'file:///handle/space+test'], @@ -88,13 +90,13 @@ describe('pathUtils', function() { for (let i = 0; i < testCases_win32.length; i++) { const t = testCases_win32[i]; expect(toFileProtocolPath(t[0], 'win32')).toBe(t[1]); - } + } for (let i = 0; i < testCases_unixlike.length; i++) { const t = testCases_unixlike[i]; expect(toFileProtocolPath(t[0], 'linux')).toBe(t[1]); - } + } done(); }); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/services_InteropService.js b/CliClient/tests/services_InteropService.js index bb9bd88f1..3a7bd0532 100644 --- a/CliClient/tests/services_InteropService.js +++ b/CliClient/tests/services_InteropService.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -44,7 +46,7 @@ describe('services_InteropService', function() { it('should export and import folders', asyncTest(async () => { const service = new InteropService(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); folder1 = await Folder.load(folder1.id); const filePath = exportDir() + '/test.jex'; @@ -79,7 +81,7 @@ describe('services_InteropService', function() { it('should export and import folders and notes', asyncTest(async () => { const service = new InteropService(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); note1 = await Note.load(note1.id); const filePath = exportDir() + '/test.jex'; @@ -118,7 +120,7 @@ describe('services_InteropService', function() { it('should export and import notes to specific folder', asyncTest(async () => { const service = new InteropService(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); note1 = await Note.load(note1.id); const filePath = exportDir() + '/test.jex'; @@ -138,7 +140,7 @@ describe('services_InteropService', function() { it('should export and import tags', asyncTest(async () => { const service = new InteropService(); const filePath = exportDir() + '/test.jex'; - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); let tag1 = await Tag.save({ title: 'mon tag' }); tag1 = await Tag.load(tag1.id); @@ -178,7 +180,7 @@ describe('services_InteropService', function() { it('should export and import resources', asyncTest(async () => { const service = new InteropService(); const filePath = exportDir() + '/test.jex'; - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); note1 = await Note.load(note1.id); @@ -186,7 +188,7 @@ describe('services_InteropService', function() { let resource1 = await Resource.load(resourceIds[0]); await service.export({ path: filePath }); - + await Note.delete(note1.id); await service.import({ path: filePath }); @@ -214,11 +216,11 @@ describe('services_InteropService', function() { it('should export and import single notes', asyncTest(async () => { const service = new InteropService(); const filePath = exportDir() + '/test.jex'; - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await service.export({ path: filePath, sourceNoteIds: [note1.id] }); - + await Note.delete(note1.id); await Folder.delete(folder1.id); @@ -234,11 +236,11 @@ describe('services_InteropService', function() { it('should export and import single folders', asyncTest(async () => { const service = new InteropService(); const filePath = exportDir() + '/test.jex'; - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await service.export({ path: filePath, sourceFolderIds: [folder1.id] }); - + await Note.delete(note1.id); await Folder.delete(folder1.id); @@ -252,17 +254,17 @@ describe('services_InteropService', function() { })); it('should export and import folder and its sub-folders', asyncTest(async () => { - + const service = new InteropService(); const filePath = exportDir() + '/test.jex'; - let folder1 = await Folder.save({ title: "folder1" }); - let folder2 = await Folder.save({ title: "folder2", parent_id: folder1.id }); - let folder3 = await Folder.save({ title: "folder3", parent_id: folder2.id }); - let folder4 = await Folder.save({ title: "folder4", parent_id: folder2.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let folder2 = await Folder.save({ title: 'folder2', parent_id: folder1.id }); + let folder3 = await Folder.save({ title: 'folder3', parent_id: folder2.id }); + let folder4 = await Folder.save({ title: 'folder4', parent_id: folder2.id }); let note1 = await Note.save({ title: 'ma note', parent_id: folder4.id }); await service.export({ path: filePath, sourceFolderIds: [folder1.id] }); - + await Note.delete(note1.id); await Folder.delete(folder1.id); await Folder.delete(folder2.id); @@ -273,7 +275,7 @@ describe('services_InteropService', function() { expect(await Note.count()).toBe(1); expect(await Folder.count()).toBe(4); - + let folder1_2 = await Folder.loadByTitle('folder1'); let folder2_2 = await Folder.loadByTitle('folder2'); let folder3_2 = await Folder.loadByTitle('folder3'); @@ -289,12 +291,12 @@ describe('services_InteropService', function() { it('should export and import links to notes', asyncTest(async () => { const service = new InteropService(); const filePath = exportDir() + '/test.jex'; - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); let note2 = await Note.save({ title: 'ma deuxième note', body: 'Lien vers première note : ' + Note.markdownTag(note1), parent_id: folder1.id }); await service.export({ path: filePath, sourceFolderIds: [folder1.id] }); - + await Note.delete(note1.id); await Note.delete(note2.id); await Folder.delete(folder1.id); @@ -322,7 +324,7 @@ describe('services_InteropService', function() { // verify that the json files exist and can be parsed const items = [folder1, note1]; for (let i = 0; i < items.length; i++) { - const jsonFile = filePath + '/' + items[i].id + '.json'; + const jsonFile = filePath + '/' + items[i].id + '.json'; let json = await fs.readFile(jsonFile, 'utf-8'); let obj = JSON.parse(json); expect(obj.id).toBe(items[i].id); @@ -357,4 +359,4 @@ describe('services_InteropService', function() { expect(await shim.fsDriver().exists(outDir + '/ジョプリン/ジョプリン.md')).toBe(true); })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/services_KvStore.js b/CliClient/tests/services_KvStore.js index 617ce9797..5c1e461f3 100644 --- a/CliClient/tests/services_KvStore.js +++ b/CliClient/tests/services_KvStore.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { asyncTest, fileContentEqual, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js'); @@ -104,4 +106,4 @@ describe('services_KvStore', function() { expect(numbers[1]).toBe(2); })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/services_ResourceService.js b/CliClient/tests/services_ResourceService.js index a486e733d..688f17e76 100644 --- a/CliClient/tests/services_ResourceService.js +++ b/CliClient/tests/services_ResourceService.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -46,7 +48,7 @@ describe('services_ResourceService', function() { it('should delete orphaned resources', asyncTest(async () => { const service = new ResourceService(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); note1 = await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); let resource1 = (await Resource.all())[0]; @@ -77,7 +79,7 @@ describe('services_ResourceService', function() { it('should not delete resource if still associated with at least one note', asyncTest(async () => { const service = new ResourceService(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); let note2 = await Note.save({ title: 'ma deuxième note', parent_id: folder1.id }); note1 = await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); @@ -86,9 +88,9 @@ describe('services_ResourceService', function() { await service.indexNoteResources(); await Note.delete(note1.id); - + await service.indexNoteResources(); - + await Note.save({ id: note2.id, body: Resource.markdownTag(resource1) }); await service.indexNoteResources(); @@ -111,7 +113,7 @@ describe('services_ResourceService', function() { it('should not delete resource if it is used in an IMG tag', asyncTest(async () => { const service = new ResourceService(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); note1 = await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); let resource1 = (await Resource.all())[0]; @@ -119,9 +121,9 @@ describe('services_ResourceService', function() { await service.indexNoteResources(); await Note.save({ id: note1.id, body: 'This is HTML: ' }); - + await service.indexNoteResources(); - + await service.deleteOrphanResources(0); expect(!!(await Resource.load(resource1.id))).toBe(true); @@ -130,7 +132,7 @@ describe('services_ResourceService', function() { it('should not process twice the same change', asyncTest(async () => { const service = new ResourceService(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); note1 = await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); let resource1 = (await Resource.all())[0]; @@ -161,13 +163,13 @@ describe('services_ResourceService', function() { // - Client 1 syncs // - Client 1 runs resource indexer - but because N1 hasn't been decrypted yet, it found that R1 is no longer associated with any note // - Client 1 decrypts notes, but too late - // + // // Eventually R1 is deleted because service thinks that it was at some point associated with a note, but no longer. const masterKey = await loadEncryptionMasterKey(); await encryptionService().enableEncryption(masterKey, '123456'); await encryptionService().loadMasterKeysFromSettings(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); // R1 await resourceService().indexNoteResources(); @@ -197,7 +199,7 @@ describe('services_ResourceService', function() { it('should double-check if the resource is still linked before deleting it', asyncTest(async () => { SearchEngine.instance().setDb(db()); // /!\ Note that we use the global search engine here, which we shouldn't but will work for now - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); note1 = await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); await resourceService().indexNoteResources(); @@ -213,4 +215,4 @@ describe('services_ResourceService', function() { expect(!!nr.is_associated).toBe(true); // And it should have fixed the situation by re-indexing the note content })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/services_Revision.js b/CliClient/tests/services_Revision.js index f6433c152..961063007 100644 --- a/CliClient/tests/services_Revision.js +++ b/CliClient/tests/services_Revision.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -22,7 +24,7 @@ describe('services_Revision', function() { beforeEach(async (done) => { await setupDatabaseAndSynchronizer(1); await switchClient(1); - Setting.setValue('revisionService.intervalBetweenRevisions', 0) + Setting.setValue('revisionService.intervalBetweenRevisions', 0); done(); }); @@ -377,7 +379,7 @@ describe('services_Revision', function() { const n1_v2 = await Note.save({ id: n1_v0.id, title: 'hello' }); await revisionService().collectRevisions(); // Note has not changed (except its timestamp) so don't create a revision - expect((await Revision.all()).length).toBe(1); + expect((await Revision.all()).length).toBe(1); })); it('should preserve user update time', asyncTest(async () => { @@ -392,7 +394,7 @@ describe('services_Revision', function() { const userUpdatedTime = Date.now() - 1000 * 60 * 60; const n1_v2 = await Note.save({ id: n1_v0.id, title: 'hello', updated_time: Date.now(), user_updated_time: userUpdatedTime }, { autoTimestamp: false }); await revisionService().collectRevisions(); // Only the user timestamp has changed, but that needs to be saved - + const revisions = await Revision.all(); expect(revisions.length).toBe(2); @@ -417,4 +419,4 @@ describe('services_Revision', function() { expect((await Revision.all()).length).toBe(2); })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/services_SearchEngine.js b/CliClient/tests/services_SearchEngine.js index 84a5c3b1d..7828f5bd8 100644 --- a/CliClient/tests/services_SearchEngine.js +++ b/CliClient/tests/services_SearchEngine.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -21,15 +23,15 @@ describe('services_SearchEngine', function() { engine = new SearchEngine(); engine.setDb(db()); - + done(); }); it('should keep the content and FTS table in sync', asyncTest(async () => { let rows, n1, n2, n3; - n1 = await Note.save({ title: "a" }); - n2 = await Note.save({ title: "b" }); + n1 = await Note.save({ title: 'a' }); + n2 = await Note.save({ title: 'b' }); await engine.syncTables(); rows = await engine.search('a'); expect(rows.length).toBe(1); @@ -61,8 +63,8 @@ describe('services_SearchEngine', function() { })); it('should, after initial indexing, save the last change ID', asyncTest(async () => { - const n1 = await Note.save({ title: "abcd efgh" }); // 3 - const n2 = await Note.save({ title: "abcd aaaaa abcd abcd" }); // 1 + const n1 = await Note.save({ title: 'abcd efgh' }); // 3 + const n2 = await Note.save({ title: 'abcd aaaaa abcd abcd' }); // 1 expect(Setting.value('searchEngine.initialIndexingDone')).toBe(false); @@ -77,9 +79,9 @@ describe('services_SearchEngine', function() { it('should order search results by relevance (1)', asyncTest(async () => { - const n1 = await Note.save({ title: "abcd efgh" }); // 3 - const n2 = await Note.save({ title: "abcd aaaaa abcd abcd" }); // 1 - const n3 = await Note.save({ title: "abcd aaaaa bbbb eeee abcd" }); // 2 + const n1 = await Note.save({ title: 'abcd efgh' }); // 3 + const n2 = await Note.save({ title: 'abcd aaaaa abcd abcd' }); // 1 + const n3 = await Note.save({ title: 'abcd aaaaa bbbb eeee abcd' }); // 2 await engine.syncTables(); const rows = await engine.search('abcd'); @@ -91,15 +93,15 @@ describe('services_SearchEngine', function() { it('should order search results by relevance (2)', asyncTest(async () => { // 1 - const n1 = await Note.save({ title: "abcd efgh", body: "XX abcd XX efgh" }); + const n1 = await Note.save({ title: 'abcd efgh', body: 'XX abcd XX efgh' }); // 4 - const n2 = await Note.save({ title: "abcd aaaaa bbbb eeee efgh" }); + const n2 = await Note.save({ title: 'abcd aaaaa bbbb eeee efgh' }); // 3 - const n3 = await Note.save({ title: "abcd aaaaa efgh" }); + const n3 = await Note.save({ title: 'abcd aaaaa efgh' }); // 2 - const n4 = await Note.save({ title: "blablablabla blabla bla abcd X efgh" }); + const n4 = await Note.save({ title: 'blablablabla blabla bla abcd X efgh' }); // 5 - const n5 = await Note.save({ title: "occurence many times but very abcd spread appart spread appart spread appart spread appart spread appart efgh occurence many times but very abcd spread appart spread appart spread appart spread appart spread appart efgh occurence many times but very abcd spread appart spread appart spread appart spread appart spread appart efgh occurence many times but very abcd spread appart spread appart spread appart spread appart spread appart efgh occurence many times but very abcd spread appart spread appart spread appart spread appart spread appart efgh" }); + const n5 = await Note.save({ title: 'occurence many times but very abcd spread appart spread appart spread appart spread appart spread appart efgh occurence many times but very abcd spread appart spread appart spread appart spread appart spread appart efgh occurence many times but very abcd spread appart spread appart spread appart spread appart spread appart efgh occurence many times but very abcd spread appart spread appart spread appart spread appart spread appart efgh occurence many times but very abcd spread appart spread appart spread appart spread appart spread appart efgh' }); await engine.syncTables(); const rows = await engine.search('abcd efgh'); @@ -114,11 +116,11 @@ describe('services_SearchEngine', function() { it('should order search results by relevance (last updated first)', asyncTest(async () => { let rows; - const n1 = await Note.save({ title: "abcd" }); + const n1 = await Note.save({ title: 'abcd' }); await sleep(0.1); - const n2 = await Note.save({ title: "abcd" }); + const n2 = await Note.save({ title: 'abcd' }); await sleep(0.1); - const n3 = await Note.save({ title: "abcd" }); + const n3 = await Note.save({ title: 'abcd' }); await sleep(0.1); await engine.syncTables(); @@ -128,7 +130,7 @@ describe('services_SearchEngine', function() { expect(rows[1].id).toBe(n2.id); expect(rows[2].id).toBe(n1.id); - await Note.save({ id: n1.id, title: "abcd" }); + await Note.save({ id: n1.id, title: 'abcd' }); await engine.syncTables(); rows = await engine.search('abcd'); @@ -140,11 +142,11 @@ describe('services_SearchEngine', function() { it('should order search results by relevance (completed to-dos last)', asyncTest(async () => { let rows; - const n1 = await Note.save({ title: "abcd", is_todo: 1 }); + const n1 = await Note.save({ title: 'abcd', is_todo: 1 }); await sleep(0.1); - const n2 = await Note.save({ title: "abcd", is_todo: 1 }); + const n2 = await Note.save({ title: 'abcd', is_todo: 1 }); await sleep(0.1); - const n3 = await Note.save({ title: "abcd", is_todo: 1 }); + const n3 = await Note.save({ title: 'abcd', is_todo: 1 }); await sleep(0.1); await engine.syncTables(); @@ -166,11 +168,11 @@ describe('services_SearchEngine', function() { it('should supports various query types', asyncTest(async () => { let rows; - const n1 = await Note.save({ title: "abcd efgh ijkl", body: "aaaa bbbb" }); - const n2 = await Note.save({ title: "iiii efgh bbbb", body: "aaaa bbbb" }); - const n3 = await Note.save({ title: "Агентство Рейтер" }); - const n4 = await Note.save({ title: "Dog" }); - const n5 = await Note.save({ title: "СООБЩИЛО" }); + const n1 = await Note.save({ title: 'abcd efgh ijkl', body: 'aaaa bbbb' }); + const n2 = await Note.save({ title: 'iiii efgh bbbb', body: 'aaaa bbbb' }); + const n3 = await Note.save({ title: 'Агентство Рейтер' }); + const n4 = await Note.save({ title: 'Dog' }); + const n5 = await Note.save({ title: 'СООБЩИЛО' }); await engine.syncTables(); @@ -216,7 +218,7 @@ describe('services_SearchEngine', function() { it('should support queries with or without accents', asyncTest(async () => { let rows; - const n1 = await Note.save({ title: "père noël" }); + const n1 = await Note.save({ title: 'père noël' }); await engine.syncTables(); @@ -228,7 +230,7 @@ describe('services_SearchEngine', function() { it('should support queries with Chinese characters', asyncTest(async () => { let rows; - const n1 = await Note.save({ title: "我是法国人" }); + const n1 = await Note.save({ title: '我是法国人' }); await engine.syncTables(); @@ -238,7 +240,7 @@ describe('services_SearchEngine', function() { it('should support queries with Japanese characters', asyncTest(async () => { let rows; - const n1 = await Note.save({ title: "私は日本語を話すことができません" }); + const n1 = await Note.save({ title: '私は日本語を話すことができません' }); await engine.syncTables(); @@ -248,7 +250,7 @@ describe('services_SearchEngine', function() { it('should support queries with Korean characters', asyncTest(async () => { let rows; - const n1 = await Note.save({ title: "이것은 한국말이다" }); + const n1 = await Note.save({ title: '이것은 한국말이다' }); await engine.syncTables(); @@ -258,7 +260,7 @@ describe('services_SearchEngine', function() { it('should support field restricted queries with Chinese characters', asyncTest(async () => { let rows; - const n1 = await Note.save({ title: "你好", body: "我是法国人" }); + const n1 = await Note.save({ title: '你好', body: '我是法国人' }); await engine.syncTables(); @@ -323,4 +325,4 @@ describe('services_SearchEngine', function() { } })); -}); \ No newline at end of file +}); diff --git a/CliClient/tests/services_rest_Api.js b/CliClient/tests/services_rest_Api.js index 54469b13c..12e80f400 100644 --- a/CliClient/tests/services_rest_Api.js +++ b/CliClient/tests/services_rest_Api.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -40,7 +42,7 @@ describe('services_rest_Api', function() { }); it('should get folders', async (done) => { - let f1 = await Folder.save({ title: "mon carnet" }); + let f1 = await Folder.save({ title: 'mon carnet' }); const response = await api.route('GET', 'folders'); expect(response.length).toBe(1); expect(response[0].title).toBe('mon carnet'); @@ -48,7 +50,7 @@ describe('services_rest_Api', function() { }); it('should update folders', async (done) => { - let f1 = await Folder.save({ title: "mon carnet" }); + let f1 = await Folder.save({ title: 'mon carnet' }); const response = await api.route('PUT', 'folders/' + f1.id, null, JSON.stringify({ title: 'modifié', })); @@ -60,12 +62,12 @@ describe('services_rest_Api', function() { }); it('should delete folders', async (done) => { - let f1 = await Folder.save({ title: "mon carnet" }); + let f1 = await Folder.save({ title: 'mon carnet' }); await api.route('DELETE', 'folders/' + f1.id); let f1b = await Folder.load(f1.id); expect(!f1b).toBe(true); - + done(); }); @@ -79,12 +81,12 @@ describe('services_rest_Api', function() { let f = await Folder.all(); expect(f.length).toBe(1); expect(f[0].title).toBe('from api'); - + done(); }); it('should get one folder', async (done) => { - let f1 = await Folder.save({ title: "mon carnet" }); + let f1 = await Folder.save({ title: 'mon carnet' }); const response = await api.route('GET', 'folders/' + f1.id); expect(response.id).toBe(f1.id); @@ -95,7 +97,7 @@ describe('services_rest_Api', function() { }); it('should get the folder notes', async (done) => { - let f1 = await Folder.save({ title: "mon carnet" }); + let f1 = await Folder.save({ title: 'mon carnet' }); const response2 = await api.route('GET', 'folders/' + f1.id + '/notes'); expect(response2.length).toBe(0); @@ -116,12 +118,12 @@ describe('services_rest_Api', function() { it('should get notes', async (done) => { let response = null; - const f1 = await Folder.save({ title: "mon carnet" }); - const f2 = await Folder.save({ title: "mon deuxième carnet" }); + const f1 = await Folder.save({ title: 'mon carnet' }); + const f2 = await Folder.save({ title: 'mon deuxième carnet' }); const n1 = await Note.save({ title: 'un', parent_id: f1.id }); const n2 = await Note.save({ title: 'deux', parent_id: f1.id }); const n3 = await Note.save({ title: 'trois', parent_id: f2.id }); - + response = await api.route('GET', 'notes'); expect(response.length).toBe(3); @@ -138,8 +140,8 @@ describe('services_rest_Api', function() { it('should create notes', async (done) => { let response = null; - const f = await Folder.save({ title: "mon carnet" }); - + const f = await Folder.save({ title: 'mon carnet' }); + response = await api.route('POST', 'notes', null, JSON.stringify({ title: 'testing', parent_id: f.id, @@ -159,11 +161,11 @@ describe('services_rest_Api', function() { it('should preserve user timestamps when creating notes', async (done) => { let response = null; - const f = await Folder.save({ title: "mon carnet" }); + const f = await Folder.save({ title: 'mon carnet' }); const updatedTime = Date.now() - 1000; const createdTime = Date.now() - 10000; - + response = await api.route('POST', 'notes', null, JSON.stringify({ parent_id: f.id, user_updated_time: updatedTime, @@ -178,8 +180,8 @@ describe('services_rest_Api', function() { it('should create notes with supplied ID', async (done) => { let response = null; - const f = await Folder.save({ title: "mon carnet" }); - + const f = await Folder.save({ title: 'mon carnet' }); + response = await api.route('POST', 'notes', null, JSON.stringify({ id: '12345678123456781234567812345678', title: 'testing', @@ -192,19 +194,19 @@ describe('services_rest_Api', function() { it('should create todos', async (done) => { let response = null; - const f = await Folder.save({ title: "stuff to do" }); + const f = await Folder.save({ title: 'stuff to do' }); response = await api.route('POST', 'notes', null, JSON.stringify({ title: 'testing', parent_id: f.id, - is_todo: 1 + is_todo: 1, })); expect(response.is_todo).toBe(1); response = await api.route('POST', 'notes', null, JSON.stringify({ title: 'testing 2', parent_id: f.id, - is_todo: 0 + is_todo: 0, })); expect(response.is_todo).toBe(0); @@ -217,7 +219,7 @@ describe('services_rest_Api', function() { response = await api.route('POST', 'notes', null, JSON.stringify({ title: 'testing 4', parent_id: f.id, - is_todo: '1' + is_todo: '1', })); expect(response.is_todo).toBe(1); done(); @@ -230,18 +232,18 @@ describe('services_rest_Api', function() { })); expect(response.id).toBe('12345678123456781234567812345678'); - + done(); }); it('should create notes with images', async (done) => { let response = null; - const f = await Folder.save({ title: "mon carnet" }); - + const f = await Folder.save({ title: 'mon carnet' }); + response = await api.route('POST', 'notes', null, JSON.stringify({ title: 'testing image', parent_id: f.id, - image_data_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII=" + image_data_url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII=', })); const resources = await Resource.all(); @@ -255,12 +257,12 @@ describe('services_rest_Api', function() { it('should delete resources', async (done) => { let response = null; - const f = await Folder.save({ title: "mon carnet" }); - + const f = await Folder.save({ title: 'mon carnet' }); + response = await api.route('POST', 'notes', null, JSON.stringify({ title: 'testing image', parent_id: f.id, - image_data_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII=" + image_data_url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII=', })); const resource = (await Resource.all())[0]; @@ -271,14 +273,14 @@ describe('services_rest_Api', function() { await api.route('DELETE', 'resources/' + resource.id); expect(await shim.fsDriver().exists(filePath)).toBe(false); expect(!(await Resource.load(resource.id))).toBe(true); - + done(); }); it('should create notes from HTML', async (done) => { let response = null; - const f = await Folder.save({ title: "mon carnet" }); - + const f = await Folder.save({ title: 'mon carnet' }); + response = await api.route('POST', 'notes', null, JSON.stringify({ title: 'testing HTML', parent_id: f.id, @@ -314,7 +316,7 @@ describe('services_rest_Api', function() { let hasThrown = await checkThrowAsync(async () => await api.route('GET', 'notes')); expect(hasThrown).toBe(true); - const response = await api.route('GET', 'notes', { token: 'mytoken' }) + const response = await api.route('GET', 'notes', { token: 'mytoken' }); expect(response.length).toBe(0); hasThrown = await checkThrowAsync(async () => await api.route('POST', 'notes', null, JSON.stringify({title:'testing'}))); @@ -324,37 +326,37 @@ describe('services_rest_Api', function() { }); it('should add tags to notes', async (done) => { - const tag = await Tag.save({ title: "mon étiquette" }); - const note = await Note.save({ title: "ma note" }); + const tag = await Tag.save({ title: 'mon étiquette' }); + const note = await Note.save({ title: 'ma note' }); const response = await api.route('POST', 'tags/' + tag.id + '/notes', null, JSON.stringify({ id: note.id, })); - const noteIds = await Tag.noteIds(tag.id); + const noteIds = await Tag.noteIds(tag.id); expect(noteIds[0]).toBe(note.id); done(); }); it('should remove tags from notes', async (done) => { - const tag = await Tag.save({ title: "mon étiquette" }); - const note = await Note.save({ title: "ma note" }); + const tag = await Tag.save({ title: 'mon étiquette' }); + const note = await Note.save({ title: 'ma note' }); await Tag.addNote(tag.id, note.id); const response = await api.route('DELETE', 'tags/' + tag.id + '/notes/' + note.id); - const noteIds = await Tag.noteIds(tag.id); + const noteIds = await Tag.noteIds(tag.id); expect(noteIds.length).toBe(0); done(); }); it('should list all tag notes', async (done) => { - const tag = await Tag.save({ title: "mon étiquette" }); - const tag2 = await Tag.save({ title: "mon étiquette 2" }); - const note1 = await Note.save({ title: "ma note un" }); - const note2 = await Note.save({ title: "ma note deux" }); + const tag = await Tag.save({ title: 'mon étiquette' }); + const tag2 = await Tag.save({ title: 'mon étiquette 2' }); + const note1 = await Note.save({ title: 'ma note un' }); + const note2 = await Note.save({ title: 'ma note deux' }); await Tag.addNote(tag.id, note1.id); await Tag.addNote(tag.id, note2.id); diff --git a/CliClient/tests/synchronizer.js b/CliClient/tests/synchronizer.js index 968e1ed7c..3b9d64f59 100644 --- a/CliClient/tests/synchronizer.js +++ b/CliClient/tests/synchronizer.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + require('app-module-path').addPath(__dirname); const { time } = require('lib/time-utils.js'); @@ -99,8 +101,8 @@ describe('Synchronizer', function() { }); it('should create remote items', asyncTest(async () => { - let folder = await Folder.save({ title: "folder1" }); - await Note.save({ title: "un", parent_id: folder.id }); + let folder = await Folder.save({ title: 'folder1' }); + await Note.save({ title: 'un', parent_id: folder.id }); let all = await allNotesFolders(); @@ -110,11 +112,11 @@ describe('Synchronizer', function() { })); it('should update remote items', asyncTest(async () => { - let folder = await Folder.save({ title: "folder1" }); - let note = await Note.save({ title: "un", parent_id: folder.id }); + let folder = await Folder.save({ title: 'folder1' }); + let note = await Note.save({ title: 'un', parent_id: folder.id }); await synchronizer().start(); - await Note.save({ title: "un UPDATE", id: note.id }); + await Note.save({ title: 'un UPDATE', id: note.id }); let all = await allNotesFolders(); await synchronizer().start(); @@ -123,8 +125,8 @@ describe('Synchronizer', function() { })); it('should create local items', asyncTest(async () => { - let folder = await Folder.save({ title: "folder1" }); - await Note.save({ title: "un", parent_id: folder.id }); + let folder = await Folder.save({ title: 'folder1' }); + await Note.save({ title: 'un', parent_id: folder.id }); await synchronizer().start(); await switchClient(2); @@ -137,8 +139,8 @@ describe('Synchronizer', function() { })); it('should update local items', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', parent_id: folder1.id }); await synchronizer().start(); await switchClient(2); @@ -148,7 +150,7 @@ describe('Synchronizer', function() { await sleep(0.1); let note2 = await Note.load(note1.id); - note2.title = "Updated on client 2"; + note2.title = 'Updated on client 2'; await Note.save(note2); note2 = await Note.load(note2.id); @@ -164,15 +166,15 @@ describe('Synchronizer', function() { })); it('should resolve note conflicts', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', parent_id: folder1.id }); await synchronizer().start(); await switchClient(2); await synchronizer().start(); let note2 = await Note.load(note1.id); - note2.title = "Updated on client 2"; + note2.title = 'Updated on client 2'; await Note.save(note2); note2 = await Note.load(note2.id); await synchronizer().start(); @@ -180,7 +182,7 @@ describe('Synchronizer', function() { await switchClient(1); let note2conf = await Note.load(note1.id); - note2conf.title = "Updated on client 1"; + note2conf.title = 'Updated on client 1'; await Note.save(note2conf); note2conf = await Note.load(note1.id); await synchronizer().start(); @@ -205,8 +207,8 @@ describe('Synchronizer', function() { })); it('should resolve folders conflicts', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', parent_id: folder1.id }); await synchronizer().start(); await switchClient(2); // ---------------------------------- @@ -216,7 +218,7 @@ describe('Synchronizer', function() { await sleep(0.1); let folder1_modRemote = await Folder.load(folder1.id); - folder1_modRemote.title = "folder1 UPDATE CLIENT 2"; + folder1_modRemote.title = 'folder1 UPDATE CLIENT 2'; await Folder.save(folder1_modRemote); folder1_modRemote = await Folder.load(folder1_modRemote.id); @@ -227,7 +229,7 @@ describe('Synchronizer', function() { await sleep(0.1); let folder1_modLocal = await Folder.load(folder1.id); - folder1_modLocal.title = "folder1 UPDATE CLIENT 1"; + folder1_modLocal.title = 'folder1 UPDATE CLIENT 1'; await Folder.save(folder1_modLocal); folder1_modLocal = await Folder.load(folder1.id); @@ -238,8 +240,8 @@ describe('Synchronizer', function() { })); it('should delete remote notes', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', parent_id: folder1.id }); await synchronizer().start(); await switchClient(2); @@ -261,8 +263,8 @@ describe('Synchronizer', function() { })); it('should not created deleted_items entries for items deleted via sync', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', parent_id: folder1.id }); await synchronizer().start(); await switchClient(2); @@ -283,9 +285,9 @@ describe('Synchronizer', function() { // property of the basicDelta() function is cleared properly at the end of a sync operation. If it is not cleared // it means items will no longer be deleted locally via sync. - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", parent_id: folder1.id }); - let note2 = await Note.save({ title: "deux", parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', parent_id: folder1.id }); + let note2 = await Note.save({ title: 'deux', parent_id: folder1.id }); let context1 = await synchronizer().start(); await switchClient(2); @@ -306,8 +308,8 @@ describe('Synchronizer', function() { })); it('should delete remote folder', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let folder2 = await Folder.save({ title: "folder2" }); + let folder1 = await Folder.save({ title: 'folder1' }); + let folder2 = await Folder.save({ title: 'folder2' }); await synchronizer().start(); await switchClient(2); @@ -325,8 +327,8 @@ describe('Synchronizer', function() { })); it('should delete local folder', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let folder2 = await Folder.save({ title: "folder2" }); + let folder1 = await Folder.save({ title: 'folder1' }); + let folder2 = await Folder.save({ title: 'folder2' }); let context1 = await synchronizer().start(); await switchClient(2); @@ -343,7 +345,7 @@ describe('Synchronizer', function() { })); it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); await synchronizer().start(); await switchClient(2); @@ -354,17 +356,17 @@ describe('Synchronizer', function() { await switchClient(1); - let note = await Note.save({ title: "note1", parent_id: folder1.id }); + let note = await Note.save({ title: 'note1', parent_id: folder1.id }); await synchronizer().start(); - let items = await allNotesFolders(); + let items = await allNotesFolders(); expect(items.length).toBe(1); expect(items[0].title).toBe('note1'); expect(items[0].is_conflict).toBe(1); })); it('should resolve conflict if note has been deleted remotely and locally', asyncTest(async () => { - let folder = await Folder.save({ title: "folder" }); - let note = await Note.save({ title: "note", parent_id: folder.title }); + let folder = await Folder.save({ title: 'folder' }); + let note = await Note.save({ title: 'note', parent_id: folder.title }); await synchronizer().start(); await switchClient(2); @@ -389,8 +391,8 @@ describe('Synchronizer', function() { // If client1 and 2 have two folders, client 1 deletes item 1 and client // 2 deletes item 2, they should both end up with no items after sync. - let folder1 = await Folder.save({ title: "folder1" }); - let folder2 = await Folder.save({ title: "folder2" }); + let folder1 = await Folder.save({ title: 'folder1' }); + let folder2 = await Folder.save({ title: 'folder2' }); await synchronizer().start(); await switchClient(2); @@ -424,8 +426,8 @@ describe('Synchronizer', function() { })); it('should handle conflict when remote note is deleted then local note is modified', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', parent_id: folder1.id }); await synchronizer().start(); await switchClient(2); @@ -456,9 +458,9 @@ describe('Synchronizer', function() { })); it('should handle conflict when remote folder is deleted then local folder is renamed', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let folder2 = await Folder.save({ title: "folder2" }); - let note1 = await Note.save({ title: "un", parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let folder2 = await Folder.save({ title: 'folder2' }); + let note1 = await Note.save({ title: 'un', parent_id: folder1.id }); await synchronizer().start(); await switchClient(2); @@ -486,11 +488,11 @@ describe('Synchronizer', function() { })); it('should allow duplicate folder titles', asyncTest(async () => { - let localF1 = await Folder.save({ title: "folder" }); + let localF1 = await Folder.save({ title: 'folder' }); await switchClient(2); - let remoteF2 = await Folder.save({ title: "folder" }); + let remoteF2 = await Folder.save({ title: 'folder' }); await synchronizer().start(); await switchClient(1); @@ -526,9 +528,9 @@ describe('Synchronizer', function() { masterKey = await loadEncryptionMasterKey(); } - let f1 = await Folder.save({ title: "folder" }); - let n1 = await Note.save({ title: "mynote" }); - let n2 = await Note.save({ title: "mynote2" }); + let f1 = await Folder.save({ title: 'folder' }); + let n1 = await Note.save({ title: 'mynote' }); + let n2 = await Note.save({ title: 'mynote2' }); let tag = await Tag.save({ title: 'mytag' }); let context1 = await synchronizer().start(); @@ -577,22 +579,22 @@ describe('Synchronizer', function() { })); it('should not sync notes with conflicts', asyncTest(async () => { - let f1 = await Folder.save({ title: "folder" }); - let n1 = await Note.save({ title: "mynote", parent_id: f1.id, is_conflict: 1 }); + let f1 = await Folder.save({ title: 'folder' }); + let n1 = await Note.save({ title: 'mynote', parent_id: f1.id, is_conflict: 1 }); await synchronizer().start(); await switchClient(2); await synchronizer().start(); let notes = await Note.all(); - let folders = await Folder.all() + let folders = await Folder.all(); expect(notes.length).toBe(0); expect(folders.length).toBe(1); })); it('should not try to delete on remote conflicted notes that have been deleted', asyncTest(async () => { - let f1 = await Folder.save({ title: "folder" }); - let n1 = await Note.save({ title: "mynote", parent_id: f1.id }); + let f1 = await Folder.save({ title: 'folder' }); + let n1 = await Note.save({ title: 'mynote', parent_id: f1.id }); await synchronizer().start(); await switchClient(2); @@ -604,15 +606,15 @@ describe('Synchronizer', function() { expect(deletedItems.length).toBe(0); })); - + async function ignorableNoteConflictTest(withEncryption) { if (withEncryption) { Setting.setValue('encryption.enabled', true); await loadEncryptionMasterKey(); } - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', is_todo: 1, parent_id: folder1.id }); await synchronizer().start(); await switchClient(2); @@ -650,7 +652,7 @@ describe('Synchronizer', function() { let notes = await Note.all(); expect(notes.length).toBe(1); expect(notes[0].id).toBe(note1.id); - expect(notes[0].todo_completed).toBe(note2.todo_completed); + expect(notes[0].todo_completed).toBe(note2.todo_completed); } else { // If the notes are encrypted however it's not possible to do this kind of // smart conflict resolving since we don't know the content, so in that @@ -673,13 +675,13 @@ describe('Synchronizer', function() { })); it('items should be downloaded again when user cancels in the middle of delta operation', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', is_todo: 1, parent_id: folder1.id }); await synchronizer().start(); await switchClient(2); - synchronizer().testingHooks_ = ['cancelDeltaLoop2']; + synchronizer().testingHooks_ = ['cancelDeltaLoop2']; let context = await synchronizer().start(); let notes = await Note.all(); expect(notes.length).toBe(0); @@ -691,13 +693,13 @@ describe('Synchronizer', function() { })); it('should skip items that cannot be synced', asyncTest(async () => { - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', is_todo: 1, parent_id: folder1.id }); const noteId = note1.id; await synchronizer().start(); let disabledItems = await BaseItem.syncDisabledItems(syncTargetId()); expect(disabledItems.length).toBe(0); - await Note.save({ id: noteId, title: "un mod", }); + await Note.save({ id: noteId, title: 'un mod' }); synchronizer().testingHooks_ = ['notesRejectedByTarget']; await synchronizer().start(); synchronizer().testingHooks_ = []; @@ -719,8 +721,8 @@ describe('Synchronizer', function() { it('notes and folders should get encrypted when encryption is enabled', asyncTest(async () => { Setting.setValue('encryption.enabled', true); const masterKey = await loadEncryptionMasterKey(); - let folder1 = await Folder.save({ title: "folder1" }); - let note1 = await Note.save({ title: "un", body: 'to be encrypted', parent_id: folder1.id }); + let folder1 = await Folder.save({ title: 'folder1' }); + let note1 = await Note.save({ title: 'un', body: 'to be encrypted', parent_id: folder1.id }); await synchronizer().start(); // After synchronisation, remote items should be encrypted but local ones remain plain text note1 = await Note.load(note1.id); @@ -762,7 +764,7 @@ describe('Synchronizer', function() { // Enable encryption on client 1 and sync an item Setting.setValue('encryption.enabled', true); await loadEncryptionMasterKey(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); await synchronizer().start(); await switchClient(2); @@ -802,7 +804,7 @@ describe('Synchronizer', function() { // Decrypt all the data. Now change the title and sync again - this time the changes should be transmitted await decryptionWorker().start(); - folder1_2 = await Folder.save({ id: folder1.id, title: "change test" }); + await Folder.save({ id: folder1.id, title: 'change test' }); // If we sync now, this time client 1 should get the changes we did earlier await synchronizer().start(); @@ -818,11 +820,11 @@ describe('Synchronizer', function() { it('should encrypt existing notes too when enabling E2EE', asyncTest(async () => { // First create a folder, without encryption enabled, and sync it - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); await synchronizer().start(); - let files = await fileApi().list() + let files = await fileApi().list(); let content = await fileApi().get(files.items[0].path); - expect(content.indexOf('folder1') >= 0).toBe(true) + expect(content.indexOf('folder1') >= 0).toBe(true); // Then enable encryption and sync again let masterKey = await encryptionService().generateMasterKey('123456'); @@ -830,10 +832,10 @@ describe('Synchronizer', function() { await encryptionService().enableEncryption(masterKey, '123456'); await encryptionService().loadMasterKeysFromSettings(); await synchronizer().start(); - + // Even though the folder has not been changed it should have been synced again so that // an encrypted version of it replaces the decrypted version. - files = await fileApi().list() + files = await fileApi().list(); expect(files.items.length).toBe(2); // By checking that the folder title is not present, we can confirm that the item has indeed been encrypted // One of the two items is the master key @@ -846,12 +848,12 @@ describe('Synchronizer', function() { it('should sync resources', asyncTest(async () => { while (insideBeforeEach) await time.msleep(500); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); let resource1 = (await Resource.all())[0]; let resourcePath1 = Resource.fullPath(resource1); - await synchronizer().start(); + await synchronizer().start(); expect((await remoteNotesFoldersResources()).length).toBe(3); await switchClient(2); @@ -864,7 +866,7 @@ describe('Synchronizer', function() { expect(resource1_2.id).toBe(resource1.id); expect(ls.fetch_status).toBe(Resource.FETCH_STATUS_IDLE); - const fetcher = new ResourceFetcher(() => { return synchronizer().api() }); + const fetcher = new ResourceFetcher(() => { return synchronizer().api(); }); fetcher.queueDownload_(resource1_2.id); await fetcher.waitForAllFinished(); @@ -879,7 +881,7 @@ describe('Synchronizer', function() { it('should handle resource download errors', asyncTest(async () => { while (insideBeforeEach) await time.msleep(500); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); let resource1 = (await Resource.all())[0]; @@ -892,8 +894,8 @@ describe('Synchronizer', function() { const fetcher = new ResourceFetcher(() => { return { // Simulate a failed download - get: () => { return new Promise((resolve, reject) => { reject(new Error('did not work')) }); } - } }); + get: () => { return new Promise((resolve, reject) => { reject(new Error('did not work')); }); }, + }; }); fetcher.queueDownload_(resource1.id); await fetcher.waitForAllFinished(); @@ -906,10 +908,10 @@ describe('Synchronizer', function() { it('should set the resource file size if it is missing', asyncTest(async () => { while (insideBeforeEach) await time.msleep(500); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); - await synchronizer().start(); + await synchronizer().start(); await switchClient(2); @@ -919,7 +921,7 @@ describe('Synchronizer', function() { r1 = await Resource.load(r1.id); expect(r1.size).toBe(-1); - const fetcher = new ResourceFetcher(() => { return synchronizer().api() }); + const fetcher = new ResourceFetcher(() => { return synchronizer().api(); }); fetcher.queueDownload_(r1.id); await fetcher.waitForAllFinished(); r1 = await Resource.load(r1.id); @@ -929,7 +931,7 @@ describe('Synchronizer', function() { it('should delete resources', asyncTest(async () => { while (insideBeforeEach) await time.msleep(500); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); let resource1 = (await Resource.all())[0]; @@ -963,7 +965,7 @@ describe('Synchronizer', function() { Setting.setValue('encryption.enabled', true); const masterKey = await loadEncryptionMasterKey(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); let resource1 = (await Resource.all())[0]; @@ -976,10 +978,10 @@ describe('Synchronizer', function() { Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); await encryptionService().loadMasterKeysFromSettings(); - const fetcher = new ResourceFetcher(() => { return synchronizer().api() }); + const fetcher = new ResourceFetcher(() => { return synchronizer().api(); }); fetcher.queueDownload_(resource1.id); await fetcher.waitForAllFinished(); - + let resource1_2 = (await Resource.all())[0]; resource1_2 = await Resource.decrypt(resource1_2); let resourcePath1_2 = Resource.fullPath(resource1_2); @@ -991,7 +993,7 @@ describe('Synchronizer', function() { Setting.setValue('encryption.enabled', true); const masterKey = await loadEncryptionMasterKey(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); await synchronizer().start(); let allEncrypted = await allSyncTargetItemsEncrypted(); @@ -1012,7 +1014,7 @@ describe('Synchronizer', function() { Setting.setValue('encryption.enabled', true); const masterKey = await loadEncryptionMasterKey(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); await synchronizer().start(); await switchClient(2); @@ -1028,16 +1030,16 @@ describe('Synchronizer', function() { // Now supply the password, and decrypt the items Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); - await encryptionService().loadMasterKeysFromSettings(); + await encryptionService().loadMasterKeysFromSettings(); await decryptionWorker().start(); // Try to disable encryption again - hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption()); + const hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption()); expect(hasThrown).toBe(false); // If we sync now the target should receive the decrypted items await synchronizer().start(); - allEncrypted = await allSyncTargetItemsEncrypted(); + const allEncrypted = await allSyncTargetItemsEncrypted(); expect(allEncrypted).toBe(false); })); @@ -1045,7 +1047,7 @@ describe('Synchronizer', function() { Setting.setValue('encryption.enabled', true); const masterKey = await loadEncryptionMasterKey(); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); let resource1 = (await Resource.all())[0]; @@ -1059,7 +1061,7 @@ describe('Synchronizer', function() { Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456'); await encryptionService().loadMasterKeysFromSettings(); - const fetcher = new ResourceFetcher(() => { return synchronizer().api() }); + const fetcher = new ResourceFetcher(() => { return synchronizer().api(); }); fetcher.queueDownload_(resource1.id); await fetcher.waitForAllFinished(); await decryptionWorker().start(); @@ -1071,10 +1073,9 @@ describe('Synchronizer', function() { it('should encrypt remote resources after encryption has been enabled', asyncTest(async () => { while (insideBeforeEach) await time.msleep(100); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); - let resource1 = (await Resource.all())[0]; await synchronizer().start(); expect(await allSyncTargetItemsEncrypted()).toBe(false); @@ -1091,7 +1092,7 @@ describe('Synchronizer', function() { it('should upload encrypted resource, but it should not mark the blob as encrypted locally', asyncTest(async () => { while (insideBeforeEach) await time.msleep(100); - let folder1 = await Folder.save({ title: "folder1" }); + let folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); const masterKey = await loadEncryptionMasterKey(); @@ -1104,8 +1105,8 @@ describe('Synchronizer', function() { })); it('should create remote items with UTF-8 content', asyncTest(async () => { - let folder = await Folder.save({ title: "Fahrräder" }); - await Note.save({ title: "Fahrräder", body: "Fahrräder", parent_id: folder.id }); + let folder = await Folder.save({ title: 'Fahrräder' }); + await Note.save({ title: 'Fahrräder', body: 'Fahrräder', parent_id: folder.id }); let all = await allNotesFolders(); await synchronizer().start(); @@ -1113,21 +1114,21 @@ describe('Synchronizer', function() { await localNotesFoldersSameAsRemote(all, expect); })); - it("should update remote items but not pull remote changes", asyncTest(async () => { - let folder = await Folder.save({ title: "folder1" }); - let note = await Note.save({ title: "un", parent_id: folder.id }); + it('should update remote items but not pull remote changes', asyncTest(async () => { + let folder = await Folder.save({ title: 'folder1' }); + let note = await Note.save({ title: 'un', parent_id: folder.id }); await synchronizer().start(); await switchClient(2); await synchronizer().start(); - await Note.save({ title: "deux", parent_id: folder.id }); + await Note.save({ title: 'deux', parent_id: folder.id }); await synchronizer().start(); await switchClient(1); - await Note.save({ title: "un UPDATE", id: note.id }); - await synchronizer().start({ syncSteps: ["update_remote"] }); + await Note.save({ title: 'un UPDATE', id: note.id }); + await synchronizer().start({ syncSteps: ['update_remote'] }); let all = await allNotesFolders(); expect(all.length).toBe(2); @@ -1135,10 +1136,10 @@ describe('Synchronizer', function() { await synchronizer().start(); let note2 = await Note.load(note.id); - expect(note2.title).toBe("un UPDATE"); + expect(note2.title).toBe('un UPDATE'); })); - it("should create a new Welcome notebook on each client", asyncTest(async () => { + it('should create a new Welcome notebook on each client', asyncTest(async () => { // Create the Welcome items on two separate clients await WelcomeUtils.createWelcomeItems(); @@ -1151,7 +1152,7 @@ describe('Synchronizer', function() { const beforeNoteCount = (await Note.all()).length; expect(beforeFolderCount === 1).toBe(true); expect(beforeNoteCount > 1).toBe(true); - + await synchronizer().start(); const afterFolderCount = (await Folder.all()).length; @@ -1175,7 +1176,7 @@ describe('Synchronizer', function() { expect(f1_1.title).toBe('Welcome MOD'); })); - it("should not save revisions when updating a note via sync", asyncTest(async () => { + it('should not save revisions when updating a note via sync', asyncTest(async () => { // When a note is updated, a revision of the original is created. // Here, on client 1, the note is updated for the first time, however since it is // via sync, we don't create a revision - that revision has already been created on client @@ -1201,7 +1202,7 @@ describe('Synchronizer', function() { expect(allRevs2[0].id).toBe(allRevs1[0].id); })); - it("should not save revisions when deleting a note via sync", asyncTest(async () => { + it('should not save revisions when deleting a note via sync', asyncTest(async () => { const n1 = await Note.save({ title: 'testing' }); await synchronizer().start(); @@ -1228,7 +1229,7 @@ describe('Synchronizer', function() { expect(notes.length).toBe(0); })); - it("should not save revisions when an item_change has been generated as a result of a sync", asyncTest(async () => { + it('should not save revisions when an item_change has been generated as a result of a sync', asyncTest(async () => { // When a note is modified an item_change object is going to be created. This // is used for example to tell the search engine, when note should be indexed. It is // also used by the revision service to tell what note should get a new revision. @@ -1266,7 +1267,7 @@ describe('Synchronizer', function() { } })); - it("should handle case when new rev is created on client, then older rev arrives later via sync", asyncTest(async () => { + it('should handle case when new rev is created on client, then older rev arrives later via sync', asyncTest(async () => { // - C1 creates note 1 // - C1 modifies note 1 - REV1 created // - C1 sync @@ -1304,7 +1305,7 @@ describe('Synchronizer', function() { expect((await revisionService().revisionNote(revisions, 1)).title).toBe('note REV2'); })); - it("should not download resources over the limit", asyncTest(async () => { + it('should not download resources over the limit', asyncTest(async () => { const note1 = await Note.save({ title: 'note' }); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); await synchronizer().start(); @@ -1322,7 +1323,7 @@ describe('Synchronizer', function() { expect(syncItems[1].sync_disabled).toBe(1); })); - it("should not upload a resource if it has not been fetched yet", asyncTest(async () => { + it('should not upload a resource if it has not been fetched yet', asyncTest(async () => { // In some rare cases, the synchronizer might try to upload a resource even though it // doesn't have the resource file. It can happen in this situation: // - C1 create resource @@ -1351,7 +1352,7 @@ describe('Synchronizer', function() { it('should decrypt the resource metadata, but not try to decrypt the file, if it is not present', asyncTest(async () => { const note1 = await Note.save({ title: 'note' }); - await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); + await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); const masterKey = await loadEncryptionMasterKey(); await encryptionService().enableEncryption(masterKey, '123456'); await encryptionService().loadMasterKeysFromSettings(); @@ -1370,7 +1371,7 @@ describe('Synchronizer', function() { expect(!!resource.encryption_applied).toBe(false); expect(!!resource.encryption_blob_encrypted).toBe(true); - const resourceFetcher = new ResourceFetcher(() => { return synchronizer().api() }); + const resourceFetcher = new ResourceFetcher(() => { return synchronizer().api(); }); await resourceFetcher.start(); await resourceFetcher.waitForAllFinished(); @@ -1400,7 +1401,7 @@ describe('Synchronizer', function() { const dateInPast = revisionService().oldNoteCutOffDate_() - 1000; - const note1 = await Note.save({ title: 'ma note', updated_time: dateInPast, created_time: dateInPast }, { autoTimestamp: false }); + await Note.save({ title: 'ma note', updated_time: dateInPast, created_time: dateInPast }, { autoTimestamp: false }); const masterKey = await loadEncryptionMasterKey(); await encryptionService().enableEncryption(masterKey, '123456'); await encryptionService().loadMasterKeysFromSettings(); diff --git a/CliClient/tests/test-utils.js b/CliClient/tests/test-utils.js index 1ced6ddd9..98d3649e2 100644 --- a/CliClient/tests/test-utils.js +++ b/CliClient/tests/test-utils.js @@ -1,3 +1,5 @@ +/* eslint-disable require-atomic-updates */ + const fs = require('fs-extra'); const { JoplinDatabase } = require('lib/joplin-database.js'); const { DatabaseDriverNode } = require('lib/database-driver-node.js'); @@ -13,7 +15,6 @@ const { Logger } = require('lib/logger.js'); const Setting = require('lib/models/Setting.js'); const MasterKey = require('lib/models/MasterKey'); const BaseItem = require('lib/models/BaseItem.js'); -const { Synchronizer } = require('lib/synchronizer.js'); const { FileApi } = require('lib/file-api.js'); const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js'); const { FileApiDriverLocal } = require('lib/file-api-driver-local.js'); @@ -67,7 +68,7 @@ SyncTargetRegistry.addClass(SyncTargetNextcloud); SyncTargetRegistry.addClass(SyncTargetDropbox); // const syncTargetId_ = SyncTargetRegistry.nameToId("nextcloud"); -const syncTargetId_ = SyncTargetRegistry.nameToId("memory"); +const syncTargetId_ = SyncTargetRegistry.nameToId('memory'); //const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem'); // const syncTargetId_ = SyncTargetRegistry.nameToId('dropbox'); const syncDir = __dirname + '/../tests/sync'; @@ -151,7 +152,7 @@ async function clearDatabase(id = null) { 'master_keys', 'item_changes', 'note_resources', - 'settings', + 'settings', 'deleted_items', 'sync_items', 'notes_normalized', @@ -186,7 +187,7 @@ async function setupDatabase(id = null) { await fs.unlink(filePath); } catch (error) { // Don't care if the file doesn't exist - }; + } databases_[id] = new JoplinDatabase(new DatabaseDriverNode()); databases_[id].setLogger(dbLogger); @@ -279,9 +280,9 @@ async function loadEncryptionMasterKey(id = null, useExisting = false) { masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); } else { // Use the one already available - materKey = await MasterKey.all(); - if (!materKey.length) throw new Error('No mater key available'); - masterKey = materKey[0]; + const masterKeys = await MasterKey.all(); + if (!masterKeys.length) throw new Error('No mater key available'); + masterKey = masterKeys[0]; } await service.loadMasterKey(masterKey, '123456', true); @@ -293,7 +294,7 @@ function fileApi() { if (fileApi_) return fileApi_; if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) { - fs.removeSync(syncDir) + fs.removeSync(syncDir); fs.mkdirpSync(syncDir, 0o755); fileApi_ = new FileApi(syncDir, new FileApiDriverLocal()); } else if (syncTargetId_ == SyncTargetRegistry.nameToId('memory')) { @@ -359,7 +360,7 @@ function asyncTest(callback) { } finally { done(); } - } + }; } async function allSyncTargetItemsEncrypted() { @@ -381,10 +382,10 @@ async function allSyncTargetItemsEncrypted() { if (remoteContent.type_ === BaseModel.TYPE_RESOURCE) { const content = await fileApi().get('.resource/' + remoteContent.id); totalCount++; - if (content.substr(0, 5) === 'JED01') output = encryptedCount++; + if (content.substr(0, 5) === 'JED01') encryptedCount++; } - if (!!remoteContent.encryption_applied) encryptedCount++; + if (remoteContent.encryption_applied) encryptedCount++; } if (!totalCount) throw new Error('No encryptable item on sync target'); diff --git a/CliClient/tests/urlUtils.js b/CliClient/tests/urlUtils.js index 5d910c456..c4e67ce09 100644 --- a/CliClient/tests/urlUtils.js +++ b/CliClient/tests/urlUtils.js @@ -1,7 +1,5 @@ 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) => { @@ -35,4 +33,4 @@ describe('urlUtils', function() { done(); }); -}); \ No newline at end of file +}); diff --git a/Clipper/joplin-webclipper/background.js b/Clipper/joplin-webclipper/background.js index f9fea65e2..b9e884727 100644 --- a/Clipper/joplin-webclipper/background.js +++ b/Clipper/joplin-webclipper/background.js @@ -1,13 +1,10 @@ let browser_ = null; -let browserName_ = null; if (typeof browser !== 'undefined') { browser_ = browser; browserSupportsPromises_ = true; - browserName_ = 'firefox'; } else if (typeof chrome !== 'undefined') { browser_ = chrome; browserSupportsPromises_ = false; - browserName_ = 'chrome'; } let env_ = null; @@ -21,7 +18,7 @@ window.joplinEnv = function() { const manifest = browser_.runtime.getManifest(); env_ = manifest.name.indexOf('[DEV]') >= 0 ? 'dev' : 'prod'; return env_; -} +}; async function browserCaptureVisibleTabs(windowId) { const options = { format: 'jpeg' }; @@ -58,7 +55,7 @@ browser_.runtime.onMessage.addListener(async (command) => { const zoom = await browserGetZoom(); const imageDataUrl = await browserCaptureVisibleTabs(null); - content = Object.assign({}, command.content); + const content = Object.assign({}, command.content); content.image_data_url = imageDataUrl; const newArea = Object.assign({}, command.content.crop_rect); @@ -68,13 +65,13 @@ browser_.runtime.onMessage.addListener(async (command) => { newArea.height *= zoom; content.crop_rect = newArea; - fetch(command.api_base_url + "/notes", { - method: "POST", + fetch(command.api_base_url + '/notes', { + method: 'POST', headers: { 'Accept': 'application/json', - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, - body: JSON.stringify(content) + body: JSON.stringify(content), }); } }); diff --git a/Clipper/joplin-webclipper/content_scripts/JSDOMParser.js b/Clipper/joplin-webclipper/content_scripts/JSDOMParser.js index dd7cb3a0a..4fb766c10 100644 --- a/Clipper/joplin-webclipper/content_scripts/JSDOMParser.js +++ b/Clipper/joplin-webclipper/content_scripts/JSDOMParser.js @@ -29,924 +29,924 @@ */ (function (global) { - // XML only defines these and the numeric ones: - - var entityTable = { - "lt": "<", - "gt": ">", - "amp": "&", - "quot": '"', - "apos": "'", - }; - - var reverseEntityTable = { - "<": "<", - ">": ">", - "&": "&", - '"': """, - "'": "'", - }; - - function encodeTextContentHTML(s) { - return s.replace(/[&<>]/g, function(x) { - return reverseEntityTable[x]; - }); - } - - function encodeHTML(s) { - return s.replace(/[&<>'"]/g, function(x) { - return reverseEntityTable[x]; - }); - } - - function decodeHTML(str) { - return str.replace(/&(quot|amp|apos|lt|gt);/g, function(match, tag) { - return entityTable[tag]; - }).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi, function(match, hex, numStr) { - var num = parseInt(hex || numStr, hex ? 16 : 10); // read num - return String.fromCharCode(num); - }); - } - - // When a style is set in JS, map it to the corresponding CSS attribute - var styleMap = { - "alignmentBaseline": "alignment-baseline", - "background": "background", - "backgroundAttachment": "background-attachment", - "backgroundClip": "background-clip", - "backgroundColor": "background-color", - "backgroundImage": "background-image", - "backgroundOrigin": "background-origin", - "backgroundPosition": "background-position", - "backgroundPositionX": "background-position-x", - "backgroundPositionY": "background-position-y", - "backgroundRepeat": "background-repeat", - "backgroundRepeatX": "background-repeat-x", - "backgroundRepeatY": "background-repeat-y", - "backgroundSize": "background-size", - "baselineShift": "baseline-shift", - "border": "border", - "borderBottom": "border-bottom", - "borderBottomColor": "border-bottom-color", - "borderBottomLeftRadius": "border-bottom-left-radius", - "borderBottomRightRadius": "border-bottom-right-radius", - "borderBottomStyle": "border-bottom-style", - "borderBottomWidth": "border-bottom-width", - "borderCollapse": "border-collapse", - "borderColor": "border-color", - "borderImage": "border-image", - "borderImageOutset": "border-image-outset", - "borderImageRepeat": "border-image-repeat", - "borderImageSlice": "border-image-slice", - "borderImageSource": "border-image-source", - "borderImageWidth": "border-image-width", - "borderLeft": "border-left", - "borderLeftColor": "border-left-color", - "borderLeftStyle": "border-left-style", - "borderLeftWidth": "border-left-width", - "borderRadius": "border-radius", - "borderRight": "border-right", - "borderRightColor": "border-right-color", - "borderRightStyle": "border-right-style", - "borderRightWidth": "border-right-width", - "borderSpacing": "border-spacing", - "borderStyle": "border-style", - "borderTop": "border-top", - "borderTopColor": "border-top-color", - "borderTopLeftRadius": "border-top-left-radius", - "borderTopRightRadius": "border-top-right-radius", - "borderTopStyle": "border-top-style", - "borderTopWidth": "border-top-width", - "borderWidth": "border-width", - "bottom": "bottom", - "boxShadow": "box-shadow", - "boxSizing": "box-sizing", - "captionSide": "caption-side", - "clear": "clear", - "clip": "clip", - "clipPath": "clip-path", - "clipRule": "clip-rule", - "color": "color", - "colorInterpolation": "color-interpolation", - "colorInterpolationFilters": "color-interpolation-filters", - "colorProfile": "color-profile", - "colorRendering": "color-rendering", - "content": "content", - "counterIncrement": "counter-increment", - "counterReset": "counter-reset", - "cursor": "cursor", - "direction": "direction", - "display": "display", - "dominantBaseline": "dominant-baseline", - "emptyCells": "empty-cells", - "enableBackground": "enable-background", - "fill": "fill", - "fillOpacity": "fill-opacity", - "fillRule": "fill-rule", - "filter": "filter", - "cssFloat": "float", - "floodColor": "flood-color", - "floodOpacity": "flood-opacity", - "font": "font", - "fontFamily": "font-family", - "fontSize": "font-size", - "fontStretch": "font-stretch", - "fontStyle": "font-style", - "fontVariant": "font-variant", - "fontWeight": "font-weight", - "glyphOrientationHorizontal": "glyph-orientation-horizontal", - "glyphOrientationVertical": "glyph-orientation-vertical", - "height": "height", - "imageRendering": "image-rendering", - "kerning": "kerning", - "left": "left", - "letterSpacing": "letter-spacing", - "lightingColor": "lighting-color", - "lineHeight": "line-height", - "listStyle": "list-style", - "listStyleImage": "list-style-image", - "listStylePosition": "list-style-position", - "listStyleType": "list-style-type", - "margin": "margin", - "marginBottom": "margin-bottom", - "marginLeft": "margin-left", - "marginRight": "margin-right", - "marginTop": "margin-top", - "marker": "marker", - "markerEnd": "marker-end", - "markerMid": "marker-mid", - "markerStart": "marker-start", - "mask": "mask", - "maxHeight": "max-height", - "maxWidth": "max-width", - "minHeight": "min-height", - "minWidth": "min-width", - "opacity": "opacity", - "orphans": "orphans", - "outline": "outline", - "outlineColor": "outline-color", - "outlineOffset": "outline-offset", - "outlineStyle": "outline-style", - "outlineWidth": "outline-width", - "overflow": "overflow", - "overflowX": "overflow-x", - "overflowY": "overflow-y", - "padding": "padding", - "paddingBottom": "padding-bottom", - "paddingLeft": "padding-left", - "paddingRight": "padding-right", - "paddingTop": "padding-top", - "page": "page", - "pageBreakAfter": "page-break-after", - "pageBreakBefore": "page-break-before", - "pageBreakInside": "page-break-inside", - "pointerEvents": "pointer-events", - "position": "position", - "quotes": "quotes", - "resize": "resize", - "right": "right", - "shapeRendering": "shape-rendering", - "size": "size", - "speak": "speak", - "src": "src", - "stopColor": "stop-color", - "stopOpacity": "stop-opacity", - "stroke": "stroke", - "strokeDasharray": "stroke-dasharray", - "strokeDashoffset": "stroke-dashoffset", - "strokeLinecap": "stroke-linecap", - "strokeLinejoin": "stroke-linejoin", - "strokeMiterlimit": "stroke-miterlimit", - "strokeOpacity": "stroke-opacity", - "strokeWidth": "stroke-width", - "tableLayout": "table-layout", - "textAlign": "text-align", - "textAnchor": "text-anchor", - "textDecoration": "text-decoration", - "textIndent": "text-indent", - "textLineThrough": "text-line-through", - "textLineThroughColor": "text-line-through-color", - "textLineThroughMode": "text-line-through-mode", - "textLineThroughStyle": "text-line-through-style", - "textLineThroughWidth": "text-line-through-width", - "textOverflow": "text-overflow", - "textOverline": "text-overline", - "textOverlineColor": "text-overline-color", - "textOverlineMode": "text-overline-mode", - "textOverlineStyle": "text-overline-style", - "textOverlineWidth": "text-overline-width", - "textRendering": "text-rendering", - "textShadow": "text-shadow", - "textTransform": "text-transform", - "textUnderline": "text-underline", - "textUnderlineColor": "text-underline-color", - "textUnderlineMode": "text-underline-mode", - "textUnderlineStyle": "text-underline-style", - "textUnderlineWidth": "text-underline-width", - "top": "top", - "unicodeBidi": "unicode-bidi", - "unicodeRange": "unicode-range", - "vectorEffect": "vector-effect", - "verticalAlign": "vertical-align", - "visibility": "visibility", - "whiteSpace": "white-space", - "widows": "widows", - "width": "width", - "wordBreak": "word-break", - "wordSpacing": "word-spacing", - "wordWrap": "word-wrap", - "writingMode": "writing-mode", - "zIndex": "z-index", - "zoom": "zoom", - }; - - // Elements that can be self-closing - var voidElems = { - "area": true, - "base": true, - "br": true, - "col": true, - "command": true, - "embed": true, - "hr": true, - "img": true, - "input": true, - "link": true, - "meta": true, - "param": true, - "source": true, - "wbr": true - }; - - var whitespace = [" ", "\t", "\n", "\r"]; - - // See http://www.w3schools.com/dom/dom_nodetype.asp - var nodeTypes = { - ELEMENT_NODE: 1, - ATTRIBUTE_NODE: 2, - TEXT_NODE: 3, - CDATA_SECTION_NODE: 4, - ENTITY_REFERENCE_NODE: 5, - ENTITY_NODE: 6, - PROCESSING_INSTRUCTION_NODE: 7, - COMMENT_NODE: 8, - DOCUMENT_NODE: 9, - DOCUMENT_TYPE_NODE: 10, - DOCUMENT_FRAGMENT_NODE: 11, - NOTATION_NODE: 12 - }; - - function getElementsByTagName(tag) { - tag = tag.toUpperCase(); - var elems = []; - var allTags = (tag === "*"); - function getElems(node) { - var length = node.children.length; - for (var i = 0; i < length; i++) { - var child = node.children[i]; - if (allTags || (child.tagName === tag)) - elems.push(child); - getElems(child); - } - } - getElems(this); - return elems; - } - - var Node = function () {}; - - Node.prototype = { - attributes: null, - childNodes: null, - localName: null, - nodeName: null, - parentNode: null, - textContent: null, - nextSibling: null, - previousSibling: null, - - get firstChild() { - return this.childNodes[0] || null; - }, - - get firstElementChild() { - return this.children[0] || null; - }, - - get lastChild() { - return this.childNodes[this.childNodes.length - 1] || null; - }, - - get lastElementChild() { - return this.children[this.children.length - 1] || null; - }, - - appendChild: function (child) { - if (child.parentNode) { - child.parentNode.removeChild(child); - } - - var last = this.lastChild; - if (last) - last.nextSibling = child; - child.previousSibling = last; - - if (child.nodeType === Node.ELEMENT_NODE) { - child.previousElementSibling = this.children[this.children.length - 1] || null; - this.children.push(child); - child.previousElementSibling && (child.previousElementSibling.nextElementSibling = child); - } - this.childNodes.push(child); - child.parentNode = this; - }, - - removeChild: function (child) { - var childNodes = this.childNodes; - var childIndex = childNodes.indexOf(child); - if (childIndex === -1) { - throw "removeChild: node not found"; - } else { - child.parentNode = null; - var prev = child.previousSibling; - var next = child.nextSibling; - if (prev) - prev.nextSibling = next; - if (next) - next.previousSibling = prev; - - if (child.nodeType === Node.ELEMENT_NODE) { - prev = child.previousElementSibling; - next = child.nextElementSibling; - if (prev) - prev.nextElementSibling = next; - if (next) - next.previousElementSibling = prev; - this.children.splice(this.children.indexOf(child), 1); - } - - child.previousSibling = child.nextSibling = null; - child.previousElementSibling = child.nextElementSibling = null; - - return childNodes.splice(childIndex, 1)[0]; - } - }, - - replaceChild: function (newNode, oldNode) { - var childNodes = this.childNodes; - var childIndex = childNodes.indexOf(oldNode); - if (childIndex === -1) { - throw "replaceChild: node not found"; - } else { - // This will take care of updating the new node if it was somewhere else before: - if (newNode.parentNode) - newNode.parentNode.removeChild(newNode); - - childNodes[childIndex] = newNode; - - // update the new node's sibling properties, and its new siblings' sibling properties - newNode.nextSibling = oldNode.nextSibling; - newNode.previousSibling = oldNode.previousSibling; - if (newNode.nextSibling) - newNode.nextSibling.previousSibling = newNode; - if (newNode.previousSibling) - newNode.previousSibling.nextSibling = newNode; - - newNode.parentNode = this; - - // Now deal with elements before we clear out those values for the old node, - // because it can help us take shortcuts here: - if (newNode.nodeType === Node.ELEMENT_NODE) { - if (oldNode.nodeType === Node.ELEMENT_NODE) { - // Both were elements, which makes this easier, we just swap things out: - newNode.previousElementSibling = oldNode.previousElementSibling; - newNode.nextElementSibling = oldNode.nextElementSibling; - if (newNode.previousElementSibling) - newNode.previousElementSibling.nextElementSibling = newNode; - if (newNode.nextElementSibling) - newNode.nextElementSibling.previousElementSibling = newNode; - this.children[this.children.indexOf(oldNode)] = newNode; - } else { - // Hard way: - newNode.previousElementSibling = (function() { - for (var i = childIndex - 1; i >= 0; i--) { - if (childNodes[i].nodeType === Node.ELEMENT_NODE) - return childNodes[i]; - } - return null; - })(); - if (newNode.previousElementSibling) { - newNode.nextElementSibling = newNode.previousElementSibling.nextElementSibling; - } else { - newNode.nextElementSibling = (function() { - for (var i = childIndex + 1; i < childNodes.length; i++) { - if (childNodes[i].nodeType === Node.ELEMENT_NODE) - return childNodes[i]; - } - return null; - })(); - } - if (newNode.previousElementSibling) - newNode.previousElementSibling.nextElementSibling = newNode; - if (newNode.nextElementSibling) - newNode.nextElementSibling.previousElementSibling = newNode; - - if (newNode.nextElementSibling) - this.children.splice(this.children.indexOf(newNode.nextElementSibling), 0, newNode); - else - this.children.push(newNode); - } - } else if (oldNode.nodeType === Node.ELEMENT_NODE) { - // new node is not an element node. - // if the old one was, update its element siblings: - if (oldNode.previousElementSibling) - oldNode.previousElementSibling.nextElementSibling = oldNode.nextElementSibling; - if (oldNode.nextElementSibling) - oldNode.nextElementSibling.previousElementSibling = oldNode.previousElementSibling; - this.children.splice(this.children.indexOf(oldNode), 1); - - // If the old node wasn't an element, neither the new nor the old node was an element, - // and the children array and its members shouldn't need any updating. - } - - - oldNode.parentNode = null; - oldNode.previousSibling = null; - oldNode.nextSibling = null; - if (oldNode.nodeType === Node.ELEMENT_NODE) { - oldNode.previousElementSibling = null; - oldNode.nextElementSibling = null; - } - return oldNode; - } - }, - - __JSDOMParser__: true, - }; - - for (var nodeType in nodeTypes) { - Node[nodeType] = Node.prototype[nodeType] = nodeTypes[nodeType]; - } - - var Attribute = function (name, value) { - this.name = name; - this._value = value; - }; - - Attribute.prototype = { - get value() { - return this._value; - }, - setValue: function(newValue) { - this._value = newValue; - }, - getEncodedValue: function() { - return encodeHTML(this._value); - }, - }; - - var Comment = function () { - this.childNodes = []; - }; - - Comment.prototype = { - __proto__: Node.prototype, - - nodeName: "#comment", - nodeType: Node.COMMENT_NODE - }; - - var Text = function () { - this.childNodes = []; - }; - - Text.prototype = { - __proto__: Node.prototype, - - nodeName: "#text", - nodeType: Node.TEXT_NODE, - get textContent() { - if (typeof this._textContent === "undefined") { - this._textContent = decodeHTML(this._innerHTML || ""); - } - return this._textContent; - }, - get innerHTML() { - if (typeof this._innerHTML === "undefined") { - this._innerHTML = encodeTextContentHTML(this._textContent || ""); - } - return this._innerHTML; - }, - - set innerHTML(newHTML) { - this._innerHTML = newHTML; - delete this._textContent; - }, - set textContent(newText) { - this._textContent = newText; - delete this._innerHTML; - }, - }; - - var Document = function (url) { - this.documentURI = url; - this.styleSheets = []; - this.childNodes = []; - this.children = []; - }; - - Document.prototype = { - __proto__: Node.prototype, - - nodeName: "#document", - nodeType: Node.DOCUMENT_NODE, - title: "", - - getElementsByTagName: getElementsByTagName, - - getElementById: function (id) { - function getElem(node) { - var length = node.children.length; - if (node.id === id) - return node; - for (var i = 0; i < length; i++) { - var el = getElem(node.children[i]); - if (el) - return el; - } - return null; - } - return getElem(this); - }, - - createElement: function (tag) { - var node = new Element(tag); - return node; - }, - - createTextNode: function (text) { - var node = new Text(); - node.textContent = text; - return node; - }, - - get baseURI() { - if (!this.hasOwnProperty("_baseURI")) { - this._baseURI = this.documentURI; - var baseElements = this.getElementsByTagName("base"); - var href = baseElements[0] && baseElements[0].getAttribute("href"); - if (href) { - try { - this._baseURI = (new URL(href, this._baseURI)).href; - } catch (ex) {/* Just fall back to documentURI */} - } - } - return this._baseURI; - }, - }; - - var Element = function (tag) { - // We use this to find the closing tag. - this._matchingTag = tag; - // We're explicitly a non-namespace aware parser, we just pretend it's all HTML. - var lastColonIndex = tag.lastIndexOf(":"); - if (lastColonIndex != -1) { - tag = tag.substring(lastColonIndex + 1); - } - this.attributes = []; - this.childNodes = []; - this.children = []; - this.nextElementSibling = this.previousElementSibling = null; - this.localName = tag.toLowerCase(); - this.tagName = tag.toUpperCase(); - this.style = new Style(this); - }; - - Element.prototype = { - __proto__: Node.prototype, - - nodeType: Node.ELEMENT_NODE, - - getElementsByTagName: getElementsByTagName, - - get className() { - return this.getAttribute("class") || ""; - }, - - set className(str) { - this.setAttribute("class", str); - }, - - get id() { - return this.getAttribute("id") || ""; - }, - - set id(str) { - this.setAttribute("id", str); - }, - - get href() { - return this.getAttribute("href") || ""; - }, - - set href(str) { - this.setAttribute("href", str); - }, - - get src() { - return this.getAttribute("src") || ""; - }, - - set src(str) { - this.setAttribute("src", str); - }, - - get srcset() { - return this.getAttribute("srcset") || ""; - }, - - set srcset(str) { - this.setAttribute("srcset", str); - }, - - get nodeName() { - return this.tagName; - }, - - get innerHTML() { - function getHTML(node) { - var i = 0; - for (i = 0; i < node.childNodes.length; i++) { - var child = node.childNodes[i]; - if (child.localName) { - arr.push("<" + child.localName); - - // serialize attribute list - for (var j = 0; j < child.attributes.length; j++) { - var attr = child.attributes[j]; - // the attribute value will be HTML escaped. - var val = attr.getEncodedValue(); - var quote = (val.indexOf('"') === -1 ? '"' : "'"); - arr.push(" " + attr.name + "=" + quote + val + quote); - } - - if (child.localName in voidElems && !child.childNodes.length) { - // if this is a self-closing element, end it here - arr.push("/>"); - } else { - // otherwise, add its children - arr.push(">"); - getHTML(child); - arr.push(""); - } - } else { - // This is a text node, so asking for innerHTML won't recurse. - arr.push(child.innerHTML); - } - } - } - - // Using Array.join() avoids the overhead from lazy string concatenation. - // See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes - var arr = []; - getHTML(this); - return arr.join(""); - }, - - set innerHTML(html) { - var parser = new JSDOMParser(); - var node = parser.parse(html); - var i; - for (i = this.childNodes.length; --i >= 0;) { - this.childNodes[i].parentNode = null; - } - this.childNodes = node.childNodes; - this.children = node.children; - for (i = this.childNodes.length; --i >= 0;) { - this.childNodes[i].parentNode = this; - } - }, - - set textContent(text) { - // clear parentNodes for existing children - for (var i = this.childNodes.length; --i >= 0;) { - this.childNodes[i].parentNode = null; - } - - var node = new Text(); - this.childNodes = [ node ]; - this.children = []; - node.textContent = text; - node.parentNode = this; - }, - - get textContent() { - function getText(node) { - var nodes = node.childNodes; - for (var i = 0; i < nodes.length; i++) { - var child = nodes[i]; - if (child.nodeType === 3) { - text.push(child.textContent); - } else { - getText(child); - } - } - } - - // Using Array.join() avoids the overhead from lazy string concatenation. - // See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes - var text = []; - getText(this); - return text.join(""); - }, - - getAttribute: function (name) { - for (var i = this.attributes.length; --i >= 0;) { - var attr = this.attributes[i]; - if (attr.name === name) { - return attr.value; - } - } - return undefined; - }, - - setAttribute: function (name, value) { - for (var i = this.attributes.length; --i >= 0;) { - var attr = this.attributes[i]; - if (attr.name === name) { - attr.setValue(value); - return; - } - } - this.attributes.push(new Attribute(name, value)); - }, - - removeAttribute: function (name) { - for (var i = this.attributes.length; --i >= 0;) { - var attr = this.attributes[i]; - if (attr.name === name) { - this.attributes.splice(i, 1); - break; - } - } - }, - - hasAttribute: function (name) { - return this.attributes.some(function (attr) { - return attr.name == name; - }); - }, - }; - - var Style = function (node) { - this.node = node; - }; - - // getStyle() and setStyle() use the style attribute string directly. This - // won't be very efficient if there are a lot of style manipulations, but - // it's the easiest way to make sure the style attribute string and the JS - // style property stay in sync. Readability.js doesn't do many style - // manipulations, so this should be okay. - Style.prototype = { - getStyle: function (styleName) { - var attr = this.node.getAttribute("style"); - if (!attr) - return undefined; - - var styles = attr.split(";"); - for (var i = 0; i < styles.length; i++) { - var style = styles[i].split(":"); - var name = style[0].trim(); - if (name === styleName) - return style[1].trim(); - } - - return undefined; - }, - - setStyle: function (styleName, styleValue) { - var value = this.node.getAttribute("style") || ""; - var index = 0; - do { - var next = value.indexOf(";", index) + 1; - var length = next - index - 1; - var style = (length > 0 ? value.substr(index, length) : value.substr(index)); - if (style.substr(0, style.indexOf(":")).trim() === styleName) { - value = value.substr(0, index).trim() + (next ? " " + value.substr(next).trim() : ""); - break; - } - index = next; - } while (index); - - value += " " + styleName + ": " + styleValue + ";"; - this.node.setAttribute("style", value.trim()); - } - }; - - // For each item in styleMap, define a getter and setter on the style - // property. - for (var jsName in styleMap) { - (function (cssName) { - Style.prototype.__defineGetter__(jsName, function () { - return this.getStyle(cssName); - }); - Style.prototype.__defineSetter__(jsName, function (value) { - this.setStyle(cssName, value); - }); - })(styleMap[jsName]); - } - - var JSDOMParser = function () { - this.currentChar = 0; - - // In makeElementNode() we build up many strings one char at a time. Using - // += for this results in lots of short-lived intermediate strings. It's - // better to build an array of single-char strings and then join() them - // together at the end. And reusing a single array (i.e. |this.strBuf|) - // over and over for this purpose uses less memory than using a new array - // for each string. - this.strBuf = []; - - // Similarly, we reuse this array to return the two arguments from - // makeElementNode(), which saves us from having to allocate a new array - // every time. - this.retPair = []; - - this.errorState = ""; - }; - - JSDOMParser.prototype = { - error: function(m) { - dump("JSDOMParser error: " + m + "\n"); - this.errorState += m + "\n"; - }, - - /** + // XML only defines these and the numeric ones: + + var entityTable = { + 'lt': '<', + 'gt': '>', + 'amp': '&', + 'quot': '"', + 'apos': '\'', + }; + + var reverseEntityTable = { + '<': '<', + '>': '>', + '&': '&', + '"': '"', + '\'': ''', + }; + + function encodeTextContentHTML(s) { + return s.replace(/[&<>]/g, function(x) { + return reverseEntityTable[x]; + }); + } + + function encodeHTML(s) { + return s.replace(/[&<>'"]/g, function(x) { + return reverseEntityTable[x]; + }); + } + + function decodeHTML(str) { + return str.replace(/&(quot|amp|apos|lt|gt);/g, function(match, tag) { + return entityTable[tag]; + }).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi, function(match, hex, numStr) { + var num = parseInt(hex || numStr, hex ? 16 : 10); // read num + return String.fromCharCode(num); + }); + } + + // When a style is set in JS, map it to the corresponding CSS attribute + var styleMap = { + 'alignmentBaseline': 'alignment-baseline', + 'background': 'background', + 'backgroundAttachment': 'background-attachment', + 'backgroundClip': 'background-clip', + 'backgroundColor': 'background-color', + 'backgroundImage': 'background-image', + 'backgroundOrigin': 'background-origin', + 'backgroundPosition': 'background-position', + 'backgroundPositionX': 'background-position-x', + 'backgroundPositionY': 'background-position-y', + 'backgroundRepeat': 'background-repeat', + 'backgroundRepeatX': 'background-repeat-x', + 'backgroundRepeatY': 'background-repeat-y', + 'backgroundSize': 'background-size', + 'baselineShift': 'baseline-shift', + 'border': 'border', + 'borderBottom': 'border-bottom', + 'borderBottomColor': 'border-bottom-color', + 'borderBottomLeftRadius': 'border-bottom-left-radius', + 'borderBottomRightRadius': 'border-bottom-right-radius', + 'borderBottomStyle': 'border-bottom-style', + 'borderBottomWidth': 'border-bottom-width', + 'borderCollapse': 'border-collapse', + 'borderColor': 'border-color', + 'borderImage': 'border-image', + 'borderImageOutset': 'border-image-outset', + 'borderImageRepeat': 'border-image-repeat', + 'borderImageSlice': 'border-image-slice', + 'borderImageSource': 'border-image-source', + 'borderImageWidth': 'border-image-width', + 'borderLeft': 'border-left', + 'borderLeftColor': 'border-left-color', + 'borderLeftStyle': 'border-left-style', + 'borderLeftWidth': 'border-left-width', + 'borderRadius': 'border-radius', + 'borderRight': 'border-right', + 'borderRightColor': 'border-right-color', + 'borderRightStyle': 'border-right-style', + 'borderRightWidth': 'border-right-width', + 'borderSpacing': 'border-spacing', + 'borderStyle': 'border-style', + 'borderTop': 'border-top', + 'borderTopColor': 'border-top-color', + 'borderTopLeftRadius': 'border-top-left-radius', + 'borderTopRightRadius': 'border-top-right-radius', + 'borderTopStyle': 'border-top-style', + 'borderTopWidth': 'border-top-width', + 'borderWidth': 'border-width', + 'bottom': 'bottom', + 'boxShadow': 'box-shadow', + 'boxSizing': 'box-sizing', + 'captionSide': 'caption-side', + 'clear': 'clear', + 'clip': 'clip', + 'clipPath': 'clip-path', + 'clipRule': 'clip-rule', + 'color': 'color', + 'colorInterpolation': 'color-interpolation', + 'colorInterpolationFilters': 'color-interpolation-filters', + 'colorProfile': 'color-profile', + 'colorRendering': 'color-rendering', + 'content': 'content', + 'counterIncrement': 'counter-increment', + 'counterReset': 'counter-reset', + 'cursor': 'cursor', + 'direction': 'direction', + 'display': 'display', + 'dominantBaseline': 'dominant-baseline', + 'emptyCells': 'empty-cells', + 'enableBackground': 'enable-background', + 'fill': 'fill', + 'fillOpacity': 'fill-opacity', + 'fillRule': 'fill-rule', + 'filter': 'filter', + 'cssFloat': 'float', + 'floodColor': 'flood-color', + 'floodOpacity': 'flood-opacity', + 'font': 'font', + 'fontFamily': 'font-family', + 'fontSize': 'font-size', + 'fontStretch': 'font-stretch', + 'fontStyle': 'font-style', + 'fontVariant': 'font-variant', + 'fontWeight': 'font-weight', + 'glyphOrientationHorizontal': 'glyph-orientation-horizontal', + 'glyphOrientationVertical': 'glyph-orientation-vertical', + 'height': 'height', + 'imageRendering': 'image-rendering', + 'kerning': 'kerning', + 'left': 'left', + 'letterSpacing': 'letter-spacing', + 'lightingColor': 'lighting-color', + 'lineHeight': 'line-height', + 'listStyle': 'list-style', + 'listStyleImage': 'list-style-image', + 'listStylePosition': 'list-style-position', + 'listStyleType': 'list-style-type', + 'margin': 'margin', + 'marginBottom': 'margin-bottom', + 'marginLeft': 'margin-left', + 'marginRight': 'margin-right', + 'marginTop': 'margin-top', + 'marker': 'marker', + 'markerEnd': 'marker-end', + 'markerMid': 'marker-mid', + 'markerStart': 'marker-start', + 'mask': 'mask', + 'maxHeight': 'max-height', + 'maxWidth': 'max-width', + 'minHeight': 'min-height', + 'minWidth': 'min-width', + 'opacity': 'opacity', + 'orphans': 'orphans', + 'outline': 'outline', + 'outlineColor': 'outline-color', + 'outlineOffset': 'outline-offset', + 'outlineStyle': 'outline-style', + 'outlineWidth': 'outline-width', + 'overflow': 'overflow', + 'overflowX': 'overflow-x', + 'overflowY': 'overflow-y', + 'padding': 'padding', + 'paddingBottom': 'padding-bottom', + 'paddingLeft': 'padding-left', + 'paddingRight': 'padding-right', + 'paddingTop': 'padding-top', + 'page': 'page', + 'pageBreakAfter': 'page-break-after', + 'pageBreakBefore': 'page-break-before', + 'pageBreakInside': 'page-break-inside', + 'pointerEvents': 'pointer-events', + 'position': 'position', + 'quotes': 'quotes', + 'resize': 'resize', + 'right': 'right', + 'shapeRendering': 'shape-rendering', + 'size': 'size', + 'speak': 'speak', + 'src': 'src', + 'stopColor': 'stop-color', + 'stopOpacity': 'stop-opacity', + 'stroke': 'stroke', + 'strokeDasharray': 'stroke-dasharray', + 'strokeDashoffset': 'stroke-dashoffset', + 'strokeLinecap': 'stroke-linecap', + 'strokeLinejoin': 'stroke-linejoin', + 'strokeMiterlimit': 'stroke-miterlimit', + 'strokeOpacity': 'stroke-opacity', + 'strokeWidth': 'stroke-width', + 'tableLayout': 'table-layout', + 'textAlign': 'text-align', + 'textAnchor': 'text-anchor', + 'textDecoration': 'text-decoration', + 'textIndent': 'text-indent', + 'textLineThrough': 'text-line-through', + 'textLineThroughColor': 'text-line-through-color', + 'textLineThroughMode': 'text-line-through-mode', + 'textLineThroughStyle': 'text-line-through-style', + 'textLineThroughWidth': 'text-line-through-width', + 'textOverflow': 'text-overflow', + 'textOverline': 'text-overline', + 'textOverlineColor': 'text-overline-color', + 'textOverlineMode': 'text-overline-mode', + 'textOverlineStyle': 'text-overline-style', + 'textOverlineWidth': 'text-overline-width', + 'textRendering': 'text-rendering', + 'textShadow': 'text-shadow', + 'textTransform': 'text-transform', + 'textUnderline': 'text-underline', + 'textUnderlineColor': 'text-underline-color', + 'textUnderlineMode': 'text-underline-mode', + 'textUnderlineStyle': 'text-underline-style', + 'textUnderlineWidth': 'text-underline-width', + 'top': 'top', + 'unicodeBidi': 'unicode-bidi', + 'unicodeRange': 'unicode-range', + 'vectorEffect': 'vector-effect', + 'verticalAlign': 'vertical-align', + 'visibility': 'visibility', + 'whiteSpace': 'white-space', + 'widows': 'widows', + 'width': 'width', + 'wordBreak': 'word-break', + 'wordSpacing': 'word-spacing', + 'wordWrap': 'word-wrap', + 'writingMode': 'writing-mode', + 'zIndex': 'z-index', + 'zoom': 'zoom', + }; + + // Elements that can be self-closing + var voidElems = { + 'area': true, + 'base': true, + 'br': true, + 'col': true, + 'command': true, + 'embed': true, + 'hr': true, + 'img': true, + 'input': true, + 'link': true, + 'meta': true, + 'param': true, + 'source': true, + 'wbr': true, + }; + + var whitespace = [' ', '\t', '\n', '\r']; + + // See http://www.w3schools.com/dom/dom_nodetype.asp + var nodeTypes = { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12, + }; + + function getElementsByTagName(tag) { + tag = tag.toUpperCase(); + var elems = []; + var allTags = (tag === '*'); + function getElems(node) { + var length = node.children.length; + for (var i = 0; i < length; i++) { + var child = node.children[i]; + if (allTags || (child.tagName === tag)) + elems.push(child); + getElems(child); + } + } + getElems(this); + return elems; + } + + var Node = function () {}; + + Node.prototype = { + attributes: null, + childNodes: null, + localName: null, + nodeName: null, + parentNode: null, + textContent: null, + nextSibling: null, + previousSibling: null, + + get firstChild() { + return this.childNodes[0] || null; + }, + + get firstElementChild() { + return this.children[0] || null; + }, + + get lastChild() { + return this.childNodes[this.childNodes.length - 1] || null; + }, + + get lastElementChild() { + return this.children[this.children.length - 1] || null; + }, + + appendChild: function (child) { + if (child.parentNode) { + child.parentNode.removeChild(child); + } + + var last = this.lastChild; + if (last) + last.nextSibling = child; + child.previousSibling = last; + + if (child.nodeType === Node.ELEMENT_NODE) { + child.previousElementSibling = this.children[this.children.length - 1] || null; + this.children.push(child); + child.previousElementSibling && (child.previousElementSibling.nextElementSibling = child); + } + this.childNodes.push(child); + child.parentNode = this; + }, + + removeChild: function (child) { + var childNodes = this.childNodes; + var childIndex = childNodes.indexOf(child); + if (childIndex === -1) { + throw 'removeChild: node not found'; + } else { + child.parentNode = null; + var prev = child.previousSibling; + var next = child.nextSibling; + if (prev) + prev.nextSibling = next; + if (next) + next.previousSibling = prev; + + if (child.nodeType === Node.ELEMENT_NODE) { + prev = child.previousElementSibling; + next = child.nextElementSibling; + if (prev) + prev.nextElementSibling = next; + if (next) + next.previousElementSibling = prev; + this.children.splice(this.children.indexOf(child), 1); + } + + child.previousSibling = child.nextSibling = null; + child.previousElementSibling = child.nextElementSibling = null; + + return childNodes.splice(childIndex, 1)[0]; + } + }, + + replaceChild: function (newNode, oldNode) { + var childNodes = this.childNodes; + var childIndex = childNodes.indexOf(oldNode); + if (childIndex === -1) { + throw 'replaceChild: node not found'; + } else { + // This will take care of updating the new node if it was somewhere else before: + if (newNode.parentNode) + newNode.parentNode.removeChild(newNode); + + childNodes[childIndex] = newNode; + + // update the new node's sibling properties, and its new siblings' sibling properties + newNode.nextSibling = oldNode.nextSibling; + newNode.previousSibling = oldNode.previousSibling; + if (newNode.nextSibling) + newNode.nextSibling.previousSibling = newNode; + if (newNode.previousSibling) + newNode.previousSibling.nextSibling = newNode; + + newNode.parentNode = this; + + // Now deal with elements before we clear out those values for the old node, + // because it can help us take shortcuts here: + if (newNode.nodeType === Node.ELEMENT_NODE) { + if (oldNode.nodeType === Node.ELEMENT_NODE) { + // Both were elements, which makes this easier, we just swap things out: + newNode.previousElementSibling = oldNode.previousElementSibling; + newNode.nextElementSibling = oldNode.nextElementSibling; + if (newNode.previousElementSibling) + newNode.previousElementSibling.nextElementSibling = newNode; + if (newNode.nextElementSibling) + newNode.nextElementSibling.previousElementSibling = newNode; + this.children[this.children.indexOf(oldNode)] = newNode; + } else { + // Hard way: + newNode.previousElementSibling = (function() { + for (var i = childIndex - 1; i >= 0; i--) { + if (childNodes[i].nodeType === Node.ELEMENT_NODE) + return childNodes[i]; + } + return null; + })(); + if (newNode.previousElementSibling) { + newNode.nextElementSibling = newNode.previousElementSibling.nextElementSibling; + } else { + newNode.nextElementSibling = (function() { + for (var i = childIndex + 1; i < childNodes.length; i++) { + if (childNodes[i].nodeType === Node.ELEMENT_NODE) + return childNodes[i]; + } + return null; + })(); + } + if (newNode.previousElementSibling) + newNode.previousElementSibling.nextElementSibling = newNode; + if (newNode.nextElementSibling) + newNode.nextElementSibling.previousElementSibling = newNode; + + if (newNode.nextElementSibling) + this.children.splice(this.children.indexOf(newNode.nextElementSibling), 0, newNode); + else + this.children.push(newNode); + } + } else if (oldNode.nodeType === Node.ELEMENT_NODE) { + // new node is not an element node. + // if the old one was, update its element siblings: + if (oldNode.previousElementSibling) + oldNode.previousElementSibling.nextElementSibling = oldNode.nextElementSibling; + if (oldNode.nextElementSibling) + oldNode.nextElementSibling.previousElementSibling = oldNode.previousElementSibling; + this.children.splice(this.children.indexOf(oldNode), 1); + + // If the old node wasn't an element, neither the new nor the old node was an element, + // and the children array and its members shouldn't need any updating. + } + + + oldNode.parentNode = null; + oldNode.previousSibling = null; + oldNode.nextSibling = null; + if (oldNode.nodeType === Node.ELEMENT_NODE) { + oldNode.previousElementSibling = null; + oldNode.nextElementSibling = null; + } + return oldNode; + } + }, + + __JSDOMParser__: true, + }; + + for (var nodeType in nodeTypes) { + Node[nodeType] = Node.prototype[nodeType] = nodeTypes[nodeType]; + } + + var Attribute = function (name, value) { + this.name = name; + this._value = value; + }; + + Attribute.prototype = { + get value() { + return this._value; + }, + setValue: function(newValue) { + this._value = newValue; + }, + getEncodedValue: function() { + return encodeHTML(this._value); + }, + }; + + var Comment = function () { + this.childNodes = []; + }; + + Comment.prototype = { + __proto__: Node.prototype, + + nodeName: '#comment', + nodeType: Node.COMMENT_NODE, + }; + + var Text = function () { + this.childNodes = []; + }; + + Text.prototype = { + __proto__: Node.prototype, + + nodeName: '#text', + nodeType: Node.TEXT_NODE, + get textContent() { + if (typeof this._textContent === 'undefined') { + this._textContent = decodeHTML(this._innerHTML || ''); + } + return this._textContent; + }, + get innerHTML() { + if (typeof this._innerHTML === 'undefined') { + this._innerHTML = encodeTextContentHTML(this._textContent || ''); + } + return this._innerHTML; + }, + + set innerHTML(newHTML) { + this._innerHTML = newHTML; + delete this._textContent; + }, + set textContent(newText) { + this._textContent = newText; + delete this._innerHTML; + }, + }; + + var Document = function (url) { + this.documentURI = url; + this.styleSheets = []; + this.childNodes = []; + this.children = []; + }; + + Document.prototype = { + __proto__: Node.prototype, + + nodeName: '#document', + nodeType: Node.DOCUMENT_NODE, + title: '', + + getElementsByTagName: getElementsByTagName, + + getElementById: function (id) { + function getElem(node) { + var length = node.children.length; + if (node.id === id) + return node; + for (var i = 0; i < length; i++) { + var el = getElem(node.children[i]); + if (el) + return el; + } + return null; + } + return getElem(this); + }, + + createElement: function (tag) { + var node = new Element(tag); + return node; + }, + + createTextNode: function (text) { + var node = new Text(); + node.textContent = text; + return node; + }, + + get baseURI() { + if (!this.hasOwnProperty('_baseURI')) { + this._baseURI = this.documentURI; + var baseElements = this.getElementsByTagName('base'); + var href = baseElements[0] && baseElements[0].getAttribute('href'); + if (href) { + try { + this._baseURI = (new URL(href, this._baseURI)).href; + } catch (ex) {/* Just fall back to documentURI */} + } + } + return this._baseURI; + }, + }; + + var Element = function (tag) { + // We use this to find the closing tag. + this._matchingTag = tag; + // We're explicitly a non-namespace aware parser, we just pretend it's all HTML. + var lastColonIndex = tag.lastIndexOf(':'); + if (lastColonIndex != -1) { + tag = tag.substring(lastColonIndex + 1); + } + this.attributes = []; + this.childNodes = []; + this.children = []; + this.nextElementSibling = this.previousElementSibling = null; + this.localName = tag.toLowerCase(); + this.tagName = tag.toUpperCase(); + this.style = new Style(this); + }; + + Element.prototype = { + __proto__: Node.prototype, + + nodeType: Node.ELEMENT_NODE, + + getElementsByTagName: getElementsByTagName, + + get className() { + return this.getAttribute('class') || ''; + }, + + set className(str) { + this.setAttribute('class', str); + }, + + get id() { + return this.getAttribute('id') || ''; + }, + + set id(str) { + this.setAttribute('id', str); + }, + + get href() { + return this.getAttribute('href') || ''; + }, + + set href(str) { + this.setAttribute('href', str); + }, + + get src() { + return this.getAttribute('src') || ''; + }, + + set src(str) { + this.setAttribute('src', str); + }, + + get srcset() { + return this.getAttribute('srcset') || ''; + }, + + set srcset(str) { + this.setAttribute('srcset', str); + }, + + get nodeName() { + return this.tagName; + }, + + get innerHTML() { + function getHTML(node) { + var i = 0; + for (i = 0; i < node.childNodes.length; i++) { + var child = node.childNodes[i]; + if (child.localName) { + arr.push('<' + child.localName); + + // serialize attribute list + for (var j = 0; j < child.attributes.length; j++) { + var attr = child.attributes[j]; + // the attribute value will be HTML escaped. + var val = attr.getEncodedValue(); + var quote = (val.indexOf('"') === -1 ? '"' : '\''); + arr.push(' ' + attr.name + '=' + quote + val + quote); + } + + if (child.localName in voidElems && !child.childNodes.length) { + // if this is a self-closing element, end it here + arr.push('/>'); + } else { + // otherwise, add its children + arr.push('>'); + getHTML(child); + arr.push(''); + } + } else { + // This is a text node, so asking for innerHTML won't recurse. + arr.push(child.innerHTML); + } + } + } + + // Using Array.join() avoids the overhead from lazy string concatenation. + // See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes + var arr = []; + getHTML(this); + return arr.join(''); + }, + + set innerHTML(html) { + var parser = new JSDOMParser(); + var node = parser.parse(html); + var i; + for (i = this.childNodes.length; --i >= 0;) { + this.childNodes[i].parentNode = null; + } + this.childNodes = node.childNodes; + this.children = node.children; + for (i = this.childNodes.length; --i >= 0;) { + this.childNodes[i].parentNode = this; + } + }, + + set textContent(text) { + // clear parentNodes for existing children + for (var i = this.childNodes.length; --i >= 0;) { + this.childNodes[i].parentNode = null; + } + + var node = new Text(); + this.childNodes = [ node ]; + this.children = []; + node.textContent = text; + node.parentNode = this; + }, + + get textContent() { + function getText(node) { + var nodes = node.childNodes; + for (var i = 0; i < nodes.length; i++) { + var child = nodes[i]; + if (child.nodeType === 3) { + text.push(child.textContent); + } else { + getText(child); + } + } + } + + // Using Array.join() avoids the overhead from lazy string concatenation. + // See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes + var text = []; + getText(this); + return text.join(''); + }, + + getAttribute: function (name) { + for (var i = this.attributes.length; --i >= 0;) { + var attr = this.attributes[i]; + if (attr.name === name) { + return attr.value; + } + } + return undefined; + }, + + setAttribute: function (name, value) { + for (var i = this.attributes.length; --i >= 0;) { + var attr = this.attributes[i]; + if (attr.name === name) { + attr.setValue(value); + return; + } + } + this.attributes.push(new Attribute(name, value)); + }, + + removeAttribute: function (name) { + for (var i = this.attributes.length; --i >= 0;) { + var attr = this.attributes[i]; + if (attr.name === name) { + this.attributes.splice(i, 1); + break; + } + } + }, + + hasAttribute: function (name) { + return this.attributes.some(function (attr) { + return attr.name == name; + }); + }, + }; + + var Style = function (node) { + this.node = node; + }; + + // getStyle() and setStyle() use the style attribute string directly. This + // won't be very efficient if there are a lot of style manipulations, but + // it's the easiest way to make sure the style attribute string and the JS + // style property stay in sync. Readability.js doesn't do many style + // manipulations, so this should be okay. + Style.prototype = { + getStyle: function (styleName) { + var attr = this.node.getAttribute('style'); + if (!attr) + return undefined; + + var styles = attr.split(';'); + for (var i = 0; i < styles.length; i++) { + var style = styles[i].split(':'); + var name = style[0].trim(); + if (name === styleName) + return style[1].trim(); + } + + return undefined; + }, + + setStyle: function (styleName, styleValue) { + var value = this.node.getAttribute('style') || ''; + var index = 0; + do { + var next = value.indexOf(';', index) + 1; + var length = next - index - 1; + var style = (length > 0 ? value.substr(index, length) : value.substr(index)); + if (style.substr(0, style.indexOf(':')).trim() === styleName) { + value = value.substr(0, index).trim() + (next ? ' ' + value.substr(next).trim() : ''); + break; + } + index = next; + } while (index); + + value += ' ' + styleName + ': ' + styleValue + ';'; + this.node.setAttribute('style', value.trim()); + }, + }; + + // For each item in styleMap, define a getter and setter on the style + // property. + for (var jsName in styleMap) { + (function (cssName) { + Style.prototype.__defineGetter__(jsName, function () { + return this.getStyle(cssName); + }); + Style.prototype.__defineSetter__(jsName, function (value) { + this.setStyle(cssName, value); + }); + })(styleMap[jsName]); + } + + var JSDOMParser = function () { + this.currentChar = 0; + + // In makeElementNode() we build up many strings one char at a time. Using + // += for this results in lots of short-lived intermediate strings. It's + // better to build an array of single-char strings and then join() them + // together at the end. And reusing a single array (i.e. |this.strBuf|) + // over and over for this purpose uses less memory than using a new array + // for each string. + this.strBuf = []; + + // Similarly, we reuse this array to return the two arguments from + // makeElementNode(), which saves us from having to allocate a new array + // every time. + this.retPair = []; + + this.errorState = ''; + }; + + JSDOMParser.prototype = { + error: function(m) { + dump('JSDOMParser error: ' + m + '\n'); + this.errorState += m + '\n'; + }, + + /** * Look at the next character without advancing the index. */ - peekNext: function () { - return this.html[this.currentChar]; - }, + peekNext: function () { + return this.html[this.currentChar]; + }, - /** + /** * Get the next character and advance the index. */ - nextChar: function () { - return this.html[this.currentChar++]; - }, + nextChar: function () { + return this.html[this.currentChar++]; + }, - /** + /** * Called after a quote character is read. This finds the next quote * character and returns the text string in between. */ - readString: function (quote) { - var str; - var n = this.html.indexOf(quote, this.currentChar); - if (n === -1) { - this.currentChar = this.html.length; - str = null; - } else { - str = this.html.substring(this.currentChar, n); - this.currentChar = n + 1; - } + readString: function (quote) { + var str; + var n = this.html.indexOf(quote, this.currentChar); + if (n === -1) { + this.currentChar = this.html.length; + str = null; + } else { + str = this.html.substring(this.currentChar, n); + this.currentChar = n + 1; + } - return str; - }, + return str; + }, - /** + /** * Called when parsing a node. This finds the next name/value attribute * pair and adds the result to the attributes list. */ - readAttribute: function (node) { - var name = ""; + readAttribute: function (node) { + var name = ''; - var n = this.html.indexOf("=", this.currentChar); - if (n === -1) { - this.currentChar = this.html.length; - } else { - // Read until a '=' character is hit; this will be the attribute key - name = this.html.substring(this.currentChar, n); - this.currentChar = n + 1; - } + var n = this.html.indexOf('=', this.currentChar); + if (n === -1) { + this.currentChar = this.html.length; + } else { + // Read until a '=' character is hit; this will be the attribute key + name = this.html.substring(this.currentChar, n); + this.currentChar = n + 1; + } - if (!name) - return; + if (!name) + return; - // After a '=', we should see a '"' for the attribute value - var c = this.nextChar(); - if (c !== '"' && c !== "'") { - this.error("Error reading attribute " + name + ", expecting '\"'"); - return; - } + // After a '=', we should see a '"' for the attribute value + var c = this.nextChar(); + if (c !== '"' && c !== '\'') { + this.error('Error reading attribute ' + name + ', expecting \'"\''); + return; + } - // Read the attribute value (and consume the matching quote) - var value = this.readString(c); + // Read the attribute value (and consume the matching quote) + var value = this.readString(c); - node.attributes.push(new Attribute(name, decodeHTML(value))); + node.attributes.push(new Attribute(name, decodeHTML(value))); - return; - }, + return; + }, - /** + /** * Parses and returns an Element node. This is called after a '<' has been * read. * @@ -954,237 +954,237 @@ * the second index is a boolean indicating whether this is a void * Element */ - makeElementNode: function (retPair) { - var c = this.nextChar(); + makeElementNode: function (retPair) { + var c = this.nextChar(); - // Read the Element tag name - var strBuf = this.strBuf; - strBuf.length = 0; - while (whitespace.indexOf(c) == -1 && c !== ">" && c !== "/") { - if (c === undefined) - return false; - strBuf.push(c); - c = this.nextChar(); - } - var tag = strBuf.join(""); + // Read the Element tag name + var strBuf = this.strBuf; + strBuf.length = 0; + while (whitespace.indexOf(c) == -1 && c !== '>' && c !== '/') { + if (c === undefined) + return false; + strBuf.push(c); + c = this.nextChar(); + } + var tag = strBuf.join(''); - if (!tag) - return false; + if (!tag) + return false; - var node = new Element(tag); + var node = new Element(tag); - // Read Element attributes - while (c !== "/" && c !== ">") { - if (c === undefined) - return false; - while (whitespace.indexOf(this.html[this.currentChar++]) != -1) { - // Advance cursor to first non-whitespace char. - } - this.currentChar--; - c = this.nextChar(); - if (c !== "/" && c !== ">") { - --this.currentChar; - this.readAttribute(node); - } - } + // Read Element attributes + while (c !== '/' && c !== '>') { + if (c === undefined) + return false; + while (whitespace.indexOf(this.html[this.currentChar++]) != -1) { + // Advance cursor to first non-whitespace char. + } + this.currentChar--; + c = this.nextChar(); + if (c !== '/' && c !== '>') { + --this.currentChar; + this.readAttribute(node); + } + } - // If this is a self-closing tag, read '/>' - var closed = false; - if (c === "/") { - closed = true; - c = this.nextChar(); - if (c !== ">") { - this.error("expected '>' to close " + tag); - return false; - } - } + // If this is a self-closing tag, read '/>' + var closed = false; + if (c === '/') { + closed = true; + c = this.nextChar(); + if (c !== '>') { + this.error('expected \'>\' to close ' + tag); + return false; + } + } - retPair[0] = node; - retPair[1] = closed; - return true; - }, + retPair[0] = node; + retPair[1] = closed; + return true; + }, - /** + /** * If the current input matches this string, advance the input index; * otherwise, do nothing. * * @returns whether input matched string */ - match: function (str) { - var strlen = str.length; - if (this.html.substr(this.currentChar, strlen).toLowerCase() === str.toLowerCase()) { - this.currentChar += strlen; - return true; - } - return false; - }, + match: function (str) { + var strlen = str.length; + if (this.html.substr(this.currentChar, strlen).toLowerCase() === str.toLowerCase()) { + this.currentChar += strlen; + return true; + } + return false; + }, - /** + /** * Searches the input until a string is found and discards all input up to * and including the matched string. */ - discardTo: function (str) { - var index = this.html.indexOf(str, this.currentChar) + str.length; - if (index === -1) - this.currentChar = this.html.length; - this.currentChar = index; - }, + discardTo: function (str) { + var index = this.html.indexOf(str, this.currentChar) + str.length; + if (index === -1) + this.currentChar = this.html.length; + this.currentChar = index; + }, - /** + /** * Reads child nodes for the given node. */ - readChildren: function (node) { - var child; - while ((child = this.readNode())) { - // Don't keep Comment nodes - if (child.nodeType !== 8) { - node.appendChild(child); - } - } - }, + readChildren: function (node) { + var child; + while ((child = this.readNode())) { + // Don't keep Comment nodes + if (child.nodeType !== 8) { + node.appendChild(child); + } + } + }, - discardNextComment: function() { - if (this.match("--")) { - this.discardTo("-->"); - } else { - var c = this.nextChar(); - while (c !== ">") { - if (c === undefined) - return null; - if (c === '"' || c === "'") - this.readString(c); - c = this.nextChar(); - } - } - return new Comment(); - }, + discardNextComment: function() { + if (this.match('--')) { + this.discardTo('-->'); + } else { + var c = this.nextChar(); + while (c !== '>') { + if (c === undefined) + return null; + if (c === '"' || c === '\'') + this.readString(c); + c = this.nextChar(); + } + } + return new Comment(); + }, - /** + /** * Reads the next child node from the input. If we're reading a closing * tag, or if we've reached the end of input, return null. * * @returns the node */ - readNode: function () { - var c = this.nextChar(); + readNode: function () { + var c = this.nextChar(); - if (c === undefined) - return null; + if (c === undefined) + return null; - // Read any text as Text node - var textNode; - if (c !== "<") { - --this.currentChar; - textNode = new Text(); - var n = this.html.indexOf("<", this.currentChar); - if (n === -1) { - textNode.innerHTML = this.html.substring(this.currentChar, this.html.length); - this.currentChar = this.html.length; - } else { - textNode.innerHTML = this.html.substring(this.currentChar, n); - this.currentChar = n; - } - return textNode; - } + // Read any text as Text node + var textNode; + if (c !== '<') { + --this.currentChar; + textNode = new Text(); + var n = this.html.indexOf('<', this.currentChar); + if (n === -1) { + textNode.innerHTML = this.html.substring(this.currentChar, this.html.length); + this.currentChar = this.html.length; + } else { + textNode.innerHTML = this.html.substring(this.currentChar, n); + this.currentChar = n; + } + return textNode; + } - if (this.match("![CDATA[")) { - var endChar = this.html.indexOf("]]>", this.currentChar); - if (endChar === -1) { - this.error("unclosed CDATA section"); - return null; - } - textNode = new Text(); - textNode.textContent = this.html.substring(this.currentChar, endChar); - this.currentChar = endChar + ("]]>").length; - return textNode; - } + if (this.match('![CDATA[')) { + var endChar = this.html.indexOf(']]>', this.currentChar); + if (endChar === -1) { + this.error('unclosed CDATA section'); + return null; + } + textNode = new Text(); + textNode.textContent = this.html.substring(this.currentChar, endChar); + this.currentChar = endChar + (']]>').length; + return textNode; + } - c = this.peekNext(); + c = this.peekNext(); - // Read Comment node. Normally, Comment nodes know their inner - // textContent, but we don't really care about Comment nodes (we throw - // them away in readChildren()). So just returning an empty Comment node - // here is sufficient. - if (c === "!" || c === "?") { - // We're still before the ! or ? that is starting this comment: - this.currentChar++; - return this.discardNextComment(); - } + // Read Comment node. Normally, Comment nodes know their inner + // textContent, but we don't really care about Comment nodes (we throw + // them away in readChildren()). So just returning an empty Comment node + // here is sufficient. + if (c === '!' || c === '?') { + // We're still before the ! or ? that is starting this comment: + this.currentChar++; + return this.discardNextComment(); + } - // If we're reading a closing tag, return null. This means we've reached - // the end of this set of child nodes. - if (c === "/") { - --this.currentChar; - return null; - } + // If we're reading a closing tag, return null. This means we've reached + // the end of this set of child nodes. + if (c === '/') { + --this.currentChar; + return null; + } - // Otherwise, we're looking at an Element node - var result = this.makeElementNode(this.retPair); - if (!result) - return null; + // Otherwise, we're looking at an Element node + var result = this.makeElementNode(this.retPair); + if (!result) + return null; - var node = this.retPair[0]; - var closed = this.retPair[1]; - var localName = node.localName; + var node = this.retPair[0]; + var closed = this.retPair[1]; + var localName = node.localName; - // If this isn't a void Element, read its child nodes - if (!closed) { - this.readChildren(node); - var closingTag = ""; - if (!this.match(closingTag)) { - this.error("expected '" + closingTag + "' and got " + this.html.substr(this.currentChar, closingTag.length)); - return null; - } - } + // If this isn't a void Element, read its child nodes + if (!closed) { + this.readChildren(node); + var closingTag = ''; + if (!this.match(closingTag)) { + this.error('expected \'' + closingTag + '\' and got ' + this.html.substr(this.currentChar, closingTag.length)); + return null; + } + } - // Only use the first title, because SVG might have other - // title elements which we don't care about (medium.com - // does this, at least). - if (localName === "title" && !this.doc.title) { - this.doc.title = node.textContent.trim(); - } else if (localName === "head") { - this.doc.head = node; - } else if (localName === "body") { - this.doc.body = node; - } else if (localName === "html") { - this.doc.documentElement = node; - } + // Only use the first title, because SVG might have other + // title elements which we don't care about (medium.com + // does this, at least). + if (localName === 'title' && !this.doc.title) { + this.doc.title = node.textContent.trim(); + } else if (localName === 'head') { + this.doc.head = node; + } else if (localName === 'body') { + this.doc.body = node; + } else if (localName === 'html') { + this.doc.documentElement = node; + } - return node; - }, + return node; + }, - /** + /** * Parses an HTML string and returns a JS implementation of the Document. */ - parse: function (html, url) { - this.html = html; - var doc = this.doc = new Document(url); - this.readChildren(doc); + parse: function (html, url) { + this.html = html; + var doc = this.doc = new Document(url); + this.readChildren(doc); - // If this is an HTML document, remove root-level children except for the - // node - if (doc.documentElement) { - for (var i = doc.childNodes.length; --i >= 0;) { - var child = doc.childNodes[i]; - if (child !== doc.documentElement) { - doc.removeChild(child); - } - } - } + // If this is an HTML document, remove root-level children except for the + // node + if (doc.documentElement) { + for (var i = doc.childNodes.length; --i >= 0;) { + var child = doc.childNodes[i]; + if (child !== doc.documentElement) { + doc.removeChild(child); + } + } + } - return doc; - } - }; + return doc; + }, + }; - // Attach the standard DOM types to the global scope - global.Node = Node; - global.Comment = Comment; - global.Document = Document; - global.Element = Element; - global.Text = Text; + // Attach the standard DOM types to the global scope + global.Node = Node; + global.Comment = Comment; + global.Document = Document; + global.Element = Element; + global.Text = Text; - // Attach JSDOMParser to the global scope - global.JSDOMParser = JSDOMParser; + // Attach JSDOMParser to the global scope + global.JSDOMParser = JSDOMParser; })(this); diff --git a/Clipper/joplin-webclipper/content_scripts/Readability-readerable.js b/Clipper/joplin-webclipper/content_scripts/Readability-readerable.js index 1be7c73fb..0e4e8f990 100644 --- a/Clipper/joplin-webclipper/content_scripts/Readability-readerable.js +++ b/Clipper/joplin-webclipper/content_scripts/Readability-readerable.js @@ -24,15 +24,15 @@ */ var REGEXPS = { - // NOTE: These two regular expressions are duplicated in - // Readability.js. Please keep both copies in sync. - unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i, - okMaybeItsACandidate: /and|article|body|column|main|shadow/i, + // NOTE: These two regular expressions are duplicated in + // Readability.js. Please keep both copies in sync. + unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i, + okMaybeItsACandidate: /and|article|body|column|main|shadow/i, }; function isNodeVisible(node) { - // Have to null-check node.style to deal with SVG and MathML nodes. - return (!node.style || node.style.display != "none") && !node.hasAttribute("hidden"); + // Have to null-check node.style to deal with SVG and MathML nodes. + return (!node.style || node.style.display != 'none') && !node.hasAttribute('hidden'); } /** @@ -41,59 +41,59 @@ function isNodeVisible(node) { * @return boolean Whether or not we suspect Readability.parse() will suceeed at returning an article object. */ function isProbablyReaderable(doc, isVisible) { - if (!isVisible) { - isVisible = isNodeVisible; - } + if (!isVisible) { + isVisible = isNodeVisible; + } - var nodes = doc.querySelectorAll("p, pre"); + var nodes = doc.querySelectorAll('p, pre'); - // Get
nodes which have
node(s) and append them into the `nodes` variable. - // Some articles' DOM structures might look like - //
- // Sentences
- //
- // Sentences
- //
- var brNodes = doc.querySelectorAll("div > br"); - if (brNodes.length) { - var set = new Set(nodes); - [].forEach.call(brNodes, function(node) { - set.add(node.parentNode); - }); - nodes = Array.from(set); - } + // Get
nodes which have
node(s) and append them into the `nodes` variable. + // Some articles' DOM structures might look like + //
+ // Sentences
+ //
+ // Sentences
+ //
+ var brNodes = doc.querySelectorAll('div > br'); + if (brNodes.length) { + var set = new Set(nodes); + [].forEach.call(brNodes, function(node) { + set.add(node.parentNode); + }); + nodes = Array.from(set); + } - var score = 0; - // This is a little cheeky, we use the accumulator 'score' to decide what to return from - // this callback: - return [].some.call(nodes, function(node) { - if (!isVisible(node)) - return false; + var score = 0; + // This is a little cheeky, we use the accumulator 'score' to decide what to return from + // this callback: + return [].some.call(nodes, function(node) { + if (!isVisible(node)) + return false; - var matchString = node.className + " " + node.id; - if (REGEXPS.unlikelyCandidates.test(matchString) && + var matchString = node.className + ' ' + node.id; + if (REGEXPS.unlikelyCandidates.test(matchString) && !REGEXPS.okMaybeItsACandidate.test(matchString)) { - return false; - } + return false; + } - if (node.matches("li p")) { - return false; - } + if (node.matches('li p')) { + return false; + } - var textContentLength = node.textContent.trim().length; - if (textContentLength < 140) { - return false; - } + var textContentLength = node.textContent.trim().length; + if (textContentLength < 140) { + return false; + } - score += Math.sqrt(textContentLength - 140); + score += Math.sqrt(textContentLength - 140); - if (score > 20) { - return true; - } - return false; - }); + if (score > 20) { + return true; + } + return false; + }); } -if (typeof exports === "object") { - exports.isProbablyReaderable = isProbablyReaderable; +if (typeof exports === 'object') { + exports.isProbablyReaderable = isProbablyReaderable; } diff --git a/Clipper/joplin-webclipper/content_scripts/Readability.js b/Clipper/joplin-webclipper/content_scripts/Readability.js index f3eef238b..db5ab6ce0 100644 --- a/Clipper/joplin-webclipper/content_scripts/Readability.js +++ b/Clipper/joplin-webclipper/content_scripts/Readability.js @@ -28,147 +28,147 @@ * @param {Object} options The options object. */ function Readability(doc, options) { - // In some older versions, people passed a URI as the first argument. Cope: - if (options && options.documentElement) { - doc = options; - options = arguments[2]; - } else if (!doc || !doc.documentElement) { - throw new Error("First argument to Readability constructor should be a document object."); - } - options = options || {}; + // In some older versions, people passed a URI as the first argument. Cope: + if (options && options.documentElement) { + doc = options; + options = arguments[2]; + } else if (!doc || !doc.documentElement) { + throw new Error('First argument to Readability constructor should be a document object.'); + } + options = options || {}; - this._doc = doc; - this._articleTitle = null; - this._articleByline = null; - this._articleDir = null; - this._articleSiteName = null; - this._attempts = []; + this._doc = doc; + this._articleTitle = null; + this._articleByline = null; + this._articleDir = null; + this._articleSiteName = null; + this._attempts = []; - // Configurable options - this._debug = !!options.debug; - this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE; - this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES; - this._charThreshold = options.charThreshold || this.DEFAULT_CHAR_THRESHOLD; - this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(options.classesToPreserve || []); + // Configurable options + this._debug = !!options.debug; + this._maxElemsToParse = options.maxElemsToParse || this.DEFAULT_MAX_ELEMS_TO_PARSE; + this._nbTopCandidates = options.nbTopCandidates || this.DEFAULT_N_TOP_CANDIDATES; + this._charThreshold = options.charThreshold || this.DEFAULT_CHAR_THRESHOLD; + this._classesToPreserve = this.CLASSES_TO_PRESERVE.concat(options.classesToPreserve || []); - // Start with all flags set - this._flags = this.FLAG_STRIP_UNLIKELYS | + // Start with all flags set + this._flags = this.FLAG_STRIP_UNLIKELYS | this.FLAG_WEIGHT_CLASSES | this.FLAG_CLEAN_CONDITIONALLY; - var logEl; + var logEl; - // Control whether log messages are sent to the console - if (this._debug) { - logEl = function(e) { - var rv = e.nodeName + " "; - if (e.nodeType == e.TEXT_NODE) { - return rv + '("' + e.textContent + '")'; - } - var classDesc = e.className && ("." + e.className.replace(/ /g, ".")); - var elDesc = ""; - if (e.id) - elDesc = "(#" + e.id + classDesc + ")"; - else if (classDesc) - elDesc = "(" + classDesc + ")"; - return rv + elDesc; - }; - this.log = function () { - if (typeof dump !== "undefined") { - var msg = Array.prototype.map.call(arguments, function(x) { - return (x && x.nodeName) ? logEl(x) : x; - }).join(" "); - dump("Reader: (Readability) " + msg + "\n"); - } else if (typeof console !== "undefined") { - var args = ["Reader: (Readability) "].concat(arguments); - console.log.apply(console, args); - } - }; - } else { - this.log = function () {}; - } + // Control whether log messages are sent to the console + if (this._debug) { + logEl = function(e) { + var rv = e.nodeName + ' '; + if (e.nodeType == e.TEXT_NODE) { + return rv + '("' + e.textContent + '")'; + } + var classDesc = e.className && ('.' + e.className.replace(/ /g, '.')); + var elDesc = ''; + if (e.id) + elDesc = '(#' + e.id + classDesc + ')'; + else if (classDesc) + elDesc = '(' + classDesc + ')'; + return rv + elDesc; + }; + this.log = function () { + if (typeof dump !== 'undefined') { + var msg = Array.prototype.map.call(arguments, function(x) { + return (x && x.nodeName) ? logEl(x) : x; + }).join(' '); + dump('Reader: (Readability) ' + msg + '\n'); + } else if (typeof console !== 'undefined') { + var args = ['Reader: (Readability) '].concat(arguments); + console.log.apply(console, args); + } + }; + } else { + this.log = function () {}; + } } Readability.prototype = { - FLAG_STRIP_UNLIKELYS: 0x1, - FLAG_WEIGHT_CLASSES: 0x2, - FLAG_CLEAN_CONDITIONALLY: 0x4, + FLAG_STRIP_UNLIKELYS: 0x1, + FLAG_WEIGHT_CLASSES: 0x2, + FLAG_CLEAN_CONDITIONALLY: 0x4, - // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType - ELEMENT_NODE: 1, - TEXT_NODE: 3, + // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType + ELEMENT_NODE: 1, + TEXT_NODE: 3, - // Max number of nodes supported by this parser. Default: 0 (no limit) - DEFAULT_MAX_ELEMS_TO_PARSE: 0, + // Max number of nodes supported by this parser. Default: 0 (no limit) + DEFAULT_MAX_ELEMS_TO_PARSE: 0, - // The number of top candidates to consider when analysing how - // tight the competition is among candidates. - DEFAULT_N_TOP_CANDIDATES: 5, + // The number of top candidates to consider when analysing how + // tight the competition is among candidates. + DEFAULT_N_TOP_CANDIDATES: 5, - // Element tags to score by default. - DEFAULT_TAGS_TO_SCORE: "section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","), + // Element tags to score by default. + DEFAULT_TAGS_TO_SCORE: 'section,h2,h3,h4,h5,h6,p,td,pre'.toUpperCase().split(','), - // The default number of chars an article must have in order to return a result - DEFAULT_CHAR_THRESHOLD: 500, + // The default number of chars an article must have in order to return a result + DEFAULT_CHAR_THRESHOLD: 500, - // All of the regular expressions in use within readability. - // Defined up here so we don't instantiate them repeatedly in loops. - REGEXPS: { - // NOTE: These two regular expressions are duplicated in - // Readability-readerable.js. Please keep both copies in sync. - unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i, - okMaybeItsACandidate: /and|article|body|column|main|shadow/i, + // All of the regular expressions in use within readability. + // Defined up here so we don't instantiate them repeatedly in loops. + REGEXPS: { + // NOTE: These two regular expressions are duplicated in + // Readability-readerable.js. Please keep both copies in sync. + unlikelyCandidates: /-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|foot|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i, + okMaybeItsACandidate: /and|article|body|column|main|shadow/i, - positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i, - negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i, - extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i, - byline: /byline|author|dateline|writtenby|p-author/i, - replaceFonts: /<(\/?)font[^>]*>/gi, - normalize: /\s{2,}/g, - videos: /\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i, - nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i, - prevLink: /(prev|earl|old|new|<|«)/i, - whitespace: /^\s*$/, - hasContent: /\S$/, - }, + positive: /article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i, + negative: /hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i, + extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i, + byline: /byline|author|dateline|writtenby|p-author/i, + replaceFonts: /<(\/?)font[^>]*>/gi, + normalize: /\s{2,}/g, + videos: /\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i, + nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i, + prevLink: /(prev|earl|old|new|<|«)/i, + whitespace: /^\s*$/, + hasContent: /\S$/, + }, - DIV_TO_P_ELEMS: [ "A", "BLOCKQUOTE", "DL", "DIV", "IMG", "OL", "P", "PRE", "TABLE", "UL", "SELECT" ], + DIV_TO_P_ELEMS: [ 'A', 'BLOCKQUOTE', 'DL', 'DIV', 'IMG', 'OL', 'P', 'PRE', 'TABLE', 'UL', 'SELECT' ], - ALTER_TO_DIV_EXCEPTIONS: ["DIV", "ARTICLE", "SECTION", "P"], + ALTER_TO_DIV_EXCEPTIONS: ['DIV', 'ARTICLE', 'SECTION', 'P'], - PRESENTATIONAL_ATTRIBUTES: [ "align", "background", "bgcolor", "border", "cellpadding", "cellspacing", "frame", "hspace", "rules", "style", "valign", "vspace" ], + PRESENTATIONAL_ATTRIBUTES: [ 'align', 'background', 'bgcolor', 'border', 'cellpadding', 'cellspacing', 'frame', 'hspace', 'rules', 'style', 'valign', 'vspace' ], - DEPRECATED_SIZE_ATTRIBUTE_ELEMS: [ "TABLE", "TH", "TD", "HR", "PRE" ], + DEPRECATED_SIZE_ATTRIBUTE_ELEMS: [ 'TABLE', 'TH', 'TD', 'HR', 'PRE' ], - // The commented out elements qualify as phrasing content but tend to be - // removed by readability when put into paragraphs, so we ignore them here. - PHRASING_ELEMS: [ - // "CANVAS", "IFRAME", "SVG", "VIDEO", - "ABBR", "AUDIO", "B", "BDO", "BR", "BUTTON", "CITE", "CODE", "DATA", - "DATALIST", "DFN", "EM", "EMBED", "I", "IMG", "INPUT", "KBD", "LABEL", - "MARK", "MATH", "METER", "NOSCRIPT", "OBJECT", "OUTPUT", "PROGRESS", "Q", - "RUBY", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "SUB", - "SUP", "TEXTAREA", "TIME", "VAR", "WBR" - ], + // The commented out elements qualify as phrasing content but tend to be + // removed by readability when put into paragraphs, so we ignore them here. + PHRASING_ELEMS: [ + // "CANVAS", "IFRAME", "SVG", "VIDEO", + 'ABBR', 'AUDIO', 'B', 'BDO', 'BR', 'BUTTON', 'CITE', 'CODE', 'DATA', + 'DATALIST', 'DFN', 'EM', 'EMBED', 'I', 'IMG', 'INPUT', 'KBD', 'LABEL', + 'MARK', 'MATH', 'METER', 'NOSCRIPT', 'OBJECT', 'OUTPUT', 'PROGRESS', 'Q', + 'RUBY', 'SAMP', 'SCRIPT', 'SELECT', 'SMALL', 'SPAN', 'STRONG', 'SUB', + 'SUP', 'TEXTAREA', 'TIME', 'VAR', 'WBR', + ], - // These are the classes that readability sets itself. - CLASSES_TO_PRESERVE: [ "page" ], + // These are the classes that readability sets itself. + CLASSES_TO_PRESERVE: [ 'page' ], - /** + /** * Run any post-process modifications to article content as necessary. * * @param Element * @return void **/ - _postProcessContent: function(articleContent) { - // Readability cannot open relative uris so we convert them to absolute uris. - this._fixRelativeUris(articleContent); + _postProcessContent: function(articleContent) { + // Readability cannot open relative uris so we convert them to absolute uris. + this._fixRelativeUris(articleContent); - // Remove classes. - this._cleanClasses(articleContent); - }, + // Remove classes. + this._cleanClasses(articleContent); + }, - /** + /** * Iterates over a NodeList, calls `filterFn` for each node and removes node * if function returned `true`. * @@ -178,33 +178,33 @@ Readability.prototype = { * @param Function filterFn the function to use as a filter * @return void */ - _removeNodes: function(nodeList, filterFn) { - for (var i = nodeList.length - 1; i >= 0; i--) { - var node = nodeList[i]; - var parentNode = node.parentNode; - if (parentNode) { - if (!filterFn || filterFn.call(this, node, i, nodeList)) { - parentNode.removeChild(node); - } - } - } - }, + _removeNodes: function(nodeList, filterFn) { + for (var i = nodeList.length - 1; i >= 0; i--) { + var node = nodeList[i]; + var parentNode = node.parentNode; + if (parentNode) { + if (!filterFn || filterFn.call(this, node, i, nodeList)) { + parentNode.removeChild(node); + } + } + } + }, - /** + /** * Iterates over a NodeList, and calls _setNodeTag for each node. * * @param NodeList nodeList The nodes to operate on * @param String newTagName the new tag name to use * @return void */ - _replaceNodeTags: function(nodeList, newTagName) { - for (var i = nodeList.length - 1; i >= 0; i--) { - var node = nodeList[i]; - this._setNodeTag(node, newTagName); - } - }, + _replaceNodeTags: function(nodeList, newTagName) { + for (var i = nodeList.length - 1; i >= 0; i--) { + var node = nodeList[i]; + this._setNodeTag(node, newTagName); + } + }, - /** + /** * Iterate over a NodeList, which doesn't natively fully implement the Array * interface. * @@ -215,11 +215,11 @@ Readability.prototype = { * @param Function fn The iterate function. * @return void */ - _forEachNode: function(nodeList, fn) { - Array.prototype.forEach.call(nodeList, fn, this); - }, + _forEachNode: function(nodeList, fn) { + Array.prototype.forEach.call(nodeList, fn, this); + }, - /** + /** * Iterate over a NodeList, return true if any of the provided iterate * function calls returns true, false otherwise. * @@ -230,11 +230,11 @@ Readability.prototype = { * @param Function fn The iterate function. * @return Boolean */ - _someNode: function(nodeList, fn) { - return Array.prototype.some.call(nodeList, fn, this); - }, + _someNode: function(nodeList, fn) { + return Array.prototype.some.call(nodeList, fn, this); + }, - /** + /** * Iterate over a NodeList, return true if all of the provided iterate * function calls return true, false otherwise. * @@ -245,36 +245,36 @@ Readability.prototype = { * @param Function fn The iterate function. * @return Boolean */ - _everyNode: function(nodeList, fn) { - return Array.prototype.every.call(nodeList, fn, this); - }, + _everyNode: function(nodeList, fn) { + return Array.prototype.every.call(nodeList, fn, this); + }, - /** + /** * Concat all nodelists passed as arguments. * * @return ...NodeList * @return Array */ - _concatNodeLists: function() { - var slice = Array.prototype.slice; - var args = slice.call(arguments); - var nodeLists = args.map(function(list) { - return slice.call(list); - }); - return Array.prototype.concat.apply([], nodeLists); - }, + _concatNodeLists: function() { + var slice = Array.prototype.slice; + var args = slice.call(arguments); + var nodeLists = args.map(function(list) { + return slice.call(list); + }); + return Array.prototype.concat.apply([], nodeLists); + }, - _getAllNodesWithTag: function(node, tagNames) { - if (node.querySelectorAll) { - return node.querySelectorAll(tagNames.join(",")); - } - return [].concat.apply([], tagNames.map(function(tag) { - var collection = node.getElementsByTagName(tag); - return Array.isArray(collection) ? collection : Array.from(collection); - })); - }, + _getAllNodesWithTag: function(node, tagNames) { + if (node.querySelectorAll) { + return node.querySelectorAll(tagNames.join(',')); + } + return [].concat.apply([], tagNames.map(function(tag) { + var collection = node.getElementsByTagName(tag); + return Array.isArray(collection) ? collection : Array.from(collection); + })); + }, - /** + /** * Removes the class="" attribute from every element in the given * subtree, except those that match CLASSES_TO_PRESERVE and * the classesToPreserve array from the options object. @@ -282,921 +282,921 @@ Readability.prototype = { * @param Element * @return void */ - _cleanClasses: function(node) { - var classesToPreserve = this._classesToPreserve; - var className = (node.getAttribute("class") || "") - .split(/\s+/) - .filter(function(cls) { - return classesToPreserve.indexOf(cls) != -1; - }) - .join(" "); + _cleanClasses: function(node) { + var classesToPreserve = this._classesToPreserve; + var className = (node.getAttribute('class') || '') + .split(/\s+/) + .filter(function(cls) { + return classesToPreserve.indexOf(cls) != -1; + }) + .join(' '); - if (className) { - node.setAttribute("class", className); - } else { - node.removeAttribute("class"); - } + if (className) { + node.setAttribute('class', className); + } else { + node.removeAttribute('class'); + } - for (node = node.firstElementChild; node; node = node.nextElementSibling) { - this._cleanClasses(node); - } - }, + for (node = node.firstElementChild; node; node = node.nextElementSibling) { + this._cleanClasses(node); + } + }, - /** + /** * Converts each and uri in the given element to an absolute URI, * ignoring #ref URIs. * * @param Element * @return void */ - _fixRelativeUris: function(articleContent) { - var baseURI = this._doc.baseURI; - var documentURI = this._doc.documentURI; - function toAbsoluteURI(uri) { - // Leave hash links alone if the base URI matches the document URI: - if (baseURI == documentURI && uri.charAt(0) == "#") { - return uri; - } - // Otherwise, resolve against base URI: - try { - return new URL(uri, baseURI).href; - } catch (ex) { - // Something went wrong, just return the original: - } - return uri; - } + _fixRelativeUris: function(articleContent) { + var baseURI = this._doc.baseURI; + var documentURI = this._doc.documentURI; + function toAbsoluteURI(uri) { + // Leave hash links alone if the base URI matches the document URI: + if (baseURI == documentURI && uri.charAt(0) == '#') { + return uri; + } + // Otherwise, resolve against base URI: + try { + return new URL(uri, baseURI).href; + } catch (ex) { + // Something went wrong, just return the original: + } + return uri; + } - var links = this._getAllNodesWithTag(articleContent, ["a"]); - this._forEachNode(links, function(link) { - var href = link.getAttribute("href"); - if (href) { - // Replace links with javascript: URIs with text content, since - // they won't work after scripts have been removed from the page. - if (href.indexOf("javascript:") === 0) { - var text = this._doc.createTextNode(link.textContent); - link.parentNode.replaceChild(text, link); - } else { - link.setAttribute("href", toAbsoluteURI(href)); - } - } - }); + var links = this._getAllNodesWithTag(articleContent, ['a']); + this._forEachNode(links, function(link) { + var href = link.getAttribute('href'); + if (href) { + // Replace links with javascript: URIs with text content, since + // they won't work after scripts have been removed from the page. + if (href.indexOf('javascript:') === 0) { + var text = this._doc.createTextNode(link.textContent); + link.parentNode.replaceChild(text, link); + } else { + link.setAttribute('href', toAbsoluteURI(href)); + } + } + }); - var imgs = this._getAllNodesWithTag(articleContent, ["img"]); - this._forEachNode(imgs, function(img) { - var src = img.getAttribute("src"); - if (src) { - img.setAttribute("src", toAbsoluteURI(src)); - } - }); - }, + var imgs = this._getAllNodesWithTag(articleContent, ['img']); + this._forEachNode(imgs, function(img) { + var src = img.getAttribute('src'); + if (src) { + img.setAttribute('src', toAbsoluteURI(src)); + } + }); + }, - /** + /** * Get the article title as an H1. * * @return void **/ - _getArticleTitle: function() { - var doc = this._doc; - var curTitle = ""; - var origTitle = ""; + _getArticleTitle: function() { + var doc = this._doc; + var curTitle = ''; + var origTitle = ''; - try { - curTitle = origTitle = doc.title.trim(); + try { + curTitle = origTitle = doc.title.trim(); - // If they had an element with id "title" in their HTML - if (typeof curTitle !== "string") - curTitle = origTitle = this._getInnerText(doc.getElementsByTagName("title")[0]); - } catch (e) {/* ignore exceptions setting the title. */} + // If they had an element with id "title" in their HTML + if (typeof curTitle !== 'string') + curTitle = origTitle = this._getInnerText(doc.getElementsByTagName('title')[0]); + } catch (e) {/* ignore exceptions setting the title. */} - var titleHadHierarchicalSeparators = false; - function wordCount(str) { - return str.split(/\s+/).length; - } + var titleHadHierarchicalSeparators = false; + function wordCount(str) { + return str.split(/\s+/).length; + } - // If there's a separator in the title, first remove the final part - if ((/ [\|\-\\\/>»] /).test(curTitle)) { - titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle); - curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, "$1"); + // If there's a separator in the title, first remove the final part + if ((/ [\|\-\\\/>»] /).test(curTitle)) { + titleHadHierarchicalSeparators = / [\\\/>»] /.test(curTitle); + curTitle = origTitle.replace(/(.*)[\|\-\\\/>»] .*/gi, '$1'); - // If the resulting title is too short (3 words or fewer), remove - // the first part instead: - if (wordCount(curTitle) < 3) - curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, "$1"); - } else if (curTitle.indexOf(": ") !== -1) { - // Check if we have an heading containing this exact string, so we - // could assume it's the full title. - var headings = this._concatNodeLists( - doc.getElementsByTagName("h1"), - doc.getElementsByTagName("h2") - ); - var trimmedTitle = curTitle.trim(); - var match = this._someNode(headings, function(heading) { - return heading.textContent.trim() === trimmedTitle; - }); + // If the resulting title is too short (3 words or fewer), remove + // the first part instead: + if (wordCount(curTitle) < 3) + curTitle = origTitle.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi, '$1'); + } else if (curTitle.indexOf(': ') !== -1) { + // Check if we have an heading containing this exact string, so we + // could assume it's the full title. + var headings = this._concatNodeLists( + doc.getElementsByTagName('h1'), + doc.getElementsByTagName('h2') + ); + var trimmedTitle = curTitle.trim(); + var match = this._someNode(headings, function(heading) { + return heading.textContent.trim() === trimmedTitle; + }); - // If we don't, let's extract the title out of the original title string. - if (!match) { - curTitle = origTitle.substring(origTitle.lastIndexOf(":") + 1); + // If we don't, let's extract the title out of the original title string. + if (!match) { + curTitle = origTitle.substring(origTitle.lastIndexOf(':') + 1); - // If the title is now too short, try the first colon instead: - if (wordCount(curTitle) < 3) { - curTitle = origTitle.substring(origTitle.indexOf(":") + 1); - // But if we have too many words before the colon there's something weird - // with the titles and the H tags so let's just use the original title instead - } else if (wordCount(origTitle.substr(0, origTitle.indexOf(":"))) > 5) { - curTitle = origTitle; - } - } - } else if (curTitle.length > 150 || curTitle.length < 15) { - var hOnes = doc.getElementsByTagName("h1"); + // If the title is now too short, try the first colon instead: + if (wordCount(curTitle) < 3) { + curTitle = origTitle.substring(origTitle.indexOf(':') + 1); + // But if we have too many words before the colon there's something weird + // with the titles and the H tags so let's just use the original title instead + } else if (wordCount(origTitle.substr(0, origTitle.indexOf(':'))) > 5) { + curTitle = origTitle; + } + } + } else if (curTitle.length > 150 || curTitle.length < 15) { + var hOnes = doc.getElementsByTagName('h1'); - if (hOnes.length === 1) - curTitle = this._getInnerText(hOnes[0]); - } + if (hOnes.length === 1) + curTitle = this._getInnerText(hOnes[0]); + } - curTitle = curTitle.trim().replace(this.REGEXPS.normalize, " "); - // If we now have 4 words or fewer as our title, and either no - // 'hierarchical' separators (\, /, > or ») were found in the original - // title or we decreased the number of words by more than 1 word, use - // the original title. - var curTitleWordCount = wordCount(curTitle); - if (curTitleWordCount <= 4 && + curTitle = curTitle.trim().replace(this.REGEXPS.normalize, ' '); + // If we now have 4 words or fewer as our title, and either no + // 'hierarchical' separators (\, /, > or ») were found in the original + // title or we decreased the number of words by more than 1 word, use + // the original title. + var curTitleWordCount = wordCount(curTitle); + if (curTitleWordCount <= 4 && (!titleHadHierarchicalSeparators || - curTitleWordCount != wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, "")) - 1)) { - curTitle = origTitle; - } + curTitleWordCount != wordCount(origTitle.replace(/[\|\-\\\/>»]+/g, '')) - 1)) { + curTitle = origTitle; + } - return curTitle; - }, + return curTitle; + }, - /** + /** * Prepare the HTML document for readability to scrape it. * This includes things like stripping javascript, CSS, and handling terrible markup. * * @return void **/ - _prepDocument: function() { - var doc = this._doc; + _prepDocument: function() { + var doc = this._doc; - // Remove all style tags in head - this._removeNodes(doc.getElementsByTagName("style")); + // Remove all style tags in head + this._removeNodes(doc.getElementsByTagName('style')); - if (doc.body) { - this._replaceBrs(doc.body); - } + if (doc.body) { + this._replaceBrs(doc.body); + } - this._replaceNodeTags(doc.getElementsByTagName("font"), "SPAN"); - }, + this._replaceNodeTags(doc.getElementsByTagName('font'), 'SPAN'); + }, - /** + /** * Finds the next element, starting from the given node, and ignoring * whitespace in between. If the given node is an element, the same node is * returned. */ - _nextElement: function (node) { - var next = node; - while (next + _nextElement: function (node) { + var next = node; + while (next && (next.nodeType != this.ELEMENT_NODE) && this.REGEXPS.whitespace.test(next.textContent)) { - next = next.nextSibling; - } - return next; - }, + next = next.nextSibling; + } + return next; + }, - /** + /** * Replaces 2 or more successive
elements with a single

. * Whitespace between
elements are ignored. For example: *

foo
bar


abc
* will become: *
foo
bar

abc

*/ - _replaceBrs: function (elem) { - this._forEachNode(this._getAllNodesWithTag(elem, ["br"]), function(br) { - var next = br.nextSibling; + _replaceBrs: function (elem) { + this._forEachNode(this._getAllNodesWithTag(elem, ['br']), function(br) { + var next = br.nextSibling; - // Whether 2 or more
elements have been found and replaced with a - //

block. - var replaced = false; + // Whether 2 or more
elements have been found and replaced with a + //

block. + var replaced = false; - // If we find a
chain, remove the
s until we hit another element - // or non-whitespace. This leaves behind the first
in the chain - // (which will be replaced with a

later). - while ((next = this._nextElement(next)) && (next.tagName == "BR")) { - replaced = true; - var brSibling = next.nextSibling; - next.parentNode.removeChild(next); - next = brSibling; - } + // If we find a
chain, remove the
s until we hit another element + // or non-whitespace. This leaves behind the first
in the chain + // (which will be replaced with a

later). + while ((next = this._nextElement(next)) && (next.tagName == 'BR')) { + replaced = true; + var brSibling = next.nextSibling; + next.parentNode.removeChild(next); + next = brSibling; + } - // If we removed a
chain, replace the remaining
with a

. Add - // all sibling nodes as children of the

until we hit another
- // chain. - if (replaced) { - var p = this._doc.createElement("p"); - br.parentNode.replaceChild(p, br); + // If we removed a
chain, replace the remaining
with a

. Add + // all sibling nodes as children of the

until we hit another
+ // chain. + if (replaced) { + var p = this._doc.createElement('p'); + br.parentNode.replaceChild(p, br); - next = p.nextSibling; - while (next) { - // If we've hit another

, we're done adding children to this

. - if (next.tagName == "BR") { - var nextElem = this._nextElement(next.nextSibling); - if (nextElem && nextElem.tagName == "BR") - break; - } + next = p.nextSibling; + while (next) { + // If we've hit another

, we're done adding children to this

. + if (next.tagName == 'BR') { + var nextElem = this._nextElement(next.nextSibling); + if (nextElem && nextElem.tagName == 'BR') + break; + } - if (!this._isPhrasingContent(next)) - break; + if (!this._isPhrasingContent(next)) + break; - // Otherwise, make this node a child of the new

. - var sibling = next.nextSibling; - p.appendChild(next); - next = sibling; - } + // Otherwise, make this node a child of the new

. + var sibling = next.nextSibling; + p.appendChild(next); + next = sibling; + } - while (p.lastChild && this._isWhitespace(p.lastChild)) { - p.removeChild(p.lastChild); - } + while (p.lastChild && this._isWhitespace(p.lastChild)) { + p.removeChild(p.lastChild); + } - if (p.parentNode.tagName === "P") - this._setNodeTag(p.parentNode, "DIV"); - } - }); - }, + if (p.parentNode.tagName === 'P') + this._setNodeTag(p.parentNode, 'DIV'); + } + }); + }, - _setNodeTag: function (node, tag) { - this.log("_setNodeTag", node, tag); - if (node.__JSDOMParser__) { - node.localName = tag.toLowerCase(); - node.tagName = tag.toUpperCase(); - return node; - } + _setNodeTag: function (node, tag) { + this.log('_setNodeTag', node, tag); + if (node.__JSDOMParser__) { + node.localName = tag.toLowerCase(); + node.tagName = tag.toUpperCase(); + return node; + } - var replacement = node.ownerDocument.createElement(tag); - while (node.firstChild) { - replacement.appendChild(node.firstChild); - } - node.parentNode.replaceChild(replacement, node); - if (node.readability) - replacement.readability = node.readability; + var replacement = node.ownerDocument.createElement(tag); + while (node.firstChild) { + replacement.appendChild(node.firstChild); + } + node.parentNode.replaceChild(replacement, node); + if (node.readability) + replacement.readability = node.readability; - for (var i = 0; i < node.attributes.length; i++) { - try { - replacement.setAttribute(node.attributes[i].name, node.attributes[i].value); - } catch (ex) { - /* it's possible for setAttribute() to throw if the attribute name + for (var i = 0; i < node.attributes.length; i++) { + try { + replacement.setAttribute(node.attributes[i].name, node.attributes[i].value); + } catch (ex) { + /* it's possible for setAttribute() to throw if the attribute name * isn't a valid XML Name. Such attributes can however be parsed from * source in HTML docs, see https://github.com/whatwg/html/issues/4275, * so we can hit them here and then throw. We don't care about such * attributes so we ignore them. */ - } - } - return replacement; - }, + } + } + return replacement; + }, - /** + /** * Prepare the article node for display. Clean out any inline styles, * iframes, forms, strip extraneous

tags, etc. * * @param Element * @return void **/ - _prepArticle: function(articleContent) { - this._cleanStyles(articleContent); + _prepArticle: function(articleContent) { + this._cleanStyles(articleContent); - // Check for data tables before we continue, to avoid removing items in - // those tables, which will often be isolated even though they're - // visually linked to other content-ful elements (text, images, etc.). - this._markDataTables(articleContent); + // Check for data tables before we continue, to avoid removing items in + // those tables, which will often be isolated even though they're + // visually linked to other content-ful elements (text, images, etc.). + this._markDataTables(articleContent); - this._fixLazyImages(articleContent); + this._fixLazyImages(articleContent); - // Clean out junk from the article content - this._cleanConditionally(articleContent, "form"); - this._cleanConditionally(articleContent, "fieldset"); - this._clean(articleContent, "object"); - this._clean(articleContent, "embed"); - this._clean(articleContent, "h1"); - this._clean(articleContent, "footer"); - this._clean(articleContent, "link"); - this._clean(articleContent, "aside"); + // Clean out junk from the article content + this._cleanConditionally(articleContent, 'form'); + this._cleanConditionally(articleContent, 'fieldset'); + this._clean(articleContent, 'object'); + this._clean(articleContent, 'embed'); + this._clean(articleContent, 'h1'); + this._clean(articleContent, 'footer'); + this._clean(articleContent, 'link'); + this._clean(articleContent, 'aside'); - // Clean out elements with little content that have "share" in their id/class combinations from final top candidates, - // which means we don't remove the top candidates even they have "share". + // Clean out elements with little content that have "share" in their id/class combinations from final top candidates, + // which means we don't remove the top candidates even they have "share". - var shareElementThreshold = this.DEFAULT_CHAR_THRESHOLD; + var shareElementThreshold = this.DEFAULT_CHAR_THRESHOLD; - this._forEachNode(articleContent.children, function (topCandidate) { - this._cleanMatchedNodes(topCandidate, function (node, matchString) { - return /share/.test(matchString) && node.textContent.length < shareElementThreshold; - }); - }); + this._forEachNode(articleContent.children, function (topCandidate) { + this._cleanMatchedNodes(topCandidate, function (node, matchString) { + return /share/.test(matchString) && node.textContent.length < shareElementThreshold; + }); + }); - // If there is only one h2 and its text content substantially equals article title, - // they are probably using it as a header and not a subheader, - // so remove it since we already extract the title separately. - var h2 = articleContent.getElementsByTagName("h2"); - if (h2.length === 1) { - var lengthSimilarRate = (h2[0].textContent.length - this._articleTitle.length) / this._articleTitle.length; - if (Math.abs(lengthSimilarRate) < 0.5) { - var titlesMatch = false; - if (lengthSimilarRate > 0) { - titlesMatch = h2[0].textContent.includes(this._articleTitle); - } else { - titlesMatch = this._articleTitle.includes(h2[0].textContent); - } - if (titlesMatch) { - this._clean(articleContent, "h2"); - } - } - } + // If there is only one h2 and its text content substantially equals article title, + // they are probably using it as a header and not a subheader, + // so remove it since we already extract the title separately. + var h2 = articleContent.getElementsByTagName('h2'); + if (h2.length === 1) { + var lengthSimilarRate = (h2[0].textContent.length - this._articleTitle.length) / this._articleTitle.length; + if (Math.abs(lengthSimilarRate) < 0.5) { + var titlesMatch = false; + if (lengthSimilarRate > 0) { + titlesMatch = h2[0].textContent.includes(this._articleTitle); + } else { + titlesMatch = this._articleTitle.includes(h2[0].textContent); + } + if (titlesMatch) { + this._clean(articleContent, 'h2'); + } + } + } - this._clean(articleContent, "iframe"); - this._clean(articleContent, "input"); - this._clean(articleContent, "textarea"); - this._clean(articleContent, "select"); - this._clean(articleContent, "button"); - this._cleanHeaders(articleContent); + this._clean(articleContent, 'iframe'); + this._clean(articleContent, 'input'); + this._clean(articleContent, 'textarea'); + this._clean(articleContent, 'select'); + this._clean(articleContent, 'button'); + this._cleanHeaders(articleContent); - // Do these last as the previous stuff may have removed junk - // that will affect these - this._cleanConditionally(articleContent, "table"); - this._cleanConditionally(articleContent, "ul"); - this._cleanConditionally(articleContent, "div"); + // Do these last as the previous stuff may have removed junk + // that will affect these + this._cleanConditionally(articleContent, 'table'); + this._cleanConditionally(articleContent, 'ul'); + this._cleanConditionally(articleContent, 'div'); - // Remove extra paragraphs - this._removeNodes(articleContent.getElementsByTagName("p"), function (paragraph) { - var imgCount = paragraph.getElementsByTagName("img").length; - var embedCount = paragraph.getElementsByTagName("embed").length; - var objectCount = paragraph.getElementsByTagName("object").length; - // At this point, nasty iframes have been removed, only remain embedded video ones. - var iframeCount = paragraph.getElementsByTagName("iframe").length; - var totalCount = imgCount + embedCount + objectCount + iframeCount; + // Remove extra paragraphs + this._removeNodes(articleContent.getElementsByTagName('p'), function (paragraph) { + var imgCount = paragraph.getElementsByTagName('img').length; + var embedCount = paragraph.getElementsByTagName('embed').length; + var objectCount = paragraph.getElementsByTagName('object').length; + // At this point, nasty iframes have been removed, only remain embedded video ones. + var iframeCount = paragraph.getElementsByTagName('iframe').length; + var totalCount = imgCount + embedCount + objectCount + iframeCount; - return totalCount === 0 && !this._getInnerText(paragraph, false); - }); + return totalCount === 0 && !this._getInnerText(paragraph, false); + }); - this._forEachNode(this._getAllNodesWithTag(articleContent, ["br"]), function(br) { - var next = this._nextElement(br.nextSibling); - if (next && next.tagName == "P") - br.parentNode.removeChild(br); - }); + this._forEachNode(this._getAllNodesWithTag(articleContent, ['br']), function(br) { + var next = this._nextElement(br.nextSibling); + if (next && next.tagName == 'P') + br.parentNode.removeChild(br); + }); - // Remove single-cell tables - this._forEachNode(this._getAllNodesWithTag(articleContent, ["table"]), function(table) { - var tbody = this._hasSingleTagInsideElement(table, "TBODY") ? table.firstElementChild : table; - if (this._hasSingleTagInsideElement(tbody, "TR")) { - var row = tbody.firstElementChild; - if (this._hasSingleTagInsideElement(row, "TD")) { - var cell = row.firstElementChild; - cell = this._setNodeTag(cell, this._everyNode(cell.childNodes, this._isPhrasingContent) ? "P" : "DIV"); - table.parentNode.replaceChild(cell, table); - } - } - }); - }, + // Remove single-cell tables + this._forEachNode(this._getAllNodesWithTag(articleContent, ['table']), function(table) { + var tbody = this._hasSingleTagInsideElement(table, 'TBODY') ? table.firstElementChild : table; + if (this._hasSingleTagInsideElement(tbody, 'TR')) { + var row = tbody.firstElementChild; + if (this._hasSingleTagInsideElement(row, 'TD')) { + var cell = row.firstElementChild; + cell = this._setNodeTag(cell, this._everyNode(cell.childNodes, this._isPhrasingContent) ? 'P' : 'DIV'); + table.parentNode.replaceChild(cell, table); + } + } + }); + }, - /** + /** * Initialize a node with the readability object. Also checks the * className/id for special names to add to its score. * * @param Element * @return void **/ - _initializeNode: function(node) { - node.readability = {"contentScore": 0}; + _initializeNode: function(node) { + node.readability = {'contentScore': 0}; - switch (node.tagName) { - case "DIV": - node.readability.contentScore += 5; - break; + switch (node.tagName) { + case 'DIV': + node.readability.contentScore += 5; + break; - case "PRE": - case "TD": - case "BLOCKQUOTE": - node.readability.contentScore += 3; - break; + case 'PRE': + case 'TD': + case 'BLOCKQUOTE': + node.readability.contentScore += 3; + break; - case "ADDRESS": - case "OL": - case "UL": - case "DL": - case "DD": - case "DT": - case "LI": - case "FORM": - node.readability.contentScore -= 3; - break; + case 'ADDRESS': + case 'OL': + case 'UL': + case 'DL': + case 'DD': + case 'DT': + case 'LI': + case 'FORM': + node.readability.contentScore -= 3; + break; - case "H1": - case "H2": - case "H3": - case "H4": - case "H5": - case "H6": - case "TH": - node.readability.contentScore -= 5; - break; - } + case 'H1': + case 'H2': + case 'H3': + case 'H4': + case 'H5': + case 'H6': + case 'TH': + node.readability.contentScore -= 5; + break; + } - node.readability.contentScore += this._getClassWeight(node); - }, + node.readability.contentScore += this._getClassWeight(node); + }, - _removeAndGetNext: function(node) { - var nextNode = this._getNextNode(node, true); - node.parentNode.removeChild(node); - return nextNode; - }, + _removeAndGetNext: function(node) { + var nextNode = this._getNextNode(node, true); + node.parentNode.removeChild(node); + return nextNode; + }, - /** + /** * Traverse the DOM from node to node, starting at the node passed in. * Pass true for the second parameter to indicate this node itself * (and its kids) are going away, and we want the next node over. * * Calling this in a loop will traverse the DOM depth-first. */ - _getNextNode: function(node, ignoreSelfAndKids) { - // First check for kids if those aren't being ignored - if (!ignoreSelfAndKids && node.firstElementChild) { - return node.firstElementChild; - } - // Then for siblings... - if (node.nextElementSibling) { - return node.nextElementSibling; - } - // And finally, move up the parent chain *and* find a sibling - // (because this is depth-first traversal, we will have already - // seen the parent nodes themselves). - do { - node = node.parentNode; - } while (node && !node.nextElementSibling); - return node && node.nextElementSibling; - }, + _getNextNode: function(node, ignoreSelfAndKids) { + // First check for kids if those aren't being ignored + if (!ignoreSelfAndKids && node.firstElementChild) { + return node.firstElementChild; + } + // Then for siblings... + if (node.nextElementSibling) { + return node.nextElementSibling; + } + // And finally, move up the parent chain *and* find a sibling + // (because this is depth-first traversal, we will have already + // seen the parent nodes themselves). + do { + node = node.parentNode; + } while (node && !node.nextElementSibling); + return node && node.nextElementSibling; + }, - _checkByline: function(node, matchString) { - if (this._articleByline) { - return false; - } + _checkByline: function(node, matchString) { + if (this._articleByline) { + return false; + } - if (node.getAttribute !== undefined) { - var rel = node.getAttribute("rel"); - var itemprop = node.getAttribute("itemprop"); - } + if (node.getAttribute !== undefined) { + var rel = node.getAttribute('rel'); + var itemprop = node.getAttribute('itemprop'); + } - if ((rel === "author" || (itemprop && itemprop.indexOf("author") !== -1) || this.REGEXPS.byline.test(matchString)) && this._isValidByline(node.textContent)) { - this._articleByline = node.textContent.trim(); - return true; - } + if ((rel === 'author' || (itemprop && itemprop.indexOf('author') !== -1) || this.REGEXPS.byline.test(matchString)) && this._isValidByline(node.textContent)) { + this._articleByline = node.textContent.trim(); + return true; + } - return false; - }, + return false; + }, - _getNodeAncestors: function(node, maxDepth) { - maxDepth = maxDepth || 0; - var i = 0, ancestors = []; - while (node.parentNode) { - ancestors.push(node.parentNode); - if (maxDepth && ++i === maxDepth) - break; - node = node.parentNode; - } - return ancestors; - }, + _getNodeAncestors: function(node, maxDepth) { + maxDepth = maxDepth || 0; + var i = 0, ancestors = []; + while (node.parentNode) { + ancestors.push(node.parentNode); + if (maxDepth && ++i === maxDepth) + break; + node = node.parentNode; + } + return ancestors; + }, - /*** + /*** * grabArticle - Using a variety of metrics (content score, classname, element types), find the content that is * most likely to be the stuff a user wants to read. Then return it wrapped up in a div. * * @param page a document to run upon. Needs to be a full document, complete with body. * @return Element **/ - _grabArticle: function (page) { - this.log("**** grabArticle ****"); - var doc = this._doc; - var isPaging = (page !== null ? true: false); - page = page ? page : this._doc.body; + _grabArticle: function (page) { + this.log('**** grabArticle ****'); + var doc = this._doc; + var isPaging = (page !== null ? true: false); + page = page ? page : this._doc.body; - // We can't grab an article if we don't have a page! - if (!page) { - this.log("No body found in document. Abort."); - return null; - } + // We can't grab an article if we don't have a page! + if (!page) { + this.log('No body found in document. Abort.'); + return null; + } - var pageCacheHtml = page.innerHTML; + var pageCacheHtml = page.innerHTML; - while (true) { - var stripUnlikelyCandidates = this._flagIsActive(this.FLAG_STRIP_UNLIKELYS); + while (true) { + var stripUnlikelyCandidates = this._flagIsActive(this.FLAG_STRIP_UNLIKELYS); - // First, node prepping. Trash nodes that look cruddy (like ones with the - // class name "comment", etc), and turn divs into P tags where they have been - // used inappropriately (as in, where they contain no other block level elements.) - var elementsToScore = []; - var node = this._doc.documentElement; + // First, node prepping. Trash nodes that look cruddy (like ones with the + // class name "comment", etc), and turn divs into P tags where they have been + // used inappropriately (as in, where they contain no other block level elements.) + var elementsToScore = []; + var node = this._doc.documentElement; - while (node) { - var matchString = node.className + " " + node.id; + while (node) { + var matchString = node.className + ' ' + node.id; - if (!this._isProbablyVisible(node)) { - this.log("Removing hidden node - " + matchString); - node = this._removeAndGetNext(node); - continue; - } + if (!this._isProbablyVisible(node)) { + this.log('Removing hidden node - ' + matchString); + node = this._removeAndGetNext(node); + continue; + } - // Check to see if this node is a byline, and remove it if it is. - if (this._checkByline(node, matchString)) { - node = this._removeAndGetNext(node); - continue; - } + // Check to see if this node is a byline, and remove it if it is. + if (this._checkByline(node, matchString)) { + node = this._removeAndGetNext(node); + continue; + } - // Remove unlikely candidates - if (stripUnlikelyCandidates) { - if (this.REGEXPS.unlikelyCandidates.test(matchString) && + // Remove unlikely candidates + if (stripUnlikelyCandidates) { + if (this.REGEXPS.unlikelyCandidates.test(matchString) && !this.REGEXPS.okMaybeItsACandidate.test(matchString) && - !this._hasAncestorTag(node, "table") && - node.tagName !== "BODY" && - node.tagName !== "A") { - this.log("Removing unlikely candidate - " + matchString); - node = this._removeAndGetNext(node); - continue; - } - } + !this._hasAncestorTag(node, 'table') && + node.tagName !== 'BODY' && + node.tagName !== 'A') { + this.log('Removing unlikely candidate - ' + matchString); + node = this._removeAndGetNext(node); + continue; + } + } - // Remove DIV, SECTION, and HEADER nodes without any content(e.g. text, image, video, or iframe). - if ((node.tagName === "DIV" || node.tagName === "SECTION" || node.tagName === "HEADER" || - node.tagName === "H1" || node.tagName === "H2" || node.tagName === "H3" || - node.tagName === "H4" || node.tagName === "H5" || node.tagName === "H6") && + // Remove DIV, SECTION, and HEADER nodes without any content(e.g. text, image, video, or iframe). + if ((node.tagName === 'DIV' || node.tagName === 'SECTION' || node.tagName === 'HEADER' || + node.tagName === 'H1' || node.tagName === 'H2' || node.tagName === 'H3' || + node.tagName === 'H4' || node.tagName === 'H5' || node.tagName === 'H6') && this._isElementWithoutContent(node)) { - node = this._removeAndGetNext(node); - continue; - } + node = this._removeAndGetNext(node); + continue; + } - if (this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName) !== -1) { - elementsToScore.push(node); - } + if (this.DEFAULT_TAGS_TO_SCORE.indexOf(node.tagName) !== -1) { + elementsToScore.push(node); + } - // Turn all divs that don't have children block level elements into p's - if (node.tagName === "DIV") { - // Put phrasing content into paragraphs. - var p = null; - var childNode = node.firstChild; - while (childNode) { - var nextSibling = childNode.nextSibling; - if (this._isPhrasingContent(childNode)) { - if (p !== null) { - p.appendChild(childNode); - } else if (!this._isWhitespace(childNode)) { - p = doc.createElement("p"); - node.replaceChild(p, childNode); - p.appendChild(childNode); - } - } else if (p !== null) { - while (p.lastChild && this._isWhitespace(p.lastChild)) { - p.removeChild(p.lastChild); - } - p = null; - } - childNode = nextSibling; - } + // Turn all divs that don't have children block level elements into p's + if (node.tagName === 'DIV') { + // Put phrasing content into paragraphs. + var p = null; + var childNode = node.firstChild; + while (childNode) { + var nextSibling = childNode.nextSibling; + if (this._isPhrasingContent(childNode)) { + if (p !== null) { + p.appendChild(childNode); + } else if (!this._isWhitespace(childNode)) { + p = doc.createElement('p'); + node.replaceChild(p, childNode); + p.appendChild(childNode); + } + } else if (p !== null) { + while (p.lastChild && this._isWhitespace(p.lastChild)) { + p.removeChild(p.lastChild); + } + p = null; + } + childNode = nextSibling; + } - // Sites like http://mobile.slate.com encloses each paragraph with a DIV - // element. DIVs with only a P element inside and no text content can be - // safely converted into plain P elements to avoid confusing the scoring - // algorithm with DIVs with are, in practice, paragraphs. - if (this._hasSingleTagInsideElement(node, "P") && this._getLinkDensity(node) < 0.25) { - var newNode = node.children[0]; - node.parentNode.replaceChild(newNode, node); - node = newNode; - elementsToScore.push(node); - } else if (!this._hasChildBlockElement(node)) { - node = this._setNodeTag(node, "P"); - elementsToScore.push(node); - } - } - node = this._getNextNode(node); - } + // Sites like http://mobile.slate.com encloses each paragraph with a DIV + // element. DIVs with only a P element inside and no text content can be + // safely converted into plain P elements to avoid confusing the scoring + // algorithm with DIVs with are, in practice, paragraphs. + if (this._hasSingleTagInsideElement(node, 'P') && this._getLinkDensity(node) < 0.25) { + var newNode = node.children[0]; + node.parentNode.replaceChild(newNode, node); + node = newNode; + elementsToScore.push(node); + } else if (!this._hasChildBlockElement(node)) { + node = this._setNodeTag(node, 'P'); + elementsToScore.push(node); + } + } + node = this._getNextNode(node); + } - /** + /** * Loop through all paragraphs, and assign a score to them based on how content-y they look. * Then add their score to their parent node. * * A score is determined by things like number of commas, class names, etc. Maybe eventually link density. **/ - var candidates = []; - this._forEachNode(elementsToScore, function(elementToScore) { - if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === "undefined") - return; + var candidates = []; + this._forEachNode(elementsToScore, function(elementToScore) { + if (!elementToScore.parentNode || typeof(elementToScore.parentNode.tagName) === 'undefined') + return; - // If this paragraph is less than 25 characters, don't even count it. - var innerText = this._getInnerText(elementToScore); - if (innerText.length < 25) - return; + // If this paragraph is less than 25 characters, don't even count it. + var innerText = this._getInnerText(elementToScore); + if (innerText.length < 25) + return; - // Exclude nodes with no ancestor. - var ancestors = this._getNodeAncestors(elementToScore, 3); - if (ancestors.length === 0) - return; + // Exclude nodes with no ancestor. + var ancestors = this._getNodeAncestors(elementToScore, 3); + if (ancestors.length === 0) + return; - var contentScore = 0; + var contentScore = 0; - // Add a point for the paragraph itself as a base. - contentScore += 1; + // Add a point for the paragraph itself as a base. + contentScore += 1; - // Add points for any commas within this paragraph. - contentScore += innerText.split(",").length; + // Add points for any commas within this paragraph. + contentScore += innerText.split(',').length; - // For every 100 characters in this paragraph, add another point. Up to 3 points. - contentScore += Math.min(Math.floor(innerText.length / 100), 3); + // For every 100 characters in this paragraph, add another point. Up to 3 points. + contentScore += Math.min(Math.floor(innerText.length / 100), 3); - // Initialize and score ancestors. - this._forEachNode(ancestors, function(ancestor, level) { - if (!ancestor.tagName || !ancestor.parentNode || typeof(ancestor.parentNode.tagName) === "undefined") - return; + // Initialize and score ancestors. + this._forEachNode(ancestors, function(ancestor, level) { + if (!ancestor.tagName || !ancestor.parentNode || typeof(ancestor.parentNode.tagName) === 'undefined') + return; - if (typeof(ancestor.readability) === "undefined") { - this._initializeNode(ancestor); - candidates.push(ancestor); - } + if (typeof(ancestor.readability) === 'undefined') { + this._initializeNode(ancestor); + candidates.push(ancestor); + } - // Node score divider: - // - parent: 1 (no division) - // - grandparent: 2 - // - great grandparent+: ancestor level * 3 - if (level === 0) - var scoreDivider = 1; - else if (level === 1) - scoreDivider = 2; - else - scoreDivider = level * 3; - ancestor.readability.contentScore += contentScore / scoreDivider; - }); - }); + // Node score divider: + // - parent: 1 (no division) + // - grandparent: 2 + // - great grandparent+: ancestor level * 3 + if (level === 0) + var scoreDivider = 1; + else if (level === 1) + scoreDivider = 2; + else + scoreDivider = level * 3; + ancestor.readability.contentScore += contentScore / scoreDivider; + }); + }); - // After we've calculated scores, loop through all of the possible - // candidate nodes we found and find the one with the highest score. - var topCandidates = []; - for (var c = 0, cl = candidates.length; c < cl; c += 1) { - var candidate = candidates[c]; + // After we've calculated scores, loop through all of the possible + // candidate nodes we found and find the one with the highest score. + var topCandidates = []; + for (var c = 0, cl = candidates.length; c < cl; c += 1) { + var candidate = candidates[c]; - // Scale the final candidates score based on link density. Good content - // should have a relatively small link density (5% or less) and be mostly - // unaffected by this operation. - var candidateScore = candidate.readability.contentScore * (1 - this._getLinkDensity(candidate)); - candidate.readability.contentScore = candidateScore; + // Scale the final candidates score based on link density. Good content + // should have a relatively small link density (5% or less) and be mostly + // unaffected by this operation. + var candidateScore = candidate.readability.contentScore * (1 - this._getLinkDensity(candidate)); + candidate.readability.contentScore = candidateScore; - this.log("Candidate:", candidate, "with score " + candidateScore); + this.log('Candidate:', candidate, 'with score ' + candidateScore); - for (var t = 0; t < this._nbTopCandidates; t++) { - var aTopCandidate = topCandidates[t]; + for (var t = 0; t < this._nbTopCandidates; t++) { + var aTopCandidate = topCandidates[t]; - if (!aTopCandidate || candidateScore > aTopCandidate.readability.contentScore) { - topCandidates.splice(t, 0, candidate); - if (topCandidates.length > this._nbTopCandidates) - topCandidates.pop(); - break; - } - } - } + if (!aTopCandidate || candidateScore > aTopCandidate.readability.contentScore) { + topCandidates.splice(t, 0, candidate); + if (topCandidates.length > this._nbTopCandidates) + topCandidates.pop(); + break; + } + } + } - var topCandidate = topCandidates[0] || null; - var neededToCreateTopCandidate = false; - var parentOfTopCandidate; + var topCandidate = topCandidates[0] || null; + var neededToCreateTopCandidate = false; + var parentOfTopCandidate; - // If we still have no top candidate, just use the body as a last resort. - // We also have to copy the body node so it is something we can modify. - if (topCandidate === null || topCandidate.tagName === "BODY") { - // Move all of the page's children into topCandidate - topCandidate = doc.createElement("DIV"); - neededToCreateTopCandidate = true; - // Move everything (not just elements, also text nodes etc.) into the container - // so we even include text directly in the body: - var kids = page.childNodes; - while (kids.length) { - this.log("Moving child out:", kids[0]); - topCandidate.appendChild(kids[0]); - } + // If we still have no top candidate, just use the body as a last resort. + // We also have to copy the body node so it is something we can modify. + if (topCandidate === null || topCandidate.tagName === 'BODY') { + // Move all of the page's children into topCandidate + topCandidate = doc.createElement('DIV'); + neededToCreateTopCandidate = true; + // Move everything (not just elements, also text nodes etc.) into the container + // so we even include text directly in the body: + var kids = page.childNodes; + while (kids.length) { + this.log('Moving child out:', kids[0]); + topCandidate.appendChild(kids[0]); + } - page.appendChild(topCandidate); + page.appendChild(topCandidate); - this._initializeNode(topCandidate); - } else if (topCandidate) { - // Find a better top candidate node if it contains (at least three) nodes which belong to `topCandidates` array - // and whose scores are quite closed with current `topCandidate` node. - var alternativeCandidateAncestors = []; - for (var i = 1; i < topCandidates.length; i++) { - if (topCandidates[i].readability.contentScore / topCandidate.readability.contentScore >= 0.75) { - alternativeCandidateAncestors.push(this._getNodeAncestors(topCandidates[i])); - } - } - var MINIMUM_TOPCANDIDATES = 3; - if (alternativeCandidateAncestors.length >= MINIMUM_TOPCANDIDATES) { - parentOfTopCandidate = topCandidate.parentNode; - while (parentOfTopCandidate.tagName !== "BODY") { - var listsContainingThisAncestor = 0; - for (var ancestorIndex = 0; ancestorIndex < alternativeCandidateAncestors.length && listsContainingThisAncestor < MINIMUM_TOPCANDIDATES; ancestorIndex++) { - listsContainingThisAncestor += Number(alternativeCandidateAncestors[ancestorIndex].includes(parentOfTopCandidate)); - } - if (listsContainingThisAncestor >= MINIMUM_TOPCANDIDATES) { - topCandidate = parentOfTopCandidate; - break; - } - parentOfTopCandidate = parentOfTopCandidate.parentNode; - } - } - if (!topCandidate.readability) { - this._initializeNode(topCandidate); - } + this._initializeNode(topCandidate); + } else if (topCandidate) { + // Find a better top candidate node if it contains (at least three) nodes which belong to `topCandidates` array + // and whose scores are quite closed with current `topCandidate` node. + var alternativeCandidateAncestors = []; + for (var i = 1; i < topCandidates.length; i++) { + if (topCandidates[i].readability.contentScore / topCandidate.readability.contentScore >= 0.75) { + alternativeCandidateAncestors.push(this._getNodeAncestors(topCandidates[i])); + } + } + var MINIMUM_TOPCANDIDATES = 3; + if (alternativeCandidateAncestors.length >= MINIMUM_TOPCANDIDATES) { + parentOfTopCandidate = topCandidate.parentNode; + while (parentOfTopCandidate.tagName !== 'BODY') { + var listsContainingThisAncestor = 0; + for (var ancestorIndex = 0; ancestorIndex < alternativeCandidateAncestors.length && listsContainingThisAncestor < MINIMUM_TOPCANDIDATES; ancestorIndex++) { + listsContainingThisAncestor += Number(alternativeCandidateAncestors[ancestorIndex].includes(parentOfTopCandidate)); + } + if (listsContainingThisAncestor >= MINIMUM_TOPCANDIDATES) { + topCandidate = parentOfTopCandidate; + break; + } + parentOfTopCandidate = parentOfTopCandidate.parentNode; + } + } + if (!topCandidate.readability) { + this._initializeNode(topCandidate); + } - // Because of our bonus system, parents of candidates might have scores - // themselves. They get half of the node. There won't be nodes with higher - // scores than our topCandidate, but if we see the score going *up* in the first - // few steps up the tree, that's a decent sign that there might be more content - // lurking in other places that we want to unify in. The sibling stuff - // below does some of that - but only if we've looked high enough up the DOM - // tree. - parentOfTopCandidate = topCandidate.parentNode; - var lastScore = topCandidate.readability.contentScore; - // The scores shouldn't get too low. - var scoreThreshold = lastScore / 3; - while (parentOfTopCandidate.tagName !== "BODY") { - if (!parentOfTopCandidate.readability) { - parentOfTopCandidate = parentOfTopCandidate.parentNode; - continue; - } - var parentScore = parentOfTopCandidate.readability.contentScore; - if (parentScore < scoreThreshold) - break; - if (parentScore > lastScore) { - // Alright! We found a better parent to use. - topCandidate = parentOfTopCandidate; - break; - } - lastScore = parentOfTopCandidate.readability.contentScore; - parentOfTopCandidate = parentOfTopCandidate.parentNode; - } + // Because of our bonus system, parents of candidates might have scores + // themselves. They get half of the node. There won't be nodes with higher + // scores than our topCandidate, but if we see the score going *up* in the first + // few steps up the tree, that's a decent sign that there might be more content + // lurking in other places that we want to unify in. The sibling stuff + // below does some of that - but only if we've looked high enough up the DOM + // tree. + parentOfTopCandidate = topCandidate.parentNode; + var lastScore = topCandidate.readability.contentScore; + // The scores shouldn't get too low. + var scoreThreshold = lastScore / 3; + while (parentOfTopCandidate.tagName !== 'BODY') { + if (!parentOfTopCandidate.readability) { + parentOfTopCandidate = parentOfTopCandidate.parentNode; + continue; + } + var parentScore = parentOfTopCandidate.readability.contentScore; + if (parentScore < scoreThreshold) + break; + if (parentScore > lastScore) { + // Alright! We found a better parent to use. + topCandidate = parentOfTopCandidate; + break; + } + lastScore = parentOfTopCandidate.readability.contentScore; + parentOfTopCandidate = parentOfTopCandidate.parentNode; + } - // If the top candidate is the only child, use parent instead. This will help sibling - // joining logic when adjacent content is actually located in parent's sibling node. - parentOfTopCandidate = topCandidate.parentNode; - while (parentOfTopCandidate.tagName != "BODY" && parentOfTopCandidate.children.length == 1) { - topCandidate = parentOfTopCandidate; - parentOfTopCandidate = topCandidate.parentNode; - } - if (!topCandidate.readability) { - this._initializeNode(topCandidate); - } - } + // If the top candidate is the only child, use parent instead. This will help sibling + // joining logic when adjacent content is actually located in parent's sibling node. + parentOfTopCandidate = topCandidate.parentNode; + while (parentOfTopCandidate.tagName != 'BODY' && parentOfTopCandidate.children.length == 1) { + topCandidate = parentOfTopCandidate; + parentOfTopCandidate = topCandidate.parentNode; + } + if (!topCandidate.readability) { + this._initializeNode(topCandidate); + } + } - // Now that we have the top candidate, look through its siblings for content - // that might also be related. Things like preambles, content split by ads - // that we removed, etc. - var articleContent = doc.createElement("DIV"); - if (isPaging) - articleContent.id = "readability-content"; + // Now that we have the top candidate, look through its siblings for content + // that might also be related. Things like preambles, content split by ads + // that we removed, etc. + var articleContent = doc.createElement('DIV'); + if (isPaging) + articleContent.id = 'readability-content'; - var siblingScoreThreshold = Math.max(10, topCandidate.readability.contentScore * 0.2); - // Keep potential top candidate's parent node to try to get text direction of it later. - parentOfTopCandidate = topCandidate.parentNode; - var siblings = parentOfTopCandidate.children; + var siblingScoreThreshold = Math.max(10, topCandidate.readability.contentScore * 0.2); + // Keep potential top candidate's parent node to try to get text direction of it later. + parentOfTopCandidate = topCandidate.parentNode; + var siblings = parentOfTopCandidate.children; - for (var s = 0, sl = siblings.length; s < sl; s++) { - var sibling = siblings[s]; - var append = false; + for (var s = 0, sl = siblings.length; s < sl; s++) { + var sibling = siblings[s]; + var append = false; - this.log("Looking at sibling node:", sibling, sibling.readability ? ("with score " + sibling.readability.contentScore) : ""); - this.log("Sibling has score", sibling.readability ? sibling.readability.contentScore : "Unknown"); + this.log('Looking at sibling node:', sibling, sibling.readability ? ('with score ' + sibling.readability.contentScore) : ''); + this.log('Sibling has score', sibling.readability ? sibling.readability.contentScore : 'Unknown'); - if (sibling === topCandidate) { - append = true; - } else { - var contentBonus = 0; + if (sibling === topCandidate) { + append = true; + } else { + var contentBonus = 0; - // Give a bonus if sibling nodes and top candidates have the example same classname - if (sibling.className === topCandidate.className && topCandidate.className !== "") - contentBonus += topCandidate.readability.contentScore * 0.2; + // Give a bonus if sibling nodes and top candidates have the example same classname + if (sibling.className === topCandidate.className && topCandidate.className !== '') + contentBonus += topCandidate.readability.contentScore * 0.2; - if (sibling.readability && + if (sibling.readability && ((sibling.readability.contentScore + contentBonus) >= siblingScoreThreshold)) { - append = true; - } else if (sibling.nodeName === "P") { - var linkDensity = this._getLinkDensity(sibling); - var nodeContent = this._getInnerText(sibling); - var nodeLength = nodeContent.length; + append = true; + } else if (sibling.nodeName === 'P') { + var linkDensity = this._getLinkDensity(sibling); + var nodeContent = this._getInnerText(sibling); + var nodeLength = nodeContent.length; - if (nodeLength > 80 && linkDensity < 0.25) { - append = true; - } else if (nodeLength < 80 && nodeLength > 0 && linkDensity === 0 && + if (nodeLength > 80 && linkDensity < 0.25) { + append = true; + } else if (nodeLength < 80 && nodeLength > 0 && linkDensity === 0 && nodeContent.search(/\.( |$)/) !== -1) { - append = true; - } - } - } + append = true; + } + } + } - if (append) { - this.log("Appending node:", sibling); + if (append) { + this.log('Appending node:', sibling); - if (this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName) === -1) { - // We have a node that isn't a common block level element, like a form or td tag. - // Turn it into a div so it doesn't get filtered out later by accident. - this.log("Altering sibling:", sibling, "to div."); + if (this.ALTER_TO_DIV_EXCEPTIONS.indexOf(sibling.nodeName) === -1) { + // We have a node that isn't a common block level element, like a form or td tag. + // Turn it into a div so it doesn't get filtered out later by accident. + this.log('Altering sibling:', sibling, 'to div.'); - sibling = this._setNodeTag(sibling, "DIV"); - } + sibling = this._setNodeTag(sibling, 'DIV'); + } - articleContent.appendChild(sibling); - // siblings is a reference to the children array, and - // sibling is removed from the array when we call appendChild(). - // As a result, we must revisit this index since the nodes - // have been shifted. - s -= 1; - sl -= 1; - } - } + articleContent.appendChild(sibling); + // siblings is a reference to the children array, and + // sibling is removed from the array when we call appendChild(). + // As a result, we must revisit this index since the nodes + // have been shifted. + s -= 1; + sl -= 1; + } + } - if (this._debug) - this.log("Article content pre-prep: " + articleContent.innerHTML); - // So we have all of the content that we need. Now we clean it up for presentation. - this._prepArticle(articleContent); - if (this._debug) - this.log("Article content post-prep: " + articleContent.innerHTML); + if (this._debug) + this.log('Article content pre-prep: ' + articleContent.innerHTML); + // So we have all of the content that we need. Now we clean it up for presentation. + this._prepArticle(articleContent); + if (this._debug) + this.log('Article content post-prep: ' + articleContent.innerHTML); - if (neededToCreateTopCandidate) { - // We already created a fake div thing, and there wouldn't have been any siblings left - // for the previous loop, so there's no point trying to create a new div, and then - // move all the children over. Just assign IDs and class names here. No need to append - // because that already happened anyway. - topCandidate.id = "readability-page-1"; - topCandidate.className = "page"; - } else { - var div = doc.createElement("DIV"); - div.id = "readability-page-1"; - div.className = "page"; - var children = articleContent.childNodes; - while (children.length) { - div.appendChild(children[0]); - } - articleContent.appendChild(div); - } + if (neededToCreateTopCandidate) { + // We already created a fake div thing, and there wouldn't have been any siblings left + // for the previous loop, so there's no point trying to create a new div, and then + // move all the children over. Just assign IDs and class names here. No need to append + // because that already happened anyway. + topCandidate.id = 'readability-page-1'; + topCandidate.className = 'page'; + } else { + var div = doc.createElement('DIV'); + div.id = 'readability-page-1'; + div.className = 'page'; + var children = articleContent.childNodes; + while (children.length) { + div.appendChild(children[0]); + } + articleContent.appendChild(div); + } - if (this._debug) - this.log("Article content after paging: " + articleContent.innerHTML); + if (this._debug) + this.log('Article content after paging: ' + articleContent.innerHTML); - var parseSuccessful = true; + var parseSuccessful = true; - // Now that we've gone through the full algorithm, check to see if - // we got any meaningful content. If we didn't, we may need to re-run - // grabArticle with different flags set. This gives us a higher likelihood of - // finding the content, and the sieve approach gives us a higher likelihood of - // finding the -right- content. - var textLength = this._getInnerText(articleContent, true).length; - if (textLength < this._charThreshold) { - parseSuccessful = false; - page.innerHTML = pageCacheHtml; + // Now that we've gone through the full algorithm, check to see if + // we got any meaningful content. If we didn't, we may need to re-run + // grabArticle with different flags set. This gives us a higher likelihood of + // finding the content, and the sieve approach gives us a higher likelihood of + // finding the -right- content. + var textLength = this._getInnerText(articleContent, true).length; + if (textLength < this._charThreshold) { + parseSuccessful = false; + page.innerHTML = pageCacheHtml; - if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) { - this._removeFlag(this.FLAG_STRIP_UNLIKELYS); - this._attempts.push({articleContent: articleContent, textLength: textLength}); - } else if (this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) { - this._removeFlag(this.FLAG_WEIGHT_CLASSES); - this._attempts.push({articleContent: articleContent, textLength: textLength}); - } else if (this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) { - this._removeFlag(this.FLAG_CLEAN_CONDITIONALLY); - this._attempts.push({articleContent: articleContent, textLength: textLength}); - } else { - this._attempts.push({articleContent: articleContent, textLength: textLength}); - // No luck after removing flags, just return the longest text we found during the different loops - this._attempts.sort(function (a, b) { - return b.textLength - a.textLength; - }); + if (this._flagIsActive(this.FLAG_STRIP_UNLIKELYS)) { + this._removeFlag(this.FLAG_STRIP_UNLIKELYS); + this._attempts.push({articleContent: articleContent, textLength: textLength}); + } else if (this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) { + this._removeFlag(this.FLAG_WEIGHT_CLASSES); + this._attempts.push({articleContent: articleContent, textLength: textLength}); + } else if (this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) { + this._removeFlag(this.FLAG_CLEAN_CONDITIONALLY); + this._attempts.push({articleContent: articleContent, textLength: textLength}); + } else { + this._attempts.push({articleContent: articleContent, textLength: textLength}); + // No luck after removing flags, just return the longest text we found during the different loops + this._attempts.sort(function (a, b) { + return b.textLength - a.textLength; + }); - // But first check if we actually have something - if (!this._attempts[0].textLength) { - return null; - } + // But first check if we actually have something + if (!this._attempts[0].textLength) { + return null; + } - articleContent = this._attempts[0].articleContent; - parseSuccessful = true; - } - } + articleContent = this._attempts[0].articleContent; + parseSuccessful = true; + } + } - if (parseSuccessful) { - // Find out text direction from ancestors of final top candidate. - var ancestors = [parentOfTopCandidate, topCandidate].concat(this._getNodeAncestors(parentOfTopCandidate)); - this._someNode(ancestors, function(ancestor) { - if (!ancestor.tagName) - return false; - var articleDir = ancestor.getAttribute("dir"); - if (articleDir) { - this._articleDir = articleDir; - return true; - } - return false; - }); - return articleContent; - } - } - }, + if (parseSuccessful) { + // Find out text direction from ancestors of final top candidate. + var ancestors = [parentOfTopCandidate, topCandidate].concat(this._getNodeAncestors(parentOfTopCandidate)); + this._someNode(ancestors, function(ancestor) { + if (!ancestor.tagName) + return false; + var articleDir = ancestor.getAttribute('dir'); + if (articleDir) { + this._articleDir = articleDir; + return true; + } + return false; + }); + return articleContent; + } + } + }, - /** + /** * Check whether the input string could be a byline. * This verifies that the input is a string, and that the length * is less than 100 chars. @@ -1204,112 +1204,112 @@ Readability.prototype = { * @param possibleByline {string} - a string to check whether its a byline. * @return Boolean - whether the input string is a byline. */ - _isValidByline: function(byline) { - if (typeof byline == "string" || byline instanceof String) { - byline = byline.trim(); - return (byline.length > 0) && (byline.length < 100); - } - return false; - }, + _isValidByline: function(byline) { + if (typeof byline == 'string' || byline instanceof String) { + byline = byline.trim(); + return (byline.length > 0) && (byline.length < 100); + } + return false; + }, - /** + /** * Attempts to get excerpt and byline metadata for the article. * * @return Object with optional "excerpt" and "byline" properties */ - _getArticleMetadata: function() { - var metadata = {}; - var values = {}; - var metaElements = this._doc.getElementsByTagName("meta"); + _getArticleMetadata: function() { + var metadata = {}; + var values = {}; + var metaElements = this._doc.getElementsByTagName('meta'); - // property is a space-separated list of values - var propertyPattern = /\s*(dc|dcterm|og|twitter)\s*:\s*(author|creator|description|title|site_name)\s*/gi; + // property is a space-separated list of values + var propertyPattern = /\s*(dc|dcterm|og|twitter)\s*:\s*(author|creator|description|title|site_name)\s*/gi; - // name is a single value - var namePattern = /^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i; + // name is a single value + var namePattern = /^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i; - // Find description tags. - this._forEachNode(metaElements, function(element) { - var elementName = element.getAttribute("name"); - var elementProperty = element.getAttribute("property"); - var content = element.getAttribute("content"); - if (!content) { - return; - } - var matches = null; - var name = null; + // Find description tags. + this._forEachNode(metaElements, function(element) { + var elementName = element.getAttribute('name'); + var elementProperty = element.getAttribute('property'); + var content = element.getAttribute('content'); + if (!content) { + return; + } + var matches = null; + var name = null; - if (elementProperty) { - matches = elementProperty.match(propertyPattern); - if (matches) { - for (var i = matches.length - 1; i >= 0; i--) { - // Convert to lowercase, and remove any whitespace - // so we can match below. - name = matches[i].toLowerCase().replace(/\s/g, ""); - // multiple authors - values[name] = content.trim(); - } - } - } - if (!matches && elementName && namePattern.test(elementName)) { - name = elementName; - if (content) { - // Convert to lowercase, remove any whitespace, and convert dots - // to colons so we can match below. - name = name.toLowerCase().replace(/\s/g, "").replace(/\./g, ":"); - values[name] = content.trim(); - } - } - }); + if (elementProperty) { + matches = elementProperty.match(propertyPattern); + if (matches) { + for (var i = matches.length - 1; i >= 0; i--) { + // Convert to lowercase, and remove any whitespace + // so we can match below. + name = matches[i].toLowerCase().replace(/\s/g, ''); + // multiple authors + values[name] = content.trim(); + } + } + } + if (!matches && elementName && namePattern.test(elementName)) { + name = elementName; + if (content) { + // Convert to lowercase, remove any whitespace, and convert dots + // to colons so we can match below. + name = name.toLowerCase().replace(/\s/g, '').replace(/\./g, ':'); + values[name] = content.trim(); + } + } + }); - // get title - metadata.title = values["dc:title"] || - values["dcterm:title"] || - values["og:title"] || - values["weibo:article:title"] || - values["weibo:webpage:title"] || - values["title"] || - values["twitter:title"]; + // get title + metadata.title = values['dc:title'] || + values['dcterm:title'] || + values['og:title'] || + values['weibo:article:title'] || + values['weibo:webpage:title'] || + values['title'] || + values['twitter:title']; - if (!metadata.title) { - metadata.title = this._getArticleTitle(); - } + if (!metadata.title) { + metadata.title = this._getArticleTitle(); + } - // get author - metadata.byline = values["dc:creator"] || - values["dcterm:creator"] || - values["author"]; + // get author + metadata.byline = values['dc:creator'] || + values['dcterm:creator'] || + values['author']; - // get description - metadata.excerpt = values["dc:description"] || - values["dcterm:description"] || - values["og:description"] || - values["weibo:article:description"] || - values["weibo:webpage:description"] || - values["description"] || - values["twitter:description"]; + // get description + metadata.excerpt = values['dc:description'] || + values['dcterm:description'] || + values['og:description'] || + values['weibo:article:description'] || + values['weibo:webpage:description'] || + values['description'] || + values['twitter:description']; - // get site name - metadata.siteName = values["og:site_name"]; + // get site name + metadata.siteName = values['og:site_name']; - return metadata; - }, + return metadata; + }, - /** + /** * Removes script tags from the document. * * @param Element **/ - _removeScripts: function(doc) { - this._removeNodes(doc.getElementsByTagName("script"), function(scriptNode) { - scriptNode.nodeValue = ""; - scriptNode.removeAttribute("src"); - return true; - }); - this._removeNodes(doc.getElementsByTagName("noscript")); - }, + _removeScripts: function(doc) { + this._removeNodes(doc.getElementsByTagName('script'), function(scriptNode) { + scriptNode.nodeValue = ''; + scriptNode.removeAttribute('src'); + return true; + }); + this._removeNodes(doc.getElementsByTagName('noscript')); + }, - /** + /** * Check if this node has only whitespace and a single element with given tag * Returns false if the DIV node contains non-empty text nodes * or if it contains no element with given tag or more than 1 element. @@ -1317,54 +1317,54 @@ Readability.prototype = { * @param Element * @param string tag of child element **/ - _hasSingleTagInsideElement: function(element, tag) { - // There should be exactly 1 element child with given tag - if (element.children.length != 1 || element.children[0].tagName !== tag) { - return false; - } + _hasSingleTagInsideElement: function(element, tag) { + // There should be exactly 1 element child with given tag + if (element.children.length != 1 || element.children[0].tagName !== tag) { + return false; + } - // And there should be no text nodes with real content - return !this._someNode(element.childNodes, function(node) { - return node.nodeType === this.TEXT_NODE && + // And there should be no text nodes with real content + return !this._someNode(element.childNodes, function(node) { + return node.nodeType === this.TEXT_NODE && this.REGEXPS.hasContent.test(node.textContent); - }); - }, + }); + }, - _isElementWithoutContent: function(node) { - return node.nodeType === this.ELEMENT_NODE && + _isElementWithoutContent: function(node) { + return node.nodeType === this.ELEMENT_NODE && node.textContent.trim().length == 0 && (node.children.length == 0 || - node.children.length == node.getElementsByTagName("br").length + node.getElementsByTagName("hr").length); - }, + node.children.length == node.getElementsByTagName('br').length + node.getElementsByTagName('hr').length); + }, - /** + /** * Determine whether element has any children block level elements. * * @param Element */ - _hasChildBlockElement: function (element) { - return this._someNode(element.childNodes, function(node) { - return this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 || + _hasChildBlockElement: function (element) { + return this._someNode(element.childNodes, function(node) { + return this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 || this._hasChildBlockElement(node); - }); - }, + }); + }, - /*** + /*** * Determine if a node qualifies as phrasing content. * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Phrasing_content **/ - _isPhrasingContent: function(node) { - return node.nodeType === this.TEXT_NODE || this.PHRASING_ELEMS.indexOf(node.tagName) !== -1 || - ((node.tagName === "A" || node.tagName === "DEL" || node.tagName === "INS") && + _isPhrasingContent: function(node) { + return node.nodeType === this.TEXT_NODE || this.PHRASING_ELEMS.indexOf(node.tagName) !== -1 || + ((node.tagName === 'A' || node.tagName === 'DEL' || node.tagName === 'INS') && this._everyNode(node.childNodes, this._isPhrasingContent)); - }, + }, - _isWhitespace: function(node) { - return (node.nodeType === this.TEXT_NODE && node.textContent.trim().length === 0) || - (node.nodeType === this.ELEMENT_NODE && node.tagName === "BR"); - }, + _isWhitespace: function(node) { + return (node.nodeType === this.TEXT_NODE && node.textContent.trim().length === 0) || + (node.nodeType === this.ELEMENT_NODE && node.tagName === 'BR'); + }, - /** + /** * Get the inner text of a node - cross browser compatibly. * This also strips out any excess whitespace to be found. * @@ -1372,113 +1372,113 @@ Readability.prototype = { * @param Boolean normalizeSpaces (default: true) * @return string **/ - _getInnerText: function(e, normalizeSpaces) { - normalizeSpaces = (typeof normalizeSpaces === "undefined") ? true : normalizeSpaces; - var textContent = e.textContent.trim(); + _getInnerText: function(e, normalizeSpaces) { + normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces; + var textContent = e.textContent.trim(); - if (normalizeSpaces) { - return textContent.replace(this.REGEXPS.normalize, " "); - } - return textContent; - }, + if (normalizeSpaces) { + return textContent.replace(this.REGEXPS.normalize, ' '); + } + return textContent; + }, - /** + /** * Get the number of times a string s appears in the node e. * * @param Element * @param string - what to split on. Default is "," * @return number (integer) **/ - _getCharCount: function(e, s) { - s = s || ","; - return this._getInnerText(e).split(s).length - 1; - }, + _getCharCount: function(e, s) { + s = s || ','; + return this._getInnerText(e).split(s).length - 1; + }, - /** + /** * Remove the style attribute on every e and under. * TODO: Test if getElementsByTagName(*) is faster. * * @param Element * @return void **/ - _cleanStyles: function(e) { - if (!e || e.tagName.toLowerCase() === "svg") - return; + _cleanStyles: function(e) { + if (!e || e.tagName.toLowerCase() === 'svg') + return; - // Remove `style` and deprecated presentational attributes - for (var i = 0; i < this.PRESENTATIONAL_ATTRIBUTES.length; i++) { - e.removeAttribute(this.PRESENTATIONAL_ATTRIBUTES[i]); - } + // Remove `style` and deprecated presentational attributes + for (var i = 0; i < this.PRESENTATIONAL_ATTRIBUTES.length; i++) { + e.removeAttribute(this.PRESENTATIONAL_ATTRIBUTES[i]); + } - if (this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName) !== -1) { - e.removeAttribute("width"); - e.removeAttribute("height"); - } + if (this.DEPRECATED_SIZE_ATTRIBUTE_ELEMS.indexOf(e.tagName) !== -1) { + e.removeAttribute('width'); + e.removeAttribute('height'); + } - var cur = e.firstElementChild; - while (cur !== null) { - this._cleanStyles(cur); - cur = cur.nextElementSibling; - } - }, + var cur = e.firstElementChild; + while (cur !== null) { + this._cleanStyles(cur); + cur = cur.nextElementSibling; + } + }, - /** + /** * Get the density of links as a percentage of the content * This is the amount of text that is inside a link divided by the total text in the node. * * @param Element * @return number (float) **/ - _getLinkDensity: function(element) { - var textLength = this._getInnerText(element).length; - if (textLength === 0) - return 0; + _getLinkDensity: function(element) { + var textLength = this._getInnerText(element).length; + if (textLength === 0) + return 0; - var linkLength = 0; + var linkLength = 0; - // XXX implement _reduceNodeList? - this._forEachNode(element.getElementsByTagName("a"), function(linkNode) { - linkLength += this._getInnerText(linkNode).length; - }); + // XXX implement _reduceNodeList? + this._forEachNode(element.getElementsByTagName('a'), function(linkNode) { + linkLength += this._getInnerText(linkNode).length; + }); - return linkLength / textLength; - }, + return linkLength / textLength; + }, - /** + /** * Get an elements class/id weight. Uses regular expressions to tell if this * element looks good or bad. * * @param Element * @return number (Integer) **/ - _getClassWeight: function(e) { - if (!this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) - return 0; + _getClassWeight: function(e) { + if (!this._flagIsActive(this.FLAG_WEIGHT_CLASSES)) + return 0; - var weight = 0; + var weight = 0; - // Look for a special classname - if (typeof(e.className) === "string" && e.className !== "") { - if (this.REGEXPS.negative.test(e.className)) - weight -= 25; + // Look for a special classname + if (typeof(e.className) === 'string' && e.className !== '') { + if (this.REGEXPS.negative.test(e.className)) + weight -= 25; - if (this.REGEXPS.positive.test(e.className)) - weight += 25; - } + if (this.REGEXPS.positive.test(e.className)) + weight += 25; + } - // Look for a special ID - if (typeof(e.id) === "string" && e.id !== "") { - if (this.REGEXPS.negative.test(e.id)) - weight -= 25; + // Look for a special ID + if (typeof(e.id) === 'string' && e.id !== '') { + if (this.REGEXPS.negative.test(e.id)) + weight -= 25; - if (this.REGEXPS.positive.test(e.id)) - weight += 25; - } + if (this.REGEXPS.positive.test(e.id)) + weight += 25; + } - return weight; - }, + return weight; + }, - /** + /** * Clean a node of all elements of type "tag". * (Unless it's a youtube/vimeo video. People love movies.) * @@ -1486,30 +1486,30 @@ Readability.prototype = { * @param string tag to clean * @return void **/ - _clean: function(e, tag) { - var isEmbed = ["object", "embed", "iframe"].indexOf(tag) !== -1; + _clean: function(e, tag) { + var isEmbed = ['object', 'embed', 'iframe'].indexOf(tag) !== -1; - this._removeNodes(e.getElementsByTagName(tag), function(element) { - // Allow youtube and vimeo videos through as people usually want to see those. - if (isEmbed) { - // First, check the elements attributes to see if any of them contain youtube or vimeo - for (var i = 0; i < element.attributes.length; i++) { - if (this.REGEXPS.videos.test(element.attributes[i].value)) { - return false; - } - } + this._removeNodes(e.getElementsByTagName(tag), function(element) { + // Allow youtube and vimeo videos through as people usually want to see those. + if (isEmbed) { + // First, check the elements attributes to see if any of them contain youtube or vimeo + for (var i = 0; i < element.attributes.length; i++) { + if (this.REGEXPS.videos.test(element.attributes[i].value)) { + return false; + } + } - // For embed with tag, check inner HTML as well. - if (element.tagName === "object" && this.REGEXPS.videos.test(element.innerHTML)) { - return false; - } - } + // For embed with tag, check inner HTML as well. + if (element.tagName === 'object' && this.REGEXPS.videos.test(element.innerHTML)) { + return false; + } + } - return true; - }); - }, + return true; + }); + }, - /** + /** * Check if a given node has one of its ancestor tag name matching the * provided one. * @param HTMLElement node @@ -1518,276 +1518,276 @@ Readability.prototype = { * @param Function filterFn a filter to invoke to determine whether this node 'counts' * @return Boolean */ - _hasAncestorTag: function(node, tagName, maxDepth, filterFn) { - maxDepth = maxDepth || 3; - tagName = tagName.toUpperCase(); - var depth = 0; - while (node.parentNode) { - if (maxDepth > 0 && depth > maxDepth) - return false; - if (node.parentNode.tagName === tagName && (!filterFn || filterFn(node.parentNode))) - return true; - node = node.parentNode; - depth++; - } - return false; - }, + _hasAncestorTag: function(node, tagName, maxDepth, filterFn) { + maxDepth = maxDepth || 3; + tagName = tagName.toUpperCase(); + var depth = 0; + while (node.parentNode) { + if (maxDepth > 0 && depth > maxDepth) + return false; + if (node.parentNode.tagName === tagName && (!filterFn || filterFn(node.parentNode))) + return true; + node = node.parentNode; + depth++; + } + return false; + }, - /** + /** * Return an object indicating how many rows and columns this table has. */ - _getRowAndColumnCount: function(table) { - var rows = 0; - var columns = 0; - var trs = table.getElementsByTagName("tr"); - for (var i = 0; i < trs.length; i++) { - var rowspan = trs[i].getAttribute("rowspan") || 0; - if (rowspan) { - rowspan = parseInt(rowspan, 10); - } - rows += (rowspan || 1); + _getRowAndColumnCount: function(table) { + var rows = 0; + var columns = 0; + var trs = table.getElementsByTagName('tr'); + for (var i = 0; i < trs.length; i++) { + var rowspan = trs[i].getAttribute('rowspan') || 0; + if (rowspan) { + rowspan = parseInt(rowspan, 10); + } + rows += (rowspan || 1); - // Now look for column-related info - var columnsInThisRow = 0; - var cells = trs[i].getElementsByTagName("td"); - for (var j = 0; j < cells.length; j++) { - var colspan = cells[j].getAttribute("colspan") || 0; - if (colspan) { - colspan = parseInt(colspan, 10); - } - columnsInThisRow += (colspan || 1); - } - columns = Math.max(columns, columnsInThisRow); - } - return {rows: rows, columns: columns}; - }, + // Now look for column-related info + var columnsInThisRow = 0; + var cells = trs[i].getElementsByTagName('td'); + for (var j = 0; j < cells.length; j++) { + var colspan = cells[j].getAttribute('colspan') || 0; + if (colspan) { + colspan = parseInt(colspan, 10); + } + columnsInThisRow += (colspan || 1); + } + columns = Math.max(columns, columnsInThisRow); + } + return {rows: rows, columns: columns}; + }, - /** + /** * Look for 'data' (as opposed to 'layout') tables, for which we use * similar checks as * https://dxr.mozilla.org/mozilla-central/rev/71224049c0b52ab190564d3ea0eab089a159a4cf/accessible/html/HTMLTableAccessible.cpp#920 */ - _markDataTables: function(root) { - var tables = root.getElementsByTagName("table"); - for (var i = 0; i < tables.length; i++) { - var table = tables[i]; - var role = table.getAttribute("role"); - if (role == "presentation") { - table._readabilityDataTable = false; - continue; - } - var datatable = table.getAttribute("datatable"); - if (datatable == "0") { - table._readabilityDataTable = false; - continue; - } - var summary = table.getAttribute("summary"); - if (summary) { - table._readabilityDataTable = true; - continue; - } + _markDataTables: function(root) { + var tables = root.getElementsByTagName('table'); + for (var i = 0; i < tables.length; i++) { + var table = tables[i]; + var role = table.getAttribute('role'); + if (role == 'presentation') { + table._readabilityDataTable = false; + continue; + } + var datatable = table.getAttribute('datatable'); + if (datatable == '0') { + table._readabilityDataTable = false; + continue; + } + var summary = table.getAttribute('summary'); + if (summary) { + table._readabilityDataTable = true; + continue; + } - var caption = table.getElementsByTagName("caption")[0]; - if (caption && caption.childNodes.length > 0) { - table._readabilityDataTable = true; - continue; - } + var caption = table.getElementsByTagName('caption')[0]; + if (caption && caption.childNodes.length > 0) { + table._readabilityDataTable = true; + continue; + } - // If the table has a descendant with any of these tags, consider a data table: - var dataTableDescendants = ["col", "colgroup", "tfoot", "thead", "th"]; - var descendantExists = function(tag) { - return !!table.getElementsByTagName(tag)[0]; - }; - if (dataTableDescendants.some(descendantExists)) { - this.log("Data table because found data-y descendant"); - table._readabilityDataTable = true; - continue; - } + // If the table has a descendant with any of these tags, consider a data table: + var dataTableDescendants = ['col', 'colgroup', 'tfoot', 'thead', 'th']; + var descendantExists = function(tag) { + return !!table.getElementsByTagName(tag)[0]; + }; + if (dataTableDescendants.some(descendantExists)) { + this.log('Data table because found data-y descendant'); + table._readabilityDataTable = true; + continue; + } - // Nested tables indicate a layout table: - if (table.getElementsByTagName("table")[0]) { - table._readabilityDataTable = false; - continue; - } + // Nested tables indicate a layout table: + if (table.getElementsByTagName('table')[0]) { + table._readabilityDataTable = false; + continue; + } - var sizeInfo = this._getRowAndColumnCount(table); - if (sizeInfo.rows >= 10 || sizeInfo.columns > 4) { - table._readabilityDataTable = true; - continue; - } - // Now just go by size entirely: - table._readabilityDataTable = sizeInfo.rows * sizeInfo.columns > 10; - } - }, + var sizeInfo = this._getRowAndColumnCount(table); + if (sizeInfo.rows >= 10 || sizeInfo.columns > 4) { + table._readabilityDataTable = true; + continue; + } + // Now just go by size entirely: + table._readabilityDataTable = sizeInfo.rows * sizeInfo.columns > 10; + } + }, - /* convert images and figures that have properties like data-src into images that can be loaded without JS */ - _fixLazyImages: function (root) { - this._forEachNode(this._getAllNodesWithTag(root, ["img", "picture", "figure"]), function (elem) { - // also check for "null" to work around https://github.com/jsdom/jsdom/issues/2580 - if ((!elem.src && (!elem.srcset || elem.srcset == "null")) || elem.className.toLowerCase().indexOf("lazy") !== -1) { - for (var i = 0; i < elem.attributes.length; i++) { - var attr = elem.attributes[i]; - if (attr.name === "src" || attr.name === "srcset") { - continue; - } - var copyTo = null; - if (/\.(jpg|jpeg|png|webp)\s+\d/.test(attr.value)) { - copyTo = "srcset"; - } else if (/^\s*\S+\.(jpg|jpeg|png|webp)\S*\s*$/.test(attr.value)) { - copyTo = "src"; - } - if (copyTo) { - //if this is an img or picture, set the attribute directly - if (elem.tagName === "IMG" || elem.tagName === "PICTURE") { - elem.setAttribute(copyTo, attr.value); - } else if (elem.tagName === "FIGURE" && !this._getAllNodesWithTag(elem, ["img", "picture"]).length) { - //if the item is a
that does not contain an image or picture, create one and place it inside the figure - //see the nytimes-3 testcase for an example - var img = this._doc.createElement("img"); - img.setAttribute(copyTo, attr.value); - elem.appendChild(img); - } - } - } - } - }); - }, + /* convert images and figures that have properties like data-src into images that can be loaded without JS */ + _fixLazyImages: function (root) { + this._forEachNode(this._getAllNodesWithTag(root, ['img', 'picture', 'figure']), function (elem) { + // also check for "null" to work around https://github.com/jsdom/jsdom/issues/2580 + if ((!elem.src && (!elem.srcset || elem.srcset == 'null')) || elem.className.toLowerCase().indexOf('lazy') !== -1) { + for (var i = 0; i < elem.attributes.length; i++) { + var attr = elem.attributes[i]; + if (attr.name === 'src' || attr.name === 'srcset') { + continue; + } + var copyTo = null; + if (/\.(jpg|jpeg|png|webp)\s+\d/.test(attr.value)) { + copyTo = 'srcset'; + } else if (/^\s*\S+\.(jpg|jpeg|png|webp)\S*\s*$/.test(attr.value)) { + copyTo = 'src'; + } + if (copyTo) { + //if this is an img or picture, set the attribute directly + if (elem.tagName === 'IMG' || elem.tagName === 'PICTURE') { + elem.setAttribute(copyTo, attr.value); + } else if (elem.tagName === 'FIGURE' && !this._getAllNodesWithTag(elem, ['img', 'picture']).length) { + //if the item is a
that does not contain an image or picture, create one and place it inside the figure + //see the nytimes-3 testcase for an example + var img = this._doc.createElement('img'); + img.setAttribute(copyTo, attr.value); + elem.appendChild(img); + } + } + } + } + }); + }, - /** + /** * Clean an element of all tags of type "tag" if they look fishy. * "Fishy" is an algorithm based on content length, classnames, link density, number of images & embeds, etc. * * @return void **/ - _cleanConditionally: function(e, tag) { - if (!this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) - return; + _cleanConditionally: function(e, tag) { + if (!this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)) + return; - var isList = tag === "ul" || tag === "ol"; + var isList = tag === 'ul' || tag === 'ol'; - // Gather counts for other typical elements embedded within. - // Traverse backwards so we can remove nodes at the same time - // without effecting the traversal. - // - // TODO: Consider taking into account original contentScore here. - this._removeNodes(e.getElementsByTagName(tag), function(node) { - // First check if this node IS data table, in which case don't remove it. - var isDataTable = function(t) { - return t._readabilityDataTable; - }; + // Gather counts for other typical elements embedded within. + // Traverse backwards so we can remove nodes at the same time + // without effecting the traversal. + // + // TODO: Consider taking into account original contentScore here. + this._removeNodes(e.getElementsByTagName(tag), function(node) { + // First check if this node IS data table, in which case don't remove it. + var isDataTable = function(t) { + return t._readabilityDataTable; + }; - if (tag === "table" && isDataTable(node)) { - return false; - } + if (tag === 'table' && isDataTable(node)) { + return false; + } - // Next check if we're inside a data table, in which case don't remove it as well. - if (this._hasAncestorTag(node, "table", -1, isDataTable)) { - return false; - } + // Next check if we're inside a data table, in which case don't remove it as well. + if (this._hasAncestorTag(node, 'table', -1, isDataTable)) { + return false; + } - var weight = this._getClassWeight(node); - var contentScore = 0; + var weight = this._getClassWeight(node); + var contentScore = 0; - this.log("Cleaning Conditionally", node); + this.log('Cleaning Conditionally', node); - if (weight + contentScore < 0) { - return true; - } + if (weight + contentScore < 0) { + return true; + } - if (this._getCharCount(node, ",") < 10) { - // If there are not very many commas, and the number of - // non-paragraph elements is more than paragraphs or other - // ominous signs, remove the element. - var p = node.getElementsByTagName("p").length; - var img = node.getElementsByTagName("img").length; - var li = node.getElementsByTagName("li").length - 100; - var input = node.getElementsByTagName("input").length; + if (this._getCharCount(node, ',') < 10) { + // If there are not very many commas, and the number of + // non-paragraph elements is more than paragraphs or other + // ominous signs, remove the element. + var p = node.getElementsByTagName('p').length; + var img = node.getElementsByTagName('img').length; + var li = node.getElementsByTagName('li').length - 100; + var input = node.getElementsByTagName('input').length; - var embedCount = 0; - var embeds = this._concatNodeLists( - node.getElementsByTagName("object"), - node.getElementsByTagName("embed"), - node.getElementsByTagName("iframe")); + var embedCount = 0; + var embeds = this._concatNodeLists( + node.getElementsByTagName('object'), + node.getElementsByTagName('embed'), + node.getElementsByTagName('iframe')); - for (var i = 0; i < embeds.length; i++) { - // If this embed has attribute that matches video regex, don't delete it. - for (var j = 0; j < embeds[i].attributes.length; j++) { - if (this.REGEXPS.videos.test(embeds[i].attributes[j].value)) { - return false; - } - } + for (var i = 0; i < embeds.length; i++) { + // If this embed has attribute that matches video regex, don't delete it. + for (var j = 0; j < embeds[i].attributes.length; j++) { + if (this.REGEXPS.videos.test(embeds[i].attributes[j].value)) { + return false; + } + } - // For embed with tag, check inner HTML as well. - if (embeds[i].tagName === "object" && this.REGEXPS.videos.test(embeds[i].innerHTML)) { - return false; - } + // For embed with tag, check inner HTML as well. + if (embeds[i].tagName === 'object' && this.REGEXPS.videos.test(embeds[i].innerHTML)) { + return false; + } - embedCount++; - } + embedCount++; + } - var linkDensity = this._getLinkDensity(node); - var contentLength = this._getInnerText(node).length; + var linkDensity = this._getLinkDensity(node); + var contentLength = this._getInnerText(node).length; - var haveToRemove = - (img > 1 && p / img < 0.5 && !this._hasAncestorTag(node, "figure")) || + var haveToRemove = + (img > 1 && p / img < 0.5 && !this._hasAncestorTag(node, 'figure')) || (!isList && li > p) || (input > Math.floor(p/3)) || - (!isList && contentLength < 25 && (img === 0 || img > 2) && !this._hasAncestorTag(node, "figure")) || + (!isList && contentLength < 25 && (img === 0 || img > 2) && !this._hasAncestorTag(node, 'figure')) || (!isList && weight < 25 && linkDensity > 0.2) || (weight >= 25 && linkDensity > 0.5) || ((embedCount === 1 && contentLength < 75) || embedCount > 1); - return haveToRemove; - } - return false; - }); - }, + return haveToRemove; + } + return false; + }); + }, - /** + /** * Clean out elements that match the specified conditions * * @param Element * @param Function determines whether a node should be removed * @return void **/ - _cleanMatchedNodes: function(e, filter) { - var endOfSearchMarkerNode = this._getNextNode(e, true); - var next = this._getNextNode(e); - while (next && next != endOfSearchMarkerNode) { - if (filter(next, next.className + " " + next.id)) { - next = this._removeAndGetNext(next); - } else { - next = this._getNextNode(next); - } - } - }, + _cleanMatchedNodes: function(e, filter) { + var endOfSearchMarkerNode = this._getNextNode(e, true); + var next = this._getNextNode(e); + while (next && next != endOfSearchMarkerNode) { + if (filter(next, next.className + ' ' + next.id)) { + next = this._removeAndGetNext(next); + } else { + next = this._getNextNode(next); + } + } + }, - /** + /** * Clean out spurious headers from an Element. Checks things like classnames and link density. * * @param Element * @return void **/ - _cleanHeaders: function(e) { - for (var headerIndex = 1; headerIndex < 3; headerIndex += 1) { - this._removeNodes(e.getElementsByTagName("h" + headerIndex), function (header) { - return this._getClassWeight(header) < 0; - }); - } - }, + _cleanHeaders: function(e) { + for (var headerIndex = 1; headerIndex < 3; headerIndex += 1) { + this._removeNodes(e.getElementsByTagName('h' + headerIndex), function (header) { + return this._getClassWeight(header) < 0; + }); + } + }, - _flagIsActive: function(flag) { - return (this._flags & flag) > 0; - }, + _flagIsActive: function(flag) { + return (this._flags & flag) > 0; + }, - _removeFlag: function(flag) { - this._flags = this._flags & ~flag; - }, + _removeFlag: function(flag) { + this._flags = this._flags & ~flag; + }, - _isProbablyVisible: function(node) { - return (!node.style || node.style.display != "none") && !node.hasAttribute("hidden"); - }, + _isProbablyVisible: function(node) { + return (!node.style || node.style.display != 'none') && !node.hasAttribute('hidden'); + }, - /** + /** * Runs readability. * * Workflow: @@ -1799,55 +1799,55 @@ Readability.prototype = { * * @return void **/ - parse: function () { - // Avoid parsing too large documents, as per configuration option - if (this._maxElemsToParse > 0) { - var numTags = this._doc.getElementsByTagName("*").length; - if (numTags > this._maxElemsToParse) { - throw new Error("Aborting parsing document; " + numTags + " elements found"); - } - } + parse: function () { + // Avoid parsing too large documents, as per configuration option + if (this._maxElemsToParse > 0) { + var numTags = this._doc.getElementsByTagName('*').length; + if (numTags > this._maxElemsToParse) { + throw new Error('Aborting parsing document; ' + numTags + ' elements found'); + } + } - // Remove script tags from the document. - this._removeScripts(this._doc); + // Remove script tags from the document. + this._removeScripts(this._doc); - this._prepDocument(); + this._prepDocument(); - var metadata = this._getArticleMetadata(); - this._articleTitle = metadata.title; + var metadata = this._getArticleMetadata(); + this._articleTitle = metadata.title; - var articleContent = this._grabArticle(); - if (!articleContent) - return null; + var articleContent = this._grabArticle(); + if (!articleContent) + return null; - this.log("Grabbed: " + articleContent.innerHTML); + this.log('Grabbed: ' + articleContent.innerHTML); - this._postProcessContent(articleContent); + this._postProcessContent(articleContent); - // If we haven't found an excerpt in the article's metadata, use the article's - // first paragraph as the excerpt. This is used for displaying a preview of - // the article's content. - if (!metadata.excerpt) { - var paragraphs = articleContent.getElementsByTagName("p"); - if (paragraphs.length > 0) { - metadata.excerpt = paragraphs[0].textContent.trim(); - } - } + // If we haven't found an excerpt in the article's metadata, use the article's + // first paragraph as the excerpt. This is used for displaying a preview of + // the article's content. + if (!metadata.excerpt) { + var paragraphs = articleContent.getElementsByTagName('p'); + if (paragraphs.length > 0) { + metadata.excerpt = paragraphs[0].textContent.trim(); + } + } - var textContent = articleContent.textContent; - return { - title: this._articleTitle, - byline: metadata.byline || this._articleByline, - dir: this._articleDir, - content: articleContent.innerHTML, - textContent: textContent, - length: textContent.length, - excerpt: metadata.excerpt, - siteName: metadata.siteName || this._articleSiteName - }; - } + var textContent = articleContent.textContent; + return { + title: this._articleTitle, + byline: metadata.byline || this._articleByline, + dir: this._articleDir, + content: articleContent.innerHTML, + textContent: textContent, + length: textContent.length, + excerpt: metadata.excerpt, + siteName: metadata.siteName || this._articleSiteName, + }; + }, }; -if (typeof module === "object") { - module.exports = Readability; +if (typeof module === 'object') { + module.exports = Readability; } diff --git a/Clipper/joplin-webclipper/content_scripts/index.js b/Clipper/joplin-webclipper/content_scripts/index.js index b8a80e6e1..f2970fb92 100644 --- a/Clipper/joplin-webclipper/content_scripts/index.js +++ b/Clipper/joplin-webclipper/content_scripts/index.js @@ -7,10 +7,14 @@ let browser_ = null; if (typeof browser !== 'undefined') { + // eslint-disable-next-line no-undef browser_ = browser; + // eslint-disable-next-line no-undef browserSupportsPromises_ = true; } else if (typeof chrome !== 'undefined') { + // eslint-disable-next-line no-undef browser_ = chrome; + // eslint-disable-next-line no-undef browserSupportsPromises_ = false; } @@ -29,7 +33,7 @@ } function pageTitle() { - const titleElements = document.getElementsByTagName("title"); + const titleElements = document.getElementsByTagName('title'); if (titleElements.length) return titleElements[0].text.trim(); return document.title.trim(); } @@ -179,7 +183,7 @@ } if (nodeName === 'source' && nodeParentName === 'picture') { - isVisible = false + isVisible = false; } if (node.nodeType === 8) { // Comments are just removed since we can't add a class @@ -208,7 +212,7 @@ } // Given a document, return a