diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..dc85c12 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,32 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the main branch + push: + # TODO: Uncomment once released + # branches: [ main ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + - name: Publish to Distributed Press + uses: hyphacoop/actions-distributed-press@v1.1.0 + with: + publish_dir: ./ + dp_url: https://api.distributed.press + refresh_token: ${{ secrets.DISTRIBUTED_PRESS_TOKEN }} + site_url: reader.distributed.press diff --git a/README.md b/README.md index d682cb0..3e20c38 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# reader.distributed.press -Read and follow federated microblogs. +# Social Reader +A P2P and offline-first ActivityPub client for reading and following microblogs on the Fediverse, avoiding dependency on always-online HTTP servers, allowing access to content anytime, anywhere. + +For more information, please visit [docs.distributed.press](https://docs.distributed.press/social-reader). \ No newline at end of file diff --git a/about.css b/about.css new file mode 100644 index 0000000..f9ec7f6 --- /dev/null +++ b/about.css @@ -0,0 +1,36 @@ +.about-container { + flex: 1; + max-width: 600px; + width: 100%; + margin: 0 20px; + margin-top: 10px; +} + +/* Apply general styles to all section elements within about-container */ +.about-container > section { + text-align: left; + color: var(--rdp-text-color); + width: 100%; + margin-bottom: 2rem; +} + +.about-info a, +.faq-section a { + color: var(--rdp-link-color); + text-decoration: underline; +} + +.about-info a:hover, +.faq-section a:hover { + text-decoration: none; +} + +.faq-section details { + margin-bottom: 1rem; + border-bottom: 1px solid var(--rdp-border-color); + padding-bottom: 1rem; +} + +.faq-section summary { + font-weight: bold; +} diff --git a/about.html b/about.html new file mode 100644 index 0000000..4b3f196 --- /dev/null +++ b/about.html @@ -0,0 +1,102 @@ + + + +About Reader + +
+ +
+
+

+ Social Reader is a P2P and offline ActivityPub client for reading and + following microblogs on the + Fediverse. +

+

+ Unlike traditional platforms, Social Reader does not index data on a + server. It empowers you to load public ActivityPub data directly, + turning your device into a personal indexer. This means + your content, your control. +

+

+ Social Reader natively supports content loading over P2P protocols such + as + ipfs:// and + hyper://. This innovation bypasses the need for always-online HTTP servers, + allowing you to access content anytime, anywhere—even offline. +

+

+ Social Reader is built on principles of low-tech; minimal dependencies, + vanilla JavaScript, unminified scripts, and IndexedDB for local data + storage. View and contribute to our open-source code on + GitHub. +

+
+ +
+

FAQs

+
+ How do I create an account on Social Reader? +

+ Social Reader is designed as a reading and following client, which + means you cannot create an account directly within the app. To + actively write and contribute to the Fediverse, you would need to + interact with the + Social Inbox + API. This can be done through platforms like + Sutty CMS or by forking and hosting + your own instance of + Staticpub + repository. +

+
+
+ + Why is Social Reader different from mainstream social platforms? + +

+ Social Reader eliminates the middleman, ensuring direct communication + with your audience without the interference of third-party algorithms. + This ad-free experience prioritizes user autonomy and engagement, + making it ideal for community leaders and organizations seeking + genuine reach and engagement. Unlike traditional social networks where + follower engagement often requires payment, Social Reader and the + broader Fediverse allow for genuine reach and engagement. +

+
+
+ I found a bug. Where do I report it? +

+ If you encounter any issues or have feedback, please file a report on + our + GitHub issues + page. We appreciate your input as it helps us improve Social Reader + for everyone. +

