-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add module files to generate HTML, CSS, and JS for an email
- Loading branch information
Showing
4 changed files
with
336 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
|
||
from .generate import Alime | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import argparse | ||
import sys | ||
|
||
from .generate import Alime | ||
|
||
|
||
def main(): | ||
parser = argparse.ArgumentParser( | ||
description= | ||
'Generate animated anti-bot email obfuscation HTML+CSS+JS for your ' | ||
'website. https://github.com/cduck/alime', | ||
epilog= | ||
'The files alime-example.html, alime.css, and alime.js will be written ' | ||
'to the current directory.\n' | ||
'More options are available from the Python code.') | ||
parser.add_argument('email', type=str, help= | ||
'The email address to obfuscate') | ||
args = parser.parse_args() | ||
|
||
gen = Alime(email=args.email) | ||
gen.save() | ||
|
||
if __name__ == '__main__': | ||
# Fix executable name in help when run with `python3 -m alime` | ||
if len(sys.argv) >= 1 and sys.argv[0].endswith('__main__.py'): | ||
sys.argv[0] = 'python -m alime' | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
|
||
from collections import Counter, defaultdict | ||
import html | ||
|
||
import math | ||
|
||
from . import templates | ||
|
||
|
||
# Default configuration | ||
PLACEHOLDER = '~' # '~' is a placeholder for remaining characters | ||
TEXT0 = 'scrambled: <~>' # Text in HTML source | ||
TEXT1 = 'Email: <scrambled ~>' # Text shown in browser | ||
TEXT2 = 'Email: placeholder@example.com <~>' # Text shown when hover or click | ||
CHAR_WIDTH = 0.6 | ||
ROT_DEGREES = 60 | ||
|
||
|
||
def counter_to_str(counter): | ||
return ''.join(counter.elements()) | ||
|
||
class Alime: | ||
def __init__(self, email='placeholder@example.com', | ||
text0=TEXT0, text1=TEXT1, text2=TEXT2, placeholder=PLACEHOLDER, | ||
char_width=CHAR_WIDTH, rot_degrees=ROT_DEGREES, | ||
templates=templates): | ||
self.email = email | ||
self.text0 = text0 | ||
self.text1 = text1 | ||
self.text2 = text2 | ||
self.placeholder = placeholder | ||
self.char_width = char_width | ||
self.rot_degrees = ROT_DEGREES | ||
self.templates = templates | ||
|
||
self._generate() | ||
|
||
def _generate(self): | ||
text2 = self.text2.replace('placeholder@example.com', self.email) | ||
|
||
# Count characters | ||
c0 = Counter(self.text0) | ||
c1 = Counter(self.text1) | ||
c2 = Counter(text2) | ||
c0[self.placeholder] = c1[self.placeholder] = c2[self.placeholder] = 0 | ||
|
||
# Find the minimum set of characters to represent the three strings | ||
c_all = Counter() | ||
for char in sorted(set(c0) | set(c1) | set(c2)): | ||
c_all[char] = max(c0[char], c1[char], c2[char]) | ||
|
||
# Determine output character order | ||
c_temp = Counter(c_all) | ||
c_rest = c_all - c0 | ||
str_rest= ''.join(sorted(counter_to_str(c_rest))) | ||
char_order = self.text0.replace(self.placeholder, str_rest) | ||
char_order, len(char_order) | ||
|
||
# Calculate character positions for 1 and 2 | ||
def calc_positions(text): | ||
char_search_index = defaultdict(int) | ||
unused_index = text.find(self.placeholder) | ||
positions = [None] * len(char_order) | ||
for i, char in enumerate(char_order): | ||
idx = text.find(char, char_search_index[char]) | ||
if idx < 0: | ||
positions[i] = unused_index | ||
else: | ||
positions[i] = idx | ||
char_search_index[char] = idx + 1 | ||
return positions, unused_index | ||
positions1, unused_index1 = calc_positions(self.text1) | ||
positions2, unused_index2 = calc_positions(text2) | ||
|
||
# Calculate text width | ||
max_width = max(len(self.text0), len(self.text1), len(text2)) | ||
em_width = round(max_width * self.char_width, 6) | ||
|
||
# Generate HTML | ||
self.generated_chars = ''.join( | ||
self.templates.CHAR.format(char=html.escape(char)) | ||
for char in char_order | ||
) | ||
self.generated_html = self.templates.HTML.format( | ||
width=em_width, | ||
chars=self.generated_chars, | ||
) | ||
|
||
# Generate CSS | ||
def dist_to_yoff(dist): | ||
rot_rad = math.radians(self.rot_degrees) | ||
return dist / 2 / math.tan(rot_rad/2) | ||
self.css_rep=''.join( | ||
self.templates.CSS_REP.format( | ||
i=i+2, | ||
left=round((positions1[i]-i)*self.char_width, 6), | ||
origin=round((positions2[i]-positions1[i]+1)/2*self.char_width, | ||
6), | ||
yoff=round(0.5+dist_to_yoff( | ||
(positions2[i]-positions1[i])*self.char_width), | ||
6), | ||
other=' display: none;' if (positions1[i] == unused_index1 | ||
and positions2[i] == unused_index2) else '', | ||
) | ||
for i in range(len(char_order)) | ||
) | ||
self.generated_css = self.templates.CSS.format( | ||
pos_deg=self.rot_degrees, | ||
neg_deg=-self.rot_degrees, | ||
rep=self.css_rep, | ||
) | ||
|
||
# JavaScript | ||
self.generated_js = self.templates.JAVASCRIPT_STATIC | ||
|
||
# Generate example page | ||
self.generated_page = self.templates.EXAMPLE_PAGE.format( | ||
html=self.generated_html) | ||
|
||
def save(self, html_fname='alime-example.html', css_fname='alime.css', | ||
js_fname='alime.js'): | ||
self.save_page(html_fname) | ||
self.save_css(css_fname) | ||
self.save_js(js_fname) | ||
|
||
def save_page(self, fname='alime-example.html'): | ||
with open(fname, 'w') as f: | ||
f.write(self.generated_page) | ||
|
||
def save_css(self, fname='alime.css'): | ||
with open(fname, 'w') as f: | ||
f.write(self.generated_css) | ||
|
||
def save_js(self, fname='alime.js'): | ||
with open(fname, 'w') as f: | ||
f.write(self.generated_js) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
HTML = '''\ | ||
<a href="#javascript-disabled" class="alime" style="width: {width}em"> | ||
<span class="sr-only">(Click to send mail)</span> | ||
<!-- Automatically generated (https://github.com/cduck/alime) --> | ||
{chars} | ||
</a>\ | ||
''' | ||
CHAR = '<span><span>{char}</span></span>' | ||
|
||
CSS = '''\ | ||
/* This file was automatically generated (https://github.com/cduck/alime) */ | ||
.alime, .alimestatic {{ | ||
display: inline-block; | ||
white-space: nowrap; | ||
padding: 0; | ||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", \ | ||
"Courier New", monospace !important; | ||
}} | ||
.alime > *:nth-child(n+2) {{ | ||
display: inline-block; | ||
padding: 0; | ||
margin: 0; | ||
width: 0.6em; | ||
height: 1em; | ||
text-align: center; | ||
position: relative; | ||
left: 0em; | ||
transform-origin: center center; | ||
transition-duration: 1s; | ||
transition-property: transform; | ||
}} | ||
.alime > * > * {{ | ||
display: inline-block; | ||
padding: 0; | ||
margin: 0; | ||
width: 0.6em; | ||
height: 1em; | ||
text-align: center; | ||
transform-origin: center center; | ||
transition-duration: 1s; | ||
transition-property: transform; | ||
}} | ||
.alime:hover > *, | ||
.alime:active > *, | ||
.alime:focus > *, | ||
.alime.alimeanim > * {{ | ||
transform: rotate({pos_deg}deg); | ||
}} | ||
.alime.alimeclicked > * {{ | ||
transform: rotate({pos_deg}deg); | ||
transition-duration: 0; | ||
transition-property: none; | ||
}} | ||
.alime:hover > * > *, | ||
.alime:active > * > *, | ||
.alime:focus > * > *, | ||
.alime.alimeanim > * > * {{ | ||
transform: rotate({neg_deg}deg); | ||
}} | ||
.alime.alimeclicked > * > * {{ | ||
transform: rotate({neg_deg}deg); | ||
transition-duration: 0; | ||
transition-property: none; | ||
}} | ||
.alime > .sr-only, .alimestatic > .sr-only {{ | ||
position: absolute; | ||
width: 1px; | ||
height: 1px; | ||
padding: 0; | ||
margin: -1px; | ||
overflow: hidden; | ||
clip: rect(0, 0, 0, 0); | ||
white-space: nowrap; | ||
border: 0; | ||
}} | ||
{rep}\ | ||
''' | ||
CSS_REP = '''\ | ||
.alime > :nth-child({i}) {{\ | ||
left: {left}em; \ | ||
transform-origin: {origin}em {yoff}em;\ | ||
{other}\ | ||
}} | ||
''' | ||
|
||
JAVASCRIPT_STATIC = '''\ | ||
/* This file was automatically generated (https://github.com/cduck/alime) */ | ||
function alime_load() { | ||
let elems = document.getElementsByClassName("alime"); | ||
for (var i=0; i < elems.length; i++) { | ||
elems[i].href = "#click-to-unscramble"; | ||
elems[i].onclick = alime_click_handler(elems[i]); | ||
} | ||
} | ||
function alime_click_handler(elem) { | ||
function alime_clicked(e) { | ||
// Immediately complete animation | ||
elem.classList.add("alimeclicked"); | ||
// Unscramble | ||
let whole_str = Array.prototype.map.call(elem.children, | ||
function(val, i) { | ||
return [val.getBoundingClientRect().x, val]; | ||
}).sort(function(a, b) { | ||
return a[0]-b[0]; | ||
}).map(function(pair) { | ||
if (pair[1].classList.contains("sr-only")) { | ||
return ""; | ||
} | ||
pair[1].style.display = "none"; | ||
return pair[1].innerText || " "; | ||
}).join(""); | ||
// Add plain clickable link | ||
let link_text = ( | ||
whole_str.substring(1, whole_str.indexOf(":")) | ||
+ "to" | ||
+ whole_str.substring(whole_str.indexOf(":"), whole_str.indexOf("<")) | ||
).trim().replace(" ", ""); | ||
let print_text = whole_str.substring(whole_str.indexOf("<"), -1).trim(); | ||
elem.classList.remove("alime"); | ||
elem.classList.remove("alimeclicked"); | ||
elem.classList.add("alimestatic"); | ||
let static_elem = document.createElement("a"); | ||
static_elem.innerText = print_text; | ||
static_elem.href = link_text; | ||
elem.href = link_text; | ||
elem.appendChild(static_elem); | ||
elem.onclick = null; | ||
} | ||
return alime_clicked; | ||
} | ||
alime_load(); | ||
''' | ||
|
||
EXAMPLE_PAGE = '''\ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>Alime Example Web Page</title> | ||
<!-- Include the following line between <head></head> in your page --> | ||
<link rel="stylesheet" type="text/css" href="alime.css"> | ||
</head> | ||
<body> | ||
<h2>Alime Example Web Page</h2> | ||
<p> | ||
<!-- Include the following lines in your page where you want your address --> | ||
{html} | ||
<!-- End include --> | ||
</p> | ||
<!-- Include the following line at the very end of <body></body> in your page \ | ||
(optional) --> | ||
<script src='alime.js'></script> | ||
</body> | ||
</html> | ||
''' |