Created
December 10, 2016 00:30
-
-
Save sgeto/0079bf86bf49db7cdfb82acc7679ccfb to your computer and use it in GitHub Desktop.
opendir_image
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
javascript: | |
/* | |
This javascript bookmarklet takes anchors, images, and media elements from | |
the page, and displays them in a nice gallery. Designed for use on open | |
directory listings, but works in many places. | |
*/ | |
var seen_urls = new Set(); | |
var image_height = 200; | |
var video_height = 300; | |
var audio_width = 1000; | |
var IMAGE_TYPES = ["\\.jpg", "\\.jpeg", "\\.jpg", "\\.bmp", "\\.tiff", "\\.tif", "\\.bmp", "\\.gif", "\\.png"].join("|"); | |
var AUDIO_TYPES = ["\\.aac", "\\.mp3", "\\.m4a", "\\.ogg", "\\.wav"].join("|"); | |
var VIDEO_TYPES = ["\\.mp4", "\\.m4v", "\\.webm", "\\.ogv"].join("|"); | |
IMAGE_TYPES = new RegExp(IMAGE_TYPES, "i"); | |
AUDIO_TYPES = new RegExp(AUDIO_TYPES, "i"); | |
VIDEO_TYPES = new RegExp(VIDEO_TYPES, "i"); | |
var has_started = false; | |
var CSS = "" | |
+ "body { background-color: #fff; }" | |
+ "audio, video { display: block; }" | |
+ "audio { width: $audio_width$px; }" | |
+ "video { height: $video_height$px; }" | |
+ "img { display: block; height: $image_height$px; max-width: 100% }" | |
+ ".control_panel { position: relative; background-color: #aaa; min-height: 10px; width: 100%; }" | |
+ ".workspace { background-color: #ddd; min-height: 10px; float: left; }" | |
+ ".arealabel { position:absolute; right: 0; bottom: 0; opacity: 0.8; background-color: #000; color: #fff; }" | |
+ ".delete_button { color: #d00; font-family: Arial; font-size: 11px; left: 0; position: absolute; top: 0; width: 25px; }" | |
+ ".ingest { position:absolute; right: 5px; top: 5px; height: 100%; width: 30% }" | |
+ ".ingestbox { position:relative; height: 75%; width:100%; box-sizing: border-box; }" | |
+ ".urldumpbox { overflow-y: scroll; height: 300px; width: 90% }" | |
+ ".load_button { position: absolute; top: 10%; width: 100%; height: 80%; word-wrap: break-word; }" | |
+ ".odi_anchor { display: block; }" | |
+ ".odi_image_div, .odi_media_div { display: inline-block; margin: 5px; float: left; position: relative; background-color: #aaa; }" | |
+ ".odi_image_div { min-width: $image_height$px; }" | |
; | |
function apply_css() | |
{ | |
console.log("applying CSS"); | |
var css = document.createElement("style"); | |
css.innerHTML = format_css(); | |
document.head.appendChild(css); | |
} | |
function array_extend(a, b) | |
{ | |
/* Append all elements of b onto a */ | |
for (var i = 0; i < b.length; i += 1) | |
{ | |
a.push(b[i]); | |
} | |
} | |
function array_remove(a, item) | |
{ | |
/* Thanks peter olson http://stackoverflow.com/a/5767335 */ | |
for(var i = a.length - 1; i >= 0; i -= 1) | |
{ | |
if(a[i].id === item.id) | |
{ | |
a.splice(i, 1); | |
} | |
} | |
} | |
function clear_page() | |
{ | |
/* Remove EVERYTHING */ | |
console.log("clearing page"); | |
document.removeChild(document.documentElement); | |
var html = document.createElement("html"); | |
document.appendChild(html); | |
var head = document.createElement("head"); | |
html.appendChild(head); | |
var body = document.createElement("body"); | |
html.appendChild(body); | |
document.documentElement = html; | |
return true; | |
} | |
function clear_workspace() | |
{ | |
console.log("clearing workspace"); | |
workspace = document.getElementById("WORKSPACE"); | |
while (workspace.children.length > 0) | |
{ | |
workspace.removeChild(workspace.children[0]); | |
} | |
return true; | |
} | |
function create_command_box(boxname, operation) | |
{ | |
var box = document.createElement("input"); | |
box.type = "text"; | |
box.id = boxname; | |
box.onkeydown=function() | |
{ | |
if (event.keyCode == 13) | |
{ | |
operation(this.value); | |
} | |
}; | |
return box; | |
} | |
function create_command_button(label, operation) | |
{ | |
var button = document.createElement("button"); | |
button.innerHTML = label; | |
button.onclick = operation; | |
return button; | |
} | |
function create_command_box_button(boxname, label, operation) | |
{ | |
var box = create_command_box(boxname, operation); | |
var button = create_command_button(label, function(){operation(box.value)}); | |
var div = document.createElement("div"); | |
div.appendChild(box); | |
div.appendChild(button); | |
div.box = box; | |
div.button = button; | |
return div; | |
} | |
function create_odi_div(url) | |
{ | |
var div = null; | |
var paramless_url = url.split("?")[0]; | |
var basename = get_basename(url); | |
if (paramless_url.match(IMAGE_TYPES)) | |
{ | |
console.log("Creating image div for " + paramless_url); | |
var div = document.createElement("div"); | |
div.id = generate_id(32); | |
div.className = "odi_image_div"; | |
div.odi_type = "image"; | |
var a = document.createElement("a"); | |
a.className = "odi_anchor"; | |
a.odi_div = div; | |
a.href = url; | |
a.target = "_blank"; | |
var img = document.createElement("img"); | |
img.odi_div = div; | |
img.anchor = a; | |
img.border = 0; | |
img.lazy_src = url; | |
img.src = ""; | |
var arealabel = document.createElement("span"); | |
arealabel.className = "arealabel"; | |
arealabel.odi_div = div; | |
arealabel.innerHTML = "0x0"; | |
img.arealabel = arealabel; | |
var load_button = document.createElement("button"); | |
load_button.className = "load_button"; | |
load_button.odi_div = div; | |
load_button.innerHTML = basename; | |
load_button.onclick = function() | |
{ | |
this.parentElement.removeChild(this); | |
lazy_load_one(this.odi_div); | |
}; | |
div.image = img; | |
div.anchor = a; | |
a.appendChild(img); | |
a.appendChild(arealabel); | |
div.appendChild(a); | |
div.appendChild(load_button); | |
} | |
else | |
{ | |
if (paramless_url.match(AUDIO_TYPES)) | |
{ | |
var mediatype = "audio"; | |
} | |
else if (paramless_url.match(VIDEO_TYPES)) | |
{ | |
var mediatype = "video"; | |
} | |
else | |
{ | |
return null; | |
} | |
console.log("Creating " + mediatype + " div for " + paramless_url); | |
var div = document.createElement("div"); | |
div.id = generate_id(32); | |
div.className = "odi_media_div"; | |
div.odi_type = "media"; | |
div.mediatype = mediatype; | |
var center = document.createElement("center"); | |
center.odi_div = div; | |
var a = document.createElement("a"); | |
a.odi_div = div; | |
a.innerHTML = get_basename(url); | |
a.target = "_blank"; | |
a.style.display = "block"; | |
a.href = url; | |
var media = document.createElement(mediatype); | |
media.odi_div = div; | |
media.controls = true; | |
media.preload = "none"; | |
sources = get_alternate_sources(url); | |
for (var sourceindex = 0; sourceindex < sources.length; sourceindex += 1) | |
{ | |
source = document.createElement("source"); | |
source.src = sources[sourceindex]; | |
source.odi_div = div; | |
media.appendChild(source); | |
} | |
div.media = media; | |
div.anchor = a; | |
center.appendChild(a); | |
div.appendChild(center); | |
div.appendChild(media); | |
} | |
if (div == null) | |
{ | |
return null; | |
} | |
div.url = url; | |
div.basename = basename; | |
button = document.createElement("button"); | |
button.className = "delete_button"; | |
button.odi_div = div; | |
button.innerHTML = "X"; | |
button.onclick = function() | |
{ | |
delete_odi_div(this); | |
}; | |
div.appendChild(button); | |
return div; | |
} | |
function create_odi_divs(urls) | |
{ | |
console.log("Creating odi divs"); | |
image_divs = []; | |
media_divs = []; | |
odi_divs = []; | |
for (var index = 0; index < urls.length; index += 1) | |
{ | |
url = urls[index]; | |
var paramless_url = url.split("?")[0]; | |
if (!url) | |
{ | |
continue; | |
} | |
var odi_div = create_odi_div(url); | |
if (odi_div == null) | |
{ | |
continue; | |
} | |
if (odi_div.odi_type == "image") | |
{ | |
image_divs.push(odi_div); | |
} | |
else | |
{ | |
media_divs.push(odi_div); | |
} | |
} | |
array_extend(odi_divs, image_divs); | |
array_extend(odi_divs, media_divs); | |
return odi_divs; | |
} | |
function create_workspace() | |
{ | |
clear_page(); | |
apply_css(); | |
console.log("creating workspace"); | |
var control_panel = document.createElement("div"); | |
var workspace = document.createElement("div"); | |
var resizer = create_command_box_button("resizer", "resize", resize_images); | |
var refilter = create_command_box_button("refilter", "remove regex", function(x){filter_re(x, true)}); | |
var rekeeper = create_command_box_button("rekeeper", "keep regex", function(x){filter_re(x, false)}); | |
var heightfilter = create_command_box_button("heightfilter", "min height", filter_height); | |
var widthfilter = create_command_box_button("widthfilter", "min width", filter_width); | |
var sorter = create_command_button("sort size", sort_size); | |
var dumper = create_command_button("dump urls", dump_urls); | |
var ingest_box = document.createElement("textarea"); | |
var ingest_button = create_command_button("ingest", ingest); | |
var start_button = create_command_button("load all", function(){start();}); | |
start_button.style.display = "block"; | |
control_panel.id = "CONTROL_PANEL"; | |
control_panel.className = "control_panel"; | |
workspace.id = "WORKSPACE"; | |
workspace.className = "workspace"; | |
var ingest_div = document.createElement("div"); | |
ingest_div.id = "INGEST"; | |
ingest_div.className = "ingest"; | |
ingest_box.id = "ingestbox"; | |
ingest_box.className = "ingestbox"; | |
ingest_div.appendChild(ingest_box); | |
ingest_div.appendChild(ingest_button); | |
ingest_div.appendChild(ingest_box); | |
ingest_div.appendChild(ingest_button); | |
document.body.appendChild(control_panel); | |
control_panel.appendChild(resizer); | |
control_panel.appendChild(refilter); | |
control_panel.appendChild(rekeeper); | |
control_panel.appendChild(heightfilter); | |
control_panel.appendChild(widthfilter); | |
control_panel.appendChild(sorter); | |
control_panel.appendChild(dumper); | |
control_panel.appendChild(ingest_div); | |
control_panel.appendChild(start_button); | |
document.body.appendChild(workspace); | |
console.log("finished workspace"); | |
} | |
function delete_odi_div(element) | |
{ | |
if (element.odi_div != undefined) | |
{ | |
element = element.odi_div; | |
} | |
if (element.media != undefined) | |
{ | |
/* http://stackoverflow.com/questions/3258587/how-to-properly-unload-destroy-a-video-element */ | |
element.media.pause(); | |
element.media.src = ""; | |
element.media.load(); | |
} | |
var parent = element.parentElement; | |
parent.removeChild(element); | |
} | |
function dump_urls() | |
{ | |
var divs = get_odi_divs(); | |
var textbox = document.getElementById("url_dump_box"); | |
if (textbox == null) | |
{ | |
textbox = document.createElement("textarea"); | |
textbox.className = "urldumpbox"; | |
textbox.id = "url_dump_box"; | |
workspace = document.getElementById("WORKSPACE"); | |
workspace.appendChild(textbox); | |
} | |
textbox.innerHTML = ""; | |
for (var index = 0; index < divs.length; index += 1) | |
{ | |
textbox.innerHTML += divs[index].url + "\n"; | |
} | |
} | |
function fill_workspace(divs) | |
{ | |
console.log("filling workspace"); | |
workspace = document.getElementById("WORKSPACE"); | |
for (var index = 0; index < divs.length; index += 1) | |
{ | |
workspace.appendChild(divs[index]); | |
} | |
} | |
function filter_dimension(dimension, minimum) | |
{ | |
minimum = parseInt(minimum); | |
images = Array.from(document.images); | |
for (var i = 0; i < images.length; i += 1) | |
{ | |
image = images[i]; | |
if (image[dimension] == 0) | |
{continue;} | |
if (image[dimension] < minimum) | |
{ | |
delete_odi_div(image); | |
continue; | |
} | |
} | |
} | |
function filter_height(minimum) | |
{ | |
filter_dimension('naturalHeight', minimum); | |
} | |
function filter_width(minimum) | |
{ | |
filter_dimension('naturalWidth', minimum); | |
} | |
function filter_re(pattern, do_delete) | |
{ | |
if (!pattern) | |
{ | |
return; | |
} | |
pattern = new RegExp(pattern, "i"); | |
do_keep = !do_delete; | |
console.log(pattern + " " + do_delete); | |
odi_divs = get_odi_divs(); | |
for (var index = 0; index < odi_divs.length; index += 1) | |
{ | |
div = odi_divs[index]; | |
match = div.url.match(pattern); | |
if ((match && do_delete) || (!match && do_keep)) | |
{ | |
delete_odi_div(div); | |
} | |
} | |
} | |
function format_css() | |
{ | |
console.log("Formatting CSS variables"); | |
var css = CSS; | |
while (true) | |
{ | |
matches = css.match("\\$.+?\\$"); | |
if (!matches) | |
{ | |
break; | |
} | |
console.log(matches); | |
matches = new Set(matches); | |
/* Originally used Array.from(set) and did regular iteration, but I found | |
that sites can override and break that conversion. */ | |
matches.forEach( | |
function(injector) | |
{ | |
var injected = injector.replace(new RegExp("\\$", 'g'), ""); | |
css = css.replace(injector, this[injected]); | |
} | |
); | |
} | |
return css; | |
} | |
function get_all_urls() | |
{ | |
console.log("Collecting urls"); | |
var urls = []; | |
function include(source, attr) | |
{ | |
for (var index = 0; index < source.length; index += 1) | |
{ | |
url = source[index][attr]; | |
if (url === undefined) | |
{continue;} | |
if (seen_urls.has(url)) | |
{continue;} | |
console.log(url); | |
if (url.indexOf("thumbs.redditmedia") != -1) | |
{console.log("Rejecting reddit thumb"); continue;} | |
if (url.indexOf("pixel.reddit") != -1 || url.indexOf("reddit.com/static/pixel") != -1) | |
{console.log("Rejecting reddit pixel"); continue} | |
/*if (url.indexOf("/thumb/") != -1) | |
{console.log("Rejecting /thumb/"); continue;}*/ | |
if (url.indexOf("/loaders/") != -1) | |
{console.log("Rejecting loader"); continue;} | |
if (url.indexOf("memegen") != -1) | |
{console.log("Rejecting retardation"); continue;} | |
if (url.indexOf("4cdn") != -1 && url.indexOf("s.jpg") != -1) | |
{console.log("Rejecting 4chan thumb"); continue;} | |
sub_urls = normalize_url(url); | |
if (sub_urls == null) | |
{continue;} | |
for (var url_index = 0; url_index < sub_urls.length; url_index += 1) | |
{ | |
sub_url = sub_urls[url_index]; | |
if (seen_urls.has(sub_url)) | |
{continue;} | |
urls.push(sub_url); | |
seen_urls.add(sub_url); | |
} | |
seen_urls.add(url); | |
} | |
} | |
var docs = []; | |
docs.push(document); | |
while (docs.length > 0) | |
{ | |
var d = docs.pop(); | |
include(d.links, "href"); | |
include(d.images, "src"); | |
include(d.getElementsByTagName("audio"), "src"); | |
include(d.getElementsByTagName("video"), "src"); | |
include(d.getElementsByTagName("source"), "src"); | |
} | |
console.log("collected " + urls.length + " urls."); | |
return urls; | |
} | |
function get_alternate_sources(url) | |
{ | |
/* | |
For sites that must try multiple resource urls, that logic | |
may go here | |
*/ | |
return [url]; | |
} | |
function get_basename(url) | |
{ | |
var basename = url.split("/"); | |
basename = basename[basename.length - 1]; | |
return basename; | |
} | |
function get_gfycat_video(id) | |
{ | |
console.log("Resolving gfycat " + id); | |
var url = "https://gfycat.com/cajax/get/" + id; | |
var request = new XMLHttpRequest(); | |
request.answer = null; | |
request.onreadystatechange = function() | |
{ | |
if (request.readyState == 4 && request.status == 200) | |
{ | |
var text = request.responseText; | |
var details = JSON.parse(text); | |
request.answer = details["gfyItem"]["mp4Url"]; | |
} | |
}; | |
var asynchronous = false; | |
request.open("GET", url, asynchronous); | |
request.send(null); | |
return request.answer; | |
} | |
function get_lazy_divs() | |
{ | |
var divs = document.getElementsByTagName("div"); | |
var lazy_elements = []; | |
for (index = 0; index < divs.length; index += 1) | |
{ | |
var div = divs[index]; | |
if (div.image && div.image.lazy_src) | |
{ | |
lazy_elements.push(div); | |
} | |
} | |
return lazy_elements; | |
} | |
function get_odi_divs() | |
{ | |
var divs = document.getElementsByTagName("div"); | |
var odi_divs = []; | |
for (index = 0; index < divs.length; index += 1) | |
{ | |
var div = divs[index]; | |
if (div.id.indexOf("odi_") == -1) | |
{ | |
continue; | |
} | |
odi_divs.push(div); | |
} | |
return odi_divs; | |
} | |
function generate_id(length) | |
{ | |
/* Thanks csharptest http://stackoverflow.com/a/1349426 */ | |
var text = []; | |
var possible = "abcdefghijklmnopqrstuvwxyz"; | |
for(var i = 0; i < length; i += 1) | |
{ | |
c = possible.charAt(Math.floor(Math.random() * possible.length)); | |
text.push(c); | |
} | |
return "odi_" + text.join(""); | |
} | |
function ingest() | |
{ | |
/* Take the text from the INGEST box, and make odi divs from it */ | |
console.log("Ingesting"); | |
var odi_divs = get_odi_divs(); | |
var ingestbox = document.getElementById("ingestbox"); | |
var text = ingestbox.value; | |
var urls = text.split("\n"); | |
for (var index = 0; index < urls.length; index += 1) | |
{ | |
url = urls[index].trim(); | |
sub_urls = normalize_url(url); | |
if (sub_urls == null) | |
{continue;} | |
for (var url_index = 0; url_index < sub_urls.length; url_index += 1) | |
{ | |
sub_url = sub_urls[url_index]; | |
var odi_div = create_odi_div(sub_url); | |
if (odi_div == null) | |
{continue;} | |
odi_divs.push(odi_div); | |
} | |
} | |
ingestbox.value = ""; | |
clear_workspace(); | |
fill_workspace(odi_divs); | |
} | |
function lazy_load_all() | |
{ | |
console.log("Starting lazyload"); | |
lazies = get_lazy_divs(); | |
lazies.reverse(); | |
lazy_buttons = document.getElementsByClassName("load_button"); | |
for (var index = 0; index < lazy_buttons.length; index += 1) | |
{ | |
lazy_buttons[index].parentElement.removeChild(lazy_buttons[index]); | |
} | |
while (lazies.length > 0) | |
{ | |
var element = lazies.pop(); | |
if (element.image != undefined) | |
{ | |
break; | |
} | |
} | |
if (element == undefined) | |
{ | |
return; | |
} | |
lazy_load_one(element, true); | |
return | |
;} | |
function lazy_load_one(element, comeback) | |
{ | |
var image = element.image; | |
if (!image.lazy_src) | |
{ | |
return; | |
} | |
image.onload = function() | |
{ | |
width = this.naturalWidth; | |
height = this.naturalHeight; | |
if (width == 161 && height == 81) | |
{delete_odi_div(this);} | |
this.arealabel.innerHTML = width + " x " + height; | |
this.odi_div.style.minWidth = "0px"; | |
if (comeback){lazy_load_all()}; | |
}; | |
image.onerror = function() | |
{ | |
delete_odi_div(this); | |
if (comeback){lazy_load_all()}; | |
}; | |
/*console.log("Lazy loading " + element.lazy_src)*/ | |
image.src = image.lazy_src; | |
image.lazy_src = null; | |
return; | |
} | |
function normalize_url(url) | |
{ | |
var protocol = window.location.protocol; | |
if (protocol == "file:") | |
{ | |
protocol = "http:"; | |
} | |
url = url.replace("http:", protocol); | |
url = url.replace("https:", protocol); | |
url = decodeURIComponent(unescape(url)); | |
url = url.replace("imgur.com/gallery/", "imgur.com/a/"); | |
if (url.indexOf("vidble") >= 0) | |
{ | |
url = url.replace("_med", ""); | |
url = url.replace("_sqr", ""); | |
} | |
else if (url.indexOf("imgur.com/a/") != -1) | |
{ | |
var urls = []; | |
var id = url.split("imgur.com/a/")[1]; | |
id = id.split("#")[0].split("?")[0]; | |
console.log("imgur album: " + id); | |
var url = "https://api.imgur.com/3/album/" + id; | |
var request = new XMLHttpRequest(); | |
request.onreadystatechange = function() | |
{ | |
if (request.readyState == 4 && request.status == 200) | |
{ | |
var text = request.responseText; | |
var images = JSON.parse(request.responseText); | |
images = images['data']['images']; | |
for (var index = 0; index < images.length; index += 1) | |
{ | |
var image = images[index]; | |
var image_url = image["mp4"] || image["link"]; | |
if (!image_url){continue;} | |
image_url = normalize_url(image_url)[0]; | |
console.log("+" + image_url); | |
urls.push(image_url); | |
} | |
} | |
}; | |
var asynchronous = false; | |
request.open("GET", url, asynchronous); | |
request.setRequestHeader("Authorization", "Client-ID 1d8d9b36339e0e2"); | |
request.send(null); | |
return urls; | |
} | |
else if (url.indexOf("imgur.com") >= 0) | |
{ | |
var url_parts = url.split("/"); | |
var image_id = url_parts[url_parts.length - 1]; | |
var extension = ".jpg"; | |
if (image_id.indexOf(".") != -1) | |
{ | |
image_id = image_id.split("."); | |
extension = "." + image_id[1]; | |
image_id = image_id[0]; | |
} | |
extension = extension.replace(".gifv", ".mp4"); | |
extension = extension.replace(".gif", ".mp4"); | |
if (image_id.length % 2 == 0) | |
{ | |
image_id = image_id.split(""); | |
image_id[image_id.length - 1] = ""; | |
image_id = image_id.join(""); | |
} | |
url = protocol + "//i.imgur.com/" + image_id + extension; | |
} | |
else if (url.indexOf("gfycat.com") >= 0) | |
{ | |
var gfy_id = url.split("/"); | |
gfy_id = gfy_id[gfy_id.length - 1]; | |
gfy_id = gfy_id.split(".")[0]; | |
if (gfy_id.length > 0) | |
{ | |
url = get_gfycat_video(gfy_id); | |
} | |
} | |
return [url]; | |
} | |
function resize_images(height) | |
{ | |
odi_divs = get_odi_divs(); | |
height = height.toString() + "px"; | |
for (var index = 0; index < odi_divs.length; index += 1) | |
{ | |
var div = odi_divs[index]; | |
if (div.image) | |
{ | |
div.image.style.height = height; | |
} | |
else if (div.media && div.mediatype == "video") | |
{ | |
div.media.style.height = height; | |
} | |
} | |
} | |
function sort_size() | |
{ | |
console.log("sorting size"); | |
odi_divs = get_odi_divs(); | |
odi_divs.sort(sort_size_comparator); | |
odi_divs.reverse(); | |
clear_workspace(); | |
fill_workspace(odi_divs); | |
} | |
function sort_size_comparator(div1, div2) | |
{ | |
if (div1.odi_type != "image" || div1.lazy_src) | |
{ | |
return -1; | |
} | |
if (div2.odi_type != "image" || div2.lazy_src) | |
{ | |
return 1; | |
} | |
pixels1 = div1.image.naturalHeight * div1.image.naturalWidth; | |
pixels2 = div2.image.naturalHeight * div2.image.naturalWidth; | |
if (pixels1 < pixels2) | |
{return -1;} | |
if (pixels1 > pixels2) | |
{return 1;} | |
return 0; | |
} | |
function start() | |
{ | |
lazy_load_all(); | |
has_started = true; | |
} | |
function main() | |
{ | |
var all_urls = get_all_urls(); | |
var divs = create_odi_divs(all_urls); | |
create_workspace(); | |
fill_workspace(divs); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment