diff --git a/src/Plugins/Autoaudio/Configurator.php b/src/Plugins/Autoaudio/Configurator.php
new file mode 100644
index 000000000..61b717c92
--- /dev/null
+++ b/src/Plugins/Autoaudio/Configurator.php
@@ -0,0 +1,31 @@
+:
';
+ }
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $tag = $this->configurator->tags[$this->tagName];
+ $tag->attributes->add('filename')->filterChain->append('urldecode');
+ $tag->attributePreprocessors->add($this->attrName, '/\\/(?\'filename\'[^\\/]+)$/');
+ }
+}
diff --git a/src/Plugins/Autoaudio/Parser.js b/src/Plugins/Autoaudio/Parser.js
new file mode 100644
index 000000000..0873e9428
--- /dev/null
+++ b/src/Plugins/Autoaudio/Parser.js
@@ -0,0 +1 @@
+const tagPriority = -1;
diff --git a/src/Plugins/Autoaudio/Parser.php b/src/Plugins/Autoaudio/Parser.php
new file mode 100644
index 000000000..7b7da0504
--- /dev/null
+++ b/src/Plugins/Autoaudio/Parser.php
@@ -0,0 +1,15 @@
+configurator->Autoaudio;
+ $this->assertTrue($this->configurator->tags->exists('AUDIO'));
+
+ $tag = $this->configurator->tags->get('AUDIO');
+ $this->assertTrue($tag->attributes->exists('src'));
+
+ $attribute = $tag->attributes->get('src');
+ $this->assertTrue($attribute->filterChain->contains(new UrlFilter));
+ }
+
+ /**
+ * @testdox Automatically creates a "filename" attribute
+ */
+ public function testCreatesFilenameAttribute()
+ {
+ $this->configurator->Autoaudio;
+ $tag = $this->configurator->tags->get('AUDIO');
+ $this->assertTrue($tag->attributes->exists('filename'));
+ }
+
+ /**
+ * @testdox Does not attempt to create a tag if it already exists
+ */
+ public function testDoesNotCreateTag()
+ {
+ $tag = $this->configurator->tags->add('AUDIO');
+ $this->configurator->plugins->load('Autoaudio');
+
+ $this->assertSame($tag, $this->configurator->tags->get('AUDIO'));
+ }
+
+ /**
+ * @testdox The name of the tag used can be changed through the "tagName" constructor option
+ */
+ public function testCustomTagName()
+ {
+ $this->configurator->plugins->load('Autoaudio', ['tagName' => 'FOO']);
+ $this->assertTrue($this->configurator->tags->exists('FOO'));
+ }
+
+ /**
+ * @testdox The name of the attribute used can be changed through the "attrName" constructor option
+ */
+ public function testCustomAttrName()
+ {
+ $this->configurator->plugins->load('Autoaudio', ['attrName' => 'bar']);
+ $this->assertTrue($this->configurator->tags['AUDIO']->attributes->exists('bar'));
+ }
+
+ /**
+ * @testdox Has a quickMatch
+ */
+ public function testConfigQuickMatch()
+ {
+ $this->assertArrayHasKey(
+ 'quickMatch',
+ $this->configurator->plugins->load('Autoaudio')->asConfig()
+ );
+ }
+
+ /**
+ * @testdox The config array contains a regexp
+ */
+ public function testConfigRegexp()
+ {
+ $this->assertArrayHasKey(
+ 'regexp',
+ $this->configurator->plugins->load('Autoaudio')->asConfig()
+ );
+ }
+
+ /**
+ * @testdox The config array contains the name of the tag
+ */
+ public function testConfigTagName()
+ {
+ $config = $this->configurator->plugins->load('Autoaudio')->asConfig();
+
+ $this->assertArrayHasKey('tagName', $config);
+ $this->assertSame('AUDIO', $config['tagName']);
+ }
+
+ /**
+ * @testdox The config array contains the name of the attribute
+ */
+ public function testConfigAttributeName()
+ {
+ $config = $this->configurator->plugins->load('Autoaudio')->asConfig();
+
+ $this->assertArrayHasKey('attrName', $config);
+ $this->assertSame('src', $config['attrName']);
+ }
+
+ /**
+ * @testdox getTag() returns the tag that is associated with this plugin
+ */
+ public function testGetTag()
+ {
+ $plugin = $this->configurator->plugins->load('Autoaudio');
+
+ $this->assertSame(
+ $this->configurator->tags['AUDIO'],
+ $plugin->getTag()
+ );
+ }
+
+ /**
+ * @testdox The JS parser contains both parser files
+ */
+ public function testJSParser()
+ {
+ $js = $this->configurator->Autoaudio->getJSParser();
+
+ foreach (['AbstractStaticUrlReplacer', 'Autoaudio'] as $pluginName)
+ {
+ $filepath = __DIR__ . '/../../../src/Plugins/' . $pluginName . '/Parser.js';
+ $this->assertStringContainsString(file_get_contents($filepath), $js);
+ }
+ }
+
+ /**
+ * @testdox File extensions are configurable
+ */
+ public function testFileExtensions()
+ {
+ $this->configurator->Autoaudio->fileExtensions = ['flac', 'wma'];
+ $this->configurator->Autoaudio->finalize();
+
+ $config = $this->configurator->Autoaudio->asConfig();
+
+ $this->assertMatchesRegularExpression(
+ $config['regexp'],
+ 'https://example.org/audio.flac'
+ );
+ $this->assertDoesNotMatchRegularExpression(
+ $config['regexp'],
+ 'https://example.org/audio.mp4'
+ );
+ }
+}
diff --git a/tests/Plugins/Autoaudio/ParserTest.php b/tests/Plugins/Autoaudio/ParserTest.php
new file mode 100644
index 000000000..a453f6661
--- /dev/null
+++ b/tests/Plugins/Autoaudio/ParserTest.php
@@ -0,0 +1,98 @@
+.. ..'
+ ],
+ [
+ 'http://example.org/audio.mp3',
+ ''
+ ],
+ [
+ 'http://example.org/audio.wav',
+ ''
+ ],
+ [
+ 'http://example.org/audio.aac',
+ ''
+ ],
+ [
+ 'http://example.org/audio.flac',
+ ''
+ ],
+ [
+ 'http://example.org/audio.m4a',
+ ''
+ ],
+ [
+ 'http://example.org/audio.wave',
+ ''
+ ],
+ [
+ '.. HTTP://EXAMPLE.ORG/AUDIO.MP3 ..',
+ '.. ..'
+ ],
+ [
+ '.. http://user:pass@example.org/audio.mp3 ..',
+ '.. http://user:pass@example.org/audio.mp3 ..'
+ ],
+ [
+ '.. http://example.org/my%20song%20(1).mp3 ..',
+ '.. ..'
+ ],
+ [
+ 'http://example.org/audio.mp4',
+ 'http://example.org/audio.mp4'
+ ],
+ [
+ 'http://example.org/audio.mp3',
+ '',
+ [],
+ function ($configurator)
+ {
+ $configurator->Autolink;
+ }
+ ],
+ [
+ 'https://recitals.pianoworld.com/recital_files/Recital_65/11.%20navindra%20Navindra%20Umanee%20-%20Bluebird.mp3',
+ ''
+ ],
+ ];
+ }
+
+ public static function getRenderingTests()
+ {
+ return [
+ [
+ 'http://example.org/audio.mp3',
+ 'audio.mp3:
'
+ ],
+ [
+ 'http://example.org/my%20song.mp3',
+ 'my song.mp3:
'
+ ],
+ ];
+ }
+}