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/>.
9 <!DOCTYPE html
><html><head><meta charset=
"UTF-8">
17 flex-direction: column;
23 flex-direction: column;
26 webview{display: none;width:
100%;height:
100%}
27 .curWV{display: inherit !important;}
28 .autocomplete-active {
29 background-color: DodgerBlue !important;
33 /*the container must be positioned relative:*/
36 display: inline-block;
41 border:
1px solid #d4d4d4;
45 /*position the autocomplete items to be the same width as the container:*/
50 .autocomplete-items div {
52 background-color: #fff;
54 .autocomplete-items div:hover {
55 background-color: #e9e9e9;
59 const { ipcRenderer
} = require('electron');
60 const fs
= require('fs');
61 const path
= require('path');
62 const readline
= require('readline');
68 var autocStrArray
= [];
69 var defaultSE
= "https://www.bing.com/search?q=%s";
70 var historyFile
= path
.join(__dirname
,'history.rec');
72 var bQueryHistory
= false;
73 let sitecssP
= path
.join(__dirname
,"sitecss");
74 let sitejsP
= path
.join(__dirname
,"sitejs");
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)})()";
86 let lastKeys_millis
= 0;
88 fs
.readFile(path
.join(__dirname
,'search.json'), 'utf8', (err
, jsonString
) => {
90 coloncommand(":js fetch2file(repositoryurl,'search.json')");
93 initSearchEngines(jsonString
,false);
95 fs
.readFile(path
.join(__dirname
,'mapkeys.json'), 'utf8', (err
, jsonStr
) => {
97 coloncommand(":js fetch2file(repositoryurl,'mapkeys.json')");
101 mapKeys
= JSON
.parse(jsonStr
);
104 appendAutoc_rec(path
.join(__dirname
,'default.autoc'),null);
105 appendAutoc_rec(path
.join(__dirname
,'bookmark.rec'),' ');
107 function initSearchEngines(jsonStr
){
110 engines
=JSON
.parse(jsonStr
, (key
, value
)=>{
111 if(!val1st
&& !(/^\d+$/u.test(key
))) val1st
=value
;
114 if(val1st
) defaultSE
=val1st
;
117 function save(filePath
, u8array
){
118 //alert(Object.prototype.toString.call(u8array))
119 fs
.writeFile (filePath
, u8array
, (err
) => {
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();
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');
145 tabs
.children
[iTab
].classList
.add('curWV');
147 async
function loadJSFile(tab
,jsF
){
149 let js
= await fs
.promises
.readFile(jsF
,'utf8');
150 tab
.executeJavaScript(js
,false);
153 async
function loadCSSFile(tab
,jsF
){
155 let js
= await fs
.promises
.readFile(jsF
,'utf8');
159 function cbStartLoading(e
){
160 if(!bDomainCSS
) return;
164 domain
= new URL(tab
.getURL()).hostname
;
166 let jsF
= path
.join(sitecssP
, domain
+".js");
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
);
179 function cbNavigate(e
){
181 if(58===url
.charCodeAt(1)){
183 if(confirm("Proceed to execute risky operations: "+url
))
186 document
.forms
[0].q
.value
= url
;
189 function cbFinishLoad(e
){
191 let url
= tab
.getURL();
193 let histItem
= tab
.getTitle()+" "+url
+"\n";
194 fs
.appendFile(historyFile
, histItem
, (err
) => {});
196 let js
= tab
.dataset
.jsonce
;
198 tab
.dataset
.jsonce
= null;
199 tab
.executeJavaScript(js
,false);
202 let domain
= new URL(url
).hostname
;
203 let jsF
= path
.join(sitejsP
, domain
+".js");
206 gjsA
.forEach((fname
)=>{
207 let jsF
= path
.join(__dirname
, fname
);
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
);
218 var tab
= document
.createElement('webview');
220 tabs
.appendChild(tab
);
222 function tabInc(num
){
223 let nTabs
= tabs
.children
.length
;
229 function tabDec(num
){
230 let nTabs
= tabs
.children
.length
;
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
);
244 if(iTab
>=nTabs
) iTab
=iTab
-1;
245 tabs
.children
[iTab
].classList
.add('curWV');
246 return getWinTitle();
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
){}
257 async
function appendAutoc_rec(filename
, delimit
){
259 const readInterface
= readline
.createInterface ({
260 input
: fs
.createReadStream (filename
, 'utf8'),
263 for await (const line
of readInterface
) {
265 if(delimit
&& (iS
=line
.lastIndexOf(delimit
))>0){
266 autocStrArray
.push(line
.substring(iS
+1));
268 autocStrArray
.push(line
);
270 lastVal
= null; //trigger full search
273 function keyPress(e
){
274 var inputE
= document
.forms
[0].q
;
275 if (e
.altKey
||e
.metaKey
)
281 tabJS("window.scrollTo(0,0)");
284 tabJS("window.scrollTo(0,document.body.scrollHeight)");
294 if(inputE
=== document
.activeElement
) return;
296 h
= -3*document
.documentElement
.clientHeight
/4;
300 h
= 3*document
.documentElement
.clientHeight
/4;
303 h
= -3*document
.documentElement
.clientHeight
/4;
308 if(inputE
=== document
.activeElement
&&
309 0!==inputE
.nextElementSibling
.children
.length
)
315 let js
= `javascript:window.scrollBy(0,${h})`;
320 if(inputE
=== document
.activeElement
){
323 tabs
.children
[iTab
].focus();
327 var curMillis
= Date
.now();
328 if(curMillis
-lastKeys_millis
>1000)
330 lastKeys_millis
= curMillis
;
341 lastKeys
= !lastKeys
? key
: lastKeys
+ key
;
342 let cmds
= mapKeys
[lastKeys
];
343 if(cmds
){//try to run cmds
344 let keyLen
= lastKeys
.length
;
346 if(lastKeys
.length
!= keyLen
) return;
352 function getQ(){return document
.forms
[0].q
.value
;}
353 function bang(query
, iSpace
){
356 let name
= query
.slice(0,iSpace
);
357 let engine
= engines
[name
];
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+/);
389 function autoc(args
){
390 if(2!=args
.length
) return;
391 let fpath
= path
.join(__dirname
,args
[1]);
394 if (!fs
.existsSync(fname
)){
395 fname
= fpath
+".autoc";
396 if (!fs
.existsSync(fname
))
397 fname
= fpath
+".rec";
401 appendAutoc_rec(fname
,delimit
);
404 if(2!=args
.length
) return;
405 let filename
= args
[1]+".js";
406 fs
.readFile(path
.join(__dirname
,filename
), 'utf8', (err
,str
) => {
408 tabs
.children
[iTab
].executeJavaScript(str
,false);
411 function savePdf(args
){
412 let filename
= "ebrowser.pdf";
415 let c0
= args
[1].charCodeAt(0);
417 if(123!=c0
){//not '{' options then it is filename
418 filename
= args
[1] + ".pdf";
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",
427 printBackground
:true,
428 pageSize
:{width
:width
,height
:h
/96}};
429 print2PDF(filename
,opts
);
434 options
= JSON
.parse(args
[i
]);
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
)=>{
452 const prefix
= "(function(){";
453 const postfix
= "})(`";
455 const fjs
= `${prefix}${js}${postfix}${q}${end}`;
460 function recQueryHistory(q
){
462 fs
.appendFile(path
.join(__dirname
,"history.autoc"), q
, (err
)=>{});
464 function handleQuery(q
){
466 let c0
=q
.charCodeAt(0);
467 let c1
=q
.charCodeAt(1);
474 tabs
.children
[iTab
].findInPage(q
.substring(1));
480 coloncommand_render(q
);
493 let c6
= q
.charCodeAt(6);
495 let c5
= q
.charCodeAt(5);
496 if(47===c5
&& 58===q
.charCodeAt(4))//http/file urls
498 if(58===c5
&& 47===q
.charCodeAt(7))//https://
500 }else if(q
.startsWith("javascript:")){
501 tabs
.children
[iTab
].executeJavaScript(q
.substring(11),false);
504 }else if(q
.startsWith("view-source:")) break;
505 else if(q
.startsWith("data:")) break;
507 let iS
= q
.indexOf(' ');
509 if(q
.length
>5 && 58===q
.charCodeAt(5)){// about:
512 if(q
.indexOf('.')>0){
516 url
= defaultSE
.replace('%s',q
);
520 if(58===url
.charCodeAt(1)){
528 tabs
.children
[iTab
].src
=url
;
530 function internalLink(url
){
531 let cmd
= url
.charCodeAt(2);
532 let subcmd
= url
.charCodeAt(3);
536 let iColon
= url
.indexOf(':',5);
537 let name
= decodeURIComponent(url
.slice(4,iColon
));
538 let rurl
= url
.substring(iColon
+1);
541 if(106===url
.charCodeAt(4) && 115===url
.charCodeAt(5) && 47===url
.charCodeAt(6)){
544 let pname
= path
.join(__dirname
,fname
);
545 if(fs
.existsSync(pname
)){
548 let js
= await fs
.promises
.readFile(pname
,'utf8');
549 let t
=tabs
.children
[iTab
];
560 let is
= url
.indexOf('%s',iColon
+1);
565 let pname
= path
.join(__dirname
,"search.json");
566 let str
= '"'+name
+'":"'+rurl
+
568 jsonAppend(pname
,125,str
);
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
);
581 let pname
= path
.join(__dirname
,"uas.json");
582 let str
= '"'+name
+'":"'+rurl
+'"\n';
583 jsonAppend(pname
,125,str
);
590 case 112: //i:p[url]#[querystring] for http POST
592 let iQ
= url
.indexOf('#',12);
599 rurl
= url
.substring(4,iQ
);
602 bytes
: Buffer
.from(url
.substring(iQ
+1))
605 rurl
= url
.substring(4);
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'});
616 case 113: //i:q for multiple queries
617 handleQueries(url
.substring(3));
621 function handleQueries(cmds
){
622 for(var cmd
of cmds
.split("\n"))
625 async
function jsonAppend(filePath
, charcode
, str
){
628 fd
= await fs
.promises
.open(filePath
, 'r+');
631 fd
= await fs
.promises
.open(filePath
, 'w+');
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;
644 let endS
= String
.fromCharCode(charcode
);
645 if(position
<0){//re-write whole file
646 str
= String
.fromCharCode(charcode
-2)+str
+endS
;
650 await fd
.truncate(position
);
651 const buf
= Buffer
.from(str
);
652 await fd
.write(buf
, 0, buf
.length
, position
);
654 }catch(e
){console
.log(e
)}
656 function autocomplete(inp
,container
,arr
) {
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
) {
671 var b
, i
, val
= this.value
;
672 if (!val
) { return false;}
674 let items
= container
.children
;
678 if(val
.startsWith(lastV
)){
679 let itemsLen
= items
.length
;
680 if(itemsLen
<=0) return;
682 iBase
= items
[i
].dataset
.index
+1;
687 let str
= arr
[b
.dataset
.index
];
688 let iStr
= str
.indexOf(val
);
690 b
.parentNode
.removeChild(b
);
693 itemInnerHTML(b
,str
,val
,iStr
);
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
);
705 itemInnerHTML(b
,str
,val
,0);
709 if(itemsLen
<MAXITEMS
)
712 if(container
.children
.length
>=MAXITEMS
) return;
718 for (; i
< arr
.length
; i
++) {
719 let iStr
= arr
[i
].indexOf(val
);
722 b
= document
.createElement("DIV");
723 itemInnerHTML(b
,arr
[i
],val
,iStr
);
725 if(container
.children
.length
>=MAXITEMS
) break;
730 for (; i
< arr
.length
; i
++) {
731 if (arr
[i
].startsWith(val
)) {
732 b
= document
.createElement("DIV");
733 itemInnerHTML(b
,arr
[i
],val
,0);
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
746 } else if (e
.keyCode
== 38) { //up
749 } else if (e
.keyCode
== 13) {
750 if (currentFocus
> -1) {
752 if (x
) x
[currentFocus
].click();
759 function addActive(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");
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>
787 <div class=
"webviews">
788 <webview class=
"curWV" allowpopups
></webview>
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
);