ebrowser v1.0.34
[uweb.git] / misc / ebrowser / index.html
blob4ad435777c620d1c5d65e2436de4c59420c3b4e5
1 <!--
2 Copyright (C) 2024 Richard Hao Cao
3 Ebrowser is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
5 Ebrowser is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
7 You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
8 -->
9 <!DOCTYPE html><html><head><meta charset="UTF-8">
10 <style>
11 html{
12 height: 100%;
13 overflow: hidden;
15 body{
16 display: flex;
17 flex-direction: column;
18 height: 100%;
19 margin-top: 1px;
21 div.webviews{
22 display: flex;
23 flex-direction: column;
24 flex-grow:1;
26 webview{display: none;width:100%;height:100%}
27 .curWV{display: inherit !important;}
28 .autocomplete-active {
29 background-color: DodgerBlue !important;
30 color: #ffffff;
32 .invis{display: none}
33 /*the container must be positioned relative:*/
34 .autocomplete {
35 position: relative;
36 display: inline-block;
37 width:100%;
39 .autocomplete-items {
40 position: absolute;
41 border: 1px solid #d4d4d4;
42 border-bottom: none;
43 border-top: none;
44 z-index: 99;
45 /*position the autocomplete items to be the same width as the container:*/
46 top: 100%;
47 left: 0;
48 right: 0;
50 .autocomplete-items div {
51 cursor: pointer;
52 background-color: #fff;
54 .autocomplete-items div:hover {
55 background-color: #e9e9e9;
57 </style>
58 <script>
59 const fs = require('fs');
60 const path = require('path');
61 const readline = require('readline');
62 var iTab = 0;
63 var tabs;
64 var engines = {};
65 var mapKeys = {};
66 var closedUrls = [];
67 var autocStrArray = [];
68 var defaultSE = "https://www.bing.com/search?q=%s";
69 var historyFile = path.join(__dirname,'history.rec');
70 var bHistory = false;
71 var bQueryHistory = false;
72 var autocMode = 0; //0 for substring, 1 for startsWith
73 const JSPREFIX_LOAD = "(async ()=>{let d=document;async function _loadJs(u){var a=d.createElement('script');a.type='text/javascript';a.async=false;a.src=u;d.body.appendChild(a);await new Promise(resolve=>a.onload=resolve)}";
74 const BML_md = JSPREFIX_LOAD + "await _loadJs('https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js');let b=d.body;b.innerHTML=marked.parse(b.textContent)})()";
76 let lastKeys;
77 let lastKeys_millis = 0;
79 fs.readFile(path.join(__dirname,'search.json'), 'utf8', (err, jsonString) => {
80 if (err) {
81 coloncommand(":js fetch2file(repositoryurl,'search.json')");
82 return;
84 initSearchEngines(jsonString,false);
85 });
86 fs.readFile(path.join(__dirname,'mapkeys.json'), 'utf8', (err, jsonStr) => {
87 if (err) {
88 coloncommand(":js fetch2file(repositoryurl,'mapkeys.json')");
89 return;
91 try {
92 mapKeys = JSON.parse(jsonStr);
93 }catch(e){}
94 });
95 appendAutoc_rec(path.join(__dirname,'default.autoc'),null);
96 appendAutoc_rec(path.join(__dirname,'bookmark.rec'),' ');
98 function initSearchEngines(jsonStr){
99 try{
100 let val1st;
101 engines=JSON.parse(jsonStr, (key, value)=>{
102 if(!val1st && !(/^\d+$/u.test(key))) val1st=value;
103 return value;
105 if(val1st) defaultSE=val1st;
106 }catch(e){}
108 function save(filePath, u8array){
109 //alert(Object.prototype.toString.call(u8array))
110 fs.writeFile (filePath, u8array, (err) => {
111 if (err) {
112 console.error (err);
113 return;
117 function print2PDF(filePath, options){
118 tabs.children[iTab].printToPDF(options)
119 .then(u8array=>save(filePath,u8array));
121 function bookmark(args){//b [filenamestem] url title :bookmark
122 let bmFileName = "bookmark.rec";
123 let tab = tabs.children[iTab];
124 let url = tab.getURL();
125 if(args.length>1)
126 bmFileName = args[1]+".rec";
127 let title = tab.getTitle();
128 let line = title + " " + url + "\n";
129 fs.appendFile(path.join(__dirname,bmFileName), line, (err)=>{});
131 function switchTab(i){
132 let tab = tabs.children[iTab];
133 if(document.activeElement == tab) tab.blur();
134 tab.classList.remove('curWV');
135 iTab = i;
136 tabs.children[iTab].classList.add('curWV');
138 function cbFinishLoad(e){
139 let tab = e.target;
140 let js = tab.dataset.jsonce;
141 if(js){
142 tab.dataset.jsonce = null;
143 tab.executeJavaScript(js,false);
145 if(!bHistory) return;
146 let histItem = tab.getTitle()+" "+tab.getURL()+"\n";
147 fs.appendFile(historyFile, histItem, (err) => {});
149 function initTab(tab){
150 tab.allowpopups = true;
151 tab.addEventListener('did-finish-load',cbFinishLoad);
153 function newTab(){
154 var tab = document.createElement('webview');
155 initTab(tab);
156 tabs.appendChild(tab);
158 function tabInc(num){
159 let nTabs = tabs.children.length;
160 if(nTabs<2) return;
161 let i = iTab +num;
162 if(i>=nTabs) i=0;
163 switchTab(i);
165 function tabDec(num){
166 let nTabs = tabs.children.length;
167 if(nTabs<2) return;
168 let i = iTab +num;
169 if(i<0) i=nTabs-1;
170 switchTab(i);
172 function tabClose(){
173 let nTabs = tabs.children.length;
174 if(nTabs<2) return "";//no remain tab
175 let tab = tabs.children[iTab];
176 closedUrls.push(tab.getURL());
177 if(document.activeElement == tab) tab.blur();
178 tabs.removeChild(tab);
179 nTabs--;
180 if(iTab>=nTabs) iTab=iTab-1;
181 tabs.children[iTab].classList.add('curWV');
182 return getWinTitle();
184 function tabJS(js){
185 tabs.children[iTab].executeJavaScript(js,false);
187 function getWinTitle(){
188 let t=tabs.children[iTab];
189 let title = (iTab+1) + '/' + tabs.children.length;
190 try{title=title+' '+t.getTitle()+' '+t.getURL()}catch(e){}
191 return title
193 async function appendAutoc_rec(filename, delimit){
194 try{
195 const readInterface = readline.createInterface ({
196 input: fs.createReadStream (filename, 'utf8'),
199 for await (const line of readInterface) {
200 let iS;
201 if(delimit && (iS=line.lastIndexOf(delimit))>0){
202 autocStrArray.push(line.substring(iS+1));
203 }else
204 autocStrArray.push(line);
206 }catch(e){return;}
208 function keyPress(e){
209 var inputE = document.forms[0].q;
210 if (e.altKey||e.metaKey)
211 return;
212 var key = e.key;
213 if(e.ctrlKey){
214 switch(key){
215 case "Home":
216 tabJS("window.scrollTo(0,0)");
217 return;
218 case "End":
219 tabJS("window.scrollTo(0,document.body.scrollHeight)");
220 return;
223 return;
225 SCROLL: do {
226 let h = -32;
227 switch(key){
228 case " ":
229 if(inputE === document.activeElement) return;
230 if(e.shiftKey){
231 h = -3*document.documentElement.clientHeight/4;
232 break;
234 case "PageDown":
235 h = 3*document.documentElement.clientHeight/4;
236 break;
237 case "PageUp":
238 h = -3*document.documentElement.clientHeight/4;
239 break;
240 case "ArrowDown":
241 h = 32;
242 case "ArrowUp":
243 if(inputE === document.activeElement &&
244 0!==inputE.nextElementSibling.children.length)
245 return;
246 break;
247 default:
248 break SCROLL;
250 let js = `javascript:window.scrollBy(0,${h})`;
251 tabJS(js);
252 return;
253 }while(false);
255 if(inputE === document.activeElement){
256 if (9===e.keyCode){//tab completion
258 return;
260 var curMillis = Date.now();
261 if(curMillis-lastKeys_millis>1000)
262 lastKeys = null;
263 lastKeys_millis = curMillis;
265 switch (key) {
266 case "!":
267 case "/":
268 case ":":
269 inputE.value = "";
270 inputE.focus();
271 lastKeys = null;
272 return;
274 lastKeys = !lastKeys ? key : lastKeys + key;
275 let cmds = mapKeys[lastKeys];
276 if(cmds){//try to run cmds
277 let keyLen = lastKeys.length;
278 setTimeout(()=>{
279 if(lastKeys.length != keyLen) return;
280 lastKeys = null;
281 for(var cmd of cmds.split("\n"))
282 handleQuery(cmd);
283 }, 500);
286 function getQ(){return document.forms[0].q.value;}
287 function bang(query, iSpace){
288 let se=defaultSE;
290 let name = query.slice(0,iSpace);
291 let engine = engines[name];
292 if(engine){
293 se = engine;
294 query = query.substring(iSpace+1);
297 return se.replace('%s',query);
299 function coloncommand(q){
300 document.title = q;
302 function coloncommand_render(cmd){
303 args = cmd.substring(1).split(/\s+/);
304 switch(args[0]){
305 case "ac":
306 autoc(args);
307 return;
308 case "b":
309 bookmark(args);
310 return;
311 case "bjs":
312 eval(cmd.slice(5));
313 return;
314 case "bml":
315 bml(args);
316 return;
317 case "pdf":
318 savePdf(args);
319 return;
323 function autoc(args){
324 if(2!=args.length) return;
325 let fpath = path.join(__dirname,args[1]);
326 let fname = fpath;
327 let delimit = ' ';
328 if (!fs.existsSync(fname)){
329 fname = fpath+".autoc";
330 if (!fs.existsSync(fname))
331 fname = fpath+".rec";
332 else
333 delimit = null;
335 appendAutoc_rec(fname,delimit);
337 function bml(args){
338 if(2!=args.length) return;
339 let filename = args[1]+".js";
340 fs.readFile(path.join(__dirname,filename), 'utf8', (err,str) => {
341 if (err) return;
342 tabs.children[iTab].executeJavaScript(str,false);
345 function savePdf(args){
346 let filename = "ebrowser.pdf";
347 let options = {};
348 if(1<args.length){
349 let c0 = args[1].charCodeAt(0);
350 let i = 1;
351 if(123!=c0){//not '{' options then it is filename
352 filename = args[1] + ".pdf";
353 i = 2;
355 if(i==args.length-1){//:Pdf [filename] {...}
356 if(2==args[i].length){// '{}'
357 let width = document.body.clientWidth/96;
358 tabs.children[iTab].executeJavaScript("document.documentElement.scrollHeight",
359 false).then((h)=>{
360 let opts = {
361 printBackground:true,
362 pageSize:{width:width,height:h/96}};
363 print2PDF(filename,opts);
365 return;
366 }else{
367 try {
368 options = JSON.parse(args[i]);
369 }catch(e){};
373 print2PDF(filename,options);
375 function bangcommand(q){
376 let iS = q.indexOf(' ',1);
377 if(iS<0) iS=q.length;
378 let fname = q.substring(1,iS);
379 let fpath = path.join(__dirname,fname+'.js');
380 if (fs.existsSync(fpath)) {
381 fs.readFile(fpath, 'utf8',(err, js)=>{
382 if (err) {
383 console.log(err);
384 return;
386 const prefix = "(function(){";
387 const postfix = "})(`";
388 const end ="`)";
389 const fjs = `${prefix}${js}${postfix}${q}${end}`;
390 eval(fjs);
394 function recQueryHistory(q){
395 if(bQueryHistory)
396 fs.appendFile(path.join(__dirname,"history.autoc"), q, (err)=>{});
398 function handleQuery(q){
399 if(q.length>1){
400 let c0=q.charCodeAt(0);
401 switch(c0){
402 case 33://"!"
403 bangcommand(q);
404 recQueryHistory(q);
405 return;
406 case 47://"/"
407 tabs.children[iTab].findInPage(q.substring(1));
408 return;
409 case 58://':'
410 let c1=q.charCodeAt(1);
411 if(c1>98 && 112!=c1)
412 coloncommand(q);
413 else
414 coloncommand_render(q);
415 recQueryHistory(q);
416 return;
419 var url=q;
420 NOREC: do{
421 do {
422 if(q.length>12){
423 let c6 = q.charCodeAt(6);
424 if(47===c6){// '/'
425 let c5 = q.charCodeAt(5);
426 if(47===c5 && 58===q.charCodeAt(4))//http/file urls
427 break NOREC;
428 if(58===c5 && 47===q.charCodeAt(7))//https://
429 break NOREC;
430 }else if(q.startsWith("javascript:")){
431 tabs.children[iTab].executeJavaScript(q.substring(11),false);
432 recQueryHistory(q);
433 return;
434 }else if(q.startsWith("view-source:")) break;
435 else if(q.startsWith("data:")) break;
437 let iS = q.indexOf(' ');
438 if(iS<0){
439 if(q.length>5 && 58===q.charCodeAt(5)){// about:
440 break;
442 if(q.indexOf('.')>0){
443 url = 'https://'+q;
444 break NOREC;
446 url = defaultSE.replace('%s',q);
447 break;
449 url = bang(q, iS);
450 }while(false);
451 recQueryHistory(q);
452 }while(false);
453 tabs.children[iTab].src=url;
455 function autocomplete(inp,container,arr) {
456 var currentFocus;
457 function clickItem(e){inp.value = e.target.dataset.str;}
458 function appendElement(el,dataindex){
459 el.dataset.str = arr[dataindex];
460 el.addEventListener("click", clickItem);
461 container.appendChild(el);
463 inp.addEventListener("input", function(e) {
464 const MAXITEMS = 10;
465 var b, i, val = this.value;
466 closeAllLists();
467 if (!val) { return false;}
468 currentFocus = -1;
469 switch(autocMode){
470 case 0:
471 for (i = 0; i < arr.length; i++) {
472 let iStr = arr[i].indexOf(val);
473 if(iStr<0) continue;
475 b = document.createElement("DIV");
476 b.innerHTML = arr[i].substr(0,iStr);
477 b.innerHTML += "<strong>" + arr[i].substr(iStr, val.length) + "</strong>";
478 b.innerHTML += arr[i].substr(iStr+val.length);
479 appendElement(b,i);
480 if(container.children.length>MAXITEMS) break;
483 return;
484 case 1://startsWith
485 for (i = 0; i < arr.length; i++) {
486 if (arr[i].substr(0, val.length) === val) {
487 b = document.createElement("DIV");
488 b.innerHTML = "<strong>" + arr[i].substr(0, val.length) + "</strong>";
489 b.innerHTML += arr[i].substr(val.length);
490 appendElement(b,i);
491 if(container.children.length>MAXITEMS) break;
496 inp.addEventListener("keydown", function(e) {
497 var x = container.getElementsByTagName("div");
498 if (0===x.length) return false;
499 if (e.keyCode == 40) {//downarrow
500 currentFocus++;
501 addActive(x);
502 } else if (e.keyCode == 38) { //up
503 currentFocus--;
504 addActive(x);
505 } else if (e.keyCode == 13) {
506 if (currentFocus > -1) {
507 e.preventDefault();
508 if (x) x[currentFocus].click();
509 currentFocus = -1;
511 closeAllLists();
514 function addActive(x) {
515 removeActive(x);
516 if (currentFocus >= x.length) currentFocus = 0;
517 if (currentFocus < 0) currentFocus = (x.length - 1);
518 x[currentFocus].classList.add("autocomplete-active");
520 function removeActive(x) {
521 for (var i = 0; i < x.length; i++) {
522 x[i].classList.remove("autocomplete-active");
525 function closeAllLists() {
526 container.innerHTML = '';
528 inp.addEventListener("blur", function () {
529 setTimeout(()=>container.classList.add("invis"),200);
531 inp.addEventListener("focus", function () {
532 container.classList.remove("invis");
535 </script>
536 </head>
537 <body>
538 <form class="autocomplete" autocomplete="off" action="javascript:handleQuery(getQ())">
539 <input type="text" name=q style="width:100%" autofocus>
540 <div class="autocomplete-items"></div>
541 </form>
542 <div class="webviews">
543 <webview class="curWV" allowpopups></webview>
544 </div>
545 <script>
546 tabs = document.body.children[1];
547 initTab(tabs.children[0]);
549 let inp = document.forms[0].q;
550 autocomplete(inp,inp.nextElementSibling,autocStrArray);
552 document.addEventListener('keydown', keyPress);
553 </script>
554 </body></html>