├── .editorconfig
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
└── workflows
│ ├── code-lint.yml
│ └── codeql-analysis.yml
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── convert-logs.js
├── gulpfile.js
├── package-lock.json
├── package.json
├── read-log.js
├── src
├── README.md
├── background.js
├── background
│ ├── clipboard.js
│ ├── commands.js
│ ├── copy.js
│ ├── helpers.js
│ ├── index.js
│ ├── menu.js
│ └── paste.js
├── colors.sass
├── content.js
├── content.sass
├── content
│ ├── capture.js
│ ├── index.js
│ ├── infobox.js
│ ├── loader.js
│ ├── scroller.js
│ ├── selection.js
│ └── table.js
├── ico128.png
├── ico16.png
├── ico19.png
├── ico32.png
├── ico38.png
├── ico48.png
├── lib
│ ├── cell.js
│ ├── css.js
│ ├── dom.js
│ ├── event.js
│ ├── keyboard.js
│ ├── matrix.js
│ ├── message.js
│ ├── number.js
│ ├── preferences.js
│ └── util.js
├── manifest.json
├── options.js
├── options.pug
├── options.sass
├── popup.js
├── popup.pug
└── popup.sass
└── test
├── jasmine.json
├── number_test.js
└── server
├── base.html
├── frame.html
├── index.js
├── public
└── img
│ ├── a.png
│ ├── b.png
│ └── c.png
└── templates
├── big.js
├── dynamic.html
├── forms.html
├── framea.html
├── frameb.html
├── frameset.html
├── frameset_content.html
├── hidden.html
├── nested.html
├── numbers.html
├── paste.html
├── script.html
├── scroll.js
├── simple.html
├── spans.html
└── styled.html
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 2
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | max_line_length = off
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: nirantak
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Description
2 |
3 | - CopyTables version:
4 | - Firefox version:
5 | - Operating System:
6 |
7 | Describe what you were trying to get done.
8 | Tell us what happened, what went wrong, and what you expected to happen.
9 |
10 | ### What I Did
11 |
12 | ```txt
13 | Paste the command(s) you ran and the output/screenshots.
14 | If there was a crash, please include any logs/traceback here.
15 | ```
16 |
--------------------------------------------------------------------------------
/.github/workflows/code-lint.yml:
--------------------------------------------------------------------------------
1 | name: Linter
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | lint:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Set up NodeJS
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: "18.3.0"
20 | cache: "npm"
21 |
22 | - name: Install Dependencies
23 | run: npm install
24 |
25 | - name: Run Code Formatter
26 | run: npm run format -- --check
27 |
28 | - name: Run Firefox Extension Linter
29 | run: |
30 | npm run prod
31 | npm run lint
32 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: CodeQL
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 | schedule:
9 | - cron: "30 6 * * 1"
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: ["javascript"]
24 |
25 | steps:
26 | - name: Checkout repository
27 | uses: actions/checkout@v2
28 |
29 | # Initializes the CodeQL tools for scanning
30 | - name: Initialize CodeQL
31 | uses: github/codeql-action/init@v1
32 | with:
33 | languages: ${{ matrix.language }}
34 |
35 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java)
36 | - name: Autobuild
37 | uses: github/codeql-action/autobuild@v1
38 |
39 | - name: Perform CodeQL Analysis
40 | uses: github/codeql-action/analyze@v1
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | app
4 | npm-debug.log
5 | *.zip
6 | ._*
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": true,
4 | "singleQuote": false,
5 | "tabWidth": 2,
6 | "trailingComma": "es5"
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Nirantak Raghav
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CopyTables
2 |
3 | 
4 |
5 | Firefox extension to select and copy table cells.
6 |
7 | [Install for Firefox](https://addons.mozilla.org/firefox/addon/copywebtables/)
8 |
9 | ## Usage
10 |
11 | - Hold Opt (macOS) or Alt (Windows) and drag to select cells.
12 | - Hold Opt+Cmd (macOS) or Alt+Ctrl (Windows) and drag to select columns.
13 | - Copy selection (or the whole table) as seen on the screen (for rich text editors)
14 | - Copy as CSV or TSV (for Spreadsheets).
15 | - Copy as HTML (for your website).
16 |
17 | Forked from [gebrkn/copytables](https://github.com/gebrkn/copytables) for Chrome ([Web Store](https://chrome.google.com/webstore/detail/copytables/ekdpkppgmlalfkphpibadldikjimijon)).
18 |
19 | ## Building the Extension
20 |
21 | Build tested using: `node` v18.3.0 (`npm` v8.11.0) on macOS 12.4
22 |
23 | ```bash
24 | # Clone the repo
25 | git clone https://github.com/nirantak/copytables.git
26 | cd copytables
27 |
28 | # Install dependencies
29 | npm install
30 |
31 | # Build zipped extension
32 | npm run deploy
33 | ## Output: copytables-0.1.1.zip
34 |
35 | # Test in development mode
36 | npm start
37 | ## This does the following:
38 | ## - Runs a dev server with dummy data on localhost:9876
39 | ## - Runs gulp to watch and rebuild for code changes
40 | ## - Opens Firefox with the extension loaded in debug mode
41 | ```
42 |
--------------------------------------------------------------------------------
/convert-logs.js:
--------------------------------------------------------------------------------
1 | /// In dev, rewrite console.logs to something more readable when logged to stdout
2 | /// In production, get rid of them
3 |
4 | function __dbg() {
5 | var nl = "\n",
6 | buf = [];
7 |
8 | function type(x) {
9 | try {
10 | return Object.prototype.toString
11 | .call(x)
12 | .replace("[object ", "")
13 | .replace("]", "");
14 | } catch (e) {
15 | return "";
16 | }
17 | }
18 |
19 | function props(x, depth) {
20 | var r = Array.isArray(x) ? [] : {};
21 | try {
22 | Object.keys(x).forEach(function (k) {
23 | r[k] = inspect(x[k], depth + 1);
24 | });
25 | return r;
26 | } catch (e) {
27 | return "error";
28 | }
29 | }
30 |
31 | function inspect(x, depth) {
32 | if (depth > 5) return "...";
33 | if (typeof x !== "object" || x === null) return x;
34 | var t = type(x),
35 | p = props(x, depth);
36 | if (t === "Object" || t === "Array") return p;
37 | var r = {};
38 | r[t] = p;
39 | return r;
40 | }
41 |
42 | buf.push(location ? location.href : "??");
43 |
44 | [].forEach.call(arguments, function (arg) {
45 | buf.push(inspect(arg, 0));
46 | });
47 |
48 | return "@@CT<<" + JSON.stringify(buf) + ">>CT@@";
49 | }
50 |
51 | function handleLogging(content, path, isDev) {
52 | var dbg = String(__dbg);
53 | var out = [];
54 |
55 | if (isDev) {
56 | out.push(dbg.replace(/\s+/g, " "));
57 | }
58 |
59 | content.split("\n").forEach(function (line, lnum) {
60 | var m = line.match(/(.*?)console\.log\((.*)\)(.*)/);
61 |
62 | if (m) {
63 | if (isDev) {
64 | out.push(
65 | [
66 | m[1],
67 | "console.log(__dbg(",
68 | JSON.stringify(path),
69 | "," + (lnum + 1),
70 | "," + m[2] + "))",
71 | m[3],
72 | ].join("")
73 | );
74 | }
75 | } else {
76 | if (isDev && line.match(/function/)) {
77 | out.push(" // " + path + ":" + (lnum + 1));
78 | }
79 | out.push(line);
80 | }
81 | });
82 |
83 | return out.join("\n");
84 | }
85 |
86 | module.exports = function (content) {
87 | return handleLogging(content, this.resourcePath, this.query == "?dev");
88 | };
89 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const clip = require("gulp-clip-empty-files"),
4 | cp = require("child_process"),
5 | del = require("del"),
6 | fs = require("fs"),
7 | gulp = require("gulp"),
8 | named = require("vinyl-named"),
9 | pug = require("gulp-pug"),
10 | path = require("path"),
11 | sass = require("gulp-sass")(require("node-sass")),
12 | sassTypes = require("node-sass").types,
13 | webpack = require("webpack-stream"),
14 | uglify = require("gulp-uglify"),
15 | through = require("through2");
16 | const DEST = "./app";
17 | const TEST_URL = "http://localhost:9876/all";
18 | const IS_DEV = process.env.NODE_ENV === "development";
19 |
20 | // based on https://coderwall.com/p/fhgu_q/inlining-images-with-gulp-sass
21 | function sassInlineImage(file) {
22 | var filePath = path.resolve(process.cwd(), file.getValue()),
23 | ext = filePath.split(".").pop(),
24 | data = fs.readFileSync(filePath),
25 | buffer = Buffer.from(data),
26 | str = '"data:image/' + ext + ";base64," + buffer.toString("base64") + '"';
27 | return sassTypes.String(str);
28 | }
29 |
30 | const webpackConfig = {
31 | mode: IS_DEV ? "development" : "production",
32 | devtool: false,
33 | module: {
34 | rules: [
35 | {
36 | test: /\.js$/,
37 | use: [
38 | {
39 | loader: __dirname + "/convert-logs?" + (IS_DEV ? "dev" : ""),
40 | },
41 | ],
42 | },
43 | ],
44 | },
45 | };
46 |
47 | gulp.task("pug", function () {
48 | return gulp.src("./src/*.pug").pipe(pug()).pipe(gulp.dest(DEST));
49 | });
50 |
51 | gulp.task("sass", function () {
52 | return gulp
53 | .src("./src/*.sass")
54 | .pipe(
55 | sass({
56 | functions: {
57 | "inline-image($file)": sassInlineImage,
58 | },
59 | })
60 | )
61 | .pipe(clip())
62 | .pipe(gulp.dest(DEST));
63 | });
64 |
65 | gulp.task("copy", function () {
66 | return gulp.src("./src/*.{png,css,json,svg}").pipe(gulp.dest(DEST));
67 | });
68 |
69 | gulp.task("js", function () {
70 | return gulp
71 | .src("./src/*.js")
72 | .pipe(named())
73 | .pipe(webpack(webpackConfig))
74 | .pipe(IS_DEV ? through.obj() : uglify())
75 | .pipe(gulp.dest(DEST));
76 | });
77 |
78 | gulp.task("clean", function (done) {
79 | del.sync(DEST);
80 | done();
81 | });
82 |
83 | gulp.task("zip", function (done) {
84 | const m = require("./src/manifest.json"),
85 | fn = `copytables-${m.version}.zip`;
86 |
87 | cp.execSync(`rm -f ./${fn} && zip -j ./${fn} ./app/*`);
88 | done();
89 | });
90 |
91 | gulp.task("make", gulp.parallel("pug", "sass", "copy", "js"));
92 | gulp.task("build", gulp.series("clean", "make"));
93 | gulp.task("deploy", gulp.series("build", "zip"));
94 |
95 | gulp.task("watch", function () {
96 | gulp.watch("./src/**/*", gulp.series("make"));
97 | });
98 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "copytables",
3 | "version": "0.1.1",
4 | "description": "Firefox extension to select and copy table cells",
5 | "author": "Nirantak Raghav",
6 | "license": "MIT",
7 | "homepage": "https://github.com/nirantak/copytables",
8 | "private": true,
9 | "scripts": {
10 | "server": "node ./test/server/index.js",
11 | "watch": "NODE_ENV=development gulp watch",
12 | "ext": "web-ext run -s ./app -u http://localhost:9876/all",
13 | "start": "npm run dev && concurrently -k -c 'cyan,green,yellow' 'npm:watch' 'npm:server' 'npm:ext'",
14 | "clean": "rm -rf ./app ./copytables*.zip",
15 | "dev": "NODE_ENV=development gulp build",
16 | "prod": "NODE_ENV=production gulp build",
17 | "deploy": "NODE_ENV=production gulp deploy",
18 | "test": "jasmine --config=./test/jasmine.json",
19 | "lint": "web-ext lint -s ./app",
20 | "format": "prettier --write '!app/**/*' '**/*.{js,scss,html,json}'"
21 | },
22 | "engines": {
23 | "node": "18.3.0",
24 | "npm": "8.11.0"
25 | },
26 | "dependencies": {
27 | "del": "^6.1.1",
28 | "gulp": "^4.0.2",
29 | "gulp-clip-empty-files": "^0.1.2",
30 | "gulp-pug": "^5.0.0",
31 | "gulp-rename": "^2.0.0",
32 | "gulp-sass": "^5.1.0",
33 | "gulp-uglify": "^3.0.2",
34 | "node-sass": "^7.0.1",
35 | "vinyl-named": "^1.1.0",
36 | "webpack-stream": "^7.0.0"
37 | },
38 | "devDependencies": {
39 | "concurrently": "^7.2.1",
40 | "express": "^4.18.1",
41 | "glob": "^8.0.3",
42 | "jasmine": "^4.1.0",
43 | "mustache": "^4.2.0",
44 | "prettier": "^2.6.2",
45 | "web-ext": "^6.8.0"
46 | },
47 | "repository": {
48 | "type": "git",
49 | "url": "git+https://github.com/nirantak/copytables.git"
50 | },
51 | "bugs": {
52 | "url": "https://github.com/nirantak/copytables/issues"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/read-log.js:
--------------------------------------------------------------------------------
1 | var readline = require("readline");
2 |
3 | function proc(line) {
4 | var m = line.match(/@@CT<<(.+?)>>CT@@/);
5 | if (!m) return "";
6 | var data = JSON.parse(m[1]);
7 |
8 | var url = data.shift();
9 |
10 | if (url.match(/^moz-extension/)) url = "";
11 |
12 | var file = data.shift();
13 | var line = data.shift();
14 |
15 | var prefix = file.split("src/")[1].split(".")[0] + ":" + line + " ";
16 |
17 | var s = data
18 | .map(function (x) {
19 | return JSON.stringify(x);
20 | })
21 | .join(" ");
22 |
23 | if (url) s += " " + url;
24 |
25 | console.log(prefix + s);
26 | }
27 |
28 | readline
29 | .createInterface({
30 | input: process.stdin,
31 | output: process.stdout,
32 | terminal: false,
33 | })
34 | .on("line", proc);
35 |
--------------------------------------------------------------------------------
/src/README.md:
--------------------------------------------------------------------------------
1 | ## References
2 |
3 | - JS API differences: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#javascript_apis
4 | - Polyfill for Chrome: https://www.npmjs.com/package/webextension-polyfill
5 |
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | require("./background/index").main();
3 | })();
4 |
--------------------------------------------------------------------------------
/src/background/clipboard.js:
--------------------------------------------------------------------------------
1 | var M = (module.exports = {});
2 |
3 | var dom = require("../lib/dom"),
4 | util = require("../lib/util");
5 |
6 | M.copyRich = function (text) {
7 | console.log(util.timeStart("copyRich"));
8 |
9 | var t = document.createElement("div");
10 | document.body.appendChild(t);
11 |
12 | t.contentEditable = true;
13 | t.innerHTML = text;
14 |
15 | dom.select(t);
16 | document.execCommand("copy");
17 | document.body.removeChild(t);
18 |
19 | console.log(util.timeEnd("copyRich"));
20 | };
21 |
22 | M.copyText = function (text) {
23 | console.log(util.timeStart("copyText"));
24 |
25 | var t = document.createElement("textarea");
26 | document.body.appendChild(t);
27 |
28 | t.value = text;
29 | t.focus();
30 | t.select();
31 |
32 | document.execCommand("copy");
33 | document.body.removeChild(t);
34 |
35 | console.log(util.timeEnd("copyText"));
36 | };
37 |
38 | M.content = function () {
39 | var cc = "",
40 | pasted = false;
41 |
42 | var pasteHandler = function (evt) {
43 | if (pasted) return;
44 | pasted = true;
45 | console.log("paste handler toggled");
46 | cc = evt.clipboardData.getData("text/html");
47 | evt.stopPropagation();
48 | evt.preventDefault();
49 | };
50 |
51 | document.addEventListener("paste", pasteHandler, true);
52 | console.log("exec paste");
53 | document.execCommand("paste");
54 | document.removeEventListener("paste", pasteHandler);
55 | return cc;
56 | };
57 |
--------------------------------------------------------------------------------
/src/background/commands.js:
--------------------------------------------------------------------------------
1 | var M = (module.exports = {});
2 |
3 | var message = require("../lib/message"),
4 | preferences = require("../lib/preferences"),
5 | util = require("../lib/util"),
6 | menu = require("./menu"),
7 | copy = require("./copy"),
8 | helpers = require("./helpers");
9 |
10 | function findTableCommand(direction, sender) {
11 | console.log("findTableCommand", direction, sender);
12 |
13 | if (!sender) {
14 | return helpers.findTable(direction);
15 | }
16 |
17 | message.frame("tableIndexFromContextMenu", sender).then(function (res) {
18 | if (res.data !== null) {
19 | helpers.findTable(direction, {
20 | tabId: sender.tabId,
21 | frameId: sender.frameId,
22 | index: res.data,
23 | });
24 | } else {
25 | helpers.findTable(direction);
26 | }
27 | });
28 | }
29 |
30 | function copyCommand(format, sender) {
31 | console.log(util.timeStart("copyCommand"));
32 |
33 | console.log("copyCommand", format, sender);
34 |
35 | var msg = {
36 | name: "beginCopy",
37 | broadcast: !sender,
38 | options: copy.options(format),
39 | };
40 |
41 | var ok = true;
42 |
43 | function doCopy(r) {
44 | if (r.data) {
45 | ok = copy.exec(format, r.data);
46 | return true;
47 | }
48 | }
49 |
50 | if (sender) {
51 | message.frame(msg, sender).then(function (r) {
52 | doCopy(r);
53 | message.frame(ok ? "endCopy" : "endCopyFailed", sender);
54 | console.log(util.timeEnd("copyCommand"));
55 | });
56 | } else {
57 | message.allFrames(msg).then(function (res) {
58 | res.some(doCopy);
59 | message.allFrames(ok ? "endCopy" : "endCopyFailed");
60 | console.log(util.timeEnd("copyCommand"));
61 | });
62 | }
63 | }
64 |
65 | function captureCommand(mode) {
66 | if (mode === "zzz") {
67 | mode = "";
68 | }
69 |
70 | var m = preferences.val("_captureMode");
71 | if (m === mode) {
72 | mode = "";
73 | }
74 |
75 | preferences.set("_captureMode", mode).then(function () {
76 | helpers.updateUI();
77 | message.allFrames("preferencesUpdated");
78 | });
79 | }
80 |
81 | function selectCommand(mode, sender) {
82 | if (sender) {
83 | message.frame({ name: "selectFromContextMenu", mode: mode }, sender);
84 | }
85 | }
86 |
87 | M.exec = function (cmd, sender) {
88 | console.log("GOT COMMAND", cmd, sender);
89 |
90 | if (sender && typeof sender.tabId === "undefined") {
91 | sender = null; // this comes from the popup
92 | }
93 |
94 | if (cmd === "copy") {
95 | preferences.copyFormats().forEach(function (f) {
96 | if (f.default) {
97 | cmd = "copy_" + f.id;
98 | }
99 | });
100 | }
101 |
102 | var m;
103 |
104 | m = cmd.match(/^copy_(\w+)/);
105 | if (m) {
106 | return copyCommand(m[1], sender);
107 | }
108 |
109 | m = cmd.match(/^capture_(\w+)/);
110 | if (m) {
111 | return captureCommand(m[1], sender);
112 | }
113 |
114 | m = cmd.match(/^select_(\w+)/);
115 | if (m) {
116 | return selectCommand(m[1], sender);
117 | }
118 |
119 | switch (cmd) {
120 | case "find_next":
121 | return findTableCommand(+1, sender);
122 | case "find_previous":
123 | return findTableCommand(-1, sender);
124 | case "open_options":
125 | return util.callBrowser("runtime.openOptionsPage");
126 | }
127 | };
128 |
--------------------------------------------------------------------------------
/src/background/copy.js:
--------------------------------------------------------------------------------
1 | var M = (module.exports = {});
2 |
3 | var paste = require("./paste"),
4 | dom = require("../lib/dom"),
5 | matrix = require("../lib/matrix"),
6 | util = require("../lib/util"),
7 | clipboard = require("./clipboard");
8 |
9 | function trimTextMatrix(mat) {
10 | mat = matrix.map(mat, function (row, cell) {
11 | return util.strip(util.nobr(cell));
12 | });
13 | return matrix.trim(mat, Boolean);
14 | }
15 |
16 | function asTabs(mat) {
17 | return trimTextMatrix(mat)
18 | .map(function (row) {
19 | return row.join("\t");
20 | })
21 | .join("\n");
22 | }
23 |
24 | function asCSV(mat) {
25 | return trimTextMatrix(mat)
26 | .map(function (row) {
27 | return row
28 | .map(function (cell) {
29 | if (cell.match(/^\w+$/) || cell.match(/^-?[0-9]+(\.[0-9]*)?$/))
30 | return cell;
31 | return '"' + cell.replace(/"/g, '""') + '"';
32 | })
33 | .join(",");
34 | })
35 | .join("\n");
36 | }
37 |
38 | function asTextile(mat) {
39 | var rows = mat.map(function (row) {
40 | var cells = row
41 | .filter(function (node) {
42 | return node.td;
43 | })
44 | .map(function (node) {
45 | var t = "",
46 | s = "";
47 |
48 | t = dom.textContent(node.td);
49 |
50 | if (node.colSpan) s += "\\" + (node.colSpan + 1);
51 | if (node.rowSpan) s += "/" + (node.rowSpan + 1);
52 | if (s) s += ".";
53 |
54 | return "|" + s + " " + t.replace("|", "|");
55 | });
56 |
57 | return cells.join(" ") + " |";
58 | });
59 |
60 | return rows.join("\n");
61 | }
62 |
63 | M.formats = {};
64 |
65 | M.formats.richHTMLCSS = {
66 | opts: {
67 | method: "clipboard",
68 | withSelection: true,
69 | keepStyles: true,
70 | keepHidden: false,
71 | },
72 | exec: function (t) {
73 | clipboard.copyRich(t.html());
74 | },
75 | };
76 |
77 | M.formats.richHTML = {
78 | opts: {
79 | method: "clipboard",
80 | withSelection: true,
81 | keepStyles: false,
82 | keepHidden: false,
83 | },
84 | exec: function (t) {
85 | clipboard.copyRich(t.html());
86 | },
87 | };
88 |
89 | M.formats.textTabs = {
90 | opts: {
91 | method: "clipboard",
92 | withSelection: true,
93 | keepStyles: false,
94 | keepHidden: false,
95 | },
96 | exec: function (t) {
97 | clipboard.copyText(asTabs(t.textMatrix()));
98 | },
99 | };
100 |
101 | M.formats.textTabsSwap = {
102 | opts: {
103 | method: "clipboard",
104 | withSelection: true,
105 | keepStyles: false,
106 | keepHidden: false,
107 | },
108 | exec: function (t) {
109 | clipboard.copyText(asTabs(matrix.transpose(t.textMatrix())));
110 | },
111 | };
112 |
113 | M.formats.textCSV = {
114 | opts: {
115 | method: "clipboard",
116 | withSelection: true,
117 | keepStyles: false,
118 | keepHidden: false,
119 | },
120 | exec: function (t) {
121 | clipboard.copyText(asCSV(t.textMatrix()));
122 | },
123 | };
124 |
125 | M.formats.textCSVSwap = {
126 | opts: {
127 | method: "clipboard",
128 | withSelection: true,
129 | keepStyles: false,
130 | keepHidden: false,
131 | },
132 | exec: function (t) {
133 | clipboard.copyText(asCSV(matrix.transpose(t.textMatrix())));
134 | },
135 | };
136 |
137 | M.formats.textHTML = {
138 | opts: {
139 | method: "clipboard",
140 | withSelection: true,
141 | keepStyles: false,
142 | keepHidden: true,
143 | },
144 | exec: function (t) {
145 | clipboard.copyText(util.reduceWhitespace(t.html()));
146 | },
147 | };
148 |
149 | M.formats.textHTMLCSS = {
150 | opts: {
151 | method: "clipboard",
152 | withSelection: true,
153 | keepStyles: true,
154 | keepHidden: true,
155 | },
156 | exec: function (t) {
157 | clipboard.copyText(util.reduceWhitespace(t.html()));
158 | },
159 | };
160 |
161 | M.formats.textTextile = {
162 | opts: {
163 | method: "clipboard",
164 | withSelection: true,
165 | keepStyles: false,
166 | keepHidden: false,
167 | },
168 | exec: function (t) {
169 | clipboard.copyText(asTextile(t.nodeMatrix()));
170 | },
171 | };
172 |
173 | //
174 |
175 | M.options = function (format) {
176 | return M.formats[format].opts;
177 | };
178 |
179 | M.exec = function (format, data) {
180 | var ok = false,
181 | t = new paste.table(),
182 | fmt = M.formats[format];
183 |
184 | if (t.init(data, fmt.opts)) {
185 | fmt.exec(t);
186 | ok = true;
187 | }
188 |
189 | t.destroy();
190 | return ok;
191 | };
192 |
--------------------------------------------------------------------------------
/src/background/helpers.js:
--------------------------------------------------------------------------------
1 | var M = (module.exports = {});
2 |
3 | var message = require("../lib/message"),
4 | preferences = require("../lib/preferences"),
5 | util = require("../lib/util"),
6 | menu = require("./menu");
7 | var badgeColor = "#1e88ff";
8 |
9 | M.updateUI = function () {
10 | preferences.load().then(function () {
11 | menu.create();
12 | M.updateBadge();
13 | });
14 | };
15 |
16 | M.setBadge = function (s) {
17 | util.callBrowser("browserAction.setBadgeText", { text: s });
18 | util.callBrowser("browserAction.setBadgeBackgroundColor", {
19 | color: badgeColor,
20 | });
21 | };
22 |
23 | M.updateBadge = function () {
24 | var mode = preferences.val("_captureMode");
25 |
26 | console.log("updateBadge mode=" + mode);
27 |
28 | switch (mode) {
29 | case "column":
30 | return M.setBadge("C");
31 | case "row":
32 | return M.setBadge("R");
33 | case "cell":
34 | return M.setBadge("E");
35 | case "table":
36 | return M.setBadge("T");
37 | default:
38 | M.setBadge("");
39 | }
40 | };
41 |
42 | M.enumTables = function () {
43 | return message.allFrames("enumTables").then(function (res) {
44 | var all = [];
45 |
46 | res.forEach(function (r) {
47 | all = all.concat(
48 | (r.data || []).map(function (t) {
49 | t.frame = {
50 | tabId: r.receiver.tabId,
51 | frameId: r.receiver.frameId,
52 | };
53 | return t;
54 | })
55 | );
56 | });
57 |
58 | return all.sort(function (a, b) {
59 | return a.frame.frameId - b.frame.frameId || a.index - b.index;
60 | });
61 | });
62 | };
63 |
64 | M.findTable = function (direction, start) {
65 | M.enumTables().then(function (allTables) {
66 | if (!allTables.length) {
67 | return;
68 | }
69 |
70 | var curr = -1;
71 |
72 | allTables.some(function (t, n) {
73 | if (
74 | start &&
75 | t.frame.frameId == start.frameId &&
76 | t.frame.tabId === start.tabId &&
77 | t.index === start.index
78 | ) {
79 | curr = n;
80 | return true;
81 | }
82 | if (!start && t.selected) {
83 | curr = n;
84 | return true;
85 | }
86 | });
87 |
88 | if (direction === +1) {
89 | if (curr === -1 || curr === allTables.length - 1) curr = 0;
90 | else curr += 1;
91 | } else {
92 | if (curr === -1 || curr === 0) curr = allTables.length - 1;
93 | else curr--;
94 | }
95 |
96 | var t = allTables[curr];
97 | message.frame({ name: "selectTableByIndex", index: t.index }, t.frame);
98 | });
99 | };
100 |
--------------------------------------------------------------------------------
/src/background/index.js:
--------------------------------------------------------------------------------
1 | /// Background script main
2 |
3 | var M = (module.exports = {});
4 |
5 | var message = require("../lib/message"),
6 | util = require("../lib/util"),
7 | preferences = require("../lib/preferences"),
8 | menu = require("./menu"),
9 | commands = require("./commands"),
10 | copy = require("./copy"),
11 | helpers = require("./helpers");
12 |
13 | var messageListeners = {
14 | dropAllSelections: function (msg) {
15 | message.allFrames("dropSelection");
16 | },
17 |
18 | dropOtherSelections: function (msg) {
19 | message.enumFrames("active").then(function (frames) {
20 | frames.forEach(function (frame) {
21 | if (frame.frameId !== msg.sender.frameId) {
22 | message.frame("dropSelection", frame);
23 | }
24 | });
25 | });
26 | },
27 |
28 | contextMenu: function (msg) {
29 | helpers.enumTables().then(function (ts) {
30 | menu.enable(
31 | ["select_row", "select_column", "select_table"],
32 | msg.selectable
33 | );
34 | menu.enable(["copy"], msg.selectable);
35 | menu.enable(["find_previous", "find_next"], ts.length > 0);
36 | });
37 | },
38 |
39 | genericCopy: function (msg) {
40 | commands.exec("copy", msg.sender);
41 | },
42 |
43 | preferencesUpdated: function (msg) {
44 | helpers.updateUI();
45 | message.broadcast("preferencesUpdated");
46 | },
47 |
48 | command: function (msg) {
49 | console.log("messageListeners.command", msg.sender);
50 | commands.exec(msg.command, msg.sender);
51 | },
52 | };
53 |
54 | function init() {
55 | menu.create();
56 | message.listen(messageListeners);
57 |
58 | util.callBrowser("contextMenus.onClicked.addListener", function (info, tab) {
59 | commands.exec(info.menuItemId, { tabId: tab.id, frameId: info.frameId });
60 | });
61 |
62 | util.callBrowser("commands.onCommand.addListener", function (cmd) {
63 | commands.exec(cmd, null);
64 | });
65 |
66 | helpers.updateUI();
67 | }
68 |
69 | M.main = function () {
70 | preferences.load().then(init);
71 | };
72 |
--------------------------------------------------------------------------------
/src/background/menu.js:
--------------------------------------------------------------------------------
1 | var M = (module.exports = {});
2 |
3 | var preferences = require("../lib/preferences"),
4 | util = require("../lib/util");
5 |
6 | var mainMenu = {
7 | id: "root",
8 | title: "Table...",
9 | children: [
10 | {
11 | id: "select_row",
12 | title: "Select Row",
13 | },
14 | {
15 | id: "select_column",
16 | title: "Select Column",
17 | },
18 | {
19 | id: "select_table",
20 | title: "Select Table",
21 | },
22 | "---",
23 | {
24 | id: "find_previous",
25 | title: "Previous Table",
26 | },
27 | {
28 | id: "find_next",
29 | title: "Next Table",
30 | },
31 | "---",
32 | {
33 | id: "copy",
34 | title: "Copy",
35 | },
36 | {
37 | id: "copyAs",
38 | title: "Copy...",
39 | },
40 | ],
41 | };
42 |
43 | var uid = 0;
44 |
45 | function createMenu(menu, parent) {
46 | var desc = {
47 | enabled: true,
48 | contexts: ["page", "selection", "link", "editable"],
49 | };
50 |
51 | if (parent) {
52 | desc.parentId = parent;
53 | }
54 |
55 | if (menu === "---") {
56 | desc.id = "uid" + ++uid;
57 | desc.type = "separator";
58 | } else {
59 | desc.id = menu.id;
60 | desc.title = menu.title;
61 | }
62 |
63 | var sub = menu.children;
64 |
65 | if (menu.id === "copyAs") {
66 | var cf = preferences.copyFormats().filter(function (f) {
67 | return f.enabled;
68 | });
69 |
70 | if (!cf.length) {
71 | return;
72 | }
73 |
74 | sub = cf.map(function (f) {
75 | return {
76 | id: "copy_" + f.id,
77 | title: f.name,
78 | };
79 | });
80 | }
81 |
82 | var mobj = util.callBrowser("contextMenus.create", desc);
83 |
84 | if (sub) {
85 | sub.forEach(function (subMenu) {
86 | createMenu(subMenu, mobj);
87 | });
88 | }
89 |
90 | return mobj;
91 | }
92 |
93 | M.create = function () {
94 | util.callBrowserAsync("contextMenus.removeAll").then(function () {
95 | createMenu(mainMenu);
96 | });
97 | };
98 |
99 | M.enable = function (ids, enabled) {
100 | ids.forEach(function (id) {
101 | util.callBrowser("contextMenus.update", id, { enabled: enabled });
102 | if (id === "copy") {
103 | var cf = preferences.copyFormats().filter(function (f) {
104 | return f.enabled;
105 | });
106 | cf.forEach(function (f) {
107 | util.callBrowser("contextMenus.update", "copy_" + f.id, {
108 | enabled: enabled,
109 | });
110 | });
111 | }
112 | });
113 | };
114 |
--------------------------------------------------------------------------------
/src/background/paste.js:
--------------------------------------------------------------------------------
1 | var M = (module.exports = {});
2 |
3 | var dom = require("../lib/dom"),
4 | matrix = require("../lib/matrix"),
5 | util = require("../lib/util"),
6 | cell = require("../lib/cell"),
7 | css = require("../lib/css"),
8 | clipboard = require("./clipboard");
9 |
10 | function toMatrix(tbl) {
11 | console.log(util.timeStart("toMatrix"));
12 |
13 | var tds = {},
14 | rows = {},
15 | cols = {};
16 |
17 | dom.cells(tbl).forEach(function (td) {
18 | var bounds = dom.bounds(td);
19 | var c = bounds.x,
20 | r = bounds.y;
21 | cols[c] = rows[r] = 1;
22 | tds[r + "/" + c] = td;
23 | });
24 |
25 | rows = Object.keys(rows).sort(util.numeric);
26 | cols = Object.keys(cols).sort(util.numeric);
27 |
28 | var mat = rows.map(function (r) {
29 | return cols.map(function (c) {
30 | var td = tds[r + "/" + c];
31 | return td ? { td: td } : {};
32 | });
33 | });
34 |
35 | matrix.each(mat, function (row, node, ri, ni) {
36 | if (!node.td) return;
37 |
38 | var rs = parseInt(dom.attr(node.td, "rowSpan")) || 1;
39 | var cs = parseInt(dom.attr(node.td, "colSpan")) || 1;
40 |
41 | for (var i = 1; i < cs; i++) {
42 | if (row[ni + i] && !row[ni + i].td) row[ni + i].colRef = node;
43 | }
44 |
45 | for (var i = 1; i < rs; i++) {
46 | if (mat[ri + i] && mat[ri + i][ni] && !mat[ri + i][ni].td)
47 | mat[ri + i][ni].rowRef = node;
48 | }
49 | });
50 |
51 | console.log(util.timeEnd("toMatrix"));
52 |
53 | return mat;
54 | }
55 |
56 | function computeSpans(mat) {
57 | matrix.each(mat, function (_, node) {
58 | if (node.colRef) node.colRef.colSpan = (node.colRef.colSpan || 0) + 1;
59 | if (node.rowRef) node.rowRef.rowSpan = (node.rowRef.rowSpan || 0) + 1;
60 | });
61 | }
62 |
63 | function trim(tbl) {
64 | console.log(util.timeStart("trim.filter"));
65 |
66 | var mat = matrix.trim(toMatrix(tbl), function (node) {
67 | return cell.selected(node.td);
68 | });
69 |
70 | console.log(util.timeEnd("trim.filter"));
71 |
72 | console.log(util.timeStart("trim.remove"));
73 |
74 | var tds = [];
75 |
76 | matrix.each(mat, function (row, node) {
77 | if (node.td) {
78 | tds.push(node.td);
79 | }
80 | });
81 |
82 | var junk = [];
83 |
84 | dom.cells(tbl).forEach(function (td) {
85 | if (tds.indexOf(td) < 0) junk.push(td);
86 | else if (!cell.selected(td)) td.innerHTML = "";
87 | });
88 |
89 | dom.remove(junk);
90 | junk = [];
91 |
92 | dom.find("tr", tbl).forEach(function (tr) {
93 | if (dom.find("td, th", tr).length === 0) {
94 | junk.push(tr);
95 | }
96 | });
97 |
98 | dom.remove(junk);
99 |
100 | console.log(util.timeEnd("trim.remove"));
101 |
102 | computeSpans(mat);
103 |
104 | matrix.each(mat, function (_, node) {
105 | if (node.td) {
106 | dom.attr(node.td, "rowSpan", node.rowSpan ? node.rowSpan + 1 : null);
107 | dom.attr(node.td, "colSpan", node.colSpan ? node.colSpan + 1 : null);
108 | }
109 | });
110 | }
111 |
112 | function fixRelativeLinks(where, fixCssUrls) {
113 | var aa = where.ownerDocument.createElement("A");
114 |
115 | function fixUrl(url) {
116 | // since we've set BASE url, this works
117 | aa.href = url;
118 | return aa.href;
119 | }
120 |
121 | function fixTags(tags, attrs) {
122 | dom.find(tags, where).forEach(function (t) {
123 | attrs.forEach(function (attr) {
124 | var v = dom.attr(t, attr);
125 | if (v) {
126 | dom.attr(t, attr, fixUrl(v));
127 | }
128 | });
129 | });
130 | }
131 |
132 | fixTags("A, AREA, LINK", ["href"]);
133 | fixTags("IMG, INPUT, SCRIPT", ["src", "longdesc", "usemap"]);
134 | fixTags("FORM", ["action"]);
135 | fixTags("Q, BLOCKQUOTE, INS, DEL", ["cite"]);
136 | fixTags("OBJECT", ["classid", "codebase", "data", "usemap"]);
137 |
138 | if (fixCssUrls) {
139 | dom.find("*", where).forEach(function (el) {
140 | var style = dom.attr(el, "style");
141 |
142 | if (!style || style.toLowerCase().indexOf("url") < 0) return;
143 |
144 | var fixStyle = style.replace(
145 | /(\burl\s*\()([^()]+)/gi,
146 | function (_, pfx, url) {
147 | url = util.strip(url);
148 | if (url[0] === '"' || url[0] === "'") {
149 | return pfx + url[0] + fixUrl(url.slice(1, -1)) + url[0];
150 | }
151 | return pfx + fixUrl(url);
152 | }
153 | );
154 |
155 | if (fixStyle !== style) dom.attr(el, "style", fixStyle);
156 | });
157 | }
158 | }
159 |
160 | function removeHiddenElements(node) {
161 | var hidden = [];
162 |
163 | dom.find("*", node).forEach(function (el) {
164 | if (!dom.visible(el)) {
165 | hidden.push(el);
166 | }
167 | });
168 |
169 | if (hidden.length) {
170 | console.log("removeHidden: " + hidden.length);
171 | dom.remove(hidden);
172 | }
173 | }
174 | tcc = function (text) {
175 | console.log(util.timeStart("textCopy"));
176 |
177 | var t = document.createElement("textarea");
178 | document.body.appendChild(t);
179 |
180 | t.value = text;
181 | t.focus();
182 | t.select();
183 |
184 | document.execCommand("copy");
185 | document.body.removeChild(t);
186 |
187 | console.log(util.timeEnd("textCopy"));
188 | };
189 |
190 | M.table = function () {};
191 |
192 | M.table.prototype.init = function (data, options) {
193 | console.log(util.timeStart("paste.init"));
194 |
195 | this.frame = document.createElement("IFRAME");
196 | this.frame.setAttribute("sandbox", "allow-same-origin");
197 | document.body.appendChild(this.frame);
198 |
199 | this.doc = this.frame.contentDocument;
200 | this.body = this.doc.body;
201 |
202 | var base = this.doc.createElement("BASE");
203 | dom.attr(base, "href", data.url);
204 | this.body.appendChild(base);
205 |
206 | // some cells (e.g. containing an image) could become width=0
207 | // after paste, breaking toMatrix calculations
208 | var css = this.doc.createElement("STYLE");
209 | css.type = "text/css";
210 | css.innerHTML = "td { min-width: 1px; }";
211 | this.body.appendChild(css);
212 |
213 | this.div = this.doc.createElement("DIV");
214 | this.div.contentEditable = true;
215 | this.body.appendChild(this.div);
216 |
217 | var ok = this.initTable(data, options);
218 |
219 | console.log("paste.init=" + ok + " method=" + options.method);
220 | console.log(util.timeEnd("paste.init"));
221 |
222 | return ok;
223 | };
224 |
225 | M.table.prototype.initTable = function (data, options) {
226 | console.log(util.timeStart("paste.insertTable"));
227 |
228 | if (options.method === "clipboard") {
229 | this.div.focus();
230 |
231 | // NB: just pasting the clipboard via `this.doc.execCommand('paste')`
232 | // is very slow for some reason. Instead, intercept paste
233 | // to obtain the clipboard and insert it via innerHTML which is waaay faster
234 |
235 | this.div.innerHTML = clipboard.content();
236 |
237 | // destroy the clipboard to avoid pasting of intermediate results
238 | // this doesn't really fix that because they can hit paste before
239 | // .content() finishes, but still...
240 | clipboard.copyText("");
241 | }
242 |
243 | if (options.method === "transfer") {
244 | this.div.innerHTML = data.html;
245 | }
246 |
247 | console.log(util.timeEnd("paste.insertTable"));
248 |
249 | this.table = dom.findOne("table", this.div);
250 |
251 | if (!this.table || dom.tag(this.table) !== "TABLE") return false;
252 |
253 | if (data.hasSelection) {
254 | console.log(util.timeStart("paste.trim"));
255 | trim(this.table);
256 | console.log(util.timeEnd("paste.trim"));
257 | }
258 |
259 | if (options.method === "transfer" && options.keepStyles) {
260 | console.log(util.timeStart("paste.restoreStyles"));
261 |
262 | dom.findSelf("*", this.table).forEach(function (el) {
263 | var uid = dom.attr(el, "data-copytables-uid");
264 | if (uid && el.style) {
265 | dom.removeAttr(el, "style");
266 | el.style.cssText = css.compute(css.read(el), data.css[uid] || {});
267 | }
268 | dom.removeAttr(el, "data-copytables-uid");
269 | });
270 |
271 | console.log(util.timeEnd("paste.restoreStyles"));
272 | }
273 |
274 | if (options.method === "transfer" && !options.keepHidden) {
275 | console.log(util.timeStart("paste.removeHidden"));
276 | removeHiddenElements(this.div);
277 | console.log(util.timeEnd("paste.removeHidden"));
278 | }
279 |
280 | fixRelativeLinks(this.div, options.keepStyles);
281 | dom.cells(this.table).forEach(cell.reset);
282 |
283 | if (!options.keepStyles) {
284 | dom.findSelf("*", this.table).forEach(function (el) {
285 | dom.removeAttr(el, "style");
286 | dom.removeAttr(el, "class");
287 | });
288 | }
289 |
290 | return true;
291 | };
292 |
293 | M.table.prototype.html = function () {
294 | return this.table.outerHTML;
295 | };
296 |
297 | M.table.prototype.nodeMatrix = function () {
298 | var mat = toMatrix(this.table);
299 | computeSpans(mat);
300 | return mat;
301 | };
302 |
303 | M.table.prototype.textMatrix = function () {
304 | return matrix.map(toMatrix(this.table), function (_, node) {
305 | return dom.textContent(node.td);
306 | });
307 | };
308 |
309 | M.table.prototype.destroy = function () {
310 | if (this.frame) document.body.removeChild(this.frame);
311 | };
312 |
--------------------------------------------------------------------------------
/src/colors.sass:
--------------------------------------------------------------------------------
1 | $border: #f0f0f0
2 | $body: #777
3 | $link: #999
4 | $accent: #1e88ff
5 | $hot: #f0a340
6 | $button: #eaeaea
7 | $lite: #f5f5f5
8 | $cell-bg: #b5d5ff
9 |
--------------------------------------------------------------------------------
/src/content.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | require("./content/index").main();
3 | })();
4 |
--------------------------------------------------------------------------------
/src/content.sass:
--------------------------------------------------------------------------------
1 | @import "colors"
2 |
3 | $i: !important
4 | $no-trans: all 0s 0s $i
5 |
6 | body[data-copytables-wait],
7 | body[data-copytables-wait] *
8 | cursor: wait $i
9 |
10 | [data-copytables-selected]:not([data-copytables-locked])
11 | background-color: $cell-bg $i
12 | transition: $no-trans
13 |
14 | [data-copytables-marked]:not([data-copytables-locked])
15 | background-color: $cell-bg $i
16 | transition: $no-trans
17 |
18 | $infobox-border: 9px
19 | $infobox-color: $accent
20 |
21 | #__copytables_infobox__
22 |
23 | position: fixed
24 | font-family: Arial, sans-serif
25 | font-size: 14px
26 |
27 | overflow: hidden
28 | white-space: nowrap
29 | z-index: 65535
30 |
31 | background-color: transparentize(darken($infobox-color, 1), 0.03)
32 | background-image: url(inline-image('src/ico16.png'))
33 | background-position: 8px center
34 | background-repeat: no-repeat
35 |
36 | padding: 0 0 0 32px
37 | transition: opacity 0.2s ease
38 |
39 | &.hidden
40 | opacity: 0
41 |
42 | &[data-position="0"]
43 | left: 0
44 | top: 0
45 | border-bottom-right-radius: $infobox-border
46 | &[data-position="1"]
47 | right: 0
48 | top: 0
49 | border-bottom-left-radius: $infobox-border
50 | &[data-position="2"]
51 | right: 0
52 | bottom: 0
53 | border-top-left-radius: $infobox-border
54 | &[data-position="3"]
55 | left: 0
56 | bottom: 0
57 | border-top-right-radius: $infobox-border
58 |
59 | b
60 | display: inline-block
61 | margin: 0 1px
62 | padding: 12px 12px 12px 0
63 | font-weight: 200
64 | color: lighten($infobox-color, 35)
65 |
66 | &:first-child
67 | border-left: none
68 |
69 | i
70 | font-style: normal
71 | padding-left: 6px
72 | font-weight: 700
73 | color: lighten($infobox-color, 45)
74 |
75 | span
76 | background-color: transparentize(darken($infobox-color, 5), 0.03)
77 | color: lighten($infobox-color, 45)
78 | padding: 12px
79 | cursor: pointer
80 |
--------------------------------------------------------------------------------
/src/content/capture.js:
--------------------------------------------------------------------------------
1 | /// Handle selecting cells with mouse
2 |
3 | var M = (module.exports = {});
4 |
5 | var dom = require("../lib/dom"),
6 | cell = require("../lib/cell"),
7 | event = require("../lib/event"),
8 | message = require("../lib/message"),
9 | preferences = require("../lib/preferences"),
10 | util = require("../lib/util"),
11 | infobox = require("./infobox"),
12 | table = require("./table"),
13 | scroller = require("./scroller");
14 |
15 | M.Capture = function () {
16 | this.anchorPoint = null;
17 | this.table = null;
18 | };
19 |
20 | M.Capture.prototype.markRect = function (evt) {
21 | var cx = evt.clientX,
22 | cy = evt.clientY;
23 |
24 | var p = this.scroller.adjustPoint(this.anchorPoint);
25 |
26 | var rect = [
27 | Math.min(cx, p.x),
28 | Math.min(cy, p.y),
29 | Math.max(cx, p.x),
30 | Math.max(cy, p.y),
31 | ];
32 |
33 | var big = 10e6;
34 |
35 | if (this.mode === "column" || this.mode === "table") {
36 | rect[1] = -big;
37 | rect[3] = +big;
38 | }
39 |
40 | if (this.mode === "row" || this.mode === "table") {
41 | rect[0] = -big;
42 | rect[2] = +big;
43 | }
44 |
45 | return rect;
46 | };
47 |
48 | M.Capture.prototype.setCaptured = function (rect) {
49 | dom.cells(this.table).forEach(function (td) {
50 | cell.unmark(td);
51 | if (util.intersect(dom.bounds(td).rect, rect)) {
52 | cell.mark(td);
53 | }
54 | });
55 | };
56 |
57 | M.Capture.prototype.setLocked = function (rect, canSelect) {
58 | dom.cells(this.table).forEach(function (td) {
59 | cell.unlock(td);
60 | if (util.intersect(dom.bounds(td).rect, rect)) {
61 | if (!canSelect) {
62 | cell.unselect(td);
63 | cell.lock(td);
64 | }
65 | }
66 | });
67 | };
68 |
69 | M.Capture.prototype.selection = function () {
70 | var self = this,
71 | tds = cell.findSelected(self.table);
72 |
73 | if (!self.selectedCells) {
74 | return [true, (self.selectedCells = tds)];
75 | }
76 |
77 | if (tds.length !== self.selectedCells.length) {
78 | return [true, (self.selectedCells = tds)];
79 | }
80 |
81 | var eq = true;
82 |
83 | tds.forEach(function (td, i) {
84 | eq = eq && td === self.selectedCells[i];
85 | });
86 |
87 | if (!eq) {
88 | return [true, (self.selectedCells = tds)];
89 | }
90 |
91 | return [false, self.selectedCells];
92 | };
93 |
94 | M.Capture.prototype.start = function (evt, mode, extend) {
95 | var t = table.locate(evt.target);
96 |
97 | this.table = t.table;
98 | this.scroller = new scroller.Scroller(t.td);
99 | this.mode = mode;
100 | this.extend = extend;
101 |
102 | if (!this.anchorPoint) extend = false;
103 |
104 | if (!extend) {
105 | this.anchorPoint = {
106 | x: dom.bounds(t.td).x + 1,
107 | y: dom.bounds(t.td).y + 1,
108 | };
109 | this.setLocked(this.markRect(evt), !cell.selected(t.td));
110 | }
111 |
112 | var self = this;
113 |
114 | var tracker = function (type, evt) {
115 | if (type === "move") self.scroller.reset();
116 | self.scroller.scroll(evt);
117 | self.setCaptured(self.markRect(evt));
118 | infobox.update(self.table);
119 |
120 | if (type === "up") {
121 | self.onDone(self.table);
122 | }
123 | };
124 |
125 | event.trackMouse(evt, tracker);
126 | };
127 |
128 | M.Capture.prototype.stop = function () {
129 | dom.find("td, th").forEach(function (td) {
130 | cell.unmark(td);
131 | cell.unlock(td);
132 | });
133 | };
134 |
--------------------------------------------------------------------------------
/src/content/index.js:
--------------------------------------------------------------------------------
1 | /// Content script main
2 |
3 | var M = (module.exports = {});
4 |
5 | var preferences = require("../lib/preferences"),
6 | keyboard = require("../lib/keyboard"),
7 | event = require("../lib/event"),
8 | message = require("../lib/message"),
9 | dom = require("../lib/dom"),
10 | util = require("../lib/util"),
11 | capture = require("./capture"),
12 | infobox = require("./infobox"),
13 | selection = require("./selection"),
14 | table = require("./table"),
15 | loader = require("./loader");
16 |
17 | var mouseButton = 0,
18 | currentCapture = null;
19 |
20 | function parseEvent(evt) {
21 | var key = keyboard.key(evt),
22 | emod = preferences.int("modifier.extend"),
23 | mods = key.modifiers.code,
24 | kmods = mods & ~emod;
25 |
26 | if (
27 | preferences.val("capture.enabled") &&
28 | preferences.val("_captureMode") &&
29 | !kmods
30 | ) {
31 | console.log("got capture", preferences.val("_captureMode"), mods & emod);
32 | return [preferences.val("_captureMode"), mods & emod];
33 | }
34 |
35 | if (!key.scan.code && kmods) {
36 | var cap = util.first(preferences.captureModes(), function (m) {
37 | return kmods === preferences.int("modifier." + m.id);
38 | });
39 |
40 | if (cap) {
41 | console.log("got modifier", cap.id, mods & emod);
42 | return [cap.id, mods & emod];
43 | }
44 | }
45 | }
46 |
47 | function destroyCapture() {
48 | if (currentCapture) {
49 | currentCapture.stop();
50 | currentCapture = null;
51 | }
52 | }
53 |
54 | function captureDone(tbl) {
55 | table.selectCaptured(tbl);
56 | if (preferences.val("capture.reset")) {
57 | preferences.set("_captureMode", "").then(function () {
58 | message.background("preferencesUpdated");
59 | });
60 | }
61 | }
62 |
63 | function startCapture(evt, mode, extend) {
64 | var t = table.locate(evt.target);
65 |
66 | if (!t) {
67 | destroyCapture();
68 | return false;
69 | }
70 |
71 | if (currentCapture && currentCapture.table !== t.table) {
72 | destroyCapture();
73 | extend = false;
74 | }
75 |
76 | if (currentCapture) {
77 | currentCapture.stop();
78 | }
79 |
80 | currentCapture = currentCapture || new capture.Capture();
81 |
82 | currentCapture.onDone = captureDone;
83 |
84 | selection.start(evt.target);
85 | currentCapture.start(evt, mode, extend);
86 | }
87 |
88 | var copyLock = false,
89 | copyWaitTimeout = 300,
90 | copyWaitTimer = 0;
91 |
92 | function beginCopy(msg) {
93 | var tbl = selection.table(),
94 | hasSelection = true;
95 |
96 | if (!tbl) {
97 | if (msg.broadcast) return null;
98 | var el = event.lastTarget(),
99 | t = table.locate(el);
100 | if (!t) return null;
101 | tbl = t.table;
102 | hasSelection = false;
103 | }
104 |
105 | copyLock = true;
106 | var data = table.copy(tbl, msg.options, hasSelection);
107 |
108 | copyWaitTimer = setTimeout(function () {
109 | dom.attr(document.body, "data-copytables-wait", 1);
110 | }, copyWaitTimeout);
111 |
112 | return data;
113 | }
114 |
115 | function endCopy() {
116 | copyLock = false;
117 | clearTimeout(copyWaitTimer);
118 | dom.removeAttr(document.body, "data-copytables-wait");
119 | }
120 |
121 | var eventListeners = {
122 | mousedownCapture: function (evt) {
123 | event.register(evt);
124 |
125 | if (evt.button !== mouseButton) {
126 | return;
127 | }
128 |
129 | var p = parseEvent(evt);
130 | console.log("parseEvent=", p);
131 |
132 | if (!p || !selection.selectable(evt.target)) {
133 | message.background("dropAllSelections");
134 | return;
135 | }
136 |
137 | window.focus();
138 | startCapture(evt, p[0], p[1]);
139 | },
140 |
141 | copy: function (evt) {
142 | if (selection.active() && !copyLock) {
143 | console.log("COPY MINE");
144 | message.background("genericCopy");
145 | event.reset(evt);
146 | } else if (copyLock) {
147 | console.log("COPY LOCKED");
148 | event.reset(evt);
149 | } else {
150 | console.log("COPY PASS");
151 | }
152 | },
153 |
154 | contextmenu: function (evt) {
155 | event.register(evt);
156 |
157 | message.background({
158 | name: "contextMenu",
159 | selectable: selection.selectable(evt.target),
160 | selected: selection.active(),
161 | });
162 | },
163 | };
164 |
165 | var messageListeners = {
166 | dropSelection: function () {
167 | if (selection.table()) {
168 | selection.drop();
169 | }
170 | // hide infobox once a selection is dropped
171 | // this means users won't be able to click and copy from the infobox
172 | infobox.remove();
173 | },
174 |
175 | preferencesUpdated: preferences.load,
176 |
177 | enumTables: function (msg) {
178 | return table.enum(selection.table());
179 | },
180 |
181 | selectTableByIndex: function (msg) {
182 | var tbl = table.byIndex(msg.index);
183 | if (tbl) {
184 | selection.select(tbl, "table");
185 | tbl.scrollIntoView(true);
186 | infobox.update(selection.table());
187 | }
188 | },
189 |
190 | selectFromContextMenu: function (msg) {
191 | var el = event.lastTarget(),
192 | t = table.locate(el);
193 |
194 | if (t) {
195 | selection.toggle(t.td, msg.mode);
196 | } else if (msg.mode === "table") {
197 | selection.toggle(dom.closest(el, "table"), "table");
198 | }
199 |
200 | infobox.update(selection.table());
201 | },
202 |
203 | tableIndexFromContextMenu: function () {
204 | var el = event.lastTarget(),
205 | tbl = dom.closest(el, "table");
206 | return tbl ? table.indexOf(tbl) : null;
207 | },
208 |
209 | beginCopy: beginCopy,
210 | endCopy: endCopy,
211 |
212 | endCopyFailed: function () {
213 | if (copyLock) {
214 | // inform the user that copy/paste failed
215 | console.error("Sorry, CopyTables was unable to copy this table.");
216 | }
217 | endCopy();
218 | },
219 | };
220 |
221 | function init() {
222 | event.listen(document, eventListeners);
223 | message.listen(messageListeners);
224 | }
225 |
226 | M.main = function () {
227 | loader.load().then(function () {
228 | if (!document.body) {
229 | console.log("no body", document.URL);
230 | return;
231 | }
232 | preferences.load().then(init);
233 | });
234 | };
235 |
--------------------------------------------------------------------------------
/src/content/infobox.js:
--------------------------------------------------------------------------------
1 | /// Display diverse functions when dragging over a table
2 |
3 | var M = (module.exports = {});
4 |
5 | var cell = require("../lib/cell"),
6 | dom = require("../lib/dom"),
7 | event = require("../lib/event"),
8 | preferences = require("../lib/preferences"),
9 | number = require("../lib/number");
10 |
11 | function getValue(td, fmt) {
12 | var val = { text: "", number: 0, isNumber: false };
13 |
14 | dom.textContentItems(td).some(function (t) {
15 | var n = number.extract(t, fmt);
16 | if (n !== null) {
17 | return (val = { text: t, number: n, isNumber: true });
18 | }
19 | });
20 |
21 | return val;
22 | }
23 |
24 | function data(tbl) {
25 | if (!tbl) {
26 | return null;
27 | }
28 |
29 | var cells = cell.findSelected(tbl);
30 |
31 | if (!cells || !cells.length) {
32 | return null;
33 | }
34 |
35 | var fmt = preferences.numberFormat();
36 | var values = [];
37 |
38 | cells.forEach(function (td) {
39 | values.push(getValue(td, fmt));
40 | });
41 |
42 | return preferences.infoFunctions().map(function (f) {
43 | return {
44 | title: f.name + ":",
45 | message: f.fn(values),
46 | };
47 | });
48 | }
49 |
50 | var boxId = "__copytables_infobox__",
51 | pendingContent = null,
52 | timer = 0,
53 | freq = 500;
54 |
55 | function getBox() {
56 | return dom.findOne("#" + boxId);
57 | }
58 |
59 | function setTimer() {
60 | if (!timer) timer = setInterval(draw, freq);
61 | }
62 |
63 | function clearTimer() {
64 | clearInterval(timer);
65 | timer = 0;
66 | }
67 |
68 | function html(items, sticky) {
69 | var h = [];
70 |
71 | items.forEach(function (item) {
72 | if (item.message !== null)
73 | h.push(" " + item.title + "" + item.message + " ");
74 | });
75 |
76 | h = h.join("");
77 |
78 | if (sticky) {
79 | h += "× ";
80 | } else {
81 | h += " ";
82 | }
83 |
84 | return h;
85 | }
86 |
87 | function init() {
88 | var box = dom.create("div", {
89 | id: boxId,
90 | "data-position": preferences.val("infobox.position") || "0",
91 | });
92 | document.body.appendChild(box);
93 |
94 | box.addEventListener("click", function (e) {
95 | if (dom.tag(e.target) === "SPAN") hide();
96 | });
97 |
98 | return box;
99 | }
100 |
101 | function draw() {
102 | if (!pendingContent) {
103 | //console.log('no pendingContent');
104 | clearTimer();
105 | return;
106 | }
107 |
108 | if (pendingContent === "hide") {
109 | //console.log('removed');
110 | dom.remove([getBox()]);
111 | clearTimer();
112 | return;
113 | }
114 |
115 | var box = getBox() || init();
116 |
117 | dom.removeClass(box, "hidden");
118 | box.innerHTML = pendingContent;
119 |
120 | pendingContent = null;
121 | //console.log('drawn');
122 | }
123 |
124 | function show(items) {
125 | var p = html(items, preferences.val("infobox.sticky"));
126 |
127 | if (p === pendingContent) {
128 | //console.log('same content');
129 | return;
130 | }
131 |
132 | if (pendingContent) {
133 | //console.log('queued');
134 | }
135 |
136 | pendingContent = p;
137 | setTimer();
138 | }
139 |
140 | function hide() {
141 | //console.log('about to remove...');
142 | pendingContent = "hide";
143 | dom.addClass(getBox(), "hidden");
144 | setTimer();
145 | }
146 |
147 | M.update = function (tbl) {
148 | if (preferences.val("infobox.enabled")) {
149 | var d = data(tbl);
150 | if (d && d.length) show(d);
151 | }
152 | };
153 |
154 | M.remove = function () {
155 | if (!preferences.val("infobox.sticky")) {
156 | hide();
157 | }
158 | };
159 |
--------------------------------------------------------------------------------
/src/content/loader.js:
--------------------------------------------------------------------------------
1 | var M = (module.exports = {});
2 |
3 | M.load = function () {
4 | return new Promise(function (resolve) {
5 | function ready() {
6 | return (
7 | document &&
8 | (document.readyState === "interactive" ||
9 | document.readyState === "complete")
10 | );
11 | }
12 |
13 | function onload(e) {
14 | if (ready()) {
15 | console.log("loaded", document.readyState, document.URL);
16 | document.removeEventListener("readystatechange", onload);
17 | resolve();
18 | }
19 | }
20 |
21 | if (ready()) {
22 | console.log("ready", document.readyState, document.URL);
23 | return resolve();
24 | }
25 |
26 | console.log("not loaded", document.readyState, document.URL);
27 | document.addEventListener("readystatechange", onload);
28 | });
29 | };
30 |
--------------------------------------------------------------------------------
/src/content/scroller.js:
--------------------------------------------------------------------------------
1 | var M = (module.exports = {});
2 |
3 | var dom = require("../lib/dom"),
4 | event = require("../lib/event"),
5 | preferences = require("../lib/preferences");
6 |
7 | function isScrollable(el) {
8 | var css = window.getComputedStyle(el);
9 | if (
10 | !css.overflowX.match(/scroll|auto/) &&
11 | !css.overflowY.match(/scroll|auto/)
12 | )
13 | return false;
14 | return el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight;
15 | }
16 |
17 | function closestScrollable(el) {
18 | while (el && el !== document.body && el !== document.documentElement) {
19 | if (isScrollable(el)) return el;
20 | el = el.parentNode;
21 | }
22 | return null;
23 | }
24 |
25 | function position(base) {
26 | return base
27 | ? { x: base.scrollLeft, y: base.scrollTop }
28 | : { x: window.scrollX, y: window.scrollY };
29 | }
30 |
31 | M.Scroller = function (el) {
32 | (this.base = closestScrollable(el.parentNode)),
33 | (this.anchor = position(this.base));
34 | this.reset();
35 | };
36 |
37 | M.Scroller.prototype.reset = function () {
38 | this.amount = preferences.int("scroll.amount");
39 | this.acceleration = preferences.int("scroll.acceleration");
40 | };
41 |
42 | M.Scroller.prototype.adjustPoint = function (pt) {
43 | var p = position(this.base);
44 | return {
45 | x: pt.x + this.anchor.x - p.x,
46 | y: pt.y + this.anchor.y - p.y,
47 | };
48 | };
49 |
50 | M.Scroller.prototype.scroll = function (e) {
51 | function adjust(a, sx, sy, ww, hh, cx, cy) {
52 | if (cx < a) sx -= a;
53 | if (cx > ww - a) sx += a;
54 | if (cy < a) sy -= a;
55 | if (cy > hh - a) sy += a;
56 | return { x: sx, y: sy };
57 | }
58 |
59 | if (this.base) {
60 | var b = dom.bounds(this.base);
61 | var p = adjust(
62 | this.amount,
63 | this.base.scrollLeft,
64 | this.base.scrollTop,
65 | this.base.clientWidth,
66 | this.base.clientHeight,
67 | e.clientX - b.x,
68 | e.clientY - b.y
69 | );
70 |
71 | this.base.scrollLeft = p.x;
72 | this.base.scrollTop = p.y;
73 | } else {
74 | var p = adjust(
75 | this.amount,
76 | window.scrollX,
77 | window.scrollY,
78 | window.innerWidth,
79 | window.innerHeight,
80 | e.clientX,
81 | e.clientY
82 | );
83 |
84 | if (p.x != window.scrollX || p.y != window.scrollY) {
85 | window.scrollTo(p.x, p.y);
86 | }
87 | }
88 |
89 | this.amount = Math.max(
90 | 1,
91 | Math.min(100, this.amount + this.amount * (this.acceleration / 100))
92 | );
93 | };
94 |
--------------------------------------------------------------------------------
/src/content/selection.js:
--------------------------------------------------------------------------------
1 | /// Selection tools.
2 |
3 | var M = (module.exports = {});
4 |
5 | var dom = require("../lib/dom"),
6 | message = require("../lib/message"),
7 | cell = require("../lib/cell"),
8 | table = require("./table"),
9 | infobox = require("./infobox");
10 |
11 | function cellsToSelect(el, mode) {
12 | if (mode === "table") {
13 | var tbl = dom.closest(el, "table");
14 | return tbl ? dom.cells(tbl) : [];
15 | }
16 |
17 | var t = table.locate(el);
18 |
19 | if (!t) {
20 | return [];
21 | }
22 |
23 | if (mode === "cell") {
24 | return [t.td];
25 | }
26 |
27 | var sel = dom.bounds(t.td);
28 |
29 | return dom.cells(t.table).filter(function (td) {
30 | var b = dom.bounds(td);
31 |
32 | switch (mode) {
33 | case "column":
34 | return sel.x === b.x;
35 | case "row":
36 | return sel.y === b.y;
37 | }
38 | });
39 | }
40 |
41 | var excludeElements = "a, input, button, textarea, select, img";
42 |
43 | M.selectable = function (el) {
44 | return !!(
45 | el &&
46 | dom.closest(el, "table") &&
47 | !dom.closest(el, excludeElements)
48 | );
49 | };
50 |
51 | M.selected = function (el) {
52 | var t = table.locate(el);
53 | return t && cell.selected(t.td);
54 | };
55 |
56 | M.drop = function () {
57 | dom.find("td, th").forEach(cell.reset);
58 | };
59 |
60 | M.active = function () {
61 | return !!cell.find("selected").length;
62 | };
63 |
64 | M.table = function () {
65 | var cs = cell.find("selected");
66 | return cs.length ? dom.closest(cs[0], "table") : null;
67 | };
68 |
69 | M.start = function (el) {
70 | var t = table.locate(el);
71 |
72 | if (!t) {
73 | return false;
74 | }
75 |
76 | window.getSelection().removeAllRanges();
77 | message.background("dropOtherSelections");
78 |
79 | if (M.table() !== t.table) {
80 | M.drop();
81 | }
82 |
83 | return true;
84 | };
85 |
86 | M.select = function (el, mode) {
87 | if (dom.is(el, "table")) el = dom.cells(el)[0];
88 | if (el && M.start(el)) {
89 | var tds = cellsToSelect(el, mode);
90 | tds.forEach(cell.select);
91 | }
92 | };
93 |
94 | M.toggle = function (el, mode) {
95 | if (dom.is(el, "table")) el = dom.cells(el)[0];
96 | if (el && M.start(el)) {
97 | var tds = cellsToSelect(el, mode),
98 | fn = tds.every(cell.selected) ? cell.reset : cell.select;
99 | tds.forEach(fn);
100 | }
101 | };
102 |
--------------------------------------------------------------------------------
/src/content/table.js:
--------------------------------------------------------------------------------
1 | var M = (module.exports = {});
2 |
3 | var dom = require("../lib/dom"),
4 | cell = require("../lib/cell"),
5 | css = require("../lib/css"),
6 | event = require("../lib/event"),
7 | util = require("../lib/util");
8 |
9 | function listTables() {
10 | var all = [];
11 |
12 | dom.find("table").forEach(function (tbl, n) {
13 | if (!dom.cells(tbl).length || !dom.visible(tbl)) return;
14 | all.push({
15 | index: n,
16 | table: tbl,
17 | });
18 | });
19 |
20 | return all;
21 | }
22 |
23 | M.locate = function (el) {
24 | var td = dom.closest(el, "td, th"),
25 | tbl = dom.closest(td, "table");
26 |
27 | return td && tbl ? { td: td, table: tbl } : null;
28 | };
29 |
30 | M.indexOf = function (tbl) {
31 | var res = -1;
32 |
33 | listTables().forEach(function (r) {
34 | if (tbl === r.table) res = r.index;
35 | });
36 |
37 | return res;
38 | };
39 |
40 | M.byIndex = function (index) {
41 | var res = null;
42 |
43 | listTables().forEach(function (r) {
44 | if (index === r.index) res = r.table;
45 | });
46 |
47 | console.log("byIndex", res);
48 | return res;
49 | };
50 |
51 | M.enum = function (selectedTable) {
52 | return listTables().map(function (r) {
53 | r.selected = r.table === selectedTable;
54 | delete r.table;
55 | return r;
56 | });
57 | };
58 |
59 | M.copy = function (tbl, options, hasSelection) {
60 | console.log(util.timeStart("table.copy"));
61 |
62 | var data = {
63 | hasSelection: hasSelection,
64 | url: document.location ? document.location.href : "",
65 | };
66 |
67 | if (hasSelection) {
68 | // lock selected cells to remove highlighting with no animation
69 | dom.cells(tbl).forEach(function (td) {
70 | if (cell.selected(td)) {
71 | cell.lock(td);
72 | }
73 | });
74 | }
75 |
76 | if (options.method === "transfer") {
77 | data.css = {};
78 |
79 | if (options.keepStyles) {
80 | dom.findSelf("*", tbl).forEach(function (el, uid) {
81 | dom.attr(el, "data-copytables-uid", uid);
82 | data.css[uid] = css.read(el);
83 | });
84 | }
85 |
86 | data.html = tbl.outerHTML;
87 |
88 | if (options.keepStyles) {
89 | dom.findSelf("*", tbl).forEach(function (el) {
90 | dom.removeAttr(el, "data-copytables-uid");
91 | });
92 | }
93 | }
94 |
95 | if (options.method === "clipboard") {
96 | // work around "unselectable" tables
97 |
98 | var style = document.createElement("STYLE");
99 | style.type = "text/css";
100 | style.innerHTML =
101 | "* { user-select: auto !important; -webkit-user-select: auto !important }";
102 | document.body.appendChild(style);
103 |
104 | dom.select(tbl);
105 |
106 | // wrap copy in a capturing handler to work around copy-hijackers
107 |
108 | var copyHandler = function (evt) {
109 | console.log("COPY IN TABLE");
110 | evt.stopPropagation();
111 | };
112 |
113 | document.addEventListener("copy", copyHandler, true);
114 | document.execCommand("copy");
115 | document.removeEventListener("copy", copyHandler, true);
116 |
117 | dom.deselect();
118 | document.body.removeChild(style);
119 | }
120 |
121 | if (hasSelection) {
122 | dom.cells(tbl).forEach(function (td) {
123 | if (cell.selected(td)) {
124 | cell.unlock(td);
125 | }
126 | });
127 | }
128 |
129 | console.log("table.copy method=" + options.method);
130 | console.log(util.timeEnd("table.copy"));
131 |
132 | return data;
133 | };
134 |
135 | M.selectCaptured = function (tbl) {
136 | dom.cells(tbl).forEach(function (td) {
137 | if (cell.locked(td)) {
138 | cell.reset(td);
139 | } else if (cell.marked(td)) {
140 | cell.unmark(td);
141 | cell.select(td);
142 | }
143 | });
144 | };
145 |
--------------------------------------------------------------------------------
/src/ico128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico128.png
--------------------------------------------------------------------------------
/src/ico16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico16.png
--------------------------------------------------------------------------------
/src/ico19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico19.png
--------------------------------------------------------------------------------
/src/ico32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico32.png
--------------------------------------------------------------------------------
/src/ico38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico38.png
--------------------------------------------------------------------------------
/src/ico48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico48.png
--------------------------------------------------------------------------------
/src/lib/cell.js:
--------------------------------------------------------------------------------
1 | /// Get/set table cell state
2 |
3 | var M = (module.exports = {});
4 |
5 | var dom = require("../lib/dom");
6 |
7 | var prefix = "data-copytables-";
8 |
9 | M.set = function (td, state) {
10 | return td && td.setAttribute(prefix + state, "1");
11 | };
12 |
13 | M.is = function (td, state) {
14 | return td && td.hasAttribute(prefix + state);
15 | };
16 |
17 | M.clear = function (td, state) {
18 | return td && td.removeAttribute(prefix + state);
19 | };
20 |
21 | M.select = function (td) {
22 | return M.set(td, "selected");
23 | };
24 |
25 | M.selected = function (td) {
26 | return M.is(td, "selected");
27 | };
28 |
29 | M.unselect = function (td) {
30 | return M.clear(td, "selected");
31 | };
32 |
33 | M.mark = function (td) {
34 | return M.set(td, "marked");
35 | };
36 |
37 | M.marked = function (td) {
38 | return M.is(td, "marked");
39 | };
40 |
41 | M.unmark = function (td) {
42 | return M.clear(td, "marked");
43 | };
44 |
45 | M.lock = function (td) {
46 | return M.set(td, "locked");
47 | };
48 |
49 | M.locked = function (td) {
50 | return M.is(td, "locked");
51 | };
52 |
53 | M.unlock = function (td) {
54 | return M.clear(td, "locked");
55 | };
56 |
57 | M.find = function (states, where) {
58 | var sel = states
59 | .split(",")
60 | .map(function (s) {
61 | return "[" + prefix + s.trim() + "]";
62 | })
63 | .join(",");
64 | return dom.find(sel, where);
65 | };
66 |
67 | M.findSelected = function (where) {
68 | var sel = "[{}selected]:not([{}locked]), [{}marked]:not([{}locked])".replace(
69 | /{}/g,
70 | prefix
71 | );
72 | return dom.find(sel, where);
73 | };
74 |
75 | M.reset = function (td) {
76 | td.removeAttribute(prefix + "selected");
77 | td.removeAttribute(prefix + "marked");
78 | td.removeAttribute(prefix + "locked");
79 | };
80 |
--------------------------------------------------------------------------------
/src/lib/css.js:
--------------------------------------------------------------------------------
1 | /// CSS tools
2 |
3 | var M = (module.exports = {});
4 |
5 | var ignore = /^(width|height|display)$|^(-webkit|animation|motion)|-origin$/,
6 | props = null;
7 |
8 | M.read = function (el) {
9 | var cs = window.getComputedStyle(el);
10 |
11 | if (!props) {
12 | props = [].filter.call(cs, function (p) {
13 | return !ignore.test(p);
14 | });
15 | }
16 |
17 | var res = {};
18 |
19 | props.forEach(function (p) {
20 | var val = cs.getPropertyValue(p);
21 | res[p] = val.replace(/\b(\d+\.\d+)(?=px\b)/g, function ($0, $1) {
22 | return Math.round(parseFloat($1));
23 | });
24 | });
25 |
26 | if (cs.getPropertyValue("display") === "none") {
27 | res["display"] = "none";
28 | }
29 |
30 | return res;
31 | };
32 |
33 | M.compute = function (defaults, custom) {
34 | var rules = [];
35 |
36 | Object.keys(custom).forEach(function (k) {
37 | if (custom[k] !== defaults[k]) {
38 | rules.push(k + ":" + custom[k]);
39 | }
40 | });
41 |
42 | return rules.join("; ");
43 | };
44 |
--------------------------------------------------------------------------------
/src/lib/dom.js:
--------------------------------------------------------------------------------
1 | /// Basic DOM library
2 |
3 | var M = (module.exports = {});
4 |
5 | function toArray(coll) {
6 | return Array.prototype.slice.call(coll || [], 0);
7 | }
8 |
9 | function each(coll, fn) {
10 | Array.prototype.forEach.call(coll || [], fn);
11 | }
12 |
13 | M.is = function (el, sel) {
14 | return el && el.matches && el.matches(sel);
15 | };
16 |
17 | M.visible = function (el) {
18 | return el && !!(el.offsetHeight || el.offsetWidth);
19 | };
20 |
21 | M.findOne = function (sel, where) {
22 | return (where || document).querySelector(sel);
23 | };
24 |
25 | M.find = function (sel, where) {
26 | return (where || document).querySelectorAll(sel);
27 | };
28 |
29 | M.tag = function (el) {
30 | if (!el || !el.tagName) return "";
31 | return String(el.tagName).toUpperCase();
32 | };
33 |
34 | M.indexOf = function (el, sel, where) {
35 | var idx = -1;
36 | M.find(sel, where).forEach(function (e, n) {
37 | if (e === el) {
38 | idx = n;
39 | }
40 | });
41 | return idx;
42 | };
43 |
44 | M.nth = function (n, sel, where) {
45 | return M.find(sel, where).item(n);
46 | };
47 |
48 | M.attr = function (el, name, value) {
49 | if (!el || !el.getAttribute) {
50 | return null;
51 | }
52 | if (arguments.length === 2) {
53 | return el.getAttribute(name);
54 | }
55 | if (value === null) {
56 | return el.removeAttribute(name);
57 | }
58 | return el.setAttribute(name, value);
59 | };
60 |
61 | M.removeAttr = function (el, name) {
62 | if (el && el.removeAttribute) {
63 | el.removeAttribute(name);
64 | }
65 | };
66 |
67 | M.findSelf = function (sel, where) {
68 | return [where || document].concat(toArray(M.find(sel, where)));
69 | };
70 |
71 | M.rows = function (tbl) {
72 | if (tbl && tbl.rows) {
73 | return toArray(tbl.rows);
74 | }
75 | return [];
76 | };
77 |
78 | M.cells = function (tbl) {
79 | var ls = [];
80 |
81 | M.rows(tbl).forEach(function (tr) {
82 | ls = ls.concat(toArray(tr.cells));
83 | });
84 |
85 | return ls;
86 | };
87 |
88 | M.remove = function (els) {
89 | els.forEach(function (el) {
90 | if (el && el.parentNode) el.parentNode.removeChild(el);
91 | });
92 | };
93 |
94 | M.closest = function (el, sel) {
95 | while (el && el.matches) {
96 | if (el.matches(sel)) return el;
97 | el = el.parentNode;
98 | }
99 | return null;
100 | };
101 |
102 | M.contains = function (parent, el) {
103 | while (el) {
104 | if (el === parent) return true;
105 | el = el.parentNode;
106 | }
107 | return false;
108 | };
109 |
110 | M.bounds = function (el) {
111 | var r = el.getBoundingClientRect();
112 | return {
113 | x: r.left,
114 | y: r.top,
115 | right: r.right,
116 | bottom: r.bottom,
117 | rect: [r.left, r.top, r.right, r.bottom],
118 | };
119 | };
120 |
121 | M.offset = function (el) {
122 | var r = { x: 0, y: 0 };
123 | while (el) {
124 | r.x += el.offsetLeft;
125 | r.y += el.offsetTop;
126 | el = el.offsetParent;
127 | }
128 | return r;
129 | };
130 |
131 | M.addClass = function (el, cls) {
132 | return el && el.classList && el.classList.add(cls);
133 | };
134 |
135 | M.removeClass = function (el, cls) {
136 | return el && el.classList && el.classList.remove(cls);
137 | };
138 |
139 | M.hasClass = function (el, cls) {
140 | return el && el.classList && el.classList.contains(cls);
141 | };
142 |
143 | function _strip(s) {
144 | return String(s || "").replace(/^\s+|\s+$/g, "");
145 | }
146 |
147 | M.textContentItems = function (node) {
148 | var c = [];
149 |
150 | function walk(n) {
151 | if (!n) return;
152 |
153 | if (n.nodeType === 3) {
154 | var t = _strip(n.textContent);
155 | if (t.length) c.push(t);
156 | return;
157 | }
158 |
159 | if (!M.visible(n)) {
160 | return;
161 | }
162 |
163 | (n.childNodes || []).forEach(walk);
164 | }
165 |
166 | walk(node);
167 | return c;
168 | };
169 |
170 | M.textContent = function (node) {
171 | return _strip(M.textContentItems(node).join(" "));
172 | };
173 |
174 | M.htmlContent = function (node) {
175 | if (!node) return "";
176 | return _strip(node.innerHTML);
177 | };
178 |
179 | M.deselect = function () {
180 | var selection = window.getSelection();
181 | selection.removeAllRanges();
182 | };
183 |
184 | M.select = function (el) {
185 | var range = document.createRange();
186 | var selection = window.getSelection();
187 | selection.removeAllRanges();
188 |
189 | range.selectNodeContents(el);
190 | selection.addRange(range);
191 | };
192 |
193 | M.create = function (tag, atts) {
194 | var e = document.createElement(tag);
195 | if (atts) {
196 | Object.keys(atts).forEach(function (a) {
197 | e.setAttribute(a, atts[a]);
198 | });
199 | }
200 | return e;
201 | };
202 |
--------------------------------------------------------------------------------
/src/lib/event.js:
--------------------------------------------------------------------------------
1 | /// Basic events library
2 |
3 | var M = (module.exports = {});
4 |
5 | var lastEvent = null;
6 |
7 | M.register = function (evt) {
8 | lastEvent = evt;
9 | };
10 |
11 | M.last = function () {
12 | return lastEvent;
13 | };
14 |
15 | M.lastTarget = function () {
16 | return lastEvent ? lastEvent.target : null;
17 | };
18 |
19 | M.reset = function (evt) {
20 | evt.stopPropagation();
21 | evt.preventDefault();
22 | };
23 |
24 | M.listen = function (target, listeners) {
25 | Object.keys(listeners).forEach(function (key) {
26 | var m = key.match(/^(\w+?)(Capture)?$/);
27 | (target || document).addEventListener(m[1], listeners[key], !!m[2]);
28 | });
29 | };
30 |
31 | M.unlisten = function (target, listeners) {
32 | Object.keys(listeners).forEach(function (key) {
33 | var m = key.match(/^(\w+?)(Capture)?$/);
34 | (target || document).removeEventListener(m[1], listeners[key], !!m[2]);
35 | });
36 | };
37 |
38 | var tracker = {
39 | active: false,
40 | lastEvent: null,
41 | timer: 0,
42 | freq: 5,
43 | };
44 |
45 | M.trackMouse = function (evt, fn) {
46 | function watch() {
47 | fn("tick", tracker.lastEvent);
48 | tracker.timer = setTimeout(watch, tracker.freq);
49 | }
50 |
51 | function reset() {
52 | clearInterval(tracker.timer);
53 | tracker.active = false;
54 | M.unlisten(document, listeners);
55 | }
56 |
57 | var listeners = {
58 | mousemove: function (evt) {
59 | M.reset(evt);
60 | tracker.lastEvent = evt;
61 |
62 | if (evt.buttons === 1) {
63 | clearTimeout(tracker.timer);
64 | fn("move", tracker.lastEvent);
65 | watch();
66 | } else {
67 | reset();
68 | fn("up", evt);
69 | }
70 | },
71 |
72 | mouseup: function (evt) {
73 | M.reset(evt);
74 | tracker.lastEvent = evt;
75 | reset();
76 | fn("up", evt);
77 | },
78 | };
79 |
80 | if (tracker.active) {
81 | console.log("mouse tracker already active");
82 | reset();
83 | }
84 |
85 | tracker.active = true;
86 | console.log("mouse tracker started");
87 |
88 | M.listen(document, listeners);
89 | listeners.mousemove(evt);
90 | };
91 |
--------------------------------------------------------------------------------
/src/lib/keyboard.js:
--------------------------------------------------------------------------------
1 | /// Keyboard events and keys
2 |
3 | var M = (module.exports = {});
4 |
5 | M.mac = navigator.userAgent.indexOf("Macintosh") > 0;
6 | M.win = navigator.userAgent.indexOf("Windows") > 0;
7 |
8 | M.modifiers = {
9 | SHIFT: 1 << 10,
10 | CTRL: 1 << 11,
11 | ALT: 1 << 12,
12 | META: 1 << 13,
13 | };
14 |
15 | M.mouseModifiers = {};
16 |
17 | if (M.mac)
18 | M.mouseModifiers = [M.modifiers.SHIFT, M.modifiers.ALT, M.modifiers.META];
19 | else if (M.win)
20 | M.mouseModifiers = [M.modifiers.SHIFT, M.modifiers.ALT, M.modifiers.CTRL];
21 | else
22 | M.mouseModifiers = [
23 | M.modifiers.SHIFT,
24 | M.modifiers.ALT,
25 | M.modifiers.CTRL,
26 | M.modifiers.META,
27 | ];
28 |
29 | M.modNames = {};
30 |
31 | M.modNames[M.modifiers.CTRL] = "Ctrl";
32 | M.modNames[M.modifiers.ALT] = M.mac ? "Opt" : "Alt";
33 | M.modNames[M.modifiers.META] = M.mac ? "Cmd" : M.win ? "Win" : "Meta";
34 | M.modNames[M.modifiers.SHIFT] = "Shift";
35 |
36 | M.modHTMLNames = {};
37 |
38 | M.modHTMLNames[M.modifiers.CTRL] = M.mac
39 | ? "⌃ control"
40 | : M.modNames[M.modifiers.CTRL];
41 | M.modHTMLNames[M.modifiers.ALT] = M.mac
42 | ? "⌥ option"
43 | : M.modNames[M.modifiers.ALT];
44 | M.modHTMLNames[M.modifiers.META] = M.mac
45 | ? "⌘ command"
46 | : M.modNames[M.modifiers.META];
47 | M.modHTMLNames[M.modifiers.SHIFT] = M.mac
48 | ? "⇧ shift"
49 | : M.modNames[M.modifiers.SHIFT];
50 |
51 | M.keyNames = {
52 | 8: "Backspace",
53 | 9: "Tab",
54 | 13: "Enter",
55 | 19: "Break",
56 | 20: "Caps",
57 | 27: "Esc",
58 | 32: "Space",
59 | 33: "PgUp",
60 | 34: "PgDn",
61 | 35: "End",
62 | 36: "Home",
63 | 37: "Left",
64 | 38: "Up",
65 | 39: "Right",
66 | 40: "Down",
67 | 45: "Ins",
68 | 46: "Del",
69 | 48: "0",
70 | 49: "1",
71 | 50: "2",
72 | 51: "3",
73 | 52: "4",
74 | 53: "5",
75 | 54: "6",
76 | 55: "7",
77 | 56: "8",
78 | 57: "9",
79 | 65: "A",
80 | 66: "B",
81 | 67: "C",
82 | 68: "D",
83 | 69: "E",
84 | 70: "F",
85 | 71: "G",
86 | 72: "H",
87 | 73: "I",
88 | 74: "J",
89 | 75: "K",
90 | 76: "L",
91 | 77: "M",
92 | 78: "N",
93 | 79: "O",
94 | 80: "P",
95 | 81: "Q",
96 | 82: "R",
97 | 83: "S",
98 | 84: "T",
99 | 85: "U",
100 | 86: "V",
101 | 87: "W",
102 | 88: "X",
103 | 89: "Y",
104 | 90: "Z",
105 | 93: "Select",
106 | 96: "Num0",
107 | 97: "Num1",
108 | 98: "Num2",
109 | 99: "Num3",
110 | 100: "Num4",
111 | 101: "Num5",
112 | 102: "Num6",
113 | 103: "Num7",
114 | 104: "Num8",
115 | 105: "Num9",
116 | 106: "Num*",
117 | 107: "Num+",
118 | 109: "Num-",
119 | 110: "Num.",
120 | 111: "Num/",
121 | 112: "F1",
122 | 113: "F2",
123 | 114: "F3",
124 | 115: "F4",
125 | 116: "F5",
126 | 117: "F6",
127 | 118: "F7",
128 | 119: "F8",
129 | 120: "F9",
130 | 121: "F10",
131 | 122: "F11",
132 | 123: "F12",
133 | 144: "NumLock",
134 | 145: "ScrollLock",
135 | 186: ";",
136 | 187: "=",
137 | 188: ",",
138 | 189: "-",
139 | 190: ".",
140 | 191: "/",
141 | 192: "`",
142 | 219: "(",
143 | 220: "\\",
144 | 221: ")",
145 | 222: "'",
146 | };
147 |
148 | M.keyCode = function (name) {
149 | var code;
150 |
151 | Object.keys(M.keyNames).some(function (c) {
152 | if (M.keyNames[c] === name) {
153 | return (code = c);
154 | }
155 | });
156 |
157 | return code;
158 | };
159 |
160 | M.key = function (e) {
161 | var mods =
162 | (M.modifiers.ALT * e.altKey) |
163 | (M.modifiers.CTRL * e.ctrlKey) |
164 | (M.modifiers.META * e.metaKey) |
165 | (M.modifiers.SHIFT * e.shiftKey);
166 |
167 | var scan = e.keyCode,
168 | sname = M.keyNames[scan],
169 | mname = [],
170 | cname = [];
171 |
172 | Object.keys(M.modifiers).forEach(function (m) {
173 | if (mods & M.modifiers[m]) {
174 | mname.push(M.modNames[M.modifiers[m]]);
175 | }
176 | });
177 |
178 | mname = mname.join(" ");
179 |
180 | var r = {
181 | modifiers: { code: 0, name: "" },
182 | scan: { code: 0, name: "" },
183 | };
184 |
185 | if (mname) {
186 | r.modifiers = { code: mods, name: mname };
187 | cname.push(mname);
188 | }
189 |
190 | if (sname) {
191 | r.scan = { code: scan, name: sname };
192 | cname.push(sname);
193 | }
194 |
195 | r.code = mods | scan;
196 | r.name = cname.join(" ");
197 |
198 | return r;
199 | };
200 |
--------------------------------------------------------------------------------
/src/lib/matrix.js:
--------------------------------------------------------------------------------
1 | /// Matrix manipulations
2 |
3 | var M = (module.exports = {});
4 |
5 | M.column = function (mat, ci) {
6 | return mat.map(function (row) {
7 | return row[ci];
8 | });
9 | };
10 |
11 | M.transpose = function (mat) {
12 | if (!mat.length) return mat;
13 | return mat[0].map(function (_, ci) {
14 | return M.column(mat, ci);
15 | });
16 | };
17 |
18 | M.trim = function (mat, fn) {
19 | var fun = function (row) {
20 | return row.some(function (cell) {
21 | return fn(cell);
22 | });
23 | };
24 |
25 | mat = mat.filter(fun);
26 | mat = M.transpose(mat).filter(fun);
27 | return M.transpose(mat);
28 | };
29 |
30 | M.each = function (mat, fn) {
31 | mat.forEach(function (row, ri) {
32 | row.forEach(function (cell, ci) {
33 | fn(row, cell, ri, ci);
34 | });
35 | });
36 | };
37 |
38 | M.map = function (mat, fn) {
39 | return mat.map(function (row, ri) {
40 | return row.map(function (cell, ci) {
41 | return fn(row, cell, ri, ci);
42 | });
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/src/lib/message.js:
--------------------------------------------------------------------------------
1 | /// Wrappers for browser.sendMessage
2 |
3 | var M = (module.exports = {});
4 |
5 | var util = require("./util");
6 |
7 | function convertMessage(msg) {
8 | if (typeof msg !== "object") {
9 | return { name: msg };
10 | }
11 | return msg;
12 | }
13 |
14 | function convertSender(sender) {
15 | if (!sender) {
16 | return {};
17 | }
18 | if (typeof sender !== "object") {
19 | return sender;
20 | }
21 | var k = Object.keys(sender);
22 | if (k.length === 1 && k[0] === "id") {
23 | return "background";
24 | }
25 | if (sender.tab) {
26 | var p = Object.assign({}, sender);
27 | p.tabId = sender.tab.id;
28 | return p;
29 | }
30 | return sender;
31 | }
32 |
33 | function toBackground(msg) {
34 | msg.to = "background";
35 | return util.callBrowserAsync("runtime.sendMessage", msg).then(function (res) {
36 | return { receiver: "background", data: res };
37 | });
38 | }
39 |
40 | function toFrame(msg, frame) {
41 | msg.to = frame;
42 | return util
43 | .callBrowserAsync("tabs.sendMessage", frame.tabId, msg, {
44 | frameId: frame.frameId,
45 | })
46 | .then(function (res) {
47 | return { receiver: frame, data: res };
48 | });
49 | }
50 |
51 | function toFrameList(msg, frames) {
52 | return Promise.all(
53 | frames.map(function (frame) {
54 | return toFrame(msg, frame);
55 | })
56 | );
57 | }
58 |
59 | M.enumFrames = function (tabFilter) {
60 | function framesInTab(tab) {
61 | return util
62 | .callBrowserAsync("webNavigation.getAllFrames", { tabId: tab.id })
63 | .then(function (frames) {
64 | if (!frames) {
65 | // Vivaldi, as of 1.8.770.56, doesn't support getAllFrames() properly
66 | // let's pretend there's only one top frame
67 | frames = [
68 | {
69 | errorOccurred: false,
70 | frameId: 0,
71 | parentFrameId: -1,
72 | },
73 | ];
74 | }
75 | return frames.map(function (f) {
76 | f.tabId = tab.id;
77 | return f;
78 | });
79 | });
80 | }
81 |
82 | if (tabFilter === "active") {
83 | tabFilter = { active: true, currentWindow: true };
84 | }
85 |
86 | return util
87 | .callBrowserAsync("tabs.query", tabFilter || {})
88 | .then(function (tabs) {
89 | return Promise.all(tabs.map(framesInTab));
90 | })
91 | .then(function (res) {
92 | return util.flatten(res);
93 | });
94 | };
95 |
96 | M.background = function (msg) {
97 | console.log("MESSAGE: background", msg);
98 | return toBackground(convertMessage(msg));
99 | };
100 |
101 | M.frame = function (msg, frame) {
102 | console.log("MESSAGE: frame", msg, frame);
103 | return toFrame(convertMessage(msg), frame);
104 | };
105 |
106 | M.allFrames = function (msg) {
107 | console.log("MESSAGE: allFrames", msg);
108 | msg = convertMessage(msg);
109 | return M.enumFrames("active").then(function (frames) {
110 | return toFrameList(msg, frames);
111 | });
112 | };
113 |
114 | M.topFrame = function (msg) {
115 | console.log("MESSAGE: topFrame", msg);
116 | msg = convertMessage(msg);
117 | return M.enumFrames("active").then(function (frames) {
118 | var top = frames.filter(function (f) {
119 | return f.frameId === 0;
120 | });
121 | if (top.length) {
122 | return toFrame(msg, top[0]);
123 | }
124 | });
125 | };
126 |
127 | M.broadcast = function (msg) {
128 | console.log("MESSAGE: broadcast", msg);
129 | msg = convertMessage(msg);
130 | return M.enumFrames().then(function (frames) {
131 | return toFrameList(msg, frames);
132 | });
133 | };
134 |
135 | M.listen = function (listeners) {
136 | util.callBrowser("runtime.onMessage.addListener", function (msg, sender, fn) {
137 | if (listeners[msg.name]) {
138 | msg.sender = convertSender(sender);
139 | var res = listeners[msg.name](msg);
140 | return fn(res);
141 | }
142 | console.log("LOST", msg.name);
143 | });
144 | };
145 |
--------------------------------------------------------------------------------
/src/lib/number.js:
--------------------------------------------------------------------------------
1 | /// Tools to work with numbers
2 |
3 | var M = (module.exports = {});
4 |
5 | function isDigit(s) {
6 | return s.match(/^\d+$/);
7 | }
8 |
9 | function parseInteger(s) {
10 | if (!isDigit(s)) {
11 | return null;
12 | }
13 | var n = Number(s);
14 | return isDigit(String(n)) ? n : null;
15 | }
16 |
17 | function parseFraction(s) {
18 | var n = parseInteger("1" + s);
19 | return n === null ? null : String(n).slice(1);
20 | }
21 |
22 | function parseGrouped(s, fmt) {
23 | if (!fmt.group || s.indexOf(fmt.group) < 0) {
24 | return parseInteger(s);
25 | }
26 |
27 | var g = "\\" + fmt.group;
28 | var re = new RegExp("^\\d{1,3}(" + g + "\\d{2,3})*$");
29 |
30 | if (!s.match(re)) {
31 | return null;
32 | }
33 |
34 | return parseInteger(s.replace(/\D+/g, ""));
35 | }
36 |
37 | M.parse = function (s, fmt) {
38 | if (s[0] === "-") {
39 | var n = M.parse(s.slice(1), fmt);
40 | return n === null ? null : -n;
41 | }
42 |
43 | if (!fmt.decimal || s.indexOf(fmt.decimal) < 0) {
44 | return parseGrouped(s, fmt);
45 | }
46 |
47 | if (s === fmt.decimal) {
48 | return null;
49 | }
50 |
51 | var ds = s.split(fmt.decimal);
52 |
53 | if (ds.length === 1) {
54 | return parseGrouped(ds[0], fmt);
55 | }
56 |
57 | if (ds.length === 2) {
58 | var a = ds[0].length ? parseGrouped(ds[0], fmt) : 0;
59 | var b = ds[1].length ? parseFraction(ds[1]) : 0;
60 |
61 | if (a === null || b === null) {
62 | return null;
63 | }
64 |
65 | return Number(a + "." + b);
66 | }
67 |
68 | return null;
69 | };
70 |
71 | M.extract = function (text, fmt) {
72 | if (!text) {
73 | return null;
74 | }
75 |
76 | text = String(text).replace(/^\s+|\s+$/g, "");
77 | if (!text) {
78 | return null;
79 | }
80 |
81 | var g = fmt.group ? "\\" + fmt.group : "";
82 | var d = fmt.decimal ? "\\" + fmt.decimal : "";
83 |
84 | var re = new RegExp("-?[\\d" + g + d + "]*\\d", "g");
85 | var m = text.match(re);
86 |
87 | if (!m || m.length !== 1) {
88 | return null;
89 | }
90 |
91 | var n = M.parse(m[0], fmt);
92 | if (n === null) {
93 | return null;
94 | }
95 |
96 | return n;
97 | };
98 |
99 | M.defaultFormat = function () {
100 | var f = { group: ",", decimal: "." };
101 |
102 | try {
103 | // Intl and formatToParts might not be available...
104 |
105 | var nf = new Intl.NumberFormat();
106 |
107 | nf.formatToParts(123456.78).forEach(function (p) {
108 | if (String(p.type) === "group") f.group = p.value;
109 | if (String(p.type) === "decimal") f.decimal = p.value;
110 | });
111 | return f;
112 | } catch (e) {}
113 |
114 | try {
115 | var s = (123456.78).toLocaleString().replace(/\d/g, ""),
116 | len = s.length;
117 |
118 | f.decimal = len > 0 ? s[len - 1] : ".";
119 | f.group = len > 1 ? s[len - 2] : "";
120 | return f;
121 | } catch (e) {}
122 |
123 | return f;
124 | };
125 |
126 | M.format = function (n, prec) {
127 | n = prec ? Number(n.toFixed(prec)) : Number(n);
128 | return (n || 0).toLocaleString();
129 | };
130 |
--------------------------------------------------------------------------------
/src/lib/preferences.js:
--------------------------------------------------------------------------------
1 | /// Preferences, stored in browser.storage
2 |
3 | var M = (module.exports = {});
4 |
5 | var keyboard = require("./keyboard"),
6 | number = require("./number"),
7 | util = require("./util");
8 |
9 | var firstMod = keyboard.modifiers.ALT,
10 | secondMod = keyboard.mac ? keyboard.modifiers.META : keyboard.modifiers.CTRL;
11 |
12 | var defaults = {
13 | "modifier.cell": firstMod,
14 | "modifier.column": firstMod | secondMod,
15 | "modifier.row": 0,
16 | "modifier.table": 0,
17 | "modifier.extend": keyboard.modifiers.SHIFT,
18 |
19 | "capture.enabled": true,
20 | "capture.reset": false,
21 |
22 | "scroll.amount": 30,
23 | "scroll.acceleration": 5,
24 |
25 | "copy.format.enabled.richHTMLCSS": true,
26 | "copy.format.enabled.richHTML": true,
27 | "copy.format.enabled.textTabs": true,
28 | "copy.format.enabled.textTabsSwap": true,
29 | "copy.format.enabled.textCSV": true,
30 | "copy.format.enabled.textCSVSwap": true,
31 | "copy.format.enabled.textHTMLCSS": true,
32 | "copy.format.enabled.textHTML": true,
33 | "copy.format.enabled.textTextile": true,
34 |
35 | "copy.format.default.richHTMLCSS": true,
36 |
37 | "infobox.enabled": true,
38 | "infobox.position": "0",
39 | };
40 |
41 | var captureModes = [
42 | {
43 | id: "zzz",
44 | name: "Off",
45 | },
46 | {
47 | id: "cell",
48 | name: "Cells",
49 | },
50 | {
51 | id: "column",
52 | name: "Columns",
53 | },
54 | {
55 | id: "row",
56 | name: "Rows",
57 | },
58 | {
59 | id: "table",
60 | name: "Tables",
61 | },
62 | ];
63 |
64 | var copyFormats = [
65 | {
66 | id: "richHTMLCSS",
67 | name: "As is",
68 | desc: "Copy the table as seen on the screen",
69 | },
70 | {
71 | id: "richHTML",
72 | name: "Plain Table",
73 | desc: "Copy the table without formatting",
74 | },
75 | {
76 | id: "textTabs",
77 | name: "Text",
78 | desc: "Copy as tab-delimited text",
79 | },
80 | {
81 | id: "textTabsSwap",
82 | name: "Text+Swap",
83 | desc: "Copy as tab-delimited text, swap columns and rows",
84 | },
85 | {
86 | id: "textCSV",
87 | name: "CSV",
88 | desc: "Copy as comma-separated text",
89 | },
90 | {
91 | id: "textCSVSwap",
92 | name: "CSV+Swap",
93 | desc: "Copy as comma-separated text, swap columns and rows",
94 | },
95 | {
96 | id: "textHTMLCSS",
97 | name: "HTML+CSS",
98 | desc: "Copy as HTML source, retain formatting",
99 | },
100 | {
101 | id: "textHTML",
102 | name: "HTML",
103 | desc: "Copy as HTML source, without formatting",
104 | },
105 | {
106 | id: "textTextile",
107 | name: "Textile",
108 | desc: "Copy as Textile (Text content)",
109 | },
110 | ];
111 |
112 | function sum(vs) {
113 | return vs.reduce(function (x, y) {
114 | return x + (Number(y) || 0);
115 | }, 0);
116 | }
117 |
118 | function getNumbers(values) {
119 | var vs = [];
120 |
121 | values.forEach(function (v) {
122 | if (v.isNumber) vs.push(v.number);
123 | });
124 |
125 | return vs.length ? vs : null;
126 | }
127 |
128 | var infoFunctions = [
129 | {
130 | name: "count",
131 | fn: function (values) {
132 | return values.length;
133 | },
134 | },
135 | {
136 | name: "sum",
137 | fn: function (values) {
138 | var vs = getNumbers(values);
139 | return vs ? number.format(sum(vs)) : null;
140 | },
141 | },
142 | {
143 | name: "average",
144 | fn: function (values) {
145 | var vs = getNumbers(values);
146 | return vs ? number.format(sum(vs) / vs.length, 2) : null;
147 | },
148 | },
149 | {
150 | name: "min",
151 | fn: function (values) {
152 | var vs = getNumbers(values);
153 | return vs ? number.format(Math.min.apply(Math, vs)) : null;
154 | },
155 | },
156 | {
157 | name: "max",
158 | fn: function (values) {
159 | var vs = getNumbers(values);
160 | return vs ? number.format(Math.max.apply(Math, vs)) : null;
161 | },
162 | },
163 | ];
164 |
165 | var prefs = {};
166 |
167 | function _constrain(min, val, max) {
168 | val = Number(val) || min;
169 | return Math.max(min, Math.min(val, max));
170 | }
171 |
172 | M.load = function () {
173 | return util.callBrowserAsync("storage.local.get", null).then(function (obj) {
174 | obj = obj || {};
175 |
176 | // from the previous version
177 | if ("modKey" in obj && String(obj.modKey) === "1") {
178 | console.log("FOUND ALTERNATE MODKEY SETTING");
179 | obj["modifier.cell"] = secondMod;
180 | delete obj.modKey;
181 | }
182 |
183 | prefs = Object.assign({}, defaults, prefs, obj);
184 |
185 | prefs["scroll.amount"] = _constrain(1, prefs["scroll.amount"], 100);
186 | prefs["scroll.acceleration"] = _constrain(
187 | 0,
188 | prefs["scroll.acceleration"],
189 | 100
190 | );
191 |
192 | if (!prefs["number.group"]) {
193 | prefs["number.group"] = number.defaultFormat().group;
194 | }
195 |
196 | if (!prefs["number.decimal"]) {
197 | prefs["number.decimal"] = number.defaultFormat().decimal;
198 | }
199 |
200 | console.log("PREFS LOAD", prefs);
201 | return prefs;
202 | });
203 | };
204 |
205 | M.save = function () {
206 | return util
207 | .callBrowserAsync("storage.local.clear")
208 | .then(function () {
209 | return util.callBrowserAsync("storage.local.set", prefs);
210 | })
211 | .then(function () {
212 | console.log("PREFS SET", prefs);
213 | return prefs;
214 | });
215 | };
216 |
217 | M.setAll = function (obj) {
218 | prefs = Object.assign({}, prefs, obj);
219 | return M.save();
220 | };
221 |
222 | M.set = function (key, val) {
223 | prefs[key] = val;
224 | return M.save();
225 | };
226 |
227 | M.val = function (key) {
228 | return prefs[key];
229 | };
230 |
231 | M.int = function (key) {
232 | return Number(M.val(key)) || 0;
233 | };
234 |
235 | M.copyFormats = function () {
236 | return copyFormats.map(function (f) {
237 | f.enabled = !!M.val("copy.format.enabled." + f.id);
238 | f.default = !!M.val("copy.format.default." + f.id);
239 | return f;
240 | });
241 | };
242 |
243 | M.numberFormat = function () {
244 | var g = M.val("number.group");
245 | var d = M.val("number.decimal");
246 |
247 | if (!g && !d) {
248 | return number.defaultFormat();
249 | }
250 |
251 | return {
252 | group: g || "",
253 | decimal: d || "",
254 | };
255 | };
256 |
257 | M.infoFunctions = function () {
258 | return infoFunctions;
259 | };
260 |
261 | M.captureModes = function () {
262 | return captureModes;
263 | };
264 |
--------------------------------------------------------------------------------
/src/lib/util.js:
--------------------------------------------------------------------------------
1 | // Utility functions.
2 |
3 | var M = (module.exports = {});
4 |
5 | M.numeric = function (a, b) {
6 | return a - b;
7 | };
8 |
9 | M.toArray = function (coll) {
10 | return Array.prototype.slice.call(coll || [], 0);
11 | };
12 |
13 | M.flatten = function (a) {
14 | while (a.some(Array.isArray)) a = Array.prototype.concat.apply([], a);
15 | return a;
16 | };
17 |
18 | M.first = function (a, fn) {
19 | for (var i = 0; i < a.length; i++) {
20 | if (fn(a[i], i)) {
21 | return a[i];
22 | }
23 | }
24 | return null;
25 | };
26 |
27 | M.intersect = function (a, b) {
28 | return !(a[0] >= b[2] || a[2] <= b[0] || a[1] >= b[3] || a[3] <= b[1]);
29 | };
30 |
31 | M.lstrip = function (s) {
32 | return s.replace(/^\s+/, "");
33 | };
34 |
35 | M.rstrip = function (s) {
36 | return s.replace(/\s+$/, "");
37 | };
38 |
39 | M.strip = function (s) {
40 | return s.replace(/^\s+|\s+$/g, "");
41 | };
42 |
43 | M.reduceWhitespace = function (html) {
44 | return html
45 | .replace(/\n\r/g, "\n")
46 | .replace(/\n[ ]+/g, "\n")
47 | .replace(/[ ]+\n/g, "\n")
48 | .replace(/\n+/g, "\n");
49 | };
50 |
51 | M.uid = function (len) {
52 | var s = "";
53 | while (len--) {
54 | s += String.fromCharCode(97 + Math.floor(Math.random() * 26));
55 | }
56 | return s;
57 | };
58 |
59 | M.nobr = function (s) {
60 | return s.replace(/[\r\n]+/g, " ");
61 | };
62 |
63 | M.format = function (s, obj) {
64 | return s.replace(/\${(\w+)}/g, function (_, $1) {
65 | return obj[$1];
66 | });
67 | };
68 |
69 | var _times = {};
70 |
71 | M.timeStart = function (name) {
72 | _times[name] = new Date();
73 | return "TIME START: " + name;
74 | };
75 |
76 | M.timeEnd = function (name) {
77 | if (_times[name]) {
78 | var t = new Date() - _times[name];
79 | delete _times[name];
80 | return "TIME END: " + name + " " + t;
81 | }
82 | };
83 |
84 | function callBrowser(useAsync, fn, args) {
85 | var parts = fn.split("."),
86 | obj = browser,
87 | method = parts.pop();
88 |
89 | parts.forEach(function (p) {
90 | obj = obj[p];
91 | });
92 |
93 | console.log("CALL_BROWSER", useAsync, fn);
94 |
95 | if (!useAsync) {
96 | try {
97 | return obj[method].apply(obj, args);
98 | } catch (err) {
99 | console.log("CALL_BROWSER_ERROR", fn, err.message);
100 | return null;
101 | }
102 | }
103 |
104 | return new Promise(function (resolve, reject) {
105 | try {
106 | obj[method].apply(obj, args).then(
107 | function (res) {
108 | resolve(res);
109 | },
110 | function (err) {
111 | console.log("CALL_BROWSER_LAST_ERROR", fn, err);
112 | resolve(null);
113 | }
114 | );
115 | } catch (err) {
116 | console.log("CALL_BROWSER_ERROR", fn, err.message);
117 | resolve(null);
118 | }
119 | });
120 | }
121 |
122 | M.callBrowser = function (fn) {
123 | return callBrowser(false, fn, [].slice.call(arguments, 1));
124 | };
125 |
126 | M.callBrowserAsync = function (fn) {
127 | return callBrowser(true, fn, [].slice.call(arguments, 1));
128 | };
129 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "CopyTables",
4 | "version": "0.1.1",
5 | "homepage_url": "https://github.com/nirantak/copytables",
6 | "description": "Select table cells, rows and columns with your mouse or from a context menu. Copy as rich text, HTML, CSV and TSV",
7 | "author": "Nirantak Raghav",
8 | "content_scripts": [
9 | {
10 | "js": ["content.js"],
11 | "css": ["content.css"],
12 | "matches": ["*://*/*", "file://*/*"],
13 | "all_frames": true
14 | }
15 | ],
16 | "background": {
17 | "scripts": ["background.js"],
18 | "persistent": false
19 | },
20 | "permissions": [
21 | "clipboardRead",
22 | "clipboardWrite",
23 | "contextMenus",
24 | "webNavigation",
25 | "storage"
26 | ],
27 | "icons": {
28 | "16": "ico16.png",
29 | "48": "ico48.png",
30 | "128": "ico128.png"
31 | },
32 | "browser_action": {
33 | "default_icon": {
34 | "19": "ico19.png",
35 | "38": "ico38.png"
36 | },
37 | "default_title": "CopyTables",
38 | "default_popup": "popup.html"
39 | },
40 | "options_ui": {
41 | "page": "options.html",
42 | "open_in_tab": true
43 | },
44 | "commands": {
45 | "capture_cell": {
46 | "description": "Capture cells"
47 | },
48 | "capture_column": {
49 | "description": "Capture columns"
50 | },
51 | "capture_row": {
52 | "description": "Capture rows"
53 | },
54 | "capture_zzz": {
55 | "description": "Turn off the capture"
56 | },
57 | "capture_table": {
58 | "description": "Capture tables"
59 | },
60 | "find_previous": {
61 | "description": "Find previous table"
62 | },
63 | "find_next": {
64 | "description": "Find next table"
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/options.js:
--------------------------------------------------------------------------------
1 | var dom = require("./lib/dom"),
2 | preferences = require("./lib/preferences"),
3 | keyboard = require("./lib/keyboard"),
4 | event = require("./lib/event"),
5 | message = require("./lib/message"),
6 | util = require("./lib/util");
7 |
8 | var mouseButtonNames = [
9 | "Left Button",
10 | "Middle/Wheel",
11 | "Right Button",
12 | "Button 3",
13 | "Button 4",
14 | ];
15 |
16 | function modifiers(mode) {
17 | var tpl = [
18 | '',
19 | ' ',
20 | " ${name} ",
21 | " ",
22 | ].join("");
23 |
24 | var kmod = preferences.val("modifier." + mode);
25 |
26 | return keyboard.mouseModifiers
27 | .map(function (m) {
28 | return util.format(tpl, {
29 | code: m,
30 | name: keyboard.modHTMLNames[m],
31 | mode: mode,
32 | checked: kmod & m ? "checked" : "",
33 | });
34 | })
35 | .join("");
36 | }
37 |
38 | function copyFormats() {
39 | var tpl = [
40 | "
",
41 | " ${name} ${desc} ",
42 | ' ',
43 | ' Enabled ' +
44 | " ",
45 | ' ',
46 | ' Default ',
47 | " ",
48 | " ",
49 | ].join("");
50 |
51 | var s = preferences.copyFormats().map(function (f) {
52 | return util.format(tpl, f);
53 | });
54 |
55 | return "";
56 | }
57 |
58 | function load() {
59 | dom.findOne("#copy-formats").innerHTML = copyFormats();
60 |
61 | dom.find("[data-modifiers]").forEach(function (el) {
62 | el.innerHTML = modifiers(dom.attr(el, "data-modifiers"));
63 | });
64 |
65 | dom.find("[data-bool]").forEach(function (el) {
66 | el.checked = !!preferences.val(dom.attr(el, "data-bool"));
67 | });
68 |
69 | dom.find("[data-select]").forEach(function (el) {
70 | el.checked =
71 | preferences.val(dom.attr(el, "data-select")) ===
72 | dom.attr(el, "data-select-value");
73 | });
74 |
75 | dom.find("[data-text]").forEach(function (el) {
76 | var t = preferences.val(dom.attr(el, "data-text"));
77 | el.value = typeof t === "undefined" ? "" : t;
78 | });
79 | }
80 |
81 | function save() {
82 | var prefs = {};
83 |
84 | dom.find("[data-modifier]").forEach(function (el) {
85 | var code = Number(dom.attr(el, "data-modifier")),
86 | mode = dom.attr(el, "data-mode");
87 | prefs["modifier." + mode] =
88 | (prefs["modifier." + mode] || 0) | (el.checked ? code : 0);
89 | });
90 |
91 | dom.find("[data-bool]").forEach(function (el) {
92 | prefs[dom.attr(el, "data-bool")] = !!el.checked;
93 | });
94 |
95 | dom.find("[data-select]").forEach(function (el) {
96 | if (el.checked)
97 | prefs[dom.attr(el, "data-select")] = dom.attr(el, "data-select-value");
98 | });
99 |
100 | dom.find("[data-text]").forEach(function (el) {
101 | prefs[dom.attr(el, "data-text")] = el.value;
102 | });
103 |
104 | preferences.setAll(prefs).then(load);
105 | message.background("preferencesUpdated");
106 | }
107 |
108 | window.onload = function () {
109 | preferences.load().then(load);
110 |
111 | event.listen(window, {
112 | input: save,
113 | change: save,
114 | click: function (e) {
115 | var cmd = dom.attr(e.target, "data-command");
116 | if (cmd) {
117 | message.background({ name: "command", command: cmd });
118 | event.reset(e);
119 | }
120 | },
121 | });
122 | };
123 |
--------------------------------------------------------------------------------
/src/options.pug:
--------------------------------------------------------------------------------
1 | mixin mousepad(name)
2 | label.mousepad
3 | input(data-text=name)
4 | span
5 |
6 | mixin modifier(code, mode)
7 | label.sticky
8 | input(type="checkbox" data-modifier=code data-mode=mode)
9 | span(data-modifier-name=code)
10 |
11 | //- see keyboard.modifiers
12 | mixin modifiers(mode)
13 | +modifier(1 << 10, mode)
14 | +modifier(1 << 11, mode)
15 | +modifier(1 << 12, mode)
16 | +modifier(1 << 13, mode)
17 |
18 | doctype html
19 | html(lang="en")
20 | head
21 | title CopyTables Options
22 | meta(charset="utf-8")
23 | meta(http-equiv="Content-type", content="text/html; charset=utf-8")
24 | meta(name="viewport", content="width=device-width, initial-scale=1")
25 | script(src="options.js")
26 | link(rel="stylesheet", type="text/css", href="options.css")
27 |
28 | body
29 |
30 | h1 CopyTables Options
31 |
32 | h2 Modifier Keys
33 | h3
34 | | Configure modifier keys for click-and-drag selection.
35 | | Note: some combinations might conflict with your system setup.
36 |
37 | .box
38 | table
39 | tr
40 | td
41 | b To select cells, hold
42 | td(data-modifiers="cell")
43 | tr
44 | td
45 | b To select columns, hold
46 | td(data-modifiers="column")
47 | tr
48 | td
49 | b To select rows, hold
50 | td(data-modifiers="row")
51 | tr
52 | td
53 | b To select tables, hold
54 | td(data-modifiers="table")
55 | tr
56 | td
57 | b To extend a selection, hold
58 | td(data-modifiers="extend")
59 |
60 |
61 | h2 Capture Mode
62 | h3
63 | | In the capture mode, you press a hot key first and then select with a single click.
64 |
65 | .box
66 | table
67 | tr
68 | td: b Enabled
69 | td
70 | label.cb
71 | input(type="checkbox" data-bool="capture.enabled")
72 | span Yes
73 | tr
74 | td
75 | b Reset
76 | i Automatically turn the capture mode off after selecting
77 | td
78 | label.cb
79 | input(type="checkbox" data-bool="capture.reset")
80 | span Yes
81 | tr
82 | td: b Keyboard Shortcuts
83 | td Configure by going to about:addons → Tools → Manage Extension Shortcuts
84 |
85 |
86 | h2 Copy Formats
87 | h3 Select copy formats you want to use. "Default" format is used when you select "Copy" from the Firefox menu.
88 |
89 | .box#copy-formats
90 |
91 | h2 Infobox
92 | h3 Infobox shows useful information about your selection (sum/average of numeric values etc).
93 |
94 | .box
95 | table
96 | tr
97 | td: b Enabled
98 | td
99 | label.cb
100 | input(type="checkbox" data-bool="infobox.enabled")
101 | span Yes
102 | tr
103 | td
104 | b Sticky
105 | i Keep infobox on screen until closed
106 | td
107 | label.cb
108 | input(type="checkbox" data-bool="infobox.sticky")
109 | span Yes
110 | tr
111 | td: b Position
112 | td
113 | table.infobox-position
114 | tr
115 | td
116 | div.position0: span
117 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="0")
118 | td
119 | div.position3: span
120 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="3")
121 | td
122 | div.position1: span
123 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="1")
124 | td
125 | div.position2: span
126 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="2")
127 |
128 | h2 Numbers
129 | h3 Configure how CopyTables parses numbers.
130 |
131 | .box
132 | table
133 | tr
134 | td
135 | b Decimal point
136 | td
137 | input(type="text" data-text="number.decimal")
138 | tr
139 | td
140 | b Group separator
141 | td
142 | input(type="text" data-text="number.group")
143 |
144 |
145 |
146 | h2 Interface
147 | h3 Miscellaneous interface options.
148 |
149 | .box
150 | table
151 | tr
152 | td
153 | b Scroll speed
154 | i Values are from 1 (very slow scrolling) to 100 (very fast).
155 | td
156 | input(type="number" data-text="scroll.amount")
157 | tr
158 | td
159 | b Scroll acceleration
160 | i Values are from 0 (no acceleration) to 100 (max acceleration).
161 | td
162 | input(type="number" data-text="scroll.acceleration")
163 |
164 |
165 | h2 Support
166 |
167 | .box
168 |
169 | h4
170 | Contact me if you have questions or want to report a problem.
171 |
172 | h4
173 | a(href="https://github.com/nirantak/copytables") Source Code
174 | | |
175 | | © (2021 - present) Nirantak Raghav
176 |
--------------------------------------------------------------------------------
/src/options.sass:
--------------------------------------------------------------------------------
1 | @import "colors"
2 |
3 | html
4 | margin: 0
5 | padding: 0
6 |
7 | body
8 | padding: 30px 30px 70px 30px
9 |
10 | body, td, th
11 | font-family: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif
12 | font-size: 12px
13 | color: $body
14 |
15 | h1
16 | font-size: 24px
17 | background: url('ico48.png') no-repeat
18 | font-weight: 300
19 | padding: 10px 0 10px 68px
20 | margin: 0
21 |
22 | h2
23 | color: $hot
24 | margin: 36px 0 6px 70px
25 | font-size: 18px
26 | font-weight: 300
27 |
28 | h3
29 | margin: 0 0 0 70px
30 | font-size: 12px
31 | font-weight: 300
32 | max-width: 600px
33 | line-height: 140%
34 |
35 | h4
36 | margin: 0 0 12px 0
37 | font-size: 12px
38 | font-weight: 300
39 | max-width: 600px
40 | line-height: 140%
41 |
42 | .box
43 | padding: 12px 0 0 100px
44 |
45 | a
46 | color: $accent
47 | cursor: pointer
48 |
49 | p
50 | margin: 6px 0
51 |
52 | table
53 | border-collapse: collapse
54 |
55 | td
56 | margin: 0
57 | padding: 12px
58 | font-weight: 400
59 | text-align: left
60 |
61 | td b
62 | padding-right: 12px
63 | font-weight: 600
64 | white-space: nowrap
65 |
66 | td u
67 | margin: 0 6px
68 | font-weight: 800
69 | text-decoration: none
70 | background: $link
71 | color: white
72 | padding: 0 4px
73 | border-radius: 8px
74 |
75 | td i
76 | display: block
77 | font-size: 11px
78 | max-width: 220px
79 | padding: 0 12px 0 0
80 |
81 | td:first-child
82 | width: 200px
83 |
84 | tr
85 | border-bottom: 1px solid $lite
86 |
87 | tr tr
88 | border-bottom: none
89 |
90 | .sticky
91 | margin-right: 6px
92 | user-select: none
93 | font-size: 11px
94 |
95 | span
96 | background: white
97 | color: $link
98 | border: 1px solid $link
99 | border-radius: 8px
100 | padding: 4px 8px
101 | cursor: pointer
102 | white-space: nowrap
103 |
104 | &:hover span
105 | color: $accent
106 | border: 1px solid $accent
107 |
108 | input:checked + span
109 | color: white
110 | background: $accent
111 | border: 1px solid $accent
112 |
113 | input
114 | display: none
115 |
116 | .mousepad
117 | display: inline-block
118 | margin-left: 6px
119 | text-align: center
120 | width: 80px
121 | user-select: none
122 | font-size: 11px
123 | background: $accent
124 | border: 1px solid $accent
125 | color: white
126 | border-radius: 8px
127 | padding: 5px 5px
128 | transition: all 0.2s ease
129 |
130 | span
131 | cursor: pointer
132 | white-space: nowrap
133 |
134 |
135 | &:hover
136 | background-color: black
137 |
138 | input
139 | display: none
140 |
141 |
142 | .cb
143 | display: block
144 | span
145 | visibility: hidden
146 | padding-left: 4px
147 |
148 | input:checked + span
149 | color: $accent
150 | visibility: visible
151 |
152 | .infobox-position
153 | td
154 | text-align: center
155 |
156 | td:first-child
157 | width: auto
158 |
159 | div
160 | width: 50px
161 | height: 30px
162 | border: 1px solid $accent
163 | position: relative
164 | margin-bottom: 5px
165 |
166 | span
167 |
168 | width: 25px
169 | height: 8px
170 | background: $accent
171 | position: absolute
172 |
173 | &.position0 span
174 | left: 1px
175 | top: 1px
176 |
177 | &.position1 span
178 | right: 1px
179 | top: 1px
180 |
181 | &.position2 span
182 | right: 1px
183 | bottom: 1px
184 |
185 | &.position3 span
186 | left: 1px
187 | bottom: 1px
188 |
189 | #custom-code
190 | width: 400px
191 | height: 200px
192 | font-family: "Monaco", "UbuntuMono", monospace
193 | font-size: 11px
194 | border: 1px solid $border
195 |
--------------------------------------------------------------------------------
/src/popup.js:
--------------------------------------------------------------------------------
1 | var dom = require("./lib/dom"),
2 | preferences = require("./lib/preferences"),
3 | message = require("./lib/message"),
4 | event = require("./lib/event"),
5 | util = require("./lib/util");
6 |
7 | function captureButtons() {
8 | var mode = preferences.val("_captureMode") || "zzz";
9 |
10 | return preferences
11 | .captureModes()
12 | .map(function (m) {
13 | return util.format(
14 | '${name} ',
15 | {
16 | id: m.id,
17 | name: m.name,
18 | cls: m.id === mode ? "on" : "",
19 | }
20 | );
21 | })
22 | .join("");
23 | }
24 |
25 | function copyButtons() {
26 | return preferences
27 | .copyFormats()
28 | .filter(function (f) {
29 | return f.enabled;
30 | })
31 | .map(function (f) {
32 | return util.format(
33 | '${name} ',
34 | f
35 | );
36 | })
37 | .join("");
38 | }
39 |
40 | function update() {
41 | dom.findOne("#copy-buttons").innerHTML = copyButtons();
42 |
43 | if (preferences.val("capture.enabled")) {
44 | dom.findOne("#capture-row").style.display = "";
45 | dom.findOne("#capture-buttons").innerHTML = captureButtons();
46 | } else {
47 | dom.findOne("#capture-row").style.display = "none";
48 | }
49 | }
50 |
51 | function init() {
52 | update();
53 |
54 | event.listen(document, {
55 | click: function (e) {
56 | var cmd = dom.attr(e.target, "data-command");
57 | if (cmd) {
58 | message.background({ name: "command", command: cmd });
59 | }
60 | if (!dom.attr(e.target, "data-keep-open")) {
61 | window.close();
62 | }
63 | },
64 | });
65 | }
66 |
67 | window.onload = function () {
68 | preferences.load().then(init);
69 | };
70 |
--------------------------------------------------------------------------------
/src/popup.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang="en")
3 | head
4 | meta(charset="utf-8")
5 | script(src="popup.js")
6 | link(rel="stylesheet", type="text/css", href="popup.css")
7 |
8 | body
9 | table
10 | tr
11 | td
12 | b Copy:
13 | td
14 | span#copy-buttons
15 |
16 | tr
17 | td
18 | b Find:
19 | td
20 | button(data-command="find_previous" data-keep-open="1") Previous Table
21 | button(data-command="find_next" data-keep-open="1") Next Table
22 |
23 | tr#capture-row
24 | td
25 | b Capture:
26 | td
27 | span#capture-buttons
28 |
29 | div
30 | p
31 | a(data-command="open_options") Options
32 |
--------------------------------------------------------------------------------
/src/popup.sass:
--------------------------------------------------------------------------------
1 | @import "colors"
2 |
3 | html
4 | margin: 0
5 | padding: 0
6 |
7 | body
8 | padding: 0
9 | min-width: 390px
10 | max-width: 600px
11 |
12 | body, td, th, button
13 | font-family: Arial, sans-serif
14 | font-size: 11px !important
15 | color: $body
16 |
17 | table
18 | width: 100%
19 |
20 | td
21 | margin: 0
22 | padding: 12px
23 | font-weight: 400
24 | text-align: left
25 | line-height: 260%
26 | border-bottom: 1px solid $border
27 |
28 | td b
29 | font-weight: 600
30 | white-space: nowrap
31 |
32 | a
33 | color: $link
34 | cursor: pointer
35 | border-bottom: 1px dotted $accent
36 |
37 | &:hover
38 | color: $accent
39 |
40 | button
41 | outline : none
42 | background: $button
43 | border: none
44 | border-radius: 6px
45 | padding: 4px 8px
46 | margin: 0 6px
47 | cursor: pointer
48 | transition: all 0.2s ease
49 |
50 | &.on, &:hover
51 | background: $accent
52 | color: white
53 |
54 | div
55 | padding: 0 0 12px 12px
56 |
--------------------------------------------------------------------------------
/test/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "./test",
3 | "spec_files": ["./*_test.js"],
4 | "helpers": ["helpers/**/*.js"],
5 | "stopSpecOnExpectationFailure": false,
6 | "random": true
7 | }
8 |
--------------------------------------------------------------------------------
/test/number_test.js:
--------------------------------------------------------------------------------
1 | var number = require("../src/lib/number");
2 |
3 | var formats = {
4 | en: { group: ",", decimal: "." },
5 | de: { group: ".", decimal: "," },
6 | cz: { group: " ", decimal: "," },
7 | };
8 |
9 | describe("parse returns null", function () {
10 | it("with no input", () => expect(number.parse("", formats.en)).toBe(null));
11 |
12 | it("with no digits", () =>
13 | expect(number.parse("abc", formats.en)).toBe(null));
14 |
15 | it("with invalid chars", () =>
16 | expect(number.parse("12@34", formats.en)).toBe(null));
17 |
18 | it("with just decimal point", () =>
19 | expect(number.parse(".", formats.en)).toBe(null));
20 |
21 | it("with two decimal points", () =>
22 | expect(number.parse("123.123.456", formats.en)).toBe(null));
23 |
24 | it("with a non-integer fraction", () =>
25 | expect(number.parse("123.123,456", formats.en)).toBe(null));
26 |
27 | it("with a group more than 3", () =>
28 | expect(number.parse("123,1234", formats.en)).toBe(null));
29 |
30 | it("with a group less than 2", () =>
31 | expect(number.parse("123,1,234", formats.en)).toBe(null));
32 |
33 | it("with an empty group", () =>
34 | expect(number.parse("123,,234", formats.en)).toBe(null));
35 |
36 | it("with overflow", () =>
37 | expect(number.parse("234234234234234234234234234234234", formats.en)).toBe(
38 | null
39 | ));
40 |
41 | it("with decimal overflow", () =>
42 | expect(
43 | number.parse("123.234234234234234234234234234234234", formats.en)
44 | ).toBe(null));
45 | });
46 |
47 | describe("extract returns null", function () {
48 | it("with no input", () => expect(number.extract("", formats.en)).toBe(null));
49 |
50 | it("with no digits", () =>
51 | expect(number.extract("abc", formats.en)).toBe(null));
52 |
53 | it("with more than one number", () =>
54 | expect(number.extract("abc 123 def 123.456", formats.en)).toBe(null));
55 |
56 | it("with invalid number", () =>
57 | expect(number.extract("abc 123.456.67", formats.en)).toBe(null));
58 | });
59 |
60 | describe("parse is ok", function () {
61 | it("with an integer", () =>
62 | expect(number.parse("12345", formats.en)).toBe(12345));
63 | it("with an negative integer", () =>
64 | expect(number.parse("-12345", formats.en)).toBe(-12345));
65 | it("with a decimal point", () =>
66 | expect(number.parse("12345.678", formats.en)).toBe(12345.678));
67 | it("with a void fraction", () =>
68 | expect(number.parse("12345.", formats.en)).toBe(12345));
69 | it("with a void fraction", () =>
70 | expect(number.parse("12345.", formats.en)).toBe(12345));
71 | it("with a void int part", () =>
72 | expect(number.parse(".123", formats.en)).toBe(0.123));
73 | it("with groups", () =>
74 | expect(number.parse("1,23,456.789", formats.en)).toBe(123456.789));
75 | it("with de groups", () =>
76 | expect(number.parse("1.23.456,789", formats.de)).toBe(123456.789));
77 | it("with cz groups", () =>
78 | expect(number.parse("1 23 456,789", formats.cz)).toBe(123456.789));
79 |
80 | it("with long decimal points", () =>
81 | expect(number.parse("552.123", formats.en)).toBe(552.123));
82 | it("with leading zero decimal points", () =>
83 | expect(number.parse("552.005", formats.en)).toBe(552.005));
84 | it("with zero decimal points", () =>
85 | expect(number.parse("552.000", formats.en)).toBe(552));
86 | });
87 |
88 | describe("extract is ok", function () {
89 | it("with an integer", () =>
90 | expect(number.extract("abc 12345 def", formats.en)).toBe(12345));
91 | it("with a negative integer", () =>
92 | expect(number.extract("abc -12345 def", formats.en)).toBe(-12345));
93 | it("with en groups and decimals", () =>
94 | expect(number.extract("abc -1,345,678.9 def", formats.en)).toBe(
95 | -1345678.9
96 | ));
97 | it("with de groups and decimals", () =>
98 | expect(number.extract("abc -1.345.678,9 def", formats.de)).toBe(
99 | -1345678.9
100 | ));
101 | it("with cz groups and decimals", () =>
102 | expect(number.extract("abc -1 345 678,9 def", formats.cz)).toBe(
103 | -1345678.9
104 | ));
105 | });
106 |
--------------------------------------------------------------------------------
/test/server/base.html:
--------------------------------------------------------------------------------
1 | CopyTables Test Page
2 |
3 |
23 |
24 |
32 |
33 |
34 |
35 |
36 | {{{content}}}
37 |
--------------------------------------------------------------------------------
/test/server/frame.html:
--------------------------------------------------------------------------------
1 |
16 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/test/server/index.js:
--------------------------------------------------------------------------------
1 | let fs = require("fs"),
2 | express = require("express"),
3 | mustache = require("mustache"),
4 | glob = require("glob"),
5 | app = express();
6 |
7 | Number.prototype.times = function (fn) {
8 | let a = [];
9 | for (let i = 0; i < Number(this); i++) a.push(fn(i));
10 | return a;
11 | };
12 |
13 | let file = (path) => fs.readFileSync(path, "utf8");
14 |
15 | let renderHelpers = {
16 | textSource() {
17 | return `
18 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
19 | Fusce et mauris eu arcu auctor imperdiet.
20 | Vivamus nec ex vitae libero ultrices vulputate eu non leo.
21 | In a mi a velit cursus mattis.
22 | Nam placerat enim quis ex euismod, quis dictum quam congue.
23 | Proin ut ante ut odio imperdiet semper ac non est.
24 | Morbi ac lectus dictum, vestibulum odio vitae, tempus risus.
25 | Fusce hendrerit felis sed nulla rhoncus fermentum.
26 | Duis dignissim sapien feugiat, cursus nulla non, vestibulum tellus.
27 | Ut commodo mauris quis dapibus venenatis.
28 | Duis iaculis mi quis massa dictum, sit amet placerat nisi ultrices.
29 | Sed mattis purus a est pharetra lacinia.
30 | Integer malesuada nisi in sodales viverra.
31 | Aliquam maximus risus ac ipsum sagittis aliquam.
32 | Duis consequat erat quis euismod fermentum.
33 | Suspendisse vitae augue id augue faucibus maximus.
34 | Vivamus nec erat rutrum, mollis ex nec, pretium mi.
35 | Vestibulum porta justo eu lorem eleifend laoreet.
36 | Cras maximus nisi et urna condimentum, a tincidunt dui fermentum.
37 | Maecenas at diam in ipsum convallis ultrices.
38 | Curabitur aliquam enim vitae neque rutrum, at rhoncus sem congue.
39 | Maecenas et ex ac lorem tempus accumsan et quis est.
40 | Nullam id ex porttitor, efficitur leo nec, maximus augue.
41 | Vivamus non sapien eu nisi commodo dignissim a at turpis.
42 | Vivamus a dolor eu odio tempor finibus id et massa.
43 | Donec finibus tellus sed dui gravida maximus.
44 | Donec malesuada ante vel sem dignissim viverra ac non nisi.
45 | Nunc euismod justo eget urna luctus dictum.
46 | Nulla sed erat tempor, lacinia massa ac, lacinia nisl.
47 | Praesent tempor tellus at velit egestas, et tincidunt lacus bibendum.
48 | `;
49 | },
50 |
51 | text() {
52 | let text = this.textSource();
53 | let a = Math.floor(Math.random() * text.length);
54 | let b = Math.floor(Math.random() * text.length);
55 |
56 | return "" + text.substr(a, b) + "
";
57 | },
58 |
59 | numTable(rows, cols) {
60 | let s = "";
61 | rows.times(function (r) {
62 | s += "";
63 | cols.times(function (c) {
64 | s += `${r + 1}.${c + 1} `;
65 | });
66 | s += " ";
67 | });
68 | return ``;
69 | },
70 | };
71 |
72 | let renderTemplate = (tpl) => {
73 | let path;
74 |
75 | path = `${__dirname}/templates/${tpl}.html`;
76 | if (fs.existsSync(path)) {
77 | return mustache.render(file(path), {
78 | text: renderHelpers.text(),
79 | });
80 | }
81 |
82 | path = `${__dirname}/templates/${tpl}.js`;
83 | if (fs.existsSync(path)) {
84 | return require(path).render(renderHelpers);
85 | }
86 |
87 | return `${tpl}=404`;
88 | };
89 |
90 | let renderDoc = (content) =>
91 | mustache.render(file(`${__dirname}/base.html`), { content: content });
92 |
93 | //
94 |
95 | app.use(express.static(__dirname + "/public"));
96 |
97 | app.get("/only/:tpl", (req, res, next) => {
98 | let content = req.params.tpl.split(",").map(renderTemplate).join("");
99 | res.send(renderDoc(content));
100 | });
101 |
102 | app.get("/base", (req, res, next) => {
103 | res.send(renderDoc(""));
104 | });
105 |
106 | app.get("/frame", (req, res, next) => {
107 | res.send(file(`${__dirname}/frame.html`));
108 | });
109 |
110 | app.get("/raw/:tpl", (req, res, next) => {
111 | let content = req.params.tpl.split(",").map(renderTemplate).join("");
112 | res.send(content);
113 | });
114 |
115 | app.get("/all", (req, res, next) => {
116 | let content = "",
117 | all =
118 | "simple spans numbers forms hidden framea nested scroll frameb styled frameset dynamic";
119 |
120 | all.split(" ").forEach((tpl) => {
121 | content += `${tpl} `;
122 | content += renderTemplate(tpl);
123 | content += renderHelpers.text();
124 | });
125 |
126 | res.send(renderDoc(content));
127 | });
128 |
129 | app.listen(9876);
130 |
--------------------------------------------------------------------------------
/test/server/public/img/a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/test/server/public/img/a.png
--------------------------------------------------------------------------------
/test/server/public/img/b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/test/server/public/img/b.png
--------------------------------------------------------------------------------
/test/server/public/img/c.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/test/server/public/img/c.png
--------------------------------------------------------------------------------
/test/server/templates/big.js:
--------------------------------------------------------------------------------
1 | module.exports.render = (h) => {
2 | function bgcolor(n) {
3 | n = ((n * 123456789) % 0xffffff).toString(16);
4 | while (n.length < 6) n = "0" + n;
5 | return `style="background-color:#${n}"`;
6 | }
7 |
8 | function chunk(n) {
9 | n = n % text.length;
10 | var a = text.substr(n + 0, n + 20);
11 | var b = text.substr(n + 20, n + 40);
12 | var c = text.substr(n + 40, n + 60);
13 | var d = text.substr(n + 60, n + 80);
14 |
15 | a = `${a} ${b} ${c} ${d} `;
16 | return a + " " + a + " " + a;
17 | }
18 |
19 | function num(n, m) {
20 | return (n & 0xffff) * m;
21 | }
22 |
23 | let rc = 500,
24 | rows = [],
25 | text = h.textSource();
26 |
27 | rc.times(function (i) {
28 | let cells = [];
29 |
30 | let k = Math.pow(7, i % 300) % 123456;
31 |
32 | cells.push(`${i} `);
33 | cells.push(`${k} `);
34 | cells.push(
35 | `${num(k, 123456)},${num(k, 123456)},${num(
36 | k,
37 | 123456
38 | )},${num(k, 123456)},${num(k, 123456)} `
39 | );
40 | cells.push(`${chunk(k)} `);
41 | cells.push(`${chunk(k + 10)} `);
42 | cells.push(`${num(k, 9876)} `);
43 |
44 | rows.push(`${cells.join("")} `);
45 | });
46 |
47 | return `
48 |
58 |
61 | `;
62 | };
63 |
--------------------------------------------------------------------------------
/test/server/templates/dynamic.html:
--------------------------------------------------------------------------------
1 |
47 |
48 |
58 |
59 |
60 |
61 | load
62 |
63 |
64 |
--------------------------------------------------------------------------------
/test/server/templates/forms.html:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/test/server/templates/framea.html:
--------------------------------------------------------------------------------
1 |
2 | {{{text}}}
3 |
4 |
--------------------------------------------------------------------------------
/test/server/templates/frameb.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/server/templates/frameset.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/server/templates/frameset_content.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/test/server/templates/hidden.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | HIDDEN 123
4 | 9999999 456
5 | 111111
6 |
7 |
8 |
9 |
10 |
11 | HIDDEN TABLE:
12 |
13 |
14 |
15 | HIDDEN 123
16 | 9999999 456
17 | 111111
18 |
19 |
20 |
21 | {{{text}}} NORMAL TABLE:
22 |
23 |
24 |
25 | HIDDEN 123
26 | 9999999 456
27 | 111111
28 |
29 |
30 |
--------------------------------------------------------------------------------
/test/server/templates/nested.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | one
4 |
5 |
6 |
7 | two
8 |
9 |
10 |
11 | three
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | four
21 |
22 |
23 | one
24 |
25 |
26 |
27 | two
28 |
29 |
30 |
31 | three
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | four
41 |
42 |
43 |
44 | {{{text}}}
45 |
--------------------------------------------------------------------------------
/test/server/templates/numbers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 1
4 |
5 |
6 | 1.07
7 |
8 |
9 |
10 |
11 |
12 |
13 | #
14 | Date
15 | price
16 |
17 |
18 |
19 |
20 | 3
21 | 04/12/2016
22 | $0.11
23 |
24 |
25 | 10
26 | 04/14/2016
27 | $0.22
28 |
29 |
30 | 5
31 | 04/19/2016
32 | $0.33
33 |
34 |
35 | 10
36 | 04/19/2016
37 | $0.44
38 |
39 |
40 | 2
41 | 04/20/2016
42 | $0.55
43 |
44 |
45 | 2
46 | 04/20/2016
47 | $0.66
48 |
49 |
50 | -567
51 | 04/19/2016
52 | $0.77
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | #
61 | price
62 |
63 |
64 |
65 |
66 | 3
67 | -0,3
68 |
69 |
70 | 3
71 | 0,341
72 |
73 |
74 | 3
75 | -1.245,576
76 |
77 |
78 | 3
79 | -1.245.678,576
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | #
88 | price
89 |
90 |
91 |
92 |
93 | 3
94 | -0,3
95 |
96 |
97 | 3
98 | -1 245,576
99 |
100 |
101 | 3
102 | -1 245 678,576
103 |
104 |
105 |
106 |
107 | {{{text}}}
108 |
--------------------------------------------------------------------------------
/test/server/templates/paste.html:
--------------------------------------------------------------------------------
1 |
14 |
15 |
29 |
30 |
31 |
32 |
33 |
34 | {{{content}}}
35 |
--------------------------------------------------------------------------------
/test/server/templates/script.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ONE:
5 |
8 | :END
9 |
10 |
11 |
12 |
17 | TWO: ??? :END
18 |
19 |
20 |
21 | three
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/server/templates/scroll.js:
--------------------------------------------------------------------------------
1 | module.exports.render = (h) => `
2 |
15 |
16 |
17 | ${h.numTable(20, 30)}
18 |
19 |
20 | ${h.text()}
21 | `;
22 |
--------------------------------------------------------------------------------
/test/server/templates/simple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello
4 | world
5 | link relative
6 |
7 |
8 | row2
9 | cell 2.2
10 | link relative 2
11 |
12 |
13 |
14 | image a
15 |
16 | link absolute
20 |
21 |
22 |
23 |
24 | image b
25 |
26 |
27 |
28 | image c
29 |
30 |
31 |
32 |
35 |
36 | external img
37 |
38 |
39 |
--------------------------------------------------------------------------------
/test/server/templates/spans.html:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 | Server
21 | Option
22 | Status
23 | Date
24 |
25 |
26 | Before update
27 | After update
28 |
29 |
30 | alpha
31 | HDD
32 | ok
33 | failed
34 | 2015/01/01
35 |
36 |
37 | SSD
38 | ok
39 | 2015/01/02
40 |
41 |
42 | beta
43 | HDD
44 | failed
45 | ok
46 | 2015/01/03
47 |
48 |
49 | SSD
50 | wait
51 | 2015/01/04
52 |
53 |
54 | no data
55 | 2015/01/05
56 |
57 |
58 | 2015/01/06
59 |
60 |
61 |
62 | {{{text}}}
63 |
--------------------------------------------------------------------------------
/test/server/templates/styled.html:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 | default style
45 | class 1 style
46 | class 2 style
47 | inline style
48 |
49 |
50 |
51 | class 2 + inline
52 |
53 | external background
54 |
59 | external background - inline
60 |
61 |
62 |
63 | td animated
64 |
65 |
66 | tr animated
67 | tr animated
68 | tr animated
69 | tr animated
70 |
71 |
72 | TD = NOT SELECTABLE
73 | TD = NOT SELECTABLE
74 |
75 |
76 |
77 |
78 |
79 | TABLE = NOT SELECTABLE
80 | TABLE = NOT SELECTABLE
81 |
82 |
83 |
84 | {{{text}}}
85 |
--------------------------------------------------------------------------------