import DOMPurify from 'dompurify' import { marked } from 'marked' import yaml from 'js-yaml' import type { EntryWithConfig, EntryWithContent, } from '@goldenwere/mackenzii-types/src/content/templates/shared' /** * Config used for DOMPurify. * This config allows for most HTML elements and a handful of attributes * necessary for the site to load intended markdown/etc */ const domPurifyConfig = { ALLOW_ARIA_ATTR: true, ALLOWED_ATTR: [ 'allowfullscreen', 'allow', 'alt', 'class', 'href', 'id', 'src', 'style', 'title', 'target', // embeds 'subtitle', 'icon', ], ALLOWED_TAGS: [ 'a', 'p', 'span', 'b', 'em', 'i', 'mark', 's', 'sub', 'sup', 'small', 'strike', 'strong', 'u', 'abbr', 'audio', 'blockquote', 'cite', 'code', 'details', 'data', 'dfn', 'div', 'iframe', 'img', 'pre', 'summary', 'samp', 'time', 'track', 'var', 'video', 'figcaption', 'figure', 'br', 'hr', 'wbr', 'ol', 'ul', 'li', 'dd', 'dl', 'dt', 'link-embed', ], } /** * Fetches a file and returns the blob content of the file * @param path the path of the file to load * @returns the blob of the file */ export const fetchAndReturnBlob = async (path: string) => { const response = await fetch(path) const blob = await response.blob() return blob } /** * Fetches and sanitizes text files * @param path the path of the text file to load * @returns the content of the text file after sanitizing */ export const fetchAndReturnText = async (path: string) => { const blob = await fetchAndReturnBlob(path) const text = await blob.text() const sanitized = DOMPurify.sanitize(text, domPurifyConfig) return sanitized } /** * Fetches, sanitizes, and parses YAML files * @param path the path of the YAML file to load * @returns the content of the YAML file after sanitizing then parsing */ export const fetchAndParseYaml = async (path: string) => { const text = await fetchAndReturnText(path) return yaml.load(text) as T } /** * Fetches, sanitizes, and parses Markdown files * @param path the path of the markdown document to load * @returns the content parsed from the markdown document */ export const fetchAndParseMarkdown = async (path: string) => { const document = await fetchAndReturnText(path) return marked.parse(document) } /** * Fetches content for an entry * @param entry the entry whose content should be fetched * @returns the markdown content for the entry */ export const fetchContent = async (entry: EntryWithContent) => { if (!!entry.content) { return entry.content } else if (!!entry.contentUrl) { return fetchAndParseMarkdown(entry.contentUrl) } return '' } /** * Fetches config for an entry * @param entry the entry whose config should be fetched * @returns the config object for the entry */ export const fetchConfig = async (entry: EntryWithConfig) => { if (!!entry.config) { return entry.config as T } else if (!!entry.configUrl) { return fetchAndParseYaml(entry.configUrl) as T } return {} as T } /** * Fetches content type from an image * @param path the path of the file to check * @returns the Content-Type header of the file */ export const getContentType = async (path: string) => { const response = await fetch(path, {method: 'HEAD'}) return response.headers.get('Content-Type') } /** * Determines if a url is a valid image * @param url the url of the image * @returns true if valid image, false otherwise */ export const isValidImage = async (url?: string): Promise => { if (!url) { return false } const contentType = await getContentType(url) return contentType?.startsWith('image') || false } /** * Creates a Promise which waits for a condition to be true * @param condition the condition which should be met * @param interval the period of time (ms) to wait between attempts * @param tries the number of attempts (up to and including this number) to try before determining the condition couldn't be met * @returns A Promise which will resolve when the condition is met or reject if timed out after a period of `(interval * tries)` */ export const awaitCondition = (condition: () => boolean, interval: number = 100, tries: number = 100) => { return new Promise((resolve, reject) => { let _attempts = 1 const wait: any = () => { _attempts += 1 if (condition()) { return resolve() } else if (_attempts > tries) { reject() } else { setTimeout(wait, interval) } } wait() }) } /** * Methods related to local storage */ export const storage = { /** * Retrieves a value from local storage * @param key the key of the local storage item * @returns the parsed object or null */ read: (key: string) => { const value = window.localStorage.getItem(key) return !!value ? JSON.parse(value) as T : null }, /** * Stores a value to local storage * @param key the key of the local storage item * @param value the object to store */ write: (key: string, value: T) => { window.localStorage.setItem(key, JSON.stringify(value)) }, }