Initial commit.

This commit is contained in:
2025-12-04 20:29:35 +01:00
commit ae789a5b40
87 changed files with 9798 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
/**
* @fileoverview Date Formatting Utilities for Rantii
* @author retoor <retoor@molodetz.nl>
* @description Date and time formatting functions
* @keywords date, time, format, timestamp, relative
*/
function formatTimestamp(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleString();
}
function formatDate(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleDateString();
}
function formatTime(timestamp) {
const date = new Date(timestamp * 1000);
return date.toLocaleTimeString();
}
function formatRelativeTime(timestamp) {
const now = Date.now();
const time = timestamp * 1000;
const diff = now - time;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const weeks = Math.floor(days / 7);
const months = Math.floor(days / 30);
const years = Math.floor(days / 365);
if (seconds < 60) {
return 'just now';
}
if (minutes < 60) {
return `${minutes}m ago`;
}
if (hours < 24) {
return `${hours}h ago`;
}
if (days < 7) {
return `${days}d ago`;
}
if (weeks < 4) {
return `${weeks}w ago`;
}
if (months < 12) {
return `${months}mo ago`;
}
return `${years}y ago`;
}
function formatFullDate(timestamp) {
const date = new Date(timestamp * 1000);
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
return date.toLocaleDateString(undefined, options);
}
function isToday(timestamp) {
const date = new Date(timestamp * 1000);
const today = new Date();
return date.toDateString() === today.toDateString();
}
function isThisWeek(timestamp) {
const date = new Date(timestamp * 1000);
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
return date >= weekAgo;
}
export {
formatTimestamp,
formatDate,
formatTime,
formatRelativeTime,
formatFullDate,
isToday,
isThisWeek
};
+148
View File
@@ -0,0 +1,148 @@
/**
* @fileoverview Markdown Rendering Wrapper for Rantii
* @author retoor <retoor@molodetz.nl>
* @description Markdown parsing and rendering with syntax highlighting
* @keywords markdown, parsing, syntax, highlight, render
*/
class MarkdownRenderer {
constructor() {
this.marked = null;
this.hljs = null;
this.initialized = false;
}
async init() {
if (this.initialized) {
return true;
}
try {
if (window.marked) {
this.marked = window.marked;
}
if (window.hljs) {
this.hljs = window.hljs;
}
if (this.marked && this.hljs) {
this.marked.setOptions({
highlight: (code, lang) => {
if (lang && this.hljs.getLanguage(lang)) {
try {
return this.hljs.highlight(code, { language: lang }).value;
} catch (e) {
return code;
}
}
return this.hljs.highlightAuto(code).value;
},
breaks: true,
gfm: true
});
} else if (this.marked) {
this.marked.setOptions({
breaks: true,
gfm: true
});
}
this.initialized = true;
return true;
} catch (e) {
return false;
}
}
render(text) {
if (!text) {
return '';
}
if (this.marked) {
try {
return this.marked.parse(text);
} catch (e) {
return this.escapeHtml(text);
}
}
return this.simpleRender(text);
}
simpleRender(text) {
let result = this.escapeHtml(text);
result = result.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
const highlighted = this.highlightCode(code.trim(), lang);
return `<pre><code class="language-${lang || 'plaintext'}">${highlighted}</code></pre>`;
});
result = result.replace(/`([^`]+)`/g, '<code>$1</code>');
result = result.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
result = result.replace(/\*([^*]+)\*/g, '<em>$1</em>');
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
result = result.replace(/\n/g, '<br>');
return result;
}
highlightCode(code, lang) {
if (this.hljs && lang && this.hljs.getLanguage(lang)) {
try {
return this.hljs.highlight(code, { language: lang }).value;
} catch (e) {
return this.escapeHtml(code);
}
}
if (this.hljs) {
try {
return this.hljs.highlightAuto(code).value;
} catch (e) {
return this.escapeHtml(code);
}
}
return this.escapeHtml(code);
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
renderInline(text) {
if (!text) {
return '';
}
let result = this.escapeHtml(text);
result = result.replace(/`([^`]+)`/g, '<code>$1</code>');
result = result.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
result = result.replace(/\*([^*]+)\*/g, '<em>$1</em>');
return result;
}
stripMarkdown(text) {
if (!text) {
return '';
}
let result = text;
result = result.replace(/```[\s\S]*?```/g, '');
result = result.replace(/`([^`]+)`/g, '$1');
result = result.replace(/\*\*([^*]+)\*\*/g, '$1');
result = result.replace(/\*([^*]+)\*/g, '$1');
result = result.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
return result;
}
}
const markdownRenderer = new MarkdownRenderer();
export { MarkdownRenderer, markdownRenderer };
+154
View File
@@ -0,0 +1,154 @@
/**
* @fileoverview Template Utilities for Rantii
* @author retoor <retoor@molodetz.nl>
* @description HTML template creation and manipulation helpers
* @keywords template, html, dom, element, create
*/
function createElement(tag, attributes = {}, children = []) {
const element = document.createElement(tag);
Object.entries(attributes).forEach(([key, value]) => {
if (key === 'className') {
element.className = value;
} else if (key === 'dataset') {
Object.entries(value).forEach(([dataKey, dataValue]) => {
element.dataset[dataKey] = dataValue;
});
} else if (key.startsWith('on') && typeof value === 'function') {
const eventName = key.substring(2).toLowerCase();
element.addEventListener(eventName, value);
} else if (key === 'style' && typeof value === 'object') {
Object.assign(element.style, value);
} else {
element.setAttribute(key, value);
}
});
children.forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else if (child instanceof Node) {
element.appendChild(child);
}
});
return element;
}
function html(strings, ...values) {
const template = document.createElement('template');
template.innerHTML = strings.reduce((result, string, i) => {
const value = values[i - 1];
if (value instanceof Node) {
return result + '<!--node-->' + string;
}
if (Array.isArray(value)) {
return result + value.map(v => v instanceof Node ? '<!--node-->' : String(v)).join('') + string;
}
return result + (value !== undefined ? String(value) : '') + string;
});
return template.content.cloneNode(true);
}
function createFragment(htmlString) {
const template = document.createElement('template');
template.innerHTML = htmlString.trim();
return template.content.cloneNode(true);
}
function clearElement(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
function replaceContent(element, newContent) {
clearElement(element);
if (typeof newContent === 'string') {
element.innerHTML = newContent;
} else if (newContent instanceof Node) {
element.appendChild(newContent);
}
}
function insertBefore(newElement, referenceElement) {
referenceElement.parentNode.insertBefore(newElement, referenceElement);
}
function insertAfter(newElement, referenceElement) {
referenceElement.parentNode.insertBefore(newElement, referenceElement.nextSibling);
}
function removeElement(element) {
if (element && element.parentNode) {
element.parentNode.removeChild(element);
}
}
function toggleClass(element, className, force) {
if (force !== undefined) {
element.classList.toggle(className, force);
} else {
element.classList.toggle(className);
}
}
function addClass(element, ...classNames) {
element.classList.add(...classNames);
}
function removeClass(element, ...classNames) {
element.classList.remove(...classNames);
}
function hasClass(element, className) {
return element.classList.contains(className);
}
function setAttributes(element, attributes) {
Object.entries(attributes).forEach(([key, value]) => {
if (value === null || value === undefined) {
element.removeAttribute(key);
} else {
element.setAttribute(key, value);
}
});
}
function getDataAttributes(element) {
return { ...element.dataset };
}
function show(element) {
element.style.display = '';
element.removeAttribute('hidden');
}
function hide(element) {
element.style.display = 'none';
}
function isVisible(element) {
return element.offsetParent !== null;
}
export {
createElement,
html,
createFragment,
clearElement,
replaceContent,
insertBefore,
insertAfter,
removeElement,
toggleClass,
addClass,
removeClass,
hasClass,
setAttributes,
getDataAttributes,
show,
hide,
isVisible
};
+119
View File
@@ -0,0 +1,119 @@
/**
* @fileoverview URL Utilities for Rantii
* @author retoor <retoor@molodetz.nl>
* @description URL parsing and detection utilities
* @keywords url, link, youtube, image, detection
*/
const URL_REGEX = /https?:\/\/[^\s<]+[^<.,:;"')\]\s]/gi;
const YOUTUBE_REGEX = /(?:youtube\.com\/(?:watch\?v=|embed\/|v\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/i;
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp'];
function extractUrls(text) {
const matches = text.match(URL_REGEX);
return matches || [];
}
function isYoutubeUrl(url) {
return YOUTUBE_REGEX.test(url);
}
function getYoutubeVideoId(url) {
const match = url.match(YOUTUBE_REGEX);
return match ? match[1] : null;
}
function getYoutubeThumbnail(videoId) {
return `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
}
function getYoutubeEmbedUrl(videoId) {
return `https://www.youtube-nocookie.com/embed/${videoId}`;
}
function isImageUrl(url) {
const lower = url.toLowerCase();
return IMAGE_EXTENSIONS.some(ext => lower.includes(ext));
}
function isGifUrl(url) {
return url.toLowerCase().includes('.gif');
}
function extractImageUrls(text) {
return extractUrls(text).filter(isImageUrl);
}
function extractYoutubeUrls(text) {
return extractUrls(text).filter(isYoutubeUrl);
}
function extractNonMediaUrls(text) {
return extractUrls(text).filter(url => !isImageUrl(url) && !isYoutubeUrl(url));
}
function sanitizeUrl(url) {
try {
const parsed = new URL(url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return null;
}
return parsed.href;
} catch {
return null;
}
}
function getDomain(url) {
try {
const parsed = new URL(url);
return parsed.hostname;
} catch {
return null;
}
}
function makeAbsoluteUrl(url, base) {
try {
return new URL(url, base).href;
} catch {
return url;
}
}
function isDevrantImageUrl(url) {
return url.includes('devrant.com') || url.includes('devrant.io');
}
function buildDevrantImageUrl(imagePath) {
if (imagePath.startsWith('http')) {
return imagePath;
}
return `https://img.devrant.com/${imagePath}`;
}
function buildAvatarUrl(avatar) {
if (!avatar || !avatar.i) {
return null;
}
return `https://avatars.devrant.com/${avatar.i}`;
}
export {
extractUrls,
isYoutubeUrl,
getYoutubeVideoId,
getYoutubeThumbnail,
getYoutubeEmbedUrl,
isImageUrl,
isGifUrl,
extractImageUrls,
extractYoutubeUrls,
extractNonMediaUrls,
sanitizeUrl,
getDomain,
makeAbsoluteUrl,
isDevrantImageUrl,
buildDevrantImageUrl,
buildAvatarUrl
};