snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 21:11:02 +05:00
commit 083a46e93b
2115 changed files with 245958 additions and 0 deletions

View File

@@ -0,0 +1,319 @@
# @tiptap/extension-audio
## 3.30.3
### Patch Changes
- Updated dependencies [965a880]
- @tiptap/core@3.30.3
## 3.30.2
### Patch Changes
- Updated dependencies [3dffed5]
- Updated dependencies [214a140]
- @tiptap/core@3.30.2
## 3.30.1
### Patch Changes
- Updated dependencies [abc8828]
- @tiptap/core@3.30.1
## 3.30.0
### Patch Changes
- Updated dependencies [0247d39]
- Updated dependencies [51909d3]
- Updated dependencies [3099eef]
- @tiptap/core@3.30.0
## 3.29.2
### Patch Changes
- @tiptap/core@3.29.2
## 3.29.1
### Patch Changes
- @tiptap/core@3.29.1
## 3.29.0
### Patch Changes
- Updated dependencies [d26840f]
- Updated dependencies [935e63f]
- Updated dependencies [b4c5a2d]
- Updated dependencies [a963d48]
- Updated dependencies [51f45b6]
- Updated dependencies [0f63969]
- Updated dependencies [9acaa65]
- @tiptap/core@3.29.0
## 3.28.0
### Patch Changes
- @tiptap/core@3.28.0
## 3.27.4
### Patch Changes
- @tiptap/core@3.27.4
## 3.27.3
### Patch Changes
- Updated dependencies [023f98c]
- @tiptap/core@3.27.3
## 3.27.2
### Patch Changes
- @tiptap/core@3.27.2
## 3.27.1
### Patch Changes
- @tiptap/core@3.27.1
## 3.27.0
### Patch Changes
- Updated dependencies [0d0094d]
- Updated dependencies [795033c]
- Updated dependencies [0e0c4f9]
- Updated dependencies [6d12bb9]
- @tiptap/core@3.27.0
## 3.26.1
### Patch Changes
- @tiptap/core@3.26.1
## 3.26.0
### Patch Changes
- @tiptap/core@3.26.0
## 3.25.0
### Patch Changes
- Updated dependencies [ec291dd]
- Updated dependencies [454e9b8]
- Updated dependencies [9cf8db0]
- Updated dependencies [3d4f94c]
- @tiptap/core@3.25.0
## 3.24.0
### Patch Changes
- @tiptap/core@3.24.0
## 3.23.6
### Patch Changes
- Updated dependencies [d168376]
- @tiptap/core@3.23.6
## 3.23.5
### Patch Changes
- Updated dependencies [835caf5]
- Updated dependencies [95e138c]
- @tiptap/core@3.23.5
## 3.23.4
### Patch Changes
- @tiptap/core@3.23.4
## 3.23.3
### Patch Changes
- @tiptap/core@3.23.3
## 3.23.2
### Patch Changes
- Updated dependencies [f98eaaf]
- @tiptap/core@3.23.2
## 3.23.1
### Patch Changes
- @tiptap/core@3.23.1
## 3.23.0
### Patch Changes
- Updated dependencies [57f8d66]
- Updated dependencies [e64e5a7]
- Updated dependencies [207a2bc]
- @tiptap/core@3.23.0
## 3.22.5
### Patch Changes
- Updated dependencies [a375002]
- @tiptap/core@3.22.5
## 3.22.4
### Patch Changes
- 27ea931: Fix dependencies installation after packages updates producing peer dependency resolution conflicts
- Updated dependencies [27ea931]
- Updated dependencies [64f36b8]
- @tiptap/core@3.22.4
## 3.22.3
### Patch Changes
- Updated dependencies [cb28e7b]
- @tiptap/core@3.22.3
## 3.22.2
### Patch Changes
- Updated dependencies [f1d504c]
- Updated dependencies [404c683]
- @tiptap/core@3.22.2
## 3.22.1
### Patch Changes
- Updated dependencies [ee03ac0]
- Updated dependencies [b88f9ed]
- @tiptap/core@3.22.1
## 3.22.0
### Patch Changes
- Updated dependencies [912a49b]
- Updated dependencies [7d4fb9a]
- Updated dependencies [0c1c112]
- Updated dependencies [0c1c112]
- Updated dependencies [f99bdc2]
- @tiptap/core@3.22.0
## 3.21.0
### Patch Changes
- @tiptap/core@3.21.0
## 3.20.6
### Patch Changes
- @tiptap/core@3.20.6
## 3.20.5
### Patch Changes
- @tiptap/core@3.20.5
## 3.20.4
### Patch Changes
- Updated dependencies [0bcf3c2]
- @tiptap/core@3.20.4
## 3.20.3
### Patch Changes
- Updated dependencies [c94fac4]
- Updated dependencies [6b9ea92]
- @tiptap/core@3.20.3
## 3.20.2
### Patch Changes
- Updated dependencies [269823d]
- @tiptap/core@3.20.2
## 3.20.1
### Patch Changes
- Updated dependencies [25f57e4]
- @tiptap/core@3.20.1
## 3.20.0
### Patch Changes
- Updated dependencies [4b731e2]
- Updated dependencies [98546ac]
- Updated dependencies [57624a1]
- Updated dependencies [76ce47d]
- @tiptap/core@3.20.0
## 3.19.0
### Patch Changes
- @tiptap/core@3.19.0
## 3.18.0
### Patch Changes
- @tiptap/core@3.18.0
## 3.17.1
### Patch Changes
- Updated dependencies [aa9709e]
- Updated dependencies [b46e66a]
- @tiptap/core@3.17.1
## 3.17.0
### Patch Changes
- Updated dependencies [eecf1c9]
- @tiptap/core@3.17.0
## 3.16.0
### Minor Changes
- b3a9fab: Add a native audio extension with demos and tests.
### Patch Changes
- @tiptap/core@3.16.0

