diff --git a/assets/message-chatgpt.png b/assets/message-chatgpt.png
new file mode 100644
index 0000000..419b13d
Binary files /dev/null and b/assets/message-chatgpt.png differ
diff --git a/assets/send-button.png b/assets/send-button.png
new file mode 100644
index 0000000..2cbed2e
Binary files /dev/null and b/assets/send-button.png differ
diff --git a/assets/stop-button.png b/assets/stop-button.png
new file mode 100644
index 0000000..ee99a7e
Binary files /dev/null and b/assets/stop-button.png differ
diff --git a/docs/_data/sidebars/home_sidebar.yml b/docs/_data/sidebars/home_sidebar.yml
index d009cdb..08a7bfa 100644
--- a/docs/_data/sidebars/home_sidebar.yml
+++ b/docs/_data/sidebars/home_sidebar.yml
@@ -13,7 +13,10 @@ entries:
title: iPyTest
url: core.html
- output: web,pdf
- title: Mock OpenAI API
+ title: Browser Automation
+ url: automation.html
+ - output: web,pdf
+ title: browser
url: browser.html
- output: web,pdf
title: LLM
diff --git a/docs/automation.html b/docs/automation.html
new file mode 100644
index 0000000..1809bca
--- /dev/null
+++ b/docs/automation.html
@@ -0,0 +1,810 @@
+---
+
+title: Browser Automation
+
+
+keywords: fastai
+sidebar: home_sidebar
+
+summary: "Using Selenium for Browser Automation"
+description: "Using Selenium for Browser Automation"
+nb_path: "nbs\2_automation.ipynb"
+---
+
+
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+
+
+
pip install undetected_chromedriver webdriver_manager
+
+
+
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+init
(*arguments
)
+
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+
+
+
+
+
Using Text-based Search for Browser Automation
+
+
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+find_elements
(prompt
, exactly
=True
)
+
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+find_elements
(prompt
, exactly
=True
)
+
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+find_element
(prompt
, closest_prompt
=None
)
+
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+click
(prompt
, closest_prompt
=None
)
+
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+input
(prompt
, text
, closest_prompt
=None
)
+
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+
+
+
+
Using Selenium for Static Inspection of Page Appearance
+
+
+
+ {% raw %}
+
+
+
+
+
+
+
+
+image_hash
(image_path
)
+
+
Calculate the hash value of the image
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+wait
(timeout
=1.7976931348623157e+308
, stability_duration
=1.0
, check_interval
=0.5
)
+
+
Wait until the screenshot does not change.
+
Args:
+timeout: how long to wait for stabilization (seconds)
+stability_duration: duration for stabilization (seconds)
+check_interval: check interval (seconds)
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+
+
+
+
Using Airtest for Browser Automation
+
+
+
+
+ {% raw %}
+
+
+
+
+
+
+
+
+find_position
(image
, timeout
=1.0
, threshold
=0.9
, interval
=0.5
, intervalfunc
=None
)
+
+
Search for image template in the screen until timeout
+
Args:
+path: image file path to be found in screenshot
+timeout: time interval how long to look for the image template
+threshold: default is None
+interval: sleep interval before next attempt to find the image template
+intervalfunc: function that is executed after unsuccessful attempt to find the image template
+
Raises:
+TargetNotFoundError: when image template is not found in screenshot
+
Returns:
+TargetNotFoundError if image template not found, otherwise returns the position where the image template has
+been found in screenshot
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+get_mouse_position
()
+
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+move_to_center
()
+
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+move_and_click
(x
, y
)
+
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+touch
(image
, text
=None
)
+
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+
+
+
+
Using Selenium for Printing to PNG and PDF
+
+
+
+
+ {% raw %}
+
+
+
+
+
+
+
+
+convert_md_with_ruby_to_html
(md_file_path
)
+
+
Convert Markdown files containing tags into HTML file
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+convert_html_with_ruby_to_png
(html_file_path
)
+
+
Use Selenium to print the HTML file as PNG
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+convert_md_with_ruby_to_png
(md_file_path
)
+
+
Convert the Markdown file containing tags into PNG
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+dialog_for_printing
(timeout
=inf
)
+
+
Dialog for printing to PDF
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+convert_html_with_ruby_to_pdf
(html_file_path
)
+
+
Use Selenium to print the HTML file as PDF
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+
+
+
+
+
+convert_md_with_ruby_to_pdf
(md_file_path
)
+
+
Convert the Markdown file containing tags into PDF
+
+
+
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+
+ {% endraw %}
+
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+ {% raw %}
+
+
+ {% endraw %}
+
+
diff --git a/ipymock/__init__.py b/ipymock/__init__.py
index ae2b423..be0bee7 100644
--- a/ipymock/__init__.py
+++ b/ipymock/__init__.py
@@ -4,7 +4,6 @@
# Cell
import py
-import sys
import traceback
import _pytest.config
diff --git a/ipymock/_nbdev.py b/ipymock/_nbdev.py
index 8e7ceac..3818c48 100644
--- a/ipymock/_nbdev.py
+++ b/ipymock/_nbdev.py
@@ -5,6 +5,65 @@
index = {"get_test_funcs": "1_core.ipynb",
"print_result": "1_core.ipynb",
"do": "1_core.ipynb",
+ "driver": "2_automation.ipynb",
+ "device_pixel_ratio": "2_automation.ipynb",
+ "init": "2_browser.ipynb",
+ "quit": "2_automation.ipynb",
+ "logger": "2_automation.ipynb",
+ "handler": "2_automation.ipynb",
+ "ok": "2_automation.ipynb",
+ "last": "2_automation.ipynb",
+ "new": "2_automation.ipynb",
+ "close": "2_automation.ipynb",
+ "find_elements": "2_automation.ipynb",
+ "find_element": "2_automation.ipynb",
+ "click": "2_automation.ipynb",
+ "input": "2_automation.ipynb",
+ "get_html_hash": "2_automation.ipynb",
+ "wait": "2_automation.ipynb",
+ "screen_hash": "2_automation.ipynb",
+ "watch": "2_automation.ipynb",
+ "try_log_screen": "2_automation.ipynb",
+ "find_position": "2_automation.ipynb",
+ "inject": "2_automation.ipynb",
+ "get_mouse_position": "2_automation.ipynb",
+ "move_to_center": "2_automation.ipynb",
+ "move_and_click": "2_automation.ipynb",
+ "exists": "2_automation.ipynb",
+ "touch": "2_automation.ipynb",
+ "fill": "2_automation.ipynb",
+ "mouseMarkRD.id": "2_automation.ipynb",
+ "mouseMarkRD.style.position": "2_automation.ipynb",
+ "mouseMarkRD.style.width": "2_automation.ipynb",
+ "mouseMarkRD.style.height": "2_automation.ipynb",
+ "mouseMarkRD.style.backgroundColor": "2_automation.ipynb",
+ "mouseMarkRD.style.zIndex": "2_automation.ipynb",
+ "mouseMarkLU.id": "2_automation.ipynb",
+ "mouseMarkLU.style.position": "2_automation.ipynb",
+ "mouseMarkLU.style.width": "2_automation.ipynb",
+ "mouseMarkLU.style.height": "2_automation.ipynb",
+ "mouseMarkLU.style.backgroundColor": "2_automation.ipynb",
+ "mouseMarkLU.style.zIndex": "2_automation.ipynb",
+ "mouseMarkRU.id": "2_automation.ipynb",
+ "mouseMarkRU.style.position": "2_automation.ipynb",
+ "mouseMarkRU.style.width": "2_automation.ipynb",
+ "mouseMarkRU.style.height": "2_automation.ipynb",
+ "mouseMarkRU.style.backgroundColor": "2_automation.ipynb",
+ "mouseMarkRU.style.zIndex": "2_automation.ipynb",
+ "mouseMarkLD.id": "2_automation.ipynb",
+ "mouseMarkLD.style.position": "2_automation.ipynb",
+ "mouseMarkLD.style.width": "2_automation.ipynb",
+ "mouseMarkLD.style.height": "2_automation.ipynb",
+ "mouseMarkLD.style.backgroundColor": "2_automation.ipynb",
+ "mouseMarkLD.style.zIndex": "2_automation.ipynb",
+ "convert_md_with_ruby_to_html": "2_automation.ipynb",
+ "convert_md_content_to_html": "2_automation.ipynb",
+ "write_html_file": "2_automation.ipynb",
+ "convert_html_with_ruby_to_png": "2_automation.ipynb",
+ "convert_md_with_ruby_to_png": "2_automation.ipynb",
+ "dialog_for_printing": "2_automation.ipynb",
+ "convert_html_with_ruby_to_pdf": "2_automation.ipynb",
+ "convert_md_with_ruby_to_pdf": "2_automation.ipynb",
"Common": "2_browser.ipynb",
"common": "4_agi.ipynb",
"get_conversations": "2_browser.ipynb",
@@ -18,7 +77,6 @@
"clear_conversations": "2_browser.ipynb",
"ChatGPTConverter": "2_browser.ipynb",
"markdownize": "2_browser.ipynb",
- "init": "2_browser.ipynb",
"login": "2_browser.ipynb",
"open_chat": "2_browser.ipynb",
"remove_portal": "2_browser.ipynb",
@@ -74,6 +132,7 @@
"DuckDuckGoSearchAPIWrapper": "4_tools_duckduckgo.ipynb"}
modules = ["__init__.py",
+ "automation.py",
"browser.py",
"llm.py",
"agi.py",
diff --git a/ipymock/automation.py b/ipymock/automation.py
new file mode 100644
index 0000000..868028e
--- /dev/null
+++ b/ipymock/automation.py
@@ -0,0 +1,520 @@
+# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/2_automation.ipynb (unless otherwise specified).
+
+__all__ = ['driver', 'device_pixel_ratio', 'init', 'quit', 'ok', 'last', 'new', 'close', 'find_elements',
+ 'find_elements', 'find_element', 'click', 'input', 'get_html_hash', 'wait', 'screen_hash', 'watch',
+ 'try_log_screen', 'find_position', 'inject', 'get_mouse_position', 'move_to_center', 'move_and_click',
+ 'exists', 'touch', 'fill', 'convert_md_with_ruby_to_html', 'convert_md_content_to_html', 'write_html_file',
+ 'convert_html_with_ruby_to_png', 'convert_md_with_ruby_to_png', 'dialog_for_printing',
+ 'convert_html_with_ruby_to_pdf', 'convert_md_with_ruby_to_pdf']
+
+# Internal Cell
+import os
+import undetected_chromedriver
+from webdriver_manager.chrome import ChromeDriverManager
+
+# Cell
+driver = None
+device_pixel_ratio = 1
+
+# Cell
+def init(*arguments):
+ chrome_options = undetected_chromedriver.ChromeOptions()
+ for argument in arguments:
+ if isinstance(argument, str):
+ chrome_options.add_argument(argument)
+ global driver
+ driver = undetected_chromedriver.Chrome(
+ options = chrome_options,
+ driver_executable_path = os.path.join(
+ os.path.dirname(ChromeDriverManager().install()), 'chromedriver.exe' if os.name == 'nt' else 'chromedriver'
+ )
+ )
+ global device_pixel_ratio
+ device_pixel_ratio = driver.execute_script('return window.devicePixelRatio;')
+
+# Cell
+def quit():
+ global driver
+ global device_pixel_ratio
+ driver.quit()
+ driver = None
+ device_pixel_ratio = 1
+
+# Internal Cell
+import logging
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
+handler = logging.StreamHandler()
+handler.setFormatter(logging.Formatter(
+ fmt = '[%(asctime)s][%(levelname)s]<%(name)s> %(message)s',
+ datefmt = '%H:%M:%S'
+))
+logger.addHandler(handler)
+
+# Internal Cell
+from selenium.common.exceptions import WebDriverException
+
+# Cell
+def ok():
+ if driver is None:
+ return False
+ try:
+ if driver.window_handles == []:
+ return False
+ except WebDriverException:
+ return False
+ return True
+
+# Cell
+def last():
+ if not ok():
+ return
+ driver.switch_to.window(driver.window_handles[-1])
+
+# Cell
+def new(url):
+ last()
+ if not ok():
+ init()
+ if 'data:,' not in driver.current_url and 'chrome://new-tab-page/' not in driver.current_url:
+ driver.switch_to.new_window('tab')
+ driver.get(url)
+
+# Cell
+def close():
+ global driver
+ global device_pixel_ratio
+ driver.close()
+ if not ok():
+ driver = None
+ device_pixel_ratio = 1
+
+# Internal Cell
+from selenium.webdriver.common.by import By
+
+# Internal Cell
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions
+
+# Cell
+def find_elements(prompt, exactly = True):
+ return [elem for elem in WebDriverWait(driver, 10).until(
+ expected_conditions.presence_of_all_elements_located((By.XPATH, f'//*[not(contains(text(), "\n")) and contains(., "{prompt}")]'))
+ ) if elem.is_displayed() and (elem.text == prompt or not exactly)]
+
+# Internal Cell
+from selenium.common.exceptions import NoSuchElementException
+
+# Cell
+def find_elements(prompt, exactly = True):
+ for scope in range(1, 5):
+ elements = driver.find_elements(By.XPATH, f'//*[.{"/*" * (scope - 1)} and not(.{"/*" * scope}) and contains(., "{prompt}")]')
+ elements = [elem for elem in elements if elem.is_displayed() and (elem.text == prompt or not exactly)]
+ logger.info(f'Search for {prompt} in scope {scope}: found {len(elements)} element(s)')
+ if elements:
+ return elements
+ raise NoSuchElementException
+
+# Cell
+def find_element(prompt, closest_prompt = None):
+ if closest_prompt is None:
+ return find_elements(prompt)[-1]
+ if isinstance(closest_prompt, str):
+ closest_prompt = find_elements(closest_prompt, False)[-1]
+ closest_location = closest_prompt.location
+ return min(
+ find_elements(prompt),
+ key = lambda elem: (elem.location['x'] - closest_location['x']) ** 2 + (elem.location['y'] - closest_location['y']) ** 2
+ )
+
+# Internal Cell
+from selenium.webdriver.common.action_chains import ActionChains
+
+# Cell
+def click(prompt, closest_prompt = None):
+ if isinstance(prompt, str):
+ prompt = find_element(prompt, closest_prompt)
+ ActionChains(driver).move_to_element(prompt).click().perform()
+ return prompt
+
+# Cell
+def input(prompt, text, closest_prompt = None):
+ prompt = click(prompt, closest_prompt)
+ ActionChains(driver).send_keys(text).perform()
+ return prompt
+
+# Internal Cell
+import hashlib, time
+
+# Cell
+def get_html_hash(xpath = '//body'):
+ """Get the hash of the element's outerHTML."""
+ # driver is the Selenium WebDriver global instance.
+ elements = driver.find_elements(By.XPATH, xpath)
+ html = elements[-1].get_attribute('outerHTML') if elements else ''
+ return hashlib.md5(html.encode('utf-8')).hexdigest(), time.time()
+
+# Cell
+def wait(timeout=float('inf'), stability_duration=1.0, check_interval=0.5, xpath='//body'):
+ """
+ Wait until the HTML of the specified element does not change.
+
+ Args:
+ timeout: Maximum wait time for stabilization (seconds).
+ stability_duration: Duration for stabilization (seconds).
+ check_interval: Interval to check for changes (seconds).
+ xpath: XPATH of the element to monitor for HTML changes.
+ """
+ # Get the initial hash value
+ previous_hash, previous_time = get_html_hash(xpath)
+
+ # Wait until the HTML does not change
+ start_time = time.time()
+ while True:
+ time.sleep(check_interval)
+
+ # Get the current hash value
+ current_hash, current_time = get_html_hash(xpath)
+
+ # Check if the hash value has stabilized
+ if current_hash == previous_hash:
+ if current_time - previous_time >= stability_duration:
+ logger.info('HTML content has stabilized.')
+ break
+ else:
+ # Update hash and time if the content changes
+ previous_hash, previous_time = current_hash, current_time
+
+ # Check for timeout
+ if current_time - start_time >= timeout:
+ logger.info('Wait for HTML stabilization timed out.')
+ break
+
+# Cell
+def screen_hash():
+ """Calculate the hash value of the screenshot."""
+ return hashlib.md5(driver.get_screenshot_as_base64().encode('utf-8')).hexdigest(), time.time()
+
+# Internal Cell
+from sys import float_info
+
+# Cell
+def watch(timeout = float_info.max, stability_duration = 1.0, check_interval = 0.5):
+ """
+ Wait until the screenshot does not change.
+
+ Args:
+ timeout: how long to wait for stabilization (seconds)
+ stability_duration: duration for stabilization (seconds)
+ check_interval: check interval (seconds)
+ """
+ # Get the initial hash value
+ previous_hash, previous_time = screen_hash()
+
+ # Wait until the image does not change
+ start_time = previous_time
+ while True:
+ time.sleep(check_interval)
+
+ # Take another screenshot and calculate the new hash value
+ current_hash, current_time = screen_hash()
+
+ # Check if the hash value has changed
+ if current_hash == previous_hash:
+ if current_time - previous_time >= stability_duration:
+ logger.info('Screenshot has stabilized.')
+ break
+ else:
+ if current_time - start_time >= timeout:
+ logger.warning('Wait for screenshot stabilization timeout.')
+ break
+ previous_hash, previous_time = current_hash, current_time
+
+# Cell
+def try_log_screen(xpath = None):
+ screenshot_path = 'screen.png'
+ if isinstance(xpath, str):
+ driver.find_element(By.XPATH, xpath).screenshot(screenshot_path)
+ return
+ driver.save_screenshot(screenshot_path)
+
+# Internal Cell
+from selenium.webdriver.common.by import By
+from selenium.webdriver.common.action_chains import ActionChains
+
+import os, time
+from airtest.aircv import get_resolution, imread
+from airtest.core.api import Template
+from airtest.core.error import TargetNotFoundError
+
+# Cell
+def find_position(image, timeout=1.0, threshold=0.9, interval=0.5, intervalfunc=None):
+ """
+ Search for image template in the screen until timeout
+
+ Args:
+ image: image file path to be found in screenshot
+ timeout: time interval how long to look for the image template
+ threshold: default is None
+ interval: sleep interval before next attempt to find the image template
+ intervalfunc: function that is executed after unsuccessful attempt to find the image template
+
+ Raises:
+ TargetNotFoundError: when image template is not found in screenshot
+
+ Returns:
+ TargetNotFoundError if image template not found, otherwise returns the position where the image template has
+ been found in screenshot
+ """
+ logger.info(f'Try to find {image}')
+ query = Template(image, rgb = True)
+ start_time = time.time()
+ while True:
+ LOG_DIR = '.'
+ file_path = os.path.join(LOG_DIR, 'screen.png')
+ driver.save_screenshot(file_path)
+ screen = imread(file_path)
+ query.resolution = get_resolution(screen)
+ # query.resolution = (1920, 1080)
+ if screen is None:
+ logger.warning('Screen is None: may be locked')
+ else:
+ if threshold:
+ query.threshold = threshold
+ match_pos = query.match_in(screen)
+ if match_pos:
+ # logger.info(f'match_pos == {match_pos}')
+ # try_log_screen(screen)
+ return match_pos[0] / device_pixel_ratio, match_pos[1] / device_pixel_ratio
+
+ if intervalfunc is not None:
+ intervalfunc()
+
+ # Raise an exception if timeout occurs, otherwise proceed to the next loop.
+ if (time.time() - start_time) > timeout:
+ # try_log_screen(screen)
+ raise TargetNotFoundError(f'Picture {query} not found in screen')
+ else:
+ # ActionChains(driver).move_by_offset(0, 0).perform()
+ time.sleep(interval)
+
+def inject():
+ # Inject JavaScript code to get the mouse coordinates.
+ driver.execute_script("""
+var mouseMarkRD = document.createElement('div');
+mouseMarkRD.id = 'mouse_mark_rd';
+mouseMarkRD.style.position = 'absolute';
+mouseMarkRD.style.width = '8px';
+mouseMarkRD.style.height = '8px';
+mouseMarkRD.style.backgroundColor = 'red';
+mouseMarkRD.style.zIndex = '9999';
+document.body.appendChild(mouseMarkRD);
+
+var mouseMarkLU = document.createElement('div');
+mouseMarkLU.id = 'mouse_mark_lu';
+mouseMarkLU.style.position = 'absolute';
+mouseMarkLU.style.width = '8px';
+mouseMarkLU.style.height = '8px';
+mouseMarkLU.style.backgroundColor = 'red';
+mouseMarkLU.style.zIndex = '9999';
+document.body.appendChild(mouseMarkLU);
+
+var mouseMarkRU = document.createElement('div');
+mouseMarkRU.id = 'mouse_mark_ru';
+mouseMarkRU.style.position = 'absolute';
+mouseMarkRU.style.width = '8px';
+mouseMarkRU.style.height = '8px';
+mouseMarkRU.style.backgroundColor = 'red';
+mouseMarkRU.style.zIndex = '9999';
+document.body.appendChild(mouseMarkRU);
+
+var mouseMarkLD = document.createElement('div');
+mouseMarkLD.id = 'mouse_mark_ld';
+mouseMarkLD.style.position = 'absolute';
+mouseMarkLD.style.width = '8px';
+mouseMarkLD.style.height = '8px';
+mouseMarkLD.style.backgroundColor = 'red';
+mouseMarkLD.style.zIndex = '9999';
+document.body.appendChild(mouseMarkLD);
+
+document.addEventListener('mousemove', function(e) {
+ mouseMarkRD.style.left = e.pageX + 2 + 'px';
+ mouseMarkRD.style.top = e.pageY + 2 + 'px';
+
+ mouseMarkLU.style.left = e.pageX - 10 + 'px';
+ mouseMarkLU.style.top = e.pageY - 10 + 'px';
+
+ mouseMarkRU.style.left = e.pageX + 2 + 'px';
+ mouseMarkRU.style.top = e.pageY - 10 + 'px';
+
+ mouseMarkLD.style.left = e.pageX - 10 + 'px';
+ mouseMarkLD.style.top = e.pageY + 2 + 'px';
+
+ window.mouseX = e.clientX;
+ window.mouseY = e.clientY;
+});
+""")
+
+def get_mouse_position():
+ return driver.execute_script('return window.mouseX'), driver.execute_script('return window.mouseY')
+
+def move_to_center():
+ body = driver.find_element(By.TAG_NAME, 'body')
+ ActionChains(driver).move_to_element(body).perform()
+ return body.rect['x'] + body.rect['width'] >> 1, body.rect['y'] + body.rect['height'] >> 1
+
+def move_and_click(x, y):
+ # Retrieve the mouse coordinates.
+ center_x, center_y = move_to_center()
+ logger.debug(f'Get center position: {center_x}, {center_y}')
+ # Move to the specified coordinates (x, y) and click.
+ ActionChains(driver).move_by_offset(x - center_x, y - center_y).click().perform()
+
+def exists(image):
+ try:
+ find_position(image)
+ return True
+ except:
+ return False
+
+def touch(image, text = None):
+ try:
+ x, y = find_position(image)
+ move_and_click(x, y)
+ if isinstance(text, str):
+ fill(text)
+ except:
+ pass
+
+def fill(text):
+ ActionChains(driver).send_keys(text).perform()
+
+# Internal Cell
+import markdown
+import os, re
+
+# Cell
+def convert_md_with_ruby_to_html(md_file_path):
+ ''' Convert Markdown file containing