├── .gitignore ├── LICENSE ├── README.md ├── extension └── chrome │ ├── content_script.js │ └── manifest.json ├── gulpfile.js ├── karma.conf.js ├── mocha.conf.js ├── nope.js ├── package.json └── tests ├── document-set-lifecycle-mode.js ├── document-write.js └── layout-triggers.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dimitri Glazkov 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nope-js 2 | A library that's kind enough to tell you "No" when you need it. 3 | 4 | ## Getting Started 5 | 6 | `npm install` 7 | 8 | `npm install --global gulp` (if you don't already have [gulp](http://gulpjs.com/) installed) 9 | 10 | `gulp test` to start [karma](http://karma-runner.github.io/) in continuous integration mode. 11 | 12 | Write tests! :smiley: 13 | 14 | ## Requirements 15 | 16 | Needs [Chrome 43+](http://updates.html5rocks.com/2015/04/DOM-attributes-now-on-the-prototype) (this is why currently karma is configured to ask for Chrome Canary). If you want to make it for an earlier version of Chrome or a rendering engine that doesn't keep [DOM properties on prototypes]((http://updates.html5rocks.com/2015/04/DOM-attributes-now-on-the-prototype) ), I am unlikely to take your patches. My apologies -- trying to keep this simple. However, would be happy have help to fix any cross-browser problems. 17 | -------------------------------------------------------------------------------- /extension/chrome/content_script.js: -------------------------------------------------------------------------------- 1 | var nope = function() { 2 | 3 | var WRITE_LIFECYCLE_MODE = "write"; 4 | var knownLifecycleModes = [ "read", WRITE_LIFECYCLE_MODE ]; 5 | var lifecycleMode = WRITE_LIFECYCLE_MODE; 6 | 7 | var terribleIdeas = { 8 | 'HTMLDocument': { 9 | 'method': [ 10 | 'write', 11 | 'writeln', 12 | 'open', 13 | 'close' 14 | ] 15 | } 16 | } 17 | 18 | var layoutTriggers = { 19 | 'Document': { 20 | 'getter': [ 21 | 'scrollingElement', 22 | ], 23 | 'method': [ 24 | 'execCommand', 25 | ] 26 | }, 27 | 'HTMLElement': { 28 | 'getter': [ 29 | 'offsetLeft', 30 | 'offsetTop', 31 | 'offsetWidth', 32 | 'offsetHeight', 33 | 'offsetParent', 34 | 'innerText', 35 | 'outerText', 36 | ] 37 | }, 38 | 'Element': { 39 | 'method': [ 40 | 'scrollIntoView', 41 | 'scrollBy', // experimental 42 | 'scrollTo', // experimental 43 | 'getClientRects', 44 | 'getBoundingClientRect', 45 | 'computedRole', // experimental 46 | 'computedName', // experimental 47 | 'focus', 48 | ], 49 | 'getter': [ 50 | 'clientLeft', 51 | 'clientWidth', 52 | 'clientHeight', 53 | 'scrollLeft', 54 | 'scrollTop', 55 | 'scrollWidth', 56 | 'scrollHeight', 57 | ], 58 | 'setter': [ 59 | 'scrollLeft', 60 | 'scrollTop', 61 | ], 62 | }, 63 | 'Range': { 64 | 'method': [ 65 | 'getClientRects', 66 | 'getBoundingClientRect', 67 | ], 68 | }, 69 | 'UIEvent': { 70 | 'getter': [ 71 | 'layerX', 72 | 'layerY', 73 | ], 74 | }, 75 | 'MouseEvent': { 76 | 'getter': [ 77 | 'offsetX', 78 | 'offsetY', 79 | ], 80 | }, 81 | 'HTMLButtonElement': { 82 | 'method': [ 83 | 'reportValidity', 84 | ] 85 | }, 86 | 'HTMLDialogElement': { 87 | 'method': [ 88 | 'showModal', 89 | ] 90 | }, 91 | 'HTMLFieldSetElement': { 92 | 'method': [ 93 | 'reportValidity', 94 | ] 95 | }, 96 | 'HTMLImageElement': { 97 | 'getter': [ 98 | 'width', 99 | 'height', 100 | 'x', 101 | 'y', 102 | ] 103 | }, 104 | 'HTMLInputElement': { 105 | 'method': [ 106 | 'reportValidity', 107 | ] 108 | }, 109 | 'HTMLButtonElement': { 110 | 'method': [ 111 | 'reportValidity', 112 | ] 113 | }, 114 | 'HTMLKeygenElement': { 115 | 'method': [ 116 | 'reportValidity', 117 | ] 118 | }, 119 | 'CSSStyleDeclaration': { 120 | 'method': [ 121 | 'getPropertyValue', 122 | ] 123 | }, 124 | 'Window': { 125 | 'method': [ 126 | 'scrollBy', 127 | 'scrollTo', 128 | ] 129 | }, 130 | 'SVGSVGElement': { 131 | 'setter': [ 132 | 'currentScale', 133 | ] 134 | }, 135 | }; 136 | 137 | processSpec(layoutTriggers); 138 | processSpec(terribleIdeas); 139 | 140 | var buggyWindowAccessors = [ 141 | 'innerHeight', 142 | 'innerWidth', 143 | 'scrollX', 144 | 'scrollY', 145 | ]; 146 | 147 | buggyWindowAccessors.forEach(function(accessor) { 148 | overrideGetterSetter(window, accessor, nope, nope); 149 | }); 150 | 151 | function nope() { 152 | throw new Error('nope'); 153 | } 154 | 155 | function synthesizeNopeWhenWriting(func) { 156 | return function() { 157 | if (lifecycleMode != WRITE_LIFECYCLE_MODE) { 158 | return func.apply(this, arguments); 159 | } 160 | throw new Error('nope'); 161 | } 162 | } 163 | 164 | define(HTMLDocument.prototype, 'setLifecycleMode', function(mode) { 165 | if (knownLifecycleModes.indexOf(mode) == -1) { 166 | throw new Error(`Unknown lifecycle mode. The known modes are: ${knownLifecycleModes}.`); 167 | return; 168 | } 169 | lifecycleMode = mode; 170 | }); 171 | 172 | function processSpec(spec) { 173 | Object.keys(spec).forEach(function(objectName) { 174 | var objectSpecs = spec[objectName]; 175 | objectSpecs.getter && objectSpecs.getter.forEach(function(getterName) { 176 | redefineGetter(window[objectName].prototype, getterName, synthesizeNopeWhenWriting); 177 | }); 178 | objectSpecs.setter && objectSpecs.setter.forEach(function(setterName) { 179 | redefineSetter(window[objectName].prototype, setterName, synthesizeNopeWhenWriting); 180 | }); 181 | objectSpecs.method && objectSpecs.method.forEach(function(methodName) { 182 | define(window[objectName].prototype, methodName, nope); 183 | }); 184 | }); 185 | } 186 | 187 | function redefineGetter(obj, key, getterSynthesizer) { 188 | var descriptor = Object.getOwnPropertyDescriptor(obj, key); 189 | if (!descriptor || !descriptor.get) 190 | throw new Error(`Unable to redefine getter ${key} on ${obj.constructor}.`); 191 | 192 | descriptor.get = getterSynthesizer(descriptor.get); 193 | 194 | Object.defineProperty(obj, key, descriptor); 195 | } 196 | 197 | function redefineSetter(obj, key, setterSynthesizer) { 198 | var descriptor = Object.getOwnPropertyDescriptor(obj, key); 199 | if (!descriptor || !descriptor.set) 200 | throw new Error(`Unable to redefine setter ${key} on ${obj.constructor}.`); 201 | 202 | descriptor.set = setterSynthesizer(descriptor.set); 203 | 204 | Object.defineProperty(obj, key, descriptor); 205 | 206 | } 207 | 208 | function overrideGetterSetter(obj, key, getter, setter) { 209 | Object.defineProperty(obj, key, { 210 | get: getter, 211 | set: setter, 212 | configurable: true, 213 | enumerable: true 214 | }); 215 | } 216 | 217 | function define(obj, key, func) { 218 | Object.defineProperty(obj, key, { 219 | value: func, 220 | writable: true, 221 | configurable: true, 222 | enumerable: true 223 | }); 224 | } 225 | 226 | } 227 | 228 | var script = document.createElement('script'); 229 | script.textContent = `~${nope}(); document.currentScript.remove();`; 230 | document.documentElement.appendChild(script); 231 | 232 | 233 | -------------------------------------------------------------------------------- /extension/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "Nope extension", 5 | "description": "Dynamically invokes nope on a page", 6 | "version": "1.0", 7 | 8 | "content_scripts": [{ 9 | "matches": [ "http://*/*", "https://*/*" ], 10 | "js": [ "content_script.js" ], 11 | "run_at": "document_start" 12 | }] 13 | } 14 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var karma = require('karma').server; 3 | 4 | gulp.task('test', function (cb) { 5 | karma.start({ 6 | configFile: __dirname + '/karma.conf.js', 7 | }, cb); 8 | }); 9 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Apr 01 2015 09:28:12 GMT-0700 (PDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | './node_modules/chai/chai.js', 19 | 'nope.js', 20 | 'mocha.conf.js', 21 | 'tests/*.js' 22 | ], 23 | 24 | 25 | // list of files to exclude 26 | exclude: [ 27 | ], 28 | 29 | 30 | // preprocess matching files before serving them to the browser 31 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 32 | preprocessors: { 33 | }, 34 | 35 | 36 | // test results reporter to use 37 | // possible values: 'dots', 'progress' 38 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 39 | reporters: ['progress'], 40 | 41 | 42 | // web server port 43 | port: 9876, 44 | 45 | 46 | // enable / disable colors in the output (reporters and logs) 47 | colors: true, 48 | 49 | 50 | // level of logging 51 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 52 | logLevel: config.LOG_INFO, 53 | 54 | 55 | // enable / disable watching file and executing tests whenever any file changes 56 | autoWatch: true, 57 | 58 | 59 | // start these browsers 60 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 61 | browsers: ['ChromeCanary'], 62 | 63 | 64 | // Continuous Integration mode 65 | // if true, Karma captures browsers, runs the tests and exits 66 | singleRun: false 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /mocha.conf.js: -------------------------------------------------------------------------------- 1 | mocha.setup({ 2 | ui: 'bdd', 3 | ignoreLeaks: true 4 | }); 5 | 6 | var assert = chai.assert; 7 | -------------------------------------------------------------------------------- /nope.js: -------------------------------------------------------------------------------- 1 | ~function() { 2 | 3 | var WRITE_LIFECYCLE_MODE = "write"; 4 | var knownLifecycleModes = [ "read", WRITE_LIFECYCLE_MODE ]; 5 | var lifecycleMode = WRITE_LIFECYCLE_MODE; 6 | 7 | define(HTMLDocument.prototype, 'write', nope); 8 | define(HTMLDocument.prototype, 'writeln', nope); 9 | define(HTMLDocument.prototype, 'open', nope); 10 | define(HTMLDocument.prototype, 'close', nope); 11 | 12 | var layoutTriggers = { 13 | 'HTMLDocument': { 14 | 'getter': [ 15 | 'scrollingElement', 16 | ], 17 | 'method': [ 18 | 'execCommand', 19 | ] 20 | }, 21 | 'Element': { 22 | 'method': [ 23 | 'scrollIntoView', 24 | 'scrollBy', // experimental 25 | 'scrollTo', // experimental 26 | 'getClientRects', 27 | 'getBoundingClientRect', 28 | 'computedRole', // experimental 29 | 'computedName', // experimental 30 | 'focus', 31 | ], 32 | 'getter': [ 33 | 'offsetLeft', 34 | 'offsetTop', 35 | 'offsetWidth', 36 | 'offsetHeight', 37 | 'offsetParent', 38 | 'clientLeft', 39 | 'clientWidth', 40 | 'clientHeight', 41 | 'scrollLeft', 42 | 'scrollTop', 43 | 'scrollWidth', 44 | 'scrollHeight', 45 | 'innerText', 46 | 'outerText', 47 | ], 48 | 'setter': [ 49 | 'scrollLeft', 50 | 'scrollTop', 51 | ], 52 | }, 53 | 'Range': { 54 | 'method': [ 55 | 'getClientRects', 56 | 'getBoundingClientRect', 57 | ], 58 | }, 59 | 'MouseEvent': { 60 | 'getter': [ 61 | 'layerX', 62 | 'layerY', 63 | 'offsetX', 64 | 'offsetY', 65 | ], 66 | }, 67 | 'HTMLButtonElement': { 68 | 'method': [ 69 | 'reportValidity', 70 | ] 71 | }, 72 | 'HTMLDialogElement': { 73 | 'method': [ 74 | 'showModal', 75 | ] 76 | }, 77 | 'HTMLFieldSetElement': { 78 | 'method': [ 79 | 'reportValidity', 80 | ] 81 | }, 82 | 'HTMLImageElement': { 83 | 'getter': [ 84 | 'width', 85 | 'height', 86 | 'x', 87 | 'y', 88 | ] 89 | }, 90 | 'HTMLInputElement': { 91 | 'method': [ 92 | 'reportValidity', 93 | ] 94 | }, 95 | 'HTMLButtonElement': { 96 | 'method': [ 97 | 'reportValidity', 98 | ] 99 | }, 100 | 'HTMLKeygenElement': { 101 | 'method': [ 102 | 'reportValidity', 103 | ] 104 | }, 105 | 'CSSStyleDeclaration': { 106 | 'method': [ 107 | 'getPropertyValue', 108 | ] 109 | }, 110 | 'Window': { 111 | 'method': [ 112 | 'scrollBy', 113 | 'scrollTo', 114 | ] 115 | }, 116 | 'SVGSVGElement': { 117 | 'setter': [ 118 | 'currentScale', 119 | ] 120 | }, 121 | '@window': { // should these stay on instance? 122 | 'getter': [ 123 | 'innerHeight', 124 | 'innerWidth', 125 | 'scrollX', 126 | 'scrollY', 127 | ] 128 | } 129 | } 130 | 131 | redefineGetter(HTMLElement.prototype, 'offsetLeft', synthesizeNopeWhenWriting); 132 | 133 | function nope() { 134 | throw new Error('nope'); 135 | } 136 | 137 | function synthesizeNopeWhenWriting(func) { 138 | return function() { 139 | if (lifecycleMode != WRITE_LIFECYCLE_MODE) { 140 | return func.apply(this, arguments); 141 | } 142 | throw new Error('nope'); 143 | } 144 | } 145 | 146 | define(HTMLDocument.prototype, 'setLifecycleMode', function(mode) { 147 | if (knownLifecycleModes.indexOf(mode) == -1) { 148 | throw new Error(`Unknown lifecycle mode. The known modes are: ${knownLifecycleModes}.`); 149 | return; 150 | } 151 | lifecycleMode = mode; 152 | }); 153 | 154 | function redefineGetter(prot, key, getterSynthesizer) { 155 | var descriptor = Object.getOwnPropertyDescriptor(prot, key); 156 | if (!descriptor || !descriptor.get) 157 | throw new Error(`Unable to redefine getter ${key} on prototype ${prot}.`); 158 | 159 | descriptor.get = getterSynthesizer(descriptor.get); 160 | 161 | Object.defineProperty(prot, key, descriptor); 162 | } 163 | 164 | function define(prot, key, func) { 165 | Object.defineProperty(prot, key, { 166 | value: func, 167 | writable: true, 168 | configurable: true, 169 | enumerable: true 170 | }); 171 | } 172 | 173 | }(); 174 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nope-js", 3 | "version": "0.0.0", 4 | "description": "A library that's kind enough to tell you \"No\" when you need it.", 5 | "main": " ", 6 | "devDependencies": { 7 | "chai": "^2.2.0", 8 | "gulp": "^3.8.11", 9 | "karma": "^0.12.31", 10 | "karma-chrome-launcher": "^0.1.7", 11 | "karma-mocha": "^0.1.10", 12 | "mocha": "^2.2.1" 13 | }, 14 | "scripts": { 15 | "test": " " 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/dglazkov/nope-js.git" 20 | }, 21 | "author": "", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/dglazkov/nope-js/issues" 25 | }, 26 | "homepage": "https://github.com/dglazkov/nope-js" 27 | } 28 | -------------------------------------------------------------------------------- /tests/document-set-lifecycle-mode.js: -------------------------------------------------------------------------------- 1 | describe('HTMLDocument.prototype.setLifecycleMode', function() { 2 | it('should not throw when "read" mode is specified', function() { 3 | assert.doesNotThrow(function() { 4 | document.setLifecycleMode('read'); 5 | }); 6 | }); 7 | 8 | it('should not throw when "write" mode is specified', function() { 9 | assert.doesNotThrow(function() { 10 | document.setLifecycleMode('write'); 11 | }); 12 | }); 13 | 14 | it('should throw when unknown mode is specified', function() { 15 | assert.throws(function() { 16 | document.setLifecycleMode('pancakes'); 17 | }); 18 | }); 19 | 20 | }); -------------------------------------------------------------------------------- /tests/document-write.js: -------------------------------------------------------------------------------- 1 | describe('HTMLDocument\'s dynamic markup insertion', function() { 2 | it('should throw for write', function() { 3 | assert.throws(function() { 4 | document.write('test'); 5 | }); 6 | }); 7 | 8 | it('should throw for writeln', function() { 9 | assert.throws(function() { 10 | document.writeln('test'); 11 | }); 12 | }); 13 | 14 | it('should throw for open', function() { 15 | assert.throws(function() { 16 | document.open(); 17 | }); 18 | }); 19 | 20 | it('should throw for close', function() { 21 | assert.throws(function() { 22 | document.close(); 23 | }); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /tests/layout-triggers.js: -------------------------------------------------------------------------------- 1 | describe('offsetLeft', function() { 2 | it('should throw when document is in "write" mode', function() { 3 | var div = document.createElement('div'); 4 | document.body.appendChild(div); 5 | document.setLifecycleMode('write'); 6 | assert.throws(function() { 7 | var offsetLeft = div.offsetLeft; 8 | }); 9 | document.body.removeChild(div); 10 | }); 11 | 12 | it('should not throw when document is in "read" mode', function() { 13 | var div = document.createElement('div'); 14 | div.style.position = 'absolute'; 15 | div.style.left = '10px'; 16 | document.body.appendChild(div); 17 | document.setLifecycleMode('read'); 18 | assert.doesNotThrow(function() { 19 | var offsetLeft = div.offsetLeft; 20 | assert.equal(offsetLeft, 10); 21 | }); 22 | document.body.removeChild(div); 23 | }); 24 | }); --------------------------------------------------------------------------------