Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add various Document Object Model methods to HtmlFrame #110

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
46 changes: 33 additions & 13 deletions tkinterweb/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,10 +427,20 @@ def bbox(self, node=None):
"""Get the bounding box of the viewport or a specified node"""
return self.tk.call(self._w, "bbox", node)

def parse_fragment(self, html):
"""Parse part of a document comprised of nodes just like a standard document,
except that the document fragment isn't part of the active document. Changes
made to the fragment don't affect the document. Returns a root node."""
return self.tk.call(self._w, "fragment", html)

def get_node_text(self, node_handle, *args):
"""Get the text content of the given node"""
return self.tk.call(node_handle, "text", *args)

def set_node_text(self, node_handle, new):
"""Set the text content of the given node"""
return self.tk.call(node_handle, "text", "set", new)

def get_node_tag(self, node_handle):
"""Get the HTML tag of the given node"""
return self.tk.call(node_handle, "tag")
Expand Down Expand Up @@ -462,6 +472,10 @@ def insert_node(self, node_handle, children_nodes):
"""Experimental, insert the specified nodes into the parent node"""
return self.tk.call(node_handle, "insert", children_nodes)

def insert_node_before(self, node_handle, children_nodes, before):
"""Same as the last one except node is placed before another node"""
return self.tk.call(node_handle, "insert", "-before", before, children_nodes)

def delete_node(self, node_handle):
"""Delete the given node"""
return self.tk.call(node_handle, "destroy")
Expand Down Expand Up @@ -666,7 +680,7 @@ def on_image(self, url):
self.image_thread_check(url, name)
done = True
if not done:
self.load_alt_image(url, name)
self.load_alt_text(url, name)
self.message_func(
f"The image {shorten(url)} could not be shown because it is not supported yet.")
self.image_setup_func(url, True)
Expand Down Expand Up @@ -1066,15 +1080,13 @@ def fetch_styles(self, sheetid, handler, errorurl="", url=None, data=None):

self.finish_download(thread)

def load_alt_image(self, url, name):
if (url in self.image_directory) and self.image_alternate_text_enabled:
def load_alt_text(self, url, name):
if (url in self.image_directory):
node = self.image_directory[url]
nodebox = self.bbox(node)
alt = self.get_node_attribute(self.image_directory[url], "alt")
if alt:
image = textimage(name, alt, nodebox, self.image_alternate_text_font, self.image_alternate_text_size, self.image_alternate_text_threshold)
self.loaded_images.add(image)
elif not self.ignore_invalid_images:
alt = self.get_node_attribute(node, "alt")
if alt and self.image_alternate_text_enabled:
self.insert_node(node, self.parse_fragment(alt))
if not self.ignore_invalid_images:
image = newimage(self.broken_image, name, "image/png", self.image_inversion_enabled)
self.loaded_images.add(image)

Expand All @@ -1095,24 +1107,28 @@ def fetch_images(self, url, name, urltype):
if image:
self.loaded_images.add(image)
self.image_setup_func(url, True)
for node in self.search("img"):
if self.get_node_attribute(node, "src") == url:
if self.get_node_children(node): self.delete_node(self.get_node_children(node))
break
elif error == "no_pycairo":
self.load_alt_image(url, name)
self.load_alt_text(url, name)
self.message_func(
"Scalable Vector Graphics could not be shown because Pycairo is not installed but is required to parse .svg files.")
self.image_setup_func(url, False)
elif error == "no_rsvg":
self.load_alt_image(url, name)
self.load_alt_text(url, name)
self.message_func(
"Scalable Vector Graphics could not be shown because Rsvg is not installed but is required to parse .svg files.")
self.image_setup_func(url, False)
elif error == "corrupt":
self.load_alt_image(url, name)
self.load_alt_text(url, name)
self.message_func(
f"The image {url} could not be shown.")
self.image_setup_func(url, False)

