├── .gitignore ├── transparent-pixel.png ├── webstore ├── screenshots │ ├── options.png │ ├── themes.png │ └── rich-content.png ├── promotional-tiles │ ├── large.png │ ├── marquee.png │ ├── marquee.xcf │ ├── small.png │ └── values.txt ├── description.txt └── description-sv.txt ├── src ├── content-scripts │ ├── isActive.js │ ├── remove.js │ └── add.js ├── options │ ├── options.html │ ├── preOptions.js │ ├── options.css │ └── options.js ├── icons │ ├── browserAction │ │ └── inactive.svg │ └── extension.svg ├── manifest.json ├── _locales │ ├── sv │ │ └── messages.json │ └── en │ │ └── messages.json ├── viewer.js └── background.js ├── .gitmodules ├── README.md ├── package.json ├── BUILDING.md ├── LICENSE ├── gulpfile.js └── .eslintrc.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | dom-distiller-reading-mode-*.zip 4 | -------------------------------------------------------------------------------- /transparent-pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metarmask/dom-distiller-reading-mode/HEAD/transparent-pixel.png -------------------------------------------------------------------------------- /webstore/screenshots/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metarmask/dom-distiller-reading-mode/HEAD/webstore/screenshots/options.png -------------------------------------------------------------------------------- /webstore/screenshots/themes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metarmask/dom-distiller-reading-mode/HEAD/webstore/screenshots/themes.png -------------------------------------------------------------------------------- /webstore/promotional-tiles/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metarmask/dom-distiller-reading-mode/HEAD/webstore/promotional-tiles/large.png -------------------------------------------------------------------------------- /webstore/promotional-tiles/marquee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metarmask/dom-distiller-reading-mode/HEAD/webstore/promotional-tiles/marquee.png -------------------------------------------------------------------------------- /webstore/promotional-tiles/marquee.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metarmask/dom-distiller-reading-mode/HEAD/webstore/promotional-tiles/marquee.xcf -------------------------------------------------------------------------------- /webstore/promotional-tiles/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metarmask/dom-distiller-reading-mode/HEAD/webstore/promotional-tiles/small.png -------------------------------------------------------------------------------- /webstore/screenshots/rich-content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metarmask/dom-distiller-reading-mode/HEAD/webstore/screenshots/rich-content.png -------------------------------------------------------------------------------- /src/content-scripts/isActive.js: -------------------------------------------------------------------------------- 1 | sessionStorage.iframeID = "dom-distiller-result-iframe"; 2 | (() => !!document.getElementById(sessionStorage.iframeID))(); 3 | -------------------------------------------------------------------------------- /src/content-scripts/remove.js: -------------------------------------------------------------------------------- 1 | document.getElementById(sessionStorage.iframeID).remove(); 2 | document.title = sessionStorage.oldTitle; 3 | document.body.setAttribute("style", sessionStorage.oldBodyStyle); 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dom-distiller"] 2 | path = src/external/dom-distiller 3 | url = https://github.com/chromium/dom-distiller 4 | [submodule "src/external/chromium/src"] 5 | path = src/external/chromium/src 6 | url = https://chromium.googlesource.com/chromium/src 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOM Distiller Reading Mode 2 | Uses [DOM Distiller](https://github.com/chromium/dom-distiller) to figure out what is important on a page and displays it in the same consistent way every time. 3 | 4 | ## Resources 5 | * [How to build the extension](BUILDING.md) 6 | * [Attribution for some of the files](webstore/description.txt) 7 | -------------------------------------------------------------------------------- /webstore/promotional-tiles/values.txt: -------------------------------------------------------------------------------- 1 | ## Gradient 2 | 3 | (Material accent colors opposite of icon color) 4 | 5 | * Start: #ffd600 6 | * Stop: #ffff00 7 | 8 | ## Drop shadow 9 | 10 | (https://material.google.com/style/icons.html x2) 11 | 12 | * Offset X: 0 13 | * Offset Y: 8 14 | * Blur radius: 8 15 | * Color: #263238 16 | * Opacity: 40 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "del": "^3.0.0", 4 | "gulp": "github:gulpjs/gulp#4.0", 5 | "gulp-changed-in-place": "^2.3.0", 6 | "gulp-rename": "^1.2.2", 7 | "gulp-replace": "^0.5.4", 8 | "gulp-zip": "^4.1.0", 9 | "optional": "^0.1.3", 10 | "stream-to-promise": "^2.2.0", 11 | "vinyl-buffer": "^1.0.1" 12 | }, 13 | "optionalDependencies": { 14 | "gulp-svg2png": "^2.0.2" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^3.9.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |

__MSG_options_introduction__

10 |

__MSG_options_theme_title__

11 |

__MSG_options_theme_description__

12 |

__MSG_options_font_title__

13 |

__MSG_options_font_description__

14 |

__MSG_options_shortcut_title__

15 |

__MSG_options_shortcut_description__

