diff --git a/content/posts/urchin.md b/content/posts/urchin.md
new file mode 100644
index 0000000..34e258b
--- /dev/null
+++ b/content/posts/urchin.md
@@ -0,0 +1,34 @@
+---
+title: 'Urchin'
+date: 2025-12-25T01:53:05Z
+draft: true
+series: "Frank's Couch"
+summary: ""
+imdb: "tt35715953"
+poster: "/images/posters/urchin.jpg"
+tags:
+ - gucci
+ - ghost theater
+ - marcel
+ - amc-south
+ - amc-lakeline
+ - anticipated
+ - no-expectations
+ - had pizza
+---
+{{< imdbposter >}}
+
+| Date watched | December 14, 2025 |
+|---------------------|-------------------|
+| Show Time | |
+| Theater | |
+| Theater Number | |
+| Pizza | |
+| Tickets | |
+| Letterboxd Rating | ***** (5.0) |
+| Crew | |
+
+{{< /imdbposter >}}
+
+Write your review here...
+
diff --git a/layouts/_default/single.html b/layouts/_default/single.html
index c64780a..3ded598 100644
--- a/layouts/_default/single.html
+++ b/layouts/_default/single.html
@@ -9,6 +9,8 @@
{{ if or (.Site.Params.remark42) (.Site.Config.Services.Disqus.Shortname) }}
{{ partial "post/comments.html" . }}
{{ end }}
+ {{/* Mastodon comments - shows if mastodon_id is set in front matter */}}
+ {{ partial "mastodon-comments.html" . }}
{{- if .Site.Params.goatcounter }}
{{ partial "analytics.html" . -}}
{{- end}}
diff --git a/layouts/partials/mastodon-comments.html b/layouts/partials/mastodon-comments.html
new file mode 100644
index 0000000..5f14889
--- /dev/null
+++ b/layouts/partials/mastodon-comments.html
@@ -0,0 +1,77 @@
+{{/*
+ Mastodon Comments Partial
+
+ Displays comments from a Mastodon post. Requires mastodon_id in front matter.
+ Comment count is fetched at build time; full comments load on button click.
+
+ Inspired by: https://andreas.scherbaum.la/post/2024-05-23_client-side-comments-with-mastodon-on-a-static-hugo-website/
+ And the vibes of: I Saw the TV Glow
+*/}}
+
+{{- $host := "tilde.zone" -}}
+{{- $username := "mnw" -}}
+
+{{- if .Params.mastodon_id -}}
+{{- $id := .Params.mastodon_id -}}
+
+{{/* Fetch comment count at build time */}}
+{{- $count := 0 -}}
+{{- $apiUrl := printf "https://%s/api/v1/statuses/%s/context" $host $id -}}
+{{- with resources.GetRemote $apiUrl -}}
+ {{- if .Err -}}
+ {{/* API error - show 0 */}}
+ {{- else -}}
+ {{- $data := .Content | transform.Unmarshal -}}
+ {{- if $data.descendants -}}
+ {{- $count = len $data.descendants -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+
+{{/* Build blocklist from front matter */}}
+{{- $blocked := slice -}}
+{{- if .Params.mastodon_blocked -}}
+ {{- $blocked = .Params.mastodon_blocked -}}
+{{- end -}}
+
+
+
+
+
+
+
+
+{{- end -}}
diff --git a/layouts/shortcodes/imdbposter.html b/layouts/shortcodes/imdbposter.html
index b4a647c..30fae84 100644
--- a/layouts/shortcodes/imdbposter.html
+++ b/layouts/shortcodes/imdbposter.html
@@ -28,20 +28,16 @@
{{- end -}}
{{- if or $year $runtime $director -}}
- {{- if or $year $runtime -}}
-
- {{- if $year -}}{{ $year }}{{- end -}}
- {{- if and $year $runtime -}} · {{- end -}}
- {{- if $runtime -}}{{ $runtime }} min{{- end -}}
-
- {{- end -}}
{{- if $director -}}
- Directed by {{- if reflect.IsSlice $director -}}
- {{ delimit $director ", " }}
- {{- else -}}
- {{ $director }}
- {{- end -}}
+ Directed by {{ if reflect.IsSlice $director }}{{ delimit $director ", " }}{{ else }}{{ $director }}{{ end }}
+
+ {{- end -}}
+ {{- if or $year $runtime -}}
+
+ {{- if $year }}{{ $year }}{{ end -}}
+ {{- if and $year $runtime }} · {{ end -}}
+ {{- if $runtime }}{{ $runtime }} min{{ end -}}
{{- end -}}
diff --git a/scripts/import_letterboxd.py b/scripts/import_letterboxd.py
index ddbf3da..b5a813d 100755
--- a/scripts/import_letterboxd.py
+++ b/scripts/import_letterboxd.py
@@ -6,13 +6,18 @@ Usage:
python scripts/import_letterboxd.py # Interactive mode - pick from recent
python scripts/import_letterboxd.py --latest # Import most recent entry
python scripts/import_letterboxd.py --list # Just list recent entries
+ python scripts/import_letterboxd.py --theater # Skip to theater questions
+ python scripts/import_letterboxd.py --home # Skip to home video questions
+
+The script will prompt for viewing details (theater vs home) and pre-fill
+the front matter table accordingly.
"""
import argparse
import os
import re
import sys
-from datetime import datetime
+from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlparse
import xml.etree.ElementTree as ET
@@ -129,7 +134,124 @@ def rating_to_stars(rating):
return f"{stars} ({rating})"
-def create_draft_post(movie, tmdb_details, poster_url):
+def prompt_viewing_details():
+ """Prompt user for viewing location details."""
+ print("\nWhere did you watch this?")
+ print(" 1. Theater")
+ print(" 2. Home")
+
+ while True:
+ choice = input("Enter 1 or 2: ").strip()
+ if choice == "1":
+ return prompt_theater_details()
+ elif choice == "2":
+ return prompt_home_details()
+ else:
+ print("Please enter 1 or 2")
+
+
+def prompt_theater_details():
+ """Prompt for theater-specific details."""
+ print("\nWhich theater?")
+ theaters = [
+ ("1", "Gucci", "gucci"),
+ ("2", "Ghost Theater", "ghost-theater"),
+ ("3", "Marcel", "marcel"),
+ ("4", "AMC South", "amc-south"),
+ ("5", "AMC Lakeline", "amc-lakeline"),
+ ("6", "Other", None),
+ ]
+ for num, name, _ in theaters:
+ print(f" {num}. {name}")
+
+ theater_name = ""
+ theater_tag = None
+ while True:
+ choice = input("Enter number: ").strip()
+ for num, name, tag in theaters:
+ if choice == num:
+ if name == "Other":
+ theater_name = input("Theater name: ").strip()
+ else:
+ theater_name = name
+ theater_tag = tag
+ break
+ if theater_name:
+ break
+ print("Please enter a valid number")
+
+ show_time = input("Show time (e.g. 7:30pm): ").strip()
+ theater_num = input("Theater number: ").strip()
+ pizza = input("Pizza? (Yes/No): ").strip() or ""
+ tickets = input("Tickets (e.g. 'At Box Office', 'A-List'): ").strip()
+ crew = input("Crew (e.g. 'Me, Coach T, Science Bro'): ").strip()
+
+ return {
+ "type": "theater",
+ "theater": theater_name,
+ "theater_tag": theater_tag,
+ "show_time": show_time,
+ "theater_num": theater_num,
+ "pizza": pizza,
+ "tickets": tickets,
+ "crew": crew,
+ }
+
+
+def prompt_home_details():
+ """Prompt for home viewing details."""
+ location = input("Location (e.g. 'Living Room', 'Woodrow Apt'): ").strip() or "Home"
+ show_time = input("Show time (optional, e.g. 'evening'): ").strip()
+ pizza = input("Pizza? (Yes/No): ").strip() or "No"
+
+ # Media format
+ print("\nMedia format?")
+ media_options = [
+ ("1", "Online"),
+ ("2", "BluRay"),
+ ("3", "DVD"),
+ ("4", "VHS"),
+ ]
+ for num, name in media_options:
+ print(f" {num}. {name}")
+ media = "Online"
+ media_choice = input("Enter number (default 1): ").strip()
+ for num, name in media_options:
+ if media_choice == num:
+ media = name
+ break
+
+ # Screen type
+ print("\nScreen?")
+ screen_options = [
+ ("1", "4k TV"),
+ ("2", "4k Computer"),
+ ("3", "1080p Computer"),
+ ("4", "Cell Phone"),
+ ("5", "Someone Elses TV"),
+ ]
+ for num, name in screen_options:
+ print(f" {num}. {name}")
+ screen = "4k TV"
+ screen_choice = input("Enter number (default 1): ").strip()
+ for num, name in screen_options:
+ if screen_choice == num:
+ screen = name
+ break
+
+ return {
+ "type": "home",
+ "theater": "Home Video",
+ "theater_tag": "homevideo",
+ "show_time": show_time,
+ "theater_num": location,
+ "pizza": pizza,
+ "media": media,
+ "screen": screen,
+ }
+
+
+def create_draft_post(movie, tmdb_details, poster_url, viewing_details=None):
"""Create a Hugo draft post for the movie."""
slug = slugify(movie["title"])
filename = f"{slug}.md"
@@ -140,7 +262,7 @@ def create_draft_post(movie, tmdb_details, poster_url):
return None
# Format the date for Hugo
- now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# Format watched date nicely
watched = movie["watched_date"]
@@ -156,6 +278,52 @@ def create_draft_post(movie, tmdb_details, poster_url):
imdb_id = tmdb_details.get("imdb_id", "")
rating_display = rating_to_stars(movie["rating"])
+ # Use viewing details if provided, otherwise use empty defaults
+ if viewing_details:
+ show_time = viewing_details.get("show_time", "")
+ theater = viewing_details.get("theater", "")
+ theater_num = viewing_details.get("theater_num", "")
+ pizza = viewing_details.get("pizza", "")
+ is_home = viewing_details.get("type") == "home"
+
+ # Build tags based on viewing type
+ tags = []
+ if viewing_details.get("theater_tag"):
+ tags.append(viewing_details["theater_tag"])
+ tags.extend(["no-expectations"])
+ if pizza.lower() == "yes":
+ tags.append("had pizza")
+ tags_yaml = "\n".join(f" - {tag}" for tag in tags)
+
+ # Different last two rows for home vs theater
+ if is_home:
+ row5_label = "Media"
+ row5_value = viewing_details.get("media", "")
+ row7_label = "Screen"
+ row7_value = viewing_details.get("screen", "")
+ else:
+ row5_label = "Tickets"
+ row5_value = viewing_details.get("tickets", "")
+ row7_label = "Crew"
+ row7_value = viewing_details.get("crew", "")
+ else:
+ show_time = ""
+ theater = ""
+ theater_num = ""
+ pizza = ""
+ row5_label = "Tickets"
+ row5_value = ""
+ row7_label = "Crew"
+ row7_value = ""
+ tags_yaml = """ - gucci
+ - ghost-theater
+ - marcel
+ - amc-south
+ - amc-lakeline
+ - anticipated
+ - no-expectations
+ - had pizza"""
+
# Build the frontmatter and content
content = f'''---
title: '{movie["title"]}'
@@ -166,26 +334,25 @@ summary: ""
imdb: "{imdb_id}"
poster: "{poster_url or ''}"
tags:
- - gucci
- - ghost theater
- - marcel
- - amc-south
- - amc-lakeline
- - anticipated
- - no-expectations
- - had pizza
+{tags_yaml}
+# Mastodon comments: After posting about this on Mastodon, add the post ID below.
+# Get the ID from the end of the toot URL, e.g. https://tilde.zone/@mnw/123456789
+# mastodon_id: ""
+# To block a reply from showing, add its full URL to this list:
+# mastodon_blocked:
+# - "https://tilde.zone/@someone/123456789"
---
{{{{< imdbposter >}}}}
-| Date watched | {watched_display} |
+| Date watched | {watched_display:<17} |
|---------------------|-------------------|
-| Show Time | |
-| Theater | |
-| Theater Number | |
-| Pizza | |
-| Tickets | |
-| Letterboxd Rating | {rating_display} |
-| Crew | |
+| Show Time | {show_time:<17} |
+| Theater | {theater:<17} |
+| Theater Number | {theater_num:<17} |
+| Pizza | {pizza:<17} |
+| {row5_label:<19} | {row5_value:<17} |
+| Letterboxd Rating | {rating_display:<17} |
+| {row7_label:<19} | {row7_value:<17} |
{{{{< /imdbposter >}}}}
@@ -209,12 +376,25 @@ def display_movies(movies, limit=10):
print()
-def import_movie(movie):
- """Import a single movie: fetch details, download poster, create post."""
+def import_movie(movie, viewing_mode=None):
+ """Import a single movie: fetch details, download poster, create post.
+
+ Args:
+ movie: Movie data from Letterboxd RSS
+ viewing_mode: 'theater', 'home', or None (will prompt)
+ """
print(f"\nImporting: {movie['title']} ({movie['year']})")
+ # Get viewing details
+ if viewing_mode == "theater":
+ viewing_details = prompt_theater_details()
+ elif viewing_mode == "home":
+ viewing_details = prompt_home_details()
+ else:
+ viewing_details = prompt_viewing_details()
+
# Get TMDB details
- print(" Fetching TMDB details...")
+ print("\n Fetching TMDB details...")
tmdb = get_tmdb_details(movie["tmdb_id"])
# Download poster
@@ -226,7 +406,7 @@ def import_movie(movie):
# Create draft post
print(" Creating draft post...")
- filepath = create_draft_post(movie, tmdb, poster_url)
+ filepath = create_draft_post(movie, tmdb, poster_url, viewing_details)
if filepath:
print(f"\nDone! Edit your draft at: {filepath.relative_to(PROJECT_ROOT)}")
@@ -241,8 +421,17 @@ def main():
parser.add_argument("--latest", action="store_true", help="Import most recent entry")
parser.add_argument("--list", action="store_true", help="Just list recent entries")
parser.add_argument("--count", type=int, default=10, help="Number of entries to show")
+ parser.add_argument("--theater", action="store_true", help="Skip viewing prompt, go straight to theater questions")
+ parser.add_argument("--home", action="store_true", help="Skip viewing prompt, go straight to home questions")
args = parser.parse_args()
+ # Determine viewing mode from flags
+ viewing_mode = None
+ if args.theater:
+ viewing_mode = "theater"
+ elif args.home:
+ viewing_mode = "home"
+
print("Fetching Letterboxd RSS feed...")
try:
root = fetch_rss()
@@ -260,7 +449,7 @@ def main():
sys.exit(0)
if args.latest:
- import_movie(movies[0])
+ import_movie(movies[0], viewing_mode)
sys.exit(0)
# Interactive mode
@@ -272,7 +461,7 @@ def main():
sys.exit(0)
idx = int(choice) - 1
if 0 <= idx < len(movies):
- import_movie(movies[idx])
+ import_movie(movies[idx], viewing_mode)
else:
print("Invalid selection")
sys.exit(1)
diff --git a/static/css/mastodon-comments.css b/static/css/mastodon-comments.css
new file mode 100644
index 0000000..868e0c0
--- /dev/null
+++ b/static/css/mastodon-comments.css
@@ -0,0 +1,213 @@
+/**
+ * Mastodon Comments Stylesheet
+ *
+ * Teletype / Fax Machine / I Saw the TV Glow aesthetic
+ * Works with various Hugo themes
+ */
+
+.mastodon-comments-section {
+ margin-top: 3rem;
+ padding-top: 2rem;
+ border-top: 2px dashed #888;
+ font-family: 'Courier New', Courier, monospace;
+}
+
+.comments-header {
+ margin: 0 0 1.5rem 0;
+ padding: 0;
+ font-size: 0.85rem;
+ color: #888;
+ background: none;
+ border: none;
+ white-space: pre;
+ overflow-x: auto;
+}
+
+.comments-intro {
+ font-size: 0.9rem;
+ margin-bottom: 1.5rem;
+ letter-spacing: 0.05em;
+}
+
+.comments-intro a {
+ text-decoration: underline;
+}
+
+.comments-note {
+ margin-top: 1rem;
+ color: #666;
+ font-size: 0.8rem;
+}
+
+#mastodon-comments-list {
+ min-height: 50px;
+}
+
+#load-comments-btn {
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 1rem;
+ padding: 0.75rem 1.5rem;
+ background: transparent;
+ border: 2px solid currentColor;
+ cursor: pointer;
+ letter-spacing: 0.1em;
+ transition: all 0.2s ease;
+}
+
+#load-comments-btn:hover {
+ background: #333;
+ color: #fff;
+}
+
+.loading,
+.no-comments,
+.comments-received,
+.comments-error {
+ padding: 1rem;
+ margin: 1rem 0;
+ background: #f5f5f5;
+ border-left: 4px solid #888;
+ font-size: 0.85rem;
+ white-space: pre-wrap;
+}
+
+.comments-error {
+ border-left-color: #c00;
+ color: #900;
+}
+
+.comments-received {
+ border-left-color: #080;
+ color: #060;
+}
+
+/* Individual comment styling */
+.mastodon-comment {
+ margin: 1.5rem 0;
+ padding: 1rem;
+ background: #fafafa;
+ border: 1px solid #ddd;
+}
+
+.mastodon-comment .comment-header pre {
+ margin: 0 0 0.75rem 0;
+ padding: 0;
+ font-size: 0.75rem;
+ color: #666;
+ background: none;
+ border: none;
+ white-space: pre;
+}
+
+.mastodon-comment .comment-author {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: 0.75rem;
+}
+
+.mastodon-comment .avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+}
+
+.mastodon-comment .author-info {
+ display: flex;
+ flex-direction: column;
+}
+
+.mastodon-comment .display-name {
+ font-weight: bold;
+ text-decoration: none;
+}
+
+.mastodon-comment .display-name:hover {
+ text-decoration: underline;
+}
+
+.mastodon-comment .handle {
+ font-size: 0.8rem;
+ color: #666;
+}
+
+.mastodon-comment .emoji {
+ height: 18px;
+ width: 18px;
+ vertical-align: middle;
+}
+
+.mastodon-comment .comment-content {
+ margin: 1rem 0;
+ line-height: 1.5;
+}
+
+.mastodon-comment .comment-content p {
+ margin: 0.5rem 0;
+}
+
+.mastodon-comment .comment-content a {
+ word-break: break-all;
+}
+
+.mastodon-comment .comment-attachments {
+ margin: 1rem 0;
+}
+
+.mastodon-comment .comment-attachments img,
+.mastodon-comment .comment-attachments video {
+ max-width: 100%;
+ max-height: 300px;
+ border: 1px solid #ccc;
+}
+
+.mastodon-comment .comment-meta {
+ font-size: 0.8rem;
+ color: #666;
+ margin-top: 0.75rem;
+ padding-top: 0.5rem;
+ border-top: 1px dashed #ccc;
+}
+
+.mastodon-comment .comment-meta a {
+ text-decoration: none;
+ color: #666;
+}
+
+.mastodon-comment .comment-meta a:hover {
+ text-decoration: underline;
+}
+
+.mastodon-comment .replies {
+ color: #888;
+}
+
+/* Dark mode support */
+@media (prefers-color-scheme: dark) {
+ .mastodon-comment {
+ background: #1a1a1a;
+ border-color: #444;
+ }
+
+ .mastodon-comment .comment-header pre {
+ color: #888;
+ }
+
+ .mastodon-comment .handle,
+ .mastodon-comment .comment-meta,
+ .mastodon-comment .comment-meta a {
+ color: #999;
+ }
+
+ .loading,
+ .no-comments,
+ .comments-received {
+ background: #222;
+ }
+
+ #load-comments-btn:hover {
+ background: #eee;
+ color: #000;
+ }
+}
diff --git a/static/images/posters/urchin.jpg b/static/images/posters/urchin.jpg
new file mode 100644
index 0000000..42484db
Binary files /dev/null and b/static/images/posters/urchin.jpg differ
diff --git a/static/js/mastodon-comments.js b/static/js/mastodon-comments.js
new file mode 100644
index 0000000..5c89312
--- /dev/null
+++ b/static/js/mastodon-comments.js
@@ -0,0 +1,141 @@
+/**
+ * Mastodon Comments Loader
+ *
+ * Fetches and displays replies to a Mastodon post.
+ * Uses DOMPurify for HTML sanitization.
+ *
+ * Inspired by Andreas Scherbaum's implementation.
+ * Aesthetic inspired by I Saw the TV Glow.
+ */
+
+var commentsLoaded = false;
+
+function escapeHtml(unsafe) {
+ if (!unsafe) return '';
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function formatDate(dateStr) {
+ var d = new Date(dateStr);
+ var year = d.getFullYear();
+ var month = String(d.getMonth() + 1).padStart(2, '0');
+ var day = String(d.getDate()).padStart(2, '0');
+ var hours = String(d.getHours()).padStart(2, '0');
+ var mins = String(d.getMinutes()).padStart(2, '0');
+ return year + '-' + month + '-' + day + ' ' + hours + ':' + mins;
+}
+
+function getUserHandle(account) {
+ var handle = '@' + account.acct;
+ if (account.acct.indexOf('@') === -1) {
+ var domain = new URL(account.url);
+ handle += '@' + domain.hostname;
+ }
+ return handle;
+}
+
+function renderComment(toot, depth) {
+ // Skip blocked toots
+ if (blockedToots.includes(toot.url)) {
+ return '';
+ }
+
+ // Process display name with custom emojis
+ var displayName = escapeHtml(toot.account.display_name || toot.account.username);
+ toot.account.emojis.forEach(function(emoji) {
+ var emojiImg = '
';
+ displayName = displayName.replace(':' + emoji.shortcode + ':', emojiImg);
+ });
+
+ var indent = depth > 0 ? ' style="margin-left: ' + (depth * 20) + 'px; border-left: 2px dashed #666;"' : '';
+
+ var html = '';
+
+ return html;
+}
+
+function renderComments(toots, parentId, depth) {
+ var html = '';
+ var replies = toots
+ .filter(function(toot) { return toot.in_reply_to_id === parentId; })
+ .sort(function(a, b) { return a.created_at.localeCompare(b.created_at); });
+
+ replies.forEach(function(toot) {
+ html += renderComment(toot, depth);
+ html += renderComments(toots, toot.id, depth + 1);
+ });
+
+ return html;
+}
+
+function loadMastodonComments() {
+ if (commentsLoaded) return;
+
+ var container = document.getElementById('mastodon-comments-list');
+ container.innerHTML = '\n++ ESTABLISHING CONNECTION TO ' + mastodonHost.toUpperCase() + ' ++\n++ PLEASE STAND BY ++\n
';
+
+ var apiUrl = 'https://' + mastodonHost + '/api/v1/statuses/' + mastodonId + '/context';
+
+ fetch(apiUrl)
+ .then(function(response) {
+ if (!response.ok) throw new Error('Network response was not ok');
+ return response.json();
+ })
+ .then(function(data) {
+ if (data.descendants && data.descendants.length > 0) {
+ var html = '';
+ html += renderComments(data.descendants, mastodonId, 0);
+ container.innerHTML = DOMPurify.sanitize(html, { ADD_ATTR: ['target'] });
+ } else {
+ container.innerHTML = '';
+ }
+ commentsLoaded = true;
+ })
+ .catch(function(error) {
+ container.innerHTML = '';
+ });
+}
diff --git a/static/js/purify.min.js b/static/js/purify.min.js
new file mode 100644
index 0000000..279d73a
--- /dev/null
+++ b/static/js/purify.min.js
@@ -0,0 +1,3 @@
+/*! @license DOMPurify 3.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.1/LICENSE */
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t){for(var n=arguments.length,o=new Array(n>2?n-2:0),r=2;r1?t-1:0),o=1;o1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:h;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function w(e){for(let t=0;t/gm),W=a(/\$\{[\w\W]*/gm),Y=a(/^data-[\-\w.\u00B7-\uFFFF]+$/),j=a(/^aria-[\-\w]+$/),X=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),q=a(/^(?:\w+script|data):/i),$=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),K=a(/^html$/i),V=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var Z=Object.freeze({__proto__:null,ARIA_ATTR:j,ATTR_WHITESPACE:$,CUSTOM_ELEMENT:V,DATA_ATTR:Y,DOCTYPE_NAME:K,ERB_EXPR:G,IS_ALLOWED_URI:X,IS_SCRIPT_OR_DATA:q,MUSTACHE_EXPR:B,TMPLIT_EXPR:W});const J=1,Q=3,ee=7,te=8,ne=9,oe=function(){return"undefined"==typeof window?null:window};var re=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:oe();const o=e=>t(e);if(o.version="3.3.1",o.removed=[],!n||!n.document||n.document.nodeType!==ne||!n.Element)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:D,Element:w,NodeFilter:B,NamedNodeMap:G=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:W,DOMParser:Y,trustedTypes:j}=n,q=w.prototype,$=O(q,"cloneNode"),V=O(q,"remove"),re=O(q,"nextSibling"),ie=O(q,"childNodes"),ae=O(q,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let le,ce="";const{implementation:se,createNodeIterator:ue,createDocumentFragment:me,getElementsByTagName:pe}=r,{importNode:fe}=a;let de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof e&&"function"==typeof ae&&se&&void 0!==se.createHTMLDocument;const{MUSTACHE_EXPR:he,ERB_EXPR:ge,TMPLIT_EXPR:Te,DATA_ATTR:ye,ARIA_ATTR:Ee,IS_SCRIPT_OR_DATA:Ae,ATTR_WHITESPACE:_e,CUSTOM_ELEMENT:be}=Z;let{IS_ALLOWED_URI:Se}=Z,Ne=null;const De=R({},[...v,...x,...L,...I,...U]);let Re=null;const we=R({},[...z,...P,...F,...H]);let Ce=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Oe=null,ve=null;const xe=Object.seal(l(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let Le=!0,ke=!0,Ie=!1,Me=!0,Ue=!1,ze=!0,Pe=!1,Fe=!1,He=!1,Be=!1,Ge=!1,We=!1,Ye=!0,je=!1,Xe=!0,qe=!1,$e={},Ke=null;const Ve=R({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ze=null;const Je=R({},["audio","video","img","source","image","track"]);let Qe=null;const et=R({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),tt="http://www.w3.org/1998/Math/MathML",nt="http://www.w3.org/2000/svg",ot="http://www.w3.org/1999/xhtml";let rt=ot,it=!1,at=null;const lt=R({},[tt,nt,ot],g);let ct=R({},["mi","mo","mn","ms","mtext"]),st=R({},["annotation-xml"]);const ut=R({},["title","style","font","a","script"]);let mt=null;const pt=["application/xhtml+xml","text/html"];let ft=null,dt=null;const ht=r.createElement("form"),gt=function(e){return e instanceof RegExp||e instanceof Function},Tt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!dt||dt!==e){if(e&&"object"==typeof e||(e={}),e=C(e),mt=-1===pt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,ft="application/xhtml+xml"===mt?g:h,Ne=_(e,"ALLOWED_TAGS")?R({},e.ALLOWED_TAGS,ft):De,Re=_(e,"ALLOWED_ATTR")?R({},e.ALLOWED_ATTR,ft):we,at=_(e,"ALLOWED_NAMESPACES")?R({},e.ALLOWED_NAMESPACES,g):lt,Qe=_(e,"ADD_URI_SAFE_ATTR")?R(C(et),e.ADD_URI_SAFE_ATTR,ft):et,Ze=_(e,"ADD_DATA_URI_TAGS")?R(C(Je),e.ADD_DATA_URI_TAGS,ft):Je,Ke=_(e,"FORBID_CONTENTS")?R({},e.FORBID_CONTENTS,ft):Ve,Oe=_(e,"FORBID_TAGS")?R({},e.FORBID_TAGS,ft):C({}),ve=_(e,"FORBID_ATTR")?R({},e.FORBID_ATTR,ft):C({}),$e=!!_(e,"USE_PROFILES")&&e.USE_PROFILES,Le=!1!==e.ALLOW_ARIA_ATTR,ke=!1!==e.ALLOW_DATA_ATTR,Ie=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Me=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Ue=e.SAFE_FOR_TEMPLATES||!1,ze=!1!==e.SAFE_FOR_XML,Pe=e.WHOLE_DOCUMENT||!1,Be=e.RETURN_DOM||!1,Ge=e.RETURN_DOM_FRAGMENT||!1,We=e.RETURN_TRUSTED_TYPE||!1,He=e.FORCE_BODY||!1,Ye=!1!==e.SANITIZE_DOM,je=e.SANITIZE_NAMED_PROPS||!1,Xe=!1!==e.KEEP_CONTENT,qe=e.IN_PLACE||!1,Se=e.ALLOWED_URI_REGEXP||X,rt=e.NAMESPACE||ot,ct=e.MATHML_TEXT_INTEGRATION_POINTS||ct,st=e.HTML_INTEGRATION_POINTS||st,Ce=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&>(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ce.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&>(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ce.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ce.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Ue&&(ke=!1),Ge&&(Be=!0),$e&&(Ne=R({},U),Re=[],!0===$e.html&&(R(Ne,v),R(Re,z)),!0===$e.svg&&(R(Ne,x),R(Re,P),R(Re,H)),!0===$e.svgFilters&&(R(Ne,L),R(Re,P),R(Re,H)),!0===$e.mathMl&&(R(Ne,I),R(Re,F),R(Re,H))),e.ADD_TAGS&&("function"==typeof e.ADD_TAGS?xe.tagCheck=e.ADD_TAGS:(Ne===De&&(Ne=C(Ne)),R(Ne,e.ADD_TAGS,ft))),e.ADD_ATTR&&("function"==typeof e.ADD_ATTR?xe.attributeCheck=e.ADD_ATTR:(Re===we&&(Re=C(Re)),R(Re,e.ADD_ATTR,ft))),e.ADD_URI_SAFE_ATTR&&R(Qe,e.ADD_URI_SAFE_ATTR,ft),e.FORBID_CONTENTS&&(Ke===Ve&&(Ke=C(Ke)),R(Ke,e.FORBID_CONTENTS,ft)),e.ADD_FORBID_CONTENTS&&(Ke===Ve&&(Ke=C(Ke)),R(Ke,e.ADD_FORBID_CONTENTS,ft)),Xe&&(Ne["#text"]=!0),Pe&&R(Ne,["html","head","body"]),Ne.table&&(R(Ne,["tbody"]),delete Oe.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw S('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw S('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');le=e.TRUSTED_TYPES_POLICY,ce=le.createHTML("")}else void 0===le&&(le=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(j,c)),null!==le&&"string"==typeof ce&&(ce=le.createHTML(""));i&&i(e),dt=e}},yt=R({},[...x,...L,...k]),Et=R({},[...I,...M]),At=function(e){f(o.removed,{element:e});try{ae(e).removeChild(e)}catch(t){V(e)}},_t=function(e,t){try{f(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){f(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(Be||Ge)try{At(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},bt=function(e){let t=null,n=null;if(He)e=""+e;else{const t=T(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===mt&&rt===ot&&(e=''+e+"");const o=le?le.createHTML(e):e;if(rt===ot)try{t=(new Y).parseFromString(o,mt)}catch(e){}if(!t||!t.documentElement){t=se.createDocument(rt,"template",null);try{t.documentElement.innerHTML=it?ce:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),rt===ot?pe.call(t,Pe?"html":"body")[0]:Pe?t.documentElement:i},St=function(e){return ue.call(e.ownerDocument||e,e,B.SHOW_ELEMENT|B.SHOW_COMMENT|B.SHOW_TEXT|B.SHOW_PROCESSING_INSTRUCTION|B.SHOW_CDATA_SECTION,null)},Nt=function(e){return e instanceof W&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof G)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Dt=function(e){return"function"==typeof D&&e instanceof D};function Rt(e,t,n){u(e,(e=>{e.call(o,t,n,dt)}))}const wt=function(e){let t=null;if(Rt(de.beforeSanitizeElements,e,null),Nt(e))return At(e),!0;const n=ft(e.nodeName);if(Rt(de.uponSanitizeElement,e,{tagName:n,allowedTags:Ne}),ze&&e.hasChildNodes()&&!Dt(e.firstElementChild)&&b(/<[/\w!]/g,e.innerHTML)&&b(/<[/\w!]/g,e.textContent))return At(e),!0;if(e.nodeType===ee)return At(e),!0;if(ze&&e.nodeType===te&&b(/<[/\w]/g,e.data))return At(e),!0;if(!(xe.tagCheck instanceof Function&&xe.tagCheck(n))&&(!Ne[n]||Oe[n])){if(!Oe[n]&&Ot(n)){if(Ce.tagNameCheck instanceof RegExp&&b(Ce.tagNameCheck,n))return!1;if(Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))return!1}if(Xe&&!Ke[n]){const t=ae(e)||e.parentNode,n=ie(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=$(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,re(e))}}}return At(e),!0}return e instanceof w&&!function(e){let t=ae(e);t&&t.tagName||(t={namespaceURI:rt,tagName:"template"});const n=h(e.tagName),o=h(t.tagName);return!!at[e.namespaceURI]&&(e.namespaceURI===nt?t.namespaceURI===ot?"svg"===n:t.namespaceURI===tt?"svg"===n&&("annotation-xml"===o||ct[o]):Boolean(yt[n]):e.namespaceURI===tt?t.namespaceURI===ot?"math"===n:t.namespaceURI===nt?"math"===n&&st[o]:Boolean(Et[n]):e.namespaceURI===ot?!(t.namespaceURI===nt&&!st[o])&&!(t.namespaceURI===tt&&!ct[o])&&!Et[n]&&(ut[n]||!yt[n]):!("application/xhtml+xml"!==mt||!at[e.namespaceURI]))}(e)?(At(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!b(/<\/no(script|embed|frames)/i,e.innerHTML)?(Ue&&e.nodeType===Q&&(t=e.textContent,u([he,ge,Te],(e=>{t=y(t,e," ")})),e.textContent!==t&&(f(o.removed,{element:e.cloneNode()}),e.textContent=t)),Rt(de.afterSanitizeElements,e,null),!1):(At(e),!0)},Ct=function(e,t,n){if(Ye&&("id"===t||"name"===t)&&(n in r||n in ht))return!1;if(ke&&!ve[t]&&b(ye,t));else if(Le&&b(Ee,t));else if(xe.attributeCheck instanceof Function&&xe.attributeCheck(t,e));else if(!Re[t]||ve[t]){if(!(Ot(e)&&(Ce.tagNameCheck instanceof RegExp&&b(Ce.tagNameCheck,e)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(e))&&(Ce.attributeNameCheck instanceof RegExp&&b(Ce.attributeNameCheck,t)||Ce.attributeNameCheck instanceof Function&&Ce.attributeNameCheck(t,e))||"is"===t&&Ce.allowCustomizedBuiltInElements&&(Ce.tagNameCheck instanceof RegExp&&b(Ce.tagNameCheck,n)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))))return!1}else if(Qe[t]);else if(b(Se,y(n,_e,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==E(n,"data:")||!Ze[e]){if(Ie&&!b(Ae,y(n,_e,"")));else if(n)return!1}else;return!0},Ot=function(e){return"annotation-xml"!==e&&T(e,be)},vt=function(e){Rt(de.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||Nt(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Re,forceKeepAttr:void 0};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=ft(a),m=c;let f="value"===a?m:A(m);if(n.attrName=s,n.attrValue=f,n.keepAttr=!0,n.forceKeepAttr=void 0,Rt(de.uponSanitizeAttribute,e,n),f=n.attrValue,!je||"id"!==s&&"name"!==s||(_t(a,e),f="user-content-"+f),ze&&b(/((--!?|])>)|<\/(style|title|textarea)/i,f)){_t(a,e);continue}if("attributename"===s&&T(f,"href")){_t(a,e);continue}if(n.forceKeepAttr)continue;if(!n.keepAttr){_t(a,e);continue}if(!Me&&b(/\/>/i,f)){_t(a,e);continue}Ue&&u([he,ge,Te],(e=>{f=y(f,e," ")}));const d=ft(e.nodeName);if(Ct(d,s,f)){if(le&&"object"==typeof j&&"function"==typeof j.getAttributeType)if(l);else switch(j.getAttributeType(d,s)){case"TrustedHTML":f=le.createHTML(f);break;case"TrustedScriptURL":f=le.createScriptURL(f)}if(f!==m)try{l?e.setAttributeNS(l,a,f):e.setAttribute(a,f),Nt(e)?At(e):p(o.removed)}catch(t){_t(a,e)}}else _t(a,e)}Rt(de.afterSanitizeAttributes,e,null)},xt=function e(t){let n=null;const o=St(t);for(Rt(de.beforeSanitizeShadowDOM,t,null);n=o.nextNode();)Rt(de.uponSanitizeShadowNode,n,null),wt(n),vt(n),n.content instanceof s&&e(n.content);Rt(de.afterSanitizeShadowDOM,t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(it=!e,it&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Dt(e)){if("function"!=typeof e.toString)throw S("toString is not a function");if("string"!=typeof(e=e.toString()))throw S("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Fe||Tt(t),o.removed=[],"string"==typeof e&&(qe=!1),qe){if(e.nodeName){const t=ft(e.nodeName);if(!Ne[t]||Oe[t])throw S("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof D)n=bt("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===J&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Be&&!Ue&&!Pe&&-1===e.indexOf("<"))return le&&We?le.createHTML(e):e;if(n=bt(e),!n)return Be?null:We?ce:""}n&&He&&At(n.firstChild);const c=St(qe?e:n);for(;i=c.nextNode();)wt(i),vt(i),i.content instanceof s&&xt(i.content);if(qe)return e;if(Be){if(Ge)for(l=me.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Re.shadowroot||Re.shadowrootmode)&&(l=fe.call(a,l,!0)),l}let m=Pe?n.outerHTML:n.innerHTML;return Pe&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&b(K,n.ownerDocument.doctype.name)&&(m="\n"+m),Ue&&u([he,ge,Te],(e=>{m=y(m,e," ")})),le&&We?le.createHTML(m):m},o.setConfig=function(){Tt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Fe=!0},o.clearConfig=function(){dt=null,Fe=!1},o.isValidAttribute=function(e,t,n){dt||Tt({});const o=ft(e),r=ft(t);return Ct(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&f(de[e],t)},o.removeHook=function(e,t){if(void 0!==t){const n=m(de[e],t);return-1===n?void 0:d(de[e],n,1)[0]}return p(de[e])},o.removeHooks=function(e){de[e]=[]},o.removeAllHooks=function(){de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();return re}));
+//# sourceMappingURL=purify.min.js.map
+ ++ TRANSMISSION RECEIVED ++
+ ++ Reply to this post on Mastodon to join the discussion. +
+ // comments loaded from {{ $host }} when you click the button +
+