except Exception:
self.load_alt_image(url, name)
self.load_alt_text(url, name)
self.message_func(
f"The image {url} could not be shown because it is corrupt or is not supported yet.")
self.image_setup_func(url, False)
Expand Down Expand Up @@ -1459,3 +1475,7 @@ def copy_selection(self, event=None):
self.clipboard_append(selected_text)
self.message_func(
f"The text '{selected_text}' has been copied to the clipboard.")

def image(self, full=""):
image = self.tk.call(self._w, "image", full)
return {image: self.tk.call(image, "data")}
73 changes: 73 additions & 0 deletions tkinterweb/docs/HTMLFRAME.md
Original file line number Diff line number Diff line change
Expand Up @@ -560,3 +560,76 @@ Parameters
* **oldwidget** *(tkinter.Widget)* - Specifies the Tkinter widget to remove. This must be a valid Tkinter widget that is currently managed by TkinterWeb.

---

#### `node_to_html(self, node, isDeep=True)`
Converts node to HTML.

Parameters
* **isDeep** *(boolean)* - Specifies whether to convert all child nodes as well.

Return type
* *string*

---

#### `get_inner_html(node)`
Get the HTML content of a node.

Parameters
* **node** *(node)* - Node in question.

Return type
* *string*

---

#### `set_inner_html(node, new)`
Get the HTML content of a node.

Parameters
* **node** *(node)* - Node in question.
* **new** *(string)* - New inner HTML.

---

#### `create_element(tagname)`
Create a new node of specified type.

Parameters
* **tagname** *(string)* - New node type e.g. <P>.

Return type
* *node*

---

#### `create_text_node(text)`
Create a new text node.

Parameters
* **text** *(string)* - New text.

Return type
* *node*

---

#### `text_content(node, text=None)`
Return or change the text content of a node.

Parameters
* **node** *(node)* - Node in question.
* **text** *(string)* - New text.

Return type
* *node*

---

#### `body()`
Returns a document's <body> element.

Return type
* *node*

---
12 changes: 12 additions & 0 deletions tkinterweb/docs/TKINTERWEB.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ Return the page zoom.
#### `insert_node(node_handle, children_nodes)`
Experimental, insert the specified nodes into the parent node.

---
#### `insert_node_before(node_handle, children_nodes, before)`
Same as the last one except node is placed before another node.

---
#### `node(*args)`
Retrieve one or more document node handles from the current document.
Expand All @@ -144,6 +148,10 @@ Parse HTML code.
#### `parse_css(sheetid=None, importcmd=None, data='')`
Parse CSS code.

---
#### `parse_fragment(html)`
Parse part of a document comprised of nodes just like a standard document, except that the document fragment isn't part of the active document. Changes made to the fragment don't affect the document. Returns a root node.

---
#### `remove_node_flags(node, name)`
Set dynamic flags on the given node.
Expand Down Expand Up @@ -188,6 +196,10 @@ Set dynamic flags on the given node.
#### `set_parsemode(mode)`
Set the page render mode.

---
#### `set_node_text(mode)`
Set the text content of the given text node.

---
#### `set_zoom(multiplier)`
Set the page zoom.
Expand Down
138 changes: 136 additions & 2 deletions tkinterweb/htmlwidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@

from bindings import TkinterWeb
from utilities import (WORKING_DIR, AutoScrollbar, StoppableThread, cachedownload, download,
notifier, threadname)
from imageutils import newimage
notifier, threadname, extract_nested, escape_Tcl)
from imageutils import newimage, createRGBimage

