diff --git a/Makefile b/Makefile
index f9d39c3..92a6910 100644
--- a/Makefile
+++ b/Makefile
@@ -16,22 +16,24 @@ MAKEPAGES := $(TMPDIR)/make-pages
YAMLS != find -L $(DATADIR) -name $(INFONAME)
-SCRIPTS != find script -name '*.js'
+TSCRIPTS != find script -name '*.ts'
STYLES != find style -name '*.css'
STYLESVGS != find style -name '*.svg'
STYLEPNGS != find style -name '*.png'
+
FONTS != find fonts \
-iname '*.eot' -or -iname '*.svg' -or \
-iname '*.ttf' -or -iname '*.woff' -or \
-iname '*.woff2' -or -iname '*.css'
-STATIC = $(SCRIPTS) $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS)
-BSTATIC = $(patsubst %,$(BUILDDIR)/%,$(STATIC))
+STATIC = $(STYLES) $(STYLEPNGS) $(STYLESVGS) $(FONTS)
+BSTATIC = $(patsubst %,$(BUILDDIR)/%,$(STATIC))
+BSCRIPTS = $(patsubst %.ts,$(BUILDDIR)/%.js,$(TSCRIPTS))
.PHONY: all build
all: build
-build: $(BUILDDIR)/index.html $(BSTATIC)
+build: $(BUILDDIR)/index.html $(BSTATIC) $(BSCRIPTS)
$(BUILDDIR)/index.html: $(DATADIR)/index.yaml $(MAKEPAGES)
echo "[index] "$@
@@ -46,6 +48,12 @@ $(BUILDDIR)/%: $(TMPDIR)/%
$(call copy,--link)
+$(TMPDIR)/%.js: %.ts
+ echo "[tsc] "$@
+ tsc --strict --noEmitOnError \
+ --lib dom,es2021 --target es2015 \
+ --outDir $(dir $@) $^
+
$(TMPDIR)/%_small.png: $(DATADIR)/%.png
$(call resize,$(SMALL),$(SMALL),^,-gravity center -crop 1:1+0)
diff --git a/make-pages/GalleryPage.hs b/make-pages/GalleryPage.hs
index 686dda9..5331e21 100644
--- a/make-pages/GalleryPage.hs
+++ b/make-pages/GalleryPage.hs
@@ -42,7 +42,7 @@ make' root (GalleryInfo {title, desc, prefix, filters, hidden}) infos = [b|@0
-
+
$0.nsfwScript
$title
diff --git a/make-pages/NsfwWarning.hs b/make-pages/NsfwWarning.hs
index 5e820ab..1f4066b 100644
--- a/make-pages/NsfwWarning.hs
+++ b/make-pages/NsfwWarning.hs
@@ -12,7 +12,7 @@ instance CanBuild What where
script :: Maybe What -> Builder
script Nothing = ""
-script (Just _) = [b||]
+script (Just _) = [b||]
dialog :: Maybe What -> Builder
dialog Nothing = ""
diff --git a/make-pages/SinglePage.hs b/make-pages/SinglePage.hs
index 02fd3e5..b249fa5 100644
--- a/make-pages/SinglePage.hs
+++ b/make-pages/SinglePage.hs
@@ -141,7 +141,7 @@ make' root siteName prefix nsfw _dataDir dir
-
+
$nsfwScript
$bgStyle
diff --git a/script/gallery.js b/script/gallery.js
deleted file mode 100644
index 10502c3..0000000
--- a/script/gallery.js
+++ /dev/null
@@ -1,204 +0,0 @@
-(function() {
-'use strict';
-
-let reqBoxes;
-let excBoxes;
-let allBoxes;
-let tags;
-let itemsByYear;
-
-let showSingles = false;
-
-
-function fillSets() {
- let checkedValues = boxes =>
- new Set(boxes.filter(b => b.checked).map(b => b.value));
-
- return [checkedValues(reqBoxes), checkedValues(excBoxes)];
-}
-
-function updateItems() {
- let [reqTags, excTags] = fillSets();
- let anyReq = reqTags.size > 0;
-
- for (let [year, items] of itemsByYear) {
- let hide = true;
-
- for (let item of items) {
- let req = tags.get(item).some(x => reqTags.has(x));
- let exc = tags.get(item).some(x => excTags.has(x));
- let hidden = exc || (anyReq && !req);
-
- item.hidden = hidden;
- hide &&= hidden;
- }
-
- document.getElementById(`marker-${year}`).hidden = hide;
- }
-
- function disp(pfx, tags) {
- return Array(...tags).map(x => pfx + x).join('\u2003'); // em space
- }
- let plus = disp('+\u2009', reqTags); // thin space
- let minus = disp('-\u2009', excTags);
- document.getElementById('filters-details').dataset.filters =
- `${plus}\u2003${minus}`.trim();
-}
-
-function update() {
- updateItems();
- history.pushState(null, "", makeFragment());
-}
-
-function converseId(id) {
- if (id.match(/^require/)) {
- return id.replace('require', 'exclude');
- } else {
- return id.replace('exclude', 'require');
- }
-}
-
-function toggle(checkbox) {
- if (checkbox.checked)
- document.getElementById(converseId(checkbox.id)).checked = false;
-
- update();
-}
-
-
-function clearForm() {
- allBoxes.forEach(b => b.checked = b.defaultChecked);
-}
-
-function clear(e) {
- clearForm();
- update();
- if (e) e.preventDefault();
-}
-
-function toggleSingles(e) {
- showSingles = !showSingles;
-
- for (let li of document.querySelectorAll('.filterlist li')) {
- let count = +li.querySelector('label').dataset.count;
- if (count <= 1) {
- li.hidden = !showSingles;
- }
- }
-
- if (e) e.preventDefault();
-}
-
-
-function makeFragment() {
- let ids = allBoxes.filter(b => b.checked).map(b => b.id);
- if (ids.length == 0) {
- return '#all';
- } else if (allBoxes.every(b => b.checked == b.defaultChecked)) {
- return '#';
- } else {
- return '#' + ids.join(';');
- }
-}
-
-function useFragment() {
- let frag = decodeURIComponent(location.hash).replace(/^#/, '');
- let details = document.getElementById('filters-details');
-
- if (!frag) {
- clearForm();
- } else if (frag == 'all') {
- allBoxes.forEach(b => b.checked = false);
- details.open = false;
- } else {
- let set = new Set(frag.split(';'));
- let re = /^(require|exclude)_|hide_filters/;
- if (Array.from(set).every(x => re.test(x))) {
- allBoxes.forEach(b => b.checked = set.has(b.id));
- details.open = !frag.match(/hide_filters|example\b/);
- }
- }
-
- updateItems();
-}
-
-
-function sortFilters(cmp) {
- function sort1(id) {
- let elt = document.getElementById(id);
- let children = [...elt.childNodes];
- children.sort(cmp);
- for (let c of children) {
- elt.removeChild(c);
- elt.appendChild(c);
- }
- }
-
- sort1('require');
- sort1('exclude');
-}
-
-function sortFiltersAlpha(e) {
- function getName(x) {
- if (x.nodeType == Node.ELEMENT_NODE) {
- return x.getElementsByTagName('input')[0].value;
- } else {
- return '';
- }
- }
- sortFilters((a, b) => getName(a).localeCompare(getName(b)));
- e.preventDefault();
-}
-
-function sortFiltersUses(e) {
- function getUses(x) {
- if (x.nodeType == Node.ELEMENT_NODE) {
- return parseInt(x.getElementsByTagName('label')[0].dataset.count);
- } else {
- return 0;
- }
- }
- sortFilters((a, b) => getUses(b) - getUses(a));
- e.preventDefault();
-}
-
-
-function setup() {
- function inputs(id) {
- let iter = document.getElementById(id).getElementsByTagName('input');
- return Array.from(iter);
- }
-
- let items = Array.from(document.getElementsByClassName('post'));
-
- itemsByYear = new Map;
- for (let item of items) {
- let year = item.dataset.year;
- if (!itemsByYear.has(year)) itemsByYear.set(year, new Set);
- itemsByYear.get(year).add(item);
- }
-
- reqBoxes = inputs('require');
- excBoxes = inputs('exclude');
- allBoxes = [...reqBoxes, ...excBoxes];
-
- tags = new Map(items.map(item => [item, item.dataset.tags.split(';')]));
-
-
- allBoxes.forEach(b => b.addEventListener('change', () => toggle(b)));
-
- function addClick(id, f) {
- document.getElementById(id).addEventListener('click', f);
- }
- addClick('clear', clear);
- addClick('sortalpha', sortFiltersAlpha);
- addClick('sortuses', sortFiltersUses);
- addClick('singles', toggleSingles);
-
- window.addEventListener('popstate', useFragment);
-
- useFragment();
-}
-
-window.addEventListener('DOMContentLoaded', setup);
-})();
diff --git a/script/gallery.ts b/script/gallery.ts
new file mode 100644
index 0000000..1d0cd39
--- /dev/null
+++ b/script/gallery.ts
@@ -0,0 +1,220 @@
+type Boxes = Set;
+
+let reqBoxes: Boxes;
+let excBoxes: Boxes;
+let allBoxes: Boxes;
+let tags: Map;
+let itemsByYear: Map>;
+
+let showSingles = false;
+
+
+function fillSets() {
+ function checkedValues(boxes: Boxes) {
+ return new Set(Array.from(boxes).filter(b => b.checked).map(b => b.value));
+ }
+
+ return [checkedValues(reqBoxes), checkedValues(excBoxes)];
+}
+
+function updateItems() {
+ let [reqTags, excTags] = fillSets();
+ let anyReq = reqTags.size > 0;
+
+ for (let [year, items] of itemsByYear) {
+ let hide = true;
+
+ for (let item of items) {
+ let req = tags.get(item)?.some(x => reqTags.has(x)) ?? false;
+ let exc = tags.get(item)?.some(x => excTags.has(x)) ?? false;
+ let hidden = exc || (anyReq && !req);
+
+ item.hidden = hidden;
+ hide &&= hidden;
+ }
+
+ let marker = document.getElementById(`marker-${year}`);
+ if (marker !== null) marker.hidden = hide;
+ }
+
+ function disp(pfx: string, tags: string[]) {
+ return Array(...tags).map(x => pfx + x).join('\u2003'); // em space
+ }
+ let plus = disp('+\u2009', Array.from(reqTags)); // thin space
+ let minus = disp('-\u2009', Array.from(excTags));
+ document.getElementById('filters-details')!.dataset.filters =
+ `${plus}\u2003${minus}`.trim();
+}
+
+function update() {
+ updateItems();
+ history.pushState(null, "", makeFragment());
+}
+
+function converseId(id: string) {
+ if (id.match(/^require/)) {
+ return id.replace('require', 'exclude');
+ } else {
+ return id.replace('exclude', 'require');
+ }
+}
+
+function toggle(checkbox: HTMLInputElement) {
+ if (checkbox.checked) {
+ let converse = document.getElementById(converseId(checkbox.id)) as HTMLInputElement;
+ converse.checked = false;
+ }
+ update();
+}
+
+
+function clearForm() {
+ allBoxes.forEach(b => b.checked = b.defaultChecked);
+}
+
+function clear(e: Event) {
+ clearForm();
+ update();
+ e.preventDefault();
+}
+
+function toggleSingles(e: Event) {
+ showSingles = !showSingles;
+
+ let elems = Array.from(document.querySelectorAll('.filterlist li'));
+ for (let li of elems) {
+ let countStr = li.querySelector('label')?.dataset.count;
+ let count = countStr ? +countStr : 0;
+ if (count <= 1 && li instanceof HTMLElement) {
+ li.hidden = !showSingles;
+ }
+ }
+
+ if (e) e.preventDefault();
+}
+
+
+function makeFragment() {
+ let allBoxesArr = Array.from(allBoxes);
+ let ids = allBoxesArr.filter(b => b.checked).map(b => b.id);
+ if (ids.length == 0) {
+ return '#all';
+ } else if (allBoxesArr.every(b => b.checked == b.defaultChecked)) {
+ return '#';
+ } else {
+ return '#' + ids.join(';');
+ }
+}
+
+function useFragment() {
+ let frag = decodeURIComponent(location.hash).replace(/^#/, '');
+ let details = document.getElementById('filters-details') as HTMLDetailsElement;
+
+ if (!frag) {
+ clearForm();
+ } else if (frag == 'all') {
+ allBoxes.forEach(b => b.checked = false);
+ details.open = false;
+ } else {
+ let set = new Set(frag.split(';'));
+ let re = /^(require|exclude)_|hide_filters/;
+ if (Array.from(set).every(x => re.test(x))) {
+ allBoxes.forEach(b => b.checked = set.has(b.id));
+ details.open = !frag.match(/hide_filters|example\b/);
+ }
+ }
+
+ updateItems();
+}
+
+
+function sortFilters(cmp: (a: Node, b: Node) => number) {
+ function sort1(id: string) {
+ let elt = document.getElementById(id);
+ if (elt === null) return;
+
+ let children = Array.from(elt.childNodes);
+ children.sort(cmp);
+ for (let c of children) {
+ elt.removeChild(c);
+ elt.appendChild(c);
+ }
+ }
+
+ sort1('require');
+ sort1('exclude');
+}
+
+function sortFiltersAlpha(e: Event) {
+ function getName(node: Node) {
+ if (node.nodeType == Node.ELEMENT_NODE) {
+ let elt = node as Element;
+ return elt.getElementsByTagName('input')[0].value;
+ } else {
+ return '';
+ }
+ }
+ sortFilters((a, b) => getName(a).localeCompare(getName(b)));
+ e.preventDefault();
+}
+
+function sortFiltersUses(e: Event) {
+ function getUses(node: Node) {
+ if (node.nodeType == Node.ELEMENT_NODE) {
+ let elt = node as Element;
+ let countStr = elt.getElementsByTagName('label')[0].dataset.count;
+ return countStr ? +countStr : 0;
+ } else {
+ return 0;
+ }
+ }
+ sortFilters((a, b) => getUses(b) - getUses(a));
+ e.preventDefault();
+}
+
+
+function setup() {
+ function inputs(id: string) {
+ let iter = document.getElementById(id)!.getElementsByTagName('input');
+ return new Set(Array.from(iter));
+ }
+
+ let items = Array.from(document.getElementsByClassName('post')) as HTMLElement[];
+
+ itemsByYear = new Map;
+ for (let item of items) {
+ let year = item.dataset.year;
+ if (year !== undefined) {
+ if (!itemsByYear.has(year)) {
+ itemsByYear.set(year, new Set([item]));
+ } else {
+ itemsByYear.get(year)?.add(item);
+ }
+ }
+ }
+
+ reqBoxes = inputs('require');
+ excBoxes = inputs('exclude');
+ allBoxes = new Set([...reqBoxes, ...excBoxes]);
+
+ tags = new Map(items.map(item => [item, item.dataset.tags?.split(';') ?? []]));
+
+
+ allBoxes.forEach(b => b.addEventListener('change', () => toggle(b)));
+
+ function addClick(id: string, f: (e: Event) => void) {
+ document.getElementById(id)!.addEventListener('click', f);
+ }
+ addClick('clear', clear);
+ addClick('sortalpha', sortFiltersAlpha);
+ addClick('sortuses', sortFiltersUses);
+ addClick('singles', toggleSingles);
+
+ window.addEventListener('popstate', useFragment);
+
+ useFragment();
+}
+
+window.addEventListener('DOMContentLoaded', setup);
+
+export {};
diff --git a/script/nsfw-warning.js b/script/nsfw-warning.js
deleted file mode 100644
index 493f36b..0000000
--- a/script/nsfw-warning.js
+++ /dev/null
@@ -1,38 +0,0 @@
-(function () {
-'use strict';
-
-let nsfwOk = 'nsfw-ok';
-
-function alreadyYes() {
- return localStorage.getItem(nsfwOk);
-}
-
-function dismiss() {
- let dialog = document.getElementById('nsfw-dialog');
- dialog.parentElement.removeChild(dialog);
-}
-
-function yes() {
- localStorage.setItem(nsfwOk, 1);
- dismiss();
-}
-
-// now just a normal link
-/*
-function no() {
- document.location = '//crouton.net';
-}
-*/
-
-function setup() {
- if (alreadyYes()) {
- dismiss();
- } else {
- document.getElementById('nsfw-yes').onclick = yes;
- // document.getElementById('nsfw-no').onclick = no;
- }
-}
-
-document.addEventListener('DOMContentLoaded', setup);
-
-})();
diff --git a/script/nsfw-warning.ts b/script/nsfw-warning.ts
new file mode 100644
index 0000000..ac901c5
--- /dev/null
+++ b/script/nsfw-warning.ts
@@ -0,0 +1,28 @@
+const nsfwOk = 'nsfw-ok';
+
+function alreadyYes() {
+ return localStorage.getItem(nsfwOk) !== undefined;
+}
+
+function dismiss() {
+ let dialog = document.getElementById('nsfw-dialog')!;
+ dialog.parentElement?.removeChild(dialog);
+}
+
+function yes() {
+ localStorage.setItem(nsfwOk, '1');
+ dismiss();
+}
+
+function setup() {
+ if (alreadyYes()) {
+ dismiss();
+ } else {
+ document.getElementById('nsfw-yes')!.onclick = yes;
+ // nsfw-no is a normal link
+ }
+}
+
+document.addEventListener('DOMContentLoaded', setup);
+
+export {};
diff --git a/script/single.js b/script/single.js
deleted file mode 100644
index 84c6309..0000000
--- a/script/single.js
+++ /dev/null
@@ -1,110 +0,0 @@
-(function() {
-'use strict';
-
-let mainfig;
-let mainimg;
-let mainlink;
-let altButtons;
-let skipAll;
-
-let opened = new Set;
-
-function openCW(id, caption, focusLink = false) {
- if (id) opened.add(id);
- mainfig.removeChild(caption);
- mainlink.tabIndex = 0;
- if (focusLink) mainlink.focus();
-}
-
-function addCWListeners(id, caption) {
- if (caption) {
- caption.addEventListener('click', e => openCW(id, caption));
- caption.addEventListener('keyup',
- e => { if (e.key == 'Enter') openCW(id, caption, true) });
- }
-}
-
-function setImage(id, src, href, cw) {
- let caption = document.getElementById('cw');
- let newCaption;
-
- let checked = skipAll ? skipAll.checked : false;
-
- if (!checked && !opened.has(id) && cw) {
- newCaption = document.getElementById('cw-template')
- .content.firstElementChild.cloneNode(true);
- newCaption.querySelector('#cw-text').innerHTML = cw;
- addCWListeners(id, newCaption);
- }
-
- if (caption) {
- openCW(null, caption);
- }
- if (newCaption) {
- mainfig.insertBefore(newCaption, mainlink);
- mainlink.tabIndex = -1;
- }
-
- mainimg.src = src;
- mainlink.href = href;
-}
-
-function activateButton(button, doPush = true) {
- setImage(button.id, button.value,
- button.dataset.link, button.dataset.warning);
-
- if (doPush) history.pushState(null, '', '#' + button.id);
-}
-
-function useFragment(firstLoad = false) {
- let button = altButtons[0];
-
- let frag = decodeURIComponent(location.hash).replace(/^#/, '');
- if (frag) {
- let selected = document.getElementById(frag);
- if (selected) button = selected;
- }
-
- let id;
-
- if (button) {
- id = button.id;
- button.checked = true;
- activateButton(button, false);
- }
-
- if (firstLoad) addCWListeners(id, document.getElementById('cw'));
-}
-
-function setup() {
- mainfig = document.getElementById('mainfig');
- mainimg = document.getElementById('mainimg');
- mainlink = document.getElementById('mainlink');
- skipAll = document.getElementById('skipAll');
-
- let alts = document.getElementById('alts');
- if (alts) {
- let inputs = Array.from(alts.getElementsByTagName('input'));
- altButtons = inputs.filter(e => e.name == 'variant');
- } else {
- altButtons = [];
- }
-
- for (let button of altButtons) {
- button.onchange = e => { if (button.checked) activateButton(button); };
- }
-
- if (skipAll) {
- skipAll.onchange = e => { if (skipAll.checked) {
- let caption = document.getElementById('cw');
- if (caption) { openCW(null, caption, false); }
- } };
- }
-
- window.addEventListener('popstate', e => useFragment());
- useFragment(true);
-}
-
-window.addEventListener('DOMContentLoaded', setup);
-
-})();
diff --git a/script/single.ts b/script/single.ts
new file mode 100644
index 0000000..0623093
--- /dev/null
+++ b/script/single.ts
@@ -0,0 +1,114 @@
+let mainfig: HTMLElement;
+let mainimg: HTMLImageElement;
+let mainlink: HTMLAnchorElement;
+let altButtons: HTMLInputElement[];
+let skipAll: HTMLInputElement;
+
+let opened = new Set;
+
+function openCW(id: string | null, caption: HTMLElement | null,
+ focusLink = false) {
+ if (id !== null) opened.add(id);
+ if (caption !== null) mainfig.removeChild(caption);
+ mainlink.tabIndex = 0;
+ if (focusLink) mainlink.focus();
+}
+
+function addCWListeners(id: string | null,
+ caption: HTMLElement | null) {
+ if (caption) {
+ caption.addEventListener('click', _e => openCW(id, caption));
+ caption.addEventListener('keyup',
+ e => { if (e.key == 'Enter') openCW(id, caption, true) });
+ }
+}
+
+function setImage(id: string | null,
+ src: string,
+ href: string,
+ cw: string) {
+ let caption = document.getElementById('cw');
+ let newCaption: HTMLElement | null = null;
+
+ let checked = skipAll ? skipAll.checked : false;
+
+ if (!checked && !opened.has(id) && cw) {
+ let template = document.getElementById('cw-template') as HTMLTemplateElement;
+ newCaption = template.content.firstElementChild!.cloneNode(true) as HTMLElement;
+ newCaption.querySelector('#cw-text')!.innerHTML = cw;
+ addCWListeners(id, newCaption);
+ }
+
+ if (caption) {
+ openCW(null, caption);
+ }
+ if (newCaption !== null) {
+ mainfig.insertBefore(newCaption, mainlink);
+ mainlink.tabIndex = -1;
+ }
+
+ mainimg.src = src;
+ mainlink.href = href;
+}
+
+function activateButton(button: HTMLInputElement,
+ doPush = true) {
+ setImage(button.id, button.value,
+ button.dataset.link!,
+ button.dataset.warning!);
+
+ if (doPush) history.pushState(null, '', '#' + button.id);
+}
+
+function useFragment(firstLoad = false) {
+ let button = altButtons[0];
+
+ let frag = decodeURIComponent(location.hash).replace(/^#/, '');
+ if (frag) {
+ let selected = document.getElementById(frag) as HTMLInputElement;
+ if (selected) button = selected;
+ }
+
+ let id: string | null = null;
+
+ if (button) {
+ id = button.id;
+ button.checked = true;
+ activateButton(button, false);
+ }
+
+ if (firstLoad) addCWListeners(id, document.getElementById('cw'));
+}
+
+function setup() {
+ mainfig = document.getElementById('mainfig')!;
+ mainimg = document.getElementById('mainimg') as HTMLImageElement;
+ mainlink = document.getElementById('mainlink') as HTMLAnchorElement;
+ skipAll = document.getElementById('skipAll') as HTMLInputElement;
+
+ let alts = document.getElementById('alts');
+ if (alts) {
+ let inputs = Array.from(alts.getElementsByTagName('input'));
+ altButtons = inputs.filter(e => e.name == 'variant');
+ } else {
+ altButtons = [];
+ }
+
+ for (let button of altButtons) {
+ button.onchange = _e => { if (button.checked) activateButton(button); };
+ }
+
+ if (skipAll) {
+ skipAll.onchange = _e => { if (skipAll.checked) {
+ let caption = document.getElementById('cw');
+ if (caption) { openCW(null, caption, false); }
+ } };
+ }
+
+ window.addEventListener('popstate', _e => useFragment());
+ useFragment(true);
+}
+
+window.addEventListener('DOMContentLoaded', setup);
+
+export {};
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..4c1a853
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "noEmitOnError": true,
+ "lib": ["ES2021", "dom"],
+ "target": "ES2015"
+ }
+}