├── 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);
--------------------------------------------------------------------------------