View File

@@ -0,0 +1,11 @@
# @tiptap/extension-audio
A native HTML5 audio node for [tiptap](https://tiptap.dev).
```js
import Audio from '@tiptap/extension-audio'
editor = new Editor({
extensions: [Audio],
})
```

View File

@@ -0,0 +1,50 @@
{
"name": "@tiptap/extension-audio",
"version": "3.30.3",
"description": "audio extension for tiptap",
"keywords": [
"tiptap",
"tiptap extension"
],
"homepage": "https://tiptap.dev",
"bugs": {
"url": "https://github.com/ueberdosis/tiptap/issues"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ueberdosis/tiptap",
"directory": "packages/extension-audio"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"files": [
"src",
"dist"
],
"type": "module",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": {
"import": "./dist/index.d.ts",
"require": "./dist/index.d.cts"
},
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "vp pack"
},
"devDependencies": {
"@tiptap/core": "workspace:^"
},
"peerDependencies": {
"@tiptap/core": "workspace:*"
}
}

View File

@@ -0,0 +1,287 @@
import { Editor } from '@tiptap/core'
import { Audio } from './audio.js'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { describe, expect, it } from 'vite-plus/test'
describe('extension-audio', () => {
const editorElClass = 'tiptap'
let editor: Editor | null = null
const createEditorEl = () => {
const editorEl = document.createElement('div')
editorEl.classList.add(editorElClass)
document.body.appendChild(editorEl)
return editorEl
}
const getEditorEl = () => document.querySelector(`.${editorElClass}`)
const destroyEditor = () => {
editor?.destroy()
editor = null
getEditorEl()?.remove()
}
it('does not output html for javascript schema or invalid links', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Audio],
content: {
type: 'doc',
content: [
{
type: 'audio',
attrs: {
// We want to ensure script URLs are filtered out
// oxlint-disable-next-line no-script-url
src: 'javascript:alert(window.origin)//audio',
},
},
],
},
})
// oxlint-disable-next-line no-script-url
expect(editor.getHTML()).not.toContain('javascript:alert')
destroyEditor()
})
it('allows base64 sources only when enabled', () => {
const dataUrl = 'data:audio/mp3;base64,aGVsbG8='
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Audio],
content: {
type: 'doc',
content: [
{
type: 'audio',
attrs: {
src: dataUrl,
},
},
],
},
})
expect(editor.getHTML()).not.toContain(dataUrl)
destroyEditor()
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Audio.configure({ allowBase64: true })],
content: {
type: 'doc',
content: [
{
type: 'audio',
attrs: {
src: dataUrl,
},
},
],
},
})
expect(editor.getHTML()).toContain(dataUrl)
destroyEditor()
})
it('respects audio attributes and options', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
Audio.configure({
autoplay: true,
controls: false,
loop: true,
muted: true,
preload: 'auto',
controlslist: 'nodownload',
crossorigin: 'anonymous',
disableRemotePlayback: true,
}),
],
content: {
type: 'doc',
content: [
{
type: 'audio',
attrs: {
src: 'https://assets.tiptap.dev/sounds/loop.mp3',
controls: true,
preload: 'none',
},
},
],
},
})
const html = editor.getHTML()
expect(html).toContain('<audio')
expect(html).toContain('src="https://assets.tiptap.dev/sounds/loop.mp3"')
expect(html).toContain('autoplay')
expect(html).toContain('loop')
expect(html).toContain('muted')
expect(html).toContain('controlslist="nodownload"')
expect(html).toContain('crossorigin="anonymous"')
expect(html).toContain('disableremoteplayback')
expect(html).toContain('preload="none"')
expect(html).toContain('controls')
destroyEditor()
})
it('does not insert invalid audio sources via command', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Audio],
})
const succeeded = editor.commands.setAudio({
// We want to ensure script URLs are filtered out
// oxlint-disable-next-line no-script-url
src: 'javascript:alert(1)',
})
expect(succeeded).toBe(false)
// oxlint-disable-next-line no-script-url
expect(editor.getHTML()).not.toContain('javascript:alert')
const validInsert = editor.commands.setAudio({
src: 'https://assets.tiptap.dev/sounds/loop.mp3',
loop: true,
muted: true,
disableremoteplayback: true,
controls: false,
})
expect(validInsert).toBe(true)
const html = editor.getHTML()
expect(html).toContain('src="https://assets.tiptap.dev/sounds/loop.mp3"')
expect(html).toContain('loop')
expect(html).toContain('muted')
expect(html).toContain('disableremoteplayback')
expect(html).not.toContain('controls')
destroyEditor()
})
it('creates audio node when pasting a valid audio URL', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Audio],
})
const audioUrl = 'https://assets.tiptap.dev/sounds/loop.mp3'
// Simulate paste by inserting plain text content that matches the paste rule regex
editor.view.pasteText(audioUrl)
const json = editor.getJSON()
const audioNode = json.content?.find(node => node.type === 'audio')
expect(audioNode).toBeDefined()
expect(audioNode?.attrs?.src).toBe(audioUrl)
destroyEditor()
})
it('does not create audio node when paste handler is disabled', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Audio.configure({ addPasteHandler: false })],
})
const audioUrl = 'https://assets.tiptap.dev/sounds/loop.mp3'
editor.commands.insertContent(audioUrl)
const json = editor.getJSON()
const audioNode = json.content?.find(node => node.type === 'audio')
expect(audioNode).toBeUndefined()
destroyEditor()
})
it('renders audio node as inline when inline option is enabled', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Audio.configure({ inline: true })],
content: {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Audio: ',
},
{
type: 'audio',
attrs: {
src: 'https://assets.tiptap.dev/sounds/loop.mp3',
},
},
],
},
],
},
})
const html = editor.getHTML()
expect(html).toContain('<p>Audio: <audio')
expect(html).toContain('src="https://assets.tiptap.dev/sounds/loop.mp3"')
destroyEditor()
})
it('renders audio node as block by default', () => {
editor = new Editor({
element: createEditorEl(),
extensions: [Document, Text, Paragraph, Audio],
content: {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Some text',
},
],
},
{
type: 'audio',
attrs: {
src: 'https://assets.tiptap.dev/sounds/loop.mp3',
},
},
],
},
})
const html = editor.getHTML()
expect(html).toContain('<p>Some text</p><audio')
destroyEditor()
})
})

