@tiptap/extension-audio 3.23.6 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +17 -18
- package/src/audio.ts +3 -1
- package/src/utils.ts +2 -1
package/dist/index.cjs
CHANGED
|
@@ -175,7 +175,9 @@ var Audio = import_core.Node.create({
|
|
|
175
175
|
}
|
|
176
176
|
);
|
|
177
177
|
const cleanedAttributes = Object.fromEntries(
|
|
178
|
-
Object.entries(mergedAttributes).filter(
|
|
178
|
+
Object.entries(mergedAttributes).filter(
|
|
179
|
+
([, value]) => value !== null && value !== void 0 && value !== false
|
|
180
|
+
)
|
|
179
181
|
);
|
|
180
182
|
return ["audio", cleanedAttributes];
|
|
181
183
|
},
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/audio.ts","../src/utils.ts"],"sourcesContent":["import { Audio } from './audio.js'\n\nexport * from './audio.js'\nexport * from './utils.js'\n\nexport default Audio\n","import { createAtomBlockMarkdownSpec, mergeAttributes, Node, nodePasteRule } from '@tiptap/core'\n\nimport { AUDIO_URL_REGEX_GLOBAL, isValidAudioUrl, sanitizeAudioSrc } from './utils.js'\n\nexport interface AudioOptions {\n /**\n * Controls if the paste handler for audio sources should be added.\n * @default true\n * @example false\n */\n addPasteHandler: boolean\n\n /**\n * Controls if the audio should autoplay.\n * @default false\n * @example true\n */\n autoplay: boolean\n\n /**\n * Controls if the audio element should render the native controls.\n * @default true\n * @example false\n */\n controls: boolean\n\n /**\n * Controls if the audio should loop.\n * @default false\n * @example true\n */\n loop: boolean\n\n /**\n * Controls if the audio should be muted.\n * @default false\n * @example true\n */\n muted: boolean\n\n /**\n * The preload behavior for the audio element.\n * @default 'metadata'\n * @example 'none'\n */\n preload: 'auto' | 'metadata' | 'none' | null\n\n /**\n * Provides the controlslist attribute for the audio element.\n * @default undefined\n * @example 'nodownload'\n */\n controlslist?: string\n\n /**\n * Sets the crossorigin attribute for the audio element.\n * @default undefined\n * @example 'anonymous'\n */\n crossorigin?: '' | 'anonymous' | 'use-credentials'\n\n /**\n * Controls if remote playback should be disabled on the audio element.\n * @default false\n * @example true\n */\n disableRemotePlayback: boolean\n\n /**\n * Allows data: URLs for audio sources.\n * @default false\n * @example true\n */\n allowBase64: boolean\n\n /**\n * The HTML attributes for an audio node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n\n /**\n * Controls if the audio node should be inline or not.\n * @default false\n * @example true\n */\n inline: boolean\n}\n\n/**\n * The options for setting an audio element.\n */\ntype SetAudioOptions = {\n src: string\n controls?: boolean\n autoplay?: boolean\n loop?: boolean\n muted?: boolean\n preload?: 'auto' | 'metadata' | 'none' | null\n controlslist?: string\n crossorigin?: '' | 'anonymous' | 'use-credentials'\n disableremoteplayback?: boolean\n HTMLAttributes?: Record<string, any>\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n audio: {\n /**\n * Insert an audio element\n * @param options The audio attributes\n * @example editor.commands.setAudio({ src: 'https://assets.tiptap.dev/sounds/loop.mp3' })\n */\n setAudio: (options: SetAudioOptions) => ReturnType\n }\n }\n}\n\n/**\n * This extension adds support for native audio elements.\n */\nexport const Audio = Node.create<AudioOptions>({\n name: 'audio',\n\n addOptions() {\n return {\n addPasteHandler: true,\n allowBase64: false,\n autoplay: false,\n controls: true,\n loop: false,\n muted: false,\n preload: 'metadata',\n controlslist: undefined,\n crossorigin: undefined,\n disableRemotePlayback: false,\n HTMLAttributes: {},\n inline: false,\n }\n },\n\n inline() {\n return this.options.inline\n },\n\n group() {\n return this.options.inline ? 'inline' : 'block'\n },\n\n atom: true,\n\n draggable: true,\n\n addAttributes() {\n return {\n src: {\n default: null,\n },\n controls: {\n default: this.options.controls,\n },\n autoplay: {\n default: this.options.autoplay,\n },\n loop: {\n default: this.options.loop,\n },\n muted: {\n default: this.options.muted,\n },\n preload: {\n default: this.options.preload,\n },\n controlslist: {\n default: this.options.controlslist,\n },\n crossorigin: {\n default: this.options.crossorigin,\n },\n disableremoteplayback: {\n default: this.options.disableRemotePlayback,\n },\n }\n },\n\n parseHTML() {\n return [\n {\n tag: this.options.allowBase64 ? 'audio[src]' : 'audio[src]:not([src^=\"data:\"])',\n },\n ]\n },\n\n addCommands() {\n return {\n setAudio:\n options =>\n ({ commands }) => {\n if (!isValidAudioUrl(options.src, this.options.allowBase64)) {\n return false\n }\n\n return commands.insertContent({\n type: this.name,\n attrs: options,\n })\n },\n }\n },\n\n addPasteRules() {\n if (!this.options.addPasteHandler) {\n return []\n }\n\n return [\n nodePasteRule({\n find: AUDIO_URL_REGEX_GLOBAL,\n type: this.type,\n getAttributes: match => {\n return { src: match[0] }\n },\n }),\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n const sanitizedSrc = sanitizeAudioSrc(HTMLAttributes.src, this.options.allowBase64)\n\n const mergedAttributes = mergeAttributes(\n this.options.HTMLAttributes,\n {\n controls: this.options.controls,\n autoplay: this.options.autoplay,\n loop: this.options.loop,\n muted: this.options.muted,\n preload: this.options.preload,\n controlslist: this.options.controlslist,\n crossorigin: this.options.crossorigin,\n disableremoteplayback: this.options.disableRemotePlayback,\n },\n {\n ...HTMLAttributes,\n src: sanitizedSrc,\n },\n )\n\n const cleanedAttributes = Object.fromEntries(\n Object.entries(mergedAttributes).filter(([, value]) => value !== null && value !== undefined && value !== false),\n )\n\n return ['audio', cleanedAttributes]\n },\n\n ...createAtomBlockMarkdownSpec({\n nodeName: 'audio',\n allowedAttributes: [\n 'src',\n 'controls',\n 'autoplay',\n 'loop',\n 'muted',\n 'preload',\n 'controlslist',\n 'crossorigin',\n 'disableremoteplayback',\n ],\n }),\n})\n","/**\n * Regular expression to match URLs pointing to common audio file formats.\n * Matches http/https URLs ending in: mp3, wav, ogg, oga, flac, m4a, aac, opus, weba, webm.\n * Supports optional query strings. Case-insensitive, single match.\n */\nexport const AUDIO_URL_REGEX = /https?:\\/\\/[^\\s]+?\\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\\?[^\\s]*)?/i\n\n/**\n * Global variant of AUDIO_URL_REGEX for matching multiple audio URLs in a string.\n * Use this when you need to find all audio URLs in a block of text.\n */\nexport const AUDIO_URL_REGEX_GLOBAL =\n /https?:\\/\\/[^\\s]+?\\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\\?[^\\s]*)?/gi\n\n/**\n * Regular expression to match base64-encoded audio data URLs.\n */\nconst AUDIO_DATA_URL_REGEX = /^data:audio\\/[a-zA-Z0-9.+-]+;base64,/i\n\n/**\n * Validates whether a URL is safe for use in an audio element.\n * Allows http, https, protocol-relative, and relative paths by default.\n * Data URLs can be enabled via the allowBase64 flag.\n *\n * @param url - The URL string to validate\n * @param allowBase64 - Whether to allow base64 data URLs (default: false)\n * @returns True if the URL is valid and safe, false otherwise\n */\nexport const isValidAudioUrl = (url: string, allowBase64 = false) => {\n if (!url) {\n return false\n }\n\n if (allowBase64 && AUDIO_DATA_URL_REGEX.test(url)) {\n return true\n }\n\n if (url.startsWith('//')) {\n return true\n }\n\n if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {\n return true\n }\n\n try {\n const parsedUrl = new URL(url)\n\n return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'\n } catch {\n return false\n }\n}\n\n/**\n * Sanitizes an audio source URL by validating it against allowed protocols.\n * Returns null for invalid or potentially unsafe URLs.\n *\n * @param url - The URL string to sanitize (can be null or undefined)\n * @param allowBase64 - Whether to allow base64 data URLs (default: false)\n * @returns The original URL if valid, or null if invalid/unsafe\n */\nexport const sanitizeAudioSrc = (url: string | null | undefined, allowBase64 = false) => {\n if (!url) {\n return null\n }\n\n return isValidAudioUrl(url, allowBase64) ? url : null\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/audio.ts","../src/utils.ts"],"sourcesContent":["import { Audio } from './audio.js'\n\nexport * from './audio.js'\nexport * from './utils.js'\n\nexport default Audio\n","import { createAtomBlockMarkdownSpec, mergeAttributes, Node, nodePasteRule } from '@tiptap/core'\n\nimport { AUDIO_URL_REGEX_GLOBAL, isValidAudioUrl, sanitizeAudioSrc } from './utils.js'\n\nexport interface AudioOptions {\n /**\n * Controls if the paste handler for audio sources should be added.\n * @default true\n * @example false\n */\n addPasteHandler: boolean\n\n /**\n * Controls if the audio should autoplay.\n * @default false\n * @example true\n */\n autoplay: boolean\n\n /**\n * Controls if the audio element should render the native controls.\n * @default true\n * @example false\n */\n controls: boolean\n\n /**\n * Controls if the audio should loop.\n * @default false\n * @example true\n */\n loop: boolean\n\n /**\n * Controls if the audio should be muted.\n * @default false\n * @example true\n */\n muted: boolean\n\n /**\n * The preload behavior for the audio element.\n * @default 'metadata'\n * @example 'none'\n */\n preload: 'auto' | 'metadata' | 'none' | null\n\n /**\n * Provides the controlslist attribute for the audio element.\n * @default undefined\n * @example 'nodownload'\n */\n controlslist?: string\n\n /**\n * Sets the crossorigin attribute for the audio element.\n * @default undefined\n * @example 'anonymous'\n */\n crossorigin?: '' | 'anonymous' | 'use-credentials'\n\n /**\n * Controls if remote playback should be disabled on the audio element.\n * @default false\n * @example true\n */\n disableRemotePlayback: boolean\n\n /**\n * Allows data: URLs for audio sources.\n * @default false\n * @example true\n */\n allowBase64: boolean\n\n /**\n * The HTML attributes for an audio node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n\n /**\n * Controls if the audio node should be inline or not.\n * @default false\n * @example true\n */\n inline: boolean\n}\n\n/**\n * The options for setting an audio element.\n */\ntype SetAudioOptions = {\n src: string\n controls?: boolean\n autoplay?: boolean\n loop?: boolean\n muted?: boolean\n preload?: 'auto' | 'metadata' | 'none' | null\n controlslist?: string\n crossorigin?: '' | 'anonymous' | 'use-credentials'\n disableremoteplayback?: boolean\n HTMLAttributes?: Record<string, any>\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n audio: {\n /**\n * Insert an audio element\n * @param options The audio attributes\n * @example editor.commands.setAudio({ src: 'https://assets.tiptap.dev/sounds/loop.mp3' })\n */\n setAudio: (options: SetAudioOptions) => ReturnType\n }\n }\n}\n\n/**\n * This extension adds support for native audio elements.\n */\nexport const Audio = Node.create<AudioOptions>({\n name: 'audio',\n\n addOptions() {\n return {\n addPasteHandler: true,\n allowBase64: false,\n autoplay: false,\n controls: true,\n loop: false,\n muted: false,\n preload: 'metadata',\n controlslist: undefined,\n crossorigin: undefined,\n disableRemotePlayback: false,\n HTMLAttributes: {},\n inline: false,\n }\n },\n\n inline() {\n return this.options.inline\n },\n\n group() {\n return this.options.inline ? 'inline' : 'block'\n },\n\n atom: true,\n\n draggable: true,\n\n addAttributes() {\n return {\n src: {\n default: null,\n },\n controls: {\n default: this.options.controls,\n },\n autoplay: {\n default: this.options.autoplay,\n },\n loop: {\n default: this.options.loop,\n },\n muted: {\n default: this.options.muted,\n },\n preload: {\n default: this.options.preload,\n },\n controlslist: {\n default: this.options.controlslist,\n },\n crossorigin: {\n default: this.options.crossorigin,\n },\n disableremoteplayback: {\n default: this.options.disableRemotePlayback,\n },\n }\n },\n\n parseHTML() {\n return [\n {\n tag: this.options.allowBase64 ? 'audio[src]' : 'audio[src]:not([src^=\"data:\"])',\n },\n ]\n },\n\n addCommands() {\n return {\n setAudio:\n options =>\n ({ commands }) => {\n if (!isValidAudioUrl(options.src, this.options.allowBase64)) {\n return false\n }\n\n return commands.insertContent({\n type: this.name,\n attrs: options,\n })\n },\n }\n },\n\n addPasteRules() {\n if (!this.options.addPasteHandler) {\n return []\n }\n\n return [\n nodePasteRule({\n find: AUDIO_URL_REGEX_GLOBAL,\n type: this.type,\n getAttributes: match => {\n return { src: match[0] }\n },\n }),\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n const sanitizedSrc = sanitizeAudioSrc(HTMLAttributes.src, this.options.allowBase64)\n\n const mergedAttributes = mergeAttributes(\n this.options.HTMLAttributes,\n {\n controls: this.options.controls,\n autoplay: this.options.autoplay,\n loop: this.options.loop,\n muted: this.options.muted,\n preload: this.options.preload,\n controlslist: this.options.controlslist,\n crossorigin: this.options.crossorigin,\n disableremoteplayback: this.options.disableRemotePlayback,\n },\n {\n ...HTMLAttributes,\n src: sanitizedSrc,\n },\n )\n\n const cleanedAttributes = Object.fromEntries(\n Object.entries(mergedAttributes).filter(\n ([, value]) => value !== null && value !== undefined && value !== false,\n ),\n )\n\n return ['audio', cleanedAttributes]\n },\n\n ...createAtomBlockMarkdownSpec({\n nodeName: 'audio',\n allowedAttributes: [\n 'src',\n 'controls',\n 'autoplay',\n 'loop',\n 'muted',\n 'preload',\n 'controlslist',\n 'crossorigin',\n 'disableremoteplayback',\n ],\n }),\n})\n","/**\n * Regular expression to match URLs pointing to common audio file formats.\n * Matches http/https URLs ending in: mp3, wav, ogg, oga, flac, m4a, aac, opus, weba, webm.\n * Supports optional query strings. Case-insensitive, single match.\n */\nexport const AUDIO_URL_REGEX =\n /https?:\\/\\/[^\\s]+?\\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\\?[^\\s]*)?/i\n\n/**\n * Global variant of AUDIO_URL_REGEX for matching multiple audio URLs in a string.\n * Use this when you need to find all audio URLs in a block of text.\n */\nexport const AUDIO_URL_REGEX_GLOBAL =\n /https?:\\/\\/[^\\s]+?\\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\\?[^\\s]*)?/gi\n\n/**\n * Regular expression to match base64-encoded audio data URLs.\n */\nconst AUDIO_DATA_URL_REGEX = /^data:audio\\/[a-zA-Z0-9.+-]+;base64,/i\n\n/**\n * Validates whether a URL is safe for use in an audio element.\n * Allows http, https, protocol-relative, and relative paths by default.\n * Data URLs can be enabled via the allowBase64 flag.\n *\n * @param url - The URL string to validate\n * @param allowBase64 - Whether to allow base64 data URLs (default: false)\n * @returns True if the URL is valid and safe, false otherwise\n */\nexport const isValidAudioUrl = (url: string, allowBase64 = false) => {\n if (!url) {\n return false\n }\n\n if (allowBase64 && AUDIO_DATA_URL_REGEX.test(url)) {\n return true\n }\n\n if (url.startsWith('//')) {\n return true\n }\n\n if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {\n return true\n }\n\n try {\n const parsedUrl = new URL(url)\n\n return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'\n } catch {\n return false\n }\n}\n\n/**\n * Sanitizes an audio source URL by validating it against allowed protocols.\n * Returns null for invalid or potentially unsafe URLs.\n *\n * @param url - The URL string to sanitize (can be null or undefined)\n * @param allowBase64 - Whether to allow base64 data URLs (default: false)\n * @returns The original URL if valid, or null if invalid/unsafe\n */\nexport const sanitizeAudioSrc = (url: string | null | undefined, allowBase64 = false) => {\n if (!url) {\n return null\n }\n\n return isValidAudioUrl(url, allowBase64) ? url : null\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -144,7 +144,9 @@ var Audio = Node.create({
|
|
|
144
144
|
}
|
|
145
145
|
);
|
|
146
146
|
const cleanedAttributes = Object.fromEntries(
|
|
147
|
-
Object.entries(mergedAttributes).filter(
|
|
147
|
+
Object.entries(mergedAttributes).filter(
|
|
148
|
+
([, value]) => value !== null && value !== void 0 && value !== false
|
|
149
|
+
)
|
|
148
150
|
);
|
|
149
151
|
return ["audio", cleanedAttributes];
|
|
150
152
|
},
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/audio.ts","../src/utils.ts","../src/index.ts"],"sourcesContent":["import { createAtomBlockMarkdownSpec, mergeAttributes, Node, nodePasteRule } from '@tiptap/core'\n\nimport { AUDIO_URL_REGEX_GLOBAL, isValidAudioUrl, sanitizeAudioSrc } from './utils.js'\n\nexport interface AudioOptions {\n /**\n * Controls if the paste handler for audio sources should be added.\n * @default true\n * @example false\n */\n addPasteHandler: boolean\n\n /**\n * Controls if the audio should autoplay.\n * @default false\n * @example true\n */\n autoplay: boolean\n\n /**\n * Controls if the audio element should render the native controls.\n * @default true\n * @example false\n */\n controls: boolean\n\n /**\n * Controls if the audio should loop.\n * @default false\n * @example true\n */\n loop: boolean\n\n /**\n * Controls if the audio should be muted.\n * @default false\n * @example true\n */\n muted: boolean\n\n /**\n * The preload behavior for the audio element.\n * @default 'metadata'\n * @example 'none'\n */\n preload: 'auto' | 'metadata' | 'none' | null\n\n /**\n * Provides the controlslist attribute for the audio element.\n * @default undefined\n * @example 'nodownload'\n */\n controlslist?: string\n\n /**\n * Sets the crossorigin attribute for the audio element.\n * @default undefined\n * @example 'anonymous'\n */\n crossorigin?: '' | 'anonymous' | 'use-credentials'\n\n /**\n * Controls if remote playback should be disabled on the audio element.\n * @default false\n * @example true\n */\n disableRemotePlayback: boolean\n\n /**\n * Allows data: URLs for audio sources.\n * @default false\n * @example true\n */\n allowBase64: boolean\n\n /**\n * The HTML attributes for an audio node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n\n /**\n * Controls if the audio node should be inline or not.\n * @default false\n * @example true\n */\n inline: boolean\n}\n\n/**\n * The options for setting an audio element.\n */\ntype SetAudioOptions = {\n src: string\n controls?: boolean\n autoplay?: boolean\n loop?: boolean\n muted?: boolean\n preload?: 'auto' | 'metadata' | 'none' | null\n controlslist?: string\n crossorigin?: '' | 'anonymous' | 'use-credentials'\n disableremoteplayback?: boolean\n HTMLAttributes?: Record<string, any>\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n audio: {\n /**\n * Insert an audio element\n * @param options The audio attributes\n * @example editor.commands.setAudio({ src: 'https://assets.tiptap.dev/sounds/loop.mp3' })\n */\n setAudio: (options: SetAudioOptions) => ReturnType\n }\n }\n}\n\n/**\n * This extension adds support for native audio elements.\n */\nexport const Audio = Node.create<AudioOptions>({\n name: 'audio',\n\n addOptions() {\n return {\n addPasteHandler: true,\n allowBase64: false,\n autoplay: false,\n controls: true,\n loop: false,\n muted: false,\n preload: 'metadata',\n controlslist: undefined,\n crossorigin: undefined,\n disableRemotePlayback: false,\n HTMLAttributes: {},\n inline: false,\n }\n },\n\n inline() {\n return this.options.inline\n },\n\n group() {\n return this.options.inline ? 'inline' : 'block'\n },\n\n atom: true,\n\n draggable: true,\n\n addAttributes() {\n return {\n src: {\n default: null,\n },\n controls: {\n default: this.options.controls,\n },\n autoplay: {\n default: this.options.autoplay,\n },\n loop: {\n default: this.options.loop,\n },\n muted: {\n default: this.options.muted,\n },\n preload: {\n default: this.options.preload,\n },\n controlslist: {\n default: this.options.controlslist,\n },\n crossorigin: {\n default: this.options.crossorigin,\n },\n disableremoteplayback: {\n default: this.options.disableRemotePlayback,\n },\n }\n },\n\n parseHTML() {\n return [\n {\n tag: this.options.allowBase64 ? 'audio[src]' : 'audio[src]:not([src^=\"data:\"])',\n },\n ]\n },\n\n addCommands() {\n return {\n setAudio:\n options =>\n ({ commands }) => {\n if (!isValidAudioUrl(options.src, this.options.allowBase64)) {\n return false\n }\n\n return commands.insertContent({\n type: this.name,\n attrs: options,\n })\n },\n }\n },\n\n addPasteRules() {\n if (!this.options.addPasteHandler) {\n return []\n }\n\n return [\n nodePasteRule({\n find: AUDIO_URL_REGEX_GLOBAL,\n type: this.type,\n getAttributes: match => {\n return { src: match[0] }\n },\n }),\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n const sanitizedSrc = sanitizeAudioSrc(HTMLAttributes.src, this.options.allowBase64)\n\n const mergedAttributes = mergeAttributes(\n this.options.HTMLAttributes,\n {\n controls: this.options.controls,\n autoplay: this.options.autoplay,\n loop: this.options.loop,\n muted: this.options.muted,\n preload: this.options.preload,\n controlslist: this.options.controlslist,\n crossorigin: this.options.crossorigin,\n disableremoteplayback: this.options.disableRemotePlayback,\n },\n {\n ...HTMLAttributes,\n src: sanitizedSrc,\n },\n )\n\n const cleanedAttributes = Object.fromEntries(\n Object.entries(mergedAttributes).filter(([, value]) => value !== null && value !== undefined && value !== false),\n )\n\n return ['audio', cleanedAttributes]\n },\n\n ...createAtomBlockMarkdownSpec({\n nodeName: 'audio',\n allowedAttributes: [\n 'src',\n 'controls',\n 'autoplay',\n 'loop',\n 'muted',\n 'preload',\n 'controlslist',\n 'crossorigin',\n 'disableremoteplayback',\n ],\n }),\n})\n","/**\n * Regular expression to match URLs pointing to common audio file formats.\n * Matches http/https URLs ending in: mp3, wav, ogg, oga, flac, m4a, aac, opus, weba, webm.\n * Supports optional query strings. Case-insensitive, single match.\n */\nexport const AUDIO_URL_REGEX = /https?:\\/\\/[^\\s]+?\\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\\?[^\\s]*)?/i\n\n/**\n * Global variant of AUDIO_URL_REGEX for matching multiple audio URLs in a string.\n * Use this when you need to find all audio URLs in a block of text.\n */\nexport const AUDIO_URL_REGEX_GLOBAL =\n /https?:\\/\\/[^\\s]+?\\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\\?[^\\s]*)?/gi\n\n/**\n * Regular expression to match base64-encoded audio data URLs.\n */\nconst AUDIO_DATA_URL_REGEX = /^data:audio\\/[a-zA-Z0-9.+-]+;base64,/i\n\n/**\n * Validates whether a URL is safe for use in an audio element.\n * Allows http, https, protocol-relative, and relative paths by default.\n * Data URLs can be enabled via the allowBase64 flag.\n *\n * @param url - The URL string to validate\n * @param allowBase64 - Whether to allow base64 data URLs (default: false)\n * @returns True if the URL is valid and safe, false otherwise\n */\nexport const isValidAudioUrl = (url: string, allowBase64 = false) => {\n if (!url) {\n return false\n }\n\n if (allowBase64 && AUDIO_DATA_URL_REGEX.test(url)) {\n return true\n }\n\n if (url.startsWith('//')) {\n return true\n }\n\n if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {\n return true\n }\n\n try {\n const parsedUrl = new URL(url)\n\n return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'\n } catch {\n return false\n }\n}\n\n/**\n * Sanitizes an audio source URL by validating it against allowed protocols.\n * Returns null for invalid or potentially unsafe URLs.\n *\n * @param url - The URL string to sanitize (can be null or undefined)\n * @param allowBase64 - Whether to allow base64 data URLs (default: false)\n * @returns The original URL if valid, or null if invalid/unsafe\n */\nexport const sanitizeAudioSrc = (url: string | null | undefined, allowBase64 = false) => {\n if (!url) {\n return null\n }\n\n return isValidAudioUrl(url, allowBase64) ? url : null\n}\n","import { Audio } from './audio.js'\n\nexport * from './audio.js'\nexport * from './utils.js'\n\nexport default Audio\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/audio.ts","../src/utils.ts","../src/index.ts"],"sourcesContent":["import { createAtomBlockMarkdownSpec, mergeAttributes, Node, nodePasteRule } from '@tiptap/core'\n\nimport { AUDIO_URL_REGEX_GLOBAL, isValidAudioUrl, sanitizeAudioSrc } from './utils.js'\n\nexport interface AudioOptions {\n /**\n * Controls if the paste handler for audio sources should be added.\n * @default true\n * @example false\n */\n addPasteHandler: boolean\n\n /**\n * Controls if the audio should autoplay.\n * @default false\n * @example true\n */\n autoplay: boolean\n\n /**\n * Controls if the audio element should render the native controls.\n * @default true\n * @example false\n */\n controls: boolean\n\n /**\n * Controls if the audio should loop.\n * @default false\n * @example true\n */\n loop: boolean\n\n /**\n * Controls if the audio should be muted.\n * @default false\n * @example true\n */\n muted: boolean\n\n /**\n * The preload behavior for the audio element.\n * @default 'metadata'\n * @example 'none'\n */\n preload: 'auto' | 'metadata' | 'none' | null\n\n /**\n * Provides the controlslist attribute for the audio element.\n * @default undefined\n * @example 'nodownload'\n */\n controlslist?: string\n\n /**\n * Sets the crossorigin attribute for the audio element.\n * @default undefined\n * @example 'anonymous'\n */\n crossorigin?: '' | 'anonymous' | 'use-credentials'\n\n /**\n * Controls if remote playback should be disabled on the audio element.\n * @default false\n * @example true\n */\n disableRemotePlayback: boolean\n\n /**\n * Allows data: URLs for audio sources.\n * @default false\n * @example true\n */\n allowBase64: boolean\n\n /**\n * The HTML attributes for an audio node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n\n /**\n * Controls if the audio node should be inline or not.\n * @default false\n * @example true\n */\n inline: boolean\n}\n\n/**\n * The options for setting an audio element.\n */\ntype SetAudioOptions = {\n src: string\n controls?: boolean\n autoplay?: boolean\n loop?: boolean\n muted?: boolean\n preload?: 'auto' | 'metadata' | 'none' | null\n controlslist?: string\n crossorigin?: '' | 'anonymous' | 'use-credentials'\n disableremoteplayback?: boolean\n HTMLAttributes?: Record<string, any>\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n audio: {\n /**\n * Insert an audio element\n * @param options The audio attributes\n * @example editor.commands.setAudio({ src: 'https://assets.tiptap.dev/sounds/loop.mp3' })\n */\n setAudio: (options: SetAudioOptions) => ReturnType\n }\n }\n}\n\n/**\n * This extension adds support for native audio elements.\n */\nexport const Audio = Node.create<AudioOptions>({\n name: 'audio',\n\n addOptions() {\n return {\n addPasteHandler: true,\n allowBase64: false,\n autoplay: false,\n controls: true,\n loop: false,\n muted: false,\n preload: 'metadata',\n controlslist: undefined,\n crossorigin: undefined,\n disableRemotePlayback: false,\n HTMLAttributes: {},\n inline: false,\n }\n },\n\n inline() {\n return this.options.inline\n },\n\n group() {\n return this.options.inline ? 'inline' : 'block'\n },\n\n atom: true,\n\n draggable: true,\n\n addAttributes() {\n return {\n src: {\n default: null,\n },\n controls: {\n default: this.options.controls,\n },\n autoplay: {\n default: this.options.autoplay,\n },\n loop: {\n default: this.options.loop,\n },\n muted: {\n default: this.options.muted,\n },\n preload: {\n default: this.options.preload,\n },\n controlslist: {\n default: this.options.controlslist,\n },\n crossorigin: {\n default: this.options.crossorigin,\n },\n disableremoteplayback: {\n default: this.options.disableRemotePlayback,\n },\n }\n },\n\n parseHTML() {\n return [\n {\n tag: this.options.allowBase64 ? 'audio[src]' : 'audio[src]:not([src^=\"data:\"])',\n },\n ]\n },\n\n addCommands() {\n return {\n setAudio:\n options =>\n ({ commands }) => {\n if (!isValidAudioUrl(options.src, this.options.allowBase64)) {\n return false\n }\n\n return commands.insertContent({\n type: this.name,\n attrs: options,\n })\n },\n }\n },\n\n addPasteRules() {\n if (!this.options.addPasteHandler) {\n return []\n }\n\n return [\n nodePasteRule({\n find: AUDIO_URL_REGEX_GLOBAL,\n type: this.type,\n getAttributes: match => {\n return { src: match[0] }\n },\n }),\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n const sanitizedSrc = sanitizeAudioSrc(HTMLAttributes.src, this.options.allowBase64)\n\n const mergedAttributes = mergeAttributes(\n this.options.HTMLAttributes,\n {\n controls: this.options.controls,\n autoplay: this.options.autoplay,\n loop: this.options.loop,\n muted: this.options.muted,\n preload: this.options.preload,\n controlslist: this.options.controlslist,\n crossorigin: this.options.crossorigin,\n disableremoteplayback: this.options.disableRemotePlayback,\n },\n {\n ...HTMLAttributes,\n src: sanitizedSrc,\n },\n )\n\n const cleanedAttributes = Object.fromEntries(\n Object.entries(mergedAttributes).filter(\n ([, value]) => value !== null && value !== undefined && value !== false,\n ),\n )\n\n return ['audio', cleanedAttributes]\n },\n\n ...createAtomBlockMarkdownSpec({\n nodeName: 'audio',\n allowedAttributes: [\n 'src',\n 'controls',\n 'autoplay',\n 'loop',\n 'muted',\n 'preload',\n 'controlslist',\n 'crossorigin',\n 'disableremoteplayback',\n ],\n }),\n})\n","/**\n * Regular expression to match URLs pointing to common audio file formats.\n * Matches http/https URLs ending in: mp3, wav, ogg, oga, flac, m4a, aac, opus, weba, webm.\n * Supports optional query strings. Case-insensitive, single match.\n */\nexport const AUDIO_URL_REGEX =\n /https?:\\/\\/[^\\s]+?\\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\\?[^\\s]*)?/i\n\n/**\n * Global variant of AUDIO_URL_REGEX for matching multiple audio URLs in a string.\n * Use this when you need to find all audio URLs in a block of text.\n */\nexport const AUDIO_URL_REGEX_GLOBAL =\n /https?:\\/\\/[^\\s]+?\\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\\?[^\\s]*)?/gi\n\n/**\n * Regular expression to match base64-encoded audio data URLs.\n */\nconst AUDIO_DATA_URL_REGEX = /^data:audio\\/[a-zA-Z0-9.+-]+;base64,/i\n\n/**\n * Validates whether a URL is safe for use in an audio element.\n * Allows http, https, protocol-relative, and relative paths by default.\n * Data URLs can be enabled via the allowBase64 flag.\n *\n * @param url - The URL string to validate\n * @param allowBase64 - Whether to allow base64 data URLs (default: false)\n * @returns True if the URL is valid and safe, false otherwise\n */\nexport const isValidAudioUrl = (url: string, allowBase64 = false) => {\n if (!url) {\n return false\n }\n\n if (allowBase64 && AUDIO_DATA_URL_REGEX.test(url)) {\n return true\n }\n\n if (url.startsWith('//')) {\n return true\n }\n\n if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {\n return true\n }\n\n try {\n const parsedUrl = new URL(url)\n\n return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'\n } catch {\n return false\n }\n}\n\n/**\n * Sanitizes an audio source URL by validating it against allowed protocols.\n * Returns null for invalid or potentially unsafe URLs.\n *\n * @param url - The URL string to sanitize (can be null or undefined)\n * @param allowBase64 - Whether to allow base64 data URLs (default: false)\n * @returns The original URL if valid, or null if invalid/unsafe\n */\nexport const sanitizeAudioSrc = (url: string | null | undefined, allowBase64 = false) => {\n if (!url) {\n return null\n }\n\n return isValidAudioUrl(url, allowBase64) ? url : null\n}\n","import { Audio } from './audio.js'\n\nexport * from './audio.js'\nexport * from './utils.js'\n\nexport default Audio\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiptap/extension-audio",
|
|
3
|
+
"version": "3.24.0",
|
|
3
4
|
"description": "audio extension for tiptap",
|
|
4
|
-
"version": "3.23.6",
|
|
5
|
-
"homepage": "https://tiptap.dev",
|
|
6
5
|
"keywords": [
|
|
7
6
|
"tiptap",
|
|
8
7
|
"tiptap extension"
|
|
9
8
|
],
|
|
9
|
+
"homepage": "https://tiptap.dev",
|
|
10
10
|
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/ueberdosis/tiptap",
|
|
14
|
+
"directory": "packages/extension-audio"
|
|
15
|
+
},
|
|
11
16
|
"funding": {
|
|
12
17
|
"type": "github",
|
|
13
18
|
"url": "https://github.com/sponsors/ueberdosis"
|
|
14
19
|
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
15
24
|
"type": "module",
|
|
25
|
+
"main": "dist/index.cjs",
|
|
26
|
+
"module": "dist/index.js",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
16
28
|
"exports": {
|
|
17
29
|
".": {
|
|
18
30
|
"types": {
|
|
@@ -23,26 +35,13 @@
|
|
|
23
35
|
"require": "./dist/index.cjs"
|
|
24
36
|
}
|
|
25
37
|
},
|
|
26
|
-
"main": "dist/index.cjs",
|
|
27
|
-
"module": "dist/index.js",
|
|
28
|
-
"types": "dist/index.d.ts",
|
|
29
|
-
"files": [
|
|
30
|
-
"src",
|
|
31
|
-
"dist"
|
|
32
|
-
],
|
|
33
38
|
"devDependencies": {
|
|
34
|
-
"@tiptap/core": "^3.
|
|
39
|
+
"@tiptap/core": "^3.24.0"
|
|
35
40
|
},
|
|
36
41
|
"peerDependencies": {
|
|
37
|
-
"@tiptap/core": "3.
|
|
38
|
-
},
|
|
39
|
-
"repository": {
|
|
40
|
-
"type": "git",
|
|
41
|
-
"url": "https://github.com/ueberdosis/tiptap",
|
|
42
|
-
"directory": "packages/extension-audio"
|
|
42
|
+
"@tiptap/core": "3.24.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
|
-
"build": "tsup"
|
|
46
|
-
"lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
|
|
45
|
+
"build": "tsup"
|
|
47
46
|
}
|
|
48
47
|
}
|
package/src/audio.ts
CHANGED
|
@@ -247,7 +247,9 @@ export const Audio = Node.create<AudioOptions>({
|
|
|
247
247
|
)
|
|
248
248
|
|
|
249
249
|
const cleanedAttributes = Object.fromEntries(
|
|
250
|
-
Object.entries(mergedAttributes).filter(
|
|
250
|
+
Object.entries(mergedAttributes).filter(
|
|
251
|
+
([, value]) => value !== null && value !== undefined && value !== false,
|
|
252
|
+
),
|
|
251
253
|
)
|
|
252
254
|
|
|
253
255
|
return ['audio', cleanedAttributes]
|
package/src/utils.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Matches http/https URLs ending in: mp3, wav, ogg, oga, flac, m4a, aac, opus, weba, webm.
|
|
4
4
|
* Supports optional query strings. Case-insensitive, single match.
|
|
5
5
|
*/
|
|
6
|
-
export const AUDIO_URL_REGEX =
|
|
6
|
+
export const AUDIO_URL_REGEX =
|
|
7
|
+
/https?:\/\/[^\s]+?\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\?[^\s]*)?/i
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Global variant of AUDIO_URL_REGEX for matching multiple audio URLs in a string.
|