├── icon.png ├── promotional.png ├── background.js ├── options.html ├── manifest.json ├── inject.js └── deba.js /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloopletech/copy-content/master/icon.png -------------------------------------------------------------------------------- /promotional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bloopletech/copy-content/master/promotional.png -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | chrome.browserAction.onClicked.addListener(function(tab) { 2 | chrome.tabs.executeScript(tab.id, { file: 'deba.js' }, function() { 3 | chrome.tabs.executeScript(tab.id, { file: 'inject.js' }); 4 | }); 5 | }); -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | copy-content Options 5 | 8 | 9 | 10 | 11 |

How to use copy-content

12 |

Simply click the extract button in the broswer toolbar. 13 | copy-content will automatically select the relevant text and copy it to the keyboard.

14 |

Attributions

15 |

Extension icon is "Copy Text" by Chris Alpaerts from the Noun Project.

16 | 17 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copy-content", 3 | "version": "1.0.0", 4 | "manifest_version": 2, 5 | "description": "With a single click, automatically selects relevant text and copies it to the clipboard.", 6 | "options_ui": { 7 | "page": "options.html", 8 | "chrome_style": true 9 | }, 10 | "icons": { 11 | "128": "icon.png" 12 | }, 13 | "background": { 14 | "scripts": [ 15 | "background.js" 16 | ], 17 | "persistent": true 18 | }, 19 | "browser_action": { 20 | "default_title": "Extract content and copy it to the clipboard." 21 | }, 22 | "permissions": [ 23 | "https://*/*", 24 | "http://*/*", 25 | "tabs" 26 | ] 27 | } -------------------------------------------------------------------------------- /inject.js: -------------------------------------------------------------------------------- 1 | console.log("injected"); 2 | function compileMatchSelectors() { 3 | var selectors = new Map(); 4 | selectors.set("body.deviantart", [".gr > .metadata > h2", ".gr > .metadata > ul > li.author", ".gr-body > .gr > .grf-indent"]); 5 | 6 | 7 | /* 8 | selectors.push("textarea"); //Form textearas 9 | selectors.push("div.alt2"); //Contents of spoiler sections on some vBulletin forum posts 10 | selectors.push("div.postcontent"); //vBulletin forum posts 11 | selectors.push("p.message"); //kusaba-based imageboard posts 12 | selectors.push("div.entry-content"); //blogspot.com blog posts 13 | selectors.push("div.boxbody"); //? 14 | selectors.push("div.entry-inner"); //Some wordpress blog posts 15 | selectors.push("div#selectable"); //pastebin.com content 16 | selectors.push("div.usertext-body"); //reddit.com posts 17 | selectors.push("blockquote.postMessage"); //futaba-based imageboard posts 18 | selectors.push("div.grf-indent"); //Deviant Art text posts 19 | selectors.push("div.story-contents"); //Certain author site 20 | 21 | return selectors.join(", ");*/ 22 | return selectors; 23 | } 24 | 25 | var matchSelectors = compileMatchSelectors(); 26 | var fallbackMatchSelector = "div, pre, blockquote"; 27 | 28 | function findElement(element, selector) { 29 | while(element && element != document) { 30 | if(element.matches(selector)) return element; 31 | element = element.parentNode; 32 | } 33 | 34 | return null; 35 | } 36 | 37 | var alertStyles = ` 38 | box-sixing: border-box; 39 | position: fixed; 40 | top: 20px; 41 | right: 20px; 42 | z-index: 10000001; 43 | padding: 15px; 44 | border-width: 1px; 45 | border-style: solid; 46 | border-radius: 4px; 47 | font-weight: normal; 48 | font-size: 14px; 49 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 50 | box-shadow: 4px 4px 6px 0px rgba(0,0,0,0.4); 51 | `; 52 | 53 | var successAlertStyles = ` 54 | border-color: #d6e9c6; 55 | color: #3c763d; 56 | background-color: #dff0d8; 57 | `; 58 | 59 | var errorAlertStyles = ` 60 | border-color: #ebccd1; 61 | color: #a94442; 62 | background-color: #f2dede; 63 | `; 64 | 65 | var textareaStyles = ` 66 | position: absolute; 67 | left: -9999px; 68 | top: -9999px; 69 | width: 100px; 70 | height: 100px; 71 | `; 72 | 73 | var successAlertElement = `
Selection copied ✓
`; 74 | var errorAlertElement = `
Could not find selection to copy
`; 75 | 76 | function alertUser(content) { 77 | var alert = document.createElement("div"); 78 | alert.innerHTML = content; 79 | document.body.appendChild(alert); 80 | 81 | setTimeout(function() { 82 | alert.remove() 83 | }, 4000); 84 | } 85 | 86 | function findSelectors() { 87 | for(var [matchSelector, nodeSelectors] of matchSelectors) { 88 | if(document.querySelector(matchSelector)) return nodeSelectors; 89 | } 90 | } 91 | 92 | function getText(selectors) { 93 | var nodes = selectors.map(function(selector) { 94 | return document.querySelector(selector); 95 | }); 96 | 97 | return deba(nodes); 98 | } 99 | 100 | function selectCopy(text) { 101 | var textarea = document.createElement("textarea"); 102 | textarea.style.cssText = textareaStyles.replace(/\s+/g, " "); 103 | textarea.textContent = text; 104 | 105 | document.body.appendChild(textarea); 106 | 107 | textarea.select(); 108 | document.execCommand("copy"); 109 | 110 | textarea.remove(); 111 | } 112 | 113 | function textractor() { 114 | var selectors = findSelectors(); 115 | 116 | if(!selectors) { 117 | alertUser(errorAlertElement); 118 | return; 119 | } 120 | 121 | console.log(selectors); 122 | selectCopy(getText(selectors)); 123 | 124 | alertUser(successAlertElement); 125 | } 126 | 127 | textractor(); -------------------------------------------------------------------------------- /deba.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | "use strict"; 3 | 4 | const Utils = { 5 | isPresent: function(text) { 6 | return text != "" && text.search(/^\s*$/) == -1; 7 | }, 8 | normalise: function(text) { 9 | return text.replace(/\s+/g, " ").trim(); 10 | } 11 | }; 12 | 13 | function Stringifier(segments) { 14 | this.segments = segments; 15 | } 16 | 17 | Stringifier.prototype.chunkUpSegments = function() { 18 | const chunks = []; 19 | let lastType = null; 20 | let currentChunk = []; 21 | 22 | for(const segment of this.segments.concat(null)) { 23 | if(lastType == null || segment == null || segment.constructor.name != lastType) { 24 | if(currentChunk.length) { 25 | chunks.push([lastType, currentChunk]); 26 | currentChunk = []; 27 | 28 | if(segment == null) break; 29 | } 30 | 31 | lastType = segment.constructor.name; 32 | } 33 | 34 | currentChunk.push(segment); 35 | } 36 | 37 | return chunks; 38 | } 39 | 40 | Stringifier.prototype.stringify = function() { 41 | const chunks = this.chunkUpSegments(); 42 | const output = []; 43 | 44 | for(const chunk of chunks) { 45 | const type = chunk[0]; 46 | const text = chunk[1].join(""); 47 | 48 | if(type == "Span") output.push(Utils.normalise(text)); 49 | else output.push(text); 50 | } 51 | 52 | return output.join(""); 53 | } 54 | 55 | function Span(text) { 56 | this.text = text; 57 | } 58 | 59 | Span.prototype.toString = function() { 60 | return this.text; 61 | } 62 | 63 | function Heading(segments, level) { 64 | this.segments = segments; 65 | this.level = level; 66 | } 67 | 68 | Heading.prototype.toArray = function() { 69 | return ["######".substr(-this.level) + " "].concat(this.segments).concat(["\n\n"]); 70 | } 71 | 72 | function ListItem(segments, last, index) { 73 | this.segments = segments; 74 | this.last = last; 75 | this.index = index; 76 | } 77 | 78 | ListItem.prototype.toArray = function() { 79 | return [this.prefix()].concat(this.segments).concat(["\n" + (this.last ? "\n" : "")]); 80 | } 81 | 82 | ListItem.prototype.prefix = function() { 83 | if(this.index == null) return "* "; 84 | else return this.index + ". "; 85 | } 86 | 87 | function DefinitionTerm(segments) { 88 | this.segments = segments; 89 | } 90 | 91 | DefinitionTerm.prototype.toArray = function() { 92 | return this.segments.concat([":\n"]); 93 | } 94 | 95 | function DefinitionDescription(segments, last) { 96 | this.segments = segments; 97 | this.last = last; 98 | } 99 | 100 | DefinitionDescription.prototype.toArray = function() { 101 | return this.segments.concat(["\n" + (this.last ? "\n" : "")]); 102 | } 103 | 104 | function Paragraph(segments) { 105 | this.segments = segments; 106 | } 107 | 108 | Paragraph.prototype.toArray = function() { 109 | return this.segments.concat(["\n\n"]); 110 | } 111 | 112 | function Document(extractor) { 113 | this.extractor = extractor; 114 | this.content = ""; 115 | 116 | this.start(); 117 | } 118 | 119 | Document.prototype.getContent = function() { 120 | return this.content; 121 | } 122 | 123 | Document.prototype.push = function(segment) { 124 | this.segments.push(segment); 125 | } 126 | 127 | Document.prototype.break = function() { 128 | this.finish(); 129 | this.start(Array.prototype.slice.call(arguments)); 130 | } 131 | 132 | Document.prototype.finish = function() { 133 | if(!this.isPresent()) return; 134 | 135 | if(this.extractor.isInBlockquote()) this.content += "> "; 136 | this.content += this.blockContent(); 137 | } 138 | 139 | Document.prototype.start = function(args) { 140 | this.segments = []; 141 | this.args = args || []; 142 | } 143 | 144 | Document.prototype.isPresent = function() { 145 | for(const segment of this.segments) if((segment instanceof Span) && Utils.isPresent(segment.toString())) return true; 146 | return false; 147 | } 148 | 149 | Document.prototype.blockContent = function() { 150 | const blockType = this.args.shift(); 151 | this.args.unshift(this.segments); 152 | this.args.unshift(null); 153 | 154 | const block = new (Function.prototype.bind.apply(blockType, this.args)); 155 | 156 | return (new Stringifier(block.toArray())).stringify(); 157 | } 158 | 159 | function Extractor(input) { 160 | this.nodes = this.arrayify(input).map(this.convertNode); 161 | 162 | this.HEADING_TAGS = ["h1", "h2", "h3", "h4", "h5", "h6"]; 163 | this.BLOCK_INITIATING_TAGS = ["address", "article", "aside", "body", "blockquote", "div", "dd", "dl", "dt", "figure", 164 | "footer", "header", "li", "main", "nav", "ol", "p", "pre", "section", "td", "th", "ul"]; 165 | this.ENHANCERS = { b: "*", strong: "*", i: "_", em: "_" }; 166 | this.SKIP_TAGS = ["head", "style", "script", "noscript"]; 167 | } 168 | 169 | Extractor.prototype.blocks = function() { 170 | return this.blocks; 171 | } 172 | 173 | Extractor.prototype.extract = function() { 174 | this.justAppendedBr = false; 175 | this.inBlockquote = false; 176 | 177 | this.document = new Document(this); 178 | 179 | for(const node of this.nodes) this.process(node); 180 | 181 | return this.document.getContent().trim(); 182 | } 183 | 184 | Extractor.prototype.arrayify = function(input) { 185 | if(Array.isArray(input)) return input; 186 | else return [input]; 187 | } 188 | 189 | Extractor.prototype.convertNode = function(input) { 190 | if(input instanceof HTMLElement) return input; 191 | else if(input instanceof Document) return input.documentElement; 192 | else if(input instanceof Window) return input.document.documentElement; 193 | else throw "input passed to Extractor not of valid type; must be an instance of HTMLElement, Document, or Window."; 194 | } 195 | 196 | Extractor.prototype.process = function(node) { 197 | const nodeName = node.nodeName.toLowerCase(); 198 | 199 | if(this.SKIP_TAGS.includes(nodeName)) return; 200 | 201 | //Handle repeated brs by making a paragraph break 202 | if(nodeName == "br") { 203 | if(this.justAppendedBr) { 204 | this.justAppendedBr = false; 205 | 206 | this.document.break(Paragraph); 207 | 208 | return; 209 | } 210 | else { 211 | this.justAppendedBr = true; 212 | } 213 | } 214 | else if(this.justAppendedBr) { 215 | this.justAppendedBr = false; 216 | 217 | this.document.push("\n"); 218 | } 219 | 220 | if(node.nodeType == Node.TEXT_NODE) { 221 | if(Utils.isPresent(node.textContent)) this.document.push(new Span(node.textContent)); 222 | 223 | return; 224 | } 225 | 226 | if(this.ENHANCERS[nodeName]) { 227 | this.document.push(new Span(this.ENHANCERS[nodeName])); 228 | this.processChildren(node); 229 | this.document.push(new Span(this.ENHANCERS[nodeName])); 230 | 231 | return; 232 | } 233 | 234 | if(nodeName == "blockquote") { 235 | this.inBlockquote = true; 236 | 237 | this.document.break(Paragraph); 238 | this.processChildren(node); 239 | this.document.break(Paragraph); 240 | 241 | this.inBlockquote = false; 242 | 243 | return; 244 | } 245 | 246 | if(nodeName == "li") { 247 | let index = null; 248 | if(node.parentNode.nodeName.toLowerCase() == "ol") { 249 | index = 1; 250 | let sibling = node; 251 | while((sibling = sibling.previousElementSibling)) index++; 252 | } 253 | 254 | this.document.break(ListItem, node.nextElementSibling == null, index); 255 | this.processChildren(node); 256 | this.document.break(Paragraph); 257 | 258 | return; 259 | } 260 | 261 | if(nodeName == "dt") { 262 | this.document.break(DefinitionTerm); 263 | this.processChildren(node); 264 | this.document.break(Paragraph); 265 | 266 | return; 267 | } 268 | 269 | if(nodeName == "dd") { 270 | this.document.break(DefinitionDescription, node.nextElementSibling == null); 271 | this.processChildren(node); 272 | this.document.break(Paragraph); 273 | 274 | return; 275 | } 276 | 277 | //These tags terminate the current paragraph, if present, and start a new paragraph 278 | if(this.BLOCK_INITIATING_TAGS.includes(nodeName)) { 279 | this.document.break(Paragraph); 280 | this.processChildren(node); 281 | this.document.break(Paragraph); 282 | 283 | return; 284 | } 285 | 286 | if(this.HEADING_TAGS.includes(nodeName)) { 287 | this.document.break(Heading, parseInt(nodeName[1])); 288 | this.processChildren(node); 289 | this.document.break(Paragraph); 290 | 291 | return; 292 | } 293 | 294 | //Pretend that the children of this node were siblings of this node (move them one level up the tree) 295 | this.processChildren(node); 296 | } 297 | 298 | Extractor.prototype.processChildren = function(node) { 299 | for(const child of node.childNodes) this.process(child); 300 | } 301 | 302 | Extractor.prototype.isInBlockquote = function() { 303 | return this.inBlockquote; 304 | } 305 | 306 | window.deba = function(input) { 307 | return (new Extractor(input)).extract(); 308 | }; 309 | })(window); --------------------------------------------------------------------------------