uweb updates to 1055
[uweb.git] / misc / ebrowser / index.html
blob488e7811d5bd88341e69a1c570ae7a0f9586599f
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 { ipcRenderer } = require('electron');
60 const fs = require('fs');
61 const path = require('path');
62 const readline = require('readline');
63 var iTab = 0;
64 var tabs;
65 var engines = {};
66 var mapKeys = {};
67 var closedUrls = [];
68 var autocStrArray = [];
69 var defaultSE = "https://www.bing.com/search?q=%s";
70 var historyFile = path.join(__dirname,'history.rec');
71 var bHistory = false;
72 var bQueryHistory = false;
73 let sitecssP = path.join(__dirname,"sitecss");
74 let sitejsP = path.join(__dirname,"sitejs");
75 let gjsA = [];
76 let gcssA = [];
77 let gcssJSA = [];
78 var bDomainJS = fs.existsSync(sitejsP);
79 var bDomainCSS = fs.existsSync(sitecssP);
80 var autocMode = 0; //0 for substring, 1 for startsWith
81 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)}";
82 const BML_tail = "})()";
83 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)})()";
85 let lastKeys;
86 let lastKeys_millis = 0;
87 var lastVal;
88 fs.readFile(path.join(__dirname,'search.json'), 'utf8', (err, jsonString) => {
89 if (err) {
90 coloncommand(":js fetch2file(repositoryurl,'search.json')");
91 return;
93 initSearchEngines(jsonString,false);
94 });
95 fs.readFile(path.join(__dirname,'mapkeys.json'), 'utf8', (err, jsonStr) => {
96 if (err) {
97 coloncommand(":js fetch2file(repositoryurl,'mapkeys.json')");
98 return;
100 try {
101 mapKeys = JSON.parse(jsonStr);
102 }catch(e){}
104 appendAutoc_rec(path.join(__dirname,'default.autoc'),null);
105 appendAutoc_rec(path.join(__dirname,'bookmark.rec'),' ');
107 function initSearchEngines(jsonStr){
108 try{
109 let val1st;
110 engines=JSON.parse(jsonStr, (key, value)=>{
111 if(!val1st && !(/^\d+$/u.test(key))) val1st=value;
112 return value;
114 if(val1st) defaultSE=val1st;
115 }catch(e){}
117 function save(filePath, u8array){
118 //alert(Object.prototype.toString.call(u8array))
119 fs.writeFile (filePath, u8array, (err) => {
120 if (err) {
121 console.error (err);
122 return;
126 function print2PDF(filePath, options){
127 tabs.children[iTab].printToPDF(options)
128 .then(u8array=>save(filePath,u8array));
130 function bookmark(args){//b [filenamestem] url title :bookmark
131 let bmFileName = "bookmark.rec";
132 let tab = tabs.children[iTab];
133 let url = tab.getURL();
134 if(args.length>1)
135 bmFileName = args[1]+".rec";
136 let title = tab.getTitle();
137 let line = title + " " + url + "\n";
138 fs.appendFile(path.join(__dirname,bmFileName), line, (err)=>{});
140 function switchTab(i){
141 let tab = tabs.children[iTab];
142 if(document.activeElement == tab) tab.blur();
143 tab.classList.remove('curWV');
144 iTab = i;
145 tabs.children[iTab].classList.add('curWV');
147 async function loadJSFile(tab,jsF){
148 try {
149 let js = await fs.promises.readFile(jsF,'utf8');
150 tab.executeJavaScript(js,false);
151 }catch(e){}
153 async function loadCSSFile(tab,jsF){
154 try {
155 let js = await fs.promises.readFile(jsF,'utf8');
156 tab.insertCSS(css);
157 }catch(e){}
159 function cbStartLoading(e){
160 if(!bDomainCSS) return;
161 let tab = e.target;
162 let domain;
163 try{
164 domain = new URL(tab.getURL()).hostname;
165 }catch(e){return;}
166 let jsF = path.join(sitecssP, domain+".js");
167 loadJSFile(tab,jsF);
168 jsF = path.join(sitecssP, domain+".css");
169 loadCSSFile(tab,jsF);
170 gcssA.forEach((fname)=>{
171 jsF = path.join(__dirname, fname);
172 loadCSSFile(tab,jsF);
174 gcssJSA.forEach((fname)=>{
175 jsF = path.join(__dirname, fname);
176 loadJSFile(tab,jsF);
179 function cbNavigate(e){
180 let url = e.url;
181 if(58===url.charCodeAt(1)){
182 e.preventDefault();
183 if(confirm("Proceed to execute risky operations: "+url))
184 internalLink(url);
185 else
186 document.forms[0].q.value = url;
189 function cbFinishLoad(e){
190 let tab = e.target;
191 let url = tab.getURL();
192 if(bHistory){
193 let histItem = tab.getTitle()+" "+url+"\n";
194 fs.appendFile(historyFile, histItem, (err) => {});
196 let js = tab.dataset.jsonce;
197 if(js){
198 tab.dataset.jsonce = null;
199 tab.executeJavaScript(js,false);
201 if(bDomainJS){
202 let domain = new URL(url).hostname;
203 let jsF = path.join(sitejsP, domain+".js");
204 loadJSFile(tab,jsF);
206 gjsA.forEach((fname)=>{
207 let jsF = path.join(__dirname, fname);
208 loadJSFile(tab,jsF);
211 function initTab(tab){
212 tab.allowpopups = true;
213 tab.addEventListener('did-finish-load',cbFinishLoad);
214 tab.addEventListener('did-start-loading',cbStartLoading);
215 tab.addEventListener('will-navigate',cbNavigate);
217 function newTab(){
218 var tab = document.createElement('webview');
219 initTab(tab);
220 tabs.appendChild(tab);
222 function tabInc(num){
223 let nTabs = tabs.children.length;
224 if(nTabs<2) return;
225 let i = iTab +num;
226 if(i>=nTabs) i=0;
227 switchTab(i);
229 function tabDec(num){
230 let nTabs = tabs.children.length;
231 if(nTabs<2) return;
232 let i = iTab +num;
233 if(i<0) i=nTabs-1;
234 switchTab(i);
236 function tabClose(){
237 let nTabs = tabs.children.length;
238 if(nTabs<2) return "";//no remain tab
239 let tab = tabs.children[iTab];
240 closedUrls.push(tab.getURL());
241 if(document.activeElement == tab) tab.blur();
242 tabs.removeChild(tab);
243 nTabs--;
244 if(iTab>=nTabs) iTab=iTab-1;
245 tabs.children[iTab].classList.add('curWV');
246 return getWinTitle();
248 function tabJS(js){
249 tabs.children[iTab].executeJavaScript(js,false);
251 function getWinTitle(){
252 let t=tabs.children[iTab];
253 let title = (iTab+1) + '/' + tabs.children.length;
254 try{title=title+' '+t.getTitle()+' '+t.getURL()}catch(e){}
255 return title
257 async function appendAutoc_rec(filename, delimit){
258 try{
259 const readInterface = readline.createInterface ({
260 input: fs.createReadStream (filename, 'utf8'),
263 for await (const line of readInterface) {
264 let iS;
265 if(delimit && (iS=line.lastIndexOf(delimit))>0){
266 autocStrArray.push(line.substring(iS+1));
267 }else
268 autocStrArray.push(line);
270 lastVal = null; //trigger full search
271 }catch(e){return;}
273 function keyPress(e){
274 var inputE = document.forms[0].q;
275 if (e.altKey||e.metaKey)
276 return;
277 var key = e.key;
278 if(e.ctrlKey){
279 switch(key){
280 case "Home":
281 tabJS("window.scrollTo(0,0)");
282 return;
283 case "End":
284 tabJS("window.scrollTo(0,document.body.scrollHeight)");
285 return;
288 return;
290 SCROLL: do {
291 let h = -32;
292 switch(key){
293 case " ":
294 if(inputE === document.activeElement) return;
295 if(e.shiftKey){
296 h = -3*document.documentElement.clientHeight/4;
297 break;
299 case "PageDown":
300 h = 3*document.documentElement.clientHeight/4;
301 break;
302 case "PageUp":
303 h = -3*document.documentElement.clientHeight/4;
304 break;
305 case "ArrowDown":
306 h = 32;
307 case "ArrowUp":
308 if(inputE === document.activeElement &&
309 0!==inputE.nextElementSibling.children.length)
310 return;
311 break;
312 default:
313 break SCROLL;
315 let js = `javascript:window.scrollBy(0,${h})`;
316 tabJS(js);
317 return;
318 }while(false);
320 if(inputE === document.activeElement){
321 if (9===e.keyCode){
322 e.preventDefault();
323 tabs.children[iTab].focus();
325 return;
327 var curMillis = Date.now();
328 if(curMillis-lastKeys_millis>1000)
329 lastKeys = null;
330 lastKeys_millis = curMillis;
332 switch (key) {
333 case "!":
334 case "/":
335 case ":":
336 inputE.value = "";
337 inputE.focus();
338 lastKeys = null;
339 return;
341 lastKeys = !lastKeys ? key : lastKeys + key;
342 let cmds = mapKeys[lastKeys];
343 if(cmds){//try to run cmds
344 let keyLen = lastKeys.length;
345 setTimeout(()=>{
346 if(lastKeys.length != keyLen) return;
347 lastKeys = null;
348 handleQueries(cmds);
349 }, 500);
352 function getQ(){return document.forms[0].q.value;}
353 function bang(query, iSpace){
354 let se=defaultSE;
356 let name = query.slice(0,iSpace);
357 let engine = engines[name];
358 if(engine){
359 se = engine;
360 query = query.substring(iSpace+1);
363 return se.replace('%s',query);
365 function coloncommand(q){
366 ipcRenderer.send("command",q);
368 function coloncommand_render(cmd){
369 args = cmd.substring(1).split(/\s+/);
370 switch(args[0]){
371 case "ac":
372 autoc(args);
373 return;
374 case "b":
375 bookmark(args);
376 return;
377 case "bjs":
378 eval(cmd.slice(5));
379 return;
380 case "bml":
381 bml(args);
382 return;
383 case "pdf":
384 savePdf(args);
385 return;
389 function autoc(args){
390 if(2!=args.length) return;
391 let fpath = path.join(__dirname,args[1]);
392 let fname = fpath;
393 let delimit = ' ';
394 if (!fs.existsSync(fname)){
395 fname = fpath+".autoc";
396 if (!fs.existsSync(fname))
397 fname = fpath+".rec";
398 else
399 delimit = null;
401 appendAutoc_rec(fname,delimit);
403 function bml(args){
404 if(2!=args.length) return;
405 let filename = args[1]+".js";
406 fs.readFile(path.join(__dirname,filename), 'utf8', (err,str) => {
407 if (err) return;
408 tabs.children[iTab].executeJavaScript(str,false);
411 function savePdf(args){
412 let filename = "ebrowser.pdf";
413 let options = {};
414 if(1<args.length){
415 let c0 = args[1].charCodeAt(0);
416 let i = 1;
417 if(123!=c0){//not '{' options then it is filename
418 filename = args[1] + ".pdf";
419 i = 2;
421 if(i==args.length-1){//:Pdf [filename] {...}
422 if(2==args[i].length){// '{}'
423 let width = document.body.clientWidth/96;
424 tabs.children[iTab].executeJavaScript("document.documentElement.scrollHeight",
425 false).then((h)=>{
426 let opts = {
427 printBackground:true,
428 pageSize:{width:width,height:h/96}};
429 print2PDF(filename,opts);
431 return;
432 }else{
433 try {
434 options = JSON.parse(args[i]);
435 }catch(e){};
439 print2PDF(filename,options);
441 function bangcommand(q){
442 let iS = q.indexOf(' ',1);
443 if(iS<0) iS=q.length;
444 let fname = q.substring(1,iS);
445 let fpath = path.join(__dirname,fname+'.js');
446 if (fs.existsSync(fpath)) {
447 fs.readFile(fpath, 'utf8',(err, js)=>{
448 if (err) {
449 console.log(err);
450 return;
452 const prefix = "(function(){";
453 const postfix = "})(`";
454 const end ="`)";
455 const fjs = `${prefix}${js}${postfix}${q}${end}`;
456 eval(fjs);
460 function recQueryHistory(q){
461 if(bQueryHistory)
462 fs.appendFile(path.join(__dirname,"history.autoc"), q, (err)=>{});
464 function handleQuery(q){
465 if(q.length>1){
466 let c0=q.charCodeAt(0);
467 let c1=q.charCodeAt(1);
468 switch(c0){
469 case 33://"!"
470 bangcommand(q);
471 recQueryHistory(q);
472 return;
473 case 47://"/"
474 tabs.children[iTab].findInPage(q.substring(1));
475 return;
476 case 58://':'
477 if(c1>98 && 112!=c1)
478 coloncommand(q);
479 else
480 coloncommand_render(q);
481 recQueryHistory(q);
482 return;
484 if(58===c1){
485 internalLink(q);
486 return;
489 var url=q;
490 NOREC: do{
491 do {
492 if(q.length>12){
493 let c6 = q.charCodeAt(6);
494 if(47===c6){// '/'
495 let c5 = q.charCodeAt(5);
496 if(47===c5 && 58===q.charCodeAt(4))//http/file urls
497 break NOREC;
498 if(58===c5 && 47===q.charCodeAt(7))//https://
499 break NOREC;
500 }else if(q.startsWith("javascript:")){
501 tabs.children[iTab].executeJavaScript(q.substring(11),false);
502 recQueryHistory(q);
503 return;
504 }else if(q.startsWith("view-source:")) break;
505 else if(q.startsWith("data:")) break;
507 let iS = q.indexOf(' ');
508 if(iS<0){
509 if(q.length>5 && 58===q.charCodeAt(5)){// about:
510 break;
512 if(q.indexOf('.')>0){
513 url = 'https://'+q;
514 break NOREC;
516 url = defaultSE.replace('%s',q);
517 break;
519 url = bang(q, iS);
520 if(58===url.charCodeAt(1)){
521 internalLink(url);
522 recQueryHistory(q);
523 return;
525 }while(false);
526 recQueryHistory(q);
527 }while(false);
528 tabs.children[iTab].src=url;
530 function internalLink(url){
531 let cmd = url.charCodeAt(2);
532 let subcmd = url.charCodeAt(3);
533 switch(cmd){
534 case 48://'0' i:0
536 let iColon = url.indexOf(':',5);
537 let name = decodeURIComponent(url.slice(4,iColon));
538 let rurl = url.substring(iColon+1);
539 switch(subcmd){
540 case 47://'/' i:0/
541 if(106===url.charCodeAt(4) && 115===url.charCodeAt(5) && 47===url.charCodeAt(6)){
542 //i:0/js/xx:[url]
543 let fname = name;
544 let pname = path.join(__dirname,fname);
545 if(fs.existsSync(pname)){
546 (async ()=>{
547 try {
548 let js = await fs.promises.readFile(pname,'utf8');
549 let t=tabs.children[iTab];
550 t.dataset.jsonce=js;
551 t.src = rurl;
552 }catch(e){}
553 })();
556 return;
557 case 48: //i:00
559 let endS;
560 let is = url.indexOf('%s',iColon+1);
561 if(is<0)
562 endS = '%s"\n';
563 else
564 endS = '"\n';
565 let pname = path.join(__dirname,"search.json");
566 let str = '"'+name+'":"'+rurl+
567 endS;
568 jsonAppend(pname,125,str);
570 return;
571 case 49: //i:01
573 let pname = path.join(__dirname,"menu.json");
574 let str = '"'+name+'",":bjs handleQuery(`'+
575 rurl+'${tabs.children[iTab].getURL()}`)"\n';
576 jsonAppend(pname,93,str);
578 return;
579 case 50: //i:02
581 let pname = path.join(__dirname,"uas.json");
582 let str = '"'+name+'":"'+rurl+'"\n';
583 jsonAppend(pname,125,str);
585 return;
586 default:
589 return;
590 case 112: //i:p[url]#[querystring] for http POST
592 let iQ = url.indexOf('#',12);
593 let data;
594 let rurl;
595 switch(subcmd){
596 case 49: //i:p1
598 if(iQ>4) {
599 rurl = url.substring(4,iQ);
600 data = [{
601 type: 'rawData',
602 bytes: Buffer.from(url.substring(iQ+1))
604 }else{
605 rurl = url.substring(4);
606 data = [];
609 case 50: //i:p2[url]#[filePath] post request with local file
610 rurl = url.substring(4,iQ);
611 data = [{type: 'file', filePath:url.substring(iQ+1)}];
613 tabs.children[iTab].loadURL(rurl,{postData:data,extraHeaders:'Content-Type: application/x-www-form-urlencoded'});
614 return;
616 case 113: //i:q for multiple queries
617 handleQueries(url.substring(3));
618 return;
621 function handleQueries(cmds){
622 for(var cmd of cmds.split("\n"))
623 handleQuery(cmd);
625 async function jsonAppend(filePath, charcode, str){
626 let fd;
627 try{
628 fd = await fs.promises.open(filePath, 'r+');
629 }catch(e){
630 try {
631 fd = await fs.promises.open(filePath, 'w+');
632 }catch(e1){return}
634 try{
635 const stats = await fd.stat();
636 const fileSize = stats.size;
637 const buffer = Buffer.alloc(1);
638 let position = fileSize-1;
639 while (position >= 0) {
640 await fd.read(buffer, 0, 1, position);
641 if (buffer[0] === charcode) break;
642 position--;
644 let endS = String.fromCharCode(charcode);
645 if(position<0){//re-write whole file
646 str = String.fromCharCode(charcode-2)+str+endS;
647 position = 0;
648 }else
649 str = ","+str+endS;
650 await fd.truncate(position);
651 const buf = Buffer.from(str);
652 await fd.write(buf, 0, buf.length, position);
653 await fd.close();
654 }catch(e){console.log(e)}
656 function autocomplete(inp,container,arr) {
657 var currentFocus;
658 function clickItem(e){inp.value = arr[e.target.dataset.index];}
659 function appendElement(el,dataindex){
660 el.dataset.index = dataindex;
661 el.addEventListener("click", clickItem);
662 container.appendChild(el);
664 function itemInnerHTML(b,str,val,iStr){
665 b.innerHTML = str.substr(0,iStr);
666 b.innerHTML += "<strong>" + str.substr(iStr, val.length) + "</strong>";
667 b.innerHTML += str.substr(iStr+val.length);
669 inp.addEventListener("input", function(e) {
670 const MAXITEMS = 30;
671 var b, i, val = this.value;
672 if (!val) { return false;}
673 currentFocus = -1;
674 let items = container.children;
675 let iBase = 0;
676 let lastV = lastVal;
677 lastVal = val;
678 if(val.startsWith(lastV)){
679 let itemsLen = items.length;
680 if(itemsLen<=0) return;
681 i = itemsLen -1;
682 iBase = items[i].dataset.index +1;
683 switch(autocMode){
684 case 0:
685 for (; i>=0; i--) {
686 b = items[i];
687 let str = arr[b.dataset.index];
688 let iStr = str.indexOf(val);
689 if(iStr<0) {
690 b.parentNode.removeChild(b);
691 continue;
693 itemInnerHTML(b,str,val,iStr);
695 break;
696 case 1:
697 for (; i>=0; i--) {
698 b = items[i];
699 let str = arr[b.dataset.index];
700 let oLen = lastval.length;
701 if(!str.startsWith(val.substring(oLen),oLen)) {
702 b.parentNode.removeChild(b);
703 continue;
705 itemInnerHTML(b,str,val,0);
707 break;
709 if(itemsLen<MAXITEMS)
710 return;
711 else
712 if(container.children.length>=MAXITEMS) return;
713 }else
714 closeAllLists();
715 i = iBase;
716 switch(autocMode){
717 case 0:
718 for (; i < arr.length; i++) {
719 let iStr = arr[i].indexOf(val);
720 if(iStr<0) continue;
722 b = document.createElement("DIV");
723 itemInnerHTML(b,arr[i],val,iStr);
724 appendElement(b,i);
725 if(container.children.length>=MAXITEMS) break;
728 return;
729 case 1://startsWith
730 for (; i < arr.length; i++) {
731 if (arr[i].startsWith(val)) {
732 b = document.createElement("DIV");
733 itemInnerHTML(b,arr[i],val,0);
734 appendElement(b,i);
735 if(container.children.length>=MAXITEMS) break;
740 inp.addEventListener("keydown", function(e) {
741 var x = container.getElementsByTagName("div");
742 if (0===x.length) return false;
743 if (e.keyCode == 40) {//downarrow
744 currentFocus++;
745 addActive(x);
746 } else if (e.keyCode == 38) { //up
747 currentFocus--;
748 addActive(x);
749 } else if (e.keyCode == 13) {
750 if (currentFocus > -1) {
751 e.preventDefault();
752 if (x) x[currentFocus].click();
753 currentFocus = -1;
755 closeAllLists();
756 lastVal = null;
759 function addActive(x) {
760 removeActive(x);
761 if (currentFocus >= x.length) currentFocus = 0;
762 if (currentFocus < 0) currentFocus = (x.length - 1);
763 x[currentFocus].classList.add("autocomplete-active");
765 function removeActive(x) {
766 for (var i = 0; i < x.length; i++) {
767 x[i].classList.remove("autocomplete-active");
770 function closeAllLists() {
771 container.innerHTML = '';
773 inp.addEventListener("blur", function () {
774 setTimeout(()=>container.classList.add("invis"),200);
776 inp.addEventListener("focus", function () {
777 container.classList.remove("invis");
780 </script>
781 </head>
782 <body>
783 <form class="autocomplete" autocomplete="off" action="javascript:handleQuery(getQ())">
784 <input type="text" name=q style="width:100%" autofocus>
785 <div class="autocomplete-items"></div>
786 </form>
787 <div class="webviews">
788 <webview class="curWV" allowpopups></webview>
789 </div>
790 <script>
791 tabs = document.body.children[1];
792 initTab(tabs.children[0]);
794 let inp = document.forms[0].q;
795 autocomplete(inp,inp.nextElementSibling,autocStrArray);
797 document.addEventListener('keydown', keyPress);
798 </script>
799 </body></html>