16 | 17 | 18 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building 2 | 1. Fetch and checkout submodules: 3 | 4 | git submodule update --init --depth 1 src/external/chromium/src 5 | git submodule update --init src/external/dom-distiller 6 | 7 | 2. Change working directory to the DOM distiller repository: 8 | 9 | cd src/external/dom-distiller 10 | 11 | 3. Install Vagrant and do: 12 | 13 | vagrant up && vagrant ssh 14 | 15 | 4. Build the DOM Distiller JavaScript: 16 | 17 | cd /vagrant && 18 | ant extractjs && 19 | exit 20 | 21 | If the build fails due to lack of memory, run the commands again. 22 | 23 | 5. Change working directory to the project root: 24 | 25 | cd ../../.. 26 | 27 | 5. Install NPM dependencies: 28 | 29 | npm install 30 | 31 | 6. Run Gulp: 32 | 33 | ./node_modules/.bin/gulp 34 | -------------------------------------------------------------------------------- /src/options/preOptions.js: -------------------------------------------------------------------------------- 1 | const msgExpressionStart = "__MSG_"; 2 | const msgExpressionEnd = "__"; 3 | const msgXPathResult = document.evaluate( 4 | `//text()[ 5 | starts-with(., "${msgExpressionStart}") and 6 | (substring(., string-length(.) - string-length("${msgExpressionEnd}") + 1) = "${msgExpressionEnd}") 7 | ]`, 8 | document, 9 | null, 10 | XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE 11 | ); 12 | 13 | for (let i = 0; i < msgXPathResult.snapshotLength; i++) { 14 | const textNode = msgXPathResult.snapshotItem(i); 15 | const messageName = Array.from(textNode.nodeValue).slice( 16 | msgExpressionStart.length, 17 | -msgExpressionEnd.length 18 | ) 19 | .join(""); 20 | textNode.nodeValue = chrome.i18n.getMessage(messageName); 21 | } 22 | 23 | // After distillation 24 | setTimeout(() => { 25 | const contentScript = document.createElement("script"); 26 | contentScript.src = "../content-scripts/add.js"; 27 | document.head.appendChild(contentScript); 28 | }, 0) 29 | -------------------------------------------------------------------------------- /src/options/options.css: -------------------------------------------------------------------------------- 1 | .select-button { 2 | display: block; 3 | /* For pseudo-element */ 4 | position: relative; 5 | width: 2em; 6 | height: 2em; 7 | margin: 0 0.2em; 8 | 9 | background: inherit; 10 | border: none; 11 | box-shadow: inset 0 0 2px; 12 | border-radius: 50%; 13 | 14 | font: inherit; 15 | font-size: 1em; 16 | white-space: nowrap; 17 | color: inherit; 18 | } 19 | 20 | .select-button-group { 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | } 25 | 26 | .select-button:focus { 27 | outline: none; 28 | } 29 | 30 | .select-button::before { 31 | content: "*"; 32 | display: block; 33 | position: absolute; 34 | top: calc(50%); 35 | left: 0; 36 | right: 0; 37 | margin: auto; 38 | transition: color 0.3s; 39 | color: transparent; 40 | } 41 | 42 | .select-button:focus::before, .select-button:hover::before { 43 | color: inherit; 44 | } 45 | 46 | #emergency-css { 47 | display: flex; 48 | align-items: center; 49 | } 50 | 51 | #emergency-css > * { 52 | margin: 1em; 53 | } 54 | -------------------------------------------------------------------------------- /src/icons/browserAction/inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_name__", 4 | "short_name": "__MSG_short_name__", 5 | "description": "__MSG_description__", 6 | "version": "1.9", 7 | "minimum_chrome_version": "60", 8 | "icons": { 9 | "128": "icons/extension.svg-128.png", 10 | "48": "icons/extension.svg-48.png", 11 | "32": "icons/extension.svg-32.png", 12 | "16": "icons/extension.svg-16.png" 13 | }, 14 | "default_locale": "en", 15 | "background": { 16 | "scripts": ["background.js"], 17 | "persistent": false 18 | }, 19 | "permissions": [ 20 | "storage", 21 | "activeTab" 22 | ], 23 | "browser_action": { 24 | "default_icon": { 25 | "16": "icons/browserAction/inactive.svg-16.png", 26 | "19": "icons/browserAction/inactive.svg-19.png", 27 | "24": "icons/browserAction/inactive.svg-24.png", 28 | "32": "icons/browserAction/inactive.svg-32.png" 29 | }, 30 | "default_title": "__MSG_browser_action_tooltip_inactive__" 31 | }, 32 | "options_ui": { 33 | "page": "options/options.html", 34 | "chrome_style": true 35 | }, 36 | "web_accessible_resources": [ 37 | "$$(CHROME_DD_CORE)/html/dom_distiller_viewer.html" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/content-scripts/add.js: -------------------------------------------------------------------------------- 1 | sessionStorage.oldTitle = document.title; 2 | sessionStorage.oldBodyStyle = document.body.getAttribute("style"); 3 | 4 | document.body.setAttribute("style", "overflow: hidden !important"); 5 | 6 | addEventListener("message", ({data, origin}) => { 7 | if(origin !== `chrome-extension://${chrome.runtime.id}`) { 8 | return; 9 | } 10 | if(data.action === "setTitle") { 11 | document.title = data.title; 12 | } 13 | }); 14 | 15 | { 16 | const iframe = document.createElement("iframe"); 17 | iframe.addEventListener("load", () => { 18 | iframe.contentWindow.focus(); 19 | }); 20 | iframe.id = sessionStorage.iframeID; 21 | iframe.src = chrome.runtime.getURL("$$(CHROME_DD_CORE)/html/dom_distiller_viewer.html"); 22 | 23 | const style = { 24 | zIndex: 2147483647, 25 | position: "fixed", 26 | left: 0, 27 | right: 0, 28 | top: 0, 29 | bottom: 0, 30 | margin: "auto", 31 | width: "100vw", 32 | height: "100vh", 33 | background: "white", 34 | border: "none" 35 | }; 36 | Object.keys(style).forEach(property => { 37 | iframe.style[property] = style[property]; 38 | }); 39 | 40 | document.documentElement.appendChild(iframe); 41 | } 42 | -------------------------------------------------------------------------------- /src/options/options.js: -------------------------------------------------------------------------------- 1 | { 2 | const link = document.createElement("link"); 3 | link.rel = "stylesheet"; 4 | link.href = "../../../../../../../options/options.css"; 5 | document.head.insertBefore(link, document.head.querySelector("link")); 6 | } 7 | 8 | const options = { 9 | theme: [ 10 | "light", 11 | "dark", 12 | "sepia" 13 | ], 14 | font: [ 15 | "sans-serif", 16 | "serif", 17 | "monospace" 18 | ] 19 | }; 20 | 21 | const headings = document.querySelectorAll("h2"); 22 | Object.keys(options).forEach((option, index) => { 23 | const p = document.createElement("p"); 24 | p.className = "select-button-group"; 25 | options[option].forEach(value => { 26 | const button = document.createElement("button"); 27 | button.className = `select-button ${value}`; 28 | button.title = chrome.i18n.getMessage( 29 | `options_${option}_name_${value.replace(/-/g, "_")}` 30 | ); 31 | if(option === "font") { 32 | button.textContent = chrome.i18n.getMessage( 33 | `options_font_abbreviation_${value.replace(/-/g, "_")}` 34 | ); 35 | } 36 | button.addEventListener("click", () => { 37 | chrome.storage.sync.set({[option]: value}); 38 | }); 39 | p.appendChild(button); 40 | }); 41 | 42 | headings[index].parentElement.insertBefore( 43 | p, 44 | headings[index].nextElementSibling 45 | ); 46 | }); 47 | 48 | { 49 | const p = document.createElement("p"); 50 | const textarea = document.createElement("textarea"); 51 | const button = document.createElement("button"); 52 | 53 | p.appendChild(textarea); 54 | p.appendChild(button); 55 | 56 | p.id = "emergency-css"; 57 | textarea.placeholder = "CSS"; 58 | button.textContent = "✔"; 59 | button.addEventListener("click", event => { 60 | chrome.storage.sync.set({"emergency-css": textarea.value}); 61 | }); 62 | document.querySelector("#content").appendChild(p); 63 | } 64 | -------------------------------------------------------------------------------- /webstore/description.txt: -------------------------------------------------------------------------------- 1 | Have you ever visited a website where the text is so ugly you get a headache trying to read or where the content is hard to find because of unimportant sidebars and share buttons? This extension will fix that for you. 2 | 3 | This extension figures out what is important on a page and shows only that in the same consistent way every time. 4 | 5 | Uses the code that powers the mobile-friendly view on phones. 6 | 7 | Planned features: 8 | 9 | * Font scaling support 10 | 11 | * Multi-page article concatenation 12 | 13 | Update notes: 14 | 15 | 1.9: 16 | * Added option for custom CSS 17 | 18 | 1.8: 19 | * Worked around issue with text direction not being retained properly 20 | * Added instructions for how to change keyboard shortcut to options 21 | 22 | 1.7: 23 | * Fixed options not loading 24 | * Fixed transition to theme colors when viewer loads 25 | * Updated DOM Distiller resources to the latest versions 26 | 27 | 1.6: 28 | * Added feedback form when uninstalling 29 | 30 | 1.5: 31 | * Fixed some sites appearing on top of viewer 32 | * Updated to the latest DOM Distiller version 33 | 34 | 1.4: 35 | * Fixed page scrollbar overlaying the scrollbar of the viewer 36 | 37 | 1.3: 38 | * Fixed distilled content never getting shown 39 | 40 | 1.2: 41 | * Fixed original page jumping to top when hiding distilled version 42 | * Fixed loading indicator not being shown 43 | * Added swedish translation 44 | 45 | 1.1: 46 | * Performance enhancements and bug fixes 47 | 48 | Extension icons were derived from the following icons: 49 | * github.com/google/material-design-icons/blob/c6fc87c71001975022f979a2f6b031620e9c7ff1/action/svg/production/ic_chrome_reader_mode_48px.svg 50 | * github.com/google/material-design-icons/blob/c6fc87c71001975022f979a2f6b031620e9c7ff1/editor/svg/production/ic_bubble_chart_48px.svg 51 | 52 | Both of the icons derived from are licensed under CC BY 4.0 (creativecommons.org/licenses/by/4.0) 53 | -------------------------------------------------------------------------------- /webstore/description-sv.txt: -------------------------------------------------------------------------------- 1 | Har du någonsin besökt en sida där texten är så ful att du får huvudvärk när du försöker läsa den eller där innehållet är svårt att hitta på grund av oviktiga sidopaneler och delningsknappar? Det här tillägget kommer att fixa det åt dig. 2 | 3 | Det här tillägget räknar ut vad som är viktigt på sidan och visar bara det på samma konsekventa sätt varje gång. 4 | 5 | Använder koden som ligger bakom det mobilvänliga läget på mobiler. 6 | 7 | Planerade funktioner: 8 | 9 | * Typsnittsskalningsstöd 10 | 11 | * Sammansättning av flersidiga artiklar 12 | 13 | Uppdateringsnoteringar: 14 | 15 | 1.9: 16 | * La till alternativ för egen CSS 17 | 18 | 1.8: 19 | * Kringgick problem där textriktning inte behölls ordentligt 20 | * La till instruktioner för hur kortkommandon ändras i alternativ 21 | 22 | 1.7: 23 | * Fixade att alternativ inte laddades 24 | * Fixade övergången till temafärger när visaren laddas 25 | * Uppdaterade DOM-destillerarresurser till de senaste versionerna 26 | 27 | 1.6: 28 | * La till ett feedbacksformulär vid avinstallation 29 | 30 | 1.5: 31 | * Fixade att vissa sidor visades ovanför visaren 32 | * Uppdaterade till den senaste DOM-destillerarversionen 33 | 34 | 1.4: 35 | * Fixade att sidans rullningslist överlappade rullningslisten för visaren 36 | 37 | 1.3: 38 | * Fixade att det destillerade innehållet aldrig visades 39 | 40 | 1.2: 41 | * Fixade att orginalsidan hoppar till toppen när den destillerade versionen döljs 42 | * Fixade att laddningsindikatorn inte visades 43 | * La till svensk översättning 44 | 45 | 1.1: 46 | * Prestandaförbättringar och buggfixar 47 | 48 | Tilläggsikoner härleddes från följande ikoner: 49 | * github.com/google/material-design-icons/blob/c6fc87c71001975022f979a2f6b031620e9c7ff1/action/svg/production/ic_chrome_reader_mode_48px.svg 50 | * github.com/google/material-design-icons/blob/c6fc87c71001975022f979a2f6b031620e9c7ff1/editor/svg/production/ic_bubble_chart_48px.svg 51 | 52 | Båda av ikonerna härledda från är licenserade under CC BY 4.0 (creativecommons.org/licenses/by/4.0) 53 | -------------------------------------------------------------------------------- /src/icons/extension.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/_locales/sv/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "DOM-destillerare läsningläge" 4 | }, 5 | "short_name": { 6 | "message": "DOM-destillerare" 7 | }, 8 | "description": { 9 | "message": "Destillera och visa bara det viktiga innehållet för att njuta av en mer fokuserad läsningsupplevelse" 10 | }, 11 | "browser_action_tooltip_inactive": { 12 | "message": "Destillera" 13 | }, 14 | "browser_action_tooltip_active": { 15 | "message": "Sluta visa destillerad version" 16 | }, 17 | "options_introduction": { 18 | "message": "Här kan du hitta två inställningar som ändrar utseendet av destillerade sidor." 19 | }, 20 | "options_theme_title": { 21 | "message": "Teman" 22 | }, 23 | "options_theme_description": { 24 | "message": "Teman ändrar färgen på bakgrunden och texten. Det finns tre olika färger att välja emellan: ljus, mörk and sepia" 25 | }, 26 | "options_theme_name_light": { 27 | "message": "Ljus" 28 | }, 29 | "options_theme_name_dark": { 30 | "message": "Mörk" 31 | }, 32 | "options_theme_name_sepia": { 33 | "message": "Sepia" 34 | }, 35 | "options_font_title": { 36 | "message": "Typsnitt" 37 | }, 38 | "options_font_description": { 39 | "message": "Du har också alternativet att ändra typsnitt. Tre olika typsnitt finns tillgängliga för dig att välja: utan seriff, seriff och fast bredd" 40 | }, 41 | "options_font_name_sans_serif": { 42 | "message": "Utan seriff" 43 | }, 44 | "options_font_name_serif": { 45 | "message": "Seriff" 46 | }, 47 | "options_font_name_monospace": { 48 | "message": "Fast bredd" 49 | }, 50 | "options_font_abbreviation_sans_serif": { 51 | "message": "Ut" 52 | }, 53 | "options_font_abbreviation_serif": { 54 | "message": "Se" 55 | }, 56 | "options_font_abbreviation_monospace": { 57 | "message": "Fa" 58 | }, 59 | "options_shortcut_title": { 60 | "message": "Kortkommando" 61 | }, 62 | "options_shortcut_description": { 63 | "message": "Om föredrar att aktivera tillägget med ditt tangentbord är du kanske intresserad av att sätta ett kortkommando. För att komma åt kortkommandon för alla tillägg, klicka på \"Kortkommandon\" längst ner i listan av tillägg." 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "DOM Distiller Reading Mode" 4 | }, 5 | "short_name": { 6 | "message": "DOM Distiller" 7 | }, 8 | "description": { 9 | "message": "Distill and display only the important content to enjoy a more focused reading experience" 10 | }, 11 | "browser_action_tooltip_inactive": { 12 | "message": "Distill" 13 | }, 14 | "browser_action_tooltip_active": { 15 | "message": "Stop showing distilled version" 16 | }, 17 | "options_introduction": { 18 | "message": "Here you'll find two options that change the appearance of distilled pages.", 19 | "description": "Acts as filler text so the options page works as a preview" 20 | }, 21 | "options_theme_title": { 22 | "message": "Themes" 23 | }, 24 | "options_theme_description": { 25 | "message": "Themes change the color of the background and the text. There are three different colors to choose from: light, dark and sepia" 26 | }, 27 | "options_theme_name_light": { 28 | "message": "Light" 29 | }, 30 | "options_theme_name_dark": { 31 | "message": "Dark" 32 | }, 33 | "options_theme_name_sepia": { 34 | "message": "Sepia" 35 | }, 36 | "options_font_title": { 37 | "message": "Fonts" 38 | }, 39 | "options_font_description": { 40 | "message": "You also have the option to change fonts. Three different fonts are available for your choosing: sans serif, serif and monospace" 41 | }, 42 | "options_font_name_sans_serif": { 43 | "message": "Sans serif" 44 | }, 45 | "options_font_name_serif": { 46 | "message": "Serif" 47 | }, 48 | "options_font_name_monospace": { 49 | "message": "Monospace" 50 | }, 51 | "options_font_abbreviation_sans_serif": { 52 | "message": "Sa" 53 | }, 54 | "options_font_abbreviation_serif": { 55 | "message": "Se" 56 | }, 57 | "options_font_abbreviation_monospace": { 58 | "message": "Mo" 59 | }, 60 | "options_shortcut_title": { 61 | "message": "Keyboard shortcut" 62 | }, 63 | "options_shortcut_description": { 64 | "message": "If you prefer to activate the extension using your keyboard you may be interested in setting a keyboard shortcut. To access shortcuts for all extensions, click \"Keyboard shortcuts\" at the bottom of the list of extensions." 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/viewer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-native-reassign, no-global-assign */ 2 | /* global addToPage, setTitle, showLoadingIndicator, useTheme, useFontFamily */ 3 | 4 | // Makes regular text have the regular text size 5 | document.body.style.fontSize = "1.33333333em"; 6 | 7 | // Workaround for https://bugs.chromium.org/p/chromium/issues/detail?id=677913 8 | document.body.setAttribute("dir", "auto"); 9 | 10 | document.body.style.transitionDuration = "0s"; 11 | // Has to be synchronous to prevent white flash 12 | document.body.className = `${localStorage["storage-sync-theme"]} ${localStorage["storage-sync-font"]}`; 13 | requestAnimationFrame(() => { 14 | document.body.style.transitionDuration = ""; 15 | }); 16 | 17 | // Emergency CSS 18 | let emergencyCSSStyle; 19 | { 20 | const style = document.createElement("style"); 21 | style.id = "emergency-css-style"; 22 | style.textContent = localStorage["storage-sync-emergency-css"]; 23 | document.head.appendChild(style); 24 | emergencyCSSStyle = style; 25 | } 26 | 27 | const baseElement = document.createElement("base"); 28 | baseElement.target = "_top"; 29 | document.head.appendChild(baseElement); 30 | 31 | const oldSetTitle = setTitle; 32 | setTitle = (...args) => { 33 | window.top.postMessage({action: "setTitle", title: args[0]}, "*"); 34 | oldSetTitle.apply(window, args); 35 | }; 36 | 37 | function isOptionsPage() { 38 | try { 39 | return top.location.href === chrome.runtime.getURL("options/options.html"); 40 | } catch(error) { 41 | return false; 42 | } 43 | } 44 | 45 | function handleResult(result) { 46 | const {"1": resultTitle, "2": {"1": resultHTML}} = result; 47 | addToPage(resultHTML); 48 | setTitle(resultTitle); 49 | // true stands for isLastPage, hides indicator 50 | showLoadingIndicator(true); 51 | if(isOptionsPage()) { 52 | const script = document.createElement("script"); 53 | script.src = chrome.runtime.getURL("options/options.js"); 54 | document.head.appendChild(script); 55 | } 56 | } 57 | 58 | chrome.runtime.sendMessage("distill-tab", result => { 59 | handleResult(result); 60 | }); 61 | 62 | const storageActions = { 63 | theme: useTheme, 64 | font: useFontFamily, 65 | "emergency-css": value => { 66 | emergencyCSSStyle.textContent = value; 67 | } 68 | }; 69 | 70 | chrome.storage.onChanged.addListener((changes, area) => { 71 | if(area === "sync") { 72 | Object.keys(changes) 73 | .forEach(key => storageActions[key](changes[key].newValue)); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onInstalled.addListener(({reason}) => { 2 | chrome.runtime.setUninstallURL("https://goo.gl/forms/r0WChsdUmHuEQ9PS2"); 3 | if(reason === "install") { 4 | chrome.storage.sync.get({ 5 | theme: "light", 6 | font: "sans-serif" 7 | }, items => { 8 | chrome.storage.sync.set(items); 9 | }); 10 | } 11 | 12 | // Remove localStorage keys that should not exist 13 | const badKeys = []; 14 | for (let i = 0; true; i++) { 15 | const key = localStorage.key(i); 16 | if(key === null) { 17 | break; 18 | } else if(key.startsWith("result-")) { 19 | badKeys.push(key); 20 | } 21 | } 22 | badKeys.forEach(key => localStorage.removeItem(key)); 23 | 24 | localStorage.removeItem("result-options"); 25 | 26 | // Save browser action icon paths to localStorage for faster access 27 | const manifest = chrome.runtime.getManifest(); 28 | const prefix = "browserActionIcon"; 29 | const inactiveIcon = manifest.browser_action.default_icon; 30 | localStorage[`${prefix}Inactive`] = JSON.stringify(inactiveIcon); 31 | const activeIcon = {}; 32 | Object.keys(inactiveIcon).forEach(size => { 33 | activeIcon[size] = inactiveIcon[size].replace("inactive", "active"); 34 | }); 35 | localStorage[`${prefix}Active`] = JSON.stringify(activeIcon); 36 | }); 37 | 38 | chrome.browserAction.onClicked.addListener(({id: tabID}) => { 39 | chrome.tabs.executeScript(tabID, { 40 | file: "content-scripts/isActive.js" 41 | }, ([active]) => { 42 | chrome.tabs.executeScript(tabID, { 43 | file: `content-scripts/${active ? "remove" : "add"}.js` 44 | }); 45 | chrome.browserAction.setIcon({ 46 | path: JSON.parse(localStorage[ 47 | active ? "browserActionIconInactive" : "browserActionIconActive" 48 | ]), 49 | tabId: tabID 50 | }); 51 | chrome.browserAction.setTitle({ 52 | title: chrome.i18n.getMessage( 53 | `browser_action_tooltip_${active ? "inactive" : "active"}` 54 | ), 55 | tabId: tabID 56 | }); 57 | }); 58 | }); 59 | 60 | chrome.storage.onChanged.addListener((changes, area) => { 61 | Object.keys(changes).forEach(key => { 62 | localStorage[`storage-${area}-${key}`] = changes[key].newValue; 63 | }); 64 | }); 65 | 66 | function isOptions(sender) { 67 | return !sender.tab; 68 | } 69 | 70 | const respondToOptions = new Set(); 71 | function tryOptionsResponse() { 72 | if("result-options" in localStorage) { 73 | const parsed = JSON.parse(localStorage["result-options"]); 74 | for(const respond of respondToOptions) { 75 | respond(parsed); 76 | } 77 | respondToOptions.clear(); 78 | } 79 | } 80 | chrome.runtime.onMessage.addListener((message, sender, respond) => { 81 | if(message === "distill-tab") { 82 | if(isOptions(sender)) { 83 | respondToOptions.add(respond); 84 | tryOptionsResponse(); 85 | return true; 86 | } 87 | chrome.tabs.executeScript(sender.tab.id, { 88 | file: "$$(CHROME_DD_CORE)/javascript/domdistiller.js" 89 | }, ([result]) => { 90 | respond(result); 91 | }); 92 | return true; 93 | } else if(message && message.type === "distill-result") { 94 | if(isOptions(sender)) { 95 | localStorage["result-options"] = JSON.stringify(message.result); 96 | tryOptionsResponse(); 97 | } 98 | } 99 | }); 100 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const del = require("del"); 3 | const Gulp = require("gulp" ); 4 | const changedInPlace = require("gulp-changed-in-place"); 5 | const rename = require("gulp-rename" ); 6 | const replace = require("gulp-replace" ); 7 | const zip = require("gulp-zip" ); 8 | const pStream = require("stream-to-promise" ); 9 | const streamToBuffer = require("vinyl-buffer" ); 10 | 11 | const optional = require ("optional" ); 12 | const svg2PNG = optional("gulp-svg2png"); 13 | 14 | const paths = { 15 | chromeDDCore: "src/external/chromium/src/components/dom_distiller/core" 16 | }; 17 | 18 | const internalSubstitutionRegex = /\$\$\((.+?)\)/g; 19 | const internalSubstitutions = { 20 | CHROME_DD_CORE: paths.chromeDDCore.replace(/^src\//, "") 21 | }; 22 | 23 | // `Gulp.src(...).pipe(changedInPlace({firstPass: true}))` 24 | // but doesn't buffer before change check 25 | function srcChanged(...args) { 26 | if(typeof args[args.length - 1] !== "object") { 27 | args.push({}); 28 | } 29 | args[args.length - 1].buffer = false; 30 | return Gulp.src(...args) 31 | .pipe(changedInPlace({ 32 | firstPass: true, 33 | howToDetermineDifference: "modification-time" 34 | })) 35 | .pipe(streamToBuffer()); 36 | } 37 | 38 | Gulp.task("simple internal resources", () => 39 | srcChanged("src/{!(external),!(external)/**/*}", {ignore: "src/icons/**/*.svg"}) 40 | .pipe(replace(internalSubstitutionRegex, (_, name) => { 41 | if(name in internalSubstitutions) { 42 | return internalSubstitutions[name]; 43 | } else { 44 | throw new Error("Unknown substitution: \"" + name + "\""); 45 | } 46 | })) 47 | .pipe(Gulp.dest("out")) 48 | ); 49 | 50 | Gulp.task("simple external resources", () => 51 | Gulp.src( 52 | "src/external/chromium/src/" + 53 | "{" + 54 | "LICENSE," + 55 | 56 | "components/dom_distiller/core/" + 57 | "{" + 58 | "css/distilledpage.css," + 59 | 60 | "javascript/dom_distiller_viewer.js" + 61 | "}" + 62 | "}", 63 | {base: "src"} 64 | ) 65 | .pipe(Gulp.dest("out")) 66 | ); 67 | 68 | Gulp.task("viewer HTML substitution", async () => { 69 | /* 70 | Placeholders (used, $n, description) 71 | | $1 | 72 | x | $2 | CSS (not in tag) 73 | x | $3 | Body `class` attribute 74 | | $4 | <noscript> title 75 | | $5 | <noscript> content 76 | x | $6 | SVG spinner 77 | | $7 | `data-original-url` attribute for close link 78 | | $8 | Close link content 79 | */ 80 | const spinner = pStream(Gulp.src( 81 | paths.chromeDDCore + "/images/dom_distiller_material_spinner.svg" 82 | )); 83 | const viewerHTMLSource = Gulp.src( 84 | paths.chromeDDCore + "/html/dom_distiller_viewer.html", {base: "src"} 85 | ); 86 | await pStream( 87 | viewerHTMLSource 88 | .pipe(replace( 89 | "$2", 90 | 91 | `<link rel="stylesheet" href="../css/distilledpage.css">` + "\n" + 92 | `<script src="../javascript/dom_distiller_viewer.js" defer></script>` + "\n" + 93 | `<script src="../../../../../../../viewer.js" defer></script>` 94 | )) 95 | .pipe(replace("$3", "light sans-serif")) 96 | .pipe(replace("$6", (await spinner)[0].contents.toString())) 97 | .pipe(Gulp.dest("out")) 98 | ) 99 | }); 100 | 101 | Gulp.task("distiller wrapper substitution", async () => { 102 | const builtJS = pStream(Gulp.src( 103 | "src/external/dom-distiller/out/domdistiller.js" 104 | )); 105 | // Split into multiple statements because `await builtJS` 106 | const wrapperSource = Gulp.src( 107 | paths.chromeDDCore + "/javascript/domdistiller.js", 108 | {base: "src"} 109 | ); 110 | await pStream( 111 | wrapperSource 112 | .pipe(replace( 113 | /<include src=".+?domdistiller.js"\/>/, 114 | (await builtJS)[0].contents.toString() 115 | )) 116 | .pipe(replace( 117 | "(function(options, stringify_output) {", 118 | "function applyDistiller(options = {}, stringify_output = false) {" 119 | )) 120 | .pipe(replace( 121 | "})($$OPTIONS, $$STRINGIFY)", 122 | 123 | "" + 124 | `} 125 | if(!document.currentScript) { 126 | applyDistiller(); 127 | } else { 128 | chrome.runtime.sendMessage({ 129 | type: "distill-result", 130 | result: applyDistiller() 131 | }); 132 | } 133 | ` 134 | + "" 135 | )) 136 | .pipe(Gulp.dest("out")) 137 | ); 138 | }); 139 | 140 | Gulp.task("external substitution", Gulp.parallel( 141 | "viewer HTML substitution", 142 | "distiller wrapper substitution" 143 | )); 144 | 145 | const svgPNGPathRegex = /(.+?\.svg)-(\d+)\.png/; 146 | Gulp.task("SVG convertion", async () => { 147 | const [{contents: manifest}] = await pStream(Gulp.src("src/manifest.json")); 148 | const svgs = new Map(); 149 | JSON.parse(manifest.toString(), (key, value) => { 150 | if(typeof value === "string") { 151 | const match = value.match(svgPNGPathRegex); 152 | if(!match) return value; 153 | const [, svgPath, size] = match; 154 | if(!svgs.has(svgPath)) svgs.set(svgPath, { 155 | sizes: new Set(), 156 | src: srcChanged("src/" + svgPath) 157 | }); 158 | svgs.get(svgPath).sizes.add(size); 159 | } 160 | return value; 161 | }); 162 | { 163 | // Generate active icon from inactive 164 | const [path, {sizes, src}] = 165 | [...svgs] 166 | .find(([k]) => k.endsWith("inactive.svg")); 167 | const activePath = path.replace("inactive.svg", "active.svg"); 168 | svgs.set(activePath, { 169 | sizes, 170 | // Reusing `src` here causes both to be replaced (!?) 171 | src: Gulp.src("src/" + path) 172 | .pipe(replace(`fill="#9c27b0"`, `fill="#ff4081"`)) 173 | }); 174 | } 175 | const promises = []; 176 | for(const [path, {sizes, src}] of svgs) { 177 | for(const size of sizes) { 178 | let start; 179 | if(svg2PNG) { 180 | start = src 181 | .pipe(svg2PNG({width: size, height: size})); 182 | } else { 183 | start = Gulp.src("transparent-pixel.png"); 184 | } 185 | promises.push(pStream( 186 | start 187 | .pipe(rename(`${path}-${size}.png`)) 188 | .pipe(Gulp.dest(`out`)) 189 | )); 190 | } 191 | } 192 | await Promise.all(promises); 193 | }); 194 | 195 | Gulp.task("clean", () => { 196 | return del(["out"]); 197 | }); 198 | 199 | Gulp.task("build", Gulp.series( 200 | "clean", 201 | Gulp.parallel( 202 | "simple internal resources", 203 | "simple external resources", 204 | "external substitution", 205 | "SVG convertion" 206 | ) 207 | )); 208 | 209 | Gulp.task("watch", async () => { 210 | console.info("Watching SVG files in src/icons"); 211 | Gulp.watch( 212 | ["src/icons/**/*.svg"], 213 | Gulp.parallel("SVG convertion") 214 | ); 215 | console.info("Watching all non-SVG files in src excluding src/external"); 216 | Gulp.watch( 217 | // Cannot use same glob as in Gulp.src 218 | "src/**/*", {ignored: ["src/icons/**/*.svg", "src/external/**/*"]}, 219 | Gulp.parallel("simple internal resources") 220 | ); 221 | await new Promise(resolve => {}); 222 | }); 223 | 224 | Gulp.task("default", Gulp.series("build", "watch")); 225 | 226 | Gulp.task("zip", async () => { 227 | const manifest = pStream(Gulp.src("out/manifest.json")); 228 | const src = Gulp.src("out/**/*"); 229 | const {version} = JSON.parse((await manifest)[0].contents.toString()); 230 | return pStream( 231 | src 232 | .pipe(zip(`dom-distiller-reading-mode-${version}.zip`)) 233 | .pipe(Gulp.dest(".")) 234 | ); 235 | }); 236 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "webextensions": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 8 9 | }, 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | "tab" 14 | ], 15 | "one-var": [ 16 | "error", 17 | "never" 18 | ], 19 | "padded-blocks": [ 20 | "error", 21 | { 22 | "blocks": "never" 23 | } 24 | ], 25 | "space-before-function-paren": [ 26 | "error", 27 | "never" 28 | ], 29 | "quote-props": [ 30 | "error", 31 | "consistent-as-needed" 32 | ], 33 | "dot-location": [ 34 | "error", 35 | "property" 36 | ], 37 | "lines-around-directive": [ 38 | "error", 39 | { 40 | "before": "never", 41 | "after": "always" 42 | } 43 | ], 44 | "keyword-spacing": [ 45 | "error", 46 | { 47 | "before": true, 48 | "after": true, 49 | "overrides": { 50 | "if": { 51 | "after": false 52 | }, 53 | "switch": { 54 | "after": false 55 | }, 56 | "while": { 57 | "after": false 58 | }, 59 | "catch": { 60 | "after": false 61 | } 62 | } 63 | } 64 | ], 65 | "arrow-parens": [ 66 | "error", 67 | "as-needed" 68 | ], 69 | "max-len": [ 70 | "error", 71 | { 72 | "ignoreStrings": true, 73 | "ignoreTemplateLiterals": true 74 | } 75 | ], 76 | "no-magic-numbers": [ 77 | "error", 78 | { 79 | "ignore": [ 80 | -1, 81 | 0, 82 | 1 83 | ] 84 | } 85 | ], 86 | "no-constant-condition": [ 87 | "error", 88 | { 89 | "checkLoops": false 90 | } 91 | ], 92 | "multiline-ternary": [ 93 | "error", 94 | "never" 95 | ], 96 | "quotes": [ 97 | "error", 98 | "double", 99 | { 100 | "avoidEscape": true, 101 | "allowTemplateLiterals": true 102 | } 103 | ], 104 | "no-implicit-coercion": [ 105 | "error", 106 | { 107 | "allow": [ 108 | "!!" 109 | ] 110 | } 111 | ], 112 | "no-mixed-spaces-and-tabs": [ 113 | "error", 114 | "smart-tabs" 115 | ], 116 | "no-console": [ 117 | "warn" 118 | ], 119 | "no-debugger": [ 120 | "warn" 121 | ], 122 | "no-cond-assign": 2, 123 | "no-control-regex": 2, 124 | "no-dupe-args": 2, 125 | "no-dupe-keys": 2, 126 | "no-duplicate-case": 2, 127 | "no-empty-character-class": 2, 128 | "no-empty": 2, 129 | "no-ex-assign": 2, 130 | "no-extra-boolean-cast": 2, 131 | "no-extra-parens": 2, 132 | "no-extra-semi": 2, 133 | "no-func-assign": 2, 134 | "no-inner-declarations": 2, 135 | "no-invalid-regexp": 2, 136 | "no-irregular-whitespace": 2, 137 | "no-obj-calls": 2, 138 | "no-prototype-builtins": 2, 139 | "no-regex-spaces": 2, 140 | "no-sparse-arrays": 2, 141 | "no-template-curly-in-string": 2, 142 | "no-unexpected-multiline": 2, 143 | "no-unreachable": 2, 144 | "no-unsafe-finally": 2, 145 | "no-unsafe-negation": 2, 146 | "use-isnan": 2, 147 | "valid-jsdoc": 2, 148 | "valid-typeof": 2, 149 | "accessor-pairs": 2, 150 | "array-callback-return": 2, 151 | "block-scoped-var": 2, 152 | "class-methods-use-this": 2, 153 | "complexity": 2, 154 | "consistent-return": 0, 155 | "curly": 2, 156 | "default-case": 0, 157 | "dot-notation": 2, 158 | "eqeqeq": 2, 159 | "guard-for-in": 2, 160 | "no-alert": 2, 161 | "no-caller": 2, 162 | "no-case-declarations": 2, 163 | "no-div-regex": 2, 164 | "no-else-return": 2, 165 | "no-empty-function": 2, 166 | "no-empty-pattern": 2, 167 | "no-eq-null": 2, 168 | "no-eval": 2, 169 | "no-extend-native": 2, 170 | "no-extra-bind": 2, 171 | "no-extra-label": 2, 172 | "no-fallthrough": 2, 173 | "no-floating-decimal": 2, 174 | "no-global-assign": 2, 175 | "no-implicit-globals": 0, 176 | "no-implied-eval": 2, 177 | "no-invalid-this": 0, 178 | "no-iterator": 2, 179 | "no-labels": 2, 180 | "no-lone-blocks": 2, 181 | "no-loop-func": 2, 182 | "no-multi-spaces": 2, 183 | "no-multi-str": 2, 184 | "no-new-func": 2, 185 | "no-new-wrappers": 2, 186 | "no-new": 0, 187 | "no-octal-escape": 2, 188 | "no-octal": 2, 189 | "no-param-reassign": 0, 190 | "no-proto": 2, 191 | "no-redeclare": 2, 192 | "no-restricted-properties": 2, 193 | "no-return-assign": 0, 194 | "no-script-url": 2, 195 | "no-self-assign": 2, 196 | "no-self-compare": 2, 197 | "no-sequences": 2, 198 | "no-throw-literal": 2, 199 | "no-unmodified-loop-condition": 2, 200 | "no-unused-expressions": 2, 201 | "no-unused-labels": 2, 202 | "no-useless-call": 2, 203 | "no-useless-concat": 0, 204 | "no-useless-escape": 2, 205 | "no-useless-return": 2, 206 | "no-void": 2, 207 | "no-warning-comments": 2, 208 | "no-with": 2, 209 | "radix": 2, 210 | "vars-on-top": 2, 211 | "wrap-iife": 2, 212 | "yoda": 2, 213 | "strict": 0, 214 | "init-declarations": 0, 215 | "no-catch-shadow": 2, 216 | "no-delete-var": 2, 217 | "no-label-var": 2, 218 | "no-restricted-globals": 2, 219 | "no-shadow-restricted-names": 2, 220 | "no-shadow": 0, 221 | "no-undef-init": 2, 222 | "no-undef": 2, 223 | "no-undefined": 0, 224 | "no-unused-vars": 2, 225 | "no-use-before-define": 2, 226 | "callback-return": 2, 227 | "global-require": 2, 228 | "handle-callback-err": 2, 229 | "no-mixed-requires": 2, 230 | "no-new-require": 2, 231 | "no-path-concat": 2, 232 | "no-process-env": 2, 233 | "no-process-exit": 2, 234 | "no-restricted-modules": 2, 235 | "no-sync": 2, 236 | "array-bracket-spacing": 2, 237 | "block-spacing": 2, 238 | "brace-style": 2, 239 | "camelcase": 2, 240 | "comma-dangle": 2, 241 | "comma-spacing": 2, 242 | "comma-style": 2, 243 | "computed-property-spacing": 2, 244 | "consistent-this": 2, 245 | "eol-last": 2, 246 | "func-call-spacing": 2, 247 | "func-name-matching": 2, 248 | "func-names": 0, 249 | "func-style": 0, 250 | "id-blacklist": 2, 251 | "id-length": 0, 252 | "id-match": 2, 253 | "jsx-quotes": 2, 254 | "key-spacing": 2, 255 | "line-comment-position": 0, 256 | "linebreak-style": 2, 257 | "lines-around-comment": 0, 258 | "max-depth": 2, 259 | "max-lines": 2, 260 | "max-nested-callbacks": 2, 261 | "max-params": 2, 262 | "max-statements-per-line": 2, 263 | "max-statements": 0, 264 | "new-cap": 2, 265 | "new-parens": 2, 266 | "newline-after-var": 0, 267 | "newline-before-return": 0, 268 | "newline-per-chained-call": 2, 269 | "no-array-constructor": 2, 270 | "no-bitwise": 2, 271 | "no-continue": 2, 272 | "no-inline-comments": 0, 273 | "no-lonely-if": 2, 274 | "no-mixed-operators": 2, 275 | "no-multiple-empty-lines": 2, 276 | "no-negated-condition": 2, 277 | "no-nested-ternary": 2, 278 | "no-new-object": 2, 279 | "no-plusplus": 0, 280 | "no-restricted-syntax": 2, 281 | "no-tabs": 0, 282 | "no-ternary": 0, 283 | "no-trailing-spaces": 2, 284 | "no-underscore-dangle": 2, 285 | "no-unneeded-ternary": 2, 286 | "no-whitespace-before-property": 2, 287 | "object-curly-newline": 0, 288 | "object-curly-spacing": 2, 289 | "object-property-newline": 0, 290 | "one-var-declaration-per-line": 2, 291 | "operator-assignment": 2, 292 | "operator-linebreak": 2, 293 | "require-jsdoc": 0, 294 | "semi-spacing": 2, 295 | "semi": 2, 296 | "sort-keys": 0, 297 | "sort-vars": 2, 298 | "space-before-blocks": 2, 299 | "space-in-parens": 2, 300 | "space-infix-ops": 2, 301 | "space-unary-ops": 2, 302 | "spaced-comment": 2, 303 | "unicode-bom": 2, 304 | "wrap-regex": 2, 305 | "arrow-body-style": 0, 306 | "arrow-spacing": 2, 307 | "constructor-super": 2, 308 | "generator-star-spacing": 2, 309 | "no-class-assign": 2, 310 | "no-confusing-arrow": 2, 311 | "no-const-assign": 2, 312 | "no-dupe-class-members": 2, 313 | "no-duplicate-imports": 2, 314 | "no-new-symbol": 2, 315 | "no-restricted-imports": 2, 316 | "no-this-before-super": 2, 317 | "no-useless-computed-key": 2, 318 | "no-useless-constructor": 2, 319 | "no-useless-rename": 2, 320 | "no-var": 2, 321 | "object-shorthand": 2, 322 | "prefer-arrow-callback": 2, 323 | "prefer-const": 2, 324 | "prefer-numeric-literals": 2, 325 | "prefer-rest-params": 2, 326 | "prefer-spread": 2, 327 | "prefer-template": 2, 328 | "require-yield": 2, 329 | "rest-spread-spacing": 2, 330 | "sort-imports": 2, 331 | "symbol-description": 2, 332 | "template-curly-spacing": 2, 333 | "yield-star-spacing": 2 334 | } 335 | } 336 | --------------------------------------------------------------------------------