@v0idd0/tabsnap 1.0.3 → 1.0.4
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/README.md +5 -1
- package/bin/tabsnap.js +4 -0
- package/package.json +5 -2
- package/src/index.js +32 -12
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
> Capture every open tab as a browser-session export: plain text, markdown, JSON, or a readme file.
|
|
10
10
|
> One click in the browser. One pipe in the terminal.
|
|
11
|
-
> Free, MIT, zero telemetry.
|
|
11
|
+
> Free, MIT, zero telemetry. Optional tracking-param stripping for cleaner exports.
|
|
12
12
|
|
|
13
13
|
[Browser-extension landing](https://extensions.voiddo.com/tabsnap/) ·
|
|
14
14
|
[CLI landing](https://tools.voiddo.com/tabsnap/) ·
|
|
@@ -47,6 +47,7 @@ npm i -g @v0idd0/tabsnap
|
|
|
47
47
|
cat tabs.json | tabsnap # markdown (default)
|
|
48
48
|
cat tabs.json | tabsnap --format=readme # readme.md
|
|
49
49
|
cat tabs.json | tabsnap -f json --no-group # flat structured array
|
|
50
|
+
cat tabs.json | tabsnap --strip-tracking # clean tracking params first
|
|
50
51
|
|
|
51
52
|
# from file
|
|
52
53
|
tabsnap --file=tabs.json -f plain
|
|
@@ -54,6 +55,9 @@ tabsnap --file=tabs.json -f plain
|
|
|
54
55
|
# include pinned + incognito tabs (skipped by default)
|
|
55
56
|
tabsnap --include-pinned --include-incognito < tabs.json
|
|
56
57
|
|
|
58
|
+
# strip common marketing params from exported URLs
|
|
59
|
+
tabsnap --strip-tracking -f readme < tabs.json
|
|
60
|
+
|
|
57
61
|
# pipe to clipboard (macOS) or any tool
|
|
58
62
|
tabsnap --format=readme < tabs.json | pbcopy
|
|
59
63
|
```
|
package/bin/tabsnap.js
CHANGED
|
@@ -25,6 +25,7 @@ options:
|
|
|
25
25
|
--no-group do not group by window (single flat list)
|
|
26
26
|
--include-pinned include pinned tabs (default: skip)
|
|
27
27
|
--include-incognito include incognito-window tabs (default: skip)
|
|
28
|
+
--strip-tracking remove common tracking params from exported urls
|
|
28
29
|
-h, --help this help
|
|
29
30
|
-v, --version print version
|
|
30
31
|
|
|
@@ -49,6 +50,7 @@ function parseArgs(argv) {
|
|
|
49
50
|
groupByWindow: true,
|
|
50
51
|
includePinned: false,
|
|
51
52
|
includeIncognito: false,
|
|
53
|
+
stripTracking: false,
|
|
52
54
|
help: false,
|
|
53
55
|
version: false,
|
|
54
56
|
};
|
|
@@ -59,6 +61,7 @@ function parseArgs(argv) {
|
|
|
59
61
|
else if (a === '--no-group') opts.groupByWindow = false;
|
|
60
62
|
else if (a === '--include-pinned') opts.includePinned = true;
|
|
61
63
|
else if (a === '--include-incognito') opts.includeIncognito = true;
|
|
64
|
+
else if (a === '--strip-tracking') opts.stripTracking = true;
|
|
62
65
|
else if (a.startsWith('--format=')) opts.format = a.slice(9);
|
|
63
66
|
else if (a === '-f' || a === '--format') opts.format = argv[++i];
|
|
64
67
|
else if (a.startsWith('--file=')) opts.file = a.slice(7);
|
|
@@ -142,6 +145,7 @@ async function main() {
|
|
|
142
145
|
groupByWindow: opts.groupByWindow,
|
|
143
146
|
includePinned: opts.includePinned,
|
|
144
147
|
includeIncognito: opts.includeIncognito,
|
|
148
|
+
stripTracking: opts.stripTracking,
|
|
145
149
|
});
|
|
146
150
|
process.stdout.write(out);
|
|
147
151
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v0idd0/tabsnap",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "tabsnap — browser session exporter and tab-list formatter for 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.",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "tabsnap — browser session exporter and tab-list formatter for markdown, plain text, JSON, or a readme file. Library + CLI. Optional tracking-param stripping keeps exports clean. Same formatters that power the tabsnap browser extension. Zero deps. Free forever from vøiddo.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"tabsnap": "./bin/tabsnap.js"
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
"formatter",
|
|
35
35
|
"json-to-markdown",
|
|
36
36
|
"json-to-readme",
|
|
37
|
+
"tracking-params",
|
|
38
|
+
"utm",
|
|
39
|
+
"url-cleanup",
|
|
37
40
|
"snapshot",
|
|
38
41
|
"voiddo",
|
|
39
42
|
"free",
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,26 @@ function escapeMarkdown(s) {
|
|
|
9
9
|
return String(s).replace(/[\[\]\\`*_]/g, ch => '\\' + ch);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const TRACKING_PARAM_RE = /^(?:utm_[a-z0-9_]+|gclid|dclid|fbclid|msclkid|mc_cid|mc_eid|ref|ref_src|referrer|trk|igshid|_hsenc|_hsmi)$/i;
|
|
13
|
+
|
|
14
|
+
function cleanUrl(url, opts) {
|
|
15
|
+
if (!url || !opts || !opts.stripTracking) return url;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = new URL(url);
|
|
18
|
+
const keys = [...new Set([...parsed.searchParams.keys()])];
|
|
19
|
+
let changed = false;
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
if (TRACKING_PARAM_RE.test(key)) {
|
|
22
|
+
parsed.searchParams.delete(key);
|
|
23
|
+
changed = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return changed ? parsed.toString() : url;
|
|
27
|
+
} catch (_) {
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
12
32
|
function groupTabsByWindow(tabs) {
|
|
13
33
|
const groups = new Map();
|
|
14
34
|
for (const t of tabs) {
|
|
@@ -37,30 +57,30 @@ function fmtMarkdown(tabs, opts) {
|
|
|
37
57
|
const dt = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
|
|
38
58
|
const head = '# tab snapshot · ' + t.length + ' tab' + (t.length === 1 ? '' : 's') + '\n_' + dt + '_\n\n';
|
|
39
59
|
if (!opts.groupByWindow) {
|
|
40
|
-
return head + t.map(formatLineMd).join('\n') + '\n';
|
|
60
|
+
return head + t.map(tab => formatLineMd(tab, opts)).join('\n') + '\n';
|
|
41
61
|
}
|
|
42
62
|
const groups = groupTabsByWindow(t);
|
|
43
63
|
return head + groups.map(g =>
|
|
44
64
|
'## window ' + g.windowIndex + ' (' + g.tabs.length + ')\n' +
|
|
45
|
-
g.tabs.map(formatLineMd).join('\n') + '\n'
|
|
65
|
+
g.tabs.map(tab => formatLineMd(tab, opts)).join('\n') + '\n'
|
|
46
66
|
).join('\n');
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
function formatLineMd(tab) {
|
|
69
|
+
function formatLineMd(tab, opts) {
|
|
50
70
|
const title = escapeMarkdown(tab.title || '(untitled)');
|
|
51
|
-
return '- [' + title + '](' + tab.url + ')';
|
|
71
|
+
return '- [' + title + '](' + cleanUrl(tab.url, opts) + ')';
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
function fmtPlain(tabs, opts) {
|
|
55
75
|
const t = filterTabs(tabs, opts);
|
|
56
76
|
if (!opts.groupByWindow) {
|
|
57
|
-
return t.map(x => (x.title || '(untitled)') + '\n ' + x.url).join('\n\n') + '\n';
|
|
77
|
+
return t.map(x => (x.title || '(untitled)') + '\n ' + cleanUrl(x.url, opts)).join('\n\n') + '\n';
|
|
58
78
|
}
|
|
59
79
|
const groups = groupTabsByWindow(t);
|
|
60
80
|
return groups.map(g =>
|
|
61
81
|
'window ' + g.windowIndex + ' (' + g.tabs.length + ')\n' +
|
|
62
82
|
'─'.repeat(40) + '\n' +
|
|
63
|
-
g.tabs.map(x => (x.title || '(untitled)') + '\n ' + x.url).join('\n\n')
|
|
83
|
+
g.tabs.map(x => (x.title || '(untitled)') + '\n ' + cleanUrl(x.url, opts)).join('\n\n')
|
|
64
84
|
).join('\n\n') + '\n';
|
|
65
85
|
}
|
|
66
86
|
|
|
@@ -75,18 +95,18 @@ function fmtJson(tabs, opts) {
|
|
|
75
95
|
out.windows = groupTabsByWindow(t).map(g => ({
|
|
76
96
|
window_index: g.windowIndex,
|
|
77
97
|
tab_count: g.tabs.length,
|
|
78
|
-
tabs: g.tabs.map(simplifyTab),
|
|
98
|
+
tabs: g.tabs.map(tab => simplifyTab(tab, opts)),
|
|
79
99
|
}));
|
|
80
100
|
} else {
|
|
81
|
-
out.tabs = t.map(simplifyTab);
|
|
101
|
+
out.tabs = t.map(tab => simplifyTab(tab, opts));
|
|
82
102
|
}
|
|
83
103
|
return JSON.stringify(out, null, 2) + '\n';
|
|
84
104
|
}
|
|
85
105
|
|
|
86
|
-
function simplifyTab(t) {
|
|
106
|
+
function simplifyTab(t, opts) {
|
|
87
107
|
return {
|
|
88
108
|
title: t.title || null,
|
|
89
|
-
url: t.url || null,
|
|
109
|
+
url: cleanUrl(t.url, opts) || null,
|
|
90
110
|
pinned: !!t.pinned,
|
|
91
111
|
active: !!t.active,
|
|
92
112
|
audible: !!t.audible,
|
|
@@ -107,12 +127,12 @@ function fmtReadme(tabs, opts) {
|
|
|
107
127
|
domains += '\n';
|
|
108
128
|
let body;
|
|
109
129
|
if (!opts.groupByWindow) {
|
|
110
|
-
body = '## all tabs\n\n' + t.map(formatLineMd).join('\n') + '\n';
|
|
130
|
+
body = '## all tabs\n\n' + t.map(tab => formatLineMd(tab, opts)).join('\n') + '\n';
|
|
111
131
|
} else {
|
|
112
132
|
const groups = groupTabsByWindow(t);
|
|
113
133
|
body = groups.map(g =>
|
|
114
134
|
'## window ' + g.windowIndex + ' · ' + g.tabs.length + ' tab' + (g.tabs.length === 1 ? '' : 's') + '\n\n' +
|
|
115
|
-
g.tabs.map(formatLineMd).join('\n') + '\n'
|
|
135
|
+
g.tabs.map(tab => formatLineMd(tab, opts)).join('\n') + '\n'
|
|
116
136
|
).join('\n');
|
|
117
137
|
}
|
|
118
138
|
return head + domains + body;
|