mirror of
https://github.com/csd4ni3l/browser.git
synced 2025-11-05 04:57:57 +01:00
Add backgrounds to finish layout tutorial, add inline and remote CSS support with a default of browser.css which currently supports fonts and backgrounds
This commit is contained in:
43
assets/css/browser.css
Normal file
43
assets/css/browser.css
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
a {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
big {
|
||||||
|
font-size: 110%;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
color: black;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
display: block;
|
||||||
|
font-size: 200%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
display: block;
|
||||||
|
font-size: 150%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
display: block;
|
||||||
|
font-size: 117%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import socket, logging, ssl, threading, os
|
import socket, logging, ssl, threading, os, ujson, time
|
||||||
|
|
||||||
|
from http_client.html_parser import HTML, CSSParser, Element, tree_to_list, get_inline_styles
|
||||||
|
|
||||||
class HTTPClient():
|
class HTTPClient():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -11,6 +13,8 @@ class HTTPClient():
|
|||||||
self.response_headers = {}
|
self.response_headers = {}
|
||||||
self.response_http_version = None
|
self.response_http_version = None
|
||||||
self.response_status = None
|
self.response_status = None
|
||||||
|
self.nodes = []
|
||||||
|
self.css_rules = []
|
||||||
self.content_response = ""
|
self.content_response = ""
|
||||||
self.view_source = False
|
self.view_source = False
|
||||||
self.redirect_count = 0
|
self.redirect_count = 0
|
||||||
@@ -20,7 +24,7 @@ class HTTPClient():
|
|||||||
with open(url.split("file://", 1)[1], "r") as file:
|
with open(url.split("file://", 1)[1], "r") as file:
|
||||||
self.content_response = file.read()
|
self.content_response = file.read()
|
||||||
|
|
||||||
def get_request(self, url, request_headers):
|
def get_request(self, url, request_headers, css=False):
|
||||||
if url.startswith("view-source:"):
|
if url.startswith("view-source:"):
|
||||||
url = url.split("view-source:")[1]
|
url = url.split("view-source:")[1]
|
||||||
self.view_source = True
|
self.view_source = True
|
||||||
@@ -54,7 +58,7 @@ class HTTPClient():
|
|||||||
|
|
||||||
cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.json"
|
cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.json"
|
||||||
if os.path.exists(f"http_cache/{cache_filename}"):
|
if os.path.exists(f"http_cache/{cache_filename}"):
|
||||||
self.needs_render = True
|
threading.Thread(target=self.parse, daemon=True).start()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
@@ -75,9 +79,9 @@ class HTTPClient():
|
|||||||
|
|
||||||
self.socket.send(request.encode())
|
self.socket.send(request.encode())
|
||||||
|
|
||||||
threading.Thread(target=self.receive_response, daemon=True).start()
|
threading.Thread(target=self.receive_response, daemon=True, args=(css,)).start()
|
||||||
|
|
||||||
def receive_response(self):
|
def receive_response(self, css=False):
|
||||||
buffer = b""
|
buffer = b""
|
||||||
headers_parsed = False
|
headers_parsed = False
|
||||||
content_length = None
|
content_length = None
|
||||||
@@ -123,6 +127,9 @@ class HTTPClient():
|
|||||||
logging.error(f"Error receiving messages: {e}")
|
logging.error(f"Error receiving messages: {e}")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self.socket.close()
|
||||||
|
self.socket = None
|
||||||
|
|
||||||
if 300 <= int(self.response_status) < 400:
|
if 300 <= int(self.response_status) < 400:
|
||||||
if self.redirect_count >= 4:
|
if self.redirect_count >= 4:
|
||||||
return
|
return
|
||||||
@@ -134,9 +141,9 @@ class HTTPClient():
|
|||||||
self.get_request(f"{self.scheme}://{self.host}{location_header}", self.request_headers)
|
self.get_request(f"{self.scheme}://{self.host}{location_header}", self.request_headers)
|
||||||
else:
|
else:
|
||||||
self.redirect_count = 0
|
self.redirect_count = 0
|
||||||
self.socket.close()
|
|
||||||
|
if not css:
|
||||||
self.needs_render = True
|
self.parse()
|
||||||
|
|
||||||
def _parse_headers(self, header_data):
|
def _parse_headers(self, header_data):
|
||||||
lines = header_data.splitlines()
|
lines = header_data.splitlines()
|
||||||
@@ -164,3 +171,61 @@ class HTTPClient():
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
logging.error(f"Error parsing header line: {line}")
|
logging.error(f"Error parsing header line: {line}")
|
||||||
self.response_headers = headers
|
self.response_headers = headers
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
self.css_rules = []
|
||||||
|
|
||||||
|
cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.json"
|
||||||
|
|
||||||
|
original_scheme = self.scheme
|
||||||
|
original_host = self.host
|
||||||
|
original_port = self.port
|
||||||
|
original_path = self.path
|
||||||
|
original_response = self.content_response
|
||||||
|
|
||||||
|
if cache_filename in os.listdir("http_cache"):
|
||||||
|
with open(f"http_cache/{cache_filename}", "r") as file:
|
||||||
|
self.nodes = HTML.from_json(ujson.load(file))
|
||||||
|
else:
|
||||||
|
self.nodes = HTML(self.content_response).parse()
|
||||||
|
with open(f"http_cache/{cache_filename}", "w") as file:
|
||||||
|
json_list = HTML.to_json(self.nodes)
|
||||||
|
file.write(ujson.dumps(json_list))
|
||||||
|
|
||||||
|
css_links = [
|
||||||
|
node.attributes["href"]
|
||||||
|
for node in tree_to_list(self.nodes, [])
|
||||||
|
if isinstance(node, Element)
|
||||||
|
and node.tag == "link"
|
||||||
|
and node.attributes.get("rel") == "stylesheet"
|
||||||
|
and "href" in node.attributes
|
||||||
|
]
|
||||||
|
|
||||||
|
for css_link in css_links:
|
||||||
|
self.content_response = ""
|
||||||
|
|
||||||
|
if "://" in css_link:
|
||||||
|
self.get_request(css_link, self.request_headers, True)
|
||||||
|
|
||||||
|
if not css_link.startswith("/"):
|
||||||
|
dir, _ = self.path.rsplit("/", 1)
|
||||||
|
css_link = dir + "/" + css_link
|
||||||
|
|
||||||
|
if css_link.startswith("//"):
|
||||||
|
self.get_request(self.scheme + ":" + css_link, self.request_headers, True)
|
||||||
|
else:
|
||||||
|
self.get_request(self.scheme + "://" + self.host + ":" + str(self.port) + css_link, self.request_headers, True)
|
||||||
|
|
||||||
|
while not self.content_response:
|
||||||
|
time.sleep(0.025)
|
||||||
|
|
||||||
|
self.css_rules.extend(CSSParser(self.content_response).parse())
|
||||||
|
|
||||||
|
self.css_rules.extend(get_inline_styles(self.nodes))
|
||||||
|
|
||||||
|
self.scheme = original_scheme
|
||||||
|
self.host = original_host
|
||||||
|
self.port = original_port
|
||||||
|
self.path = original_path
|
||||||
|
self.content_response = original_response
|
||||||
|
self.needs_render = True
|
||||||
@@ -1,13 +1,5 @@
|
|||||||
SELF_CLOSING_TAGS = [
|
from utils.constants import SELF_CLOSING_TAGS, HEAD_TAGS, INHERITED_PROPERTIES
|
||||||
"area", "base", "br", "col", "embed", "hr", "img", "input",
|
import html.entities
|
||||||
"link", "meta", "param", "source", "track", "wbr",
|
|
||||||
]
|
|
||||||
|
|
||||||
HEAD_TAGS = [
|
|
||||||
"base", "basefont", "bgsound", "noscript",
|
|
||||||
"link", "meta", "title", "style", "script",
|
|
||||||
]
|
|
||||||
|
|
||||||
class Element:
|
class Element:
|
||||||
def __init__(self, tag, attributes, parent):
|
def __init__(self, tag, attributes, parent):
|
||||||
self.tag = tag
|
self.tag = tag
|
||||||
@@ -81,7 +73,6 @@ class HTML():
|
|||||||
|
|
||||||
return tag, attributes
|
return tag, attributes
|
||||||
|
|
||||||
|
|
||||||
def add_tag(self, tag):
|
def add_tag(self, tag):
|
||||||
tag, attributes = self.get_attributes(tag)
|
tag, attributes = self.get_attributes(tag)
|
||||||
|
|
||||||
@@ -150,4 +141,186 @@ class HTML():
|
|||||||
elif json_list[0] == "element":
|
elif json_list[0] == "element":
|
||||||
element = Element(json_list[1], json_list[2], parent)
|
element = Element(json_list[1], json_list[2], parent)
|
||||||
element.children = [HTML.from_json(child, element) for child in json_list[3]]
|
element.children = [HTML.from_json(child, element) for child in json_list[3]]
|
||||||
return element
|
return element
|
||||||
|
|
||||||
|
class TagSelector:
|
||||||
|
def __init__(self, tag):
|
||||||
|
self.tag = tag
|
||||||
|
self.priority = 1
|
||||||
|
|
||||||
|
def matches(self, node):
|
||||||
|
return isinstance(node, Element) and self.tag == node.tag
|
||||||
|
|
||||||
|
class DescendantSelector:
|
||||||
|
def __init__(self, ancestor, descendant):
|
||||||
|
self.ancestor = ancestor
|
||||||
|
self.descendant = descendant
|
||||||
|
self.priority = ancestor.priority + descendant.priority
|
||||||
|
|
||||||
|
def matches(self, node):
|
||||||
|
if not self.descendant.matches(node): return False
|
||||||
|
while node.parent:
|
||||||
|
if self.ancestor.matches(node.parent): return True
|
||||||
|
node = node.parent
|
||||||
|
return False
|
||||||
|
|
||||||
|
def cascade_priority(rule):
|
||||||
|
selector, body = rule
|
||||||
|
return selector.priority
|
||||||
|
|
||||||
|
def get_inline_styles(node):
|
||||||
|
all_rules = []
|
||||||
|
|
||||||
|
for node in node.children:
|
||||||
|
if isinstance(node, Element) and node.tag == "style":
|
||||||
|
all_rules.extend(CSSParser(node.children[0].text).parse()) # node's first children will just be a text element that contains the css
|
||||||
|
|
||||||
|
all_rules.extend(get_inline_styles(node))
|
||||||
|
|
||||||
|
return all_rules
|
||||||
|
|
||||||
|
class CSSParser:
|
||||||
|
def __init__(self, s):
|
||||||
|
self.s = s
|
||||||
|
self.i = 0
|
||||||
|
|
||||||
|
def whitespace(self):
|
||||||
|
while self.i < len(self.s) and self.s[self.i].isspace():
|
||||||
|
self.i += 1
|
||||||
|
|
||||||
|
def literal(self, literal):
|
||||||
|
if not (self.i < len(self.s) and self.s[self.i] == literal):
|
||||||
|
raise Exception("Parsing error")
|
||||||
|
self.i += 1
|
||||||
|
|
||||||
|
def word(self):
|
||||||
|
start = self.i
|
||||||
|
while self.i < len(self.s):
|
||||||
|
if self.s[self.i].isalnum() or self.s[self.i] in "#-.%":
|
||||||
|
self.i += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if not (self.i > start):
|
||||||
|
raise Exception("Parsing error")
|
||||||
|
return self.s[start:self.i]
|
||||||
|
|
||||||
|
def pair(self):
|
||||||
|
prop = self.word()
|
||||||
|
|
||||||
|
self.whitespace()
|
||||||
|
self.literal(":")
|
||||||
|
self.whitespace()
|
||||||
|
|
||||||
|
val = self.word()
|
||||||
|
|
||||||
|
return prop.casefold(), val
|
||||||
|
|
||||||
|
def ignore_until(self, chars):
|
||||||
|
while self.i < len(self.s):
|
||||||
|
if self.s[self.i] in chars:
|
||||||
|
return self.s[self.i]
|
||||||
|
else:
|
||||||
|
self.i += 1
|
||||||
|
return None
|
||||||
|
|
||||||
|
def body(self):
|
||||||
|
pairs = {}
|
||||||
|
while self.i < len(self.s) and self.s[self.i] != "}":
|
||||||
|
try:
|
||||||
|
prop, val = self.pair()
|
||||||
|
pairs[prop] = val
|
||||||
|
|
||||||
|
self.whitespace()
|
||||||
|
|
||||||
|
self.literal(";")
|
||||||
|
|
||||||
|
self.whitespace()
|
||||||
|
except Exception:
|
||||||
|
why = self.ignore_until([";", "}"])
|
||||||
|
if why == ";":
|
||||||
|
self.literal(";")
|
||||||
|
self.whitespace()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return pairs
|
||||||
|
|
||||||
|
def selector(self):
|
||||||
|
out = TagSelector(self.word().casefold())
|
||||||
|
self.whitespace()
|
||||||
|
while self.i < len(self.s) and self.s[self.i] != "{":
|
||||||
|
tag = self.word()
|
||||||
|
descendant = TagSelector(tag.casefold())
|
||||||
|
out = DescendantSelector(out, descendant)
|
||||||
|
self.whitespace()
|
||||||
|
return out
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
rules = []
|
||||||
|
while self.i < len(self.s):
|
||||||
|
try:
|
||||||
|
self.whitespace()
|
||||||
|
|
||||||
|
selector = self.selector()
|
||||||
|
|
||||||
|
self.literal("{")
|
||||||
|
|
||||||
|
self.whitespace()
|
||||||
|
|
||||||
|
body = self.body()
|
||||||
|
|
||||||
|
self.literal("}")
|
||||||
|
|
||||||
|
rules.append((selector, body))
|
||||||
|
except Exception:
|
||||||
|
why = self.ignore_until(["}"])
|
||||||
|
if why == "}":
|
||||||
|
self.literal("}")
|
||||||
|
self.whitespace()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return rules
|
||||||
|
|
||||||
|
def style(node, rules):
|
||||||
|
node.style = {}
|
||||||
|
|
||||||
|
for property, default_value in INHERITED_PROPERTIES.items():
|
||||||
|
if node.parent:
|
||||||
|
node.style[property] = node.parent.style[property]
|
||||||
|
else:
|
||||||
|
node.style[property] = default_value
|
||||||
|
|
||||||
|
for selector, body in rules:
|
||||||
|
if not selector.matches(node): continue
|
||||||
|
for property, value in body.items():
|
||||||
|
node.style[property] = value
|
||||||
|
|
||||||
|
if isinstance(node, Element) and "style" in node.attributes:
|
||||||
|
pairs = CSSParser(node.attributes["style"]).body()
|
||||||
|
for property, value in pairs.items():
|
||||||
|
node.style[property] = value
|
||||||
|
|
||||||
|
if node.style["font-size"].endswith("%"):
|
||||||
|
if node.parent:
|
||||||
|
parent_font_size = node.parent.style["font-size"]
|
||||||
|
else:
|
||||||
|
parent_font_size = INHERITED_PROPERTIES["font-size"]
|
||||||
|
|
||||||
|
node_pct = float(node.style["font-size"][:-1]) / 100
|
||||||
|
parent_px = float(parent_font_size[:-2])
|
||||||
|
node.style["font-size"] = str(node_pct * parent_px) + "px"
|
||||||
|
|
||||||
|
for child in node.children:
|
||||||
|
style(child, rules)
|
||||||
|
|
||||||
|
def tree_to_list(tree, list):
|
||||||
|
list.append(tree)
|
||||||
|
for child in tree.children:
|
||||||
|
tree_to_list(child, list)
|
||||||
|
return list
|
||||||
|
|
||||||
|
def replace_symbols(text):
|
||||||
|
for key, value in html.entities.html5.items():
|
||||||
|
text = text.replace(f"&{key};", value)
|
||||||
|
|
||||||
|
return text
|
||||||
@@ -1,22 +1,31 @@
|
|||||||
import arcade, arcade.gui, pyglet, os, ujson
|
import arcade, pyglet
|
||||||
|
|
||||||
from utils.constants import token_pattern, emoji_pattern
|
from utils.constants import BLOCK_ELEMENTS, token_pattern, emoji_pattern
|
||||||
|
from utils.utils import get_color_from_name, hex_to_rgb
|
||||||
|
|
||||||
from http_client.connection import HTTPClient
|
from http_client.connection import HTTPClient
|
||||||
from http_client.html_parser import HTML, Text, Element
|
from http_client.html_parser import CSSParser, Text, Element, style, cascade_priority, replace_symbols
|
||||||
|
|
||||||
BLOCK_ELEMENTS = [
|
|
||||||
"html", "body", "article", "section", "nav", "aside",
|
|
||||||
"h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "header",
|
|
||||||
"footer", "address", "p", "hr", "pre", "blockquote",
|
|
||||||
"ol", "ul", "menu", "li", "dl", "dt", "dd", "figure",
|
|
||||||
"figcaption", "main", "div", "table", "form", "fieldset",
|
|
||||||
"legend", "details", "summary"
|
|
||||||
]
|
|
||||||
|
|
||||||
HSTEP = 13
|
HSTEP = 13
|
||||||
VSTEP = 18
|
VSTEP = 18
|
||||||
|
|
||||||
|
font_cache = {}
|
||||||
|
class DrawText:
|
||||||
|
def __init__(self, x1, y1, text, font, color):
|
||||||
|
self.top = y1
|
||||||
|
self.left = x1
|
||||||
|
self.text = text
|
||||||
|
self.font = font
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
class DrawRect:
|
||||||
|
def __init__(self, x1, y1, width, height, color):
|
||||||
|
self.top = y1
|
||||||
|
self.left = x1
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.color = color
|
||||||
|
|
||||||
class BlockLayout:
|
class BlockLayout:
|
||||||
def __init__(self, node, parent, previous):
|
def __init__(self, node, parent, previous):
|
||||||
self.node = node
|
self.node = node
|
||||||
@@ -27,12 +36,20 @@ class BlockLayout:
|
|||||||
self.display_list = []
|
self.display_list = []
|
||||||
self.line = []
|
self.line = []
|
||||||
|
|
||||||
self.font_cache = {}
|
|
||||||
|
|
||||||
self.x, self.y, self.width, self.height = None, None, None, None
|
self.x, self.y, self.width, self.height = None, None, None, None
|
||||||
|
|
||||||
def paint(self):
|
def paint(self):
|
||||||
return self.display_list
|
cmds = []
|
||||||
|
if self.layout_mode() == "inline":
|
||||||
|
bgcolor = self.node.style.get("background-color", "transparent")
|
||||||
|
if bgcolor != "transparent":
|
||||||
|
rect = DrawRect(self.x, self.y, self.width, self.height, hex_to_rgb(bgcolor) if bgcolor.startswith("#") else get_color_from_name(bgcolor))
|
||||||
|
cmds.append(rect)
|
||||||
|
|
||||||
|
for x, y, word, font, color in self.display_list:
|
||||||
|
cmds.append(DrawText(x, y, word, font, color))
|
||||||
|
|
||||||
|
return cmds
|
||||||
|
|
||||||
def layout_mode(self):
|
def layout_mode(self):
|
||||||
if isinstance(self.node, Text):
|
if isinstance(self.node, Text):
|
||||||
@@ -65,9 +82,6 @@ class BlockLayout:
|
|||||||
else:
|
else:
|
||||||
self.cursor_x = 0
|
self.cursor_x = 0
|
||||||
self.cursor_y = 0
|
self.cursor_y = 0
|
||||||
self.weight = "normal"
|
|
||||||
self.style = "roman"
|
|
||||||
self.size = 16
|
|
||||||
|
|
||||||
self.line = []
|
self.line = []
|
||||||
self.recurse(self.node)
|
self.recurse(self.node)
|
||||||
@@ -77,89 +91,68 @@ class BlockLayout:
|
|||||||
child.layout()
|
child.layout()
|
||||||
|
|
||||||
if mode == "block":
|
if mode == "block":
|
||||||
self.height = sum([
|
self.height = sum([child.height for child in self.children])
|
||||||
child.height for child in self.children])
|
|
||||||
else:
|
else:
|
||||||
self.height = self.cursor_y
|
self.height = self.cursor_y
|
||||||
|
|
||||||
def ensure_font(self, size, weight, style, emoji):
|
def ensure_font(self, font_family, size, weight, style, emoji):
|
||||||
if not (size, weight, style, emoji) in self.font_cache:
|
if not (font_family, size, weight, style, emoji) in font_cache:
|
||||||
self.font_cache[(size, weight, style, emoji)] = pyglet.font.load("Roboto", size, weight, style == "italic") if not emoji else pyglet.font.load("OpenMoji Color", size, weight, style == "italic")
|
font_cache[(font_family, size, weight, style, emoji)] = pyglet.font.load(font_family, size, weight, style == "italic") if not emoji else pyglet.font.load("OpenMoji Color", size, weight, style == "italic")
|
||||||
|
|
||||||
return self.font_cache[(size, weight, style, emoji)]
|
return font_cache[(font_family, size, weight, style, emoji)]
|
||||||
|
|
||||||
def word(self, word: str, emoji=False):
|
def recurse(self, node):
|
||||||
font = self.ensure_font(self.size, self.weight, self.style, emoji)
|
if isinstance(node, Text):
|
||||||
|
word_list = [match.group(0) for match in token_pattern.finditer(node.text)]
|
||||||
|
|
||||||
|
for word in word_list:
|
||||||
|
if emoji_pattern.fullmatch(word):
|
||||||
|
self.word(self.node, word, emoji=True)
|
||||||
|
else:
|
||||||
|
self.word(self.node, replace_symbols(word))
|
||||||
|
else:
|
||||||
|
if node.tag == "br":
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
for child in node.children:
|
||||||
|
self.recurse(child)
|
||||||
|
|
||||||
|
def word(self, node, word: str, emoji=False):
|
||||||
|
weight = node.style["font-weight"]
|
||||||
|
style = node.style["font-style"]
|
||||||
|
font_family = node.style["font-family"]
|
||||||
|
style = "roman" if style == "normal" else style
|
||||||
|
size = int(float(node.style["font-size"][:-2]))
|
||||||
|
color = get_color_from_name(node.style["color"])
|
||||||
|
|
||||||
|
font = self.ensure_font(font_family, size, weight, style, emoji)
|
||||||
|
|
||||||
w = font.get_text_size(word + (" " if not emoji else " "))[0]
|
w = font.get_text_size(word + (" " if not emoji else " "))[0]
|
||||||
|
|
||||||
if self.cursor_x + w > self.width:
|
if self.cursor_x + w > self.width:
|
||||||
self.flush()
|
self.flush()
|
||||||
|
|
||||||
self.line.append((self.cursor_x, word, font))
|
self.line.append((self.cursor_x, word, font, color))
|
||||||
self.cursor_x += w + font.get_text_size(" ")[0]
|
self.cursor_x += w + font.get_text_size(" ")[0]
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
if not self.line:
|
if not self.line:
|
||||||
return
|
return
|
||||||
|
|
||||||
fonts_on_line = [font for x, word, font in self.line]
|
fonts_on_line = [font for x, word, font, color in self.line]
|
||||||
max_ascent = max(font.ascent for font in fonts_on_line)
|
max_ascent = max(font.ascent for font in fonts_on_line)
|
||||||
max_descent = min(font.descent for font in fonts_on_line)
|
max_descent = min(font.descent for font in fonts_on_line)
|
||||||
|
|
||||||
baseline = self.cursor_y + 1.25 * max_ascent
|
baseline = self.cursor_y + 2 * max_ascent
|
||||||
|
|
||||||
for rel_x, word, font in self.line:
|
for rel_x, word, font, color in self.line:
|
||||||
x = self.x + rel_x
|
x = self.x + rel_x
|
||||||
y = self.y + baseline - font.ascent
|
y = self.y + baseline - font.ascent
|
||||||
self.display_list.append((x, y, word, font))
|
self.display_list.append((x, y, word, font, color))
|
||||||
|
|
||||||
self.cursor_x = 0
|
self.cursor_x = 0
|
||||||
self.line = []
|
self.line = []
|
||||||
self.cursor_y = baseline + 1.25 * max_descent
|
self.cursor_y = baseline + 2 * max_descent
|
||||||
|
|
||||||
def recurse(self, tree):
|
|
||||||
if isinstance(tree, Text):
|
|
||||||
if "{" in tree.text or "}" in tree.text:
|
|
||||||
return
|
|
||||||
|
|
||||||
word_list = [match.group(0) for match in token_pattern.finditer(tree.text)]
|
|
||||||
|
|
||||||
for word in word_list:
|
|
||||||
if emoji_pattern.fullmatch(word):
|
|
||||||
self.word(word, emoji=True)
|
|
||||||
else:
|
|
||||||
self.word(word)
|
|
||||||
else:
|
|
||||||
self.open_tag(tree.tag)
|
|
||||||
for child in tree.children:
|
|
||||||
self.recurse(child)
|
|
||||||
self.close_tag(tree.tag)
|
|
||||||
|
|
||||||
def open_tag(self, tag):
|
|
||||||
if tag == "i":
|
|
||||||
self.style = "italic"
|
|
||||||
elif tag == "b":
|
|
||||||
self.weight = "bold"
|
|
||||||
elif tag == "small":
|
|
||||||
self.size -= 2
|
|
||||||
elif tag == "big":
|
|
||||||
self.size += 4
|
|
||||||
elif tag == "br":
|
|
||||||
self.flush()
|
|
||||||
|
|
||||||
def close_tag(self, tag):
|
|
||||||
if tag == "i":
|
|
||||||
self.style = "roman"
|
|
||||||
elif tag == "b":
|
|
||||||
self.weight = "normal"
|
|
||||||
elif tag == "small":
|
|
||||||
self.size += 2
|
|
||||||
elif tag == "big":
|
|
||||||
self.size -= 4
|
|
||||||
elif tag == "p":
|
|
||||||
self.flush()
|
|
||||||
self.cursor_y += VSTEP
|
|
||||||
|
|
||||||
class DocumentLayout:
|
class DocumentLayout:
|
||||||
def __init__(self, node):
|
def __init__(self, node):
|
||||||
@@ -191,7 +184,7 @@ class Renderer():
|
|||||||
def __init__(self, http_client: HTTPClient, window: arcade.Window):
|
def __init__(self, http_client: HTTPClient, window: arcade.Window):
|
||||||
self.content = ''
|
self.content = ''
|
||||||
self.request_scheme = 'http'
|
self.request_scheme = 'http'
|
||||||
|
self.window = window
|
||||||
self.http_client = http_client
|
self.http_client = http_client
|
||||||
|
|
||||||
self.scroll_y = 0
|
self.scroll_y = 0
|
||||||
@@ -199,18 +192,17 @@ class Renderer():
|
|||||||
self.allow_scroll = False
|
self.allow_scroll = False
|
||||||
self.smallest_y = 0
|
self.smallest_y = 0
|
||||||
|
|
||||||
self.text_labels: list[pyglet.text.Label] = []
|
self.widgets: list[pyglet.text.Label] = []
|
||||||
self.text_to_create = []
|
self.text_to_create = []
|
||||||
|
|
||||||
self.window = window
|
|
||||||
self.window.on_mouse_scroll = self.on_mouse_scroll
|
self.window.on_mouse_scroll = self.on_mouse_scroll
|
||||||
self.window.on_resize = self.on_resize
|
self.window.on_resize = self.on_resize
|
||||||
|
|
||||||
self.batch = pyglet.graphics.Batch()
|
self.batch = pyglet.graphics.Batch()
|
||||||
|
|
||||||
def hide_out_of_bounds_labels(self):
|
def hide_out_of_bounds_labels(self):
|
||||||
for widget in self.text_labels:
|
for widget in self.widgets:
|
||||||
invisible = (widget.y + widget.content_height) > self.window.height * 0.925
|
invisible = (widget.y + (widget.content_height if not isinstance(widget, pyglet.shapes.Rectangle) else widget.height)) > self.window.height * 0.925
|
||||||
# Doing visible flag set manually since it takes a lot of time
|
# Doing visible flag set manually since it takes a lot of time
|
||||||
if widget.visible:
|
if widget.visible:
|
||||||
if invisible:
|
if invisible:
|
||||||
@@ -220,22 +212,23 @@ class Renderer():
|
|||||||
widget.visible = True
|
widget.visible = True
|
||||||
|
|
||||||
def on_resize(self, width, height):
|
def on_resize(self, width, height):
|
||||||
self.http_client.needs_render = True
|
if self.http_client.css_rules:
|
||||||
|
self.http_client.needs_render = True
|
||||||
|
|
||||||
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
|
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
|
||||||
if not self.allow_scroll:
|
if not self.allow_scroll:
|
||||||
return
|
return
|
||||||
|
|
||||||
old_y = self.scroll_y
|
old_y = self.scroll_y
|
||||||
self.scroll_y = max(0, min(abs(self.scroll_y - (scroll_y * self.scroll_y_speed)), abs(self.smallest_y) - (self.window.height * 0.925) - 10)) # flip scroll direction
|
self.scroll_y = max(0, min(abs(self.scroll_y - (scroll_y * self.scroll_y_speed)), abs(self.smallest_y) - (self.window.height * 0.925) + 5)) # flip scroll direction
|
||||||
|
|
||||||
for widget in self.text_labels:
|
for widget in self.widgets:
|
||||||
widget.y += (self.scroll_y - old_y)
|
widget.y += (self.scroll_y - old_y)
|
||||||
|
|
||||||
self.hide_out_of_bounds_labels()
|
self.hide_out_of_bounds_labels()
|
||||||
|
|
||||||
def add_text(self, x, y, text, font, multiline=False):
|
def add_text(self, x, y, text, font, color, multiline=False):
|
||||||
self.text_labels.append(
|
self.widgets.append(
|
||||||
pyglet.text.Label(
|
pyglet.text.Label(
|
||||||
text=text,
|
text=text,
|
||||||
font_name=font.name,
|
font_name=font.name,
|
||||||
@@ -243,7 +236,7 @@ class Renderer():
|
|||||||
weight=font.weight,
|
weight=font.weight,
|
||||||
font_size=font.size,
|
font_size=font.size,
|
||||||
multiline=multiline,
|
multiline=multiline,
|
||||||
color=arcade.color.BLACK,
|
color=color,
|
||||||
x=x,
|
x=x,
|
||||||
y=(self.window.height * 0.925) - y,
|
y=(self.window.height * 0.925) - y,
|
||||||
batch=self.batch
|
batch=self.batch
|
||||||
@@ -253,6 +246,18 @@ class Renderer():
|
|||||||
if (self.window.height * 0.925) - y < self.smallest_y:
|
if (self.window.height * 0.925) - y < self.smallest_y:
|
||||||
self.smallest_y = y
|
self.smallest_y = y
|
||||||
|
|
||||||
|
def add_background(self, left, top, width, height, color):
|
||||||
|
self.widgets.append(
|
||||||
|
pyglet.shapes.Rectangle(
|
||||||
|
left,
|
||||||
|
(self.window.height * 0.925) - top - height,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
color,
|
||||||
|
batch=self.batch
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
if not self.http_client.needs_render:
|
if not self.http_client.needs_render:
|
||||||
return
|
return
|
||||||
@@ -260,36 +265,27 @@ class Renderer():
|
|||||||
self.http_client.needs_render = False
|
self.http_client.needs_render = False
|
||||||
self.allow_scroll = True
|
self.allow_scroll = True
|
||||||
|
|
||||||
for child in self.text_labels:
|
for child in self.widgets:
|
||||||
child.delete()
|
child.delete()
|
||||||
del child
|
del child
|
||||||
|
|
||||||
self.text_labels.clear()
|
self.widgets.clear()
|
||||||
self.smallest_y = 0
|
self.smallest_y = 0
|
||||||
|
|
||||||
if self.http_client.view_source or self.http_client.scheme == "file":
|
if self.http_client.view_source or self.http_client.scheme == "file":
|
||||||
self.add_text(x=HSTEP, y=0, text=self.http_client.content_response, font=pyglet.font.load("Roboto", 16), multiline=True)
|
self.add_text(x=HSTEP, y=0, text=self.http_client.content_response, font=pyglet.font.load("Roboto", 16), multiline=True)
|
||||||
elif self.http_client.scheme == "http" or self.http_client.scheme == "https":
|
elif self.http_client.scheme == "http" or self.http_client.scheme == "https":
|
||||||
if not os.path.exists("http_cache"):
|
style(self.http_client.nodes, sorted(self.http_client.css_rules + CSSParser(open("assets/css/browser.css").read()).parse(), key=cascade_priority))
|
||||||
os.makedirs("http_cache")
|
|
||||||
|
|
||||||
cache_filename = f"{self.http_client.scheme}_{self.http_client.host}_{self.http_client.port}_{self.http_client.path.replace('/', '_')}.json"
|
self.document = DocumentLayout(self.http_client.nodes)
|
||||||
|
|
||||||
if cache_filename in os.listdir("http_cache"):
|
|
||||||
with open(f"http_cache/{cache_filename}", "r") as file:
|
|
||||||
self.nodes = HTML.from_json(ujson.load(file))
|
|
||||||
else:
|
|
||||||
self.nodes = HTML(self.http_client.content_response).parse()
|
|
||||||
with open(f"http_cache/{cache_filename}", "w") as file:
|
|
||||||
json_list = HTML.to_json(self.nodes)
|
|
||||||
file.write(ujson.dumps(json_list))
|
|
||||||
|
|
||||||
self.document = DocumentLayout(self.nodes)
|
|
||||||
self.document.layout()
|
self.document.layout()
|
||||||
self.display_list = []
|
self.cmds = []
|
||||||
paint_tree(self.document, self.display_list)
|
paint_tree(self.document, self.cmds)
|
||||||
|
|
||||||
for x, y, text, font in self.display_list:
|
for cmd in self.cmds:
|
||||||
self.add_text(x, y, text, font)
|
if isinstance(cmd, DrawText):
|
||||||
|
self.add_text(cmd.left, cmd.top, cmd.text, cmd.font, cmd.color)
|
||||||
|
elif isinstance(cmd, DrawRect):
|
||||||
|
self.add_background(cmd.left, cmd.top, cmd.width, cmd.height, cmd.color)
|
||||||
|
|
||||||
self.hide_out_of_bounds_labels()
|
self.hide_out_of_bounds_labels()
|
||||||
5
run.py
5
run.py
@@ -14,9 +14,12 @@ sys.excepthook = on_exception
|
|||||||
pyglet.resource.path.append(os.getcwd())
|
pyglet.resource.path.append(os.getcwd())
|
||||||
pyglet.font.add_directory('./assets/fonts')
|
pyglet.font.add_directory('./assets/fonts')
|
||||||
|
|
||||||
if not log_dir in os.listdir():
|
if not os.path.exists(log_dir):
|
||||||
os.makedirs(log_dir)
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
|
if not os.path.exists("http_cache"):
|
||||||
|
os.makedirs("http_cache")
|
||||||
|
|
||||||
while len(os.listdir(log_dir)) >= 5:
|
while len(os.listdir(log_dir)) >= 5:
|
||||||
files = [(file, os.path.getctime(os.path.join(log_dir, file))) for file in os.listdir(log_dir)]
|
files = [(file, os.path.getctime(os.path.join(log_dir, file))) for file in os.listdir(log_dir)]
|
||||||
oldest_file = sorted(files, key=lambda x: x[1])[0][0]
|
oldest_file = sorted(files, key=lambda x: x[1])[0][0]
|
||||||
|
|||||||
@@ -32,6 +32,36 @@ menu_background_color = arcade.color.WHITE
|
|||||||
log_dir = 'logs'
|
log_dir = 'logs'
|
||||||
discord_presence_id = 1393164073566208051
|
discord_presence_id = 1393164073566208051
|
||||||
|
|
||||||
|
BLOCK_ELEMENTS = [
|
||||||
|
"html", "body", "article", "section", "nav", "aside",
|
||||||
|
"h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "header",
|
||||||
|
"footer", "address", "p", "hr", "pre", "blockquote",
|
||||||
|
"ol", "ul", "menu", "li", "dl", "dt", "dd", "figure",
|
||||||
|
"figcaption", "main", "div", "table", "form", "fieldset",
|
||||||
|
"legend", "details", "summary"
|
||||||
|
]
|
||||||
|
|
||||||
|
SELF_CLOSING_TAGS = [
|
||||||
|
"area", "base", "br", "col", "embed", "hr", "img", "input",
|
||||||
|
"link", "meta", "param", "source", "track", "wbr",
|
||||||
|
]
|
||||||
|
|
||||||
|
HEAD_TAGS = [
|
||||||
|
"base", "basefont", "bgsound", "noscript",
|
||||||
|
"link", "meta", "title", "style", "script",
|
||||||
|
]
|
||||||
|
|
||||||
|
INHERITED_PROPERTIES = {
|
||||||
|
"font-family": "Arial",
|
||||||
|
"font-size": "16px",
|
||||||
|
"font-style": "normal",
|
||||||
|
"font-weight": "normal",
|
||||||
|
"color": "black",
|
||||||
|
"display": "inline",
|
||||||
|
"width": "auto",
|
||||||
|
"height": "auto"
|
||||||
|
}
|
||||||
|
|
||||||
button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK),
|
button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK),
|
||||||
'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK)}
|
'press': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK)}
|
||||||
big_button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26),
|
big_button_style = {'normal': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26), 'hover': UITextureButtonStyle(font_name="Roboto", font_color=arcade.color.BLACK, font_size=26),
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import arcade.gui, arcade
|
import arcade.gui, arcade
|
||||||
|
from http_client.html_parser import CSSParser
|
||||||
|
|
||||||
button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button.png"))
|
button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button.png"))
|
||||||
button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button_hovered.png"))
|
button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button_hovered.png"))
|
||||||
|
|
||||||
|
DEFAULT_STYLE_SHEET = CSSParser(open("assets/css/browser.css").read()).parse()
|
||||||
|
|||||||
@@ -63,3 +63,23 @@ class FakePyPresence():
|
|||||||
...
|
...
|
||||||
def close(self, *args, **kwargs):
|
def close(self, *args, **kwargs):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
def hex_to_rgb(hex_color):
|
||||||
|
hex_color = hex_color.lstrip('#')
|
||||||
|
if len(hex_color) != 6:
|
||||||
|
return (127, 127, 127)
|
||||||
|
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||||
|
|
||||||
|
def get_color_from_name(rgb_name):
|
||||||
|
rgb_name = rgb_name.upper()
|
||||||
|
if rgb_name.startswith("LIGHT"):
|
||||||
|
color_name = rgb_name.split("LIGHT")[1]
|
||||||
|
color_value = arcade.csscolor.__dict__.get(f"LIGHT_{color_name}")
|
||||||
|
if not color_value:
|
||||||
|
arcade.color.__dict__.get(f"LIGHT_{color_name}")
|
||||||
|
|
||||||
|
if color_value:
|
||||||
|
return color_value
|
||||||
|
|
||||||
|
color_value = arcade.csscolor.__dict__.get(rgb_name)
|
||||||
|
return color_value if color_value else arcade.color.__dict__.get(rgb_name, arcade.color.GRAY)
|
||||||
|
|||||||
Reference in New Issue
Block a user