View File

@@ -0,0 +1,272 @@
import { createAtomBlockMarkdownSpec, mergeAttributes, Node, nodePasteRule } from '@tiptap/core'
import { AUDIO_URL_REGEX_GLOBAL, isValidAudioUrl, sanitizeAudioSrc } from './utils.js'
export interface AudioOptions {
/**
* Controls if the paste handler for audio sources should be added.
* @default true
* @example false
*/
addPasteHandler: boolean
/**
* Controls if the audio should autoplay.
* @default false
* @example true
*/
autoplay: boolean
/**
* Controls if the audio element should render the native controls.
* @default true
* @example false
*/
controls: boolean
/**
* Controls if the audio should loop.
* @default false
* @example true
*/
loop: boolean
/**
* Controls if the audio should be muted.
* @default false
* @example true
*/
muted: boolean
/**
* The preload behavior for the audio element.
* @default 'metadata'
* @example 'none'
*/
preload: 'auto' | 'metadata' | 'none' | null
/**
* Provides the controlslist attribute for the audio element.
* @default undefined
* @example 'nodownload'
*/
controlslist?: string
/**
* Sets the crossorigin attribute for the audio element.
* @default undefined
* @example 'anonymous'
*/
crossorigin?: '' | 'anonymous' | 'use-credentials'
/**
* Controls if remote playback should be disabled on the audio element.
* @default false
* @example true
*/
disableRemotePlayback: boolean
/**
* Allows data: URLs for audio sources.
* @default false
* @example true
*/
allowBase64: boolean
/**
* The HTML attributes for an audio node.
* @default {}
* @example { class: 'foo' }
*/
HTMLAttributes: Record<string, any>
/**
* Controls if the audio node should be inline or not.
* @default false
* @example true
*/
inline: boolean
}
/**
* The options for setting an audio element.
*/
type SetAudioOptions = {
src: string
controls?: boolean
autoplay?: boolean
loop?: boolean
muted?: boolean
preload?: 'auto' | 'metadata' | 'none' | null
controlslist?: string
crossorigin?: '' | 'anonymous' | 'use-credentials'
disableremoteplayback?: boolean
HTMLAttributes?: Record<string, any>
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
audio: {
/**
* Insert an audio element
* @param options The audio attributes
* @example editor.commands.setAudio({ src: 'https://assets.tiptap.dev/sounds/loop.mp3' })
*/
setAudio: (options: SetAudioOptions) => ReturnType
}
}
}
/**
* This extension adds support for native audio elements.
*/
export const Audio = Node.create<AudioOptions>({
name: 'audio',
addOptions() {
return {
addPasteHandler: true,
allowBase64: false,
autoplay: false,
controls: true,
loop: false,
muted: false,
preload: 'metadata',
controlslist: undefined,
crossorigin: undefined,
disableRemotePlayback: false,
HTMLAttributes: {},
inline: false,
}
},
inline() {
return this.options.inline
},
group() {
return this.options.inline ? 'inline' : 'block'
},
atom: true,
draggable: true,
addAttributes() {
return {
src: {
default: null,
},
controls: {
default: this.options.controls,
},
autoplay: {
default: this.options.autoplay,
},
loop: {
default: this.options.loop,
},
muted: {
default: this.options.muted,
},
preload: {
default: this.options.preload,
},
controlslist: {
default: this.options.controlslist,
},
crossorigin: {
default: this.options.crossorigin,
},
disableremoteplayback: {
default: this.options.disableRemotePlayback,
},
}
},
parseHTML() {
return [
{
tag: this.options.allowBase64 ? 'audio[src]' : 'audio[src]:not([src^="data:"])',
},
]
},
addCommands() {
return {
setAudio:
options =>
({ commands }) => {
if (!isValidAudioUrl(options.src, this.options.allowBase64)) {
return false
}
return commands.insertContent({
type: this.name,
attrs: options,
})
},
}
},
addPasteRules() {
if (!this.options.addPasteHandler) {
return []
}
return [
nodePasteRule({
find: AUDIO_URL_REGEX_GLOBAL,
type: this.type,
getAttributes: match => {
return { src: match[0] }
},
}),
]
},
renderHTML({ HTMLAttributes }) {
const sanitizedSrc = sanitizeAudioSrc(HTMLAttributes.src, this.options.allowBase64)
const mergedAttributes = mergeAttributes(
this.options.HTMLAttributes,
{
controls: this.options.controls,
autoplay: this.options.autoplay,
loop: this.options.loop,
muted: this.options.muted,
preload: this.options.preload,
controlslist: this.options.controlslist,
crossorigin: this.options.crossorigin,
disableremoteplayback: this.options.disableRemotePlayback,
},
{
...HTMLAttributes,
src: sanitizedSrc,
},
)
const cleanedAttributes = Object.fromEntries(
Object.entries(mergedAttributes).filter(
([, value]) => value !== null && value !== undefined && value !== false,
),
)
return ['audio', cleanedAttributes]
},
...createAtomBlockMarkdownSpec({
nodeName: 'audio',
allowedAttributes: [
'src',
'controls',
'autoplay',
'loop',
'muted',
'preload',
'controlslist',
'crossorigin',
'disableremoteplayback',
],
}),
})

