mirror of
https://github.com/torappinfo/uweb.git
synced 2024-08-14 23:54:59 +00:00
636 lines
17 KiB
HTML
636 lines
17 KiB
HTML
<!--
|
|
Copyright (C) 2024 Richard Hao Cao
|
|
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.
|
|
|
|
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.
|
|
|
|
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
-->
|
|
<!DOCTYPE html><html><head><meta charset="UTF-8">
|
|
<style>
|
|
html{
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
body{
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
margin-top: 1px;
|
|
}
|
|
div.webviews{
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-grow:1;
|
|
}
|
|
webview{display: none;width:100%;height:100%}
|
|
.curWV{display: inherit !important;}
|
|
.autocomplete-active {
|
|
background-color: DodgerBlue !important;
|
|
color: #ffffff;
|
|
}
|
|
.invis{display: none}
|
|
/*the container must be positioned relative:*/
|
|
.autocomplete {
|
|
position: relative;
|
|
display: inline-block;
|
|
width:100%;
|
|
}
|
|
.autocomplete-items {
|
|
position: absolute;
|
|
border: 1px solid #d4d4d4;
|
|
border-bottom: none;
|
|
border-top: none;
|
|
z-index: 99;
|
|
/*position the autocomplete items to be the same width as the container:*/
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
}
|
|
.autocomplete-items div {
|
|
cursor: pointer;
|
|
background-color: #fff;
|
|
}
|
|
.autocomplete-items div:hover {
|
|
background-color: #e9e9e9;
|
|
}
|
|
</style>
|
|
<script>
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const readline = require('readline');
|
|
var iTab = 0;
|
|
var tabs;
|
|
var engines = {};
|
|
var mapKeys = {};
|
|
var closedUrls = [];
|
|
var autocStrArray = [];
|
|
var defaultSE = "https://www.bing.com/search?q=%s";
|
|
var historyFile = path.join(__dirname,'history.rec');
|
|
var bHistory = false;
|
|
var bQueryHistory = false;
|
|
var autocMode = 0; //0 for substring, 1 for startsWith
|
|
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)}";
|
|
const BML_tail = "})()";
|
|
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)})()";
|
|
|
|
let lastKeys;
|
|
let lastKeys_millis = 0;
|
|
var lastVal;
|
|
fs.readFile(path.join(__dirname,'search.json'), 'utf8', (err, jsonString) => {
|
|
if (err) {
|
|
coloncommand(":js fetch2file(repositoryurl,'search.json')");
|
|
return;
|
|
}
|
|
initSearchEngines(jsonString,false);
|
|
});
|
|
fs.readFile(path.join(__dirname,'mapkeys.json'), 'utf8', (err, jsonStr) => {
|
|
if (err) {
|
|
coloncommand(":js fetch2file(repositoryurl,'mapkeys.json')");
|
|
return;
|
|
}
|
|
try {
|
|
mapKeys = JSON.parse(jsonStr);
|
|
}catch(e){}
|
|
});
|
|
appendAutoc_rec(path.join(__dirname,'default.autoc'),null);
|
|
appendAutoc_rec(path.join(__dirname,'bookmark.rec'),' ');
|
|
|
|
function initSearchEngines(jsonStr){
|
|
try{
|
|
let val1st;
|
|
engines=JSON.parse(jsonStr, (key, value)=>{
|
|
if(!val1st && !(/^\d+$/u.test(key))) val1st=value;
|
|
return value;
|
|
});
|
|
if(val1st) defaultSE=val1st;
|
|
}catch(e){}
|
|
}
|
|
function save(filePath, u8array){
|
|
//alert(Object.prototype.toString.call(u8array))
|
|
fs.writeFile (filePath, u8array, (err) => {
|
|
if (err) {
|
|
console.error (err);
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
function print2PDF(filePath, options){
|
|
tabs.children[iTab].printToPDF(options)
|
|
.then(u8array=>save(filePath,u8array));
|
|
}
|
|
function bookmark(args){//b [filenamestem] url title :bookmark
|
|
let bmFileName = "bookmark.rec";
|
|
let tab = tabs.children[iTab];
|
|
let url = tab.getURL();
|
|
if(args.length>1)
|
|
bmFileName = args[1]+".rec";
|
|
let title = tab.getTitle();
|
|
let line = title + " " + url + "\n";
|
|
fs.appendFile(path.join(__dirname,bmFileName), line, (err)=>{});
|
|
}
|
|
function switchTab(i){
|
|
let tab = tabs.children[iTab];
|
|
if(document.activeElement == tab) tab.blur();
|
|
tab.classList.remove('curWV');
|
|
iTab = i;
|
|
tabs.children[iTab].classList.add('curWV');
|
|
}
|
|
function cbFinishLoad(e){
|
|
let tab = e.target;
|
|
let js = tab.dataset.jsonce;
|
|
if(js){
|
|
tab.dataset.jsonce = null;
|
|
tab.executeJavaScript(js,false);
|
|
}
|
|
if(!bHistory) return;
|
|
let histItem = tab.getTitle()+" "+tab.getURL()+"\n";
|
|
fs.appendFile(historyFile, histItem, (err) => {});
|
|
}
|
|
function initTab(tab){
|
|
tab.allowpopups = true;
|
|
tab.addEventListener('did-finish-load',cbFinishLoad);
|
|
}
|
|
function newTab(){
|
|
var tab = document.createElement('webview');
|
|
initTab(tab);
|
|
tabs.appendChild(tab);
|
|
}
|
|
function tabInc(num){
|
|
let nTabs = tabs.children.length;
|
|
if(nTabs<2) return;
|
|
let i = iTab +num;
|
|
if(i>=nTabs) i=0;
|
|
switchTab(i);
|
|
}
|
|
function tabDec(num){
|
|
let nTabs = tabs.children.length;
|
|
if(nTabs<2) return;
|
|
let i = iTab +num;
|
|
if(i<0) i=nTabs-1;
|
|
switchTab(i);
|
|
}
|
|
function tabClose(){
|
|
let nTabs = tabs.children.length;
|
|
if(nTabs<2) return "";//no remain tab
|
|
let tab = tabs.children[iTab];
|
|
closedUrls.push(tab.getURL());
|
|
if(document.activeElement == tab) tab.blur();
|
|
tabs.removeChild(tab);
|
|
nTabs--;
|
|
if(iTab>=nTabs) iTab=iTab-1;
|
|
tabs.children[iTab].classList.add('curWV');
|
|
return getWinTitle();
|
|
}
|
|
function tabJS(js){
|
|
tabs.children[iTab].executeJavaScript(js,false);
|
|
}
|
|
function getWinTitle(){
|
|
let t=tabs.children[iTab];
|
|
let title = (iTab+1) + '/' + tabs.children.length;
|
|
try{title=title+' '+t.getTitle()+' '+t.getURL()}catch(e){}
|
|
return title
|
|
}
|
|
async function appendAutoc_rec(filename, delimit){
|
|
try{
|
|
const readInterface = readline.createInterface ({
|
|
input: fs.createReadStream (filename, 'utf8'),
|
|
});
|
|
|
|
for await (const line of readInterface) {
|
|
let iS;
|
|
if(delimit && (iS=line.lastIndexOf(delimit))>0){
|
|
autocStrArray.push(line.substring(iS+1));
|
|
}else
|
|
autocStrArray.push(line);
|
|
}
|
|
lastVal = null; //trigger full search
|
|
}catch(e){return;}
|
|
}
|
|
function keyPress(e){
|
|
var inputE = document.forms[0].q;
|
|
if (e.altKey||e.metaKey)
|
|
return;
|
|
var key = e.key;
|
|
if(e.ctrlKey){
|
|
switch(key){
|
|
case "Home":
|
|
tabJS("window.scrollTo(0,0)");
|
|
return;
|
|
case "End":
|
|
tabJS("window.scrollTo(0,document.body.scrollHeight)");
|
|
return;
|
|
|
|
}
|
|
return;
|
|
}
|
|
SCROLL: do {
|
|
let h = -32;
|
|
switch(key){
|
|
case " ":
|
|
if(inputE === document.activeElement) return;
|
|
if(e.shiftKey){
|
|
h = -3*document.documentElement.clientHeight/4;
|
|
break;
|
|
}
|
|
case "PageDown":
|
|
h = 3*document.documentElement.clientHeight/4;
|
|
break;
|
|
case "PageUp":
|
|
h = -3*document.documentElement.clientHeight/4;
|
|
break;
|
|
case "ArrowDown":
|
|
h = 32;
|
|
case "ArrowUp":
|
|
if(inputE === document.activeElement &&
|
|
0!==inputE.nextElementSibling.children.length)
|
|
return;
|
|
break;
|
|
default:
|
|
break SCROLL;
|
|
}
|
|
let js = `javascript:window.scrollBy(0,${h})`;
|
|
tabJS(js);
|
|
return;
|
|
}while(false);
|
|
|
|
if(inputE === document.activeElement){
|
|
if (9===e.keyCode){
|
|
e.preventDefault();
|
|
tabs.children[iTab].focus();
|
|
}
|
|
return;
|
|
}
|
|
var curMillis = Date.now();
|
|
if(curMillis-lastKeys_millis>1000)
|
|
lastKeys = null;
|
|
lastKeys_millis = curMillis;
|
|
|
|
switch (key) {
|
|
case "!":
|
|
case "/":
|
|
case ":":
|
|
inputE.value = "";
|
|
inputE.focus();
|
|
lastKeys = null;
|
|
return;
|
|
}
|
|
lastKeys = !lastKeys ? key : lastKeys + key;
|
|
let cmds = mapKeys[lastKeys];
|
|
if(cmds){//try to run cmds
|
|
let keyLen = lastKeys.length;
|
|
setTimeout(()=>{
|
|
if(lastKeys.length != keyLen) return;
|
|
lastKeys = null;
|
|
for(var cmd of cmds.split("\n"))
|
|
handleQuery(cmd);
|
|
}, 500);
|
|
}
|
|
}
|
|
function getQ(){return document.forms[0].q.value;}
|
|
function bang(query, iSpace){
|
|
let se=defaultSE;
|
|
{
|
|
let name = query.slice(0,iSpace);
|
|
let engine = engines[name];
|
|
if(engine){
|
|
se = engine;
|
|
query = query.substring(iSpace+1);
|
|
}
|
|
}
|
|
return se.replace('%s',query);
|
|
}
|
|
function coloncommand(q){
|
|
document.title = q;
|
|
}
|
|
function coloncommand_render(cmd){
|
|
args = cmd.substring(1).split(/\s+/);
|
|
switch(args[0]){
|
|
case "ac":
|
|
autoc(args);
|
|
return;
|
|
case "b":
|
|
bookmark(args);
|
|
return;
|
|
case "bjs":
|
|
eval(cmd.slice(5));
|
|
return;
|
|
case "bml":
|
|
bml(args);
|
|
return;
|
|
case "pdf":
|
|
savePdf(args);
|
|
return;
|
|
}
|
|
|
|
}
|
|
function autoc(args){
|
|
if(2!=args.length) return;
|
|
let fpath = path.join(__dirname,args[1]);
|
|
let fname = fpath;
|
|
let delimit = ' ';
|
|
if (!fs.existsSync(fname)){
|
|
fname = fpath+".autoc";
|
|
if (!fs.existsSync(fname))
|
|
fname = fpath+".rec";
|
|
else
|
|
delimit = null;
|
|
}
|
|
appendAutoc_rec(fname,delimit);
|
|
}
|
|
function bml(args){
|
|
if(2!=args.length) return;
|
|
let filename = args[1]+".js";
|
|
fs.readFile(path.join(__dirname,filename), 'utf8', (err,str) => {
|
|
if (err) return;
|
|
tabs.children[iTab].executeJavaScript(str,false);
|
|
});
|
|
}
|
|
function savePdf(args){
|
|
let filename = "ebrowser.pdf";
|
|
let options = {};
|
|
if(1<args.length){
|
|
let c0 = args[1].charCodeAt(0);
|
|
let i = 1;
|
|
if(123!=c0){//not '{' options then it is filename
|
|
filename = args[1] + ".pdf";
|
|
i = 2;
|
|
}
|
|
if(i==args.length-1){//:Pdf [filename] {...}
|
|
if(2==args[i].length){// '{}'
|
|
let width = document.body.clientWidth/96;
|
|
tabs.children[iTab].executeJavaScript("document.documentElement.scrollHeight",
|
|
false).then((h)=>{
|
|
let opts = {
|
|
printBackground:true,
|
|
pageSize:{width:width,height:h/96}};
|
|
print2PDF(filename,opts);
|
|
});
|
|
return;
|
|
}else{
|
|
try {
|
|
options = JSON.parse(args[i]);
|
|
}catch(e){};
|
|
}
|
|
}
|
|
}
|
|
print2PDF(filename,options);
|
|
}
|
|
function bangcommand(q){
|
|
let iS = q.indexOf(' ',1);
|
|
if(iS<0) iS=q.length;
|
|
let fname = q.substring(1,iS);
|
|
let fpath = path.join(__dirname,fname+'.js');
|
|
if (fs.existsSync(fpath)) {
|
|
fs.readFile(fpath, 'utf8',(err, js)=>{
|
|
if (err) {
|
|
console.log(err);
|
|
return;
|
|
}
|
|
const prefix = "(function(){";
|
|
const postfix = "})(`";
|
|
const end ="`)";
|
|
const fjs = `${prefix}${js}${postfix}${q}${end}`;
|
|
eval(fjs);
|
|
});
|
|
}
|
|
}
|
|
function recQueryHistory(q){
|
|
if(bQueryHistory)
|
|
fs.appendFile(path.join(__dirname,"history.autoc"), q, (err)=>{});
|
|
}
|
|
function handleQuery(q){
|
|
if(q.length>1){
|
|
let c0=q.charCodeAt(0);
|
|
switch(c0){
|
|
case 33://"!"
|
|
bangcommand(q);
|
|
recQueryHistory(q);
|
|
return;
|
|
case 47://"/"
|
|
tabs.children[iTab].findInPage(q.substring(1));
|
|
return;
|
|
case 58://':'
|
|
let c1=q.charCodeAt(1);
|
|
if(c1>98 && 112!=c1)
|
|
coloncommand(q);
|
|
else
|
|
coloncommand_render(q);
|
|
recQueryHistory(q);
|
|
return;
|
|
}
|
|
}
|
|
var url=q;
|
|
NOREC: do{
|
|
do {
|
|
if(q.length>12){
|
|
let c6 = q.charCodeAt(6);
|
|
if(58===q.charCodeAt(1)){
|
|
internalLink(q);
|
|
return;
|
|
}else if(47===c6){// '/'
|
|
let c5 = q.charCodeAt(5);
|
|
if(47===c5 && 58===q.charCodeAt(4))//http/file urls
|
|
break NOREC;
|
|
if(58===c5 && 47===q.charCodeAt(7))//https://
|
|
break NOREC;
|
|
}else if(q.startsWith("javascript:")){
|
|
tabs.children[iTab].executeJavaScript(q.substring(11),false);
|
|
recQueryHistory(q);
|
|
return;
|
|
}else if(q.startsWith("view-source:")) break;
|
|
else if(q.startsWith("data:")) break;
|
|
}
|
|
let iS = q.indexOf(' ');
|
|
if(iS<0){
|
|
if(q.length>5 && 58===q.charCodeAt(5)){// about:
|
|
break;
|
|
}
|
|
if(q.indexOf('.')>0){
|
|
url = 'https://'+q;
|
|
break NOREC;
|
|
}
|
|
url = defaultSE.replace('%s',q);
|
|
break;
|
|
}
|
|
url = bang(q, iS);
|
|
if(58===url.charCodeAt(1)){
|
|
internalLink(url);
|
|
recQueryHistory(q);
|
|
return;
|
|
}
|
|
}while(false);
|
|
recQueryHistory(q);
|
|
}while(false);
|
|
tabs.children[iTab].src=url;
|
|
}
|
|
function internalLink(url){
|
|
let cmd = url.charCodeAt(2);
|
|
let subcmd = url.charCodeAt(3);
|
|
let iColon = url.indexOf(':',2);
|
|
switch(cmd){
|
|
case 48://'0' i:0
|
|
switch(subcmd){
|
|
case 47://'/' i:0/
|
|
if(106===url.charCodeAt(4) && 115===url.charCodeAt(5) && 47===url.charCodeAt(6)){
|
|
//i:0/js/xx:[url]
|
|
let fname = url.slice(4,iColon);
|
|
let pname = path.join(__dirname,fname);
|
|
if(fs.existsSync(pname)){
|
|
(async ()=>{
|
|
try {
|
|
let js = await fs.promises.readFile(pname,'utf8');
|
|
let t=tabs.children[iTab];
|
|
t.dataset.jsonce=js;
|
|
t.src = url.substring(iColon+1);
|
|
}catch(e){}
|
|
})();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function autocomplete(inp,container,arr) {
|
|
var currentFocus;
|
|
function clickItem(e){inp.value = arr[e.target.dataset.index];}
|
|
function appendElement(el,dataindex){
|
|
el.dataset.index = dataindex;
|
|
el.addEventListener("click", clickItem);
|
|
container.appendChild(el);
|
|
}
|
|
function itemInnerHTML(b,str,val,iStr){
|
|
b.innerHTML = str.substr(0,iStr);
|
|
b.innerHTML += "<strong>" + str.substr(iStr, val.length) + "</strong>";
|
|
b.innerHTML += str.substr(iStr+val.length);
|
|
}
|
|
inp.addEventListener("input", function(e) {
|
|
const MAXITEMS = 30;
|
|
var b, i, val = this.value;
|
|
if (!val) { return false;}
|
|
currentFocus = -1;
|
|
let items = container.children;
|
|
let iBase = 0;
|
|
let lastV = lastVal;
|
|
lastVal = val;
|
|
if(val.startsWith(lastV)){
|
|
let itemsLen = items.length;
|
|
if(itemsLen<=0) return;
|
|
i = itemsLen -1;
|
|
iBase = items[i].dataset.index +1;
|
|
switch(autocMode){
|
|
case 0:
|
|
for (; i>=0; i--) {
|
|
b = items[i];
|
|
let str = arr[b.dataset.index];
|
|
let iStr = str.indexOf(val);
|
|
if(iStr<0) {
|
|
b.parentNode.removeChild(b);
|
|
continue;
|
|
}
|
|
itemInnerHTML(b,str,val,iStr);
|
|
}
|
|
break;
|
|
case 1:
|
|
for (; i>=0; i--) {
|
|
b = items[i];
|
|
let str = arr[b.dataset.index];
|
|
let oLen = lastval.length;
|
|
if(!str.startsWith(val.substring(oLen),oLen)) {
|
|
b.parentNode.removeChild(b);
|
|
continue;
|
|
}
|
|
itemInnerHTML(b,str,val,0);
|
|
}
|
|
break;
|
|
}
|
|
if(itemsLen<MAXITEMS)
|
|
return;
|
|
else
|
|
if(container.children.length>=MAXITEMS) return;
|
|
}else
|
|
closeAllLists();
|
|
i = iBase;
|
|
switch(autocMode){
|
|
case 0:
|
|
for (; i < arr.length; i++) {
|
|
let iStr = arr[i].indexOf(val);
|
|
if(iStr<0) continue;
|
|
{
|
|
b = document.createElement("DIV");
|
|
itemInnerHTML(b,arr[i],val,iStr);
|
|
appendElement(b,i);
|
|
if(container.children.length>=MAXITEMS) break;
|
|
}
|
|
}
|
|
return;
|
|
case 1://startsWith
|
|
for (; i < arr.length; i++) {
|
|
if (arr[i].startsWith(val)) {
|
|
b = document.createElement("DIV");
|
|
itemInnerHTML(b,arr[i],val,0);
|
|
appendElement(b,i);
|
|
if(container.children.length>=MAXITEMS) break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
inp.addEventListener("keydown", function(e) {
|
|
var x = container.getElementsByTagName("div");
|
|
if (0===x.length) return false;
|
|
if (e.keyCode == 40) {//downarrow
|
|
currentFocus++;
|
|
addActive(x);
|
|
} else if (e.keyCode == 38) { //up
|
|
currentFocus--;
|
|
addActive(x);
|
|
} else if (e.keyCode == 13) {
|
|
if (currentFocus > -1) {
|
|
e.preventDefault();
|
|
if (x) x[currentFocus].click();
|
|
currentFocus = -1;
|
|
}
|
|
closeAllLists();
|
|
lastVal = null;
|
|
}
|
|
});
|
|
function addActive(x) {
|
|
removeActive(x);
|
|
if (currentFocus >= x.length) currentFocus = 0;
|
|
if (currentFocus < 0) currentFocus = (x.length - 1);
|
|
x[currentFocus].classList.add("autocomplete-active");
|
|
}
|
|
function removeActive(x) {
|
|
for (var i = 0; i < x.length; i++) {
|
|
x[i].classList.remove("autocomplete-active");
|
|
}
|
|
}
|
|
function closeAllLists() {
|
|
container.innerHTML = '';
|
|
}
|
|
inp.addEventListener("blur", function () {
|
|
setTimeout(()=>container.classList.add("invis"),200);
|
|
});
|
|
inp.addEventListener("focus", function () {
|
|
container.classList.remove("invis");
|
|
});
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<form class="autocomplete" autocomplete="off" action="javascript:handleQuery(getQ())">
|
|
<input type="text" name=q style="width:100%" autofocus>
|
|
<div class="autocomplete-items"></div>
|
|
</form>
|
|
<div class="webviews">
|
|
<webview class="curWV" allowpopups></webview>
|
|
</div>
|
|
<script>
|
|
tabs = document.body.children[1];
|
|
initTab(tabs.children[0]);
|
|
{
|
|
let inp = document.forms[0].q;
|
|
autocomplete(inp,inp.nextElementSibling,autocStrArray);
|
|
}
|
|
document.addEventListener('keydown', keyPress);
|
|
</script>
|
|
</body></html>
|