diff --git a/.eslintignore b/.eslintignore index 9f7712124f..fcba99d7ad 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 c47ef21a2d..30c87e4c81 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,10 +4,25 @@ 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, @@ -24,13 +39,15 @@ module.exports = { "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 9f7712124f..0000000000 --- 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 a3c93e6082..0000000000 --- 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 e96a7a0c33..5b1a8a6573 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 c7425d0390..c17d6e6c00 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 f11b43eaf8..d1d2453596 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 bc5134638e..8883029b30 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 20464127eb..99edb885b4 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 b38510af0c..814daff00c 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 895549b728..4fd5113660 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 7b4bd530fc..d5348d06f1 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 0b22ef4cd8..7d92737bf3 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 04237b681a..fd64a54350 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 51833dcecc..e8a16e0d0c 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 05d2938e07..f5073d8eca 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 b05f3f55b8..fa8c757468 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 0672c26323..01e26124b6 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 c6e6a5650e..e3d761c417 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 9fb39ec450..9dc6b367a1 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 32be81aff7..55fb7538be 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 5b900c4147..e4f7343bc8 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 624bce7522..e7acb002c6 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 eddb3bf694..aaf248054c 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 016e08d78c..e26a88b1b9 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 0f44aef62a..4b559e8972 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 029681a112..54a1b8f1c7 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 cc668b49a1..032a892857 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 79f4c19a7f..ffecfcf872 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 d96fb52751..ca4bc57d96 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 729bab96ef..3747d6c164 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 45b60671ca..28f8d4bbe5 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 b141be41ae..4cf729c4e5 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 8e1ecea2f1..9d8c18d11d 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 3f14157d4c..fc1385f3f2 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 3a01ae5b4e..de28bbce85 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 0bda410bff..fd150b7490 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 1fb41b26d9..81a39991fc 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 eedfa58f7c..eb5d2dbf65 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 1f83e2cdad..b84076ce21 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 3373c0ee32..2e750f3d9c 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 c25a635c2e..bcdfc3a94f 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 9a3603b249..8a02ae6765 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 f28fb0c064..80bd8d8d3a 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 e6bc30bd1c..5cf6e5f9d7 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 ad1f43eaa7..10f7091913 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 30d44df444..34a3029eb3 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 ff68e189d9..1ba0702e3f 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 d0a29538ea..fe2b1f3bad 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 ae911da774..a676f76c68 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 a56359fc7b..fde9421ce7 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 7ee9c17844..d0d0b28b54 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 b9ba881831..301381f987 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 be64ec1aab..6fe3534316 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 fd2a5fbdeb..a77056e4da 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 9174a0a064..b12c84fc28 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 8e48e4989e..1566970857 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 a4a477a3a2..0e37dbd4df 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 f2ddd6af25..a1fad3bea1 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 1603abdbbd..e914e6afa5 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 10be8dd622..145b769634 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 9203b19ed8..ef9c073602 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 9c274e0ffd..9f0cc84c58 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 c43cdee11c..846fe6d88e 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 fbcf1b72a0..760097209e 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 876b337b37..247a55d5d1 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 64ae223055..a344eb1302 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 9a45ef95c0..7bb2937653 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 53cce04394..a1a7de0c25 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 cf136e631f..a959d1674a 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 dba746026f..fce59209a8 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 bb9bd88f1e..3a7bd0532d 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 617ce97974..5c1e461f3c 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 a486e733d3..688f17e762 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 f6433c152a..9610630079 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 84a5c3b1d7..7828f5bd84 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 54469b13c8..12e80f400e 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 968e1ed7c6..3b9d64f598 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 1ced6ddd91..98d3649e25 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 5d910c456e..c4e67ce09d 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 f9fea65e21..b9e8847278 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 dd7cb3a0a5..4fb766c107 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 1be7c73fbf..0e4e8f9904 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 f3eef238b7..db5ab6ce07 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 b8a80e6e12..f2970fb921 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