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
index b1e1db2..f09b0c6 100644
--- a/db.js
+++ b/db.js
@@ -1,9 +1,12 @@
+/* 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'
@@ -25,67 +28,118 @@ 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 class ActivityPubDB {
+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, 1, {
+ 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.call(globalThis, url, {
+ response = await this.#fetch(url, {
headers: {
- Accept: 'application/json'
+ Accept: ACCEPT_HEADER
}
})
} catch (error) {
- console.error('P2P loading failed, trying HTTP gateway:', error)
- }
-
- // If direct fetch was not successful, attempt fetching from a gateway for P2P protocols
- if (!response || !response.ok) {
- let gatewayUrl = url
-
- if (url.startsWith('hyper://')) {
- gatewayUrl = url.replace('hyper://', 'https://hyper.hypha.coop/hyper/')
- } else if (url.startsWith('ipns://')) {
- gatewayUrl = url.replace('ipns://', 'https://ipfs.hypha.coop/ipns/')
- }
-
- try {
- response = await this.fetch.call(globalThis, gatewayUrl, {
+ if (isP2P(url)) {
+ // Maybe the browser can't load p2p URLs
+ response = await this.#gateWayFetch(url, {
headers: {
- Accept: 'application/json'
+ Accept: ACCEPT_HEADER
+ }
+ })
+ } else {
+ // Try the proxy, maybe it's cors?
+ response = await this.#proxiedFetch(url, {
+ headers: {
+ Accept: ACCEPT_HEADER
}
})
- } catch (error) {
- console.error('Fetching from gateway failed:', error)
- throw new Error(`Failed to fetch ${url} from gateway`)
}
}
if (!response.ok) {
- throw new Error(`HTTP error! Status: ${response.status}`)
+ 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()
}
@@ -109,7 +163,9 @@ export class ActivityPubDB {
async getNote (url) {
try {
- return this.db.get(NOTES_STORE, url)
+ 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)
@@ -117,32 +173,73 @@ export class ActivityPubDB {
}
}
- async searchNotes (criteria) {
+ 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')
- const notes = []
- const index = criteria.attributedTo
- ? tx.store.index('attributedTo')
- : tx.store
-
- // Use async iteration to iterate over the store or index
- for await (const cursor of index.iterate(criteria.attributedTo)) {
- notes.push(cursor.value)
+ 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)
}
- // Implement additional filtering logic if needed based on other criteria (like time ranges or tags)
- // For example:
- // notes.filter(note => note.published >= criteria.startDate && note.published <= criteria.endDate);
- return notes.sort((a, b) => b.published - a.published) // Sort by published date in descending order
+ // 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) {
+ 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)
+ await this.ingestActivityCollection(actor.outbox, actor.id, isInitial)
} else {
console.error(`No outbox found for actor at URL ${url}`)
}
@@ -151,33 +248,85 @@ export class ActivityPubDB {
// e.g., if (actor.followers) { ... }
}
- async ingestActivityCollection (collectionOrUrl, actorId) {
+ async ingestActivityCollection (collectionOrUrl, actorId, isInitial = false) {
console.log(
`Fetching collection for actor ID ${actorId}:`,
collectionOrUrl
)
+ const sort = isInitial ? -1 : 1
- const collection = await this.#get(collectionOrUrl)
+ const cursor = this.iterateCollection(collectionOrUrl, {
+ limit: Infinity,
+ sort
+ })
- for await (const activity of this.iterateCollection(collection)) {
- await this.ingestActivity(activity, actorId)
+ 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 (collection) {
- // TODO: handle pagination here, if collection contains a 'next' or 'first' link.
- const items = collection.orderedItems || collection.items
+ 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 (!items) {
- console.error('No items found in collection:', collection)
- return // Exit if no items to iterate over
+ 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 activity = await this.#get(itemOrUrl)
-
- if (activity) {
- yield activity
+ const item = await this.#get(itemOrUrl)
+
+ if (item) {
+ yield item
}
}
}
@@ -197,6 +346,9 @@ export class ActivityPubDB {
}
}
+ 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)
@@ -204,48 +356,133 @@ export class ActivityPubDB {
console.log('Ingesting activity:', activity)
await this.db.put(ACTIVITIES_STORE, activity)
- if (activity.type !== TYPE_CREATE || activity.type !== TYPE_UPDATE) {
- let note
- if (typeof activity.object === 'string') {
- note = await this.#get(activity.object)
- } else {
- note = activity.object
- }
+ if (activity.type === TYPE_CREATE || activity.type === TYPE_UPDATE) {
+ const note = await this.#get(activity.object)
if (note.type === TYPE_NOTE) {
- note.id = activity.id; // Use the Create activity's ID for the note ID
- console.log("Ingesting note:", note);
- await this.ingestNote(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) {
+ async ingestNote (note) {
+ console.log('Ingesting note', note)
// Convert needed fields to date
- note.published = new Date(note.published);
+ note.published = new Date(note.published)
// Add tag_names field
- note.tag_names = (note.tags || []).map(({ name }) => name);
+ 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);
+ 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);
+ 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);
+ 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) {
@@ -258,11 +495,16 @@ function upgrade (db) {
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 })
@@ -275,8 +517,10 @@ function upgrade (db) {
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)
@@ -284,4 +528,44 @@ function upgrade (db) {
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
index 88ff977..25a7914 100644
--- a/dbInstance.js
+++ b/dbInstance.js
@@ -1,3 +1,3 @@
-import { ActivityPubDB } from "./db.js";
+import { ActivityPubDB } from './db.js'
-export const db = await ActivityPubDB.load();
+export const db = await ActivityPubDB.load()
diff --git a/dependencies/dompurify/purify.js b/dependencies/dompurify/purify.js
index da0df8a..2319962 100644
--- a/dependencies/dompurify/purify.js
+++ b/dependencies/dompurify/purify.js
@@ -1,10 +1,13 @@
/*! @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';
+ 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,
@@ -12,48 +15,48 @@
isFrozen,
getPrototypeOf,
getOwnPropertyDescriptor
- } = Object;
+ } = Object
let {
freeze,
seal,
create
- } = Object; // eslint-disable-line import/no-mutable-exports
+ } = Object // eslint-disable-line import/no-mutable-exports
let {
apply,
construct
- } = typeof Reflect !== 'undefined' && Reflect;
+ } = typeof Reflect !== 'undefined' && Reflect
if (!freeze) {
- freeze = function freeze(x) {
- return x;
- };
+ freeze = function freeze (x) {
+ return x
+ }
}
if (!seal) {
- seal = function seal(x) {
- return x;
- };
+ seal = function seal (x) {
+ return x
+ }
}
if (!apply) {
- apply = function apply(fun, thisValue, args) {
- return fun.apply(thisValue, args);
- };
+ apply = function apply (fun, thisValue, args) {
+ return fun.apply(thisValue, args)
+ }
}
if (!construct) {
- construct = function construct(Func, args) {
- return new Func(...args);
- };
+ 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);
+ 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.
@@ -61,13 +64,13 @@
* @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) {
+ 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];
+ args[_key - 1] = arguments[_key]
}
- return apply(func, thisArg, args);
- };
+ return apply(func, thisArg, args)
+ }
}
/**
@@ -76,13 +79,13 @@
* @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) {
+ function unconstruct (func) {
return function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
- args[_key2] = arguments[_key2];
+ args[_key2] = arguments[_key2]
}
- return construct(func, args);
- };
+ return construct(func, args)
+ }
}
/**
@@ -93,30 +96,30 @@
* @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) {
- let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;
+ 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);
+ setPrototypeOf(set, null)
}
- let l = array.length;
+ let l = array.length
while (l--) {
- let element = array[l];
+ let element = array[l]
if (typeof element === 'string') {
- const lcElement = transformCaseFunc(element);
+ const lcElement = transformCaseFunc(element)
if (lcElement !== element) {
// Config presets (e.g. tags.js, attrs.js) are immutable.
if (!isFrozen(array)) {
- array[l] = lcElement;
+ array[l] = lcElement
}
- element = lcElement;
+ element = lcElement
}
}
- set[element] = true;
+ set[element] = true
}
- return set;
+ return set
}
/**
@@ -125,14 +128,14 @@
* @param {Array} array - The array to be cleaned.
* @returns {Array} The cleaned version of the array
*/
- function cleanArray(array) {
+ function cleanArray (array) {
for (let index = 0; index < array.length; index++) {
- const isPropertyExist = objectHasOwnProperty(array, index);
+ const isPropertyExist = objectHasOwnProperty(array, index)
if (!isPropertyExist) {
- array[index] = null;
+ array[index] = null
}
}
- return array;
+ return array
}
/**
@@ -141,21 +144,21 @@
* @param {Object} object - The object to be cloned.
* @returns {Object} A new object that copies the original.
*/
- function clone(object) {
- const newObject = create(null);
+ function clone (object) {
+ const newObject = create(null)
for (const [property, value] of entries(object)) {
- const isPropertyExist = objectHasOwnProperty(object, property);
+ const isPropertyExist = objectHasOwnProperty(object, property)
if (isPropertyExist) {
if (Array.isArray(value)) {
- newObject[property] = cleanArray(value);
+ newObject[property] = cleanArray(value)
} else if (value && typeof value === 'object' && value.constructor === Object) {
- newObject[property] = clone(value);
+ newObject[property] = clone(value)
} else {
- newObject[property] = value;
+ newObject[property] = value
}
}
}
- return newObject;
+ return newObject
}
/**
@@ -165,79 +168,79 @@
* @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) {
+ function lookupGetter (object, prop) {
while (object !== null) {
- const desc = getOwnPropertyDescriptor(object, prop);
+ const desc = getOwnPropertyDescriptor(object, prop)
if (desc) {
if (desc.get) {
- return unapply(desc.get);
+ return unapply(desc.get)
}
if (typeof desc.value === 'function') {
- return unapply(desc.value);
+ return unapply(desc.value)
}
}
- object = getPrototypeOf(object);
+ object = getPrototypeOf(object)
}
- function fallbackValue() {
- return null;
+ function fallbackValue () {
+ return null
}
- return fallbackValue;
+ 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']);
+ 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']);
+ 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']);
+ 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 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']);
+ 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 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 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 DOCTYPE_NAME = seal(/^html$/i)
- var EXPRESSIONS = /*#__PURE__*/Object.freeze({
+ const EXPRESSIONS = /* #__PURE__ */Object.freeze({
__proto__: null,
- MUSTACHE_EXPR: MUSTACHE_EXPR,
- ERB_EXPR: ERB_EXPR,
- TMPLIT_EXPR: TMPLIT_EXPR,
- DATA_ATTR: DATA_ATTR,
- ARIA_ATTR: ARIA_ATTR,
- IS_ALLOWED_URI: IS_ALLOWED_URI,
- IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
- ATTR_WHITESPACE: ATTR_WHITESPACE,
- DOCTYPE_NAME: DOCTYPE_NAME
- });
-
- const getGlobal = function getGlobal() {
- return typeof window === 'undefined' ? null : window;
- };
+ 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.
@@ -247,63 +250,63 @@
* @return {TrustedTypePolicy} The policy created (or null, if Trusted Types
* are not supported or creating the policy failed).
*/
- const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {
+ const _createTrustedTypesPolicy = function _createTrustedTypesPolicy (trustedTypes, purifyHostElement) {
if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') {
- return null;
+ 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';
+ let suffix = null
+ const ATTR_NAME = 'data-tt-policy-suffix'
if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {
- suffix = purifyHostElement.getAttribute(ATTR_NAME);
+ suffix = purifyHostElement.getAttribute(ATTR_NAME)
}
- const policyName = 'dompurify' + (suffix ? '#' + suffix : '');
+ const policyName = 'dompurify' + (suffix ? '#' + suffix : '')
try {
return trustedTypes.createPolicy(policyName, {
- createHTML(html) {
- return html;
+ createHTML (html) {
+ return html
},
- createScriptURL(scriptUrl) {
- return scriptUrl;
+ 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;
+ console.warn('TrustedTypes policy ' + policyName + ' could not be created.')
+ return null
}
- };
- function createDOMPurify() {
- let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
- const DOMPurify = root => createDOMPurify(root);
+ }
+ 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';
+ DOMPurify.version = '3.0.9'
/**
* Array of elements that DOMPurify removed during sanitation.
* Empty if nothing was removed.
*/
- DOMPurify.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;
+ DOMPurify.isSupported = false
+ return DOMPurify
}
let {
document
- } = window;
- const originalDocument = document;
- const currentScript = originalDocument.currentScript;
+ } = window
+ const originalDocument = document
+ const currentScript = originalDocument.currentScript
const {
DocumentFragment,
HTMLTemplateElement,
@@ -314,12 +317,12 @@
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');
+ } = 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
@@ -328,28 +331,28 @@
// document, so we use that as our parent document to ensure nothing
// is inherited.
if (typeof HTMLTemplateElement === 'function') {
- const template = document.createElement('template');
+ const template = document.createElement('template')
if (template.content && template.content.ownerDocument) {
- document = template.content.ownerDocument;
+ document = template.content.ownerDocument
}
}
- let trustedTypesPolicy;
- let emptyHTML = '';
+ let trustedTypesPolicy
+ let emptyHTML = ''
const {
implementation,
createNodeIterator,
createDocumentFragment,
getElementsByTagName
- } = document;
+ } = document
const {
importNode
- } = originalDocument;
- let hooks = {};
+ } = originalDocument
+ let hooks = {}
/**
* Expose whether this browser supports running the full DOMPurify.
*/
- DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
+ DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined
const {
MUSTACHE_EXPR,
ERB_EXPR,
@@ -358,10 +361,10 @@
ARIA_ATTR,
IS_SCRIPT_OR_DATA,
ATTR_WHITESPACE
- } = EXPRESSIONS;
+ } = EXPRESSIONS
let {
IS_ALLOWED_URI: IS_ALLOWED_URI$1
- } = EXPRESSIONS;
+ } = EXPRESSIONS
/**
* We consider the elements and attributes below to be safe. Ideally
@@ -369,12 +372,12 @@
*/
/* allowed element names */
- let ALLOWED_TAGS = null;
- const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);
+ 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]);
+ 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.
@@ -401,60 +404,60 @@
enumerable: true,
value: false
}
- }));
+ }))
/* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
- let FORBID_TAGS = null;
+ let FORBID_TAGS = null
/* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
- let FORBID_ATTR = null;
+ let FORBID_ATTR = null
/* Decide if ARIA attributes are okay */
- let ALLOW_ARIA_ATTR = true;
+ let ALLOW_ARIA_ATTR = true
/* Decide if custom data attributes are okay */
- let ALLOW_DATA_ATTR = true;
+ let ALLOW_DATA_ATTR = true
/* Decide if unknown protocols are okay */
- let ALLOW_UNKNOWN_PROTOCOLS = false;
+ 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;
+ 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;
+ let SAFE_FOR_TEMPLATES = false
/* Decide if document with ... should be returned */
- let WHOLE_DOCUMENT = false;
+ let WHOLE_DOCUMENT = false
/* Track whether config is already set on this instance of DOMPurify. */
- let SET_CONFIG = false;
+ 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;
+ 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;
+ 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;
+ 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;
+ 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;
+ 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.
@@ -469,57 +472,57 @@
* 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-';
+ let SANITIZE_NAMED_PROPS = false
+ const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'
/* Keep element content when removing element? */
- let KEEP_CONTENT = true;
+ 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;
+ let IN_PLACE = false
/* Allow usage of profiles like html, svg and mathMl */
- let USE_PROFILES = {};
+ 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']);
+ 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']);
+ 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';
+ 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;
+ 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);
+ 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;
+ 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;
+ 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;
- };
+ const formElement = document.createElement('form')
+ const isRegexOrFunction = function isRegexOrFunction (testValue) {
+ return testValue instanceof RegExp || testValue instanceof Function
+ }
/**
* _parseConfig
@@ -527,190 +530,190 @@
* @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] : {};
+ const _parseConfig = function _parseConfig () {
+ let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}
if (CONFIG && CONFIG === cfg) {
- return;
+ return
}
/* Shield configuration object from tampering */
if (!cfg || typeof cfg !== 'object') {
- cfg = {};
+ cfg = {}
}
/* Shield configuration object from prototype pollution */
- cfg = clone(cfg);
+ 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;
+ 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;
+ 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;
+ 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,
+ cfg.ADD_URI_SAFE_ATTR,
// eslint-disable-line indent
transformCaseFunc // eslint-disable-line indent
) // eslint-disable-line indent
- : DEFAULT_URI_SAFE_ATTRIBUTES;
+ : 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,
+ 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 || {};
+ : 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;
+ 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;
+ 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;
+ CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements
}
if (SAFE_FOR_TEMPLATES) {
- ALLOW_DATA_ATTR = false;
+ ALLOW_DATA_ATTR = false
}
if (RETURN_DOM_FRAGMENT) {
- RETURN_DOM = true;
+ RETURN_DOM = true
}
/* Parse profile info */
if (USE_PROFILES) {
- ALLOWED_TAGS = addToSet({}, text);
- ALLOWED_ATTR = [];
+ ALLOWED_TAGS = addToSet({}, text)
+ ALLOWED_ATTR = []
if (USE_PROFILES.html === true) {
- addToSet(ALLOWED_TAGS, html$1);
- addToSet(ALLOWED_ATTR, html);
+ 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);
+ 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);
+ 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);
+ 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);
+ ALLOWED_TAGS = clone(ALLOWED_TAGS)
}
- addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
+ addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc)
}
if (cfg.ADD_ATTR) {
if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
- ALLOWED_ATTR = clone(ALLOWED_ATTR);
+ ALLOWED_ATTR = clone(ALLOWED_ATTR)
}
- addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
+ addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc)
}
if (cfg.ADD_URI_SAFE_ATTR) {
- addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
+ 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);
+ FORBID_CONTENTS = clone(FORBID_CONTENTS)
}
- addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
+ addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc)
}
/* Add #text in case KEEP_CONTENT is set to true */
if (KEEP_CONTENT) {
- ALLOWED_TAGS['#text'] = true;
+ 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']);
+ 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;
+ 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.');
+ 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.');
+ throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.')
}
// Overwrite existing TrustedTypes policy.
- trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
+ trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY
// Sign local variables required by `sanitize`.
- emptyHTML = trustedTypesPolicy.createHTML('');
+ emptyHTML = trustedTypesPolicy.createHTML('')
} else {
// Uninitialized policy, attempt to initialize the internal dompurify policy.
if (trustedTypesPolicy === undefined) {
- trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
+ trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript)
}
// If creating the internal policy succeeded sign internal variables.
if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
- emptyHTML = trustedTypesPolicy.createHTML('');
+ emptyHTML = trustedTypesPolicy.createHTML('')
}
}
// Prevent further manipulation of configuration.
// Not available in IE8, Safari 5, etc.
if (freeze) {
- freeze(cfg);
+ 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']);
+ 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']);
+ 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]);
+ 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
@@ -718,8 +721,8 @@
* namespace that a spec-compliant parser would never
* return. Return true otherwise.
*/
- const _checkValidNamespace = function _checkValidNamespace(element) {
- let parent = getParentNode(element);
+ 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.
@@ -727,94 +730,94 @@
parent = {
namespaceURI: NAMESPACE,
tagName: 'template'
- };
+ }
}
- const tagName = stringToLowerCase(element.tagName);
- const parentTagName = stringToLowerCase(parent.tagName);
+ const tagName = stringToLowerCase(element.tagName)
+ const parentTagName = stringToLowerCase(parent.tagName)
if (!ALLOWED_NAMESPACES[element.namespaceURI]) {
- return false;
+ return false
}
if (element.namespaceURI === SVG_NAMESPACE) {
// The only way to switch from HTML namespace to SVG
// is via