@v0idd0/tabsnap 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/bin/tabsnap.js +152 -0
- package/package.json +56 -0
- package/src/index.js +156 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vøiddo
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# tabsnap
|
|
2
|
+
|
|
3
|
+
> One-click browser extension that captures every open tab as plain text,
|
|
4
|
+
> markdown, JSON, or a readme file. Free, MIT, zero telemetry.
|
|
5
|
+
|
|
6
|
+
[Landing page](https://extensions.voiddo.com/tabsnap/) ·
|
|
7
|
+
[Privacy policy](https://extensions.voiddo.com/tabsnap/privacy/) ·
|
|
8
|
+
[Contact](https://extensions.voiddo.com/tabsnap/contact/)
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
Click the toolbar icon → tabsnap reads the title and URL of every tab you have
|
|
13
|
+
open and renders them in the popup as one of four formats:
|
|
14
|
+
|
|
15
|
+
- **markdown** — nested list grouped by window, hostnames as section headers
|
|
16
|
+
- **plain** — one tab per line, title + URL
|
|
17
|
+
- **json** — structured array, ready to feed into another tool
|
|
18
|
+
- **readme.md** — full markdown document with a domain-summary table at the top
|
|
19
|
+
|
|
20
|
+
Hit **copy** or **download** and you're done.
|
|
21
|
+
|
|
22
|
+
## Privacy
|
|
23
|
+
|
|
24
|
+
tabsnap reads only `chrome.tabs` metadata (title, URL, windowId, pinned,
|
|
25
|
+
incognito) and only when you click the toolbar icon. **No backend. No
|
|
26
|
+
analytics. No telemetry.** Your tabs never leave your machine.
|
|
27
|
+
|
|
28
|
+
Permissions:
|
|
29
|
+
|
|
30
|
+
- `tabs` — list open tabs
|
|
31
|
+
- `downloads` — save the snapshot to a file (only on user click)
|
|
32
|
+
- `storage` — remember your popup preferences across sessions
|
|
33
|
+
|
|
34
|
+
No host permissions. No `activeTab`. No content scripts.
|
|
35
|
+
|
|
36
|
+
## Repo layout
|
|
37
|
+
|
|
38
|
+
chrome/ Manifest V3 source for Chrome / Edge / Brave
|
|
39
|
+
firefox/ Manifest V3 source with browser_specific_settings.gecko
|
|
40
|
+
edge/ Manifest V3 source (mirrors chrome/)
|
|
41
|
+
dist/ Pre-built ZIPs ready for store submission
|
|
42
|
+
shared/ Files copied into each platform tree at build time
|
|
43
|
+
|
|
44
|
+
## Build
|
|
45
|
+
|
|
46
|
+
The platform directories are already self-contained — you can load any of
|
|
47
|
+
`chrome/`, `firefox/`, or `edge/` as an unpacked extension in the matching
|
|
48
|
+
browser. The pre-built ZIPs in `dist/` are produced by zipping each platform
|
|
49
|
+
directory.
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT — see `LICENSE`.
|
|
54
|
+
|
|
55
|
+
Built by [vøiddo](https://voiddo.com/), a small studio shipping AI-flavoured
|
|
56
|
+
tools, browser extensions and weird browser games.
|
package/bin/tabsnap.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* @v0idd0/tabsnap CLI — read a tab list (JSON), print formatted snapshot.
|
|
3
|
+
Input shapes accepted:
|
|
4
|
+
- array of tab objects: [{title,url,windowId?,pinned?,incognito?}, ...]
|
|
5
|
+
- object with .tabs: {tabs: [...]}
|
|
6
|
+
- object with .windows: {windows: [{tabs:[...]}, ...]} (auto-flatten)
|
|
7
|
+
Reads from --file=<path>, or stdin if no file given.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const { formatTabs, FORMATS } = require('../src/index.js');
|
|
14
|
+
|
|
15
|
+
const HELP = `tabsnap — format a tab list as markdown / plain / json / readme
|
|
16
|
+
|
|
17
|
+
usage:
|
|
18
|
+
tabsnap [options] # reads JSON from stdin
|
|
19
|
+
tabsnap --file=tabs.json # reads from file
|
|
20
|
+
cat tabs.json | tabsnap -f readme
|
|
21
|
+
|
|
22
|
+
options:
|
|
23
|
+
-f, --format=<fmt> one of: ${FORMATS.join(', ')} (default: markdown)
|
|
24
|
+
-i, --file=<path> read JSON tab list from file instead of stdin
|
|
25
|
+
--no-group do not group by window (single flat list)
|
|
26
|
+
--include-pinned include pinned tabs (default: skip)
|
|
27
|
+
--include-incognito include incognito-window tabs (default: skip)
|
|
28
|
+
-h, --help this help
|
|
29
|
+
-v, --version print version
|
|
30
|
+
|
|
31
|
+
input shapes accepted:
|
|
32
|
+
[{"title":"…","url":"…","windowId":1,"pinned":false}, …]
|
|
33
|
+
{"tabs": [...]}
|
|
34
|
+
{"windows": [{"tabs":[...]}, ...]}
|
|
35
|
+
also: { "snapshot_at": "...", "windows": [...] } from a previous tabsnap export
|
|
36
|
+
|
|
37
|
+
examples:
|
|
38
|
+
tabsnap < tabs.json
|
|
39
|
+
tabsnap --file=tabs.json --format=json --no-group
|
|
40
|
+
curl -s api.example.com/tabs | tabsnap -f readme > snapshot.md
|
|
41
|
+
|
|
42
|
+
free, MIT, zero telemetry. https://extensions.voiddo.com/tabsnap/
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
function parseArgs(argv) {
|
|
46
|
+
const opts = {
|
|
47
|
+
format: 'markdown',
|
|
48
|
+
file: null,
|
|
49
|
+
groupByWindow: true,
|
|
50
|
+
includePinned: false,
|
|
51
|
+
includeIncognito: false,
|
|
52
|
+
help: false,
|
|
53
|
+
version: false,
|
|
54
|
+
};
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const a = argv[i];
|
|
57
|
+
if (a === '-h' || a === '--help') opts.help = true;
|
|
58
|
+
else if (a === '-v' || a === '--version') opts.version = true;
|
|
59
|
+
else if (a === '--no-group') opts.groupByWindow = false;
|
|
60
|
+
else if (a === '--include-pinned') opts.includePinned = true;
|
|
61
|
+
else if (a === '--include-incognito') opts.includeIncognito = true;
|
|
62
|
+
else if (a.startsWith('--format=')) opts.format = a.slice(9);
|
|
63
|
+
else if (a === '-f' || a === '--format') opts.format = argv[++i];
|
|
64
|
+
else if (a.startsWith('--file=')) opts.file = a.slice(7);
|
|
65
|
+
else if (a === '-i' || a === '--file') opts.file = argv[++i];
|
|
66
|
+
else {
|
|
67
|
+
process.stderr.write('tabsnap: unknown argument "' + a + '"\nrun with --help for usage.\n');
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!FORMATS.includes(opts.format)) {
|
|
72
|
+
process.stderr.write('tabsnap: invalid format "' + opts.format + '" — use one of: ' + FORMATS.join(', ') + '\n');
|
|
73
|
+
process.exit(2);
|
|
74
|
+
}
|
|
75
|
+
return opts;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeTabs(input) {
|
|
79
|
+
if (Array.isArray(input)) return input;
|
|
80
|
+
if (input && Array.isArray(input.tabs)) return input.tabs;
|
|
81
|
+
if (input && Array.isArray(input.windows)) {
|
|
82
|
+
return input.windows.flatMap((w, i) => {
|
|
83
|
+
const wid = (w.windowId !== undefined) ? w.windowId : (w.window_index !== undefined ? w.window_index : (i + 1));
|
|
84
|
+
return (w.tabs || []).map(t => ({ ...t, windowId: t.windowId !== undefined ? t.windowId : wid }));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
throw new Error('tabsnap: input does not match any accepted shape (array / .tabs / .windows). got ' + typeof input);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function readStdin() {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
let buf = '';
|
|
93
|
+
process.stdin.setEncoding('utf8');
|
|
94
|
+
process.stdin.on('data', chunk => { buf += chunk; });
|
|
95
|
+
process.stdin.on('end', () => resolve(buf));
|
|
96
|
+
process.stdin.on('error', reject);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function main() {
|
|
101
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
102
|
+
if (opts.help) { process.stdout.write(HELP); return; }
|
|
103
|
+
if (opts.version) {
|
|
104
|
+
const pkg = require('../package.json');
|
|
105
|
+
process.stdout.write(pkg.version + '\n');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let raw;
|
|
110
|
+
if (opts.file) {
|
|
111
|
+
raw = fs.readFileSync(opts.file, 'utf8');
|
|
112
|
+
} else if (process.stdin.isTTY) {
|
|
113
|
+
process.stderr.write('tabsnap: no input. pipe JSON in, or use --file=<path>. run --help for usage.\n');
|
|
114
|
+
process.exit(2);
|
|
115
|
+
} else {
|
|
116
|
+
raw = await readStdin();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(raw);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
process.stderr.write('tabsnap: input is not valid JSON: ' + e.message + '\n');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let tabs;
|
|
128
|
+
try {
|
|
129
|
+
tabs = normalizeTabs(parsed);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
process.stderr.write(e.message + '\n');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// backfill missing windowId so groupByWindow works on flat input
|
|
136
|
+
let nextWid = 1;
|
|
137
|
+
for (const t of tabs) {
|
|
138
|
+
if (t.windowId === undefined || t.windowId === null) t.windowId = nextWid;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const out = formatTabs(tabs, opts.format, {
|
|
142
|
+
groupByWindow: opts.groupByWindow,
|
|
143
|
+
includePinned: opts.includePinned,
|
|
144
|
+
includeIncognito: opts.includeIncognito,
|
|
145
|
+
});
|
|
146
|
+
process.stdout.write(out);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
main().catch(e => {
|
|
150
|
+
process.stderr.write('tabsnap: unexpected error: ' + (e.stack || e.message || e) + '\n');
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@v0idd0/tabsnap",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "tabsnap — format a tab list (or any tabs-shaped JSON) as markdown, plain text, JSON, or a readme file. Library + CLI. Same formatters that power the tabsnap browser extension. Zero deps. Free forever from vøiddo.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tabsnap": "./bin/tabsnap.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node test.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"tabs",
|
|
20
|
+
"tab-snapshot",
|
|
21
|
+
"tab-list",
|
|
22
|
+
"markdown",
|
|
23
|
+
"browser-extension",
|
|
24
|
+
"chrome-tabs",
|
|
25
|
+
"firefox-tabs",
|
|
26
|
+
"cli",
|
|
27
|
+
"formatter",
|
|
28
|
+
"json-to-markdown",
|
|
29
|
+
"json-to-readme",
|
|
30
|
+
"snapshot",
|
|
31
|
+
"voiddo",
|
|
32
|
+
"free",
|
|
33
|
+
"zero-deps"
|
|
34
|
+
],
|
|
35
|
+
"author": "vøiddo <support@voiddo.com> (https://voiddo.com)",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://extensions.voiddo.com/tabsnap/",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/voidd0/tabsnap.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/voidd0/tabsnap/issues",
|
|
44
|
+
"email": "support@voiddo.com"
|
|
45
|
+
},
|
|
46
|
+
"funding": {
|
|
47
|
+
"type": "individual",
|
|
48
|
+
"url": "https://voiddo.com/contact/"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=14"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/* @v0idd0/tabsnap — pure-function tab-list serializers.
|
|
2
|
+
Exports: formatTabs, filterTabs, groupTabsByWindow, hostnameOf, countByDomain
|
|
3
|
+
No DOM. No node-only globals. Works in any JS runtime that has URL + Date. */
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
function escapeMarkdown(s) {
|
|
8
|
+
if (!s) return '';
|
|
9
|
+
return String(s).replace(/[\[\]\\`*_]/g, ch => '\\' + ch);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function groupTabsByWindow(tabs) {
|
|
13
|
+
const groups = new Map();
|
|
14
|
+
for (const t of tabs) {
|
|
15
|
+
if (!groups.has(t.windowId)) groups.set(t.windowId, []);
|
|
16
|
+
groups.get(t.windowId).push(t);
|
|
17
|
+
}
|
|
18
|
+
return [...groups.entries()].map(([windowId, tabs], i) => ({
|
|
19
|
+
windowIndex: i + 1,
|
|
20
|
+
windowId,
|
|
21
|
+
tabs,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function filterTabs(tabs, opts) {
|
|
26
|
+
let out = tabs.slice();
|
|
27
|
+
if (!opts.includePinned) out = out.filter(t => !t.pinned);
|
|
28
|
+
if (!opts.includeIncognito) out = out.filter(t => !t.incognito);
|
|
29
|
+
if (opts.currentWindowOnly && opts.currentWindowId !== undefined) {
|
|
30
|
+
out = out.filter(t => t.windowId === opts.currentWindowId);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fmtMarkdown(tabs, opts) {
|
|
36
|
+
const t = filterTabs(tabs, opts);
|
|
37
|
+
const dt = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
|
|
38
|
+
const head = '# tab snapshot · ' + t.length + ' tab' + (t.length === 1 ? '' : 's') + '\n_' + dt + '_\n\n';
|
|
39
|
+
if (!opts.groupByWindow) {
|
|
40
|
+
return head + t.map(formatLineMd).join('\n') + '\n';
|
|
41
|
+
}
|
|
42
|
+
const groups = groupTabsByWindow(t);
|
|
43
|
+
return head + groups.map(g =>
|
|
44
|
+
'## window ' + g.windowIndex + ' (' + g.tabs.length + ')\n' +
|
|
45
|
+
g.tabs.map(formatLineMd).join('\n') + '\n'
|
|
46
|
+
).join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatLineMd(tab) {
|
|
50
|
+
const title = escapeMarkdown(tab.title || '(untitled)');
|
|
51
|
+
return '- [' + title + '](' + tab.url + ')';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fmtPlain(tabs, opts) {
|
|
55
|
+
const t = filterTabs(tabs, opts);
|
|
56
|
+
if (!opts.groupByWindow) {
|
|
57
|
+
return t.map(x => (x.title || '(untitled)') + '\n ' + x.url).join('\n\n') + '\n';
|
|
58
|
+
}
|
|
59
|
+
const groups = groupTabsByWindow(t);
|
|
60
|
+
return groups.map(g =>
|
|
61
|
+
'window ' + g.windowIndex + ' (' + g.tabs.length + ')\n' +
|
|
62
|
+
'─'.repeat(40) + '\n' +
|
|
63
|
+
g.tabs.map(x => (x.title || '(untitled)') + '\n ' + x.url).join('\n\n')
|
|
64
|
+
).join('\n\n') + '\n';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fmtJson(tabs, opts) {
|
|
68
|
+
const t = filterTabs(tabs, opts);
|
|
69
|
+
const out = {
|
|
70
|
+
snapshot_at: new Date().toISOString(),
|
|
71
|
+
count: t.length,
|
|
72
|
+
tool: 'tabsnap',
|
|
73
|
+
};
|
|
74
|
+
if (opts.groupByWindow) {
|
|
75
|
+
out.windows = groupTabsByWindow(t).map(g => ({
|
|
76
|
+
window_index: g.windowIndex,
|
|
77
|
+
tab_count: g.tabs.length,
|
|
78
|
+
tabs: g.tabs.map(simplifyTab),
|
|
79
|
+
}));
|
|
80
|
+
} else {
|
|
81
|
+
out.tabs = t.map(simplifyTab);
|
|
82
|
+
}
|
|
83
|
+
return JSON.stringify(out, null, 2) + '\n';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function simplifyTab(t) {
|
|
87
|
+
return {
|
|
88
|
+
title: t.title || null,
|
|
89
|
+
url: t.url || null,
|
|
90
|
+
pinned: !!t.pinned,
|
|
91
|
+
active: !!t.active,
|
|
92
|
+
audible: !!t.audible,
|
|
93
|
+
domain: hostnameOf(t.url),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function fmtReadme(tabs, opts) {
|
|
98
|
+
const t = filterTabs(tabs, opts);
|
|
99
|
+
const dt = new Date().toISOString().slice(0, 10);
|
|
100
|
+
const head = '# tab graveyard\n\n' +
|
|
101
|
+
'> ' + t.length + ' tab' + (t.length === 1 ? '' : 's') +
|
|
102
|
+
' captured ' + dt + ' via [tabsnap](https://extensions.voiddo.com/tabsnap/).\n\n';
|
|
103
|
+
const counts = countByDomain(t);
|
|
104
|
+
let domains = '## by domain\n\n';
|
|
105
|
+
domains += '| domain | count |\n|---|---|\n';
|
|
106
|
+
for (const [d, c] of counts) domains += '| ' + d + ' | ' + c + ' |\n';
|
|
107
|
+
domains += '\n';
|
|
108
|
+
let body;
|
|
109
|
+
if (!opts.groupByWindow) {
|
|
110
|
+
body = '## all tabs\n\n' + t.map(formatLineMd).join('\n') + '\n';
|
|
111
|
+
} else {
|
|
112
|
+
const groups = groupTabsByWindow(t);
|
|
113
|
+
body = groups.map(g =>
|
|
114
|
+
'## window ' + g.windowIndex + ' · ' + g.tabs.length + ' tab' + (g.tabs.length === 1 ? '' : 's') + '\n\n' +
|
|
115
|
+
g.tabs.map(formatLineMd).join('\n') + '\n'
|
|
116
|
+
).join('\n');
|
|
117
|
+
}
|
|
118
|
+
return head + domains + body;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hostnameOf(url) {
|
|
122
|
+
if (!url) return null;
|
|
123
|
+
try { return new URL(url).hostname; } catch (_) { return null; }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function countByDomain(tabs) {
|
|
127
|
+
const m = new Map();
|
|
128
|
+
for (const t of tabs) {
|
|
129
|
+
const d = hostnameOf(t.url) || '(local)';
|
|
130
|
+
m.set(d, (m.get(d) || 0) + 1);
|
|
131
|
+
}
|
|
132
|
+
return [...m.entries()].sort((a, b) => b[1] - a[1]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const FORMATTERS = {
|
|
136
|
+
markdown: fmtMarkdown,
|
|
137
|
+
plain: fmtPlain,
|
|
138
|
+
json: fmtJson,
|
|
139
|
+
readme: fmtReadme,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const FORMATS = Object.keys(FORMATTERS);
|
|
143
|
+
|
|
144
|
+
function formatTabs(tabs, format, opts) {
|
|
145
|
+
const fn = FORMATTERS[format] || FORMATTERS.markdown;
|
|
146
|
+
return fn(tabs, opts || {});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
formatTabs,
|
|
151
|
+
filterTabs,
|
|
152
|
+
groupTabsByWindow,
|
|
153
|
+
hostnameOf,
|
|
154
|
+
countByDomain,
|
|
155
|
+
FORMATS,
|
|
156
|
+
};
|