Skip to content

Instantly share code, notes, and snippets.

@wircho
Created February 4, 2020 05:07
Show Gist options
  • Save wircho/cf3f46c29efb6c989ad90ea56fbdef11 to your computer and use it in GitHub Desktop.
Save wircho/cf3f46c29efb6c989ad90ea56fbdef11 to your computer and use it in GitHub Desktop.
Selenium script for converting <imgm>formula</imgm> tags into LaTeX images on your Squarespace drafts. Use at your own risk.
from selenium.webdriver.common.action_chains import ActionChains
import selenium.webdriver
import inspect
import time
from selenium.webdriver.common.keys import Keys
import os
import tempfile
import subprocess
import shutil
import sys
import random
import getpass
###########################################################################
# From https://gist.github.com/florentbr/349b1ab024ca9f3de56e6bf8af2ac69e #
###########################################################################
# TODO: Integrate this code and imports (while keeping source link)
from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement
import os.path
# JavaScript: HTML5 File drop
# source : https://gist.github.com/florentbr/0eff8b785e85e93ecc3ce500169bd676
# param1 WebElement : Drop area element
# param2 Double : Optional - Drop offset x relative to the top/left corner of the drop area. Center if 0.
# param3 Double : Optional - Drop offset y relative to the top/left corner of the drop area. Center if 0.
# return WebElement : File input
JS_DROP_FILES = "var c=arguments,b=c[0],k=c[1];c=c[2];for(var d=b.ownerDocument||document,l=0;;){var e=b.getBoundingClientRect(),g=e.left+(k||e.width/2),h=e.top+(c||e.height/2),f=d.elementFromPoint(g,h);if(f&&b.contains(f))break;if(1<++l)throw b=Error('Element not interactable'),b.code=15,b;b.scrollIntoView({behavior:'instant',block:'center',inline:'center'})}var a=d.createElement('INPUT');a.setAttribute('type','file');a.setAttribute('multiple','');a.setAttribute('style','position:fixed;z-index:2147483647;left:0;top:0;');a.onchange=function(b){a.parentElement.removeChild(a);b.stopPropagation();var c={constructor:DataTransfer,effectAllowed:'all',dropEffect:'none',types:['Files'],files:a.files,setData:function(){},getData:function(){},clearData:function(){},setDragImage:function(){}};window.DataTransferItemList&&(c.items=Object.setPrototypeOf(Array.prototype.map.call(a.files,function(a){return{constructor:DataTransferItem,kind:'file',type:a.type,getAsFile:function(){return a},getAsString:function(b){var c=new FileReader;c.onload=function(a){b(a.target.result)};c.readAsText(a)}}}),{constructor:DataTransferItemList,add:function(){},clear:function(){},remove:function(){}}));['dragenter','dragover','drop'].forEach(function(a){var b=d.createEvent('DragEvent');b.initMouseEvent(a,!0,!0,d.defaultView,0,0,0,g,h,!1,!1,!1,!1,0,null);Object.setPrototypeOf(b,null);b.dataTransfer=c;Object.setPrototypeOf(b,DragEvent.prototype);f.dispatchEvent(b)})};d.documentElement.appendChild(a);a.getBoundingClientRect();return a;"
def drop_files(element, files, offsetX=0, offsetY=0):
driver = element.parent
isLocal = not driver._is_remote or '127.0.0.1' in driver.command_executor._url
paths = []
# ensure files are present, and upload to the remote server if session is remote
for file in (files if isinstance(files, list) else [files]) :
if not os.path.isfile(file) :
raise FileNotFoundError(file)
paths.append(file if isLocal else element._upload(file))
value = '\n'.join(paths)
elm_input = driver.execute_script(JS_DROP_FILES, element, offsetX, offsetY)
elm_input._execute('sendKeysToElement', {'value': [value], 'text': value})
WebElement.drop_files = drop_files
################################
# END #
################################
class LaTeXGenerator():
def __init__(self):
self.tex_file = "formula.tex"
with open(self.tex_file, "w") as file:
# # Standalone is faster but sometimes
# # crops off part of the formula
# tex = """
# \\ifdefined\\formula
# \\else
# \\def\\formula{E = m c^2}
# \\fi
# \\documentclass{standalone}
# \\usepackage{amsmath}
# \\usepackage{varwidth}
# \\begin{document}
# \\begin{varwidth}{\\linewidth}
# \\[ \\formula \\]
# \\end{varwidth}
# \\end{document}
# """
tex = """
\\documentclass{article}
\\usepackage[utf8x]{inputenc}
\\pagestyle{empty}
\\begin{document}
\\[ \\formula \\]
\\end{document}
"""
file.write(tex)
self.dvi_file = "formula.dvi"
self.ps_file = "formula.ps"
self.pdf_file = "formula.pdf"
def generate(self, formula, name = None):
png_file = os.path.join("images", (str(random.randint(0, 100)) if name is None else name) + ".png")
subprocess.run(["pdflatex", "\\def\\formula{" + formula + "}\\input{" + self.tex_file + "}"])
subprocess.run(["convert", "-density", "512", self.pdf_file, "-quality", "100", png_file])
subprocess.run(["convert", png_file, "-trim", "+repage", png_file])
return os.path.abspath(png_file)
# Retries a task multiple times until it does not raise exceptions.
# `task` is a function that may return a value.
def retry(every = 1, times = 15, condition = lambda x: True, task = lambda: None):
print(f"\nAttempting: {inspect.getsource(task)}")
for i in range(times):
print(f"Attempt #{i + 1}")
if i == times - 1: return task()
try:
result = task()
if not condition(result):
print(f"Attempt succeeded but condition failed.")
raise Exception("Condition unsatisfied!")
print(f"Attempt succeeded.")
return result
except: time.sleep(every)
by_xpath = selenium.webdriver.common.by.By.XPATH
enter = Keys.ENTER
nonempty = lambda x: len(x) > 0
# Gets a browser logged into Squarespace
def get_blog_browser(username, password):
browser = selenium.webdriver.Chrome()
browser.switch_to_window(browser.current_window_handle)
browser.get("https://squarespace.com")
retry(task = lambda: browser.find_element(by_xpath, '//a[@class="www-navigation__desktop__account-info__login-button"]').click())
retry(task = lambda: browser.execute_script('arguments[0].style.backgroundColor = "black"; arguments[0].style.color = "black";', browser.find_element(by_xpath, '//input[@name="email"]')))
retry(task = lambda: browser.execute_script('arguments[0].style.backgroundColor = "black"; arguments[0].style.color = "black";', browser.find_element(by_xpath, '//input[@name="password"]')))
retry(task = lambda: browser.find_element(by_xpath, '//input[@name="email"]').send_keys(username))
retry(task = lambda: browser.find_element(by_xpath, '//input[@name="password"]').send_keys(password))
retry(task = lambda: browser.find_element(by_xpath, '//button[@data-test="login-button"]').click())
retry(task = lambda: browser.find_element(by_xpath, '//p[text()="interoper.io"]/ancestor::div[@data-test="website-card"]').click())
return browser
# Automatically updates your drafts' LaTeX formulas, given two browsers logged into Squarespace
def update_latex_formulas(browser, css, latex, tag, img_class):
css.switch_to_window(css.current_window_handle)
retry(task = lambda: css.find_element(by_xpath, '//a[@data-test="menuItem-design"]').click())
retry(task = lambda: css.find_element(by_xpath, '//a[@data-test="menuItem-custom-css"]').click())
retry(task = lambda: css.find_element(by_xpath, '//input[@type="button" and contains(@class,"upload-asset")]').click())
browser.switch_to_window(browser.current_window_handle)
retry(task = lambda: browser.find_element(by_xpath, '//a[@data-test="menuItem-pages"]').click())
retry(task = lambda: browser.find_element(by_xpath, '//span[@class="icon icon-blog"]').click())
drafts = retry(condition = nonempty, task = lambda: browser.find_elements(by_xpath, '//div[@class="flag draft"]/ancestor::div[contains(@class,"sqs-item-view sqs-blog-item")]'))
print(f"Found {len(drafts)} drafts.")
for draft in drafts:
time.sleep(1)
title = draft.find_element(by_xpath, '//span[@class="title"]').text
print(f"Draft: {title}")
draft.click()
time.sleep(2)
retry(task = lambda: browser.find_element(by_xpath, '//button[@data-test="frameToolbarEdit"]').click())
time.sleep(2)
def settings_and_scroll():
try:
buttons = browser.find_elements(by_xpath, '//button[contains(@class,"IconButton-button") and contains(@class,"Toolbar-button")]')
buttons[-1].click()
except selenium.common.exceptions.ElementClickInterceptedException: pass
browser.execute_script('document.querySelector("div.sqs-layout-scroll").scrollTo(0, document.querySelector("div.sqs-layout-scroll").scrollHeight)')
retry(times = 3, condition = nonempty, task = settings_and_scroll)
empty_blocks = None
while empty_blocks is None or len(empty_blocks) > 0:
time.sleep(1)
print(f"Found empty blocks: {empty_blocks}")
empty_blocks = browser.find_elements(by_xpath, '//div[contains(@class,"code-block")]/div[contains(@class,"sqs-block-content") and not(.//p) and not(.//h1) and not(.//h2) and not(.//h3) and not(.//h4) and not(.//h5) and not(.//h6) and not(.//center) and not(.//img)]')
while True:
time.sleep(1)
try: block = retry(times = 5, task = lambda: browser.find_element(by_xpath, f'//{tag}/ancestor::div[contains(@class,"code-block")]/div[@class="sqs-editing-overlay"]'))
except: break
retry(task = lambda: browser.execute_script("arguments[0].scrollIntoView();", block))
ActionChains(browser).move_to_element_with_offset(block, 20, 20).double_click().perform()
while True:
time.sleep(1)
try: opening = retry(times = 5, task = lambda: browser.find_element(by_xpath, f'//span[@class="cm-tag" and text()="{tag}"]'))
except: break;
(formula, angle, closingAngle, angleText, text) = browser.execute_script("""
let tag = arguments[0];
let angle = tag.previousSibling;
if (!angle.innerText.endsWith("<")) { throw "Bad opening tag"; }
console.log("Found opening tag.");
let text = "<" + tag.innerText;
let nextElement = tag.nextSibling;
while (nextElement.classList === undefined || !nextElement.classList.contains("cm-tag") || nextElement.innerText !== tag.innerText) {
console.log("Adding element:")
console.log(nextElement);
text += (nextElement.innerText === undefined) ? nextElement.wholeText : nextElement.innerText;
nextElement = nextElement.nextSibling;
}
let closingTag = nextElement;
let endAngle = closingTag.previousSibling;
if (!endAngle.innerText.endsWith("</")) { throw "Bad closing tag"; }
let closingAngle = closingTag.nextSibling;
if (!closingAngle.innerText.startsWith(">")) { throw "Bad closing tag"; }
console.log("Found closing tag.")
text += closingTag.innerText + ">";
console.log("Full text: " + text);
let temp = document.createElement('template');
temp.innerHTML = text;
element = temp.content.firstElementChild;
formula = element.innerText;
return [formula, angle, closingAngle, angle.innerText, text];
""", opening)
print(formula)
print(angle)
print(closingAngle)
time.sleep(2)
try: browser.execute_script("""
let angle = arguments[0]
angle.scrollIntoView();
parent = angle.parentNode;
while (parent.scrollHeight <= parent.clientHeight) { parent = parent.parentNode; }
let angleCoords = angle.getBoundingClientRect();
let parentCoords = parent.getBoundingClientRect();
let diffY = angleCoords.y - parentCoords.y;
parent.scrollTo(0, diffY + 50);
""", angle)
except: pass
time.sleep(1)
action = ActionChains(browser)
action.move_to_element_with_offset(angle, 0, 2)
action.click()
for _ in range(len(angleText) - 1): action.send_keys(Keys.RIGHT)
action.key_down(Keys.SHIFT)
for _ in range(len(text)): action.send_keys(Keys.RIGHT)
action.key_up(Keys.SHIFT)
action.send_keys(f'<img class="{img_class}" formula="{formula}" srcset=" 4x"/>')
for _ in range(6): action.send_keys(Keys.LEFT)
action.perform()
css.switch_to_window(css.current_window_handle)
area = retry(task = lambda: css.find_element(by_xpath, '//button[contains(@class,"upload-indicator")]'));
path = latex.generate(formula.replace("\\(", "{").replace("\\)", "}"))
area.drop_files(path)
time.sleep(2)
uploaded = retry(condition = lambda element: os.path.basename(element.get_attribute("data-img-src")) == os.path.basename(path), task = lambda: css.find_element(by_xpath, '(//div[contains(@class,"main-image")])[last()]'));
url = uploaded.get_attribute("data-img-src")
browser.switch_to_window(browser.current_window_handle)
ActionChains(browser).send_keys(url).perform()
retry(task = lambda: browser.find_elements(by_xpath, '//input[@data-test="dialog-saveAndClose"]')[-1].click());
retry(task = lambda: browser.find_element(by_xpath, '//input[@data-test="dialog-saveAndClose"]').click());
done = retry(task = lambda: browser.find_element(by_xpath, '//button[@data-test="frameToolbarDone"]'))
ActionChains(browser).move_to_element(done).perform()
time.sleep(1)
try: browser.find_element(by_xpath, '//button[@data-test="frameToolbarSave"]').click()
except: browser.find_element(by_xpath, '//button[@data-test="frameToolbarCancel"]').click()
# Main code
if __name__ == "__main__":
def confirm_warning():
confirmation = None
while confirmation != "I understand": confirmation = input("Type 'I understand' to confirm that\nyou understand the warning above\nor hit CTRL+C to quit: ")
print("\n[WARNING] A Squarespace bug (as of 2020/February/3) means that anything that you typed inside curly brackets {} could BE LOST if you hit the Options (gear) icon. This tool DOES THAT EXACTLY. To prevent losing any data, you MUST type your drafts' LaTeX equations using ESCAPED PARENTHESES \\(\\) instead. For example, you MUST type <imgm>\\frac\\(1\\)\\(2\\)</imgm> instead of <imgm>\\frac{1}{2}</imgm>.\n")
confirm_warning()
print("""\n[WARNING] THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n""")
confirm_warning()
print("\nPlease enter your Squarespace credentials.\n")
latex = LaTeXGenerator()
username = getpass.getpass("Squarespace username: ")
password = getpass.getpass("Squarespace password: ")
def input_or_default(q, d):
r = input(f"{q} ({d}): ")
if len(r) == 0: return d
return r
print("\nBy default, an HTML or Markdown tag like <imgm>x^2+y^2=z^2</imgm>, becomes an image tag like <img class=\"m\" formula=\"x^2 + y^2 = z^2\" srcset=\"https://some/image/url.png 4x\"/>. You can change the tag 'imgm' and the class 'm', or simply hit Enter to keep these defaults.\n")
tag = input_or_default("Formula tag", "imgm")
img_class = input_or_default("Generated img class", "m")
browser = get_blog_browser(username, password)
browser.maximize_window()
css = get_blog_browser(username, password)
update_latex_formulas(browser, css, latex, tag, img_class)
input("Press enter to close browsers.")
browser.close()
css.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment