cli-browser 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.
@@ -0,0 +1,259 @@
1
+ import * as cheerio from 'cheerio';
2
+ import { theme } from './themes.js';
3
+
4
+ class Renderer {
5
+ constructor() {
6
+ this.links = [];
7
+ this.images = [];
8
+ }
9
+
10
+ render(html, url = '') {
11
+ this.links = [];
12
+ this.images = [];
13
+
14
+ const $ = cheerio.load(html);
15
+
16
+ // Remove unwanted elements
17
+ $('script, style, noscript, iframe, svg, nav, footer, header, .ad, .ads, .advertisement, .sidebar, .menu, .nav, .cookie, .popup, .modal, .banner').remove();
18
+
19
+ const title = $('title').text().trim() || 'Untitled Page';
20
+ const body = $('body');
21
+
22
+ let output = '';
23
+ output += this._processElement($, body, 0);
24
+
25
+ // Deduplicate links
26
+ const uniqueLinks = [];
27
+ const seen = new Set();
28
+ for (const link of this.links) {
29
+ const key = link.url;
30
+ if (!seen.has(key) && link.url && !link.url.startsWith('#') && !link.url.startsWith('javascript:')) {
31
+ seen.add(key);
32
+ // Resolve relative URLs
33
+ if (link.url.startsWith('/') && url) {
34
+ try {
35
+ const base = new URL(url);
36
+ link.url = `${base.protocol}//${base.host}${link.url}`;
37
+ } catch {}
38
+ }
39
+ uniqueLinks.push(link);
40
+ }
41
+ }
42
+ this.links = uniqueLinks;
43
+
44
+ return {
45
+ title,
46
+ content: this._cleanOutput(output),
47
+ links: this.links,
48
+ images: this.images,
49
+ };
50
+ }
51
+
52
+ renderReader(html, url = '') {
53
+ const $ = cheerio.load(html);
54
+
55
+ // Remove navigation, ads, sidebars, etc
56
+ $('script, style, noscript, iframe, svg, nav, footer, header, aside, .ad, .ads, .advertisement, .sidebar, .menu, .nav, .cookie, .popup, .modal, .banner, .share, .social, .related, .comments, .comment, form, button, input, select, textarea').remove();
57
+
58
+ const title = $('title').text().trim() ||
59
+ $('h1').first().text().trim() ||
60
+ 'Untitled Page';
61
+
62
+ // Try to find main content
63
+ let mainContent = $('article, [role="main"], main, .post-content, .article-content, .entry-content, .content, #content, #main').first();
64
+ if (!mainContent.length) {
65
+ mainContent = $('body');
66
+ }
67
+
68
+ this.links = [];
69
+ this.images = [];
70
+
71
+ let output = this._processElement($, mainContent, 0);
72
+
73
+ // Resolve relative URLs
74
+ const uniqueLinks = [];
75
+ const seen = new Set();
76
+ for (const link of this.links) {
77
+ if (!seen.has(link.url) && link.url && !link.url.startsWith('#') && !link.url.startsWith('javascript:')) {
78
+ seen.add(link.url);
79
+ if (link.url.startsWith('/') && url) {
80
+ try {
81
+ const base = new URL(url);
82
+ link.url = `${base.protocol}//${base.host}${link.url}`;
83
+ } catch {}
84
+ }
85
+ uniqueLinks.push(link);
86
+ }
87
+ }
88
+ this.links = uniqueLinks;
89
+
90
+ return {
91
+ title,
92
+ content: this._cleanOutput(output),
93
+ links: this.links,
94
+ images: this.images,
95
+ };
96
+ }
97
+
98
+ getPageInfo(html) {
99
+ const $ = cheerio.load(html);
100
+ return {
101
+ title: $('title').text().trim(),
102
+ description: $('meta[name="description"]').attr('content') || $('meta[property="og:description"]').attr('content') || '',
103
+ keywords: $('meta[name="keywords"]').attr('content') || '',
104
+ language: $('html').attr('lang') || '',
105
+ author: $('meta[name="author"]').attr('content') || '',
106
+ ogTitle: $('meta[property="og:title"]').attr('content') || '',
107
+ ogImage: $('meta[property="og:image"]').attr('content') || '',
108
+ ogType: $('meta[property="og:type"]').attr('content') || '',
109
+ canonical: $('link[rel="canonical"]').attr('href') || '',
110
+ favicon: $('link[rel="icon"]').attr('href') || $('link[rel="shortcut icon"]').attr('href') || '',
111
+ charset: $('meta[charset]').attr('charset') || '',
112
+ viewport: $('meta[name="viewport"]').attr('content') || '',
113
+ generator: $('meta[name="generator"]').attr('content') || '',
114
+ };
115
+ }
116
+
117
+ _processElement($, el, depth) {
118
+ let output = '';
119
+
120
+ $(el).contents().each((_, node) => {
121
+ if (node.type === 'text') {
122
+ const text = $(node).text();
123
+ if (text.trim()) {
124
+ output += text.replace(/\s+/g, ' ');
125
+ }
126
+ } else if (node.type === 'tag') {
127
+ const tagName = node.tagName?.toLowerCase();
128
+ const $node = $(node);
129
+
130
+ switch (tagName) {
131
+ case 'h1':
132
+ output += '\n\n' + theme.title(' ══ ' + $node.text().trim() + ' ══') + '\n\n';
133
+ break;
134
+ case 'h2':
135
+ output += '\n\n' + theme.title(' ── ' + $node.text().trim() + ' ──') + '\n';
136
+ break;
137
+ case 'h3':
138
+ output += '\n\n' + theme.accent(' ▸ ' + $node.text().trim()) + '\n';
139
+ break;
140
+ case 'h4':
141
+ case 'h5':
142
+ case 'h6':
143
+ output += '\n' + theme.accent(' ' + $node.text().trim()) + '\n';
144
+ break;
145
+ case 'p':
146
+ output += '\n ' + this._processElement($, $node, depth).trim() + '\n';
147
+ break;
148
+ case 'br':
149
+ output += '\n';
150
+ break;
151
+ case 'hr':
152
+ output += '\n' + theme.muted(' ' + '─'.repeat(60)) + '\n';
153
+ break;
154
+ case 'a': {
155
+ const href = $node.attr('href') || '';
156
+ const text = $node.text().trim();
157
+ if (text && href) {
158
+ const linkIdx = this.links.length + 1;
159
+ this.links.push({ text, url: href });
160
+ output += theme.link(text) + theme.muted(` [${linkIdx}]`);
161
+ } else if (text) {
162
+ output += text;
163
+ }
164
+ break;
165
+ }
166
+ case 'img': {
167
+ const src = $node.attr('src') || '';
168
+ const alt = $node.attr('alt') || 'image';
169
+ if (src) {
170
+ this.images.push({ src, alt });
171
+ output += theme.muted(`[IMG: ${alt}]`);
172
+ }
173
+ break;
174
+ }
175
+ case 'strong':
176
+ case 'b':
177
+ output += theme.highlight($node.text().trim());
178
+ break;
179
+ case 'em':
180
+ case 'i':
181
+ output += theme.info($node.text().trim());
182
+ break;
183
+ case 'code':
184
+ output += theme.warning('`' + $node.text().trim() + '`');
185
+ break;
186
+ case 'pre': {
187
+ const code = $node.text().trim();
188
+ output += '\n' + theme.muted(' ┌─ code ────────────────────') + '\n';
189
+ code.split('\n').forEach(line => {
190
+ output += theme.muted(' │ ') + theme.secondary(line) + '\n';
191
+ });
192
+ output += theme.muted(' └─────────────────────────────') + '\n';
193
+ break;
194
+ }
195
+ case 'ul':
196
+ $node.children('li').each((_, li) => {
197
+ output += '\n ' + theme.accent(' • ') + this._processElement($, $(li), depth + 1).trim();
198
+ });
199
+ output += '\n';
200
+ break;
201
+ case 'ol':
202
+ $node.children('li').each((i, li) => {
203
+ output += '\n ' + theme.accent(` ${i + 1}. `) + this._processElement($, $(li), depth + 1).trim();
204
+ });
205
+ output += '\n';
206
+ break;
207
+ case 'li':
208
+ output += this._processElement($, $node, depth + 1);
209
+ break;
210
+ case 'blockquote':
211
+ output += '\n' + theme.muted(' │ ') + theme.info(this._processElement($, $node, depth + 1).trim()) + '\n';
212
+ break;
213
+ case 'table': {
214
+ output += '\n';
215
+ $node.find('tr').each((_, tr) => {
216
+ const cells = [];
217
+ $(tr).find('th, td').each((__, cell) => {
218
+ cells.push($(cell).text().trim());
219
+ });
220
+ output += ' ' + theme.muted('│') + cells.map(c => ` ${c} `).join(theme.muted('│')) + theme.muted('│') + '\n';
221
+ });
222
+ output += '\n';
223
+ break;
224
+ }
225
+ case 'div':
226
+ case 'section':
227
+ case 'article':
228
+ case 'main':
229
+ case 'span':
230
+ case 'figure':
231
+ case 'figcaption':
232
+ output += this._processElement($, $node, depth);
233
+ break;
234
+ default:
235
+ output += this._processElement($, $node, depth);
236
+ }
237
+ }
238
+ });
239
+
240
+ return output;
241
+ }
242
+
243
+ _cleanOutput(text) {
244
+ return text
245
+ .replace(/\n{4,}/g, '\n\n\n')
246
+ .replace(/[ \t]+$/gm, '')
247
+ .trim();
248
+ }
249
+
250
+ getLinks() {
251
+ return this.links;
252
+ }
253
+
254
+ getImages() {
255
+ return this.images;
256
+ }
257
+ }
258
+
259
+ export default new Renderer();
package/src/search.js ADDED
@@ -0,0 +1,113 @@
1
+ import axios from 'axios';
2
+ import config from './config.js';
3
+ import { theme } from './themes.js';
4
+
5
+ class SearchEngine {
6
+ constructor() {
7
+ this.baseUrl = config.get('searxng');
8
+ this.lastResults = [];
9
+ }
10
+
11
+ async search(query, options = {}) {
12
+ const {
13
+ engine = null,
14
+ type = 'general',
15
+ time = null,
16
+ page = 1,
17
+ language = 'en',
18
+ } = options;
19
+
20
+ const params = {
21
+ q: query,
22
+ format: 'json',
23
+ categories: type,
24
+ pageno: page,
25
+ language,
26
+ };
27
+
28
+ if (engine) {
29
+ params.engines = engine;
30
+ }
31
+
32
+ if (time) {
33
+ const timeMap = {
34
+ day: 'day',
35
+ week: 'week',
36
+ month: 'month',
37
+ year: 'year',
38
+ };
39
+ params.time_range = timeMap[time] || time;
40
+ }
41
+
42
+ const proxy = config.get('proxy');
43
+ const axiosConfig = {
44
+ params,
45
+ headers: {
46
+ 'User-Agent': config.get('userAgent'),
47
+ Accept: 'application/json',
48
+ },
49
+ timeout: 15000,
50
+ };
51
+
52
+ if (proxy) {
53
+ axiosConfig.proxy = {
54
+ host: proxy.split(':')[0],
55
+ port: parseInt(proxy.split(':')[1]),
56
+ };
57
+ }
58
+
59
+ const response = await axios.get(`${this.baseUrl}/search`, axiosConfig);
60
+ const data = response.data;
61
+
62
+ this.lastResults = (data.results || []).map((r, i) => ({
63
+ index: i + 1,
64
+ title: r.title || 'Untitled',
65
+ url: r.url || '',
66
+ content: r.content || '',
67
+ engine: r.engine || '',
68
+ category: r.category || type,
69
+ publishedDate: r.publishedDate || null,
70
+ thumbnail: r.thumbnail || null,
71
+ img_src: r.img_src || null,
72
+ }));
73
+
74
+ return {
75
+ results: this.lastResults,
76
+ query: data.query || query,
77
+ number_of_results: data.number_of_results || this.lastResults.length,
78
+ suggestions: data.suggestions || [],
79
+ infoboxes: data.infoboxes || [],
80
+ };
81
+ }
82
+
83
+ async multiSearch(query, options = {}) {
84
+ const categories = ['general', 'images', 'videos', 'news'];
85
+ const results = {};
86
+
87
+ const promises = categories.map(async (cat) => {
88
+ try {
89
+ const res = await this.search(query, { ...options, type: cat });
90
+ results[cat] = res.results.slice(0, 5);
91
+ } catch {
92
+ results[cat] = [];
93
+ }
94
+ });
95
+
96
+ await Promise.all(promises);
97
+ return results;
98
+ }
99
+
100
+ async imageSearch(query, options = {}) {
101
+ return this.search(query, { ...options, type: 'images' });
102
+ }
103
+
104
+ getLastResults() {
105
+ return this.lastResults;
106
+ }
107
+
108
+ getResultByIndex(idx) {
109
+ return this.lastResults[idx - 1] || null;
110
+ }
111
+ }
112
+
113
+ export default new SearchEngine();
@@ -0,0 +1,37 @@
1
+ import config from './config.js';
2
+
3
+ class SessionManager {
4
+ getSessions() {
5
+ return config.get('sessions') || {};
6
+ }
7
+
8
+ saveSession(domain, cookies) {
9
+ const sessions = this.getSessions();
10
+ sessions[domain] = {
11
+ cookies,
12
+ savedAt: new Date().toISOString(),
13
+ };
14
+ config.set('sessions', sessions);
15
+ }
16
+
17
+ getSession(domain) {
18
+ const sessions = this.getSessions();
19
+ return sessions[domain] || null;
20
+ }
21
+
22
+ removeSession(domain) {
23
+ const sessions = this.getSessions();
24
+ delete sessions[domain];
25
+ config.set('sessions', sessions);
26
+ }
27
+
28
+ clearAll() {
29
+ config.set('sessions', {});
30
+ }
31
+
32
+ listDomains() {
33
+ return Object.keys(this.getSessions());
34
+ }
35
+ }
36
+
37
+ export default new SessionManager();
package/src/stats.js ADDED
@@ -0,0 +1,66 @@
1
+ import config from './config.js';
2
+
3
+ class StatsManager {
4
+ constructor() {
5
+ this.sessionStart = Date.now();
6
+ }
7
+
8
+ incrementSearch() {
9
+ const stats = config.get('stats');
10
+ stats.searchCount = (stats.searchCount || 0) + 1;
11
+ config.set('stats', stats);
12
+ }
13
+
14
+ incrementPageVisit() {
15
+ const stats = config.get('stats');
16
+ stats.pagesVisited = (stats.pagesVisited || 0) + 1;
17
+ config.set('stats', stats);
18
+ }
19
+
20
+ getStats() {
21
+ const stats = config.get('stats');
22
+ const now = Date.now();
23
+ const sessionTime = now - this.sessionStart;
24
+ const totalTime = (stats.totalTime || 0) + sessionTime;
25
+
26
+ return {
27
+ searchCount: stats.searchCount || 0,
28
+ pagesVisited: stats.pagesVisited || 0,
29
+ sessionTime: this.formatTime(sessionTime),
30
+ totalTime: this.formatTime(totalTime),
31
+ bookmarkCount: (config.get('bookmarks') || []).length,
32
+ historyCount: (config.get('history') || []).length,
33
+ };
34
+ }
35
+
36
+ formatTime(ms) {
37
+ const seconds = Math.floor(ms / 1000);
38
+ const minutes = Math.floor(seconds / 60);
39
+ const hours = Math.floor(minutes / 60);
40
+
41
+ if (hours > 0) {
42
+ return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
43
+ }
44
+ if (minutes > 0) {
45
+ return `${minutes}m ${seconds % 60}s`;
46
+ }
47
+ return `${seconds}s`;
48
+ }
49
+
50
+ saveTotalTime() {
51
+ const stats = config.get('stats');
52
+ stats.totalTime = (stats.totalTime || 0) + (Date.now() - this.sessionStart);
53
+ config.set('stats', stats);
54
+ }
55
+
56
+ reset() {
57
+ config.set('stats', {
58
+ searchCount: 0,
59
+ pagesVisited: 0,
60
+ startTime: Date.now(),
61
+ totalTime: 0,
62
+ });
63
+ }
64
+ }
65
+
66
+ export default new StatsManager();
package/src/tabs.js ADDED
@@ -0,0 +1,147 @@
1
+ import config from './config.js';
2
+
3
+ class TabManager {
4
+ constructor() {
5
+ this.tabs = [this._createTab('New Tab')];
6
+ this.activeTab = 0;
7
+ }
8
+
9
+ _createTab(title = 'New Tab') {
10
+ return {
11
+ id: Date.now(),
12
+ title,
13
+ url: null,
14
+ html: null,
15
+ content: null,
16
+ links: [],
17
+ history: [],
18
+ historyIndex: -1,
19
+ };
20
+ }
21
+
22
+ newTab(title) {
23
+ if (this.tabs.length >= config.get('maxTabs')) {
24
+ throw new Error(`Maximum tabs (${config.get('maxTabs')}) reached`);
25
+ }
26
+ const tab = this._createTab(title);
27
+ this.tabs.push(tab);
28
+ this.activeTab = this.tabs.length - 1;
29
+ return this.activeTab;
30
+ }
31
+
32
+ closeTab(index) {
33
+ if (index === undefined) index = this.activeTab;
34
+ if (this.tabs.length <= 1) {
35
+ throw new Error('Cannot close the last tab');
36
+ }
37
+ if (index < 0 || index >= this.tabs.length) {
38
+ throw new Error('Invalid tab index');
39
+ }
40
+ this.tabs.splice(index, 1);
41
+ if (this.activeTab >= this.tabs.length) {
42
+ this.activeTab = this.tabs.length - 1;
43
+ }
44
+ return this.activeTab;
45
+ }
46
+
47
+ switchTab(index) {
48
+ if (index < 0 || index >= this.tabs.length) {
49
+ throw new Error(`Tab ${index + 1} does not exist`);
50
+ }
51
+ this.activeTab = index;
52
+ return this.getCurrentTab();
53
+ }
54
+
55
+ getCurrentTab() {
56
+ return this.tabs[this.activeTab];
57
+ }
58
+
59
+ setCurrentPage(url, title, html, content, links) {
60
+ const tab = this.getCurrentTab();
61
+
62
+ // Add to tab history
63
+ if (tab.url) {
64
+ tab.history = tab.history.slice(0, tab.historyIndex + 1);
65
+ tab.history.push({
66
+ url: tab.url,
67
+ title: tab.title,
68
+ html: tab.html,
69
+ content: tab.content,
70
+ links: tab.links,
71
+ });
72
+ tab.historyIndex = tab.history.length - 1;
73
+ }
74
+
75
+ tab.url = url;
76
+ tab.title = title || 'Untitled';
77
+ tab.html = html;
78
+ tab.content = content;
79
+ tab.links = links || [];
80
+ }
81
+
82
+ goBack() {
83
+ const tab = this.getCurrentTab();
84
+ if (tab.historyIndex < 0) {
85
+ throw new Error('No previous page');
86
+ }
87
+
88
+ const prev = tab.history[tab.historyIndex];
89
+ tab.historyIndex--;
90
+
91
+ // Save current to forward
92
+ const currentPage = {
93
+ url: tab.url,
94
+ title: tab.title,
95
+ html: tab.html,
96
+ content: tab.content,
97
+ links: tab.links,
98
+ };
99
+
100
+ tab.url = prev.url;
101
+ tab.title = prev.title;
102
+ tab.html = prev.html;
103
+ tab.content = prev.content;
104
+ tab.links = prev.links;
105
+
106
+ // Store current page for forward
107
+ if (tab.historyIndex + 2 < tab.history.length) {
108
+ tab.history[tab.historyIndex + 2] = currentPage;
109
+ } else {
110
+ tab.history.push(currentPage);
111
+ }
112
+
113
+ return tab;
114
+ }
115
+
116
+ goForward() {
117
+ const tab = this.getCurrentTab();
118
+ if (tab.historyIndex + 2 >= tab.history.length) {
119
+ throw new Error('No next page');
120
+ }
121
+ tab.historyIndex += 2;
122
+
123
+ const next = tab.history[tab.historyIndex];
124
+ tab.url = next.url;
125
+ tab.title = next.title;
126
+ tab.html = next.html;
127
+ tab.content = next.content;
128
+ tab.links = next.links;
129
+
130
+ return tab;
131
+ }
132
+
133
+ getAllTabs() {
134
+ return this.tabs.map((tab, i) => ({
135
+ index: i + 1,
136
+ title: tab.title,
137
+ url: tab.url,
138
+ active: i === this.activeTab,
139
+ }));
140
+ }
141
+
142
+ getActiveIndex() {
143
+ return this.activeTab;
144
+ }
145
+ }
146
+
147
+ export default new TabManager();