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 fs
= require('fs');
60 const path
= require('path');
61 const readline
= require('readline');
67 var autocStrArray
= [];
68 var defaultSE
= "https://www.bing.com/search?q=%s";
69 var historyFile
= path
.join(__dirname
,'history.rec');
71 var bQueryHistory
= false;
72 let sitecssP
= path
.join(__dirname
,"sitecss");
73 let sitejsP
= path
.join(__dirname
,"sitejs");
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)})()";
85 let lastKeys_millis
= 0;
87 fs
.readFile(path
.join(__dirname
,'search.json'), 'utf8', (err
, jsonString
) => {
89 coloncommand(":js fetch2file(repositoryurl,'search.json')");
92 initSearchEngines(jsonString
,false);
94 fs
.readFile(path
.join(__dirname
,'mapkeys.json'), 'utf8', (err
, jsonStr
) => {
96 coloncommand(":js fetch2file(repositoryurl,'mapkeys.json')");
100 mapKeys
= JSON
.parse(jsonStr
);
103 appendAutoc_rec(path
.join(__dirname
,'default.autoc'),null);
104 appendAutoc_rec(path
.join(__dirname
,'bookmark.rec'),' ');
106 function initSearchEngines(jsonStr
){
109 engines
=JSON
.parse(jsonStr
, (key
, value
)=>{
110 if(!val1st
&& !(/^\d+$/u.test(key
))) val1st
=value
;
113 if(val1st
) defaultSE
=val1st
;
116 function save(filePath
, u8array
){
117 //alert(Object.prototype.toString.call(u8array))
118 fs
.writeFile (filePath
, u8array
, (err
) => {
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();
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');
144 tabs
.children
[iTab
].classList
.add('curWV');
146 async
function loadJSFile(tab
,jsF
){
148 let js
= await fs
.promises
.readFile(jsF
,'utf8');
149 tab
.executeJavaScript(js
,false);
152 async
function loadCSSFile(tab
,jsF
){
154 let js
= await fs
.promises
.readFile(jsF
,'utf8');
158 function cbStartLoading(e
){
159 if(!bDomainCSS
) return;
163 domain
= new URL(tab
.getURL()).hostname
;
165 let jsF
= path
.join(sitecssP
, domain
+".js");
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
);
178 function cbFinishLoad(e
){
180 let url
= tab
.getURL();
182 let histItem
= tab
.getTitle()+" "+url
+"\n";
183 fs
.appendFile(historyFile
, histItem
, (err
) => {});
185 let js
= tab
.dataset
.jsonce
;
187 tab
.dataset
.jsonce
= null;
188 tab
.executeJavaScript(js
,false);
191 let domain
= new URL(url
).hostname
;
192 let jsF
= path
.join(sitejsP
, domain
+".js");
195 gjsA
.forEach((fname
)=>{
196 let jsF
= path
.join(__dirname
, fname
);
200 function initTab(tab
){
201 tab
.allowpopups
= true;
202 tab
.addEventListener('did-finish-load',cbFinishLoad
);
203 tab
.addEventListener('did-start-loading',cbStartLoading
);
206 var tab
= document
.createElement('webview');
208 tabs
.appendChild(tab
);
210 function tabInc(num
){
211 let nTabs
= tabs
.children
.length
;
217 function tabDec(num
){
218 let nTabs
= tabs
.children
.length
;
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
);
232 if(iTab
>=nTabs
) iTab
=iTab
-1;
233 tabs
.children
[iTab
].classList
.add('curWV');
234 return getWinTitle();
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
){}
245 async
function appendAutoc_rec(filename
, delimit
){
247 const readInterface
= readline
.createInterface ({
248 input
: fs
.createReadStream (filename
, 'utf8'),
251 for await (const line
of readInterface
) {
253 if(delimit
&& (iS
=line
.lastIndexOf(delimit
))>0){
254 autocStrArray
.push(line
.substring(iS
+1));
256 autocStrArray
.push(line
);
258 lastVal
= null; //trigger full search
261 function keyPress(e
){
262 var inputE
= document
.forms
[0].q
;
263 if (e
.altKey
||e
.metaKey
)
269 tabJS("window.scrollTo(0,0)");
272 tabJS("window.scrollTo(0,document.body.scrollHeight)");
282 if(inputE
=== document
.activeElement
) return;
284 h
= -3*document
.documentElement
.clientHeight
/4;
288 h
= 3*document
.documentElement
.clientHeight
/4;
291 h
= -3*document
.documentElement
.clientHeight
/4;
296 if(inputE
=== document
.activeElement
&&
297 0!==inputE
.nextElementSibling
.children
.length
)
303 let js
= `javascript:window.scrollBy(0,${h})`;
308 if(inputE
=== document
.activeElement
){
311 tabs
.children
[iTab
].focus();
315 var curMillis
= Date
.now();
316 if(curMillis
-lastKeys_millis
>1000)
318 lastKeys_millis
= curMillis
;
329 lastKeys
= !lastKeys
? key
: lastKeys
+ key
;
330 let cmds
= mapKeys
[lastKeys
];
331 if(cmds
){//try to run cmds
332 let keyLen
= lastKeys
.length
;
334 if(lastKeys
.length
!= keyLen
) return;
336 for(var cmd
of cmds
.split("\n"))
341 function getQ(){return document
.forms
[0].q
.value
;}
342 function bang(query
, iSpace
){
345 let name
= query
.slice(0,iSpace
);
346 let engine
= engines
[name
];
349 query
= query
.substring(iSpace
+1);
352 return se
.replace('%s',query
);
354 function coloncommand(q
){
357 function coloncommand_render(cmd
){
358 args
= cmd
.substring(1).split(/\s+/);
378 function autoc(args
){
379 if(2!=args
.length
) return;
380 let fpath
= path
.join(__dirname
,args
[1]);
383 if (!fs
.existsSync(fname
)){
384 fname
= fpath
+".autoc";
385 if (!fs
.existsSync(fname
))
386 fname
= fpath
+".rec";
390 appendAutoc_rec(fname
,delimit
);
393 if(2!=args
.length
) return;
394 let filename
= args
[1]+".js";
395 fs
.readFile(path
.join(__dirname
,filename
), 'utf8', (err
,str
) => {
397 tabs
.children
[iTab
].executeJavaScript(str
,false);
400 function savePdf(args
){
401 let filename
= "ebrowser.pdf";
404 let c0
= args
[1].charCodeAt(0);
406 if(123!=c0
){//not '{' options then it is filename
407 filename
= args
[1] + ".pdf";
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",
416 printBackground
:true,
417 pageSize
:{width
:width
,height
:h
/96}};
418 print2PDF(filename
,opts
);
423 options
= JSON
.parse(args
[i
]);
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
)=>{
441 const prefix
= "(function(){";
442 const postfix
= "})(`";
444 const fjs
= `${prefix}${js}${postfix}${q}${end}`;
449 function recQueryHistory(q
){
451 fs
.appendFile(path
.join(__dirname
,"history.autoc"), q
, (err
)=>{});
453 function handleQuery(q
){
455 let c0
=q
.charCodeAt(0);
462 tabs
.children
[iTab
].findInPage(q
.substring(1));
465 let c1
=q
.charCodeAt(1);
469 coloncommand_render(q
);
478 let c6
= q
.charCodeAt(6);
479 if(58===q
.charCodeAt(1)){
482 }else if(47===c6
){// '/'
483 let c5
= q
.charCodeAt(5);
484 if(47===c5
&& 58===q
.charCodeAt(4))//http/file urls
486 if(58===c5
&& 47===q
.charCodeAt(7))//https://
488 }else if(q
.startsWith("javascript:")){
489 tabs
.children
[iTab
].executeJavaScript(q
.substring(11),false);
492 }else if(q
.startsWith("view-source:")) break;
493 else if(q
.startsWith("data:")) break;
495 let iS
= q
.indexOf(' ');
497 if(q
.length
>5 && 58===q
.charCodeAt(5)){// about:
500 if(q
.indexOf('.')>0){
504 url
= defaultSE
.replace('%s',q
);
508 if(58===url
.charCodeAt(1)){
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);
526 if(106===url
.charCodeAt(4) && 115===url
.charCodeAt(5) && 47===url
.charCodeAt(6)){
528 let fname
= url
.slice(4,iColon
);
529 let pname
= path
.join(__dirname
,fname
);
530 if(fs
.existsSync(pname
)){
533 let js
= await fs
.promises
.readFile(pname
,'utf8');
534 let t
=tabs
.children
[iTab
];
536 t
.src
= url
.substring(iColon
+1);
544 function autocomplete(inp
,container
,arr
) {
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
) {
559 var b
, i
, val
= this.value
;
560 if (!val
) { return false;}
562 let items
= container
.children
;
566 if(val
.startsWith(lastV
)){
567 let itemsLen
= items
.length
;
568 if(itemsLen
<=0) return;
570 iBase
= items
[i
].dataset
.index
+1;
575 let str
= arr
[b
.dataset
.index
];
576 let iStr
= str
.indexOf(val
);
578 b
.parentNode
.removeChild(b
);
581 itemInnerHTML(b
,str
,val
,iStr
);
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
);
593 itemInnerHTML(b
,str
,val
,0);
597 if(itemsLen
<MAXITEMS
)
600 if(container
.children
.length
>=MAXITEMS
) return;
606 for (; i
< arr
.length
; i
++) {
607 let iStr
= arr
[i
].indexOf(val
);
610 b
= document
.createElement("DIV");
611 itemInnerHTML(b
,arr
[i
],val
,iStr
);
613 if(container
.children
.length
>=MAXITEMS
) break;
618 for (; i
< arr
.length
; i
++) {
619 if (arr
[i
].startsWith(val
)) {
620 b
= document
.createElement("DIV");
621 itemInnerHTML(b
,arr
[i
],val
,0);
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
634 } else if (e
.keyCode
== 38) { //up
637 } else if (e
.keyCode
== 13) {
638 if (currentFocus
> -1) {
640 if (x
) x
[currentFocus
].click();
647 function addActive(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");
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>
675 <div class=
"webviews">
676 <webview class=
"curWV" allowpopups
></webview>
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
);