View File

@@ -0,0 +1,6 @@
import { Audio } from './audio.js'
export * from './audio.js'
export * from './utils.js'
export default Audio

View File

@@ -0,0 +1,70 @@
/**
* Regular expression to match URLs pointing to common audio file formats.
* Matches http/https URLs ending in: mp3, wav, ogg, oga, flac, m4a, aac, opus, weba, webm.
* Supports optional query strings. Case-insensitive, single match.
*/
export const AUDIO_URL_REGEX =
/https?:\/\/[^\s]+?\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\?[^\s]*)?/i
/**
* Global variant of AUDIO_URL_REGEX for matching multiple audio URLs in a string.
* Use this when you need to find all audio URLs in a block of text.
*/
export const AUDIO_URL_REGEX_GLOBAL =
/https?:\/\/[^\s]+?\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\?[^\s]*)?/gi
/**
* Regular expression to match base64-encoded audio data URLs.
*/
const AUDIO_DATA_URL_REGEX = /^data:audio\/[a-zA-Z0-9.+-]+;base64,/i
/**
* Validates whether a URL is safe for use in an audio element.
* Allows http, https, protocol-relative, and relative paths by default.
* Data URLs can be enabled via the allowBase64 flag.
*
* @param url - The URL string to validate
* @param allowBase64 - Whether to allow base64 data URLs (default: false)
* @returns True if the URL is valid and safe, false otherwise
*/
export const isValidAudioUrl = (url: string, allowBase64 = false) => {
if (!url) {
return false
}
if (allowBase64 && AUDIO_DATA_URL_REGEX.test(url)) {
return true
}
if (url.startsWith('//')) {
return true
}
if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
return true
}
try {
const parsedUrl = new URL(url)
return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'
} catch {
return false
}
}
/**
* Sanitizes an audio source URL by validating it against allowed protocols.
* Returns null for invalid or potentially unsafe URLs.
*
* @param url - The URL string to sanitize (can be null or undefined)
* @param allowBase64 - Whether to allow base64 data URLs (default: false)
* @returns The original URL if valid, or null if invalid/unsafe
*/
export const sanitizeAudioSrc = (url: string | null | undefined, allowBase64 = false) => {
if (!url) {
return null
}
return isValidAudioUrl(url, allowBase64) ? url : null
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vite-plus'
import { basePackConfig } from '../../pack.config.mjs'
export default defineConfig({
pack: {
entry: ['src/index.ts'],
...basePackConfig(),
},
})