@tiptap/extension-audio 3.16.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/LICENSE.md +21 -0
- package/README.md +11 -0
- package/dist/index.cjs +208 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +140 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +177 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/audio.ts +270 -0
- package/src/index.ts +6 -0
- package/src/utils.ts +69 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Tiptap GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AUDIO_URL_REGEX: () => AUDIO_URL_REGEX,
|
|
24
|
+
AUDIO_URL_REGEX_GLOBAL: () => AUDIO_URL_REGEX_GLOBAL,
|
|
25
|
+
Audio: () => Audio,
|
|
26
|
+
default: () => index_default,
|
|
27
|
+
isValidAudioUrl: () => isValidAudioUrl,
|
|
28
|
+
sanitizeAudioSrc: () => sanitizeAudioSrc
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/audio.ts
|
|
33
|
+
var import_core = require("@tiptap/core");
|
|
34
|
+
|
|
35
|
+
// src/utils.ts
|
|
36
|
+
var AUDIO_URL_REGEX = /https?:\/\/[^\s]+?\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\?[^\s]*)?/i;
|
|
37
|
+
var AUDIO_URL_REGEX_GLOBAL = /https?:\/\/[^\s]+?\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\?[^\s]*)?/gi;
|
|
38
|
+
var AUDIO_DATA_URL_REGEX = /^data:audio\/[a-zA-Z0-9.+-]+;base64,/i;
|
|
39
|
+
var isValidAudioUrl = (url, allowBase64 = false) => {
|
|
40
|
+
if (!url) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (allowBase64 && AUDIO_DATA_URL_REGEX.test(url)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (url.startsWith("//")) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const parsedUrl = new URL(url);
|
|
54
|
+
return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var sanitizeAudioSrc = (url, allowBase64 = false) => {
|
|
60
|
+
if (!url) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return isValidAudioUrl(url, allowBase64) ? url : null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/audio.ts
|
|
67
|
+
var Audio = import_core.Node.create({
|
|
68
|
+
name: "audio",
|
|
69
|
+
addOptions() {
|
|
70
|
+
return {
|
|
71
|
+
addPasteHandler: true,
|
|
72
|
+
allowBase64: false,
|
|
73
|
+
autoplay: false,
|
|
74
|
+
controls: true,
|
|
75
|
+
loop: false,
|
|
76
|
+
muted: false,
|
|
77
|
+
preload: "metadata",
|
|
78
|
+
controlslist: void 0,
|
|
79
|
+
crossorigin: void 0,
|
|
80
|
+
disableRemotePlayback: false,
|
|
81
|
+
HTMLAttributes: {},
|
|
82
|
+
inline: false
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
inline() {
|
|
86
|
+
return this.options.inline;
|
|
87
|
+
},
|
|
88
|
+
group() {
|
|
89
|
+
return this.options.inline ? "inline" : "block";
|
|
90
|
+
},
|
|
91
|
+
atom: true,
|
|
92
|
+
draggable: true,
|
|
93
|
+
addAttributes() {
|
|
94
|
+
return {
|
|
95
|
+
src: {
|
|
96
|
+
default: null
|
|
97
|
+
},
|
|
98
|
+
controls: {
|
|
99
|
+
default: this.options.controls
|
|
100
|
+
},
|
|
101
|
+
autoplay: {
|
|
102
|
+
default: this.options.autoplay
|
|
103
|
+
},
|
|
104
|
+
loop: {
|
|
105
|
+
default: this.options.loop
|
|
106
|
+
},
|
|
107
|
+
muted: {
|
|
108
|
+
default: this.options.muted
|
|
109
|
+
},
|
|
110
|
+
preload: {
|
|
111
|
+
default: this.options.preload
|
|
112
|
+
},
|
|
113
|
+
controlslist: {
|
|
114
|
+
default: this.options.controlslist
|
|
115
|
+
},
|
|
116
|
+
crossorigin: {
|
|
117
|
+
default: this.options.crossorigin
|
|
118
|
+
},
|
|
119
|
+
disableremoteplayback: {
|
|
120
|
+
default: this.options.disableRemotePlayback
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
parseHTML() {
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
tag: this.options.allowBase64 ? "audio[src]" : 'audio[src]:not([src^="data:"])'
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
},
|
|
131
|
+
addCommands() {
|
|
132
|
+
return {
|
|
133
|
+
setAudio: (options) => ({ commands }) => {
|
|
134
|
+
if (!isValidAudioUrl(options.src, this.options.allowBase64)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return commands.insertContent({
|
|
138
|
+
type: this.name,
|
|
139
|
+
attrs: options
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
addPasteRules() {
|
|
145
|
+
if (!this.options.addPasteHandler) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
return [
|
|
149
|
+
(0, import_core.nodePasteRule)({
|
|
150
|
+
find: AUDIO_URL_REGEX_GLOBAL,
|
|
151
|
+
type: this.type,
|
|
152
|
+
getAttributes: (match) => {
|
|
153
|
+
return { src: match[0] };
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
];
|
|
157
|
+
},
|
|
158
|
+
renderHTML({ HTMLAttributes }) {
|
|
159
|
+
const sanitizedSrc = sanitizeAudioSrc(HTMLAttributes.src, this.options.allowBase64);
|
|
160
|
+
const mergedAttributes = (0, import_core.mergeAttributes)(
|
|
161
|
+
this.options.HTMLAttributes,
|
|
162
|
+
{
|
|
163
|
+
controls: this.options.controls,
|
|
164
|
+
autoplay: this.options.autoplay,
|
|
165
|
+
loop: this.options.loop,
|
|
166
|
+
muted: this.options.muted,
|
|
167
|
+
preload: this.options.preload,
|
|
168
|
+
controlslist: this.options.controlslist,
|
|
169
|
+
crossorigin: this.options.crossorigin,
|
|
170
|
+
disableremoteplayback: this.options.disableRemotePlayback
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
...HTMLAttributes,
|
|
174
|
+
src: sanitizedSrc
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
const cleanedAttributes = Object.fromEntries(
|
|
178
|
+
Object.entries(mergedAttributes).filter(([, value]) => value !== null && value !== void 0 && value !== false)
|
|
179
|
+
);
|
|
180
|
+
return ["audio", cleanedAttributes];
|
|
181
|
+
},
|
|
182
|
+
...(0, import_core.createAtomBlockMarkdownSpec)({
|
|
183
|
+
nodeName: "audio",
|
|
184
|
+
allowedAttributes: [
|
|
185
|
+
"src",
|
|
186
|
+
"controls",
|
|
187
|
+
"autoplay",
|
|
188
|
+
"loop",
|
|
189
|
+
"muted",
|
|
190
|
+
"preload",
|
|
191
|
+
"controlslist",
|
|
192
|
+
"crossorigin",
|
|
193
|
+
"disableremoteplayback"
|
|
194
|
+
]
|
|
195
|
+
})
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// src/index.ts
|
|
199
|
+
var index_default = Audio;
|
|
200
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
201
|
+
0 && (module.exports = {
|
|
202
|
+
AUDIO_URL_REGEX,
|
|
203
|
+
AUDIO_URL_REGEX_GLOBAL,
|
|
204
|
+
Audio,
|
|
205
|
+
isValidAudioUrl,
|
|
206
|
+
sanitizeAudioSrc
|
|
207
|
+
});
|
|
208
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Node } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
interface AudioOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Controls if the paste handler for audio sources should be added.
|
|
6
|
+
* @default true
|
|
7
|
+
* @example false
|
|
8
|
+
*/
|
|
9
|
+
addPasteHandler: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Controls if the audio should autoplay.
|
|
12
|
+
* @default false
|
|
13
|
+
* @example true
|
|
14
|
+
*/
|
|
15
|
+
autoplay: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Controls if the audio element should render the native controls.
|
|
18
|
+
* @default true
|
|
19
|
+
* @example false
|
|
20
|
+
*/
|
|
21
|
+
controls: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Controls if the audio should loop.
|
|
24
|
+
* @default false
|
|
25
|
+
* @example true
|
|
26
|
+
*/
|
|
27
|
+
loop: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Controls if the audio should be muted.
|
|
30
|
+
* @default false
|
|
31
|
+
* @example true
|
|
32
|
+
*/
|
|
33
|
+
muted: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The preload behavior for the audio element.
|
|
36
|
+
* @default 'metadata'
|
|
37
|
+
* @example 'none'
|
|
38
|
+
*/
|
|
39
|
+
preload: 'auto' | 'metadata' | 'none' | null;
|
|
40
|
+
/**
|
|
41
|
+
* Provides the controlslist attribute for the audio element.
|
|
42
|
+
* @default undefined
|
|
43
|
+
* @example 'nodownload'
|
|
44
|
+
*/
|
|
45
|
+
controlslist?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Sets the crossorigin attribute for the audio element.
|
|
48
|
+
* @default undefined
|
|
49
|
+
* @example 'anonymous'
|
|
50
|
+
*/
|
|
51
|
+
crossorigin?: '' | 'anonymous' | 'use-credentials';
|
|
52
|
+
/**
|
|
53
|
+
* Controls if remote playback should be disabled on the audio element.
|
|
54
|
+
* @default false
|
|
55
|
+
* @example true
|
|
56
|
+
*/
|
|
57
|
+
disableRemotePlayback: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Allows data: URLs for audio sources.
|
|
60
|
+
* @default false
|
|
61
|
+
* @example true
|
|
62
|
+
*/
|
|
63
|
+
allowBase64: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The HTML attributes for an audio node.
|
|
66
|
+
* @default {}
|
|
67
|
+
* @example { class: 'foo' }
|
|
68
|
+
*/
|
|
69
|
+
HTMLAttributes: Record<string, any>;
|
|
70
|
+
/**
|
|
71
|
+
* Controls if the audio node should be inline or not.
|
|
72
|
+
* @default false
|
|
73
|
+
* @example true
|
|
74
|
+
*/
|
|
75
|
+
inline: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The options for setting an audio element.
|
|
79
|
+
*/
|
|
80
|
+
type SetAudioOptions = {
|
|
81
|
+
src: string;
|
|
82
|
+
controls?: boolean;
|
|
83
|
+
autoplay?: boolean;
|
|
84
|
+
loop?: boolean;
|
|
85
|
+
muted?: boolean;
|
|
86
|
+
preload?: 'auto' | 'metadata' | 'none' | null;
|
|
87
|
+
controlslist?: string;
|
|
88
|
+
crossorigin?: '' | 'anonymous' | 'use-credentials';
|
|
89
|
+
disableremoteplayback?: boolean;
|
|
90
|
+
HTMLAttributes?: Record<string, any>;
|
|
91
|
+
};
|
|
92
|
+
declare module '@tiptap/core' {
|
|
93
|
+
interface Commands<ReturnType> {
|
|
94
|
+
audio: {
|
|
95
|
+
/**
|
|
96
|
+
* Insert an audio element
|
|
97
|
+
* @param options The audio attributes
|
|
98
|
+
* @example editor.commands.setAudio({ src: 'https://assets.tiptap.dev/sounds/loop.mp3' })
|
|
99
|
+
*/
|
|
100
|
+
setAudio: (options: SetAudioOptions) => ReturnType;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* This extension adds support for native audio elements.
|
|
106
|
+
*/
|
|
107
|
+
declare const Audio: Node<AudioOptions, any>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Regular expression to match URLs pointing to common audio file formats.
|
|
111
|
+
* Matches http/https URLs ending in: mp3, wav, ogg, oga, flac, m4a, aac, opus, weba, webm.
|
|
112
|
+
* Supports optional query strings. Case-insensitive, single match.
|
|
113
|
+
*/
|
|
114
|
+
declare const AUDIO_URL_REGEX: RegExp;
|
|
115
|
+
/**
|
|
116
|
+
* Global variant of AUDIO_URL_REGEX for matching multiple audio URLs in a string.
|
|
117
|
+
* Use this when you need to find all audio URLs in a block of text.
|
|
118
|
+
*/
|
|
119
|
+
declare const AUDIO_URL_REGEX_GLOBAL: RegExp;
|
|
120
|
+
/**
|
|
121
|
+
* Validates whether a URL is safe for use in an audio element.
|
|
122
|
+
* Allows http, https, protocol-relative, and relative paths by default.
|
|
123
|
+
* Data URLs can be enabled via the allowBase64 flag.
|
|
124
|
+
*
|
|
125
|
+
* @param url - The URL string to validate
|
|
126
|
+
* @param allowBase64 - Whether to allow base64 data URLs (default: false)
|
|
127
|
+
* @returns True if the URL is valid and safe, false otherwise
|
|
128
|
+
*/
|
|
129
|
+
declare const isValidAudioUrl: (url: string, allowBase64?: boolean) => boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Sanitizes an audio source URL by validating it against allowed protocols.
|
|
132
|
+
* Returns null for invalid or potentially unsafe URLs.
|
|
133
|
+
*
|
|
134
|
+
* @param url - The URL string to sanitize (can be null or undefined)
|
|
135
|
+
* @param allowBase64 - Whether to allow base64 data URLs (default: false)
|
|
136
|
+
* @returns The original URL if valid, or null if invalid/unsafe
|
|
137
|
+
*/
|
|
138
|
+
declare const sanitizeAudioSrc: (url: string | null | undefined, allowBase64?: boolean) => string | null;
|
|
139
|
+
|
|
140
|
+
export { AUDIO_URL_REGEX, AUDIO_URL_REGEX_GLOBAL, Audio, type AudioOptions, Audio as default, isValidAudioUrl, sanitizeAudioSrc };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Node } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
interface AudioOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Controls if the paste handler for audio sources should be added.
|
|
6
|
+
* @default true
|
|
7
|
+
* @example false
|
|
8
|
+
*/
|
|
9
|
+
addPasteHandler: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Controls if the audio should autoplay.
|
|
12
|
+
* @default false
|
|
13
|
+
* @example true
|
|
14
|
+
*/
|
|
15
|
+
autoplay: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Controls if the audio element should render the native controls.
|
|
18
|
+
* @default true
|
|
19
|
+
* @example false
|
|
20
|
+
*/
|
|
21
|
+
controls: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Controls if the audio should loop.
|
|
24
|
+
* @default false
|
|
25
|
+
* @example true
|
|
26
|
+
*/
|
|
27
|
+
loop: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Controls if the audio should be muted.
|
|
30
|
+
* @default false
|
|
31
|
+
* @example true
|
|
32
|
+
*/
|
|
33
|
+
muted: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The preload behavior for the audio element.
|
|
36
|
+
* @default 'metadata'
|
|
37
|
+
* @example 'none'
|
|
38
|
+
*/
|
|
39
|
+
preload: 'auto' | 'metadata' | 'none' | null;
|
|
40
|
+
/**
|
|
41
|
+
* Provides the controlslist attribute for the audio element.
|
|
42
|
+
* @default undefined
|
|
43
|
+
* @example 'nodownload'
|
|
44
|
+
*/
|
|
45
|
+
controlslist?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Sets the crossorigin attribute for the audio element.
|
|
48
|
+
* @default undefined
|
|
49
|
+
* @example 'anonymous'
|
|
50
|
+
*/
|
|
51
|
+
crossorigin?: '' | 'anonymous' | 'use-credentials';
|
|
52
|
+
/**
|
|
53
|
+
* Controls if remote playback should be disabled on the audio element.
|
|
54
|
+
* @default false
|
|
55
|
+
* @example true
|
|
56
|
+
*/
|
|
57
|
+
disableRemotePlayback: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Allows data: URLs for audio sources.
|
|
60
|
+
* @default false
|
|
61
|
+
* @example true
|
|
62
|
+
*/
|
|
63
|
+
allowBase64: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The HTML attributes for an audio node.
|
|
66
|
+
* @default {}
|
|
67
|
+
* @example { class: 'foo' }
|
|
68
|
+
*/
|
|
69
|
+
HTMLAttributes: Record<string, any>;
|
|
70
|
+
/**
|
|
71
|
+
* Controls if the audio node should be inline or not.
|
|
72
|
+
* @default false
|
|
73
|
+
* @example true
|
|
74
|
+
*/
|
|
75
|
+
inline: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The options for setting an audio element.
|
|
79
|
+
*/
|
|
80
|
+
type SetAudioOptions = {
|
|
81
|
+
src: string;
|
|
82
|
+
controls?: boolean;
|
|
83
|
+
autoplay?: boolean;
|
|
84
|
+
loop?: boolean;
|
|
85
|
+
muted?: boolean;
|
|
86
|
+
preload?: 'auto' | 'metadata' | 'none' | null;
|
|
87
|
+
controlslist?: string;
|
|
88
|
+
crossorigin?: '' | 'anonymous' | 'use-credentials';
|
|
89
|
+
disableremoteplayback?: boolean;
|
|
90
|
+
HTMLAttributes?: Record<string, any>;
|
|
91
|
+
};
|
|
92
|
+
declare module '@tiptap/core' {
|
|
93
|
+
interface Commands<ReturnType> {
|
|
94
|
+
audio: {
|
|
95
|
+
/**
|
|
96
|
+
* Insert an audio element
|
|
97
|
+
* @param options The audio attributes
|
|
98
|
+
* @example editor.commands.setAudio({ src: 'https://assets.tiptap.dev/sounds/loop.mp3' })
|
|
99
|
+
*/
|
|
100
|
+
setAudio: (options: SetAudioOptions) => ReturnType;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* This extension adds support for native audio elements.
|
|
106
|
+
*/
|
|
107
|
+
declare const Audio: Node<AudioOptions, any>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Regular expression to match URLs pointing to common audio file formats.
|
|
111
|
+
* Matches http/https URLs ending in: mp3, wav, ogg, oga, flac, m4a, aac, opus, weba, webm.
|
|
112
|
+
* Supports optional query strings. Case-insensitive, single match.
|
|
113
|
+
*/
|
|
114
|
+
declare const AUDIO_URL_REGEX: RegExp;
|
|
115
|
+
/**
|
|
116
|
+
* Global variant of AUDIO_URL_REGEX for matching multiple audio URLs in a string.
|
|
117
|
+
* Use this when you need to find all audio URLs in a block of text.
|
|
118
|
+
*/
|
|
119
|
+
declare const AUDIO_URL_REGEX_GLOBAL: RegExp;
|
|
120
|
+
/**
|
|
121
|
+
* Validates whether a URL is safe for use in an audio element.
|
|
122
|
+
* Allows http, https, protocol-relative, and relative paths by default.
|
|
123
|
+
* Data URLs can be enabled via the allowBase64 flag.
|
|
124
|
+
*
|
|
125
|
+
* @param url - The URL string to validate
|
|
126
|
+
* @param allowBase64 - Whether to allow base64 data URLs (default: false)
|
|
127
|
+
* @returns True if the URL is valid and safe, false otherwise
|
|
128
|
+
*/
|
|
129
|
+
declare const isValidAudioUrl: (url: string, allowBase64?: boolean) => boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Sanitizes an audio source URL by validating it against allowed protocols.
|
|
132
|
+
* Returns null for invalid or potentially unsafe URLs.
|
|
133
|
+
*
|
|
134
|
+
* @param url - The URL string to sanitize (can be null or undefined)
|
|
135
|
+
* @param allowBase64 - Whether to allow base64 data URLs (default: false)
|
|
136
|
+
* @returns The original URL if valid, or null if invalid/unsafe
|
|
137
|
+
*/
|
|
138
|
+
declare const sanitizeAudioSrc: (url: string | null | undefined, allowBase64?: boolean) => string | null;
|
|
139
|
+
|
|
140
|
+
export { AUDIO_URL_REGEX, AUDIO_URL_REGEX_GLOBAL, Audio, type AudioOptions, Audio as default, isValidAudioUrl, sanitizeAudioSrc };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/audio.ts
|
|
2
|
+
import { createAtomBlockMarkdownSpec, mergeAttributes, Node, nodePasteRule } from "@tiptap/core";
|
|
3
|
+
|
|
4
|
+
// src/utils.ts
|
|
5
|
+
var AUDIO_URL_REGEX = /https?:\/\/[^\s]+?\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\?[^\s]*)?/i;
|
|
6
|
+
var AUDIO_URL_REGEX_GLOBAL = /https?:\/\/[^\s]+?\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\?[^\s]*)?/gi;
|
|
7
|
+
var AUDIO_DATA_URL_REGEX = /^data:audio\/[a-zA-Z0-9.+-]+;base64,/i;
|
|
8
|
+
var isValidAudioUrl = (url, allowBase64 = false) => {
|
|
9
|
+
if (!url) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (allowBase64 && AUDIO_DATA_URL_REGEX.test(url)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (url.startsWith("//")) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const parsedUrl = new URL(url);
|
|
23
|
+
return parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var sanitizeAudioSrc = (url, allowBase64 = false) => {
|
|
29
|
+
if (!url) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return isValidAudioUrl(url, allowBase64) ? url : null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/audio.ts
|
|
36
|
+
var Audio = Node.create({
|
|
37
|
+
name: "audio",
|
|
38
|
+
addOptions() {
|
|
39
|
+
return {
|
|
40
|
+
addPasteHandler: true,
|
|
41
|
+
allowBase64: false,
|
|
42
|
+
autoplay: false,
|
|
43
|
+
controls: true,
|
|
44
|
+
loop: false,
|
|
45
|
+
muted: false,
|
|
46
|
+
preload: "metadata",
|
|
47
|
+
controlslist: void 0,
|
|
48
|
+
crossorigin: void 0,
|
|
49
|
+
disableRemotePlayback: false,
|
|
50
|
+
HTMLAttributes: {},
|
|
51
|
+
inline: false
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
inline() {
|
|
55
|
+
return this.options.inline;
|
|
56
|
+
},
|
|
57
|
+
group() {
|
|
58
|
+
return this.options.inline ? "inline" : "block";
|
|
59
|
+
},
|
|
60
|
+
atom: true,
|
|
61
|
+
draggable: true,
|
|
62
|
+
addAttributes() {
|
|
63
|
+
return {
|
|
64
|
+
src: {
|
|
65
|
+
default: null
|
|
66
|
+
},
|
|
67
|
+
controls: {
|
|
68
|
+
default: this.options.controls
|
|
69
|
+
},
|
|
70
|
+
autoplay: {
|
|
71
|
+
default: this.options.autoplay
|
|
72
|
+
},
|
|
73
|
+
loop: {
|
|
74
|
+
default: this.options.loop
|
|
75
|
+
},
|
|
76
|
+
muted: {
|
|
77
|
+
default: this.options.muted
|
|
78
|
+
},
|
|
79
|
+
preload: {
|
|
80
|
+
default: this.options.preload
|
|
81
|
+
},
|
|
82
|
+
controlslist: {
|
|
83
|
+
default: this.options.controlslist
|
|
84
|
+
},
|
|
85
|
+
crossorigin: {
|
|
86
|
+
default: this.options.crossorigin
|
|
87
|
+
},
|
|
88
|
+
disableremoteplayback: {
|
|
89
|
+
default: this.options.disableRemotePlayback
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
parseHTML() {
|
|
94
|
+
return [
|
|
95
|
+
{
|
|
96
|
+
tag: this.options.allowBase64 ? "audio[src]" : 'audio[src]:not([src^="data:"])'
|
|
97
|
+
}
|
|
98
|
+
];
|
|
99
|
+
},
|
|
100
|
+
addCommands() {
|
|
101
|
+
return {
|
|
102
|
+
setAudio: (options) => ({ commands }) => {
|
|
103
|
+
if (!isValidAudioUrl(options.src, this.options.allowBase64)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return commands.insertContent({
|
|
107
|
+
type: this.name,
|
|
108
|
+
attrs: options
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
addPasteRules() {
|
|
114
|
+
if (!this.options.addPasteHandler) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
return [
|
|
118
|
+
nodePasteRule({
|
|
119
|
+
find: AUDIO_URL_REGEX_GLOBAL,
|
|
120
|
+
type: this.type,
|
|
121
|
+
getAttributes: (match) => {
|
|
122
|
+
return { src: match[0] };
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
];
|
|
126
|
+
},
|
|
127
|
+
renderHTML({ HTMLAttributes }) {
|
|
128
|
+
const sanitizedSrc = sanitizeAudioSrc(HTMLAttributes.src, this.options.allowBase64);
|
|
129
|
+
const mergedAttributes = mergeAttributes(
|
|
130
|
+
this.options.HTMLAttributes,
|
|
131
|
+
{
|
|
132
|
+
controls: this.options.controls,
|
|
133
|
+
autoplay: this.options.autoplay,
|
|
134
|
+
loop: this.options.loop,
|
|
135
|
+
muted: this.options.muted,
|
|
136
|
+
preload: this.options.preload,
|
|
137
|
+
controlslist: this.options.controlslist,
|
|
138
|
+
crossorigin: this.options.crossorigin,
|
|
139
|
+
disableremoteplayback: this.options.disableRemotePlayback
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
...HTMLAttributes,
|
|
143
|
+
src: sanitizedSrc
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
const cleanedAttributes = Object.fromEntries(
|
|
147
|
+
Object.entries(mergedAttributes).filter(([, value]) => value !== null && value !== void 0 && value !== false)
|
|
148
|
+
);
|
|
149
|
+
return ["audio", cleanedAttributes];
|
|
150
|
+
},
|
|
151
|
+
...createAtomBlockMarkdownSpec({
|
|
152
|
+
nodeName: "audio",
|
|
153
|
+
allowedAttributes: [
|
|
154
|
+
"src",
|
|
155
|
+
"controls",
|
|
156
|
+
"autoplay",
|
|
157
|
+
"loop",
|
|
158
|
+
"muted",
|
|
159
|
+
"preload",
|
|
160
|
+
"controlslist",
|
|
161
|
+
"crossorigin",
|
|
162
|
+
"disableremoteplayback"
|
|
163
|
+
]
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// src/index.ts
|
|
168
|
+
var index_default = Audio;
|
|
169
|
+
export {
|
|
170
|
+
AUDIO_URL_REGEX,
|
|
171
|
+
AUDIO_URL_REGEX_GLOBAL,
|
|
172
|
+
Audio,
|
|
173
|
+
index_default as default,
|
|
174
|
+
isValidAudioUrl,
|
|
175
|
+
sanitizeAudioSrc
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tiptap/extension-audio",
|
|
3
|
+
"description": "audio extension for tiptap",
|
|
4
|
+
"version": "3.16.0",
|
|
5
|
+
"homepage": "https://tiptap.dev",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"tiptap",
|
|
8
|
+
"tiptap extension"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"funding": {
|
|
12
|
+
"type": "github",
|
|
13
|
+
"url": "https://github.com/sponsors/ueberdosis"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": {
|
|
19
|
+
"import": "./dist/index.d.ts",
|
|
20
|
+
"require": "./dist/index.d.cts"
|
|
21
|
+
},
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"main": "dist/index.cjs",
|
|
27
|
+
"module": "dist/index.js",
|
|
28
|
+
"types": "dist/index.d.ts",
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@tiptap/core": "^3.16.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@tiptap/core": "^3.16.0"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/ueberdosis/tiptap",
|
|
42
|
+
"directory": "packages/extension-audio"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/audio.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { createAtomBlockMarkdownSpec, mergeAttributes, Node, nodePasteRule } from '@tiptap/core'
|
|
2
|
+
|
|
3
|
+
import { AUDIO_URL_REGEX_GLOBAL, isValidAudioUrl, sanitizeAudioSrc } from './utils.js'
|
|
4
|
+
|
|
5
|
+
export interface AudioOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Controls if the paste handler for audio sources should be added.
|
|
8
|
+
* @default true
|
|
9
|
+
* @example false
|
|
10
|
+
*/
|
|
11
|
+
addPasteHandler: boolean
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Controls if the audio should autoplay.
|
|
15
|
+
* @default false
|
|
16
|
+
* @example true
|
|
17
|
+
*/
|
|
18
|
+
autoplay: boolean
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Controls if the audio element should render the native controls.
|
|
22
|
+
* @default true
|
|
23
|
+
* @example false
|
|
24
|
+
*/
|
|
25
|
+
controls: boolean
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Controls if the audio should loop.
|
|
29
|
+
* @default false
|
|
30
|
+
* @example true
|
|
31
|
+
*/
|
|
32
|
+
loop: boolean
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Controls if the audio should be muted.
|
|
36
|
+
* @default false
|
|
37
|
+
* @example true
|
|
38
|
+
*/
|
|
39
|
+
muted: boolean
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The preload behavior for the audio element.
|
|
43
|
+
* @default 'metadata'
|
|
44
|
+
* @example 'none'
|
|
45
|
+
*/
|
|
46
|
+
preload: 'auto' | 'metadata' | 'none' | null
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Provides the controlslist attribute for the audio element.
|
|
50
|
+
* @default undefined
|
|
51
|
+
* @example 'nodownload'
|
|
52
|
+
*/
|
|
53
|
+
controlslist?: string
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sets the crossorigin attribute for the audio element.
|
|
57
|
+
* @default undefined
|
|
58
|
+
* @example 'anonymous'
|
|
59
|
+
*/
|
|
60
|
+
crossorigin?: '' | 'anonymous' | 'use-credentials'
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Controls if remote playback should be disabled on the audio element.
|
|
64
|
+
* @default false
|
|
65
|
+
* @example true
|
|
66
|
+
*/
|
|
67
|
+
disableRemotePlayback: boolean
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Allows data: URLs for audio sources.
|
|
71
|
+
* @default false
|
|
72
|
+
* @example true
|
|
73
|
+
*/
|
|
74
|
+
allowBase64: boolean
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The HTML attributes for an audio node.
|
|
78
|
+
* @default {}
|
|
79
|
+
* @example { class: 'foo' }
|
|
80
|
+
*/
|
|
81
|
+
HTMLAttributes: Record<string, any>
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Controls if the audio node should be inline or not.
|
|
85
|
+
* @default false
|
|
86
|
+
* @example true
|
|
87
|
+
*/
|
|
88
|
+
inline: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The options for setting an audio element.
|
|
93
|
+
*/
|
|
94
|
+
type SetAudioOptions = {
|
|
95
|
+
src: string
|
|
96
|
+
controls?: boolean
|
|
97
|
+
autoplay?: boolean
|
|
98
|
+
loop?: boolean
|
|
99
|
+
muted?: boolean
|
|
100
|
+
preload?: 'auto' | 'metadata' | 'none' | null
|
|
101
|
+
controlslist?: string
|
|
102
|
+
crossorigin?: '' | 'anonymous' | 'use-credentials'
|
|
103
|
+
disableremoteplayback?: boolean
|
|
104
|
+
HTMLAttributes?: Record<string, any>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
declare module '@tiptap/core' {
|
|
108
|
+
interface Commands<ReturnType> {
|
|
109
|
+
audio: {
|
|
110
|
+
/**
|
|
111
|
+
* Insert an audio element
|
|
112
|
+
* @param options The audio attributes
|
|
113
|
+
* @example editor.commands.setAudio({ src: 'https://assets.tiptap.dev/sounds/loop.mp3' })
|
|
114
|
+
*/
|
|
115
|
+
setAudio: (options: SetAudioOptions) => ReturnType
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* This extension adds support for native audio elements.
|
|
122
|
+
*/
|
|
123
|
+
export const Audio = Node.create<AudioOptions>({
|
|
124
|
+
name: 'audio',
|
|
125
|
+
|
|
126
|
+
addOptions() {
|
|
127
|
+
return {
|
|
128
|
+
addPasteHandler: true,
|
|
129
|
+
allowBase64: false,
|
|
130
|
+
autoplay: false,
|
|
131
|
+
controls: true,
|
|
132
|
+
loop: false,
|
|
133
|
+
muted: false,
|
|
134
|
+
preload: 'metadata',
|
|
135
|
+
controlslist: undefined,
|
|
136
|
+
crossorigin: undefined,
|
|
137
|
+
disableRemotePlayback: false,
|
|
138
|
+
HTMLAttributes: {},
|
|
139
|
+
inline: false,
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
inline() {
|
|
144
|
+
return this.options.inline
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
group() {
|
|
148
|
+
return this.options.inline ? 'inline' : 'block'
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
atom: true,
|
|
152
|
+
|
|
153
|
+
draggable: true,
|
|
154
|
+
|
|
155
|
+
addAttributes() {
|
|
156
|
+
return {
|
|
157
|
+
src: {
|
|
158
|
+
default: null,
|
|
159
|
+
},
|
|
160
|
+
controls: {
|
|
161
|
+
default: this.options.controls,
|
|
162
|
+
},
|
|
163
|
+
autoplay: {
|
|
164
|
+
default: this.options.autoplay,
|
|
165
|
+
},
|
|
166
|
+
loop: {
|
|
167
|
+
default: this.options.loop,
|
|
168
|
+
},
|
|
169
|
+
muted: {
|
|
170
|
+
default: this.options.muted,
|
|
171
|
+
},
|
|
172
|
+
preload: {
|
|
173
|
+
default: this.options.preload,
|
|
174
|
+
},
|
|
175
|
+
controlslist: {
|
|
176
|
+
default: this.options.controlslist,
|
|
177
|
+
},
|
|
178
|
+
crossorigin: {
|
|
179
|
+
default: this.options.crossorigin,
|
|
180
|
+
},
|
|
181
|
+
disableremoteplayback: {
|
|
182
|
+
default: this.options.disableRemotePlayback,
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
parseHTML() {
|
|
188
|
+
return [
|
|
189
|
+
{
|
|
190
|
+
tag: this.options.allowBase64 ? 'audio[src]' : 'audio[src]:not([src^="data:"])',
|
|
191
|
+
},
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
addCommands() {
|
|
196
|
+
return {
|
|
197
|
+
setAudio:
|
|
198
|
+
options =>
|
|
199
|
+
({ commands }) => {
|
|
200
|
+
if (!isValidAudioUrl(options.src, this.options.allowBase64)) {
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return commands.insertContent({
|
|
205
|
+
type: this.name,
|
|
206
|
+
attrs: options,
|
|
207
|
+
})
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
addPasteRules() {
|
|
213
|
+
if (!this.options.addPasteHandler) {
|
|
214
|
+
return []
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return [
|
|
218
|
+
nodePasteRule({
|
|
219
|
+
find: AUDIO_URL_REGEX_GLOBAL,
|
|
220
|
+
type: this.type,
|
|
221
|
+
getAttributes: match => {
|
|
222
|
+
return { src: match[0] }
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
renderHTML({ HTMLAttributes }) {
|
|
229
|
+
const sanitizedSrc = sanitizeAudioSrc(HTMLAttributes.src, this.options.allowBase64)
|
|
230
|
+
|
|
231
|
+
const mergedAttributes = mergeAttributes(
|
|
232
|
+
this.options.HTMLAttributes,
|
|
233
|
+
{
|
|
234
|
+
controls: this.options.controls,
|
|
235
|
+
autoplay: this.options.autoplay,
|
|
236
|
+
loop: this.options.loop,
|
|
237
|
+
muted: this.options.muted,
|
|
238
|
+
preload: this.options.preload,
|
|
239
|
+
controlslist: this.options.controlslist,
|
|
240
|
+
crossorigin: this.options.crossorigin,
|
|
241
|
+
disableremoteplayback: this.options.disableRemotePlayback,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
...HTMLAttributes,
|
|
245
|
+
src: sanitizedSrc,
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
const cleanedAttributes = Object.fromEntries(
|
|
250
|
+
Object.entries(mergedAttributes).filter(([, value]) => value !== null && value !== undefined && value !== false),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return ['audio', cleanedAttributes]
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
...createAtomBlockMarkdownSpec({
|
|
257
|
+
nodeName: 'audio',
|
|
258
|
+
allowedAttributes: [
|
|
259
|
+
'src',
|
|
260
|
+
'controls',
|
|
261
|
+
'autoplay',
|
|
262
|
+
'loop',
|
|
263
|
+
'muted',
|
|
264
|
+
'preload',
|
|
265
|
+
'controlslist',
|
|
266
|
+
'crossorigin',
|
|
267
|
+
'disableremoteplayback',
|
|
268
|
+
],
|
|
269
|
+
}),
|
|
270
|
+
})
|
package/src/index.ts
ADDED
package/src/utils.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regular expression to match URLs pointing to common audio file formats.
|
|
3
|
+
* Matches http/https URLs ending in: mp3, wav, ogg, oga, flac, m4a, aac, opus, weba, webm.
|
|
4
|
+
* Supports optional query strings. Case-insensitive, single match.
|
|
5
|
+
*/
|
|
6
|
+
export const AUDIO_URL_REGEX = /https?:\/\/[^\s]+?\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\?[^\s]*)?/i
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Global variant of AUDIO_URL_REGEX for matching multiple audio URLs in a string.
|
|
10
|
+
* Use this when you need to find all audio URLs in a block of text.
|
|
11
|
+
*/
|
|
12
|
+
export const AUDIO_URL_REGEX_GLOBAL =
|
|
13
|
+
/https?:\/\/[^\s]+?\.(?:mp3|wav|ogg|oga|flac|m4a|aac|opus|weba|webm)(?:\?[^\s]*)?/gi
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Regular expression to match base64-encoded audio data URLs.
|
|
17
|
+
*/
|
|
18
|
+
const AUDIO_DATA_URL_REGEX = /^data:audio\/[a-zA-Z0-9.+-]+;base64,/i
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates whether a URL is safe for use in an audio element.
|
|
22
|
+
* Allows http, https, protocol-relative, and relative paths by default.
|
|
23
|
+
* Data URLs can be enabled via the allowBase64 flag.
|
|
24
|
+
*
|
|
25
|
+
* @param url - The URL string to validate
|
|
26
|
+
* @param allowBase64 - Whether to allow base64 data URLs (default: false)
|
|
27
|
+
* @returns True if the URL is valid and safe, false otherwise
|
|
28
|
+
*/
|
|
29
|
+
export const isValidAudioUrl = (url: string, allowBase64 = false) => {
|
|
30
|
+
if (!url) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (allowBase64 && AUDIO_DATA_URL_REGEX.test(url)) {
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (url.startsWith('//')) {
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const parsedUrl = new URL(url)
|
|
48
|
+
|
|
49
|
+
return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'
|
|
50
|
+
} catch {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sanitizes an audio source URL by validating it against allowed protocols.
|
|
57
|
+
* Returns null for invalid or potentially unsafe URLs.
|
|
58
|
+
*
|
|
59
|
+
* @param url - The URL string to sanitize (can be null or undefined)
|
|
60
|
+
* @param allowBase64 - Whether to allow base64 data URLs (default: false)
|
|
61
|
+
* @returns The original URL if valid, or null if invalid/unsafe
|
|
62
|
+
*/
|
|
63
|
+
export const sanitizeAudioSrc = (url: string | null | undefined, allowBase64 = false) => {
|
|
64
|
+
if (!url) {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return isValidAudioUrl(url, allowBase64) ? url : null
|
|
69
|
+
}
|