diff --git a/lib/util.gd b/lib/util.gd
new file mode 100644
index 00000000..f33c5f11
--- /dev/null
+++ b/lib/util.gd
@@ -0,0 +1,135 @@
+extends Object
+class_name Util
+
+#region HTML RegEx and UnEscape HTML
+static var _img_re: RegEx = RegEx.create_from_string(r'
]*src=["\']([^"\']+)["\'][^>]*/?>')
+static var _h2_open_re: RegEx = RegEx.create_from_string(r'
]*>')
+static var _p_open_re: RegEx = RegEx.create_from_string(r'
]*>')
+static var _ul_re: RegEx = RegEx.create_from_string(r'?ul[^>]*>')
+static var _div_re: RegEx = RegEx.create_from_string(r'?div[^>]*>')
+static var _any_tag_re: RegEx = RegEx.create_from_string(r'<[^>]*>')
+static var _html_replacements: Dictionary[String, String] = {
+ "<": "<",
+ ">": ">",
+ "&": "&",
+ """: "\"",
+ "'": "'",
+ "'": "'",
+ "/": "/",
+ "`": "`",
+ " ": " ",
+ "©": "©",
+ "®": "®",
+ "€": "€",
+ "£": "£",
+ "¥": "¥",
+ "–": "–",
+ "—": "—",
+ "‘": "‘",
+ "’": "’",
+ "“": "“",
+ "”": "”",
+ "…": "…",
+ "•": "•"
+ }
+#endregion
+
+static func convert_html_to_bbcode(html: String) -> String:
+ # NOTE: Unescape HTML entities
+ var bb = html_unescape(html)
+
+ # NOTE: Replace
with [img]...[/img]
+ bb = _img_re.sub(bb, "[img]$1[/img]", true)
+
+ # NOTE: Remove
tags, convert
to newline
+ bb = _h2_open_re.sub(bb, "[b]", true)
+ bb = bb.replace("", "[/b]\n")
+
+ # NOTE: List Items: -> •, -> newline
+ bb = bb.replace("", "• ")
+ bb = bb.replace("", "\n")
+
+ # NOTE: Remove
+ bb = _ul_re.sub(bb, "", true)
+
+ # NOTE: Remove and
+ bb = _div_re.sub(bb, "", true)
+
+ # NOTE: Remove any remaining tags
+ bb = _any_tag_re.sub(bb, "", true)
+
+ # NOTE: Trim and normalize spacing
+ bb = bb.strip_edges()
+ bb = bb.replace("\n\n\n", "\n\n")
+
+ return bb
+
+
+static func html_unescape(text: String) -> String:
+ for key in _html_replacements.keys():
+ text = text.replace(key, _html_replacements[key])
+ return text
+
+static func resize_img_to_max_dim(img: Image, max_dim: int) -> Image:
+ var width: int
+ var height: int
+ var _is_taller: bool = img.get_size().x < img.get_size().y
+ if _is_taller:
+ @warning_ignore("integer_division")
+ width = (max_dim * img.get_size().x) / img.get_size().y
+ height = max_dim
+ else:
+ width = max_dim
+ @warning_ignore("integer_division")
+ height = (max_dim * img.get_size().y) / img.get_size().x
+
+ var resized_image: Image = img.duplicate()
+ resized_image.resize(width, height)
+ return resized_image
+
+#region Time Parsers
+const WEEKDAYS = ["Sun","Mon","Tue", "Wed", "Thu", "Fri", "Sat"]
+const MONTHS_SHORT = ["Jan","Feb","Mar","Apr","May", "Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
+const FORMAT_STRING_TIME = "%d:%02d:%02d"
+const FORMAT_STRING_DATE = "{day} {month} {year}"
+const FORMAT_STRING_DATE_FILE = "{year}_{month}_{day}"
+const FORMAT_STRING_TIME_FILE = "%d_%02d_%02d"
+
+static func unix_to_string(
+ unix: float,
+ include_weekday := true,
+ include_time := true,
+ dz_enabled := true) -> String:
+ if !unix: return "Never"
+ if dz_enabled:
+ var tz_offset_usec = Time.get_time_zone_from_system().bias * 60
+ unix += tz_offset_usec
+ var dict: Dictionary = Time.get_datetime_dict_from_unix_time(floori(unix))
+ dict.weekday = WEEKDAYS[dict.weekday]
+ dict.month = MONTHS_SHORT[dict.month-1]
+
+ var format_string: String = FORMAT_STRING_DATE
+ if include_weekday:
+ format_string = "{weekday} " + format_string
+ if include_time:
+ var time_string: String = (FORMAT_STRING_TIME % [dict.hour, dict.minute, dict.second])
+ format_string = format_string + " - " + time_string
+ return format_string.format(dict)
+
+static func unix_to_string_filepath(unix: float, include_time := false) -> String:
+ if !unix: return "Never"
+
+ var tz_offset_usec = Time.get_time_zone_from_system().bias * 60
+ unix += tz_offset_usec
+ var dict: Dictionary = Time.get_datetime_dict_from_unix_time(floori(unix))
+ dict.weekday = WEEKDAYS[dict.weekday]
+ dict.month = MONTHS_SHORT[dict.month-1]
+
+ var format_string: String = FORMAT_STRING_DATE_FILE.format(dict)
+
+ if include_time:
+ var time_string: String = (FORMAT_STRING_TIME_FILE % [dict.hour, dict.minute, dict.second])
+ format_string += "_" + time_string
+
+ return format_string
+#endregion
diff --git a/lib/util.gd.uid b/lib/util.gd.uid
new file mode 100644
index 00000000..3c3fe472
--- /dev/null
+++ b/lib/util.gd.uid
@@ -0,0 +1 @@
+uid://227p3cajo6lc