import tkinter as tk
from tkinter import ttk
Expand Down Expand Up @@ -139,6 +139,10 @@ def __init__(self, master, messages_enabled=True, vertical_scrollbar="auto", hor
self.yview_moveto = self.html.yview_moveto
self.yview_scroll = self.html.yview_scroll

# Python functions used by Tcl later in this file
self.html.tk.createcommand("node_to_html", self.node_to_html)
self.html.tk.createcommand("text_content_get", self.text_content)

def yview_toelement(self, selector, index=0):
"Find an element that matches a given CSS selectors and scroll to it"
nodes = self.html.search(selector)
Expand Down Expand Up @@ -534,6 +538,136 @@ def add_css(self, css_source):
else:
self.html.parse_css(data=css_source, override=True)

def node_to_html(self, node, isDeep=True):
return self.tk.eval(r"""
proc TclNode_to_html {node} {
set tag [$node tag]
if {$tag eq ""} {
append ret [$node text -pre]
} else {
append ret <$tag
foreach {zKey zVal} [$node attribute] {
set zEscaped [string map [list "\x22" "\x5C\x22"] $zVal]
append ret " $zKey=\"$zEscaped\""
}
append ret >
if {%s} {
append ret [node_to_childrenHtml $node]
}
append ret </$tag>
}
}
proc node_to_childrenHtml {node} {
set ret ""
foreach child [$node children] {
append ret [TclNode_to_html $child]
}
return $ret
}
return [TclNode_to_html %s]
""" % (isDeep, extract_nested(node))
)

def get_inner_html(self, node):
return self.tk.eval("""
set node %s
if {[$node tag] eq ""} {error "$node is not an HTMLElement"}

set ret ""
foreach child [$node children] {
append ret [node_to_html $child 1]
}
update
return $ret
""" % extract_nested(node)
)

def set_inner_html(self, node, new):
if extract_nested(node) is None: raise ValueError("Node is empty.")
self.tk.eval("""
set node %s

if {[$node tag] eq ""} {error "$node is not an HTMLElement"}

# Destroy the existing children (and their descendants) of $node.
set children [$node children]
$node remove $children
foreach child $children {
$child destroy
}

set newHtml "%s"
# Insert the new descendants, created by parseing $newHtml.
set children [%s fragment $newHtml]
$node insert $children
update
""" % (extract_nested(node), escape_Tcl(new), self.html)
)

def create_element(self, tagname):
return self.tk.eval("""
set node [%s fragment "<%s>"]
if {$node eq ""} {error "DOMException NOT_SUPPORTED_ERR"}
return $node
""" % (self.html, tagname)
)

def create_text_node(self, text):
return self.tk.eval("""
set tkw %s
set text "%s"
if {$text eq ""} {
# Special case - The [fragment] API usually parses an empty string
# to an empty fragment. So create a text node with text "X", then
# set the text to an empty string.
set node [$tkw fragment X]
$node text set ""
} else {
set escaped [string map {< &lt; > &gt;} $text]
set node [$tkw fragment $escaped]
}
return $node
""" % (self.html, escape_Tcl(text))
)

def text_content(self, node, text=None):
return self.tk.eval("""
proc get_child_text {node} {
set t ""
foreach child [$node children] {
if {[$child tag] eq ""} {
append t [$child text -pre]
} else {
append t [get_child_text $child]
}
}
return $t
}
set node %s
if {$node eq ""} {error "DOMException NOT_SUPPORTED_ERR"}
set textnode %s
if {$textnode ne ""} {
foreach child [$node children] {
$child destroy
}
$node insert $textnode
} else {
return [get_child_text $node]
}
""" % (extract_nested(node), self.create_text_node(text) if text else '""')
)

def body(self):
return self.tk.eval(f"""set body [lindex [[{self.html} node] children] 1]""")

def screenshot(self, name="", full=False):
image = self.html.image("-full" if full else None)
data = image[next(iter(image))]
height = len(data)
width = len(data[0].split())
self.message_func(f"Screenshot taken: {name} {width}x{height}.")
return createRGBimage(data, name, width, height)


class HtmlLabel(HtmlFrame):
def __init__(self, master, text="", messages_enabled=False, **kw):
Expand Down
10 changes: 10 additions & 0 deletions tkinterweb/imageutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,13 @@ def blankimage(name):
image = Image.new("RGBA", (1, 1))
image = PhotoImage(image, name=name)
return image

def createRGBimage(data, name, w, h):
image = Image.new("RGB", (w, h))
for y, row in enumerate(data):
for x, hexc in enumerate(row.split()):
rgb = tuple(int(hexc[1:][i:i+2], 16) for i in (0, 2, 4))
image.putpixel((x, y), rgb)

if name: image.save(name)
return image
Loading