mirror of
https://github.com/csd4ni3l/browser.git
synced 2025-11-05 04:57:57 +01:00
Add css caching, adapt to LineLayout and Textlayout, add link support, add a resolve_url function, cache colors and space widths for fonts, fix bug crashing if font doesnt exist
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -180,4 +180,5 @@ test*.py
|
||||
logs/
|
||||
logs
|
||||
settings.json
|
||||
http_cache/
|
||||
html_cache/
|
||||
css_cache/
|
||||
@@ -2,6 +2,21 @@ import socket, logging, ssl, threading, os, ujson, time
|
||||
|
||||
from http_client.html_parser import HTML, CSSParser, Element, tree_to_list, get_inline_styles
|
||||
|
||||
def resolve_url(scheme, host, port, path, url):
|
||||
if "://" in url: return url
|
||||
if not url.startswith("/"):
|
||||
dir, _ = path.rsplit("/", 1)
|
||||
while url.startswith("../"):
|
||||
_, url = url.split("/", 1)
|
||||
if "/" in dir:
|
||||
dir, _ = dir.rsplit("/", 1)
|
||||
url = f"{dir}/{url}"
|
||||
|
||||
if url.startswith("//"):
|
||||
return f"{scheme}:{url}"
|
||||
else:
|
||||
return f"{scheme}://{host}:{port}{url}"
|
||||
|
||||
class HTTPClient():
|
||||
def __init__(self):
|
||||
self.scheme = "http"
|
||||
@@ -57,7 +72,7 @@ class HTTPClient():
|
||||
self.request_headers["Host"] = self.host
|
||||
|
||||
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"html_cache/{cache_filename}"):
|
||||
threading.Thread(target=self.parse, daemon=True).start()
|
||||
return
|
||||
|
||||
@@ -175,7 +190,7 @@ class HTTPClient():
|
||||
def parse(self):
|
||||
self.css_rules = []
|
||||
|
||||
cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.json"
|
||||
html_cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}.json"
|
||||
|
||||
original_scheme = self.scheme
|
||||
original_host = self.host
|
||||
@@ -183,12 +198,12 @@ class HTTPClient():
|
||||
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:
|
||||
if html_cache_filename in os.listdir("html_cache"):
|
||||
with open(f"html_cache/{html_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:
|
||||
with open(f"html_cache/{html_cache_filename}", "w") as file:
|
||||
json_list = HTML.to_json(self.nodes)
|
||||
file.write(ujson.dumps(json_list))
|
||||
|
||||
@@ -204,22 +219,22 @@ class HTTPClient():
|
||||
for css_link in css_links:
|
||||
self.content_response = ""
|
||||
|
||||
if "://" in css_link:
|
||||
self.get_request(css_link, self.request_headers, True)
|
||||
css_cache_filename = f"{self.scheme}_{self.host}_{self.port}_{self.path.replace('/', '_')}_{css_link.replace('/', '_')}.json" # we need to include the other variables so for example /styles.css wouldnt be cached for all websites
|
||||
|
||||
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)
|
||||
if css_cache_filename in os.listdir("css_cache"):
|
||||
with open(f"css_cache/{css_cache_filename}", "r") as file:
|
||||
rules = CSSParser.from_json(ujson.load(file))
|
||||
else:
|
||||
self.get_request(self.scheme + "://" + self.host + ":" + str(self.port) + css_link, self.request_headers, True)
|
||||
|
||||
self.get_request(resolve_url(self.scheme, self.host, self.port, self.path, css_link), self.request_headers, css=True)
|
||||
while not self.content_response:
|
||||
time.sleep(0.025)
|
||||
|
||||
self.css_rules.extend(CSSParser(self.content_response).parse())
|
||||
rules = CSSParser(self.content_response).parse()
|
||||
|
||||
with open(f"css_cache/{css_cache_filename}", "w") as file:
|
||||
ujson.dump(CSSParser.to_json(rules), file)
|
||||
|
||||
self.css_rules.extend(rules)
|
||||
|
||||
self.css_rules.extend(get_inline_styles(self.nodes))
|
||||
|
||||
|
||||
@@ -281,6 +281,30 @@ class CSSParser:
|
||||
break
|
||||
return rules
|
||||
|
||||
@classmethod
|
||||
def convert_selector_to_json(self, selector):
|
||||
if isinstance(selector, TagSelector):
|
||||
return ["tag", selector.tag, selector.priority]
|
||||
elif isinstance(selector, DescendantSelector):
|
||||
return ["descendant", self.convert_selector_to_json(selector.ancestor), self.convert_selector_to_json(selector.descendant)]
|
||||
|
||||
@classmethod
|
||||
def get_selector_from_json(self, selector_list):
|
||||
if selector_list[0] == "tag":
|
||||
selector = TagSelector(selector_list[1])
|
||||
selector.priority = selector_list[2]
|
||||
return selector
|
||||
elif selector_list[0] == "descendant":
|
||||
return DescendantSelector(self.get_selector_from_json(selector_list[1]), self.get_selector_from_json(selector_list[2]))
|
||||
|
||||
@classmethod
|
||||
def to_json(self, rules_list: list[tuple[TagSelector | DescendantSelector, dict[str, str]]]):
|
||||
return [[self.convert_selector_to_json(rule[0]), rule[1]] for rule in rules_list]
|
||||
|
||||
@classmethod
|
||||
def from_json(self, rules_list):
|
||||
return [(self.get_selector_from_json(rule[0]), rule[1]) for rule in rules_list]
|
||||
|
||||
def style(node, rules):
|
||||
node.style = {}
|
||||
|
||||
|
||||
@@ -3,13 +3,28 @@ import arcade, pyglet
|
||||
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.html_parser import CSSParser, Text, Element, style, cascade_priority, replace_symbols
|
||||
from http_client.connection import HTTPClient, resolve_url
|
||||
from http_client.html_parser import CSSParser, Text, Element, style, cascade_priority, replace_symbols, tree_to_list
|
||||
|
||||
from pyglet.font.base import Font as BaseFont
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
HSTEP = 13
|
||||
VSTEP = 18
|
||||
|
||||
font_cache = {}
|
||||
|
||||
def ensure_font(font_family, size, weight, style, emoji):
|
||||
if not (font_family, size, weight, style, emoji) in font_cache:
|
||||
font_cache[(font_family, size, weight, style, emoji)] = pyglet.font.load(font_family if pyglet.font.have_font(font_family) else "Arial", size, weight, style == "italic") if not emoji else pyglet.font.load("OpenMoji Color", size, weight, style == "italic")
|
||||
|
||||
return font_cache[(font_family, size, weight, style, emoji)]
|
||||
|
||||
@lru_cache
|
||||
def get_space_width(font: BaseFont):
|
||||
return font.get_text_size(" ")[0]
|
||||
|
||||
class DrawText:
|
||||
def __init__(self, x1, y1, text, font, color):
|
||||
self.top = y1
|
||||
@@ -26,6 +41,75 @@ class DrawRect:
|
||||
self.height = height
|
||||
self.color = color
|
||||
|
||||
class LineLayout:
|
||||
def __init__(self, node, parent, previous):
|
||||
self.node = node
|
||||
self.parent = parent
|
||||
self.previous = previous
|
||||
self.children = []
|
||||
|
||||
def paint(self):
|
||||
return []
|
||||
|
||||
def layout(self):
|
||||
self.width = self.parent.width
|
||||
self.x = self.parent.x
|
||||
|
||||
if self.previous:
|
||||
self.y = self.previous.y + self.previous.height
|
||||
else:
|
||||
self.y = self.parent.y
|
||||
|
||||
for word in self.children:
|
||||
word.layout()
|
||||
|
||||
if not self.children:
|
||||
self.height = 0
|
||||
return
|
||||
|
||||
fonts_on_line = [word.font for word in self.children]
|
||||
max_ascent = max(font.ascent for font in fonts_on_line)
|
||||
|
||||
baseline = self.y + 2 * max_ascent
|
||||
|
||||
for word in self.children:
|
||||
word.y = baseline - word.font.ascent
|
||||
|
||||
max_descent = min(font.descent for font in fonts_on_line)
|
||||
|
||||
self.height = 2 * (max_ascent + max_descent)
|
||||
|
||||
class TextLayout():
|
||||
def __init__(self, node, word, emoji, parent, previous):
|
||||
self.node = node
|
||||
self.word = word
|
||||
self.children = []
|
||||
self.parent = parent
|
||||
self.previous = previous
|
||||
self.emoji = emoji
|
||||
|
||||
def paint(self):
|
||||
color = get_color_from_name(self.node.style["color"])
|
||||
return [DrawText(self.x, self.y, self.word, self.font, color)]
|
||||
|
||||
def layout(self):
|
||||
weight = self.node.style["font-weight"]
|
||||
style = self.node.style["font-style"]
|
||||
font_family = self.node.style["font-family"]
|
||||
style = "roman" if style == "normal" else style
|
||||
size = int(float(self.node.style["font-size"][:-2]))
|
||||
self.font = ensure_font(font_family, size, weight, style, self.emoji)
|
||||
|
||||
self.width = self.font.get_text_size(self.word + (" " if not self.emoji else " "))[0]
|
||||
|
||||
if self.previous:
|
||||
space = get_space_width(self.previous.font)
|
||||
self.x = self.previous.x + space + self.previous.width
|
||||
else:
|
||||
self.x = self.parent.x
|
||||
|
||||
self.height = self.font.ascent + self.font.descent
|
||||
|
||||
class BlockLayout:
|
||||
def __init__(self, node, parent, previous):
|
||||
self.node = node
|
||||
@@ -33,8 +117,7 @@ class BlockLayout:
|
||||
self.previous = previous
|
||||
|
||||
self.children = []
|
||||
self.display_list = []
|
||||
self.line = []
|
||||
self.cursor_x = 0
|
||||
|
||||
self.x, self.y, self.width, self.height = None, None, None, None
|
||||
|
||||
@@ -46,17 +129,12 @@ class BlockLayout:
|
||||
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):
|
||||
if isinstance(self.node, Text):
|
||||
return "inline"
|
||||
elif any([isinstance(child, Element) and \
|
||||
child.tag in BLOCK_ELEMENTS
|
||||
for child in self.node.children]):
|
||||
elif any([isinstance(child, Element) and child.tag in BLOCK_ELEMENTS for child in self.node.children]):
|
||||
return "block"
|
||||
elif self.node.children:
|
||||
return "inline"
|
||||
@@ -80,26 +158,13 @@ class BlockLayout:
|
||||
self.children.append(next)
|
||||
previous = next
|
||||
else:
|
||||
self.cursor_x = 0
|
||||
self.cursor_y = 0
|
||||
|
||||
self.line = []
|
||||
self.new_line()
|
||||
self.recurse(self.node)
|
||||
self.flush()
|
||||
|
||||
for child in self.children:
|
||||
child.layout()
|
||||
|
||||
if mode == "block":
|
||||
self.height = sum([child.height for child in self.children])
|
||||
else:
|
||||
self.height = self.cursor_y
|
||||
|
||||
def ensure_font(self, font_family, size, weight, style, emoji):
|
||||
if not (font_family, size, weight, style, emoji) in font_cache:
|
||||
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 font_cache[(font_family, size, weight, style, emoji)]
|
||||
|
||||
def recurse(self, node):
|
||||
if isinstance(node, Text):
|
||||
@@ -109,10 +174,10 @@ class BlockLayout:
|
||||
if emoji_pattern.fullmatch(word):
|
||||
self.word(self.node, word, emoji=True)
|
||||
else:
|
||||
self.word(self.node, replace_symbols(word))
|
||||
self.word(self.node, word)
|
||||
else:
|
||||
if node.tag == "br":
|
||||
self.flush()
|
||||
self.new_line()
|
||||
|
||||
for child in node.children:
|
||||
self.recurse(child)
|
||||
@@ -123,36 +188,28 @@ class BlockLayout:
|
||||
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)
|
||||
font = ensure_font(font_family, size, weight, style, emoji)
|
||||
|
||||
w = font.get_text_size(word + (" " if not emoji else " "))[0]
|
||||
|
||||
if self.cursor_x + w > self.width:
|
||||
self.flush()
|
||||
self.new_line()
|
||||
|
||||
self.line.append((self.cursor_x, word, font, color))
|
||||
self.cursor_x += w + font.get_text_size(" ")[0]
|
||||
line = self.children[-1]
|
||||
previous_word = line.children[-1] if line.children else None
|
||||
text = TextLayout(node, word, emoji, line, previous_word)
|
||||
line.children.append(text)
|
||||
|
||||
def flush(self):
|
||||
if not self.line:
|
||||
return
|
||||
|
||||
fonts_on_line = [font for x, word, font, color in self.line]
|
||||
max_ascent = max(font.ascent for font in fonts_on_line)
|
||||
max_descent = min(font.descent for font in fonts_on_line)
|
||||
|
||||
baseline = self.cursor_y + 2 * max_ascent
|
||||
|
||||
for rel_x, word, font, color in self.line:
|
||||
x = self.x + rel_x
|
||||
y = self.y + baseline - font.ascent
|
||||
self.display_list.append((x, y, word, font, color))
|
||||
self.cursor_x += w + get_space_width(font)
|
||||
|
||||
def new_line(self):
|
||||
self.cursor_x = 0
|
||||
self.line = []
|
||||
self.cursor_y = baseline + 2 * max_descent
|
||||
|
||||
last_line = self.children[-1] if self.children else None
|
||||
new_line = LineLayout(self.node, self, last_line)
|
||||
|
||||
self.children.append(new_line)
|
||||
|
||||
class DocumentLayout:
|
||||
def __init__(self, node):
|
||||
@@ -169,7 +226,6 @@ class DocumentLayout:
|
||||
self.y = VSTEP
|
||||
child.layout()
|
||||
self.height = child.height
|
||||
self.display_list = child.display_list
|
||||
|
||||
def paint(self):
|
||||
return []
|
||||
@@ -196,6 +252,7 @@ class Renderer():
|
||||
self.text_to_create = []
|
||||
|
||||
self.window.on_mouse_scroll = self.on_mouse_scroll
|
||||
self.window.on_mouse_press = self.on_mouse_press
|
||||
self.window.on_resize = self.on_resize
|
||||
|
||||
self.batch = pyglet.graphics.Batch()
|
||||
@@ -222,11 +279,36 @@ class Renderer():
|
||||
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) + 5)) # flip scroll direction
|
||||
|
||||
|
||||
for widget in self.widgets:
|
||||
widget.y += (self.scroll_y - old_y)
|
||||
|
||||
self.hide_out_of_bounds_labels()
|
||||
|
||||
def on_mouse_press(self, x: int, y: int, button: int, modifiers: int):
|
||||
y -= self.scroll_y
|
||||
|
||||
objs = [
|
||||
obj for obj in tree_to_list(self.document, [])
|
||||
if obj.x <= x < obj.x + obj.width
|
||||
and ((self.window.height * 0.925) - obj.y - obj.height) <= y < ((self.window.height * 0.925) - obj.y)
|
||||
]
|
||||
|
||||
if not objs:
|
||||
return
|
||||
|
||||
|
||||
elt = objs[-1].node
|
||||
|
||||
while elt:
|
||||
if isinstance(elt, Text):
|
||||
pass
|
||||
elif elt.tag == "a" and "href" in elt.attributes:
|
||||
url = resolve_url(self.http_client.scheme, self.http_client.host, self.http_client.port, self.http_client.path, elt.attributes["href"])
|
||||
self.http_client.get_request(url, self.http_client.request_headers)
|
||||
|
||||
elt = elt.parent
|
||||
|
||||
def add_text(self, x, y, text, font, color, multiline=False):
|
||||
self.widgets.append(
|
||||
pyglet.text.Label(
|
||||
|
||||
7
run.py
7
run.py
@@ -20,8 +20,11 @@ pyglet.font.add_directory('./assets/fonts')
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
|
||||
if not os.path.exists("http_cache"):
|
||||
os.makedirs("http_cache")
|
||||
if not os.path.exists("html_cache"):
|
||||
os.makedirs("html_cache")
|
||||
|
||||
if not os.path.exists("css_cache"):
|
||||
os.makedirs("css_cache")
|
||||
|
||||
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)]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import logging, traceback
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
import pyglet.display, arcade
|
||||
|
||||
def dump_platform():
|
||||
@@ -64,19 +66,21 @@ class FakePyPresence():
|
||||
def close(self, *args, **kwargs):
|
||||
...
|
||||
|
||||
@lru_cache
|
||||
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))
|
||||
|
||||
@lru_cache
|
||||
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}")
|
||||
color_value = arcade.color.__dict__.get(f"LIGHT_{color_name}")
|
||||
|
||||
if color_value:
|
||||
return color_value
|
||||
|
||||
Reference in New Issue
Block a user