Initial commit.
This commit is contained in:
@@ -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
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user