ebrowser v1.0.51: download.json to add user-defined context-menu
[uweb.git] / misc / ebrowser / index.html
blob2555d83e587e01ea679519d94d06c88480dfa333
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 let sitecssP = path.join(__dirname,"sitecss");
73 let sitejsP = path.join(__dirname,"sitejs");
74 let gjsA = [];
75 let gcssA = [];
76 let gcssJSA = [];
77 var bDomainJS = fs.existsSync(sitejsP);
78 var bDomainCSS = fs.existsSync(sitecssP);
79 var autocMode = 0; //0 for substring, 1 for startsWith
80 const BML_head = "(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)}";
81 const BML_tail = "})()";
82 const BML_md = BML_head + "await _loadJs('https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js');let b=d.body;b.innerHTML=marked.parse(b.textContent)})()";
84 let lastKeys;
85 let lastKeys_millis = 0;
86 var lastVal;
87 fs.readFile(path.join(__dirname,'search.json'), 'utf8', (err, jsonString) => {
88 if (err) {
89 coloncommand(":js fetch2file(repositoryurl,'search.json')");
90 return;
92 initSearchEngines(jsonString,false);
93 });
94 fs.readFile(path.join(__dirname,'mapkeys.json'), 'utf8', (err, jsonStr) => {
95 if (err) {
96 coloncommand(":js fetch2file(repositoryurl,'mapkeys.json')");
97 return;
99 try {
100 mapKeys = JSON.parse(jsonStr);
101 }catch(e){}
103 appendAutoc_rec(path.join(__dirname,'default.autoc'),null);
104 appendAutoc_rec(path.join(__dirname,'bookmark.rec'),' ');
106 function initSearchEngines(jsonStr){
107 try{
108 let val1st;
109 engines=JSON.parse(jsonStr, (key, value)=>{
110 if(!val1st && !(/^\d+$/u.test(key))) val1st=value;
111 return value;
113 if(val1st) defaultSE=val1st;
114 }catch(e){}
116 function save(filePath, u8array){
117 //alert(Object.prototype.toString.call(u8array))
118 fs.writeFile (filePath, u8array, (err) => {
119 if (err) {
120 console.error (err);
121 return;
125 function print2PDF(filePath, options){
126 tabs.children[iTab].printToPDF(options)
127 .then(u8array=>save(filePath,u8array));
129 function bookmark(args){//b [filenamestem] url title :bookmark
130 let bmFileName = "bookmark.rec";
131 let tab = tabs.children[iTab];
132 let url = tab.getURL();
133 if(args.length>1)
134 bmFileName = args[1]+".rec";
135 let title = tab.getTitle();
136 let line = title + " " + url + "\n";
137 fs.appendFile(path.join(__dirname,bmFileName), line, (err)=>{});
139 function switchTab(i){
140 let tab = tabs.children[iTab];
141 if(document.activeElement == tab) tab.blur();
142 tab.classList.remove('curWV');
143 iTab = i;
144 tabs.children[iTab].classList.add('curWV');
146 async function loadJSFile(tab,jsF){
147 try {
148 let js = await fs.promises.readFile(jsF,'utf8');
149 tab.executeJavaScript(js,false);
150 }catch(e){}
152 async function loadCSSFile(tab,jsF){
153 try {
154 let js = await fs.promises.readFile(jsF,'utf8');
155 tab.insertCSS(css);
156 }catch(e){}
158 function cbStartLoading(e){
159 if(!bDomainCSS) return;
160 let tab = e.target;
161 let domain;
162 try{
163 domain = new URL(tab.getURL()).hostname;
164 }catch(e){return;}
165 let jsF = path.join(sitecssP, domain+".js");
166 loadJSFile(tab,jsF);
167 jsF = path.join(sitecssP, domain+".css");
168 loadCSSFile(tab,jsF);
169 gcssA.forEach((fname)=>{
170 jsF = path.join(__dirname, fname);
171 loadCSSFile(tab,jsF);
173 gcssJSA.forEach((fname)=>{
174 jsF = path.join(__dirname, fname);
175 loadJSFile(tab,jsF);
178 function cbFinishLoad(e){
179 let tab = e.target;
180 let url = tab.getURL();
181 if(bHistory){
182 let histItem = tab.getTitle()+" "+url+"\n";
183 fs.appendFile(historyFile, histItem, (err) => {});
185 let js = tab.dataset.jsonce;
186 if(js){
187 tab.dataset.jsonce = null;
188 tab.executeJavaScript(js,false);
190 if(bDomainJS){
191 let domain = new URL(url).hostname;
192 let jsF = path.join(sitejsP, domain+".js");
193 loadJSFile(tab,jsF);
195 gjsA.forEach((fname)=>{
196 let jsF = path.join(__dirname, fname);
197 loadJSFile(tab,jsF);
200 function initTab(tab){
201 tab.allowpopups = true;
202 tab.addEventListener('did-finish-load',cbFinishLoad);
203 tab.addEventListener('did-start-loading',cbStartLoading);
205 function newTab(){
206 var tab = document.createElement('webview');
207 initTab(tab);
208 tabs.appendChild(tab);
210 function tabInc(num){
211 let nTabs = tabs.children.length;
212 if(nTabs<2) return;
213 let i = iTab +num;
214 if(i>=nTabs) i=0;
215 switchTab(i);
217 function tabDec(num){
218 let nTabs = tabs.children.length;
219 if(nTabs<2) return;
220 let i = iTab +num;
221 if(i<0) i=nTabs-1;
222 switchTab(i);
224 function tabClose(){
225 let nTabs = tabs.children.length;
226 if(nTabs<2) return "";//no remain tab
227 let tab = tabs.children[iTab];
228 closedUrls.push(tab.getURL());
229 if(document.activeElement == tab) tab.blur();
230 tabs.removeChild(tab);
231 nTabs--;
232 if(iTab>=nTabs) iTab=iTab-1;
233 tabs.children[iTab].classList.add('curWV');
234 return getWinTitle();
236 function tabJS(js){
237 tabs.children[iTab].executeJavaScript(js,false);
239 function getWinTitle(){
240 let t=tabs.children[iTab];
241 let title = (iTab+1) + '/' + tabs.children.length;
242 try{title=title+' '+t.getTitle()+' '+t.getURL()}catch(e){}
243 return title
245 async function appendAutoc_rec(filename, delimit){
246 try{
247 const readInterface = readline.createInterface ({
248 input: fs.createReadStream (filename, 'utf8'),
251 for await (const line of readInterface) {
252 let iS;
253 if(delimit && (iS=line.lastIndexOf(delimit))>0){
254 autocStrArray.push(line.substring(iS+1));
255 }else
256 autocStrArray.push(line);
258 lastVal = null; //trigger full search
259 }catch(e){return;}
261 function keyPress(e){
262 var inputE = document.forms[0].q;
263 if (e.altKey||e.metaKey)
264 return;
265 var key = e.key;
266 if(e.ctrlKey){
267 switch(key){
268 case "Home":
269 tabJS("window.scrollTo(0,0)");
270 return;
271 case "End":
272 tabJS("window.scrollTo(0,document.body.scrollHeight)");
273 return;
276 return;
278 SCROLL: do {
279 let h = -32;
280 switch(key){
281 case " ":
282 if(inputE === document.activeElement) return;
283 if(e.shiftKey){
284 h = -3*document.documentElement.clientHeight/4;
285 break;
287 case "PageDown":
288 h = 3*document.documentElement.clientHeight/4;
289 break;
290 case "PageUp":
291 h = -3*document.documentElement.clientHeight/4;
292 break;
293 case "ArrowDown":
294 h = 32;
295 case "ArrowUp":
296 if(inputE === document.activeElement &&
297 0!==inputE.nextElementSibling.children.length)
298 return;
299 break;
300 default:
301 break SCROLL;
303 let js = `javascript:window.scrollBy(0,${h})`;
304 tabJS(js);
305 return;
306 }while(false);
308 if(inputE === document.activeElement){
309 if (9===e.keyCode){
310 e.preventDefault();
311 tabs.children[iTab].focus();
313 return;
315 var curMillis = Date.now();
316 if(curMillis-lastKeys_millis>1000)
317 lastKeys = null;
318 lastKeys_millis = curMillis;
320 switch (key) {
321 case "!":
322 case "/":
323 case ":":
324 inputE.value = "";
325 inputE.focus();
326 lastKeys = null;
327 return;
329 lastKeys = !lastKeys ? key : lastKeys + key;
330 let cmds = mapKeys[lastKeys];
331 if(cmds){//try to run cmds
332 let keyLen = lastKeys.length;
333 setTimeout(()=>{
334 if(lastKeys.length != keyLen) return;
335 lastKeys = null;
336 for(var cmd of cmds.split("\n"))
337 handleQuery(cmd);
338 }, 500);
341 function getQ(){return document.forms[0].q.value;}
342 function bang(query, iSpace){
343 let se=defaultSE;
345 let name = query.slice(0,iSpace);
346 let engine = engines[name];
347 if(engine){
348 se = engine;
349 query = query.substring(iSpace+1);
352 return se.replace('%s',query);
354 function coloncommand(q){
355 document.title = q;
357 function coloncommand_render(cmd){
358 args = cmd.substring(1).split(/\s+/);
359 switch(args[0]){
360 case "ac":
361 autoc(args);
362 return;
363 case "b":
364 bookmark(args);
365 return;
366 case "bjs":
367 eval(cmd.slice(5));
368 return;
369 case "bml":
370 bml(args);
371 return;
372 case "pdf":
373 savePdf(args);
374 return;
378 function autoc(args){
379 if(2!=args.length) return;
380 let fpath = path.join(__dirname,args[1]);
381 let fname = fpath;
382 let delimit = ' ';
383 if (!fs.existsSync(fname)){
384 fname = fpath+".autoc";
385 if (!fs.existsSync(fname))
386 fname = fpath+".rec";
387 else
388 delimit = null;
390 appendAutoc_rec(fname,delimit);
392 function bml(args){
393 if(2!=args.length) return;
394 let filename = args[1]+".js";
395 fs.readFile(path.join(__dirname,filename), 'utf8', (err,str) => {
396 if (err) return;
397 tabs.children[iTab].executeJavaScript(str,false);
400 function savePdf(args){
401 let filename = "ebrowser.pdf";
402 let options = {};
403 if(1<args.length){
404 let c0 = args[1].charCodeAt(0);
405 let i = 1;
406 if(123!=c0){//not '{' options then it is filename
407 filename = args[1] + ".pdf";
408 i = 2;
410 if(i==args.length-1){//:Pdf [filename] {...}
411 if(2==args[i].length){// '{}'
412 let width = document.body.clientWidth/96;
413 tabs.children[iTab].executeJavaScript("document.documentElement.scrollHeight",
414 false).then((h)=>{
415 let opts = {
416 printBackground:true,
417 pageSize:{width:width,height:h/96}};
418 print2PDF(filename,opts);
420 return;
421 }else{
422 try {
423 options = JSON.parse(args[i]);
424 }catch(e){};
428 print2PDF(filename,options);
430 function bangcommand(q){
431 let iS = q.indexOf(' ',1);
432 if(iS<0) iS=q.length;
433 let fname = q.substring(1,iS);
434 let fpath = path.join(__dirname,fname+'.js');
435 if (fs.existsSync(fpath)) {
436 fs.readFile(fpath, 'utf8',(err, js)=>{
437 if (err) {
438 console.log(err);
439 return;
441 const prefix = "(function(){";
442 const postfix = "})(`";
443 const end ="`)";
444 const fjs = `${prefix}${js}${postfix}${q}${end}`;
445 eval(fjs);
449 function recQueryHistory(q){
450 if(bQueryHistory)
451 fs.appendFile(path.join(__dirname,"history.autoc"), q, (err)=>{});
453 function handleQuery(q){
454 if(q.length>1){
455 let c0=q.charCodeAt(0);
456 switch(c0){
457 case 33://"!"
458 bangcommand(q);
459 recQueryHistory(q);
460 return;
461 case 47://"/"
462 tabs.children[iTab].findInPage(q.substring(1));
463 return;
464 case 58://':'
465 let c1=q.charCodeAt(1);
466 if(c1>98 && 112!=c1)
467 coloncommand(q);
468 else
469 coloncommand_render(q);
470 recQueryHistory(q);
471 return;
474 var url=q;
475 NOREC: do{
476 do {
477 if(q.length>12){
478 let c6 = q.charCodeAt(6);
479 if(58===q.charCodeAt(1)){
480 internalLink(q);
481 return;
482 }else if(47===c6){// '/'
483 let c5 = q.charCodeAt(5);
484 if(47===c5 && 58===q.charCodeAt(4))//http/file urls
485 break NOREC;
486 if(58===c5 && 47===q.charCodeAt(7))//https://
487 break NOREC;
488 }else if(q.startsWith("javascript:")){
489 tabs.children[iTab].executeJavaScript(q.substring(11),false);
490 recQueryHistory(q);
491 return;
492 }else if(q.startsWith("view-source:")) break;
493 else if(q.startsWith("data:")) break;
495 let iS = q.indexOf(' ');
496 if(iS<0){
497 if(q.length>5 && 58===q.charCodeAt(5)){// about:
498 break;
500 if(q.indexOf('.')>0){
501 url = 'https://'+q;
502 break NOREC;
504 url = defaultSE.replace('%s',q);
505 break;
507 url = bang(q, iS);
508 if(58===url.charCodeAt(1)){
509 internalLink(url);
510 recQueryHistory(q);
511 return;
513 }while(false);
514 recQueryHistory(q);
515 }while(false);
516 tabs.children[iTab].src=url;
518 function internalLink(url){
519 let cmd = url.charCodeAt(2);
520 let subcmd = url.charCodeAt(3);
521 let iColon = url.indexOf(':',2);
522 switch(cmd){
523 case 48://'0' i:0
524 switch(subcmd){
525 case 47://'/' i:0/
526 if(106===url.charCodeAt(4) && 115===url.charCodeAt(5) && 47===url.charCodeAt(6)){
527 //i:0/js/xx:[url]
528 let fname = url.slice(4,iColon);
529 let pname = path.join(__dirname,fname);
530 if(fs.existsSync(pname)){
531 (async ()=>{
532 try {
533 let js = await fs.promises.readFile(pname,'utf8');
534 let t=tabs.children[iTab];
535 t.dataset.jsonce=js;
536 t.src = url.substring(iColon+1);
537 }catch(e){}
538 })();
544 function autocomplete(inp,container,arr) {
545 var currentFocus;
546 function clickItem(e){inp.value = arr[e.target.dataset.index];}
547 function appendElement(el,dataindex){
548 el.dataset.index = dataindex;
549 el.addEventListener("click", clickItem);
550 container.appendChild(el);
552 function itemInnerHTML(b,str,val,iStr){
553 b.innerHTML = str.substr(0,iStr);
554 b.innerHTML += "<strong>" + str.substr(iStr, val.length) + "</strong>";
555 b.innerHTML += str.substr(iStr+val.length);
557 inp.addEventListener("input", function(e) {
558 const MAXITEMS = 30;
559 var b, i, val = this.value;
560 if (!val) { return false;}
561 currentFocus = -1;
562 let items = container.children;
563 let iBase = 0;
564 let lastV = lastVal;
565 lastVal = val;
566 if(val.startsWith(lastV)){
567 let itemsLen = items.length;
568 if(itemsLen<=0) return;
569 i = itemsLen -1;
570 iBase = items[i].dataset.index +1;
571 switch(autocMode){
572 case 0:
573 for (; i>=0; i--) {
574 b = items[i];
575 let str = arr[b.dataset.index];
576 let iStr = str.indexOf(val);
577 if(iStr<0) {
578 b.parentNode.removeChild(b);
579 continue;
581 itemInnerHTML(b,str,val,iStr);
583 break;
584 case 1:
585 for (; i>=0; i--) {
586 b = items[i];
587 let str = arr[b.dataset.index];
588 let oLen = lastval.length;
589 if(!str.startsWith(val.substring(oLen),oLen)) {
590 b.parentNode.removeChild(b);
591 continue;
593 itemInnerHTML(b,str,val,0);
595 break;
597 if(itemsLen<MAXITEMS)
598 return;
599 else
600 if(container.children.length>=MAXITEMS) return;
601 }else
602 closeAllLists();
603 i = iBase;
604 switch(autocMode){
605 case 0:
606 for (; i < arr.length; i++) {
607 let iStr = arr[i].indexOf(val);
608 if(iStr<0) continue;
610 b = document.createElement("DIV");
611 itemInnerHTML(b,arr[i],val,iStr);
612 appendElement(b,i);
613 if(container.children.length>=MAXITEMS) break;
616 return;
617 case 1://startsWith
618 for (; i < arr.length; i++) {
619 if (arr[i].startsWith(val)) {
620 b = document.createElement("DIV");
621 itemInnerHTML(b,arr[i],val,0);
622 appendElement(b,i);
623 if(container.children.length>=MAXITEMS) break;
628 inp.addEventListener("keydown", function(e) {
629 var x = container.getElementsByTagName("div");
630 if (0===x.length) return false;
631 if (e.keyCode == 40) {//downarrow
632 currentFocus++;
633 addActive(x);
634 } else if (e.keyCode == 38) { //up
635 currentFocus--;
636 addActive(x);
637 } else if (e.keyCode == 13) {
638 if (currentFocus > -1) {
639 e.preventDefault();
640 if (x) x[currentFocus].click();
641 currentFocus = -1;
643 closeAllLists();
644 lastVal = null;
647 function addActive(x) {
648 removeActive(x);
649 if (currentFocus >= x.length) currentFocus = 0;
650 if (currentFocus < 0) currentFocus = (x.length - 1);
651 x[currentFocus].classList.add("autocomplete-active");
653 function removeActive(x) {
654 for (var i = 0; i < x.length; i++) {
655 x[i].classList.remove("autocomplete-active");
658 function closeAllLists() {
659 container.innerHTML = '';
661 inp.addEventListener("blur", function () {
662 setTimeout(()=>container.classList.add("invis"),200);
664 inp.addEventListener("focus", function () {
665 container.classList.remove("invis");
668 </script>
669 </head>
670 <body>
671 <form class="autocomplete" autocomplete="off" action="javascript:handleQuery(getQ())">
672 <input type="text" name=q style="width:100%" autofocus>
673 <div class="autocomplete-items"></div>
674 </form>
675 <div class="webviews">
676 <webview class="curWV" allowpopups></webview>
677 </div>
678 <script>
679 tabs = document.body.children[1];
680 initTab(tabs.children[0]);
682 let inp = document.forms[0].q;
683 autocomplete(inp,inp.nextElementSibling,autocStrArray);
685 document.addEventListener('keydown', keyPress);
686 </script>
687 </body></html>