+
+
+
+
+ +
+
+ + + diff --git a/actor-mini-profile.css b/actor-mini-profile.css new file mode 100644 index 0000000..5579e0c --- /dev/null +++ b/actor-mini-profile.css @@ -0,0 +1,32 @@ +.mini-profile { + display: flex; + align-items: center; + text-align: left; + cursor: pointer; + background: none; + border: none; + padding: 0; + margin-bottom: 4px; + color: inherit; + font: inherit; +} + +.profile-mini-icon { + width: 28px; + height: 28px; + border-radius: 50%; + background-color: #000000; + margin-right: 6px; +} + +.profile-mini-name { + color: var(--rdp-text-color); +} + +.profile-followed-date { + text-align: center; + font-size: 0.875rem; + color: var(--rdp-details-color); + margin-left: 34px; + margin-bottom: 6px; +} diff --git a/actor-mini-profile.js b/actor-mini-profile.js new file mode 100644 index 0000000..923cf66 --- /dev/null +++ b/actor-mini-profile.js @@ -0,0 +1,80 @@ +import { db } from './dbInstance.js' + +class ActorMiniProfile extends HTMLElement { + static get observedAttributes () { + return ['url'] + } + + constructor () { + super() + this.url = '' + } + + connectedCallback () { + this.url = this.getAttribute('url') + this.fetchAndRenderActorInfo(this.url) + } + + attributeChangedCallback (name, oldValue, newValue) { + if (name === 'url' && newValue !== oldValue) { + this.url = newValue + this.fetchAndRenderActorInfo(this.url) + } + } + + async fetchAndRenderActorInfo (url) { + try { + const actorInfo = await db.getActor(url) + if (actorInfo) { + this.renderActorInfo(actorInfo) + } + } catch (error) { + console.error('Error fetching actor info:', error) + } + } + + renderActorInfo (actorInfo) { + // Clear existing content + this.innerHTML = '' + + // Container for the icon and name, which should be a button for clickable actions + const clickableContainer = document.createElement('button') + clickableContainer.className = 'mini-profile' + clickableContainer.setAttribute('type', 'button') + + let iconUrl = './assets/profile.png' + if (actorInfo.icon) { + iconUrl = actorInfo.icon.url || (Array.isArray(actorInfo.icon) ? actorInfo.icon[0].url : iconUrl) + } + + // Actor icon + const img = document.createElement('img') + img.className = 'profile-mini-icon' + img.src = iconUrl + img.alt = actorInfo.name ? actorInfo.name : 'Actor icon' + clickableContainer.appendChild(img) + + // Actor name + if (actorInfo.name) { + const pName = document.createElement('div') + pName.classList.add('profile-mini-name') + pName.textContent = actorInfo.name + clickableContainer.appendChild(pName) + } + + // Append the clickable container + this.appendChild(clickableContainer) + + // Add click event to the clickable container for navigation + clickableContainer.addEventListener('click', () => { + window.location.href = `/profile.html?actor=${encodeURIComponent(this.url)}` + }) + + const pDate = document.createElement('span') + pDate.classList.add('profile-followed-date') + pDate.textContent = ` - Followed At: ${this.getAttribute('followed-at')}` + this.appendChild(pDate) + } +} + +customElements.define('actor-mini-profile', ActorMiniProfile) diff --git a/actor-profile.css b/actor-profile.css new file mode 100644 index 0000000..e50fbc9 --- /dev/null +++ b/actor-profile.css @@ -0,0 +1,101 @@ +.profile { + margin-top: 20px; +} + +.profile-container { + text-align: center; +} + +.distributed-post-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.profile-icon { + width: 50px; + height: 50px; + border-radius: 50%; + background-color: #000000; + margin-right: 8px; + margin-bottom: 8px; +} + +.profile-details { + display: flex; + flex-direction: column; +} + +.profile-name { + color: var(--rdp-text-color); + font-weight: bold; +} + +.profile-username { + color: var(--rdp-text-color); + margin-top: 1px; +} + +.profile-summary { + color: var(--rdp-details-color); + width: 500px; + margin-left: auto; + margin-right: auto; + margin-top: 8px; + margin-bottom: 10px; + overflow-wrap: break-word; +} + +follow-button { + appearance: none; + border: 1px solid var(--rdp-border-color); + border-radius: 4px; + box-shadow: rgba(27, 31, 35, 0.1) 0 1px 0; + box-sizing: border-box; + cursor: pointer; + display: inline-block; + font-family: inherit; + font-size: inherit; + font-weight: 600; + line-height: 20px; + padding: 4px 16px; + position: relative; + text-align: center; + text-decoration: none; + touch-action: manipulation; + vertical-align: middle; + white-space: nowrap; +} + +follow-button[state="follow"], +follow-button[state="unfollow"] { + color: #fff; +} + +follow-button[state="follow"] { + background-color: #3b82f6; +} +follow-button[state="follow"]:hover { + background-color: #2563eb; +} + +follow-button[state="unfollow"] { + background-color: #ef4444; +} +follow-button[state="unfollow"]:hover { + background-color: #dc2626; +} + +.actor-profile { + flex: 1; + max-width: 600px; + width: 100%; + margin: 0 20px; +} + +@media (max-width: 768px) { + .profile-summary { + width: 100%; + } +} diff --git a/actor-profile.js b/actor-profile.js new file mode 100644 index 0000000..d04a676 --- /dev/null +++ b/actor-profile.js @@ -0,0 +1,159 @@ +import { db } from './dbInstance.js' + +class ActorProfile extends HTMLElement { + static get observedAttributes () { + return ['url'] + } + + constructor () { + super() + this.url = '' + } + + connectedCallback () { + this.url = this.getAttribute('url') + this.fetchAndRenderActorProfile(this.url) + } + + async fetchAndRenderActorProfile (url) { + try { + const actorInfo = await db.getActor(url) + if (actorInfo) { + this.renderActorProfile(actorInfo) + } else { + this.renderError('Actor information not found') + } + } catch (error) { + console.error('Error fetching actor info:', error) + this.renderError('An error occurred while fetching actor information.') + } + } + + renderActorProfile (actorInfo) { + // Clear existing content + this.innerHTML = '' + + const profileContainer = document.createElement('div') + profileContainer.classList.add('profile') + + // Create a container for the actor icon and name, to center them + const actorContainer = document.createElement('div') + actorContainer.classList.add('profile-container') + + // Handle both single icon object and array of icons + let iconUrl = './assets/profile.png' // Default profile image path + if (actorInfo.icon) { + if (Array.isArray(actorInfo.icon) && actorInfo.icon.length > 0) { + iconUrl = actorInfo.icon[0].url + } else if (actorInfo.icon.url) { + iconUrl = actorInfo.icon.url + } + } + + const img = document.createElement('img') + img.classList.add('profile-icon') + img.src = iconUrl + img.alt = actorInfo.name ? actorInfo.name : 'Actor icon' + actorContainer.appendChild(img) // Append to the actor container + + if (actorInfo.name) { + const pName = document.createElement('div') + pName.classList.add('profile-name') + pName.textContent = actorInfo.name + actorContainer.appendChild(pName) // Append to the actor container + } + + if (actorInfo.preferredUsername) { + const pUserName = document.createElement('a') + pUserName.classList.add('profile-username') + pUserName.href = db.getObjectPage(actorInfo) + pUserName.textContent = `@${actorInfo.preferredUsername}` + actorContainer.appendChild(pUserName) // Append to the actor container + } + + if (actorInfo.summary) { + const pUserSummary = document.createElement('div') + pUserSummary.classList.add('profile-summary') + pUserSummary.textContent = `${actorInfo.summary}` + actorContainer.appendChild(pUserSummary) // Append to the actor container + } + + // Instead of creating a button, create a FollowButton component + const followButton = document.createElement('follow-button') + followButton.setAttribute('url', this.url) + actorContainer.appendChild(followButton) + + // Append the actor container to the profile container + profileContainer.appendChild(actorContainer) + + // Create the distributed-outbox component and append it to the profile container + const distributedOutbox = document.createElement('distributed-outbox') + profileContainer.appendChild(distributedOutbox) + + // Append the profile container to the main component + this.appendChild(profileContainer) + + // Update distributed-outbox URL based on fetched actorInfo + distributedOutbox.setAttribute( + 'url', + actorInfo.outbox + ) + this.dispatchEvent(new CustomEvent('outboxUpdated', { bubbles: true })) + } + + renderError (message) { + this.innerHTML = '' // Clear existing content + const errorComponent = document.createElement('error-message') + errorComponent.setAttribute('message', message) + this.appendChild(errorComponent) + } +} + +customElements.define('actor-profile', ActorProfile) + +class FollowButton extends HTMLElement { + static get observedAttributes () { + return ['url'] + } + + constructor () { + super() + this.url = this.getAttribute('url') || '' + this.state = 'unknown' + } + + connectedCallback () { + this.updateState() + this.render() + this.addEventListener('click', this.toggleFollowState.bind(this)) + } + + attributeChangedCallback (name, oldValue, newValue) { + if (name === 'url' && newValue !== oldValue) { + this.url = newValue + this.updateState() + } + } + + async updateState () { + const isFollowed = await db.isActorFollowed(this.url) + this.state = isFollowed ? 'unfollow' : 'follow' + this.render() + } + + async toggleFollowState () { + if (this.state === 'follow') { + await db.followActor(this.url) + } else if (this.state === 'unfollow') { + await db.unfollowActor(this.url) + } + this.updateState() + } + + render () { + this.textContent = this.state === 'follow' ? 'Follow' : 'Unfollow' + this.setAttribute('state', this.state) + } +} + +customElements.define('follow-button', FollowButton) diff --git a/assets/profile.png b/assets/profile.png new file mode 100644 index 0000000..6dfa7b6 Binary files /dev/null and b/assets/profile.png differ diff --git a/common.css b/common.css new file mode 100644 index 0000000..2dbeca8 --- /dev/null +++ b/common.css @@ -0,0 +1,83 @@ +html { + background: var(--bg-color); + font-family: var(--rdp-font); +} + +/* Main styles */ +img, +video { + max-width: 100%; +} + +.container { + display: flex; + justify-content: space-between; + max-width: 1200px; + width: 100%; + margin-top: 20px; + position: relative; +} + +.load-more-btn-container { + text-align: center; + margin-top: 15px; + margin-bottom: 15px; +} + +.load-more-btn { + color: #000; + background-color: #fff; + appearance: none; + border: 1px solid var(--rdp-border-color); + border-radius: 4px; + box-shadow: rgba(27, 31, 35, 0.1) 0 1px 0; + box-sizing: border-box; + cursor: pointer; + display: inline-block; + font-family: inherit; + font-size: inherit; + font-weight: 400; + line-height: 20px; + padding: 4px 12px; + position: relative; + text-align: center; + text-decoration: none; + touch-action: manipulation; + vertical-align: middle; + white-space: nowrap; +} + +.load-more-btn:hover { + background-color: #f3f4f6; +} + +/* Empty right column for balance */ +.right-column { + flex: 0 0 200px; +} + +@media screen and (max-width: 1280px) { + .right-column { + flex: 0 0 100px; + } +} + +@media screen and (max-width: 768px) { + .container { + flex-direction: column; + align-items: center; + } + + .main-content { + width: 80%; + max-width: 100%; + margin-top: 175px; + } + .actor-profile{ + width: 100%; + } + + sidebar-nav { + width: 100%; + } +} diff --git a/db.js b/db.js new file mode 100644 index 0000000..f09b0c6 --- /dev/null +++ b/db.js @@ -0,0 +1,571 @@ +/* globals DOMParser */ +import { openDB } from './dependencies/idb/index.js' + +export const DEFAULT_DB = 'default' +export const ACTORS_STORE = 'actors' +export const NOTES_STORE = 'notes' +export const ACTIVITIES_STORE = 'activities' +export const FOLLOWED_ACTORS_STORE = 'followedActors' +export const DEFAULT_LIMIT = 32 + +export const ID_FIELD = 'id' +export const URL_FIELD = 'url' +export const CREATED_FIELD = 'created' +export const UPDATED_FIELD = 'updated' +export const PUBLISHED_FIELD = 'published' +export const TO_FIELD = 'to' +export const CC_FIELD = 'cc' +export const IN_REPLY_TO_FIELD = 'inReplyTo' +export const TAG_NAMES_FIELD = 'tag_names' +export const ATTRIBUTED_TO_FIELD = 'attributedTo' +export const CONVERSATION_FIELD = 'conversation' +export const ACTOR_FIELD = 'actor' + +export const PUBLISHED_SUFFIX = ', published' + +export const TYPE_CREATE = 'Create' +export const TYPE_UPDATE = 'Update' +export const TYPE_NOTE = 'Note' +export const TYPE_DELETE = 'Delete' + +export const HYPER_PREFIX = 'hyper://' +export const IPNS_PREFIX = 'ipns://' + +const ACCEPT_HEADER = +'application/activity+json, application/ld+json, application/json, text/html' + +// TODO: When ingesting notes and actors, wrap any dates in `new Date()` +// TODO: When ingesting notes add a "tag_names" field which is just the names of the tag +// TODO: When ingesting notes, also load their replies + +export function isP2P (url) { + return url.startsWith(HYPER_PREFIX) || url.startsWith(IPNS_PREFIX) +} + +export class ActivityPubDB extends EventTarget { + constructor (db, fetch = globalThis.fetch) { + super() + this.db = db + this.fetch = fetch + } + + static async load (name = DEFAULT_DB, fetch = globalThis.fetch) { + const db = await openDB(name, 2, { + upgrade + }) + + return new ActivityPubDB(db, fetch) + } + + resolveURL (url) { + // TODO: Check if mention + return this.#get(url) + } + + getObjectPage (data) { + if (typeof data === 'string') return data + const { url, id } = data + + if (!url) return id + if (typeof url === 'string') return url + if (Array.isArray(url)) { + const firstLink = url.find((item) => (typeof item === 'string') || item.href) + if (firstLink) return firstLink.href || firstLink + } else if (url.href) { + return url.href + } + return id + } + + #fetch (...args) { + const { fetch } = this + return fetch(...args) + } + + #gateWayFetch (url, options = {}) { + let gatewayUrl = url + // TODO: Don't hardcode the gateway + if (url.startsWith(HYPER_PREFIX)) { + gatewayUrl = url.replace(HYPER_PREFIX, 'https://hyper.hypha.coop/hyper/') + } else if (url.startsWith(IPNS_PREFIX)) { + gatewayUrl = url.replace(IPNS_PREFIX, 'https://ipfs.hypha.coop/ipns/') + } + + return this.#fetch(gatewayUrl, options) + } + + #proxiedFetch (url, options = {}) { + const proxiedURL = 'https://corsproxy.io/?' + encodeURIComponent(url) + return this.#fetch(proxiedURL, options) + } + + async #get (url) { + if (url && typeof url === 'object') { + return url + } + let response + // Try fetching directly for all URLs (including P2P URLs) + // TODO: Signed fetch + try { + response = await this.#fetch(url, { + headers: { + Accept: ACCEPT_HEADER + } + }) + } catch (error) { + if (isP2P(url)) { + // Maybe the browser can't load p2p URLs + response = await this.#gateWayFetch(url, { + headers: { + Accept: ACCEPT_HEADER + } + }) + } else { + // Try the proxy, maybe it's cors? + response = await this.#proxiedFetch(url, { + headers: { + Accept: ACCEPT_HEADER + } + }) + } + } + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}.\n${url}\n${await response.text()}`) + } + if (isResponseHTML(response)) { + const jsonLdUrl = await getResponseLink(response) + if (jsonLdUrl) return this.#get(jsonLdUrl) + // No JSON-LD link found in HTML + throw new Error('No JSON-LD link found in the response') + } + + return await response.json() + } + + async getActor (url) { + // TODO: Try to load from cache + const actor = await this.#get(url) + this.db.put(ACTORS_STORE, actor) + return this.db.get(ACTORS_STORE, actor.id) + } + + async getAllActors () { + const tx = this.db.transaction(ACTORS_STORE) + const actors = [] + + for await (const cursor of tx.store) { + actors.push(cursor.value) + } + + return actors + } + + async getNote (url) { + try { + const note = await this.db.get(NOTES_STORE, url) + if (!note) throw new Error('Not loaded') + return note + } catch { + const note = await this.#get(url) + await this.ingestNote(note) + return note + } + } + + async getActivity (url) { + try { + return this.db.get(ACTIVITIES_STORE, url) + } catch { + const activity = await this.#get(url) + await this.ingestActivity(activity) + return activity + } + } + + async * searchActivities (actor, { limit = DEFAULT_LIMIT, skip = 0 } = {}) { + const indexName = ACTOR_FIELD + ', published' + const tx = this.db.transaction(ACTIVITIES_STORE, 'read') + const index = tx.store.index(indexName) + + let count = 0 + + for await (const cursor of index.iterate(actor)) { + if (count === 0) { + cursor.advance(skip) + } + yield cursor.value + count++ + if (count >= limit) break + } + + await tx.done() + } + + async * searchNotes ({ attributedTo } = {}, { skip = 0, limit = DEFAULT_LIMIT, sort = -1 } = {}) { + const tx = this.db.transaction(NOTES_STORE, 'readonly') + let count = 0 + const direction = sort > 0 ? 'next' : 'prev' // 'prev' for descending order + let cursor = null + + const indexName = attributedTo ? ATTRIBUTED_TO_FIELD + ', published' : PUBLISHED_FIELD + + const index = tx.store.index(indexName) + + if (attributedTo) { + cursor = await index.openCursor([attributedTo], direction) + } else { + cursor = await index.openCursor(null, direction) + } + + // Skip the required entries + if (skip) await cursor.advance(skip) + + // Collect the required limit of entries + while (cursor) { + if (count >= limit) break + count++ + yield cursor.value + cursor = await cursor.continue() + } + + await tx.done + } + + async ingestActor (url, isInitial = false) { + console.log(`Starting ingestion for actor from URL: ${url}`) + const actor = await this.getActor(url) + console.log('Actor received:', actor) + + // If actor has an 'outbox', ingest it as a collection + if (actor.outbox) { + await this.ingestActivityCollection(actor.outbox, actor.id, isInitial) + } else { + console.error(`No outbox found for actor at URL ${url}`) + } + + // This is where we might add more features to our actor ingestion process. + // e.g., if (actor.followers) { ... } + } + + async ingestActivityCollection (collectionOrUrl, actorId, isInitial = false) { + console.log( + `Fetching collection for actor ID ${actorId}:`, + collectionOrUrl + ) + const sort = isInitial ? -1 : 1 + + const cursor = this.iterateCollection(collectionOrUrl, { + limit: Infinity, + sort + }) + + for await (const activity of cursor) { + // Assume newest items will be first + const wasNew = await this.ingestActivity(activity, actorId) + if (!wasNew) { + console.log('Caught up with', actorId || collectionOrUrl) + break + } + } + } + + async * iterateCollection (collectionOrUrl, { skip = 0, limit = DEFAULT_LIMIT, sort = 1 } = {}) { + const collection = await this.#get(collectionOrUrl) + + let items = collection.orderedItems || collection.items || [] + let next, prev + + if (sort === -1) { + items = items.reverse() + prev = collection.last // Start from the last page if sorting in descending order + } else { + next = collection.first // Start from the first page if sorting in ascending order + } + + let toSkip = skip + let count = 0 + + if (items) { + for await (const item of this.#getAll(items)) { + if (toSkip > 0) { + toSkip-- + } else { + yield item + count++ + if (count >= limit) return + } + } + } + + // Iterate through pages in the specified order + while (sort === -1 ? prev : next) { + const page = await this.#get(sort === -1 ? prev : next) + next = page.next + prev = page.prev + items = page.orderedItems || page.items + + if (sort === -1) { + items = items.reverse() + } + + for await (const item of this.#getAll(items)) { + if (toSkip > 0) { + toSkip-- + } else { + yield item + count++ + if (count >= limit) return + } + } + } + } + + async * #getAll (items) { + for (const itemOrUrl of items) { + const item = await this.#get(itemOrUrl) + + if (item) { + yield item + } + } + } + + async ingestActivity (activity) { + // Check if the activity has an 'id' and create one if it does not + if (!activity.id) { + if (typeof activity.object === 'string') { + // Use the URL of the object as the id for the activity + activity.id = activity.object + } else { + console.error( + 'Activity does not have an ID and cannot be processed:', + activity + ) + return // Skip this activity + } + } + + const existing = await this.db.get(ACTIVITIES_STORE, activity.id) + if (existing) return false + + // Convert the published date to a Date object + activity.published = new Date(activity.published) + + // Store the activity in the ACTIVITIES_STORE + console.log('Ingesting activity:', activity) + await this.db.put(ACTIVITIES_STORE, activity) + + if (activity.type === TYPE_CREATE || activity.type === TYPE_UPDATE) { + const note = await this.#get(activity.object) + if (note.type === TYPE_NOTE) { + console.log('Ingesting note:', note) + await this.ingestNote(note) + } + } else if (activity.type === TYPE_DELETE) { + // Handle 'Delete' activity type + await this.deleteNote(activity.object) + } + + return true + } + + async ingestNote (note) { + console.log('Ingesting note', note) + // Convert needed fields to date + note.published = new Date(note.published) + // Add tag_names field + note.tag_names = (note.tags || []).map(({ name }) => name) + // Try to retrieve an existing note from the database + const existingNote = await this.db.get(NOTES_STORE, note.id) + console.log(existingNote) + // If there's an existing note and the incoming note is newer, update it + if (existingNote && new Date(note.published) > new Date(existingNote.published)) { + console.log(`Updating note with newer version: ${note.id}`) + await this.db.put(NOTES_STORE, note) + } else if (!existingNote) { + // If no existing note, just add the new note + console.log(`Adding new note: ${note.id}`) + await this.db.put(NOTES_STORE, note) + } + // If the existing note is newer, do not replace it + // TODO: Loop through replies + } + + async deleteNote (url) { + // delete note using the url as the `id` from the notes store + this.db.delete(NOTES_STORE, url) + } + + // Method to follow an actor + async followActor (url) { + const followedAt = new Date() + await this.db.put(FOLLOWED_ACTORS_STORE, { url, followedAt }) + + await this.ingestActor(url, true) + console.log(`Followed actor: ${url} at ${followedAt}`) + this.dispatchEvent(new CustomEvent('actorFollowed', { detail: { url, followedAt } })) + } + + // Method to unfollow an actor + async unfollowActor (url) { + await this.db.delete(FOLLOWED_ACTORS_STORE, url) + await this.purgeActor(url) + console.log(`Unfollowed and purged actor: ${url}`) + this.dispatchEvent(new CustomEvent('actorUnfollowed', { detail: { url } })) + } + + async purgeActor (url) { + // First, remove the actor from the ACTORS_STORE + const actor = await this.getActor(url) + if (actor) { + await this.db.delete(ACTORS_STORE, actor.id) + console.log(`Removed actor: ${url}`) + } + + // Remove all activities related to this actor from the ACTIVITIES_STORE using async iteration + const activitiesTx = this.db.transaction(ACTIVITIES_STORE, 'readwrite') + const activitiesStore = activitiesTx.objectStore(ACTIVITIES_STORE) + const activitiesIndex = activitiesStore.index(ACTOR_FIELD) + + for await (const cursor of activitiesIndex.iterate(actor.id)) { + await activitiesStore.delete(cursor.primaryKey) + } + + await activitiesTx.done + console.log(`Removed all activities related to actor: ${url}`) + + // Additionally, remove notes associated with the actor's activities using async iteration + const notesTx = this.db.transaction(NOTES_STORE, 'readwrite') + const notesStore = notesTx.objectStore(NOTES_STORE) + const notesIndex = notesStore.index(ATTRIBUTED_TO_FIELD) + + for await (const cursor of notesIndex.iterate(actor.id)) { + await notesStore.delete(cursor.primaryKey) + } + + await notesTx.done + console.log(`Removed all notes related to actor: ${url}`) + } + + // Method to retrieve all followed actors + async getFollowedActors () { + const tx = this.db.transaction(FOLLOWED_ACTORS_STORE, 'readonly') + const store = tx.objectStore(FOLLOWED_ACTORS_STORE) + const followedActors = [] + for await (const cursor of store) { + followedActors.push(cursor.value) + } + return followedActors + } + + // Method to check if an actor is followed + async isActorFollowed (url) { + try { + const record = await this.db.get(FOLLOWED_ACTORS_STORE, url) + return !!record // Convert the record to a boolean indicating if the actor is followed + } catch (error) { + console.error(`Error checking if actor is followed: ${url}`, error) + return false // Assume not followed if there's an error + } + } + + async hasFollowedActors () { + const followedActors = await this.getFollowedActors() + return followedActors.length > 0 + } + + async setTheme (themeName) { + await this.db.put('settings', { key: 'theme', value: themeName }) + } + + async getTheme () { + const themeSetting = await this.db.get('settings', 'theme') + return themeSetting ? themeSetting.value : null + } +} + +function upgrade (db) { + const actors = db.createObjectStore(ACTORS_STORE, { + keyPath: 'id', + autoIncrement: false + }) + + actors.createIndex(CREATED_FIELD, CREATED_FIELD) + actors.createIndex(UPDATED_FIELD, UPDATED_FIELD) + actors.createIndex(URL_FIELD, URL_FIELD) + + db.createObjectStore(FOLLOWED_ACTORS_STORE, { + keyPath: 'url' + }) + + const notes = db.createObjectStore(NOTES_STORE, { + keyPath: 'id', + autoIncrement: false + }) + notes.createIndex(ATTRIBUTED_TO_FIELD, ATTRIBUTED_TO_FIELD, { unique: false }) + notes.createIndex(PUBLISHED_FIELD, PUBLISHED_FIELD, { unique: false }) + addRegularIndex(notes, TO_FIELD) + addRegularIndex(notes, URL_FIELD) + addRegularIndex(notes, TAG_NAMES_FIELD, { multiEntry: true }) + addSortedIndex(notes, IN_REPLY_TO_FIELD) + addSortedIndex(notes, ATTRIBUTED_TO_FIELD) + addSortedIndex(notes, CONVERSATION_FIELD) + addSortedIndex(notes, TO_FIELD) + + const activities = db.createObjectStore(ACTIVITIES_STORE, { + keyPath: 'id', + autoIncrement: false + }) + activities.createIndex(ACTOR_FIELD, ACTOR_FIELD) + addSortedIndex(activities, ACTOR_FIELD) + addSortedIndex(activities, TO_FIELD) + addRegularIndex(activities, PUBLISHED_FIELD) + + function addRegularIndex (store, field, options = {}) { + store.createIndex(field, field, options) + } + function addSortedIndex (store, field, options = {}) { + store.createIndex(field + ', published', [field, PUBLISHED_FIELD], options) + } + + db.createObjectStore('settings', { keyPath: 'key' }) +} + +// TODO: prefer p2p alternate links when possible +async function getResponseLink (response) { +// For HTML responses, look for the link in the HTTP headers + const linkHeader = response.headers.get('Link') + if (linkHeader) { + const matches = linkHeader.match( + /<([^>]+)>;\s*rel="alternate";\s*type="application\/ld\+json"/ + ) + if (matches && matches[1]) { + // Found JSON-LD link in headers, fetch that URL + return matches[1] + } + } + // If no link header or alternate JSON-LD link is found, or response is HTML without JSON-LD link, process as HTML + const htmlContent = await response.text() + const jsonLdUrl = await parsePostHtml(htmlContent) + + return jsonLdUrl +} + +async function parsePostHtml (htmlContent) { + const parser = new DOMParser() + const doc = parser.parseFromString(htmlContent, 'text/html') + const alternateLinks = doc.querySelectorAll('link[rel="alternate"]') + console.log(...alternateLinks) + for (const link of alternateLinks) { + if (!link.type) continue + if (link.type.includes('application/ld+json') || link.type.includes('application/activity+json')) { + return link.href + } + } + return null +} + +function isResponseHTML (response) { + return response.headers.get('content-type').includes('text/html') +} diff --git a/dbInstance.js b/dbInstance.js new file mode 100644 index 0000000..25a7914 --- /dev/null +++ b/dbInstance.js @@ -0,0 +1,3 @@ +import { ActivityPubDB } from './db.js' + +export const db = await ActivityPubDB.load() diff --git a/dependencies/dompurify/purify.js b/dependencies/dompurify/purify.js new file mode 100644 index 0000000..2319962 --- /dev/null +++ b/dependencies/dompurify/purify.js @@ -0,0 +1,1516 @@ +/*! @license DOMPurify 3.0.9 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.9/LICENSE */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + ? module.exports = factory() + : typeof define === 'function' && define.amd + ? define(factory) + : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.DOMPurify = factory()) +})(this, function () { + 'use strict' + + const { + entries, + setPrototypeOf, + isFrozen, + getPrototypeOf, + getOwnPropertyDescriptor + } = Object + let { + freeze, + seal, + create + } = Object // eslint-disable-line import/no-mutable-exports + let { + apply, + construct + } = typeof Reflect !== 'undefined' && Reflect + if (!freeze) { + freeze = function freeze (x) { + return x + } + } + if (!seal) { + seal = function seal (x) { + return x + } + } + if (!apply) { + apply = function apply (fun, thisValue, args) { + return fun.apply(thisValue, args) + } + } + if (!construct) { + construct = function construct (Func, args) { + return new Func(...args) + } + } + const arrayForEach = unapply(Array.prototype.forEach) + const arrayPop = unapply(Array.prototype.pop) + const arrayPush = unapply(Array.prototype.push) + const stringToLowerCase = unapply(String.prototype.toLowerCase) + const stringToString = unapply(String.prototype.toString) + const stringMatch = unapply(String.prototype.match) + const stringReplace = unapply(String.prototype.replace) + const stringIndexOf = unapply(String.prototype.indexOf) + const stringTrim = unapply(String.prototype.trim) + const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty) + const regExpTest = unapply(RegExp.prototype.test) + const typeErrorCreate = unconstruct(TypeError) + + /** + * Creates a new function that calls the given function with a specified thisArg and arguments. + * + * @param {Function} func - The function to be wrapped and called. + * @returns {Function} A new function that calls the given function with a specified thisArg and arguments. + */ + function unapply (func) { + return function (thisArg) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key] + } + return apply(func, thisArg, args) + } + } + + /** + * Creates a new function that constructs an instance of the given constructor function with the provided arguments. + * + * @param {Function} func - The constructor function to be wrapped and called. + * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments. + */ + function unconstruct (func) { + return function () { + for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2] + } + return construct(func, args) + } + } + + /** + * Add properties to a lookup table + * + * @param {Object} set - The set to which elements will be added. + * @param {Array} array - The array containing elements to be added to the set. + * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set. + * @returns {Object} The modified set with added elements. + */ + function addToSet (set, array) { + const transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase + if (setPrototypeOf) { + // Make 'in' and truthy checks like Boolean(set.constructor) + // independent of any properties defined on Object.prototype. + // Prevent prototype setters from intercepting set as a this value. + setPrototypeOf(set, null) + } + let l = array.length + while (l--) { + let element = array[l] + if (typeof element === 'string') { + const lcElement = transformCaseFunc(element) + if (lcElement !== element) { + // Config presets (e.g. tags.js, attrs.js) are immutable. + if (!isFrozen(array)) { + array[l] = lcElement + } + element = lcElement + } + } + set[element] = true + } + return set + } + + /** + * Clean up an array to harden against CSPP + * + * @param {Array} array - The array to be cleaned. + * @returns {Array} The cleaned version of the array + */ + function cleanArray (array) { + for (let index = 0; index < array.length; index++) { + const isPropertyExist = objectHasOwnProperty(array, index) + if (!isPropertyExist) { + array[index] = null + } + } + return array + } + + /** + * Shallow clone an object + * + * @param {Object} object - The object to be cloned. + * @returns {Object} A new object that copies the original. + */ + function clone (object) { + const newObject = create(null) + for (const [property, value] of entries(object)) { + const isPropertyExist = objectHasOwnProperty(object, property) + if (isPropertyExist) { + if (Array.isArray(value)) { + newObject[property] = cleanArray(value) + } else if (value && typeof value === 'object' && value.constructor === Object) { + newObject[property] = clone(value) + } else { + newObject[property] = value + } + } + } + return newObject + } + + /** + * This method automatically checks if the prop is function or getter and behaves accordingly. + * + * @param {Object} object - The object to look up the getter function in its prototype chain. + * @param {String} prop - The property name for which to find the getter function. + * @returns {Function} The getter function found in the prototype chain or a fallback function. + */ + function lookupGetter (object, prop) { + while (object !== null) { + const desc = getOwnPropertyDescriptor(object, prop) + if (desc) { + if (desc.get) { + return unapply(desc.get) + } + if (typeof desc.value === 'function') { + return unapply(desc.value) + } + } + object = getPrototypeOf(object) + } + function fallbackValue () { + return null + } + return fallbackValue + } + + const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']) + + // SVG + const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']) + const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']) + + // List of SVG elements that are disallowed by default. + // We still need to know them so that we can do namespace + // checks properly in case one wants to add them to + // allow-list. + const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']) + const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']) + + // Similarly to SVG, we want to know all MathML elements, + // even those that we disallow by default. + const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']) + const text = freeze(['#text']) + + const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'xmlns', 'slot']) + const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']) + const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']) + const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']) + + // eslint-disable-next-line unicorn/better-regex + const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm) // Specify template detection regex for SAFE_FOR_TEMPLATES mode + const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm) + const TMPLIT_EXPR = seal(/\${[\w\W]*}/gm) + const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/) // eslint-disable-line no-useless-escape + const ARIA_ATTR = seal(/^aria-[\-\w]+$/) // eslint-disable-line no-useless-escape + const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape + ) + + const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i) + const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex + ) + + const DOCTYPE_NAME = seal(/^html$/i) + + const EXPRESSIONS = /* #__PURE__ */Object.freeze({ + __proto__: null, + MUSTACHE_EXPR, + ERB_EXPR, + TMPLIT_EXPR, + DATA_ATTR, + ARIA_ATTR, + IS_ALLOWED_URI, + IS_SCRIPT_OR_DATA, + ATTR_WHITESPACE, + DOCTYPE_NAME + }) + + const getGlobal = function getGlobal () { + return typeof window === 'undefined' ? null : window + } + + /** + * Creates a no-op policy for internal use only. + * Don't export this function outside this module! + * @param {TrustedTypePolicyFactory} trustedTypes The policy factory. + * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix). + * @return {TrustedTypePolicy} The policy created (or null, if Trusted Types + * are not supported or creating the policy failed). + */ + const _createTrustedTypesPolicy = function _createTrustedTypesPolicy (trustedTypes, purifyHostElement) { + if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') { + return null + } + + // Allow the callers to control the unique policy name + // by adding a data-tt-policy-suffix to the script element with the DOMPurify. + // Policy creation with duplicate names throws in Trusted Types. + let suffix = null + const ATTR_NAME = 'data-tt-policy-suffix' + if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) { + suffix = purifyHostElement.getAttribute(ATTR_NAME) + } + const policyName = 'dompurify' + (suffix ? '#' + suffix : '') + try { + return trustedTypes.createPolicy(policyName, { + createHTML (html) { + return html + }, + createScriptURL (scriptUrl) { + return scriptUrl + } + }) + } catch (_) { + // Policy creation failed (most likely another DOMPurify script has + // already run). Skip creating the policy, as this will only cause errors + // if TT are enforced. + console.warn('TrustedTypes policy ' + policyName + ' could not be created.') + return null + } + } + function createDOMPurify () { + const window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal() + const DOMPurify = root => createDOMPurify(root) + + /** + * Version label, exposed for easier checks + * if DOMPurify is up to date or not + */ + DOMPurify.version = '3.0.9' + + /** + * Array of elements that DOMPurify removed during sanitation. + * Empty if nothing was removed. + */ + DOMPurify.removed = [] + if (!window || !window.document || window.document.nodeType !== 9) { + // Not running in a browser, provide a factory function + // so that you can pass your own Window + DOMPurify.isSupported = false + return DOMPurify + } + let { + document + } = window + const originalDocument = document + const currentScript = originalDocument.currentScript + const { + DocumentFragment, + HTMLTemplateElement, + Node, + Element, + NodeFilter, + NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap, + HTMLFormElement, + DOMParser, + trustedTypes + } = window + const ElementPrototype = Element.prototype + const cloneNode = lookupGetter(ElementPrototype, 'cloneNode') + const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling') + const getChildNodes = lookupGetter(ElementPrototype, 'childNodes') + const getParentNode = lookupGetter(ElementPrototype, 'parentNode') + + // As per issue #47, the web-components registry is inherited by a + // new document created via createHTMLDocument. As per the spec + // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) + // a new empty registry is used when creating a template contents owner + // document, so we use that as our parent document to ensure nothing + // is inherited. + if (typeof HTMLTemplateElement === 'function') { + const template = document.createElement('template') + if (template.content && template.content.ownerDocument) { + document = template.content.ownerDocument + } + } + let trustedTypesPolicy + let emptyHTML = '' + const { + implementation, + createNodeIterator, + createDocumentFragment, + getElementsByTagName + } = document + const { + importNode + } = originalDocument + let hooks = {} + + /** + * Expose whether this browser supports running the full DOMPurify. + */ + DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined + const { + MUSTACHE_EXPR, + ERB_EXPR, + TMPLIT_EXPR, + DATA_ATTR, + ARIA_ATTR, + IS_SCRIPT_OR_DATA, + ATTR_WHITESPACE + } = EXPRESSIONS + let { + IS_ALLOWED_URI: IS_ALLOWED_URI$1 + } = EXPRESSIONS + + /** + * We consider the elements and attributes below to be safe. Ideally + * don't add any new ones but feel free to remove unwanted ones. + */ + + /* allowed element names */ + let ALLOWED_TAGS = null + const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]) + + /* Allowed attribute names */ + let ALLOWED_ATTR = null + const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]) + + /* + * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements. + * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) + * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) + * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. + */ + let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, { + tagNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + attributeNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + allowCustomizedBuiltInElements: { + writable: true, + configurable: false, + enumerable: true, + value: false + } + })) + + /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ + let FORBID_TAGS = null + + /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ + let FORBID_ATTR = null + + /* Decide if ARIA attributes are okay */ + let ALLOW_ARIA_ATTR = true + + /* Decide if custom data attributes are okay */ + let ALLOW_DATA_ATTR = true + + /* Decide if unknown protocols are okay */ + let ALLOW_UNKNOWN_PROTOCOLS = false + + /* Decide if self-closing tags in attributes are allowed. + * Usually removed due to a mXSS issue in jQuery 3.0 */ + let ALLOW_SELF_CLOSE_IN_ATTR = true + + /* Output should be safe for common template engines. + * This means, DOMPurify removes data attributes, mustaches and ERB + */ + let SAFE_FOR_TEMPLATES = false + + /* Decide if document with ... should be returned */ + let WHOLE_DOCUMENT = false + + /* Track whether config is already set on this instance of DOMPurify. */ + let SET_CONFIG = false + + /* Decide if all elements (e.g. style, script) must be children of + * document.body. By default, browsers might move them to document.head */ + let FORCE_BODY = false + + /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html + * string (or a TrustedHTML object if Trusted Types are supported). + * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead + */ + let RETURN_DOM = false + + /* Decide if a DOM `DocumentFragment` should be returned, instead of a html + * string (or a TrustedHTML object if Trusted Types are supported) */ + let RETURN_DOM_FRAGMENT = false + + /* Try to return a Trusted Type object instead of a string, return a string in + * case Trusted Types are not supported */ + let RETURN_TRUSTED_TYPE = false + + /* Output should be free from DOM clobbering attacks? + * This sanitizes markups named with colliding, clobberable built-in DOM APIs. + */ + let SANITIZE_DOM = true + + /* Achieve full DOM Clobbering protection by isolating the namespace of named + * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. + * + * HTML/DOM spec rules that enable DOM Clobbering: + * - Named Access on Window (§7.3.3) + * - DOM Tree Accessors (§3.1.5) + * - Form Element Parent-Child Relations (§4.10.3) + * - Iframe srcdoc / Nested WindowProxies (§4.8.5) + * - HTMLCollection (§4.2.10.2) + * + * Namespace isolation is implemented by prefixing `id` and `name` attributes + * with a constant string, i.e., `user-content-` + */ + let SANITIZE_NAMED_PROPS = false + const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-' + + /* Keep element content when removing element? */ + let KEEP_CONTENT = true + + /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead + * of importing it into a new Document and returning a sanitized copy */ + let IN_PLACE = false + + /* Allow usage of profiles like html, svg and mathMl */ + let USE_PROFILES = {} + + /* Tags to ignore content of when KEEP_CONTENT is true */ + let FORBID_CONTENTS = null + const DEFAULT_FORBID_CONTENTS = addToSet({}, ['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']) + + /* Tags that are safe for data: URIs */ + let DATA_URI_TAGS = null + const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']) + + /* Attributes safe for values like "javascript:" */ + let URI_SAFE_ATTRIBUTES = null + const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']) + const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML' + const SVG_NAMESPACE = 'http://www.w3.org/2000/svg' + const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml' + /* Document namespace */ + let NAMESPACE = HTML_NAMESPACE + let IS_EMPTY_INPUT = false + + /* Allowed XHTML+XML namespaces */ + let ALLOWED_NAMESPACES = null + const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString) + + /* Parsing of strict XHTML documents */ + let PARSER_MEDIA_TYPE = null + const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'] + const DEFAULT_PARSER_MEDIA_TYPE = 'text/html' + let transformCaseFunc = null + + /* Keep a reference to config to pass to hooks */ + let CONFIG = null + + /* Ideally, do not touch anything below this line */ + /* ______________________________________________ */ + + const formElement = document.createElement('form') + const isRegexOrFunction = function isRegexOrFunction (testValue) { + return testValue instanceof RegExp || testValue instanceof Function + } + + /** + * _parseConfig + * + * @param {Object} cfg optional config literal + */ + // eslint-disable-next-line complexity + const _parseConfig = function _parseConfig () { + let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {} + if (CONFIG && CONFIG === cfg) { + return + } + + /* Shield configuration object from tampering */ + if (!cfg || typeof cfg !== 'object') { + cfg = {} + } + + /* Shield configuration object from prototype pollution */ + cfg = clone(cfg) + PARSER_MEDIA_TYPE = + // eslint-disable-next-line unicorn/prefer-includes + SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE + + // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. + transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase + + /* Set configuration parameters */ + ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS + ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR + ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES + URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), + // eslint-disable-line indent + cfg.ADD_URI_SAFE_ATTR, + // eslint-disable-line indent + transformCaseFunc // eslint-disable-line indent + ) // eslint-disable-line indent + : DEFAULT_URI_SAFE_ATTRIBUTES + DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), + // eslint-disable-line indent + cfg.ADD_DATA_URI_TAGS, + // eslint-disable-line indent + transformCaseFunc // eslint-disable-line indent + ) // eslint-disable-line indent + : DEFAULT_DATA_URI_TAGS + FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS + FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : {} + FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : {} + USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false + ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false // Default true + ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false // Default true + ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false // Default false + ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false // Default true + SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false // Default false + WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false // Default false + RETURN_DOM = cfg.RETURN_DOM || false // Default false + RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false // Default false + RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false // Default false + FORCE_BODY = cfg.FORCE_BODY || false // Default false + SANITIZE_DOM = cfg.SANITIZE_DOM !== false // Default true + SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false // Default false + KEEP_CONTENT = cfg.KEEP_CONTENT !== false // Default true + IN_PLACE = cfg.IN_PLACE || false // Default false + IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI + NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE + CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {} + if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) { + CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck + } + if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) { + CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck + } + if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') { + CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements + } + if (SAFE_FOR_TEMPLATES) { + ALLOW_DATA_ATTR = false + } + if (RETURN_DOM_FRAGMENT) { + RETURN_DOM = true + } + + /* Parse profile info */ + if (USE_PROFILES) { + ALLOWED_TAGS = addToSet({}, text) + ALLOWED_ATTR = [] + if (USE_PROFILES.html === true) { + addToSet(ALLOWED_TAGS, html$1) + addToSet(ALLOWED_ATTR, html) + } + if (USE_PROFILES.svg === true) { + addToSet(ALLOWED_TAGS, svg$1) + addToSet(ALLOWED_ATTR, svg) + addToSet(ALLOWED_ATTR, xml) + } + if (USE_PROFILES.svgFilters === true) { + addToSet(ALLOWED_TAGS, svgFilters) + addToSet(ALLOWED_ATTR, svg) + addToSet(ALLOWED_ATTR, xml) + } + if (USE_PROFILES.mathMl === true) { + addToSet(ALLOWED_TAGS, mathMl$1) + addToSet(ALLOWED_ATTR, mathMl) + addToSet(ALLOWED_ATTR, xml) + } + } + + /* Merge configuration parameters */ + if (cfg.ADD_TAGS) { + if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { + ALLOWED_TAGS = clone(ALLOWED_TAGS) + } + addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc) + } + if (cfg.ADD_ATTR) { + if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { + ALLOWED_ATTR = clone(ALLOWED_ATTR) + } + addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc) + } + if (cfg.ADD_URI_SAFE_ATTR) { + addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) + } + if (cfg.FORBID_CONTENTS) { + if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { + FORBID_CONTENTS = clone(FORBID_CONTENTS) + } + addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc) + } + + /* Add #text in case KEEP_CONTENT is set to true */ + if (KEEP_CONTENT) { + ALLOWED_TAGS['#text'] = true + } + + /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ + if (WHOLE_DOCUMENT) { + addToSet(ALLOWED_TAGS, ['html', 'head', 'body']) + } + + /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ + if (ALLOWED_TAGS.table) { + addToSet(ALLOWED_TAGS, ['tbody']) + delete FORBID_TAGS.tbody + } + if (cfg.TRUSTED_TYPES_POLICY) { + if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') { + throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.') + } + if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') { + throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.') + } + + // Overwrite existing TrustedTypes policy. + trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY + + // Sign local variables required by `sanitize`. + emptyHTML = trustedTypesPolicy.createHTML('') + } else { + // Uninitialized policy, attempt to initialize the internal dompurify policy. + if (trustedTypesPolicy === undefined) { + trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript) + } + + // If creating the internal policy succeeded sign internal variables. + if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') { + emptyHTML = trustedTypesPolicy.createHTML('') + } + } + + // Prevent further manipulation of configuration. + // Not available in IE8, Safari 5, etc. + if (freeze) { + freeze(cfg) + } + CONFIG = cfg + } + const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']) + const HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'desc', 'title', 'annotation-xml']) + + // Certain elements are allowed in both SVG and HTML + // namespace. We need to specify them explicitly + // so that they don't get erroneously deleted from + // HTML namespace. + const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']) + + /* Keep track of all possible SVG and MathML tags + * so that we can perform the namespace checks + * correctly. */ + const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]) + const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]) + + /** + * @param {Element} element a DOM element whose namespace is being checked + * @returns {boolean} Return false if the element has a + * namespace that a spec-compliant parser would never + * return. Return true otherwise. + */ + const _checkValidNamespace = function _checkValidNamespace (element) { + let parent = getParentNode(element) + + // In JSDOM, if we're inside shadow DOM, then parentNode + // can be null. We just simulate parent in this case. + if (!parent || !parent.tagName) { + parent = { + namespaceURI: NAMESPACE, + tagName: 'template' + } + } + const tagName = stringToLowerCase(element.tagName) + const parentTagName = stringToLowerCase(parent.tagName) + if (!ALLOWED_NAMESPACES[element.namespaceURI]) { + return false + } + if (element.namespaceURI === SVG_NAMESPACE) { + // The only way to switch from HTML namespace to SVG + // is via . If it happens via any other tag, then + // it should be killed. + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName === 'svg' + } + + // The only way to switch from MathML to SVG is via` + // svg if parent is either or MathML + // text integration points. + if (parent.namespaceURI === MATHML_NAMESPACE) { + return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) + } + + // We only allow elements that are defined in SVG + // spec. All others are disallowed in SVG namespace. + return Boolean(ALL_SVG_TAGS[tagName]) + } + if (element.namespaceURI === MATHML_NAMESPACE) { + // The only way to switch from HTML namespace to MathML + // is via . If it happens via any other tag, then + // it should be killed. + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName === 'math' + } + + // The only way to switch from SVG to MathML is via + // and HTML integration points + if (parent.namespaceURI === SVG_NAMESPACE) { + return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName] + } + + // We only allow elements that are defined in MathML + // spec. All others are disallowed in MathML namespace. + return Boolean(ALL_MATHML_TAGS[tagName]) + } + if (element.namespaceURI === HTML_NAMESPACE) { + // The only way to switch from SVG to HTML is via + // HTML integration points, and from MathML to HTML + // is via MathML text integration points + if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { + return false + } + if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { + return false + } + + // We disallow tags that are specific for MathML + // or SVG and should never appear in HTML namespace + return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]) + } + + // For XHTML and XML documents that support custom namespaces + if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { + return true + } + + // The code should never reach this place (this means + // that the element somehow got namespace that is not + // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). + // Return false just in case. + return false + } + + /** + * _forceRemove + * + * @param {Node} node a DOM node + */ + const _forceRemove = function _forceRemove (node) { + arrayPush(DOMPurify.removed, { + element: node + }) + try { + // eslint-disable-next-line unicorn/prefer-dom-node-remove + node.parentNode.removeChild(node) + } catch (_) { + node.remove() + } + } + + /** + * _removeAttribute + * + * @param {String} name an Attribute name + * @param {Node} node a DOM node + */ + const _removeAttribute = function _removeAttribute (name, node) { + try { + arrayPush(DOMPurify.removed, { + attribute: node.getAttributeNode(name), + from: node + }) + } catch (_) { + arrayPush(DOMPurify.removed, { + attribute: null, + from: node + }) + } + node.removeAttribute(name) + + // We void attribute values for unremovable "is"" attributes + if (name === 'is' && !ALLOWED_ATTR[name]) { + if (RETURN_DOM || RETURN_DOM_FRAGMENT) { + try { + _forceRemove(node) + } catch (_) {} + } else { + try { + node.setAttribute(name, '') + } catch (_) {} + } + } + } + + /** + * _initDocument + * + * @param {String} dirty a string of dirty markup + * @return {Document} a DOM, filled with the dirty markup + */ + const _initDocument = function _initDocument (dirty) { + /* Create a HTML document */ + let doc = null + let leadingWhitespace = null + if (FORCE_BODY) { + dirty = '' + dirty + } else { + /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ + const matches = stringMatch(dirty, /^[\r\n\t ]+/) + leadingWhitespace = matches && matches[0] + } + if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { + // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) + dirty = '' + dirty + '' + } + const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty + /* + * Use the DOMParser API by default, fallback later if needs be + * DOMParser not work for svg when has multiple root element. + */ + if (NAMESPACE === HTML_NAMESPACE) { + try { + doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE) + } catch (_) {} + } + + /* Use createHTMLDocument in case DOMParser is not available */ + if (!doc || !doc.documentElement) { + doc = implementation.createDocument(NAMESPACE, 'template', null) + try { + doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload + } catch (_) { + // Syntax error if dirtyPayload is invalid xml + } + } + const body = doc.body || doc.documentElement + if (dirty && leadingWhitespace) { + body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null) + } + + /* Work on whole document or just its body */ + if (NAMESPACE === HTML_NAMESPACE) { + return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0] + } + return WHOLE_DOCUMENT ? doc.documentElement : body + } + + /** + * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document. + * + * @param {Node} root The root element or node to start traversing on. + * @return {NodeIterator} The created NodeIterator + */ + const _createNodeIterator = function _createNodeIterator (root) { + return createNodeIterator.call(root.ownerDocument || root, root, + // eslint-disable-next-line no-bitwise + NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, null) + } + + /** + * _isClobbered + * + * @param {Node} elm element to check for clobbering attacks + * @return {Boolean} true if clobbered, false if safe + */ + const _isClobbered = function _isClobbered (elm) { + return elm instanceof HTMLFormElement && (typeof elm.nodeName !== 'string' || typeof elm.textContent !== 'string' || typeof elm.removeChild !== 'function' || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== 'function' || typeof elm.setAttribute !== 'function' || typeof elm.namespaceURI !== 'string' || typeof elm.insertBefore !== 'function' || typeof elm.hasChildNodes !== 'function') + } + + /** + * Checks whether the given object is a DOM node. + * + * @param {Node} object object to check whether it's a DOM node + * @return {Boolean} true is object is a DOM node + */ + const _isNode = function _isNode (object) { + return typeof Node === 'function' && object instanceof Node + } + + /** + * _executeHook + * Execute user configurable hooks + * + * @param {String} entryPoint Name of the hook's entry point + * @param {Node} currentNode node to work on with the hook + * @param {Object} data additional hook parameters + */ + const _executeHook = function _executeHook (entryPoint, currentNode, data) { + if (!hooks[entryPoint]) { + return + } + arrayForEach(hooks[entryPoint], hook => { + hook.call(DOMPurify, currentNode, data, CONFIG) + }) + } + + /** + * _sanitizeElements + * + * @protect nodeName + * @protect textContent + * @protect removeChild + * + * @param {Node} currentNode to check for permission to exist + * @return {Boolean} true if node was killed, false if left alive + */ + const _sanitizeElements = function _sanitizeElements (currentNode) { + let content = null + + /* Execute a hook if present */ + _executeHook('beforeSanitizeElements', currentNode, null) + + /* Check if element is clobbered or can clobber */ + if (_isClobbered(currentNode)) { + _forceRemove(currentNode) + return true + } + + /* Now let's check the element's type and name */ + const tagName = transformCaseFunc(currentNode.nodeName) + + /* Execute a hook if present */ + _executeHook('uponSanitizeElement', currentNode, { + tagName, + allowedTags: ALLOWED_TAGS + }) + + /* Detect mXSS attempts abusing namespace confusion */ + if (currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) { + _forceRemove(currentNode) + return true + } + + /* Remove element if anything forbids its presence */ + if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { + /* Check if we have a custom element to handle */ + if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) { + if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) { + return false + } + if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) { + return false + } + } + + /* Keep content except for bad-listed elements */ + if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { + const parentNode = getParentNode(currentNode) || currentNode.parentNode + const childNodes = getChildNodes(currentNode) || currentNode.childNodes + if (childNodes && parentNode) { + const childCount = childNodes.length + for (let i = childCount - 1; i >= 0; --i) { + parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode)) + } + } + } + _forceRemove(currentNode) + return true + } + + /* Check whether element has a valid namespace */ + if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) { + _forceRemove(currentNode) + return true + } + + /* Make sure that older browsers don't get fallback-tag mXSS */ + if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) { + _forceRemove(currentNode) + return true + } + + /* Sanitize element content to be template-safe */ + if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) { + /* Get the element's text content */ + content = currentNode.textContent + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + content = stringReplace(content, expr, ' ') + }) + if (currentNode.textContent !== content) { + arrayPush(DOMPurify.removed, { + element: currentNode.cloneNode() + }) + currentNode.textContent = content + } + } + + /* Execute a hook if present */ + _executeHook('afterSanitizeElements', currentNode, null) + return false + } + + /** + * _isValidAttribute + * + * @param {string} lcTag Lowercase tag name of containing element. + * @param {string} lcName Lowercase attribute name. + * @param {string} value Attribute value. + * @return {Boolean} Returns true if `value` is valid, otherwise false. + */ + // eslint-disable-next-line complexity + const _isValidAttribute = function _isValidAttribute (lcTag, lcName, value) { + /* Make sure attribute cannot clobber */ + if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) { + return false + } + + /* Allow valid data-* attributes: At least one character after "-" + (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) + XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) + We don't need to check the value; it's always URI safe. */ + if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) { + if ( + // First condition does a very basic check if a) it's basically a valid custom element tagname AND + // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck + // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck + _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) || + // Alternative, second condition checks if it's an `is`-attribute, AND + // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck + lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else { + return false + } + /* Check value is safe. First, is attr inert? If so, is safe */ + } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) { + return false + } else ; + return true + } + + /** + * _isBasicCustomElement + * checks if at least one dash is included in tagName, and it's not the first char + * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name + * + * @param {string} tagName name of the tag of the node to sanitize + * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false. + */ + const _isBasicCustomElement = function _isBasicCustomElement (tagName) { + return tagName !== 'annotation-xml' && tagName.indexOf('-') > 0 + } + + /** + * _sanitizeAttributes + * + * @protect attributes + * @protect nodeName + * @protect removeAttribute + * @protect setAttribute + * + * @param {Node} currentNode to sanitize + */ + const _sanitizeAttributes = function _sanitizeAttributes (currentNode) { + /* Execute a hook if present */ + _executeHook('beforeSanitizeAttributes', currentNode, null) + const { + attributes + } = currentNode + + /* Check if we have attributes; if not we might have a text node */ + if (!attributes) { + return + } + const hookEvent = { + attrName: '', + attrValue: '', + keepAttr: true, + allowedAttributes: ALLOWED_ATTR + } + let l = attributes.length + + /* Go backwards over all attributes; safely remove bad ones */ + while (l--) { + const attr = attributes[l] + const { + name, + namespaceURI, + value: attrValue + } = attr + const lcName = transformCaseFunc(name) + let value = name === 'value' ? attrValue : stringTrim(attrValue) + + /* Execute a hook if present */ + hookEvent.attrName = lcName + hookEvent.attrValue = value + hookEvent.keepAttr = true + hookEvent.forceKeepAttr = undefined // Allows developers to see this is a property they can set + _executeHook('uponSanitizeAttribute', currentNode, hookEvent) + value = hookEvent.attrValue + /* Did the hooks approve of the attribute? */ + if (hookEvent.forceKeepAttr) { + continue + } + + /* Remove attribute */ + _removeAttribute(name, currentNode) + + /* Did the hooks approve of the attribute? */ + if (!hookEvent.keepAttr) { + continue + } + + /* Work around a security issue in jQuery 3.0 */ + if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) { + _removeAttribute(name, currentNode) + continue + } + + /* Sanitize attribute content to be template-safe */ + if (SAFE_FOR_TEMPLATES) { + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + value = stringReplace(value, expr, ' ') + }) + } + + /* Is `value` valid for this attribute? */ + const lcTag = transformCaseFunc(currentNode.nodeName) + if (!_isValidAttribute(lcTag, lcName, value)) { + continue + } + + /* Full DOM Clobbering protection via namespace isolation, + * Prefix id and name attributes with `user-content-` + */ + if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) { + // Remove the attribute with this value + _removeAttribute(name, currentNode) + + // Prefix the value and later re-create the attribute with the sanitized value + value = SANITIZE_NAMED_PROPS_PREFIX + value + } + + /* Handle attributes that require Trusted Types */ + if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') { + if (namespaceURI) ; else { + switch (trustedTypes.getAttributeType(lcTag, lcName)) { + case 'TrustedHTML': + { + value = trustedTypesPolicy.createHTML(value) + break + } + case 'TrustedScriptURL': + { + value = trustedTypesPolicy.createScriptURL(value) + break + } + } + } + } + + /* Handle invalid data-* attribute set by try-catching it */ + try { + if (namespaceURI) { + currentNode.setAttributeNS(namespaceURI, name, value) + } else { + /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */ + currentNode.setAttribute(name, value) + } + arrayPop(DOMPurify.removed) + } catch (_) {} + } + + /* Execute a hook if present */ + _executeHook('afterSanitizeAttributes', currentNode, null) + } + + /** + * _sanitizeShadowDOM + * + * @param {DocumentFragment} fragment to iterate over recursively + */ + const _sanitizeShadowDOM = function _sanitizeShadowDOM (fragment) { + let shadowNode = null + const shadowIterator = _createNodeIterator(fragment) + + /* Execute a hook if present */ + _executeHook('beforeSanitizeShadowDOM', fragment, null) + while (shadowNode = shadowIterator.nextNode()) { + /* Execute a hook if present */ + _executeHook('uponSanitizeShadowNode', shadowNode, null) + + /* Sanitize tags and elements */ + if (_sanitizeElements(shadowNode)) { + continue + } + + /* Deep shadow DOM detected */ + if (shadowNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(shadowNode.content) + } + + /* Check attributes, sanitize if necessary */ + _sanitizeAttributes(shadowNode) + } + + /* Execute a hook if present */ + _executeHook('afterSanitizeShadowDOM', fragment, null) + } + + /** + * Sanitize + * Public method providing core sanitation functionality + * + * @param {String|Node} dirty string or DOM node + * @param {Object} cfg object + */ + // eslint-disable-next-line complexity + DOMPurify.sanitize = function (dirty) { + const cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {} + let body = null + let importedNode = null + let currentNode = null + let returnNode = null + /* Make sure we have a string to sanitize. + DO NOT return early, as this will return the wrong type if + the user has requested a DOM object rather than a string */ + IS_EMPTY_INPUT = !dirty + if (IS_EMPTY_INPUT) { + dirty = '' + } + + /* Stringify, in case dirty is an object */ + if (typeof dirty !== 'string' && !_isNode(dirty)) { + if (typeof dirty.toString === 'function') { + dirty = dirty.toString() + if (typeof dirty !== 'string') { + throw typeErrorCreate('dirty is not a string, aborting') + } + } else { + throw typeErrorCreate('toString is not a function') + } + } + + /* Return dirty HTML if DOMPurify cannot run */ + if (!DOMPurify.isSupported) { + return dirty + } + + /* Assign config vars */ + if (!SET_CONFIG) { + _parseConfig(cfg) + } + + /* Clean up removed elements */ + DOMPurify.removed = [] + + /* Check if dirty is correctly typed for IN_PLACE */ + if (typeof dirty === 'string') { + IN_PLACE = false + } + if (IN_PLACE) { + /* Do some early pre-sanitization to avoid unsafe root nodes */ + if (dirty.nodeName) { + const tagName = transformCaseFunc(dirty.nodeName) + if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { + throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place') + } + } + } else if (dirty instanceof Node) { + /* If dirty is a DOM element, append to an empty document to avoid + elements being stripped by the parser */ + body = _initDocument('') + importedNode = body.ownerDocument.importNode(dirty, true) + if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') { + /* Node is already a body, use as is */ + body = importedNode + } else if (importedNode.nodeName === 'HTML') { + body = importedNode + } else { + // eslint-disable-next-line unicorn/prefer-dom-node-append + body.appendChild(importedNode) + } + } else { + /* Exit directly if we have nothing to do */ + if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && + // eslint-disable-next-line unicorn/prefer-includes + dirty.indexOf('<') === -1) { + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty + } + + /* Initialize the document to work on */ + body = _initDocument(dirty) + + /* Check we have a DOM node from the data */ + if (!body) { + return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '' + } + } + + /* Remove first element node (ours) if FORCE_BODY is set */ + if (body && FORCE_BODY) { + _forceRemove(body.firstChild) + } + + /* Get node iterator */ + const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body) + + /* Now start iterating over the created document */ + while (currentNode = nodeIterator.nextNode()) { + /* Sanitize tags and elements */ + if (_sanitizeElements(currentNode)) { + continue + } + + /* Shadow DOM detected, sanitize it */ + if (currentNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(currentNode.content) + } + + /* Check attributes, sanitize if necessary */ + _sanitizeAttributes(currentNode) + } + + /* If we sanitized `dirty` in-place, return it. */ + if (IN_PLACE) { + return dirty + } + + /* Return sanitized string or DOM */ + if (RETURN_DOM) { + if (RETURN_DOM_FRAGMENT) { + returnNode = createDocumentFragment.call(body.ownerDocument) + while (body.firstChild) { + // eslint-disable-next-line unicorn/prefer-dom-node-append + returnNode.appendChild(body.firstChild) + } + } else { + returnNode = body + } + if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) { + /* + AdoptNode() is not used because internal state is not reset + (e.g. the past names map of a HTMLFormElement), this is safe + in theory but we would rather not risk another attack vector. + The state that is cloned by importNode() is explicitly defined + by the specs. + */ + returnNode = importNode.call(originalDocument, returnNode, true) + } + return returnNode + } + let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML + + /* Serialize doctype if allowed */ + if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) { + serializedHTML = '\n' + serializedHTML + } + + /* Sanitize final string template-safe */ + if (SAFE_FOR_TEMPLATES) { + arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { + serializedHTML = stringReplace(serializedHTML, expr, ' ') + }) + } + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML + } + + /** + * Public method to set the configuration once + * setConfig + * + * @param {Object} cfg configuration object + */ + DOMPurify.setConfig = function () { + const cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {} + _parseConfig(cfg) + SET_CONFIG = true + } + + /** + * Public method to remove the configuration + * clearConfig + * + */ + DOMPurify.clearConfig = function () { + CONFIG = null + SET_CONFIG = false + } + + /** + * Public method to check if an attribute value is valid. + * Uses last set config, if any. Otherwise, uses config defaults. + * isValidAttribute + * + * @param {String} tag Tag name of containing element. + * @param {String} attr Attribute name. + * @param {String} value Attribute value. + * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false. + */ + DOMPurify.isValidAttribute = function (tag, attr, value) { + /* Initialize shared config vars if necessary. */ + if (!CONFIG) { + _parseConfig({}) + } + const lcTag = transformCaseFunc(tag) + const lcName = transformCaseFunc(attr) + return _isValidAttribute(lcTag, lcName, value) + } + + /** + * AddHook + * Public method to add DOMPurify hooks + * + * @param {String} entryPoint entry point for the hook to add + * @param {Function} hookFunction function to execute + */ + DOMPurify.addHook = function (entryPoint, hookFunction) { + if (typeof hookFunction !== 'function') { + return + } + hooks[entryPoint] = hooks[entryPoint] || [] + arrayPush(hooks[entryPoint], hookFunction) + } + + /** + * RemoveHook + * Public method to remove a DOMPurify hook at a given entryPoint + * (pops it from the stack of hooks if more are present) + * + * @param {String} entryPoint entry point for the hook to remove + * @return {Function} removed(popped) hook + */ + DOMPurify.removeHook = function (entryPoint) { + if (hooks[entryPoint]) { + return arrayPop(hooks[entryPoint]) + } + } + + /** + * RemoveHooks + * Public method to remove all DOMPurify hooks at a given entryPoint + * + * @param {String} entryPoint entry point for the hooks to remove + */ + DOMPurify.removeHooks = function (entryPoint) { + if (hooks[entryPoint]) { + hooks[entryPoint] = [] + } + } + + /** + * RemoveAllHooks + * Public method to remove all DOMPurify hooks + */ + DOMPurify.removeAllHooks = function () { + hooks = {} + } + return DOMPurify + } + const purify = createDOMPurify() + + return purify +}) +// # sourceMappingURL=purify.js.map + +export default DOMPurify diff --git a/dependencies/dompurify/purify.js.map b/dependencies/dompurify/purify.js.map new file mode 100644 index 0000000..42e764c --- /dev/null +++ b/dependencies/dompurify/purify.js.map @@ -0,0 +1 @@ +{"version":3,"file":"purify.js","sources":["../src/utils.js","../src/tags.js","../src/attrs.js","../src/regexp.js","../src/purify.js"],"sourcesContent":["const {\n entries,\n setPrototypeOf,\n isFrozen,\n getPrototypeOf,\n getOwnPropertyDescriptor,\n} = Object;\n\nlet { freeze, seal, create } = Object; // eslint-disable-line import/no-mutable-exports\nlet { apply, construct } = typeof Reflect !== 'undefined' && Reflect;\n\nif (!freeze) {\n freeze = function (x) {\n return x;\n };\n}\n\nif (!seal) {\n seal = function (x) {\n return x;\n };\n}\n\nif (!apply) {\n apply = function (fun, thisValue, args) {\n return fun.apply(thisValue, args);\n };\n}\n\nif (!construct) {\n construct = function (Func, args) {\n return new Func(...args);\n };\n}\n\nconst arrayForEach = unapply(Array.prototype.forEach);\nconst arrayIndexOf = unapply(Array.prototype.indexOf);\nconst arrayPop = unapply(Array.prototype.pop);\nconst arrayPush = unapply(Array.prototype.push);\nconst arraySlice = unapply(Array.prototype.slice);\n\nconst stringToLowerCase = unapply(String.prototype.toLowerCase);\nconst stringToString = unapply(String.prototype.toString);\nconst stringMatch = unapply(String.prototype.match);\nconst stringReplace = unapply(String.prototype.replace);\nconst stringIndexOf = unapply(String.prototype.indexOf);\nconst stringTrim = unapply(String.prototype.trim);\n\nconst objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);\n\nconst regExpTest = unapply(RegExp.prototype.test);\n\nconst typeErrorCreate = unconstruct(TypeError);\n\n/**\n * Creates a new function that calls the given function with a specified thisArg and arguments.\n *\n * @param {Function} func - The function to be wrapped and called.\n * @returns {Function} A new function that calls the given function with a specified thisArg and arguments.\n */\nfunction unapply(func) {\n return (thisArg, ...args) => apply(func, thisArg, args);\n}\n\n/**\n * Creates a new function that constructs an instance of the given constructor function with the provided arguments.\n *\n * @param {Function} func - The constructor function to be wrapped and called.\n * @returns {Function} A new function that constructs an instance of the given constructor function with the provided arguments.\n */\nfunction unconstruct(func) {\n return (...args) => construct(func, args);\n}\n\n/**\n * Add properties to a lookup table\n *\n * @param {Object} set - The set to which elements will be added.\n * @param {Array} array - The array containing elements to be added to the set.\n * @param {Function} transformCaseFunc - An optional function to transform the case of each element before adding to the set.\n * @returns {Object} The modified set with added elements.\n */\nfunction addToSet(set, array, transformCaseFunc = stringToLowerCase) {\n if (setPrototypeOf) {\n // Make 'in' and truthy checks like Boolean(set.constructor)\n // independent of any properties defined on Object.prototype.\n // Prevent prototype setters from intercepting set as a this value.\n setPrototypeOf(set, null);\n }\n\n let l = array.length;\n while (l--) {\n let element = array[l];\n if (typeof element === 'string') {\n const lcElement = transformCaseFunc(element);\n if (lcElement !== element) {\n // Config presets (e.g. tags.js, attrs.js) are immutable.\n if (!isFrozen(array)) {\n array[l] = lcElement;\n }\n\n element = lcElement;\n }\n }\n\n set[element] = true;\n }\n\n return set;\n}\n\n/**\n * Clean up an array to harden against CSPP\n *\n * @param {Array} array - The array to be cleaned.\n * @returns {Array} The cleaned version of the array\n */\nfunction cleanArray(array) {\n for (let index = 0; index < array.length; index++) {\n const isPropertyExist = objectHasOwnProperty(array, index);\n\n if (!isPropertyExist) {\n array[index] = null;\n }\n }\n\n return array;\n}\n\n/**\n * Shallow clone an object\n *\n * @param {Object} object - The object to be cloned.\n * @returns {Object} A new object that copies the original.\n */\nfunction clone(object) {\n const newObject = create(null);\n\n for (const [property, value] of entries(object)) {\n const isPropertyExist = objectHasOwnProperty(object, property);\n\n if (isPropertyExist) {\n if (Array.isArray(value)) {\n newObject[property] = cleanArray(value);\n } else if (\n value &&\n typeof value === 'object' &&\n value.constructor === Object\n ) {\n newObject[property] = clone(value);\n } else {\n newObject[property] = value;\n }\n }\n }\n\n return newObject;\n}\n\n/**\n * This method automatically checks if the prop is function or getter and behaves accordingly.\n *\n * @param {Object} object - The object to look up the getter function in its prototype chain.\n * @param {String} prop - The property name for which to find the getter function.\n * @returns {Function} The getter function found in the prototype chain or a fallback function.\n */\nfunction lookupGetter(object, prop) {\n while (object !== null) {\n const desc = getOwnPropertyDescriptor(object, prop);\n\n if (desc) {\n if (desc.get) {\n return unapply(desc.get);\n }\n\n if (typeof desc.value === 'function') {\n return unapply(desc.value);\n }\n }\n\n object = getPrototypeOf(object);\n }\n\n function fallbackValue() {\n return null;\n }\n\n return fallbackValue;\n}\n\nexport {\n // Array\n arrayForEach,\n arrayIndexOf,\n arrayPop,\n arrayPush,\n arraySlice,\n // Object\n entries,\n freeze,\n getPrototypeOf,\n getOwnPropertyDescriptor,\n isFrozen,\n setPrototypeOf,\n seal,\n clone,\n create,\n objectHasOwnProperty,\n // RegExp\n regExpTest,\n // String\n stringIndexOf,\n stringMatch,\n stringReplace,\n stringToLowerCase,\n stringToString,\n stringTrim,\n // Errors\n typeErrorCreate,\n // Other\n lookupGetter,\n addToSet,\n // Reflect\n unapply,\n unconstruct,\n};\n","import { freeze } from './utils.js';\n\nexport const html = freeze([\n 'a',\n 'abbr',\n 'acronym',\n 'address',\n 'area',\n 'article',\n 'aside',\n 'audio',\n 'b',\n 'bdi',\n 'bdo',\n 'big',\n 'blink',\n 'blockquote',\n 'body',\n 'br',\n 'button',\n 'canvas',\n 'caption',\n 'center',\n 'cite',\n 'code',\n 'col',\n 'colgroup',\n 'content',\n 'data',\n 'datalist',\n 'dd',\n 'decorator',\n 'del',\n 'details',\n 'dfn',\n 'dialog',\n 'dir',\n 'div',\n 'dl',\n 'dt',\n 'element',\n 'em',\n 'fieldset',\n 'figcaption',\n 'figure',\n 'font',\n 'footer',\n 'form',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'head',\n 'header',\n 'hgroup',\n 'hr',\n 'html',\n 'i',\n 'img',\n 'input',\n 'ins',\n 'kbd',\n 'label',\n 'legend',\n 'li',\n 'main',\n 'map',\n 'mark',\n 'marquee',\n 'menu',\n 'menuitem',\n 'meter',\n 'nav',\n 'nobr',\n 'ol',\n 'optgroup',\n 'option',\n 'output',\n 'p',\n 'picture',\n 'pre',\n 'progress',\n 'q',\n 'rp',\n 'rt',\n 'ruby',\n 's',\n 'samp',\n 'section',\n 'select',\n 'shadow',\n 'small',\n 'source',\n 'spacer',\n 'span',\n 'strike',\n 'strong',\n 'style',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'template',\n 'textarea',\n 'tfoot',\n 'th',\n 'thead',\n 'time',\n 'tr',\n 'track',\n 'tt',\n 'u',\n 'ul',\n 'var',\n 'video',\n 'wbr',\n]);\n\n// SVG\nexport const svg = freeze([\n 'svg',\n 'a',\n 'altglyph',\n 'altglyphdef',\n 'altglyphitem',\n 'animatecolor',\n 'animatemotion',\n 'animatetransform',\n 'circle',\n 'clippath',\n 'defs',\n 'desc',\n 'ellipse',\n 'filter',\n 'font',\n 'g',\n 'glyph',\n 'glyphref',\n 'hkern',\n 'image',\n 'line',\n 'lineargradient',\n 'marker',\n 'mask',\n 'metadata',\n 'mpath',\n 'path',\n 'pattern',\n 'polygon',\n 'polyline',\n 'radialgradient',\n 'rect',\n 'stop',\n 'style',\n 'switch',\n 'symbol',\n 'text',\n 'textpath',\n 'title',\n 'tref',\n 'tspan',\n 'view',\n 'vkern',\n]);\n\nexport const svgFilters = freeze([\n 'feBlend',\n 'feColorMatrix',\n 'feComponentTransfer',\n 'feComposite',\n 'feConvolveMatrix',\n 'feDiffuseLighting',\n 'feDisplacementMap',\n 'feDistantLight',\n 'feDropShadow',\n 'feFlood',\n 'feFuncA',\n 'feFuncB',\n 'feFuncG',\n 'feFuncR',\n 'feGaussianBlur',\n 'feImage',\n 'feMerge',\n 'feMergeNode',\n 'feMorphology',\n 'feOffset',\n 'fePointLight',\n 'feSpecularLighting',\n 'feSpotLight',\n 'feTile',\n 'feTurbulence',\n]);\n\n// List of SVG elements that are disallowed by default.\n// We still need to know them so that we can do namespace\n// checks properly in case one wants to add them to\n// allow-list.\nexport const svgDisallowed = freeze([\n 'animate',\n 'color-profile',\n 'cursor',\n 'discard',\n 'font-face',\n 'font-face-format',\n 'font-face-name',\n 'font-face-src',\n 'font-face-uri',\n 'foreignobject',\n 'hatch',\n 'hatchpath',\n 'mesh',\n 'meshgradient',\n 'meshpatch',\n 'meshrow',\n 'missing-glyph',\n 'script',\n 'set',\n 'solidcolor',\n 'unknown',\n 'use',\n]);\n\nexport const mathMl = freeze([\n 'math',\n 'menclose',\n 'merror',\n 'mfenced',\n 'mfrac',\n 'mglyph',\n 'mi',\n 'mlabeledtr',\n 'mmultiscripts',\n 'mn',\n 'mo',\n 'mover',\n 'mpadded',\n 'mphantom',\n 'mroot',\n 'mrow',\n 'ms',\n 'mspace',\n 'msqrt',\n 'mstyle',\n 'msub',\n 'msup',\n 'msubsup',\n 'mtable',\n 'mtd',\n 'mtext',\n 'mtr',\n 'munder',\n 'munderover',\n 'mprescripts',\n]);\n\n// Similarly to SVG, we want to know all MathML elements,\n// even those that we disallow by default.\nexport const mathMlDisallowed = freeze([\n 'maction',\n 'maligngroup',\n 'malignmark',\n 'mlongdiv',\n 'mscarries',\n 'mscarry',\n 'msgroup',\n 'mstack',\n 'msline',\n 'msrow',\n 'semantics',\n 'annotation',\n 'annotation-xml',\n 'mprescripts',\n 'none',\n]);\n\nexport const text = freeze(['#text']);\n","import { freeze } from './utils.js';\n\nexport const html = freeze([\n 'accept',\n 'action',\n 'align',\n 'alt',\n 'autocapitalize',\n 'autocomplete',\n 'autopictureinpicture',\n 'autoplay',\n 'background',\n 'bgcolor',\n 'border',\n 'capture',\n 'cellpadding',\n 'cellspacing',\n 'checked',\n 'cite',\n 'class',\n 'clear',\n 'color',\n 'cols',\n 'colspan',\n 'controls',\n 'controlslist',\n 'coords',\n 'crossorigin',\n 'datetime',\n 'decoding',\n 'default',\n 'dir',\n 'disabled',\n 'disablepictureinpicture',\n 'disableremoteplayback',\n 'download',\n 'draggable',\n 'enctype',\n 'enterkeyhint',\n 'face',\n 'for',\n 'headers',\n 'height',\n 'hidden',\n 'high',\n 'href',\n 'hreflang',\n 'id',\n 'inputmode',\n 'integrity',\n 'ismap',\n 'kind',\n 'label',\n 'lang',\n 'list',\n 'loading',\n 'loop',\n 'low',\n 'max',\n 'maxlength',\n 'media',\n 'method',\n 'min',\n 'minlength',\n 'multiple',\n 'muted',\n 'name',\n 'nonce',\n 'noshade',\n 'novalidate',\n 'nowrap',\n 'open',\n 'optimum',\n 'pattern',\n 'placeholder',\n 'playsinline',\n 'poster',\n 'preload',\n 'pubdate',\n 'radiogroup',\n 'readonly',\n 'rel',\n 'required',\n 'rev',\n 'reversed',\n 'role',\n 'rows',\n 'rowspan',\n 'spellcheck',\n 'scope',\n 'selected',\n 'shape',\n 'size',\n 'sizes',\n 'span',\n 'srclang',\n 'start',\n 'src',\n 'srcset',\n 'step',\n 'style',\n 'summary',\n 'tabindex',\n 'title',\n 'translate',\n 'type',\n 'usemap',\n 'valign',\n 'value',\n 'width',\n 'xmlns',\n 'slot',\n]);\n\nexport const svg = freeze([\n 'accent-height',\n 'accumulate',\n 'additive',\n 'alignment-baseline',\n 'ascent',\n 'attributename',\n 'attributetype',\n 'azimuth',\n 'basefrequency',\n 'baseline-shift',\n 'begin',\n 'bias',\n 'by',\n 'class',\n 'clip',\n 'clippathunits',\n 'clip-path',\n 'clip-rule',\n 'color',\n 'color-interpolation',\n 'color-interpolation-filters',\n 'color-profile',\n 'color-rendering',\n 'cx',\n 'cy',\n 'd',\n 'dx',\n 'dy',\n 'diffuseconstant',\n 'direction',\n 'display',\n 'divisor',\n 'dur',\n 'edgemode',\n 'elevation',\n 'end',\n 'fill',\n 'fill-opacity',\n 'fill-rule',\n 'filter',\n 'filterunits',\n 'flood-color',\n 'flood-opacity',\n 'font-family',\n 'font-size',\n 'font-size-adjust',\n 'font-stretch',\n 'font-style',\n 'font-variant',\n 'font-weight',\n 'fx',\n 'fy',\n 'g1',\n 'g2',\n 'glyph-name',\n 'glyphref',\n 'gradientunits',\n 'gradienttransform',\n 'height',\n 'href',\n 'id',\n 'image-rendering',\n 'in',\n 'in2',\n 'k',\n 'k1',\n 'k2',\n 'k3',\n 'k4',\n 'kerning',\n 'keypoints',\n 'keysplines',\n 'keytimes',\n 'lang',\n 'lengthadjust',\n 'letter-spacing',\n 'kernelmatrix',\n 'kernelunitlength',\n 'lighting-color',\n 'local',\n 'marker-end',\n 'marker-mid',\n 'marker-start',\n 'markerheight',\n 'markerunits',\n 'markerwidth',\n 'maskcontentunits',\n 'maskunits',\n 'max',\n 'mask',\n 'media',\n 'method',\n 'mode',\n 'min',\n 'name',\n 'numoctaves',\n 'offset',\n 'operator',\n 'opacity',\n 'order',\n 'orient',\n 'orientation',\n 'origin',\n 'overflow',\n 'paint-order',\n 'path',\n 'pathlength',\n 'patterncontentunits',\n 'patterntransform',\n 'patternunits',\n 'points',\n 'preservealpha',\n 'preserveaspectratio',\n 'primitiveunits',\n 'r',\n 'rx',\n 'ry',\n 'radius',\n 'refx',\n 'refy',\n 'repeatcount',\n 'repeatdur',\n 'restart',\n 'result',\n 'rotate',\n 'scale',\n 'seed',\n 'shape-rendering',\n 'specularconstant',\n 'specularexponent',\n 'spreadmethod',\n 'startoffset',\n 'stddeviation',\n 'stitchtiles',\n 'stop-color',\n 'stop-opacity',\n 'stroke-dasharray',\n 'stroke-dashoffset',\n 'stroke-linecap',\n 'stroke-linejoin',\n 'stroke-miterlimit',\n 'stroke-opacity',\n 'stroke',\n 'stroke-width',\n 'style',\n 'surfacescale',\n 'systemlanguage',\n 'tabindex',\n 'targetx',\n 'targety',\n 'transform',\n 'transform-origin',\n 'text-anchor',\n 'text-decoration',\n 'text-rendering',\n 'textlength',\n 'type',\n 'u1',\n 'u2',\n 'unicode',\n 'values',\n 'viewbox',\n 'visibility',\n 'version',\n 'vert-adv-y',\n 'vert-origin-x',\n 'vert-origin-y',\n 'width',\n 'word-spacing',\n 'wrap',\n 'writing-mode',\n 'xchannelselector',\n 'ychannelselector',\n 'x',\n 'x1',\n 'x2',\n 'xmlns',\n 'y',\n 'y1',\n 'y2',\n 'z',\n 'zoomandpan',\n]);\n\nexport const mathMl = freeze([\n 'accent',\n 'accentunder',\n 'align',\n 'bevelled',\n 'close',\n 'columnsalign',\n 'columnlines',\n 'columnspan',\n 'denomalign',\n 'depth',\n 'dir',\n 'display',\n 'displaystyle',\n 'encoding',\n 'fence',\n 'frame',\n 'height',\n 'href',\n 'id',\n 'largeop',\n 'length',\n 'linethickness',\n 'lspace',\n 'lquote',\n 'mathbackground',\n 'mathcolor',\n 'mathsize',\n 'mathvariant',\n 'maxsize',\n 'minsize',\n 'movablelimits',\n 'notation',\n 'numalign',\n 'open',\n 'rowalign',\n 'rowlines',\n 'rowspacing',\n 'rowspan',\n 'rspace',\n 'rquote',\n 'scriptlevel',\n 'scriptminsize',\n 'scriptsizemultiplier',\n 'selection',\n 'separator',\n 'separators',\n 'stretchy',\n 'subscriptshift',\n 'supscriptshift',\n 'symmetric',\n 'voffset',\n 'width',\n 'xmlns',\n]);\n\nexport const xml = freeze([\n 'xlink:href',\n 'xml:id',\n 'xlink:title',\n 'xml:space',\n 'xmlns:xlink',\n]);\n","import { seal } from './utils.js';\n\n// eslint-disable-next-line unicorn/better-regex\nexport const MUSTACHE_EXPR = seal(/\\{\\{[\\w\\W]*|[\\w\\W]*\\}\\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode\nexport const ERB_EXPR = seal(/<%[\\w\\W]*|[\\w\\W]*%>/gm);\nexport const TMPLIT_EXPR = seal(/\\${[\\w\\W]*}/gm);\nexport const DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]/); // eslint-disable-line no-useless-escape\nexport const ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\nexport const IS_ALLOWED_URI = seal(\n /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n);\nexport const IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\nexport const ATTR_WHITESPACE = seal(\n /[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n);\nexport const DOCTYPE_NAME = seal(/^html$/i);\n","import * as TAGS from './tags.js';\nimport * as ATTRS from './attrs.js';\nimport * as EXPRESSIONS from './regexp.js';\nimport {\n addToSet,\n clone,\n entries,\n freeze,\n arrayForEach,\n arrayPop,\n arrayPush,\n stringMatch,\n stringReplace,\n stringToLowerCase,\n stringToString,\n stringIndexOf,\n stringTrim,\n regExpTest,\n typeErrorCreate,\n lookupGetter,\n create,\n objectHasOwnProperty,\n} from './utils.js';\n\nconst getGlobal = function () {\n return typeof window === 'undefined' ? null : window;\n};\n\n/**\n * Creates a no-op policy for internal use only.\n * Don't export this function outside this module!\n * @param {TrustedTypePolicyFactory} trustedTypes The policy factory.\n * @param {HTMLScriptElement} purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).\n * @return {TrustedTypePolicy} The policy created (or null, if Trusted Types\n * are not supported or creating the policy failed).\n */\nconst _createTrustedTypesPolicy = function (trustedTypes, purifyHostElement) {\n if (\n typeof trustedTypes !== 'object' ||\n typeof trustedTypes.createPolicy !== 'function'\n ) {\n return null;\n }\n\n // Allow the callers to control the unique policy name\n // by adding a data-tt-policy-suffix to the script element with the DOMPurify.\n // Policy creation with duplicate names throws in Trusted Types.\n let suffix = null;\n const ATTR_NAME = 'data-tt-policy-suffix';\n if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {\n suffix = purifyHostElement.getAttribute(ATTR_NAME);\n }\n\n const policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n\n try {\n return trustedTypes.createPolicy(policyName, {\n createHTML(html) {\n return html;\n },\n createScriptURL(scriptUrl) {\n return scriptUrl;\n },\n });\n } catch (_) {\n // Policy creation failed (most likely another DOMPurify script has\n // already run). Skip creating the policy, as this will only cause errors\n // if TT are enforced.\n console.warn(\n 'TrustedTypes policy ' + policyName + ' could not be created.'\n );\n return null;\n }\n};\n\nfunction createDOMPurify(window = getGlobal()) {\n const DOMPurify = (root) => createDOMPurify(root);\n\n /**\n * Version label, exposed for easier checks\n * if DOMPurify is up to date or not\n */\n DOMPurify.version = VERSION;\n\n /**\n * Array of elements that DOMPurify removed during sanitation.\n * Empty if nothing was removed.\n */\n DOMPurify.removed = [];\n\n if (!window || !window.document || window.document.nodeType !== 9) {\n // Not running in a browser, provide a factory function\n // so that you can pass your own Window\n DOMPurify.isSupported = false;\n\n return DOMPurify;\n }\n\n let { document } = window;\n\n const originalDocument = document;\n const currentScript = originalDocument.currentScript;\n const {\n DocumentFragment,\n HTMLTemplateElement,\n Node,\n Element,\n NodeFilter,\n NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,\n HTMLFormElement,\n DOMParser,\n trustedTypes,\n } = window;\n\n const ElementPrototype = Element.prototype;\n\n const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n const getParentNode = lookupGetter(ElementPrototype, 'parentNode');\n\n // As per issue #47, the web-components registry is inherited by a\n // new document created via createHTMLDocument. As per the spec\n // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)\n // a new empty registry is used when creating a template contents owner\n // document, so we use that as our parent document to ensure nothing\n // is inherited.\n if (typeof HTMLTemplateElement === 'function') {\n const template = document.createElement('template');\n if (template.content && template.content.ownerDocument) {\n document = template.content.ownerDocument;\n }\n }\n\n let trustedTypesPolicy;\n let emptyHTML = '';\n\n const {\n implementation,\n createNodeIterator,\n createDocumentFragment,\n getElementsByTagName,\n } = document;\n const { importNode } = originalDocument;\n\n let hooks = {};\n\n /**\n * Expose whether this browser supports running the full DOMPurify.\n */\n DOMPurify.isSupported =\n typeof entries === 'function' &&\n typeof getParentNode === 'function' &&\n implementation &&\n implementation.createHTMLDocument !== undefined;\n\n const {\n MUSTACHE_EXPR,\n ERB_EXPR,\n TMPLIT_EXPR,\n DATA_ATTR,\n ARIA_ATTR,\n IS_SCRIPT_OR_DATA,\n ATTR_WHITESPACE,\n } = EXPRESSIONS;\n\n let { IS_ALLOWED_URI } = EXPRESSIONS;\n\n /**\n * We consider the elements and attributes below to be safe. Ideally\n * don't add any new ones but feel free to remove unwanted ones.\n */\n\n /* allowed element names */\n let ALLOWED_TAGS = null;\n const DEFAULT_ALLOWED_TAGS = addToSet({}, [\n ...TAGS.html,\n ...TAGS.svg,\n ...TAGS.svgFilters,\n ...TAGS.mathMl,\n ...TAGS.text,\n ]);\n\n /* Allowed attribute names */\n let ALLOWED_ATTR = null;\n const DEFAULT_ALLOWED_ATTR = addToSet({}, [\n ...ATTRS.html,\n ...ATTRS.svg,\n ...ATTRS.mathMl,\n ...ATTRS.xml,\n ]);\n\n /*\n * Configure how DOMPUrify should handle custom elements and their attributes as well as customized built-in elements.\n * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)\n * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)\n * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.\n */\n let CUSTOM_ELEMENT_HANDLING = Object.seal(\n create(null, {\n tagNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n attributeNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null,\n },\n allowCustomizedBuiltInElements: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: false,\n },\n })\n );\n\n /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n let FORBID_TAGS = null;\n\n /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n let FORBID_ATTR = null;\n\n /* Decide if ARIA attributes are okay */\n let ALLOW_ARIA_ATTR = true;\n\n /* Decide if custom data attributes are okay */\n let ALLOW_DATA_ATTR = true;\n\n /* Decide if unknown protocols are okay */\n let ALLOW_UNKNOWN_PROTOCOLS = false;\n\n /* Decide if self-closing tags in attributes are allowed.\n * Usually removed due to a mXSS issue in jQuery 3.0 */\n let ALLOW_SELF_CLOSE_IN_ATTR = true;\n\n /* Output should be safe for common template engines.\n * This means, DOMPurify removes data attributes, mustaches and ERB\n */\n let SAFE_FOR_TEMPLATES = false;\n\n /* Decide if document with ... should be returned */\n let WHOLE_DOCUMENT = false;\n\n /* Track whether config is already set on this instance of DOMPurify. */\n let SET_CONFIG = false;\n\n /* Decide if all elements (e.g. style, script) must be children of\n * document.body. By default, browsers might move them to document.head */\n let FORCE_BODY = false;\n\n /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported).\n * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead\n */\n let RETURN_DOM = false;\n\n /* Decide if a DOM `DocumentFragment` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported) */\n let RETURN_DOM_FRAGMENT = false;\n\n /* Try to return a Trusted Type object instead of a string, return a string in\n * case Trusted Types are not supported */\n let RETURN_TRUSTED_TYPE = false;\n\n /* Output should be free from DOM clobbering attacks?\n * This sanitizes markups named with colliding, clobberable built-in DOM APIs.\n */\n let SANITIZE_DOM = true;\n\n /* Achieve full DOM Clobbering protection by isolating the namespace of named\n * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.\n *\n * HTML/DOM spec rules that enable DOM Clobbering:\n * - Named Access on Window (§7.3.3)\n * - DOM Tree Accessors (§3.1.5)\n * - Form Element Parent-Child Relations (§4.10.3)\n * - Iframe srcdoc / Nested WindowProxies (§4.8.5)\n * - HTMLCollection (§4.2.10.2)\n *\n * Namespace isolation is implemented by prefixing `id` and `name` attributes\n * with a constant string, i.e., `user-content-`\n */\n let SANITIZE_NAMED_PROPS = false;\n const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';\n\n /* Keep element content when removing element? */\n let KEEP_CONTENT = true;\n\n /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead\n * of importing it into a new Document and returning a sanitized copy */\n let IN_PLACE = false;\n\n /* Allow usage of profiles like html, svg and mathMl */\n let USE_PROFILES = {};\n\n /* Tags to ignore content of when KEEP_CONTENT is true */\n let FORBID_CONTENTS = null;\n const DEFAULT_FORBID_CONTENTS = addToSet({}, [\n 'annotation-xml',\n 'audio',\n 'colgroup',\n 'desc',\n 'foreignobject',\n 'head',\n 'iframe',\n 'math',\n 'mi',\n 'mn',\n 'mo',\n 'ms',\n 'mtext',\n 'noembed',\n 'noframes',\n 'noscript',\n 'plaintext',\n 'script',\n 'style',\n 'svg',\n 'template',\n 'thead',\n 'title',\n 'video',\n 'xmp',\n ]);\n\n /* Tags that are safe for data: URIs */\n let DATA_URI_TAGS = null;\n const DEFAULT_DATA_URI_TAGS = addToSet({}, [\n 'audio',\n 'video',\n 'img',\n 'source',\n 'image',\n 'track',\n ]);\n\n /* Attributes safe for values like \"javascript:\" */\n let URI_SAFE_ATTRIBUTES = null;\n const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, [\n 'alt',\n 'class',\n 'for',\n 'id',\n 'label',\n 'name',\n 'pattern',\n 'placeholder',\n 'role',\n 'summary',\n 'title',\n 'value',\n 'style',\n 'xmlns',\n ]);\n\n const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';\n const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';\n /* Document namespace */\n let NAMESPACE = HTML_NAMESPACE;\n let IS_EMPTY_INPUT = false;\n\n /* Allowed XHTML+XML namespaces */\n let ALLOWED_NAMESPACES = null;\n const DEFAULT_ALLOWED_NAMESPACES = addToSet(\n {},\n [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE],\n stringToString\n );\n\n /* Parsing of strict XHTML documents */\n let PARSER_MEDIA_TYPE = null;\n const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];\n const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';\n let transformCaseFunc = null;\n\n /* Keep a reference to config to pass to hooks */\n let CONFIG = null;\n\n /* Ideally, do not touch anything below this line */\n /* ______________________________________________ */\n\n const formElement = document.createElement('form');\n\n const isRegexOrFunction = function (testValue) {\n return testValue instanceof RegExp || testValue instanceof Function;\n };\n\n /**\n * _parseConfig\n *\n * @param {Object} cfg optional config literal\n */\n // eslint-disable-next-line complexity\n const _parseConfig = function (cfg = {}) {\n if (CONFIG && CONFIG === cfg) {\n return;\n }\n\n /* Shield configuration object from tampering */\n if (!cfg || typeof cfg !== 'object') {\n cfg = {};\n }\n\n /* Shield configuration object from prototype pollution */\n cfg = clone(cfg);\n\n PARSER_MEDIA_TYPE =\n // eslint-disable-next-line unicorn/prefer-includes\n SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1\n ? DEFAULT_PARSER_MEDIA_TYPE\n : cfg.PARSER_MEDIA_TYPE;\n\n // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.\n transformCaseFunc =\n PARSER_MEDIA_TYPE === 'application/xhtml+xml'\n ? stringToString\n : stringToLowerCase;\n\n /* Set configuration parameters */\n ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS')\n ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc)\n : DEFAULT_ALLOWED_TAGS;\n ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR')\n ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc)\n : DEFAULT_ALLOWED_ATTR;\n ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES')\n ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString)\n : DEFAULT_ALLOWED_NAMESPACES;\n URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR')\n ? addToSet(\n clone(DEFAULT_URI_SAFE_ATTRIBUTES), // eslint-disable-line indent\n cfg.ADD_URI_SAFE_ATTR, // eslint-disable-line indent\n transformCaseFunc // eslint-disable-line indent\n ) // eslint-disable-line indent\n : DEFAULT_URI_SAFE_ATTRIBUTES;\n DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS')\n ? addToSet(\n clone(DEFAULT_DATA_URI_TAGS), // eslint-disable-line indent\n cfg.ADD_DATA_URI_TAGS, // eslint-disable-line indent\n transformCaseFunc // eslint-disable-line indent\n ) // eslint-disable-line indent\n : DEFAULT_DATA_URI_TAGS;\n FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS')\n ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc)\n : DEFAULT_FORBID_CONTENTS;\n FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS')\n ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc)\n : {};\n FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR')\n ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc)\n : {};\n USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES')\n ? cfg.USE_PROFILES\n : false;\n ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true\n ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true\n ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false\n ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true\n SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false\n WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false\n RETURN_DOM = cfg.RETURN_DOM || false; // Default false\n RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false\n RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false\n FORCE_BODY = cfg.FORCE_BODY || false; // Default false\n SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true\n SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false\n KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true\n IN_PLACE = cfg.IN_PLACE || false; // Default false\n IS_ALLOWED_URI = cfg.ALLOWED_URI_REGEXP || EXPRESSIONS.IS_ALLOWED_URI;\n NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;\n CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n if (\n cfg.CUSTOM_ELEMENT_HANDLING &&\n isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)\n ) {\n CUSTOM_ELEMENT_HANDLING.tagNameCheck =\n cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;\n }\n\n if (\n cfg.CUSTOM_ELEMENT_HANDLING &&\n isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)\n ) {\n CUSTOM_ELEMENT_HANDLING.attributeNameCheck =\n cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;\n }\n\n if (\n cfg.CUSTOM_ELEMENT_HANDLING &&\n typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements ===\n 'boolean'\n ) {\n CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements =\n cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;\n }\n\n if (SAFE_FOR_TEMPLATES) {\n ALLOW_DATA_ATTR = false;\n }\n\n if (RETURN_DOM_FRAGMENT) {\n RETURN_DOM = true;\n }\n\n /* Parse profile info */\n if (USE_PROFILES) {\n ALLOWED_TAGS = addToSet({}, TAGS.text);\n ALLOWED_ATTR = [];\n if (USE_PROFILES.html === true) {\n addToSet(ALLOWED_TAGS, TAGS.html);\n addToSet(ALLOWED_ATTR, ATTRS.html);\n }\n\n if (USE_PROFILES.svg === true) {\n addToSet(ALLOWED_TAGS, TAGS.svg);\n addToSet(ALLOWED_ATTR, ATTRS.svg);\n addToSet(ALLOWED_ATTR, ATTRS.xml);\n }\n\n if (USE_PROFILES.svgFilters === true) {\n addToSet(ALLOWED_TAGS, TAGS.svgFilters);\n addToSet(ALLOWED_ATTR, ATTRS.svg);\n addToSet(ALLOWED_ATTR, ATTRS.xml);\n }\n\n if (USE_PROFILES.mathMl === true) {\n addToSet(ALLOWED_TAGS, TAGS.mathMl);\n addToSet(ALLOWED_ATTR, ATTRS.mathMl);\n addToSet(ALLOWED_ATTR, ATTRS.xml);\n }\n }\n\n /* Merge configuration parameters */\n if (cfg.ADD_TAGS) {\n if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {\n ALLOWED_TAGS = clone(ALLOWED_TAGS);\n }\n\n addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);\n }\n\n if (cfg.ADD_ATTR) {\n if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {\n ALLOWED_ATTR = clone(ALLOWED_ATTR);\n }\n\n addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);\n }\n\n if (cfg.ADD_URI_SAFE_ATTR) {\n addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);\n }\n\n if (cfg.FORBID_CONTENTS) {\n if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n FORBID_CONTENTS = clone(FORBID_CONTENTS);\n }\n\n addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);\n }\n\n /* Add #text in case KEEP_CONTENT is set to true */\n if (KEEP_CONTENT) {\n ALLOWED_TAGS['#text'] = true;\n }\n\n /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */\n if (WHOLE_DOCUMENT) {\n addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);\n }\n\n /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */\n if (ALLOWED_TAGS.table) {\n addToSet(ALLOWED_TAGS, ['tbody']);\n delete FORBID_TAGS.tbody;\n }\n\n if (cfg.TRUSTED_TYPES_POLICY) {\n if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {\n throw typeErrorCreate(\n 'TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook.'\n );\n }\n\n if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {\n throw typeErrorCreate(\n 'TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook.'\n );\n }\n\n // Overwrite existing TrustedTypes policy.\n trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;\n\n // Sign local variables required by `sanitize`.\n emptyHTML = trustedTypesPolicy.createHTML('');\n } else {\n // Uninitialized policy, attempt to initialize the internal dompurify policy.\n if (trustedTypesPolicy === undefined) {\n trustedTypesPolicy = _createTrustedTypesPolicy(\n trustedTypes,\n currentScript\n );\n }\n\n // If creating the internal policy succeeded sign internal variables.\n if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {\n emptyHTML = trustedTypesPolicy.createHTML('');\n }\n }\n\n // Prevent further manipulation of configuration.\n // Not available in IE8, Safari 5, etc.\n if (freeze) {\n freeze(cfg);\n }\n\n CONFIG = cfg;\n };\n\n const MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, [\n 'mi',\n 'mo',\n 'mn',\n 'ms',\n 'mtext',\n ]);\n\n const HTML_INTEGRATION_POINTS = addToSet({}, [\n 'foreignobject',\n 'desc',\n 'title',\n 'annotation-xml',\n ]);\n\n // Certain elements are allowed in both SVG and HTML\n // namespace. We need to specify them explicitly\n // so that they don't get erroneously deleted from\n // HTML namespace.\n const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, [\n 'title',\n 'style',\n 'font',\n 'a',\n 'script',\n ]);\n\n /* Keep track of all possible SVG and MathML tags\n * so that we can perform the namespace checks\n * correctly. */\n const ALL_SVG_TAGS = addToSet({}, [\n ...TAGS.svg,\n ...TAGS.svgFilters,\n ...TAGS.svgDisallowed,\n ]);\n const ALL_MATHML_TAGS = addToSet({}, [\n ...TAGS.mathMl,\n ...TAGS.mathMlDisallowed,\n ]);\n\n /**\n * @param {Element} element a DOM element whose namespace is being checked\n * @returns {boolean} Return false if the element has a\n * namespace that a spec-compliant parser would never\n * return. Return true otherwise.\n */\n const _checkValidNamespace = function (element) {\n let parent = getParentNode(element);\n\n // In JSDOM, if we're inside shadow DOM, then parentNode\n // can be null. We just simulate parent in this case.\n if (!parent || !parent.tagName) {\n parent = {\n namespaceURI: NAMESPACE,\n tagName: 'template',\n };\n }\n\n const tagName = stringToLowerCase(element.tagName);\n const parentTagName = stringToLowerCase(parent.tagName);\n\n if (!ALLOWED_NAMESPACES[element.namespaceURI]) {\n return false;\n }\n\n if (element.namespaceURI === SVG_NAMESPACE) {\n // The only way to switch from HTML namespace to SVG\n // is via . If it happens via any other tag, then\n // it should be killed.\n if (parent.namespaceURI === HTML_NAMESPACE) {\n return tagName === 'svg';\n }\n\n // The only way to switch from MathML to SVG is via`\n // svg if parent is either or MathML\n // text integration points.\n if (parent.namespaceURI === MATHML_NAMESPACE) {\n return (\n tagName === 'svg' &&\n (parentTagName === 'annotation-xml' ||\n MATHML_TEXT_INTEGRATION_POINTS[parentTagName])\n );\n }\n\n // We only allow elements that are defined in SVG\n // spec. All others are disallowed in SVG namespace.\n return Boolean(ALL_SVG_TAGS[tagName]);\n }\n\n if (element.namespaceURI === MATHML_NAMESPACE) {\n // The only way to switch from HTML namespace to MathML\n // is via . If it happens via any other tag, then\n // it should be killed.\n if (parent.namespaceURI === HTML_NAMESPACE) {\n return tagName === 'math';\n }\n\n // The only way to switch from SVG to MathML is via\n // and HTML integration points\n if (parent.namespaceURI === SVG_NAMESPACE) {\n return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];\n }\n\n // We only allow elements that are defined in MathML\n // spec. All others are disallowed in MathML namespace.\n return Boolean(ALL_MATHML_TAGS[tagName]);\n }\n\n if (element.namespaceURI === HTML_NAMESPACE) {\n // The only way to switch from SVG to HTML is via\n // HTML integration points, and from MathML to HTML\n // is via MathML text integration points\n if (\n parent.namespaceURI === SVG_NAMESPACE &&\n !HTML_INTEGRATION_POINTS[parentTagName]\n ) {\n return false;\n }\n\n if (\n parent.namespaceURI === MATHML_NAMESPACE &&\n !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]\n ) {\n return false;\n }\n\n // We disallow tags that are specific for MathML\n // or SVG and should never appear in HTML namespace\n return (\n !ALL_MATHML_TAGS[tagName] &&\n (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName])\n );\n }\n\n // For XHTML and XML documents that support custom namespaces\n if (\n PARSER_MEDIA_TYPE === 'application/xhtml+xml' &&\n ALLOWED_NAMESPACES[element.namespaceURI]\n ) {\n return true;\n }\n\n // The code should never reach this place (this means\n // that the element somehow got namespace that is not\n // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).\n // Return false just in case.\n return false;\n };\n\n /**\n * _forceRemove\n *\n * @param {Node} node a DOM node\n */\n const _forceRemove = function (node) {\n arrayPush(DOMPurify.removed, { element: node });\n\n try {\n // eslint-disable-next-line unicorn/prefer-dom-node-remove\n node.parentNode.removeChild(node);\n } catch (_) {\n node.remove();\n }\n };\n\n /**\n * _removeAttribute\n *\n * @param {String} name an Attribute name\n * @param {Node} node a DOM node\n */\n const _removeAttribute = function (name, node) {\n try {\n arrayPush(DOMPurify.removed, {\n attribute: node.getAttributeNode(name),\n from: node,\n });\n } catch (_) {\n arrayPush(DOMPurify.removed, {\n attribute: null,\n from: node,\n });\n }\n\n node.removeAttribute(name);\n\n // We void attribute values for unremovable \"is\"\" attributes\n if (name === 'is' && !ALLOWED_ATTR[name]) {\n if (RETURN_DOM || RETURN_DOM_FRAGMENT) {\n try {\n _forceRemove(node);\n } catch (_) {}\n } else {\n try {\n node.setAttribute(name, '');\n } catch (_) {}\n }\n }\n };\n\n /**\n * _initDocument\n *\n * @param {String} dirty a string of dirty markup\n * @return {Document} a DOM, filled with the dirty markup\n */\n const _initDocument = function (dirty) {\n /* Create a HTML document */\n let doc = null;\n let leadingWhitespace = null;\n\n if (FORCE_BODY) {\n dirty = '' + dirty;\n } else {\n /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */\n const matches = stringMatch(dirty, /^[\\r\\n\\t ]+/);\n leadingWhitespace = matches && matches[0];\n }\n\n if (\n PARSER_MEDIA_TYPE === 'application/xhtml+xml' &&\n NAMESPACE === HTML_NAMESPACE\n ) {\n // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)\n dirty =\n '' +\n dirty +\n '';\n }\n\n const dirtyPayload = trustedTypesPolicy\n ? trustedTypesPolicy.createHTML(dirty)\n : dirty;\n /*\n * Use the DOMParser API by default, fallback later if needs be\n * DOMParser not work for svg when has multiple root element.\n */\n if (NAMESPACE === HTML_NAMESPACE) {\n try {\n doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);\n } catch (_) {}\n }\n\n /* Use createHTMLDocument in case DOMParser is not available */\n if (!doc || !doc.documentElement) {\n doc = implementation.createDocument(NAMESPACE, 'template', null);\n try {\n doc.documentElement.innerHTML = IS_EMPTY_INPUT\n ? emptyHTML\n : dirtyPayload;\n } catch (_) {\n // Syntax error if dirtyPayload is invalid xml\n }\n }\n\n const body = doc.body || doc.documentElement;\n\n if (dirty && leadingWhitespace) {\n body.insertBefore(\n document.createTextNode(leadingWhitespace),\n body.childNodes[0] || null\n );\n }\n\n /* Work on whole document or just its body */\n if (NAMESPACE === HTML_NAMESPACE) {\n return getElementsByTagName.call(\n doc,\n WHOLE_DOCUMENT ? 'html' : 'body'\n )[0];\n }\n\n return WHOLE_DOCUMENT ? doc.documentElement : body;\n };\n\n /**\n * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.\n *\n * @param {Node} root The root element or node to start traversing on.\n * @return {NodeIterator} The created NodeIterator\n */\n const _createNodeIterator = function (root) {\n return createNodeIterator.call(\n root.ownerDocument || root,\n root,\n // eslint-disable-next-line no-bitwise\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT,\n null\n );\n };\n\n /**\n * _isClobbered\n *\n * @param {Node} elm element to check for clobbering attacks\n * @return {Boolean} true if clobbered, false if safe\n */\n const _isClobbered = function (elm) {\n return (\n elm instanceof HTMLFormElement &&\n (typeof elm.nodeName !== 'string' ||\n typeof elm.textContent !== 'string' ||\n typeof elm.removeChild !== 'function' ||\n !(elm.attributes instanceof NamedNodeMap) ||\n typeof elm.removeAttribute !== 'function' ||\n typeof elm.setAttribute !== 'function' ||\n typeof elm.namespaceURI !== 'string' ||\n typeof elm.insertBefore !== 'function' ||\n typeof elm.hasChildNodes !== 'function')\n );\n };\n\n /**\n * Checks whether the given object is a DOM node.\n *\n * @param {Node} object object to check whether it's a DOM node\n * @return {Boolean} true is object is a DOM node\n */\n const _isNode = function (object) {\n return typeof Node === 'function' && object instanceof Node;\n };\n\n /**\n * _executeHook\n * Execute user configurable hooks\n *\n * @param {String} entryPoint Name of the hook's entry point\n * @param {Node} currentNode node to work on with the hook\n * @param {Object} data additional hook parameters\n */\n const _executeHook = function (entryPoint, currentNode, data) {\n if (!hooks[entryPoint]) {\n return;\n }\n\n arrayForEach(hooks[entryPoint], (hook) => {\n hook.call(DOMPurify, currentNode, data, CONFIG);\n });\n };\n\n /**\n * _sanitizeElements\n *\n * @protect nodeName\n * @protect textContent\n * @protect removeChild\n *\n * @param {Node} currentNode to check for permission to exist\n * @return {Boolean} true if node was killed, false if left alive\n */\n const _sanitizeElements = function (currentNode) {\n let content = null;\n\n /* Execute a hook if present */\n _executeHook('beforeSanitizeElements', currentNode, null);\n\n /* Check if element is clobbered or can clobber */\n if (_isClobbered(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Now let's check the element's type and name */\n const tagName = transformCaseFunc(currentNode.nodeName);\n\n /* Execute a hook if present */\n _executeHook('uponSanitizeElement', currentNode, {\n tagName,\n allowedTags: ALLOWED_TAGS,\n });\n\n /* Detect mXSS attempts abusing namespace confusion */\n if (\n currentNode.hasChildNodes() &&\n !_isNode(currentNode.firstElementChild) &&\n regExpTest(/<[/\\w]/g, currentNode.innerHTML) &&\n regExpTest(/<[/\\w]/g, currentNode.textContent)\n ) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Remove element if anything forbids its presence */\n if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n /* Check if we have a custom element to handle */\n if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)\n ) {\n return false;\n }\n\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)\n ) {\n return false;\n }\n }\n\n /* Keep content except for bad-listed elements */\n if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {\n const parentNode = getParentNode(currentNode) || currentNode.parentNode;\n const childNodes = getChildNodes(currentNode) || currentNode.childNodes;\n\n if (childNodes && parentNode) {\n const childCount = childNodes.length;\n\n for (let i = childCount - 1; i >= 0; --i) {\n parentNode.insertBefore(\n cloneNode(childNodes[i], true),\n getNextSibling(currentNode)\n );\n }\n }\n }\n\n _forceRemove(currentNode);\n return true;\n }\n\n /* Check whether element has a valid namespace */\n if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Make sure that older browsers don't get fallback-tag mXSS */\n if (\n (tagName === 'noscript' ||\n tagName === 'noembed' ||\n tagName === 'noframes') &&\n regExpTest(/<\\/no(script|embed|frames)/i, currentNode.innerHTML)\n ) {\n _forceRemove(currentNode);\n return true;\n }\n\n /* Sanitize element content to be template-safe */\n if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) {\n /* Get the element's text content */\n content = currentNode.textContent;\n\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {\n content = stringReplace(content, expr, ' ');\n });\n\n if (currentNode.textContent !== content) {\n arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });\n currentNode.textContent = content;\n }\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeElements', currentNode, null);\n\n return false;\n };\n\n /**\n * _isValidAttribute\n *\n * @param {string} lcTag Lowercase tag name of containing element.\n * @param {string} lcName Lowercase attribute name.\n * @param {string} value Attribute value.\n * @return {Boolean} Returns true if `value` is valid, otherwise false.\n */\n // eslint-disable-next-line complexity\n const _isValidAttribute = function (lcTag, lcName, value) {\n /* Make sure attribute cannot clobber */\n if (\n SANITIZE_DOM &&\n (lcName === 'id' || lcName === 'name') &&\n (value in document || value in formElement)\n ) {\n return false;\n }\n\n /* Allow valid data-* attributes: At least one character after \"-\"\n (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)\n XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)\n We don't need to check the value; it's always URI safe. */\n if (\n ALLOW_DATA_ATTR &&\n !FORBID_ATTR[lcName] &&\n regExpTest(DATA_ATTR, lcName)\n ) {\n // This attribute is safe\n } else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) {\n // This attribute is safe\n /* Otherwise, check the name is permitted */\n } else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {\n if (\n // First condition does a very basic check if a) it's basically a valid custom element tagname AND\n // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck\n (_isBasicCustomElement(lcTag) &&\n ((CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag)) ||\n (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag))) &&\n ((CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName)) ||\n (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)))) ||\n // Alternative, second condition checks if it's an `is`-attribute, AND\n // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n (lcName === 'is' &&\n CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements &&\n ((CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value)) ||\n (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))))\n ) {\n // If user has supplied a regexp or function in CUSTOM_ELEMENT_HANDLING.tagNameCheck, we need to also allow derived custom elements using the same tagName test.\n // Additionally, we need to allow attributes passing the CUSTOM_ELEMENT_HANDLING.attributeNameCheck user has configured, as custom elements can define these at their own discretion.\n } else {\n return false;\n }\n /* Check value is safe. First, is attr inert? If so, is safe */\n } else if (URI_SAFE_ATTRIBUTES[lcName]) {\n // This attribute is safe\n /* Check no script, data or unknown possibly unsafe URI\n unless we know URI values are safe for that attribute */\n } else if (\n regExpTest(IS_ALLOWED_URI, stringReplace(value, ATTR_WHITESPACE, ''))\n ) {\n // This attribute is safe\n /* Keep image data URIs alive if src/xlink:href is allowed */\n /* Further prevent gadget XSS for dynamically built script tags */\n } else if (\n (lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') &&\n lcTag !== 'script' &&\n stringIndexOf(value, 'data:') === 0 &&\n DATA_URI_TAGS[lcTag]\n ) {\n // This attribute is safe\n /* Allow unknown protocols: This provides support for links that\n are handled by protocol handlers which may be unknown ahead of\n time, e.g. fb:, spotify: */\n } else if (\n ALLOW_UNKNOWN_PROTOCOLS &&\n !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))\n ) {\n // This attribute is safe\n /* Check for binary attributes */\n } else if (value) {\n return false;\n } else {\n // Binary attributes are safe at this point\n /* Anything else, presume unsafe, do not add it back */\n }\n\n return true;\n };\n\n /**\n * _isBasicCustomElement\n * checks if at least one dash is included in tagName, and it's not the first char\n * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name\n *\n * @param {string} tagName name of the tag of the node to sanitize\n * @returns {boolean} Returns true if the tag name meets the basic criteria for a custom element, otherwise false.\n */\n const _isBasicCustomElement = function (tagName) {\n return tagName !== 'annotation-xml' && tagName.indexOf('-') > 0;\n };\n\n /**\n * _sanitizeAttributes\n *\n * @protect attributes\n * @protect nodeName\n * @protect removeAttribute\n * @protect setAttribute\n *\n * @param {Node} currentNode to sanitize\n */\n const _sanitizeAttributes = function (currentNode) {\n /* Execute a hook if present */\n _executeHook('beforeSanitizeAttributes', currentNode, null);\n\n const { attributes } = currentNode;\n\n /* Check if we have attributes; if not we might have a text node */\n if (!attributes) {\n return;\n }\n\n const hookEvent = {\n attrName: '',\n attrValue: '',\n keepAttr: true,\n allowedAttributes: ALLOWED_ATTR,\n };\n let l = attributes.length;\n\n /* Go backwards over all attributes; safely remove bad ones */\n while (l--) {\n const attr = attributes[l];\n const { name, namespaceURI, value: attrValue } = attr;\n const lcName = transformCaseFunc(name);\n\n let value = name === 'value' ? attrValue : stringTrim(attrValue);\n\n /* Execute a hook if present */\n hookEvent.attrName = lcName;\n hookEvent.attrValue = value;\n hookEvent.keepAttr = true;\n hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set\n _executeHook('uponSanitizeAttribute', currentNode, hookEvent);\n value = hookEvent.attrValue;\n /* Did the hooks approve of the attribute? */\n if (hookEvent.forceKeepAttr) {\n continue;\n }\n\n /* Remove attribute */\n _removeAttribute(name, currentNode);\n\n /* Did the hooks approve of the attribute? */\n if (!hookEvent.keepAttr) {\n continue;\n }\n\n /* Work around a security issue in jQuery 3.0 */\n if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\\/>/i, value)) {\n _removeAttribute(name, currentNode);\n continue;\n }\n\n /* Sanitize attribute content to be template-safe */\n if (SAFE_FOR_TEMPLATES) {\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {\n value = stringReplace(value, expr, ' ');\n });\n }\n\n /* Is `value` valid for this attribute? */\n const lcTag = transformCaseFunc(currentNode.nodeName);\n if (!_isValidAttribute(lcTag, lcName, value)) {\n continue;\n }\n\n /* Full DOM Clobbering protection via namespace isolation,\n * Prefix id and name attributes with `user-content-`\n */\n if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {\n // Remove the attribute with this value\n _removeAttribute(name, currentNode);\n\n // Prefix the value and later re-create the attribute with the sanitized value\n value = SANITIZE_NAMED_PROPS_PREFIX + value;\n }\n\n /* Handle attributes that require Trusted Types */\n if (\n trustedTypesPolicy &&\n typeof trustedTypes === 'object' &&\n typeof trustedTypes.getAttributeType === 'function'\n ) {\n if (namespaceURI) {\n /* Namespaces are not yet supported, see https://bugs.chromium.org/p/chromium/issues/detail?id=1305293 */\n } else {\n switch (trustedTypes.getAttributeType(lcTag, lcName)) {\n case 'TrustedHTML': {\n value = trustedTypesPolicy.createHTML(value);\n break;\n }\n\n case 'TrustedScriptURL': {\n value = trustedTypesPolicy.createScriptURL(value);\n break;\n }\n\n default: {\n break;\n }\n }\n }\n }\n\n /* Handle invalid data-* attribute set by try-catching it */\n try {\n if (namespaceURI) {\n currentNode.setAttributeNS(namespaceURI, name, value);\n } else {\n /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. \"x-schema\". */\n currentNode.setAttribute(name, value);\n }\n\n arrayPop(DOMPurify.removed);\n } catch (_) {}\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeAttributes', currentNode, null);\n };\n\n /**\n * _sanitizeShadowDOM\n *\n * @param {DocumentFragment} fragment to iterate over recursively\n */\n const _sanitizeShadowDOM = function (fragment) {\n let shadowNode = null;\n const shadowIterator = _createNodeIterator(fragment);\n\n /* Execute a hook if present */\n _executeHook('beforeSanitizeShadowDOM', fragment, null);\n\n while ((shadowNode = shadowIterator.nextNode())) {\n /* Execute a hook if present */\n _executeHook('uponSanitizeShadowNode', shadowNode, null);\n\n /* Sanitize tags and elements */\n if (_sanitizeElements(shadowNode)) {\n continue;\n }\n\n /* Deep shadow DOM detected */\n if (shadowNode.content instanceof DocumentFragment) {\n _sanitizeShadowDOM(shadowNode.content);\n }\n\n /* Check attributes, sanitize if necessary */\n _sanitizeAttributes(shadowNode);\n }\n\n /* Execute a hook if present */\n _executeHook('afterSanitizeShadowDOM', fragment, null);\n };\n\n /**\n * Sanitize\n * Public method providing core sanitation functionality\n *\n * @param {String|Node} dirty string or DOM node\n * @param {Object} cfg object\n */\n // eslint-disable-next-line complexity\n DOMPurify.sanitize = function (dirty, cfg = {}) {\n let body = null;\n let importedNode = null;\n let currentNode = null;\n let returnNode = null;\n /* Make sure we have a string to sanitize.\n DO NOT return early, as this will return the wrong type if\n the user has requested a DOM object rather than a string */\n IS_EMPTY_INPUT = !dirty;\n if (IS_EMPTY_INPUT) {\n dirty = '';\n }\n\n /* Stringify, in case dirty is an object */\n if (typeof dirty !== 'string' && !_isNode(dirty)) {\n if (typeof dirty.toString === 'function') {\n dirty = dirty.toString();\n if (typeof dirty !== 'string') {\n throw typeErrorCreate('dirty is not a string, aborting');\n }\n } else {\n throw typeErrorCreate('toString is not a function');\n }\n }\n\n /* Return dirty HTML if DOMPurify cannot run */\n if (!DOMPurify.isSupported) {\n return dirty;\n }\n\n /* Assign config vars */\n if (!SET_CONFIG) {\n _parseConfig(cfg);\n }\n\n /* Clean up removed elements */\n DOMPurify.removed = [];\n\n /* Check if dirty is correctly typed for IN_PLACE */\n if (typeof dirty === 'string') {\n IN_PLACE = false;\n }\n\n if (IN_PLACE) {\n /* Do some early pre-sanitization to avoid unsafe root nodes */\n if (dirty.nodeName) {\n const tagName = transformCaseFunc(dirty.nodeName);\n if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n throw typeErrorCreate(\n 'root node is forbidden and cannot be sanitized in-place'\n );\n }\n }\n } else if (dirty instanceof Node) {\n /* If dirty is a DOM element, append to an empty document to avoid\n elements being stripped by the parser */\n body = _initDocument('');\n importedNode = body.ownerDocument.importNode(dirty, true);\n if (importedNode.nodeType === 1 && importedNode.nodeName === 'BODY') {\n /* Node is already a body, use as is */\n body = importedNode;\n } else if (importedNode.nodeName === 'HTML') {\n body = importedNode;\n } else {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n body.appendChild(importedNode);\n }\n } else {\n /* Exit directly if we have nothing to do */\n if (\n !RETURN_DOM &&\n !SAFE_FOR_TEMPLATES &&\n !WHOLE_DOCUMENT &&\n // eslint-disable-next-line unicorn/prefer-includes\n dirty.indexOf('<') === -1\n ) {\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE\n ? trustedTypesPolicy.createHTML(dirty)\n : dirty;\n }\n\n /* Initialize the document to work on */\n body = _initDocument(dirty);\n\n /* Check we have a DOM node from the data */\n if (!body) {\n return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';\n }\n }\n\n /* Remove first element node (ours) if FORCE_BODY is set */\n if (body && FORCE_BODY) {\n _forceRemove(body.firstChild);\n }\n\n /* Get node iterator */\n const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);\n\n /* Now start iterating over the created document */\n while ((currentNode = nodeIterator.nextNode())) {\n /* Sanitize tags and elements */\n if (_sanitizeElements(currentNode)) {\n continue;\n }\n\n /* Shadow DOM detected, sanitize it */\n if (currentNode.content instanceof DocumentFragment) {\n _sanitizeShadowDOM(currentNode.content);\n }\n\n /* Check attributes, sanitize if necessary */\n _sanitizeAttributes(currentNode);\n }\n\n /* If we sanitized `dirty` in-place, return it. */\n if (IN_PLACE) {\n return dirty;\n }\n\n /* Return sanitized string or DOM */\n if (RETURN_DOM) {\n if (RETURN_DOM_FRAGMENT) {\n returnNode = createDocumentFragment.call(body.ownerDocument);\n\n while (body.firstChild) {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n returnNode.appendChild(body.firstChild);\n }\n } else {\n returnNode = body;\n }\n\n if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {\n /*\n AdoptNode() is not used because internal state is not reset\n (e.g. the past names map of a HTMLFormElement), this is safe\n in theory but we would rather not risk another attack vector.\n The state that is cloned by importNode() is explicitly defined\n by the specs.\n */\n returnNode = importNode.call(originalDocument, returnNode, true);\n }\n\n return returnNode;\n }\n\n let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;\n\n /* Serialize doctype if allowed */\n if (\n WHOLE_DOCUMENT &&\n ALLOWED_TAGS['!doctype'] &&\n body.ownerDocument &&\n body.ownerDocument.doctype &&\n body.ownerDocument.doctype.name &&\n regExpTest(EXPRESSIONS.DOCTYPE_NAME, body.ownerDocument.doctype.name)\n ) {\n serializedHTML =\n '\\n' + serializedHTML;\n }\n\n /* Sanitize final string template-safe */\n if (SAFE_FOR_TEMPLATES) {\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], (expr) => {\n serializedHTML = stringReplace(serializedHTML, expr, ' ');\n });\n }\n\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE\n ? trustedTypesPolicy.createHTML(serializedHTML)\n : serializedHTML;\n };\n\n /**\n * Public method to set the configuration once\n * setConfig\n *\n * @param {Object} cfg configuration object\n */\n DOMPurify.setConfig = function (cfg = {}) {\n _parseConfig(cfg);\n SET_CONFIG = true;\n };\n\n /**\n * Public method to remove the configuration\n * clearConfig\n *\n */\n DOMPurify.clearConfig = function () {\n CONFIG = null;\n SET_CONFIG = false;\n };\n\n /**\n * Public method to check if an attribute value is valid.\n * Uses last set config, if any. Otherwise, uses config defaults.\n * isValidAttribute\n *\n * @param {String} tag Tag name of containing element.\n * @param {String} attr Attribute name.\n * @param {String} value Attribute value.\n * @return {Boolean} Returns true if `value` is valid. Otherwise, returns false.\n */\n DOMPurify.isValidAttribute = function (tag, attr, value) {\n /* Initialize shared config vars if necessary. */\n if (!CONFIG) {\n _parseConfig({});\n }\n\n const lcTag = transformCaseFunc(tag);\n const lcName = transformCaseFunc(attr);\n return _isValidAttribute(lcTag, lcName, value);\n };\n\n /**\n * AddHook\n * Public method to add DOMPurify hooks\n *\n * @param {String} entryPoint entry point for the hook to add\n * @param {Function} hookFunction function to execute\n */\n DOMPurify.addHook = function (entryPoint, hookFunction) {\n if (typeof hookFunction !== 'function') {\n return;\n }\n\n hooks[entryPoint] = hooks[entryPoint] || [];\n arrayPush(hooks[entryPoint], hookFunction);\n };\n\n /**\n * RemoveHook\n * Public method to remove a DOMPurify hook at a given entryPoint\n * (pops it from the stack of hooks if more are present)\n *\n * @param {String} entryPoint entry point for the hook to remove\n * @return {Function} removed(popped) hook\n */\n DOMPurify.removeHook = function (entryPoint) {\n if (hooks[entryPoint]) {\n return arrayPop(hooks[entryPoint]);\n }\n };\n\n /**\n * RemoveHooks\n * Public method to remove all DOMPurify hooks at a given entryPoint\n *\n * @param {String} entryPoint entry point for the hooks to remove\n */\n DOMPurify.removeHooks = function (entryPoint) {\n if (hooks[entryPoint]) {\n hooks[entryPoint] = [];\n }\n };\n\n /**\n * RemoveAllHooks\n * Public method to remove all DOMPurify hooks\n */\n DOMPurify.removeAllHooks = function () {\n hooks = {};\n };\n\n return DOMPurify;\n}\n\nexport default createDOMPurify();\n"],"names":["entries","setPrototypeOf","isFrozen","getPrototypeOf","getOwnPropertyDescriptor","Object","freeze","seal","create","apply","construct","Reflect","x","fun","thisValue","args","Func","arrayForEach","unapply","Array","prototype","forEach","arrayPop","pop","arrayPush","push","stringToLowerCase","String","toLowerCase","stringToString","toString","stringMatch","match","stringReplace","replace","stringIndexOf","indexOf","stringTrim","trim","objectHasOwnProperty","hasOwnProperty","regExpTest","RegExp","test","typeErrorCreate","unconstruct","TypeError","func","thisArg","_len","arguments","length","_key","_len2","_key2","addToSet","set","array","transformCaseFunc","undefined","l","element","lcElement","cleanArray","index","isPropertyExist","clone","object","newObject","property","value","isArray","constructor","lookupGetter","prop","desc","get","fallbackValue","html","svg","svgFilters","svgDisallowed","mathMl","mathMlDisallowed","text","xml","MUSTACHE_EXPR","ERB_EXPR","TMPLIT_EXPR","DATA_ATTR","ARIA_ATTR","IS_ALLOWED_URI","IS_SCRIPT_OR_DATA","ATTR_WHITESPACE","DOCTYPE_NAME","getGlobal","window","_createTrustedTypesPolicy","trustedTypes","purifyHostElement","createPolicy","suffix","ATTR_NAME","hasAttribute","getAttribute","policyName","createHTML","createScriptURL","scriptUrl","_","console","warn","createDOMPurify","DOMPurify","root","version","VERSION","removed","document","nodeType","isSupported","originalDocument","currentScript","DocumentFragment","HTMLTemplateElement","Node","Element","NodeFilter","NamedNodeMap","MozNamedAttrMap","HTMLFormElement","DOMParser","ElementPrototype","cloneNode","getNextSibling","getChildNodes","getParentNode","template","createElement","content","ownerDocument","trustedTypesPolicy","emptyHTML","implementation","createNodeIterator","createDocumentFragment","getElementsByTagName","importNode","hooks","createHTMLDocument","EXPRESSIONS","ALLOWED_TAGS","DEFAULT_ALLOWED_TAGS","TAGS","ALLOWED_ATTR","DEFAULT_ALLOWED_ATTR","ATTRS","CUSTOM_ELEMENT_HANDLING","tagNameCheck","writable","configurable","enumerable","attributeNameCheck","allowCustomizedBuiltInElements","FORBID_TAGS","FORBID_ATTR","ALLOW_ARIA_ATTR","ALLOW_DATA_ATTR","ALLOW_UNKNOWN_PROTOCOLS","ALLOW_SELF_CLOSE_IN_ATTR","SAFE_FOR_TEMPLATES","WHOLE_DOCUMENT","SET_CONFIG","FORCE_BODY","RETURN_DOM","RETURN_DOM_FRAGMENT","RETURN_TRUSTED_TYPE","SANITIZE_DOM","SANITIZE_NAMED_PROPS","SANITIZE_NAMED_PROPS_PREFIX","KEEP_CONTENT","IN_PLACE","USE_PROFILES","FORBID_CONTENTS","DEFAULT_FORBID_CONTENTS","DATA_URI_TAGS","DEFAULT_DATA_URI_TAGS","URI_SAFE_ATTRIBUTES","DEFAULT_URI_SAFE_ATTRIBUTES","MATHML_NAMESPACE","SVG_NAMESPACE","HTML_NAMESPACE","NAMESPACE","IS_EMPTY_INPUT","ALLOWED_NAMESPACES","DEFAULT_ALLOWED_NAMESPACES","PARSER_MEDIA_TYPE","SUPPORTED_PARSER_MEDIA_TYPES","DEFAULT_PARSER_MEDIA_TYPE","CONFIG","formElement","isRegexOrFunction","testValue","Function","_parseConfig","cfg","ADD_URI_SAFE_ATTR","ADD_DATA_URI_TAGS","ALLOWED_URI_REGEXP","ADD_TAGS","ADD_ATTR","table","tbody","TRUSTED_TYPES_POLICY","MATHML_TEXT_INTEGRATION_POINTS","HTML_INTEGRATION_POINTS","COMMON_SVG_AND_HTML_ELEMENTS","ALL_SVG_TAGS","ALL_MATHML_TAGS","_checkValidNamespace","parent","tagName","namespaceURI","parentTagName","Boolean","_forceRemove","node","parentNode","removeChild","remove","_removeAttribute","name","attribute","getAttributeNode","from","removeAttribute","setAttribute","_initDocument","dirty","doc","leadingWhitespace","matches","dirtyPayload","parseFromString","documentElement","createDocument","innerHTML","body","insertBefore","createTextNode","childNodes","call","_createNodeIterator","SHOW_ELEMENT","SHOW_COMMENT","SHOW_TEXT","_isClobbered","elm","nodeName","textContent","attributes","hasChildNodes","_isNode","_executeHook","entryPoint","currentNode","data","hook","_sanitizeElements","allowedTags","firstElementChild","_isBasicCustomElement","childCount","i","expr","_isValidAttribute","lcTag","lcName","_sanitizeAttributes","hookEvent","attrName","attrValue","keepAttr","allowedAttributes","attr","forceKeepAttr","getAttributeType","setAttributeNS","_sanitizeShadowDOM","fragment","shadowNode","shadowIterator","nextNode","sanitize","importedNode","returnNode","appendChild","firstChild","nodeIterator","shadowroot","shadowrootmode","serializedHTML","outerHTML","doctype","setConfig","clearConfig","isValidAttribute","tag","addHook","hookFunction","removeHook","removeHooks","removeAllHooks"],"mappings":"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"} \ No newline at end of file diff --git a/dependencies/idb/LICENSE b/dependencies/idb/LICENSE new file mode 100644 index 0000000..f8b22ce --- /dev/null +++ b/dependencies/idb/LICENSE @@ -0,0 +1,6 @@ +ISC License (ISC) +Copyright (c) 2016, Jake Archibald + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/dependencies/idb/async-iterators.d.ts b/dependencies/idb/async-iterators.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dependencies/idb/async-iterators.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dependencies/idb/database-extras.d.ts b/dependencies/idb/database-extras.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dependencies/idb/database-extras.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dependencies/idb/entry.d.ts b/dependencies/idb/entry.d.ts new file mode 100644 index 0000000..0bc1f04 --- /dev/null +++ b/dependencies/idb/entry.d.ts @@ -0,0 +1,627 @@ +export interface OpenDBCallbacks { + /** + * Called if this version of the database has never been opened before. Use it to specify the + * schema for the database. + * + * @param database A database instance that you can use to add/remove stores and indexes. + * @param oldVersion Last version of the database opened by the user. + * @param newVersion Whatever new version you provided. + * @param transaction The transaction for this upgrade. + * This is useful if you need to get data from other stores as part of a migration. + * @param event The event object for the associated 'upgradeneeded' event. + */ + upgrade?(database: IDBPDatabase, oldVersion: number, newVersion: number | null, transaction: IDBPTransaction[], 'versionchange'>, event: IDBVersionChangeEvent): void; + /** + * Called if there are older versions of the database open on the origin, so this version cannot + * open. + * + * @param currentVersion Version of the database that's blocking this one. + * @param blockedVersion The version of the database being blocked (whatever version you provided to `openDB`). + * @param event The event object for the associated `blocked` event. + */ + blocked?(currentVersion: number, blockedVersion: number | null, event: IDBVersionChangeEvent): void; + /** + * Called if this connection is blocking a future version of the database from opening. + * + * @param currentVersion Version of the open database (whatever version you provided to `openDB`). + * @param blockedVersion The version of the database that's being blocked. + * @param event The event object for the associated `versionchange` event. + */ + blocking?(currentVersion: number, blockedVersion: number | null, event: IDBVersionChangeEvent): void; + /** + * Called if the browser abnormally terminates the connection. + * This is not called when `db.close()` is called. + */ + terminated?(): void; +} +/** + * Open a database. + * + * @param name Name of the database. + * @param version Schema version. + * @param callbacks Additional callbacks. + */ +export declare function openDB(name: string, version?: number, { blocked, upgrade, blocking, terminated }?: OpenDBCallbacks): Promise>; +export interface DeleteDBCallbacks { + /** + * Called if there are connections to this database open, so it cannot be deleted. + * + * @param currentVersion Version of the database that's blocking the delete operation. + * @param event The event object for the associated `blocked` event. + */ + blocked?(currentVersion: number, event: IDBVersionChangeEvent): void; +} +/** + * Delete a database. + * + * @param name Name of the database. + */ +export declare function deleteDB(name: string, { blocked }?: DeleteDBCallbacks): Promise; +export { unwrap, wrap } from './wrap-idb-value.js'; +type KeyToKeyNoIndex = { + [K in keyof T]: string extends K ? never : number extends K ? never : K; +}; +type ValuesOf = T extends { + [K in keyof T]: infer U; +} ? U : never; +type KnownKeys = ValuesOf>; +type Omit = Pick>; +export interface DBSchema { + [s: string]: DBSchemaValue; +} +interface IndexKeys { + [s: string]: IDBValidKey; +} +interface DBSchemaValue { + key: IDBValidKey; + value: any; + indexes?: IndexKeys; +} +/** + * Extract known object store names from the DB schema type. + * + * @template DBTypes DB schema type, or unknown if the DB isn't typed. + */ +export type StoreNames = DBTypes extends DBSchema ? KnownKeys : string; +/** + * Extract database value types from the DB schema type. + * + * @template DBTypes DB schema type, or unknown if the DB isn't typed. + * @template StoreName Names of the object stores to get the types of. + */ +export type StoreValue> = DBTypes extends DBSchema ? DBTypes[StoreName]['value'] : any; +/** + * Extract database key types from the DB schema type. + * + * @template DBTypes DB schema type, or unknown if the DB isn't typed. + * @template StoreName Names of the object stores to get the types of. + */ +export type StoreKey> = DBTypes extends DBSchema ? DBTypes[StoreName]['key'] : IDBValidKey; +/** + * Extract the names of indexes in certain object stores from the DB schema type. + * + * @template DBTypes DB schema type, or unknown if the DB isn't typed. + * @template StoreName Names of the object stores to get the types of. + */ +export type IndexNames> = DBTypes extends DBSchema ? keyof DBTypes[StoreName]['indexes'] : string; +/** + * Extract the types of indexes in certain object stores from the DB schema type. + * + * @template DBTypes DB schema type, or unknown if the DB isn't typed. + * @template StoreName Names of the object stores to get the types of. + * @template IndexName Names of the indexes to get the types of. + */ +export type IndexKey, IndexName extends IndexNames> = DBTypes extends DBSchema ? IndexName extends keyof DBTypes[StoreName]['indexes'] ? DBTypes[StoreName]['indexes'][IndexName] : IDBValidKey : IDBValidKey; +type CursorSource>, StoreName extends StoreNames, IndexName extends IndexNames | unknown, Mode extends IDBTransactionMode = 'readonly'> = IndexName extends IndexNames ? IDBPIndex : IDBPObjectStore; +type CursorKey, IndexName extends IndexNames | unknown> = IndexName extends IndexNames ? IndexKey : StoreKey; +type IDBPDatabaseExtends = Omit; +/** + * A variation of DOMStringList with precise string types + */ +export interface TypedDOMStringList extends DOMStringList { + contains(string: T): boolean; + item(index: number): T | null; + [index: number]: T; + [Symbol.iterator](): IterableIterator; +} +interface IDBTransactionOptions { + /** + * The durability of the transaction. + * + * The default is "default". Using "relaxed" provides better performance, but with fewer + * guarantees. Web applications are encouraged to use "relaxed" for ephemeral data such as caches + * or quickly changing records, and "strict" in cases where reducing the risk of data loss + * outweighs the impact to performance and power. + */ + durability?: 'default' | 'strict' | 'relaxed'; +} +export interface IDBPDatabase extends IDBPDatabaseExtends { + /** + * The names of stores in the database. + */ + readonly objectStoreNames: TypedDOMStringList>; + /** + * Creates a new object store. + * + * Throws a "InvalidStateError" DOMException if not called within an upgrade transaction. + */ + createObjectStore>(name: Name, optionalParameters?: IDBObjectStoreParameters): IDBPObjectStore>, Name, 'versionchange'>; + /** + * Deletes the object store with the given name. + * + * Throws a "InvalidStateError" DOMException if not called within an upgrade transaction. + */ + deleteObjectStore(name: StoreNames): void; + /** + * Start a new transaction. + * + * @param storeNames The object store(s) this transaction needs. + * @param mode + * @param options + */ + transaction, Mode extends IDBTransactionMode = 'readonly'>(storeNames: Name, mode?: Mode, options?: IDBTransactionOptions): IDBPTransaction; + transaction>, Mode extends IDBTransactionMode = 'readonly'>(storeNames: Names, mode?: Mode, options?: IDBTransactionOptions): IDBPTransaction; + /** + * Add a value to a store. + * + * Rejects if an item of a given key already exists in the store. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param value + * @param key + */ + add>(storeName: Name, value: StoreValue, key?: StoreKey | IDBKeyRange): Promise>; + /** + * Deletes all records in a store. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + */ + clear(name: StoreNames): Promise; + /** + * Retrieves the number of records matching the given query in a store. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param key + */ + count>(storeName: Name, key?: StoreKey | IDBKeyRange | null): Promise; + /** + * Retrieves the number of records matching the given query in an index. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param indexName Name of the index within the store. + * @param key + */ + countFromIndex, IndexName extends IndexNames>(storeName: Name, indexName: IndexName, key?: IndexKey | IDBKeyRange | null): Promise; + /** + * Deletes records in a store matching the given query. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param key + */ + delete>(storeName: Name, key: StoreKey | IDBKeyRange): Promise; + /** + * Retrieves the value of the first record in a store matching the query. + * + * Resolves with undefined if no match is found. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param query + */ + get>(storeName: Name, query: StoreKey | IDBKeyRange): Promise | undefined>; + /** + * Retrieves the value of the first record in an index matching the query. + * + * Resolves with undefined if no match is found. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param indexName Name of the index within the store. + * @param query + */ + getFromIndex, IndexName extends IndexNames>(storeName: Name, indexName: IndexName, query: IndexKey | IDBKeyRange): Promise | undefined>; + /** + * Retrieves all values in a store that match the query. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param query + * @param count Maximum number of values to return. + */ + getAll>(storeName: Name, query?: StoreKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves all values in an index that match the query. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param indexName Name of the index within the store. + * @param query + * @param count Maximum number of values to return. + */ + getAllFromIndex, IndexName extends IndexNames>(storeName: Name, indexName: IndexName, query?: IndexKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the keys of records in a store matching the query. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param query + * @param count Maximum number of keys to return. + */ + getAllKeys>(storeName: Name, query?: StoreKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the keys of records in an index matching the query. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param indexName Name of the index within the store. + * @param query + * @param count Maximum number of keys to return. + */ + getAllKeysFromIndex, IndexName extends IndexNames>(storeName: Name, indexName: IndexName, query?: IndexKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the key of the first record in a store that matches the query. + * + * Resolves with undefined if no match is found. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param query + */ + getKey>(storeName: Name, query: StoreKey | IDBKeyRange): Promise | undefined>; + /** + * Retrieves the key of the first record in an index that matches the query. + * + * Resolves with undefined if no match is found. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param indexName Name of the index within the store. + * @param query + */ + getKeyFromIndex, IndexName extends IndexNames>(storeName: Name, indexName: IndexName, query: IndexKey | IDBKeyRange): Promise | undefined>; + /** + * Put an item in the database. + * + * Replaces any item with the same key. + * + * This is a shortcut that creates a transaction for this single action. If you need to do more + * than one action, create a transaction instead. + * + * @param storeName Name of the store. + * @param value + * @param key + */ + put>(storeName: Name, value: StoreValue, key?: StoreKey | IDBKeyRange): Promise>; +} +type IDBPTransactionExtends = Omit; +export interface IDBPTransaction> = ArrayLike>, Mode extends IDBTransactionMode = 'readonly'> extends IDBPTransactionExtends { + /** + * The transaction's mode. + */ + readonly mode: Mode; + /** + * The names of stores in scope for this transaction. + */ + readonly objectStoreNames: TypedDOMStringList; + /** + * The transaction's connection. + */ + readonly db: IDBPDatabase; + /** + * Promise for the completion of this transaction. + */ + readonly done: Promise; + /** + * The associated object store, if the transaction covers a single store, otherwise undefined. + */ + readonly store: TxStores[1] extends undefined ? IDBPObjectStore : undefined; + /** + * Returns an IDBObjectStore in the transaction's scope. + */ + objectStore(name: StoreName): IDBPObjectStore; +} +type IDBPObjectStoreExtends = Omit; +export interface IDBPObjectStore> = ArrayLike>, StoreName extends StoreNames = StoreNames, Mode extends IDBTransactionMode = 'readonly'> extends IDBPObjectStoreExtends { + /** + * The names of indexes in the store. + */ + readonly indexNames: TypedDOMStringList>; + /** + * The associated transaction. + */ + readonly transaction: IDBPTransaction; + /** + * Add a value to the store. + * + * Rejects if an item of a given key already exists in the store. + */ + add: Mode extends 'readonly' ? undefined : (value: StoreValue, key?: StoreKey | IDBKeyRange) => Promise>; + /** + * Deletes all records in store. + */ + clear: Mode extends 'readonly' ? undefined : () => Promise; + /** + * Retrieves the number of records matching the given query. + */ + count(key?: StoreKey | IDBKeyRange | null): Promise; + /** + * Creates a new index in store. + * + * Throws an "InvalidStateError" DOMException if not called within an upgrade transaction. + */ + createIndex: Mode extends 'versionchange' ? >(name: IndexName, keyPath: string | string[], options?: IDBIndexParameters) => IDBPIndex : undefined; + /** + * Deletes records in store matching the given query. + */ + delete: Mode extends 'readonly' ? undefined : (key: StoreKey | IDBKeyRange) => Promise; + /** + * Retrieves the value of the first record matching the query. + * + * Resolves with undefined if no match is found. + */ + get(query: StoreKey | IDBKeyRange): Promise | undefined>; + /** + * Retrieves all values that match the query. + * + * @param query + * @param count Maximum number of values to return. + */ + getAll(query?: StoreKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the keys of records matching the query. + * + * @param query + * @param count Maximum number of keys to return. + */ + getAllKeys(query?: StoreKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the key of the first record that matches the query. + * + * Resolves with undefined if no match is found. + */ + getKey(query: StoreKey | IDBKeyRange): Promise | undefined>; + /** + * Get a query of a given name. + */ + index>(name: IndexName): IDBPIndex; + /** + * Opens a cursor over the records matching the query. + * + * Resolves with null if no matches are found. + * + * @param query If null, all records match. + * @param direction + */ + openCursor(query?: StoreKey | IDBKeyRange | null, direction?: IDBCursorDirection): Promise | null>; + /** + * Opens a cursor over the keys matching the query. + * + * Resolves with null if no matches are found. + * + * @param query If null, all records match. + * @param direction + */ + openKeyCursor(query?: StoreKey | IDBKeyRange | null, direction?: IDBCursorDirection): Promise | null>; + /** + * Put an item in the store. + * + * Replaces any item with the same key. + */ + put: Mode extends 'readonly' ? undefined : (value: StoreValue, key?: StoreKey | IDBKeyRange) => Promise>; + /** + * Iterate over the store. + */ + [Symbol.asyncIterator](): AsyncIterableIterator>; + /** + * Iterate over the records matching the query. + * + * @param query If null, all records match. + * @param direction + */ + iterate(query?: StoreKey | IDBKeyRange | null, direction?: IDBCursorDirection): AsyncIterableIterator>; +} +type IDBPIndexExtends = Omit; +export interface IDBPIndex> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames = IndexNames, Mode extends IDBTransactionMode = 'readonly'> extends IDBPIndexExtends { + /** + * The IDBObjectStore the index belongs to. + */ + readonly objectStore: IDBPObjectStore; + /** + * Retrieves the number of records matching the given query. + */ + count(key?: IndexKey | IDBKeyRange | null): Promise; + /** + * Retrieves the value of the first record matching the query. + * + * Resolves with undefined if no match is found. + */ + get(query: IndexKey | IDBKeyRange): Promise | undefined>; + /** + * Retrieves all values that match the query. + * + * @param query + * @param count Maximum number of values to return. + */ + getAll(query?: IndexKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the keys of records matching the query. + * + * @param query + * @param count Maximum number of keys to return. + */ + getAllKeys(query?: IndexKey | IDBKeyRange | null, count?: number): Promise[]>; + /** + * Retrieves the key of the first record that matches the query. + * + * Resolves with undefined if no match is found. + */ + getKey(query: IndexKey | IDBKeyRange): Promise | undefined>; + /** + * Opens a cursor over the records matching the query. + * + * Resolves with null if no matches are found. + * + * @param query If null, all records match. + * @param direction + */ + openCursor(query?: IndexKey | IDBKeyRange | null, direction?: IDBCursorDirection): Promise | null>; + /** + * Opens a cursor over the keys matching the query. + * + * Resolves with null if no matches are found. + * + * @param query If null, all records match. + * @param direction + */ + openKeyCursor(query?: IndexKey | IDBKeyRange | null, direction?: IDBCursorDirection): Promise | null>; + /** + * Iterate over the index. + */ + [Symbol.asyncIterator](): AsyncIterableIterator>; + /** + * Iterate over the records matching the query. + * + * Resolves with null if no matches are found. + * + * @param query If null, all records match. + * @param direction + */ + iterate(query?: IndexKey | IDBKeyRange | null, direction?: IDBCursorDirection): AsyncIterableIterator>; +} +type IDBPCursorExtends = Omit; +export interface IDBPCursor> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> extends IDBPCursorExtends { + /** + * The key of the current index or object store item. + */ + readonly key: CursorKey; + /** + * The key of the current object store item. + */ + readonly primaryKey: StoreKey; + /** + * Returns the IDBObjectStore or IDBIndex the cursor was opened from. + */ + readonly source: CursorSource; + /** + * Advances the cursor a given number of records. + * + * Resolves to null if no matching records remain. + */ + advance(this: T, count: number): Promise; + /** + * Advance the cursor by one record (unless 'key' is provided). + * + * Resolves to null if no matching records remain. + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + */ + continue(this: T, key?: CursorKey): Promise; + /** + * Advance the cursor by given keys. + * + * The operation is 'and' – both keys must be satisfied. + * + * Resolves to null if no matching records remain. + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + * @param primaryKey and where the object store has a key equal to or greater than this value. + */ + continuePrimaryKey(this: T, key: CursorKey, primaryKey: StoreKey): Promise; + /** + * Delete the current record. + */ + delete: Mode extends 'readonly' ? undefined : () => Promise; + /** + * Updated the current record. + */ + update: Mode extends 'readonly' ? undefined : (value: StoreValue) => Promise>; + /** + * Iterate over the cursor. + */ + [Symbol.asyncIterator](): AsyncIterableIterator>; +} +type IDBPCursorIteratorValueExtends> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> = Omit, 'advance' | 'continue' | 'continuePrimaryKey'>; +export interface IDBPCursorIteratorValue> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> extends IDBPCursorIteratorValueExtends { + /** + * Advances the cursor a given number of records. + */ + advance(this: T, count: number): void; + /** + * Advance the cursor by one record (unless 'key' is provided). + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + */ + continue(this: T, key?: CursorKey): void; + /** + * Advance the cursor by given keys. + * + * The operation is 'and' – both keys must be satisfied. + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + * @param primaryKey and where the object store has a key equal to or greater than this value. + */ + continuePrimaryKey(this: T, key: CursorKey, primaryKey: StoreKey): void; +} +export interface IDBPCursorWithValue> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> extends IDBPCursor { + /** + * The value of the current item. + */ + readonly value: StoreValue; + /** + * Iterate over the cursor. + */ + [Symbol.asyncIterator](): AsyncIterableIterator>; +} +type IDBPCursorWithValueIteratorValueExtends> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> = Omit, 'advance' | 'continue' | 'continuePrimaryKey'>; +export interface IDBPCursorWithValueIteratorValue> = ArrayLike>, StoreName extends StoreNames = StoreNames, IndexName extends IndexNames | unknown = unknown, Mode extends IDBTransactionMode = 'readonly'> extends IDBPCursorWithValueIteratorValueExtends { + /** + * Advances the cursor a given number of records. + */ + advance(this: T, count: number): void; + /** + * Advance the cursor by one record (unless 'key' is provided). + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + */ + continue(this: T, key?: CursorKey): void; + /** + * Advance the cursor by given keys. + * + * The operation is 'and' – both keys must be satisfied. + * + * @param key Advance to the index or object store with a key equal to or greater than this value. + * @param primaryKey and where the object store has a key equal to or greater than this value. + */ + continuePrimaryKey(this: T, key: CursorKey, primaryKey: StoreKey): void; +} diff --git a/dependencies/idb/index.d.ts b/dependencies/idb/index.d.ts new file mode 100644 index 0000000..02bc7a4 --- /dev/null +++ b/dependencies/idb/index.d.ts @@ -0,0 +1,3 @@ +export * from './entry.js'; +import './database-extras.js'; +import './async-iterators.js'; diff --git a/dependencies/idb/index.js b/dependencies/idb/index.js new file mode 100644 index 0000000..9e4feb5 --- /dev/null +++ b/dependencies/idb/index.js @@ -0,0 +1,292 @@ +const instanceOfAny = (object, constructors) => constructors.some((c) => object instanceof c) + +let idbProxyableTypes +let cursorAdvanceMethods +// This is a function to prevent it throwing up in node environments. +function getIdbProxyableTypes () { + return (idbProxyableTypes || + (idbProxyableTypes = [ + IDBDatabase, + IDBObjectStore, + IDBIndex, + IDBCursor, + IDBTransaction + ])) +} +// This is a function to prevent it throwing up in node environments. +function getCursorAdvanceMethods () { + return (cursorAdvanceMethods || + (cursorAdvanceMethods = [ + IDBCursor.prototype.advance, + IDBCursor.prototype.continue, + IDBCursor.prototype.continuePrimaryKey + ])) +} +const transactionDoneMap = new WeakMap() +const transformCache = new WeakMap() +const reverseTransformCache = new WeakMap() +function promisifyRequest (request) { + const promise = new Promise((resolve, reject) => { + const unlisten = () => { + request.removeEventListener('success', success) + request.removeEventListener('error', error) + } + const success = () => { + resolve(wrap(request.result)) + unlisten() + } + const error = () => { + reject(request.error) + unlisten() + } + request.addEventListener('success', success) + request.addEventListener('error', error) + }) + // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This + // is because we create many promises from a single IDBRequest. + reverseTransformCache.set(promise, request) + return promise +} +function cacheDonePromiseForTransaction (tx) { + // Early bail if we've already created a done promise for this transaction. + if (transactionDoneMap.has(tx)) { return } + const done = new Promise((resolve, reject) => { + const unlisten = () => { + tx.removeEventListener('complete', complete) + tx.removeEventListener('error', error) + tx.removeEventListener('abort', error) + } + const complete = () => { + resolve() + unlisten() + } + const error = () => { + reject(tx.error || new DOMException('AbortError', 'AbortError')) + unlisten() + } + tx.addEventListener('complete', complete) + tx.addEventListener('error', error) + tx.addEventListener('abort', error) + }) + // Cache it for later retrieval. + transactionDoneMap.set(tx, done) +} +let idbProxyTraps = { + get (target, prop, receiver) { + if (target instanceof IDBTransaction) { + // Special handling for transaction.done. + if (prop === 'done') { return transactionDoneMap.get(target) } + // Make tx.store return the only store in the transaction, or undefined if there are many. + if (prop === 'store') { + return receiver.objectStoreNames[1] + ? undefined + : receiver.objectStore(receiver.objectStoreNames[0]) + } + } + // Else transform whatever we get back. + return wrap(target[prop]) + }, + set (target, prop, value) { + target[prop] = value + return true + }, + has (target, prop) { + if (target instanceof IDBTransaction && + (prop === 'done' || prop === 'store')) { + return true + } + return prop in target + } +} +function replaceTraps (callback) { + idbProxyTraps = callback(idbProxyTraps) +} +function wrapFunction (func) { + // Due to expected object equality (which is enforced by the caching in `wrap`), we + // only create one new func per func. + // Cursor methods are special, as the behaviour is a little more different to standard IDB. In + // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the + // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense + // with real promises, so each advance methods returns a new promise for the cursor object, or + // undefined if the end of the cursor has been reached. + if (getCursorAdvanceMethods().includes(func)) { + return function (...args) { + // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use + // the original object. + func.apply(unwrap(this), args) + return wrap(this.request) + } + } + return function (...args) { + // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use + // the original object. + return wrap(func.apply(unwrap(this), args)) + } +} +function transformCachableValue (value) { + if (typeof value === 'function') { return wrapFunction(value) } + // This doesn't return, it just creates a 'done' promise for the transaction, + // which is later returned for transaction.done (see idbObjectHandler). + if (value instanceof IDBTransaction) { cacheDonePromiseForTransaction(value) } + if (instanceOfAny(value, getIdbProxyableTypes())) { return new Proxy(value, idbProxyTraps) } + // Return the same value back if we're not going to transform it. + return value +} +function wrap (value) { + // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because + // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached. + if (value instanceof IDBRequest) { return promisifyRequest(value) } + // If we've already transformed this value before, reuse the transformed value. + // This is faster, but it also provides object equality. + if (transformCache.has(value)) { return transformCache.get(value) } + const newValue = transformCachableValue(value) + // Not all types are transformed. + // These may be primitive types, so they can't be WeakMap keys. + if (newValue !== value) { + transformCache.set(value, newValue) + reverseTransformCache.set(newValue, value) + } + return newValue +} +const unwrap = (value) => reverseTransformCache.get(value) + +/** + * Open a database. + * + * @param name Name of the database. + * @param version Schema version. + * @param callbacks Additional callbacks. + */ +function openDB (name, version, { blocked, upgrade, blocking, terminated } = {}) { + const request = indexedDB.open(name, version) + const openPromise = wrap(request) + if (upgrade) { + request.addEventListener('upgradeneeded', (event) => { + upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event) + }) + } + if (blocked) { + request.addEventListener('blocked', (event) => blocked( + // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405 + event.oldVersion, event.newVersion, event)) + } + openPromise + .then((db) => { + if (terminated) { db.addEventListener('close', () => terminated()) } + if (blocking) { + db.addEventListener('versionchange', (event) => blocking(event.oldVersion, event.newVersion, event)) + } + }) + .catch(() => { }) + return openPromise +} +/** + * Delete a database. + * + * @param name Name of the database. + */ +function deleteDB (name, { blocked } = {}) { + const request = indexedDB.deleteDatabase(name) + if (blocked) { + request.addEventListener('blocked', (event) => blocked( + // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405 + event.oldVersion, event)) + } + return wrap(request).then(() => undefined) +} + +const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'] +const writeMethods = ['put', 'add', 'delete', 'clear'] +const cachedMethods = new Map() +function getMethod (target, prop) { + if (!(target instanceof IDBDatabase && + !(prop in target) && + typeof prop === 'string')) { + return + } + if (cachedMethods.get(prop)) { return cachedMethods.get(prop) } + const targetFuncName = prop.replace(/FromIndex$/, '') + const useIndex = prop !== targetFuncName + const isWrite = writeMethods.includes(targetFuncName) + if ( + // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge. + !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || + !(isWrite || readMethods.includes(targetFuncName))) { + return + } + const method = async function (storeName, ...args) { + // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :( + const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly') + let target = tx.store + if (useIndex) { target = target.index(args.shift()) } + // Must reject if op rejects. + // If it's a write operation, must reject if tx.done rejects. + // Must reject with op rejection first. + // Must resolve with op value. + // Must handle both promises (no unhandled rejections) + return (await Promise.all([ + target[targetFuncName](...args), + isWrite && tx.done + ]))[0] + } + cachedMethods.set(prop, method) + return method +} +replaceTraps((oldTraps) => ({ + ...oldTraps, + get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver), + has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop) +})) + +const advanceMethodProps = ['continue', 'continuePrimaryKey', 'advance'] +const methodMap = {} +const advanceResults = new WeakMap() +const ittrProxiedCursorToOriginalProxy = new WeakMap() +const cursorIteratorTraps = { + get (target, prop) { + if (!advanceMethodProps.includes(prop)) { return target[prop] } + let cachedFunc = methodMap[prop] + if (!cachedFunc) { + cachedFunc = methodMap[prop] = function (...args) { + advanceResults.set(this, ittrProxiedCursorToOriginalProxy.get(this)[prop](...args)) + } + } + return cachedFunc + } +} +async function * iterate (...args) { + // tslint:disable-next-line:no-this-assignment + let cursor = this + if (!(cursor instanceof IDBCursor)) { + cursor = await cursor.openCursor(...args) + } + if (!cursor) { return } + cursor = cursor + const proxiedCursor = new Proxy(cursor, cursorIteratorTraps) + ittrProxiedCursorToOriginalProxy.set(proxiedCursor, cursor) + // Map this double-proxy back to the original, so other cursor methods work. + reverseTransformCache.set(proxiedCursor, unwrap(cursor)) + while (cursor) { + yield proxiedCursor + // If one of the advancing methods was not called, call continue(). + cursor = await (advanceResults.get(proxiedCursor) || cursor.continue()) + advanceResults.delete(proxiedCursor) + } +} +function isIteratorProp (target, prop) { + return ((prop === Symbol.asyncIterator && + instanceOfAny(target, [IDBIndex, IDBObjectStore, IDBCursor])) || + (prop === 'iterate' && instanceOfAny(target, [IDBIndex, IDBObjectStore]))) +} +replaceTraps((oldTraps) => ({ + ...oldTraps, + get (target, prop, receiver) { + if (isIteratorProp(target, prop)) { return iterate } + return oldTraps.get(target, prop, receiver) + }, + has (target, prop) { + return isIteratorProp(target, prop) || oldTraps.has(target, prop) + } +})) + +export { deleteDB, openDB, unwrap, wrap } diff --git a/dependencies/idb/util.d.ts b/dependencies/idb/util.d.ts new file mode 100644 index 0000000..8bb5539 --- /dev/null +++ b/dependencies/idb/util.d.ts @@ -0,0 +1,3 @@ +export type Constructor = new (...args: any[]) => any; +export type Func = (...args: any[]) => any; +export declare const instanceOfAny: (object: any, constructors: Constructor[]) => boolean; diff --git a/dependencies/idb/wrap-idb-value.d.ts b/dependencies/idb/wrap-idb-value.d.ts new file mode 100644 index 0000000..a8d8cd4 --- /dev/null +++ b/dependencies/idb/wrap-idb-value.d.ts @@ -0,0 +1,34 @@ +import { IDBPCursor, IDBPCursorWithValue, IDBPDatabase, IDBPIndex, IDBPObjectStore, IDBPTransaction } from './entry.js'; +export declare const reverseTransformCache: WeakMap; +export declare function replaceTraps(callback: (currentTraps: ProxyHandler) => ProxyHandler): void; +/** + * Enhance an IDB object with helpers. + * + * @param value The thing to enhance. + */ +export declare function wrap(value: IDBDatabase): IDBPDatabase; +export declare function wrap(value: IDBIndex): IDBPIndex; +export declare function wrap(value: IDBObjectStore): IDBPObjectStore; +export declare function wrap(value: IDBTransaction): IDBPTransaction; +export declare function wrap(value: IDBOpenDBRequest): Promise; +export declare function wrap(value: IDBRequest): Promise; +/** + * Revert an enhanced IDB object to a plain old miserable IDB one. + * + * Will also revert a promise back to an IDBRequest. + * + * @param value The enhanced object to revert. + */ +interface Unwrap { + (value: IDBPCursorWithValue): IDBCursorWithValue; + (value: IDBPCursor): IDBCursor; + (value: IDBPDatabase): IDBDatabase; + (value: IDBPIndex): IDBIndex; + (value: IDBPObjectStore): IDBObjectStore; + (value: IDBPTransaction): IDBTransaction; + (value: Promise>): IDBOpenDBRequest; + (value: Promise): IDBOpenDBRequest; + (value: Promise): IDBRequest; +} +export declare const unwrap: Unwrap; +export {}; diff --git a/error-message.js b/error-message.js new file mode 100644 index 0000000..b8d5ad5 --- /dev/null +++ b/error-message.js @@ -0,0 +1,36 @@ +class ErrorMessage extends HTMLElement { + constructor () { + super() + this.attachShadow({ mode: 'open' }) + + // Create the main element for the error message + const errorElement = document.createElement('p') + errorElement.classList.add('error') + errorElement.textContent = + this.getAttribute('message') || 'An error occurred' + + const style = document.createElement('style') + style.textContent = ` + .error { + color: var(--rdp-details-color); + text-align: center; + margin: 20px; + font-size: 1rem; + } + ` + + this.shadowRoot.append(style, errorElement) + } + + static get observedAttributes () { + return ['message'] + } + + attributeChangedCallback (name, oldValue, newValue) { + if (name === 'message' && oldValue !== newValue) { + this.shadowRoot.querySelector('.error').textContent = newValue + } + } +} + +customElements.define('error-message', ErrorMessage) diff --git a/example/outbox.html b/example/outbox.html new file mode 100644 index 0000000..474b62f --- /dev/null +++ b/example/outbox.html @@ -0,0 +1,17 @@ + +Reader Outbox + +
+ + + +
+ + + diff --git a/example/post.html b/example/post.html new file mode 100644 index 0000000..5b99b51 --- /dev/null +++ b/example/post.html @@ -0,0 +1,28 @@ + +Reader Post + +
+ + + + +
+ + + + diff --git a/followed-accounts.css b/followed-accounts.css new file mode 100644 index 0000000..6cd25e7 --- /dev/null +++ b/followed-accounts.css @@ -0,0 +1,34 @@ +.followed-container { + flex: 1; + max-width: 600px; + width: 100%; + margin: 0 20px; + color: var(--rdp-text-color); + display: flex; + flex-direction: column; + align-items: center; + margin-top: 10px; +} + +.imp-exp-btn { + margin-bottom: 20px; +} + +#exportFollowedList, +#importFollowedList { + cursor: pointer; +} + +.cache-warning-msg { + font-size: 0.775em; + margin-top: 10px; +} + +followed-actors-list { + text-align: left; + color: var(--rdp-text-color); + width: 80%; + max-width: fit-content; + margin: 0 auto; + overflow-wrap: break-word; +} diff --git a/followed-accounts.html b/followed-accounts.html new file mode 100644 index 0000000..1a328c9 --- /dev/null +++ b/followed-accounts.html @@ -0,0 +1,65 @@ + + + +Followed Accounts + +
+ +
+

+ You're following + accounts on the + fediverse.
+ To sync your list of followed accounts across multiple devices, be sure + to ⬆️ export your followed list from one device and 📥 import it on the + other. +

+
+ + + +
+ + ⚠️ Before clearing your browser's cache, ensure you export your followed + list. +
+
+ +
+
+ + + + + diff --git a/followed-accounts.js b/followed-accounts.js new file mode 100644 index 0000000..1b5de4c --- /dev/null +++ b/followed-accounts.js @@ -0,0 +1,128 @@ +import { db } from './dbInstance.js' + +export class FollowedActorsList extends HTMLElement { + constructor () { + super() + this.updateFollowedActors = this.updateFollowedActors.bind(this) + } + + connectedCallback () { + this.renderFollowedActors() + + db.addEventListener('actorFollowed', this.updateFollowedActors) + db.addEventListener('actorUnfollowed', this.updateFollowedActors) + + this.addEventListener('exportFollowed', FollowedActorsList.exportFollowedList) + this.addEventListener('importFollowed', (e) => { + FollowedActorsList.importFollowedList(e.detail.file) + }) + } + + disconnectedCallback () { + db.removeEventListener('actorFollowed', this.updateFollowedActors) + db.removeEventListener('actorUnfollowed', this.updateFollowedActors) + + this.removeEventListener('exportFollowed', FollowedActorsList.exportFollowedList) + this.removeEventListener('importFollowed', FollowedActorsList.importFollowedList) + } + + async updateFollowedActors () { + await this.renderFollowedActors() + const followCount = document.querySelector('followed-count') + if (followCount) { + followCount.updateCount() + } + } + + async renderFollowedActors () { + const followedActors = await db.getFollowedActors() + this.innerHTML = '' + followedActors.forEach((actor) => { + const actorElement = document.createElement('actor-mini-profile') + actorElement.setAttribute('url', actor.url) + actorElement.setAttribute('followed-at', this.formatDate(actor.followedAt)) + this.appendChild(actorElement) + }) + } + + formatDate (dateString) { + const options = { year: 'numeric', month: 'long', day: 'numeric' } + const date = new Date(dateString) + return date.toLocaleDateString('en-US', options) + } + + static async exportFollowedList () { + const followedActors = await db.getFollowedActors() + const blob = new Blob([JSON.stringify(followedActors, null, 2)], { + type: 'application/json' + }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'reader-followed-accounts.json' + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) + } + + static async importFollowedList (file) { + const reader = new FileReader() + reader.onload = async (e) => { + const followedActors = JSON.parse(e.target.result) + for (const actor of followedActors) { + if (!(await db.isActorFollowed(actor.url))) { + await db.followActor(actor.url) + } + } + } + reader.readAsText(file) + } +} + +customElements.define('followed-actors-list', FollowedActorsList) + +class FollowedCount extends HTMLElement { + connectedCallback () { + this.updateCountOnLoad() + db.addEventListener('actorFollowed', () => this.updateCount()) + db.addEventListener('actorUnfollowed', () => this.updateCount()) + } + + disconnectedCallback () { + db.removeEventListener('actorFollowed', () => this.updateCount()) + db.removeEventListener('actorUnfollowed', () => this.updateCount()) + } + + async updateCountOnLoad () { + setTimeout(() => this.updateCount(), 100) + } + + async updateCount () { + const followedActors = await db.getFollowedActors() + this.textContent = followedActors.length + } +} + +customElements.define('followed-count', FollowedCount) + +// test following/unfollowing +// (async () => { +// const actorUrl1 = "https://example.com/actor/1"; +// const actorUrl2 = "https://example.com/actor/2"; + +// console.log("Following actors..."); +// await db.followActor(actorUrl1); +// await db.followActor(actorUrl2); + +// console.log("Retrieving followed actors..."); +// let followedActors = await db.getFollowedActors(); +// console.log("Followed Actors:", followedActors); + +// console.log("Unfollowing an actor..."); +// await db.unfollowActor(actorUrl2); + +// console.log("Retrieving followed actors after unfollowing..."); +// followedActors = await db.getFollowedActors(); +// console.log("Followed Actors after unfollowing:", followedActors); +// })(); diff --git a/index.css b/index.css new file mode 100644 index 0000000..f268d71 --- /dev/null +++ b/index.css @@ -0,0 +1,13 @@ +/* Vars */ +:root { + --bg-color: #ffffff; +} + +/* Main styles */ +html { + background: var(--bg-color); +} + +* { + box-sizing: border-box; +} \ No newline at end of file diff --git a/index.html b/index.html index febeba1..54d695c 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,27 @@ -Distributed Press Reader + + +Social Reader - -

- Work in progress, check back later or follow us on GitHub. -

+
+ + +
+ +
+
+ + + + + + + + diff --git a/outbox.css b/outbox.css new file mode 100644 index 0000000..6957cd9 --- /dev/null +++ b/outbox.css @@ -0,0 +1,5 @@ +.repost-label { + color: var(--rdp-details-color); + font-size: 0.875rem; + text-align: center; +} diff --git a/outbox.js b/outbox.js new file mode 100644 index 0000000..330b9d5 --- /dev/null +++ b/outbox.js @@ -0,0 +1,256 @@ +import { db } from './dbInstance.js' + +class DistributedOutbox extends HTMLElement { + skip = 0 + limit = 32 + + constructor () { + super() + this.renderedItems = new Map() // Tracks rendered items by ID + } + + static get observedAttributes () { + return ['url'] + } + + connectedCallback () { + this.outboxUrl = this.getAttribute('url') + this.loadOutbox(this.outboxUrl) + } + + async loadOutbox (outboxUrl) { + this.clearContent() + const items = await this.collectItems(outboxUrl, { skip: this.skip, limit: this.limit + 1 }) + items.slice(0, this.limit).forEach(item => this.processItem(item)) + + // Update skip for next potential load + this.skip += this.limit + + // Check if there are more items to load + if (items.length > this.limit) { + this.createLoadMoreButton() + } + } + + async loadMore () { + this.removeLoadMoreButton() + const items = await this.collectItems(this.outboxUrl, { skip: this.skip, limit: this.limit + 1 }) + items.slice(0, this.limit).forEach(item => this.processItem(item)) + + this.skip += this.limit + + if (items.length > this.limit) { + this.createLoadMoreButton() + } + } + + async collectItems (outboxUrl, { skip, limit }) { + const items = [] + for await (const item of db.iterateCollection(outboxUrl, { skip, limit })) { + items.push(item) + } + return items + } + + processItem (item) { + const itemKey = item.id || item.object + if (!itemKey) { + console.error('Item key is undefined, item:', item) + return + } + if (!this.renderedItems.has(itemKey)) { + this.renderItem(item) + this.renderedItems.set(itemKey, true) + } + } + + renderItem (item) { + const activityElement = document.createElement('distributed-activity') + activityElement.type = item.type + activityElement.data = item + this.appendChild(activityElement) + } + + createLoadMoreButton () { + this.removeLoadMoreButton() + + const loadMoreBtn = document.createElement('button') + loadMoreBtn.textContent = 'Load More' + loadMoreBtn.className = 'load-more-btn' + + const loadMoreBtnWrapper = document.createElement('div') + loadMoreBtnWrapper.className = 'load-more-btn-container' + loadMoreBtnWrapper.appendChild(loadMoreBtn) + + loadMoreBtn.addEventListener('click', () => this.loadMore()) + this.appendChild(loadMoreBtnWrapper) + } + + clearContent () { + this.innerHTML = '' + this.renderedItems.clear() + } + + removeLoadMoreButton () { + const loadMoreBtnWrapper = this.querySelector('.load-more-btn-container') + if (loadMoreBtnWrapper) { + loadMoreBtnWrapper.remove() + } + } + + attributeChangedCallback (name, oldValue, newValue) { + if (name === 'url' && newValue !== oldValue) { + this.outboxUrl = newValue + this.loadOutbox(this.outboxUrl) + } + } +} + +// Register the new element with the browser +customElements.define('distributed-outbox', DistributedOutbox) + +class DistributedActivity extends HTMLElement { + constructor () { + super() + this.activityType = '' + this.activityData = {} + this.activityUrl = null + } + + static get observedAttributes () { + return ['type', 'data', 'url'] + } + + async connectedCallback () { + // Check if the component already has type and data set as properties + if (this.type && this.data) { + this.activityType = this.type + this.activityData = this.data + this.renderActivity() + } else if (this.activityUrl) { + // Load from URL if type and data are not set + await this.loadDataFromUrl(this.activityUrl) + } else { + console.error('Activity data is not provided and no URL is specified.') + } + } + + async loadDataFromUrl (activityUrl) { + try { + const activityData = await db.getActivity(activityUrl) + this.type = activityData.type + this.data = activityData + this.connectedCallback() + } catch (error) { + console.error('Error loading activity data from URL:', error) + } + } + + async fetchAndDisplayPost () { + let postUrl + // Determine the source of the post (direct activity or URL pointing to the activity) + const isDirectPost = + typeof this.activityData.object === 'string' || + this.activityData.object instanceof String + + if (isDirectPost) { + postUrl = this.activityData.object + } else if (this.activityData.object && this.activityData.object.id) { + postUrl = this.activityData.object.id + } else { + postUrl = this.activityData.object + } + + // Create and append the distributed-post component without clearing previous content + const distributedPostElement = document.createElement('distributed-post') + distributedPostElement.setAttribute('url', postUrl) + this.appendChild(distributedPostElement) + } + + displayUnimplemented () { + const message = `Activity type ${this.activityType} is not implemented yet.` + const messageElement = document.createElement('p') + messageElement.textContent = message + this.appendChild(messageElement) + } + + renderActivity () { + // Clear existing content + this.innerHTML = '' + + switch (this.activityType) { + case 'Create': + this.fetchAndDisplayPost() + break + case 'Update': + this.fetchAndDisplayPost() + break + case 'Announce': + this.displayRepostedActivity() + break + case 'Follow': + this.displayFollowActivity() + break + case 'Like': + this.displayLikeActivity() + break + default: + this.displayUnimplemented() + break + } + } + + displayRepostedActivity () { + const actorUrl = this.activityData.actor + db.getActor(actorUrl).then(actorData => { + const actorDisplayName = actorData.preferredUsername || actorData.name || actorUrl.split('/').pop().split('@').pop() // Fallback to URL parsing if name is unavailable + const repostLabel = document.createElement('p') + repostLabel.textContent = `Reposted by ${actorDisplayName} ⇄` + repostLabel.className = 'repost-label' + this.appendChild(repostLabel) + this.fetchAndDisplayPost() + }).catch(error => { + console.error('Error loading actor data:', error) + this.fetchAndDisplayPost() // Continue to display the post even if actor loading fails + }) + } + + displayFollowActivity () { + const from = this.activityData.actor + const to = this.activityData.object + const message = `New follow request from ${from} to ${to}` + const messageElement = document.createElement('p') + messageElement.textContent = message + this.appendChild(messageElement) + } + + displayLikeActivity () { + const message = `New like on ${this.activityData.object}` + const messageElement = document.createElement('p') + messageElement.textContent = message + this.appendChild(messageElement) + } + + attributeChangedCallback (name, oldValue, newValue) { + if (newValue !== oldValue) { + if (name === 'type') { + this.activityType = newValue + this.renderActivity() + } else if (name === 'data') { + this.activityData = JSON.parse(newValue) + this.renderActivity() + } else if (name === 'url') { + this.loadDataFromUrl(newValue) + .then(() => { + this.renderActivity() + }) + .catch((error) => { + console.error('Error loading activity data from URL:', error) + }) + } + } + } +} + +// Register the new element with the browser +customElements.define('distributed-activity', DistributedActivity) diff --git a/post.css b/post.css new file mode 100644 index 0000000..f788527 --- /dev/null +++ b/post.css @@ -0,0 +1,136 @@ +/* Vars */ +:root { + --rdp-font: "Arial", sans-serif; + --rdp-bg-color: #ebf3f5; + --rdp-text-color: #000000; + --rdp-cw-color: #f87171; + --rdp-link-color: #0ea5e9; + --rdp-details-color: #4d626a; + --rdp-border-color: #cccccc; + --rdp-border-radius: 6px; +} + +/* Component styles */ +.distributed-post { + background: var(--rdp-bg-color); + border: 1px solid var(--rdp-border-color); + border-radius: var(--rdp-border-radius); + padding: 16px; + max-width: 500px; + margin: 16px auto; +} + +.distributed-post-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.distributed-post-author { + display: flex; + align-items: center; +} + +.actor-icon { + width: 50px; + height: 50px; + border-radius: 50%; + background-color: #000000; + margin-right: 8px; + cursor: pointer; +} + +.actor-details { + display: flex; + flex-direction: column; +} + +.actor-name { + color: var(--rdp-text-color); + font-weight: bold; + cursor: pointer; +} + +.actor-username { + color: var(--rdp-details-color); + margin-top: 1px; +} + +.time-ago { + font-size: 0.875rem; + color: var(--rdp-details-color); +} +.time-ago:hover{ + text-decoration: none; +} + +.full-date{ + color: var(--rdp-details-color); +} +.full-date:hover{ + text-decoration: none; +} + +.cw-summary { + color: var(--rdp-cw-color); +} + +.see-more-toggle { + color: var(--rdp-details-color); + font-size: 0.725em; + margin-left: 4px; + cursor: pointer; +} + +.post-content { + font-size: 16px; + color: var(--rdp-text-color); + padding: 6px; + margin-bottom: 10px; + overflow-wrap: break-word; +} + +.post-content a{ + color: var(--rdp-link-color); + text-decoration: underline; +} +.post-content a:hover{ + text-decoration: none; +} + +.post-footer { + margin-top: 10px; + font-size: 0.875rem; + color: var(--rdp-details-color); +} + +.individual-post{ + flex: 1; + max-width: 600px; + width: 100%; + margin: 0 20px; +} + +@media (max-width: 768px) { + .distributed-post { + max-width: 80%; + padding: 12px; + margin: 8px auto; + } + + .post-content, + .post-footer, + .time-ago { + font-size: 0.875rem; + } + + .actor-icon { + width: 40px; + height: 40px; + } + + .individual-post{ + margin-top: 170px; + } +} diff --git a/post.html b/post.html index 28ae837..fe4fa5d 100644 --- a/post.html +++ b/post.html @@ -1,13 +1,38 @@ -Distributed Press Reader + + +Reader Post +
+ +
+ +
+
+ +
+
+ + + + + + + + diff --git a/post.js b/post.js new file mode 100644 index 0000000..2b7bd09 --- /dev/null +++ b/post.js @@ -0,0 +1,381 @@ +/* global customElements, HTMLElement */ +import DOMPurify from './dependencies/dompurify/purify.js' +import { db } from './dbInstance.js' + +function formatDate (dateString) { + const options = { year: 'numeric', month: 'short', day: 'numeric' } + return new Date(dateString).toLocaleDateString(undefined, options) +} + +// Helper function to calculate elapsed time (e.g., 1h, 1d, 1w) +function timeSince (dateString) { + const date = new Date(dateString) + const seconds = Math.floor((new Date() - date) / 1000) + + let interval = seconds / 31536000 // 365 * 24 * 60 * 60 + if (interval > 1) { + return formatDate(dateString) // Return formatted date if more than a year + } + interval = seconds / 2592000 // 30 * 24 * 60 * 60 + if (interval > 1) { + return Math.floor(interval) + 'mo' + } + interval = seconds / 604800 // 7 * 24 * 60 * 60 + if (interval > 1) { + return Math.floor(interval) + 'w' + } + interval = seconds / 86400 // 24 * 60 * 60 + if (interval > 1) { + return Math.floor(interval) + 'd' + } + interval = seconds / 3600 // 60 * 60 + if (interval > 1) { + return Math.floor(interval) + 'h' + } + interval = seconds / 60 + if (interval > 1) { + return Math.floor(interval) + 'm' + } + return Math.floor(seconds) + 's' +} + +// Define a class for the web component +class DistributedPost extends HTMLElement { + static get observedAttributes () { + return ['url'] + } + + connectedCallback () { + this.loadAndRenderPost(this.getAttribute('url')) + } + + async loadAndRenderPost (postUrl) { + if (!postUrl) { + this.renderErrorContent('No post URL provided') + return + } + + try { + const content = await db.getNote(postUrl) + + // Assuming JSON-LD content has a "summary" field + this.renderPostContent(content) + } catch (error) { + console.error(error) + this.renderErrorContent(error.message) + } + } + + async renderPostContent (jsonLdData) { + // Clear existing content + this.innerHTML = '' + + // Check if jsonLdData is an activity instead of a note + if ('object' in jsonLdData) { + this.renderErrorContent('Expected a Note but received an Activity') + return + } + + // Create the container for the post + const postContainer = document.createElement('div') + postContainer.classList.add('distributed-post') + + // Header for the post, which will contain actor info and published time + const postHeader = document.createElement('header') + postHeader.classList.add('distributed-post-header') + + // Determine the source of 'attributedTo' based on the structure of jsonLdData + let attributedToSource = jsonLdData.attributedTo + if ('object' in jsonLdData && 'attributedTo' in jsonLdData.object) { + attributedToSource = jsonLdData.object.attributedTo + } + + // Create elements for each field, using the determined source for 'attributedTo' + if (attributedToSource) { + const actorInfo = document.createElement('actor-info') + actorInfo.setAttribute('url', attributedToSource) + postHeader.appendChild(actorInfo) + } + + // Published time element + const publishedTime = document.createElement('a') + publishedTime.href = `/post.html?url=${encodeURIComponent(db.getObjectPage(jsonLdData))}` + publishedTime.classList.add('time-ago') + const elapsed = timeSince(jsonLdData.published) + publishedTime.textContent = elapsed + postHeader.appendChild(publishedTime) + + // Append the header to the post container + postContainer.appendChild(postHeader) + + // Main content of the post + const postContent = document.createElement('div') + postContent.classList.add('post-content') + + // Determine content source based on structure of jsonLdData + const contentSource = jsonLdData.content || (jsonLdData.object && jsonLdData.object.content) + + // Sanitize content and create a DOM from it + const sanitizedContent = DOMPurify.sanitize(contentSource) + const parser = new DOMParser() + const contentDOM = parser.parseFromString(sanitizedContent, 'text/html') + + // Process all anchor elements to handle actor and posts mentions + const anchors = contentDOM.querySelectorAll('a') + anchors.forEach(async (anchor) => { + const href = anchor.getAttribute('href') + if (href) { + const fediverseActorMatch = href.match(/^(https?|ipns|hyper):\/\/([^\/]+)\/@(\w+)$/) + const jsonldActorMatch = href.endsWith('about.jsonld') + const mastodonPostMatch = href.match(/^(https?|ipns|hyper):\/\/([^\/]+)\/@(\w+)\/(\d+)$/) + const jsonldPostMatch = href.endsWith('.jsonld') + + if (fediverseActorMatch || jsonldActorMatch) { + anchor.setAttribute('href', `/profile.html?actor=${encodeURIComponent(href)}`) + try { + const actorData = await db.getActor(href) + if (actorData) { + anchor.setAttribute('href', `/profile.html?actor=${encodeURIComponent(href)}`) + } else { + console.log('Actor not found in DB, default redirection applied.') + } + } catch (error) { + console.error('Error fetching actor data:', error) + } + } else if (mastodonPostMatch || jsonldPostMatch) { + anchor.setAttribute('href', `/post.html?url=${encodeURIComponent(href)}`) + try { + const noteData = await db.getNote(href) + if (noteData) { + anchor.setAttribute('href', `/post.html?url=${encodeURIComponent(href)}`) + } else { + console.log('Post not found in DB, default redirection applied.') + } + } catch (error) { + console.error('Error fetching note data:', error) + } + } else { + anchor.setAttribute('href', href) + } + } + }) + + // Determine if the content is marked as sensitive in either the direct jsonLdData or within jsonLdData.object + const isSensitive = + jsonLdData.sensitive || + (jsonLdData.object && jsonLdData.object.sensitive) + + const summary = + jsonLdData.summary || + (jsonLdData.object && jsonLdData.object.summary) + + // Handle sensitive content + if (isSensitive) { + const details = document.createElement('details') + const summary = document.createElement('summary') + summary.classList.add('cw-summary') + summary.textContent = 'Sensitive Content (click to view)' + details.appendChild(summary) + const content = document.createElement('p') + content.innerHTML = DOMPurify.sanitize(contentSource) + details.appendChild(content) + postContent.appendChild(details) + } else if (summary) { + // Non-sensitive content with a summary (post title) + const details = document.createElement('details') + const summaryElement = document.createElement('summary') + summaryElement.textContent = summary // Post title goes here + details.appendChild(summaryElement) + + // Adding the "Show more" and "Show less" toggle text + const toggleText = document.createElement('span') + toggleText.textContent = 'Show more' + toggleText.classList.add('see-more-toggle') + summaryElement.appendChild(toggleText) + + const contentElement = document.createElement('p') + contentElement.innerHTML = DOMPurify.sanitize(jsonLdData.content) + details.appendChild(contentElement) + postContent.appendChild(details) + + // Event listener to toggle the text of the Show more/Show less element + details.addEventListener('toggle', function () { + toggleText.textContent = details.open ? 'Show less' : 'Show more' + }) + } else { + const content = document.createElement('p') + content.innerHTML = contentDOM.body.innerHTML + postContent.appendChild(content) + } + + // Append the content to the post container + postContainer.appendChild(postContent) + + // Footer of the post, which will contain the full published date and platform, but only the date is clickable + const postFooter = document.createElement('footer') + postFooter.classList.add('post-footer') + + // Create a container for the full date and additional text + const dateContainer = document.createElement('div') + + // Create the clickable link for the date + const fullDateLink = document.createElement('a') + fullDateLink.href = `/post.html?url=${encodeURIComponent(jsonLdData.id)}` + fullDateLink.classList.add('full-date') + fullDateLink.textContent = formatDate(jsonLdData.published) + dateContainer.appendChild(fullDateLink) + + // Add the ' · reader web' text outside of the link + const readerWebText = document.createElement('span') + readerWebText.textContent = ' · reader web' + dateContainer.appendChild(readerWebText) + + // Append the date container to the footer + postFooter.appendChild(dateContainer) + + // Handle attachments of other Fedi instances + if (!isSensitive && !jsonLdData.summary && jsonLdData.attachment && jsonLdData.attachment.length > 0) { + const attachmentsContainer = document.createElement('div') + attachmentsContainer.className = 'attachments-container' + + jsonLdData.attachment.forEach(attachment => { + if (attachment.mediaType.startsWith('image/')) { + // If it's an image + const img = document.createElement('img') + img.src = attachment.url + img.alt = attachment.name || 'Attached image' + img.className = 'attachment-image' + attachmentsContainer.appendChild(img) + } else if (attachment.mediaType.startsWith('video/')) { + // If it's a video + const video = document.createElement('video') + video.src = attachment.url + video.alt = attachment.name || 'Attached video' + video.className = 'attachment-video' + video.controls = true + attachmentsContainer.appendChild(video) + } + }) + postContainer.appendChild(attachmentsContainer) + } + + // Append the footer to the post container + postContainer.appendChild(postFooter) + + // Append the whole post container to the custom element + this.appendChild(postContainer) + } + + // appendField to optionally allow HTML content + appendField (label, value, isHTML = false) { + if (value) { + const p = document.createElement('p') + const strong = document.createElement('strong') + strong.textContent = `${label}:` + p.appendChild(strong) + if (isHTML) { + // If the content is HTML, set innerHTML directly + const span = document.createElement('span') + span.innerHTML = value + p.appendChild(span) + } else { + // If not, treat it as text + p.appendChild(document.createTextNode(` ${value}`)) + } + this.appendChild(p) + } + } + + renderErrorContent (errorMessage) { + // Clear existing content + this.innerHTML = '' + + const errorComponent = document.createElement('error-message') + errorComponent.setAttribute('message', errorMessage) + this.appendChild(errorComponent) + } +} + +// Register the new element with the browser +customElements.define('distributed-post', DistributedPost) + +// Define a class for the web component +class ActorInfo extends HTMLElement { + static get observedAttributes () { + return ['url'] + } + + constructor () { + super() + this.actorUrl = '' + } + + attributeChangedCallback (name, oldValue, newValue) { + if (name === 'url' && newValue) { + this.actorUrl = newValue + this.fetchAndRenderActorInfo(newValue) + } + } + + navigateToActorProfile () { + window.location.href = `/profile.html?actor=${encodeURIComponent(this.actorUrl)}` + } + + async fetchAndRenderActorInfo (url) { + try { + const actorInfo = await db.getActor(url) + if (actorInfo) { + // Clear existing content + this.innerHTML = '' + + const author = document.createElement('div') + author.classList.add('distributed-post-author') + + const authorDetails = document.createElement('div') + authorDetails.classList.add('actor-details') + + // Handle both single icon object and array of icons + let iconUrl = './assets/profile.png' // Default profile image path + if (actorInfo.icon) { + if (Array.isArray(actorInfo.icon) && actorInfo.icon.length > 0) { + iconUrl = actorInfo.icon[0].url || actorInfo.id + } else if (actorInfo.icon.url) { + iconUrl = actorInfo.icon.url || actorInfo.id + } + } + + const img = document.createElement('img') + img.classList.add('actor-icon') + img.src = iconUrl + img.alt = actorInfo.name ? actorInfo.name : 'Actor icon' + img.addEventListener('click', this.navigateToActorProfile.bind(this)) + author.appendChild(img) + + if (actorInfo.name) { + const pName = document.createElement('div') + pName.classList.add('actor-name') + pName.textContent = actorInfo.name + pName.addEventListener('click', this.navigateToActorProfile.bind(this)) + authorDetails.appendChild(pName) + } + + if (actorInfo.preferredUsername) { + const pUserName = document.createElement('div') + pUserName.classList.add('actor-username') + pUserName.textContent = `@${actorInfo.preferredUsername}` + authorDetails.appendChild(pUserName) + } + // Append the authorDetails to the author div + author.appendChild(authorDetails) + // Append the author container to the actor-info component + this.appendChild(author) + } + } catch (error) { + const errorElement = renderError(error.message) + this.appendChild(errorElement) + } + } +} + +// Register the new element with the browser +customElements.define('actor-info', ActorInfo) diff --git a/profile.html b/profile.html new file mode 100644 index 0000000..83c9e8b --- /dev/null +++ b/profile.html @@ -0,0 +1,56 @@ + + + +User Profile + +
+ +
+ + +
+
+ +
+
+ + + + + + + + diff --git a/search-template.html b/search-template.html new file mode 100644 index 0000000..1bd4f92 --- /dev/null +++ b/search-template.html @@ -0,0 +1,4 @@ +
+ + +
diff --git a/search.css b/search.css new file mode 100644 index 0000000..5b190b6 --- /dev/null +++ b/search.css @@ -0,0 +1,18 @@ +distributed-search form { + display: flex; + margin-top: 8px; +} + +distributed-search input { + width: 10em; + padding: 4px; + border: 1px solid var(--rdp-border-color); + border-radius: 2px; +} + +distributed-search button { + margin-left: 4px; + border: 1px solid var(--rdp-border-color); + border-radius: 2px; + cursor: pointer; +} diff --git a/search.js b/search.js new file mode 100644 index 0000000..0d4f7f9 --- /dev/null +++ b/search.js @@ -0,0 +1,69 @@ +import { db } from './dbInstance.js' + +const response = await fetch('./search-template.html') +const text = await response.text() +const template = document.createElement('template') +template.innerHTML = text + +const style = document.createElement('style') +style.textContent = '@import url("./search.css");' +document.head.appendChild(style) + +class DistributedSearch extends HTMLElement { + constructor () { + super() + this.init() + } + + get form () { + return this.querySelector('form') + } + + get url () { + return this.querySelector('[name=url]').value + } + + init () { + const instance = template.content.cloneNode(true) + this.appendChild(instance) + + this.form.addEventListener('submit', (e) => { + e.preventDefault() + this.handleSubmit(e) + }) + } + + async handleSubmit () { + // TODO: Detect `@username@domain syntax + const url = this.url + try { + // TODO: Redirect to p2p version + const data = await db.resolveURL(url) + const { id, type } = data + + if (type === 'Person' || type === 'Service') { + const newURL = new URL('/profile.html', window.location.href) + newURL.searchParams.set('actor', id) + window.location.href = newURL.href + } else if (type === 'Note') { + const newURL = new URL('/post.html', window.location.href) + newURL.searchParams.set('url', id) + window.location.href = newURL.href + } else { + throw new Error(`Invalid JSON-LD type: ${type}`) + } + } catch (e) { + this.showError(e) + } + } + + showError (e) { + console.error(e) + const element = document.createElement('error-message') + element.setAttribute('message', e.message) + + this.appendChild(element) + } +} + +customElements.define('distributed-search', DistributedSearch) diff --git a/sidebar.css b/sidebar.css new file mode 100644 index 0000000..56a737b --- /dev/null +++ b/sidebar.css @@ -0,0 +1,86 @@ +sidebar-nav .header-branding { + color: var(--rdp-text-color); +} +sidebar-nav .home-page-link { + text-decoration: none; + cursor: pointer; +} + +sidebar-nav { + position: sticky; + top: 0; + flex: 0 0 250px; + display: flex; + flex-direction: column; + align-items: flex-start; + height: 100vh; + overflow-y: auto; +} + +sidebar-nav h1 { + font-family: "Times New Roman", Times, serif; + font-size: 1.8em; + font-weight: normal; + margin-bottom: 0.6em; +} + +sidebar-nav .controls a { + color: var(--rdp-text-color); + text-decoration: none; + font-size: 0.875rem; + font-weight: bold; + margin-bottom: 0.4em; +} +sidebar-nav .controls a:hover { + text-decoration: underline; +} + +sidebar-nav nav { + display: flex; + flex-direction: column; +} + +sidebar-nav nav a { + color: var(--rdp-details-color); + text-decoration: underline; + font-size: 0.775rem; +} + +sidebar-nav nav a:hover { + text-decoration: none; +} + +@media screen and (max-width: 1280px) { + sidebar-nav { + flex: 0 0 200px; + margin-left: 14px; + } +} + +@media screen and (max-width: 768px) { + sidebar-nav { + width: 100%; + position: fixed; + top: 0; + z-index: 100; + max-height: calc(100vh - 78vh); + overflow-y: auto; + align-items: center; + background-color: var(--bg-color); + } + + sidebar-nav h1 { + margin-bottom: 0.2em; + text-align: center; + } + + sidebar-nav nav { + text-align: center; + } +} + +@media screen and (max-width: 400px) { + sidebar-nav { + padding-bottom: 50px; + } +} diff --git a/sidebar.html b/sidebar.html new file mode 100644 index 0000000..96b0b87 --- /dev/null +++ b/sidebar.html @@ -0,0 +1,13 @@ + +

Social Reader

+
+ + + \ No newline at end of file diff --git a/sidebar.js b/sidebar.js new file mode 100644 index 0000000..ff74048 --- /dev/null +++ b/sidebar.js @@ -0,0 +1,24 @@ +import './search.js' + +const response = await fetch('./sidebar.html') +const text = await response.text() +const template = document.createElement('template') +template.innerHTML = text + +const style = document.createElement('style') +style.textContent = '@import url("./sidebar.css");' +document.head.appendChild(style) + +class SidebarNav extends HTMLElement { + constructor () { + super() + this.init() + } + + init () { + const instance = template.content.cloneNode(true) + this.appendChild(instance) + } +} + +customElements.define('sidebar-nav', SidebarNav) diff --git a/style.css b/style.css deleted file mode 100644 index 8e1102e..0000000 --- a/style.css +++ /dev/null @@ -1,28 +0,0 @@ -/* -Vars - -Any colors, sizings, etc should be set as vars and reused -*/ - - -:root { - --rdp-font: "system-ui" -} - -/* -Main styles - -These should set the default look and feel of the app. -Stuff like font color/size, backgrounds, button/paragraph styles. -*/ - -html { - font-family: var(--rdp-font); -} - - -/* -Component styles -*/ - -// TODO diff --git a/theme-selector.js b/theme-selector.js new file mode 100644 index 0000000..6eadb1e --- /dev/null +++ b/theme-selector.js @@ -0,0 +1,141 @@ +import { db } from './dbInstance.js' + +class ThemeSelector extends HTMLElement { + constructor () { + super() + this.attachShadow({ mode: 'open' }) + this.shadowRoot.appendChild(this.buildTemplate()) + this.shadowRoot.querySelector('#theme-select').addEventListener('change', this.changeTheme.bind(this)) + } + + async connectedCallback () { + // Append colorblind filters to the main document + document.body.appendChild(this.createColorBlindFilters()) + const currentTheme = await db.getTheme() + this.shadowRoot.querySelector('#theme-select').value = currentTheme || 'light' + this.applyTheme(currentTheme || 'light') + } + + appendColorBlindFiltersToBody () { + const existingSvg = document.querySelector('#colorblind-filters') + if (!existingSvg) { + document.body.appendChild(createColorBlindFilters()) + } + } + + changeTheme (event) { + const newTheme = event.target.value + db.setTheme(newTheme) + this.applyTheme(newTheme) + } + + applyTheme (themeName) { + document.documentElement.setAttribute('data-theme', themeName) + } + + buildTemplate () { + const template = document.createElement('template') + + const style = document.createElement('style') + style.textContent = ` + select { + padding: 4px; + margin: 6px 0; + border: 1px solid var(--rdp-border-color); + border-radius: 4px; + width: 60px; + } + ` + + // Create the select element + const select = document.createElement('select') + select.id = 'theme-select' + + // Create and append the options + const options = [ + { value: 'light', text: 'Light' }, + { value: 'dark', text: 'Dark' }, + { value: '', text: '👁️ Color Blind Themes 👁️', disabled: true }, + { value: 'deuteranomaly', text: 'Deuteranomaly (Green-Weak)' }, + { value: 'protanomaly', text: 'Protanomaly (Red-Weak)' }, + { value: 'deuteranopia', text: 'Deuteranopia (Green-Blind)' }, + { value: 'protanopia', text: 'Protanopia (Red-Blind)' }, + { value: 'tritanopia', text: 'Tritanopia (Blue-Blind)' }, + { value: 'tritanomaly', text: 'Tritanomaly (Blue-Weak)' }, + { value: 'achromatopsia', text: 'Achromatopsia (All-Color-Blind)' } + ] + + // Create and append the options + options.forEach(({ value, text }) => { + const option = document.createElement('option') + option.value = value + option.textContent = text + select.appendChild(option) + }) + + // Append the select & style to the template's content + template.content.appendChild(select) + template.content.appendChild(style) + + return template.content + } + + createColorBlindFilters () { + const svgNS = 'http://www.w3.org/2000/svg' + const svg = document.createElementNS(svgNS, 'svg') + svg.setAttribute('id', 'colorblind-filters') + svg.setAttribute('style', 'display: none') + + const defs = document.createElementNS(svgNS, 'defs') + + const filters = [ + { + id: 'deuteranopia', + values: '0.29031,0.70969,0.00000,0,0 0.29031,0.70969,0.00000,0,0 -0.02197,0.02197,1.00000,0,0 0,0,0,1,0' + }, + { + id: 'deuteranomaly', + values: '0.57418,0.42582,0.00000,0,0 0.17418,0.82582,0.00000,0,0 -0.01318,0.01318,1.00000,0,0 0,0,0,1,0' + }, + { + id: 'protanopia', + values: '0.10889,0.89111,0.00000,0,0 0.10889,0.89111,0.00000,0,0 0.00447,-0.00447,1.00000,0,0 0,0,0,1,0' + }, + { + id: 'protanomaly', + values: '0.46533,0.53467,0.00000,0,0 0.06533,0.93467,0.00000,0,0 0.00268,-0.00268,1.00000,0,0 0,0,0,1,0' + }, + { + id: 'tritanopia', + values: '1.00000,0.15236,-0.15236,0,0 0.00000,0.86717,0.13283,0,0 0.00000,0.86717,0.13283,0,0 0,0,0,1,0' + }, + { + id: 'tritanomaly', + values: '1.00000,0.09142,-0.09142,0,0 0.00000,0.92030,0.07970,0,0 0.00000,0.52030,0.47970,0,0 0,0,0,1,0' + }, + { + id: 'achromatopsia', + values: '0.299,0.587,0.114,0,0 0.299,0.587,0.114,0,0 0.299,0.587,0.114,0,0 0,0,0,1,0' + } + ] + + // Iterate through each filter and append to defs + filters.forEach((filter) => { + const filterElem = document.createElementNS(svgNS, 'filter') + filterElem.setAttribute('id', filter.id) + filterElem.setAttribute('color-interpolation-filters', 'linearRGB') + + const feColorMatrix = document.createElementNS(svgNS, 'feColorMatrix') + feColorMatrix.setAttribute('type', 'matrix') + feColorMatrix.setAttribute('values', filter.values) + + filterElem.appendChild(feColorMatrix) + defs.appendChild(filterElem) + }) + + svg.appendChild(defs) + return svg + } +} + +customElements.define('theme-selector', ThemeSelector) diff --git a/theme.css b/theme.css new file mode 100644 index 0000000..03f2e4f --- /dev/null +++ b/theme.css @@ -0,0 +1,124 @@ +:root[data-theme="light"] { + --bg-color: #ffffff; + --rdp-bg-color: #ebf3f5; + --rdp-text-color: #000000; + --rdp-cw-color: #f87171; + --rdp-link-color: #0ea5e9; + --rdp-details-color: #4d626a; + --rdp-border-color: #cccccc; +} + +:root[data-theme="dark"] { + --bg-color: #18181b; + --rdp-bg-color: #27272a; + --rdp-text-color: #d1d5db; + --rdp-cw-color: #f87171; + --rdp-link-color: #0ea5e9; + --rdp-details-color: #94a3b8; + --rdp-border-color: #71717a; +} + +:root[data-theme="deuteranomaly"] { + --bg-color: #ffffff; + --rdp-bg-color: #ebf3f5; + --rdp-text-color: #000000; + --rdp-cw-color: #d09b6b; + --rdp-link-color: #3faae9; + --rdp-details-color: #4d626a; + --rdp-border-color: #cccccc; +} + +:root[data-theme="protanomaly"] { + --bg-color: #ffffff; + --rdp-bg-color: #ebf3f5; + --rdp-text-color: #000000; + --rdp-cw-color: #c38473; + --rdp-link-color: #3ba5e9; + --rdp-details-color: #5b636c; + --rdp-border-color: #cccccc; +} + +:root[data-theme="deuteranopia"] { + --bg-color: #ffffff; + --rdp-bg-color: #f0f0f5; + --rdp-text-color: #000000; + --rdp-cw-color: #b0b067; + --rdp-link-color: #9190ea; + --rdp-details-color: #5c5b6a; + --rdp-border-color: #cccccc; +} + +:root[data-theme="protanopia"] { + --bg-color: #ffffff; + --rdp-bg-color: #f2f2f5; + --rdp-text-color: #000000; + --rdp-cw-color: #8e8e74; + --rdp-link-color: #9e9de8; + --rdp-details-color: #60606a; + --rdp-border-color: #cccccc; +} + +:root[data-theme="tritanopia"] { + --bg-color: #ffffff; + --rdp-bg-color: #ebf3f3; + --rdp-text-color: #000000; + --rdp-cw-color: #f5716e; + --rdp-link-color: #3cb0b0; + --rdp-details-color: #5b7070; + --rdp-border-color: #cccccc; +} + +:root[data-theme="tritanomaly"] { + --bg-color: #ffffff; + --rdp-bg-color: #ebf3f4; + --rdp-text-color: #000000; + --rdp-cw-color: #f5716f; + --rdp-link-color: #40b6d0; + --rdp-details-color: #4c6365; + --rdp-border-color: #cccccc; +} + +:root[data-theme="achromatopsia"] { + --bg-color: #ffffff; + --rdp-bg-color: #f0f0f0; + --rdp-text-color: #000000; + --rdp-cw-color: #9e9e9e; + --rdp-link-color: #8d8d8d; + --rdp-details-color: #5b5b5b; + --rdp-border-color: #cccccc; +} + +:root[data-theme="deuteranomaly"] img, +:root[data-theme="deuteranomaly"] video { + filter: url(#deuteranomaly); +} + +:root[data-theme="protanomaly"] img, +:root[data-theme="protanomaly"] video { + filter: url(#protanomaly); +} + +:root[data-theme="deuteranopia"] img, +:root[data-theme="deuteranopia"] video { + filter: url(#deuteranopia); +} + +:root[data-theme="protanopia"] img, +:root[data-theme="protanopia"] video { + filter: url(#protanopia); +} + +:root[data-theme="tritanopia"] img, +:root[data-theme="tritanopia"] video { + filter: url(#tritanopia); +} + +:root[data-theme="tritanomaly"] img, +:root[data-theme="tritanomaly"] video { + filter: url(#tritanomaly); +} + +:root[data-theme="achromatopsia"] img, +:root[data-theme="achromatopsia"] video { + filter: url(#achromatopsia); +} diff --git a/timeline.css b/timeline.css new file mode 100644 index 0000000..c322362 --- /dev/null +++ b/timeline.css @@ -0,0 +1,25 @@ +body { + margin: 0; + padding: 0; + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; + font-family: var(--rdp-font); + background: var(--bg-color); +} + +reader-timeline { + flex: 1; + max-width: 600px; + width: 100%; + margin: 0 20px; +} + +@media screen and (max-width: 768px) { + reader-timeline { + width: 100%; + max-width: 100%; + margin-top: 175px; + } +} diff --git a/timeline.js b/timeline.js new file mode 100644 index 0000000..b42477b --- /dev/null +++ b/timeline.js @@ -0,0 +1,85 @@ +import { db } from './dbInstance.js' + +let hasLoaded = false + +class ReaderTimeline extends HTMLElement { + skip = 0 + limit = 32 + hasMoreItems = true + loadMoreBtn = null + + constructor () { + super() + this.loadMoreBtn = document.createElement('button') + this.loadMoreBtn.textContent = 'Load More..' + this.loadMoreBtn.className = 'load-more-btn' + + this.loadMoreBtnWrapper = document.createElement('div') + this.loadMoreBtnWrapper.className = 'load-more-btn-container' + this.loadMoreBtnWrapper.appendChild(this.loadMoreBtn) + + this.loadMoreBtn.addEventListener('click', () => this.loadMore()) + } + + connectedCallback () { + this.initializeDefaultFollowedActors().then(() => this.initTimeline()) + } + + async initializeDefaultFollowedActors () { + const defaultActors = [ + 'https://social.distributed.press/v1/@announcements@social.distributed.press/', + 'ipns://distributed.press/about.ipns.jsonld', + 'hyper://hypha.coop/about.hyper.jsonld', + 'https://sutty.nl/about.jsonld' + // "https://akhilesh.sutty.nl/about.jsonld", + // "https://staticpub.mauve.moe/about.jsonld", + ] + + // Check if followed actors have already been initialized + const hasFollowedActors = await db.hasFollowedActors() + if (!hasFollowedActors) { + await Promise.all( + defaultActors.map(async (actorUrl) => { + await db.followActor(actorUrl) + }) + ) + } + } + + async initTimeline () { + if (!hasLoaded) { + hasLoaded = true + const followedActors = await db.getFollowedActors() + await Promise.all(followedActors.map(({ url }) => db.ingestActor(url))) + } + this.loadMore() + } + + async loadMore () { + // Remove the button before loading more items + this.loadMoreBtnWrapper.remove() + + let count = 0 + for await (const note of db.searchNotes({}, { skip: this.skip, limit: this.limit })) { + count++ + this.appendNoteElement(note) + } + + // Update skip value and determine if there are more items + this.skip += this.limit + this.hasMoreItems = count === this.limit + + // Append the button at the end if there are more items + if (this.hasMoreItems) { + this.appendChild(this.loadMoreBtnWrapper) + } + } + + appendNoteElement (note) { + const activityElement = document.createElement('distributed-post') + activityElement.setAttribute('url', note.id) + this.appendChild(activityElement) + } +} + +customElements.define('reader-timeline', ReaderTimeline)