diff --git a/packages/typo3-fractor/config/typo3-13.php b/packages/typo3-fractor/config/typo3-13.php
index cdb7b27f..cc3232a4 100644
--- a/packages/typo3-fractor/config/typo3-13.php
+++ b/packages/typo3-fractor/config/typo3-13.php
@@ -4,6 +4,7 @@
use a9f\Typo3Fractor\TYPO3v13\TypoScript\MigrateIncludeTypoScriptSyntaxFractor;
use a9f\Typo3Fractor\TYPO3v13\TypoScript\RemovePageDoktypeRecyclerFromUserTsConfigFractor;
+use a9f\Typo3Fractor\TYPO3v13\TypoScript\MigratePluginContentElementAndPluginSubtypesFractor;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
@@ -14,4 +15,5 @@
$services->set(MigrateIncludeTypoScriptSyntaxFractor::class);
$services->set(RemovePageDoktypeRecyclerFromUserTsConfigFractor::class);
+ $services->set(MigratePluginContentElementAndPluginSubtypesFractor::class);
};
diff --git a/packages/typo3-fractor/docs/typo3-fractor-rules.md b/packages/typo3-fractor/docs/typo3-fractor-rules.md
index d953bf25..133b9c00 100644
--- a/packages/typo3-fractor/docs/typo3-fractor-rules.md
+++ b/packages/typo3-fractor/docs/typo3-fractor-rules.md
@@ -316,6 +316,122 @@ Migrate password and salted password to password type
+## MigratePluginContentElementAndPluginSubtypesFractorChatGPT
+
+Migrate plugin content element and plugin subtypes (list_type)
+
+- class: [`a9f\Typo3Fractor\TYPO3v13\TypoScript\MigratePluginContentElementAndPluginSubtypesFractorChatGPT`](../rules/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractorChatGPT.php)
+
+```diff
+-tt_content.list.20.examples_pi1 = USER
+-tt_content.list.20.examples_pi1 {
+- userFunc = MyVendor\Examples\Controller\ExampleController->example
++tt_content.examples_pi1 =< lib.contentElement
++tt_content.examples_pi1 {
++ 20 = USER
++ 20 {
++ userFunc = MyVendor\Examples\Controller\ExampleController->example
++ }
++ templateName = Generic
+ }
+
+-tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
++tt_content.examples_pi1.20 < plugin.tx_examples_pi1
+```
+
+
+
+```diff
+-tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
++tt_content.examples_pi1.20 < plugin.tx_examples_pi1
+```
+
+
+
+## MigratePluginContentElementAndPluginSubtypesFractorGeminiV1
+
+Migrate plugin content element and plugin subtypes (list_type)
+
+- class: [`a9f\Typo3Fractor\TYPO3v13\TypoScript\MigratePluginContentElementAndPluginSubtypesFractorGeminiV1`](../rules/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractorGeminiV1.php)
+
+```diff
+-tt_content.list.20.examples_pi1 = USER
+-tt_content.list.20.examples_pi1 {
+- userFunc = MyVendor\Examples\Controller\ExampleController->example
++tt_content.examples_pi1 =< lib.contentElement
++tt_content.examples_pi1 {
++ 20 = USER
++ 20 {
++ userFunc = MyVendor\Examples\Controller\ExampleController->example
++ }
++ templateName = Generic
+ }
+
+-tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
++tt_content.examples_pi1.20 < plugin.tx_examples_pi1
+```
+
+
+
+```diff
+-tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
++tt_content.examples_pi1.20 < plugin.tx_examples_pi1
+```
+
+
+
+## MigratePluginContentElementAndPluginSubtypesFractorGeminiV2
+
+Migrate plugin content element and plugin subtypes (list_type)
+
+- class: [`a9f\Typo3Fractor\TYPO3v13\TypoScript\MigratePluginContentElementAndPluginSubtypesFractorGeminiV2`](../rules/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractorGeminiV2.php)
+
+```diff
+-tt_content.list.20.examples_pi1 = USER
+-tt_content.list.20.examples_pi1 {
+- userFunc = MyVendor\Examples\Controller\ExampleController->example
++tt_content.examples_pi1 =< lib.contentElement
++tt_content.examples_pi1 {
++ 20 = USER
++ 20 {
++ userFunc = MyVendor\Examples\Controller\ExampleController->example
++ }
++ templateName = Generic
+ }
+
+-tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
++tt_content.examples_pi1.20 < plugin.tx_examples_pi1
+```
+
+
+
+```diff
+-tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
++tt_content.examples_pi1.20 < plugin.tx_examples_pi1
+```
+
+
+
+## MigratePluginContentElementAndPluginSubtypesFractor
+
+Migrate plugin content element and plugin subtypes (list_type)
+
+- class: [`a9f\Typo3Fractor\TYPO3v13\TypoScript\MigratePluginContentElementAndPluginSubtypesFractor`](../rules/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor.php)
+
+```diff
+-tt_content.list.20.examples_pi1 = USER
+-tt_content.list.20.examples_pi1 {
++tt_content.examples_pi1 = USER
++tt_content.examples_pi1 {
+ userFunc = MyVendor\Examples\Controller\ExampleController->example
+ }
+
+-tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
++tt_content.examples_pi1 < plugin.tx_examples_pi1
+```
+
+
+
## MigrateRenderTypeColorpickerToTypeColorFlexFormFractor
Migrate renderType colorpicker to type color
diff --git a/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture.typoscript.fixture b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture.typoscript.fixture
new file mode 100644
index 00000000..d92a46ae
--- /dev/null
+++ b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture.typoscript.fixture
@@ -0,0 +1,13 @@
+tt_content.list.20.examples_pi1 = USER
+tt_content.list.20.examples_pi1 {
+ userFunc = MyVendor\Examples\Controller\ExampleController->example
+}
+-----
+tt_content.examples_pi1 =< lib.contentElement
+tt_content.examples_pi1 {
+ 20 = USER
+ 20 {
+ userFunc = MyVendor\Examples\Controller\ExampleController->example
+ }
+ templateName = Generic
+}
diff --git a/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_copy_tt_content.typoscript.fixture b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_copy_tt_content.typoscript.fixture
new file mode 100644
index 00000000..bc21a5dd
--- /dev/null
+++ b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_copy_tt_content.typoscript.fixture
@@ -0,0 +1,3 @@
+temp.example < tt_content.list.20.examples_pi1
+-----
+temp.example < tt_content.examples_pi1.20
diff --git a/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_mixed_nested.typoscript.fixture b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_mixed_nested.typoscript.fixture
new file mode 100644
index 00000000..e767bd61
--- /dev/null
+++ b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_mixed_nested.typoscript.fixture
@@ -0,0 +1,15 @@
+tt_content {
+ list.20 {
+ examples_pi1 {
+ userFunc = MyVendor\Examples\Controller\ExampleController->example
+ }
+ }
+}
+-----
+tt_content {
+ examples_pi1 {
+ 20 {
+ userFunc = MyVendor\Examples\Controller\ExampleController->example
+ }
+ }
+}
diff --git a/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_nested.typoscript.fixture b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_nested.typoscript.fixture
new file mode 100644
index 00000000..1346f289
--- /dev/null
+++ b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_nested.typoscript.fixture
@@ -0,0 +1,17 @@
+tt_content {
+ list {
+ 20 {
+ examples_pi1 {
+ userFunc = MyVendor\Examples\Controller\ExampleController->example
+ }
+ }
+ }
+}
+-----
+tt_content {
+ examples_pi1 {
+ 20 {
+ userFunc = MyVendor\Examples\Controller\ExampleController->example
+ }
+ }
+}
diff --git a/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_plugin_top_level.typoscript.fixture b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_plugin_top_level.typoscript.fixture
new file mode 100644
index 00000000..81f350b8
--- /dev/null
+++ b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_plugin_top_level.typoscript.fixture
@@ -0,0 +1,5 @@
+tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
+-----
+tt_content.examples_pi1 =< lib.contentElement
+tt_content.examples_pi1.20 < plugin.tx_examples_pi1
+tt_content.examples_pi1.templateName = Generic
diff --git a/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_plugin_top_level_reference.typoscript.fixture b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_plugin_top_level_reference.typoscript.fixture
new file mode 100644
index 00000000..c6849beb
--- /dev/null
+++ b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_plugin_top_level_reference.typoscript.fixture
@@ -0,0 +1,5 @@
+tt_content.list.20.examples_pi1 =< plugin.tx_examples_pi1
+-----
+tt_content.examples_pi1 =< lib.contentElement
+tt_content.examples_pi1.20 =< plugin.tx_examples_pi1
+tt_content.examples_pi1.templateName = Generic
diff --git a/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_reference_tt_content.typoscript.fixture b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_reference_tt_content.typoscript.fixture
new file mode 100644
index 00000000..fbd1a32f
--- /dev/null
+++ b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/Fixtures/fixture_reference_tt_content.typoscript.fixture
@@ -0,0 +1,3 @@
+temp.example =< tt_content.list.20.examples_pi1
+-----
+temp.example =< tt_content.examples_pi1.20
diff --git a/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/MigratePluginContentElementAndPluginSubtypesFractorTest.php b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/MigratePluginContentElementAndPluginSubtypesFractorTest.php
new file mode 100644
index 00000000..038aee38
--- /dev/null
+++ b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/MigratePluginContentElementAndPluginSubtypesFractorTest.php
@@ -0,0 +1,27 @@
+doTestFile($filePath);
+ }
+
+ public static function provideData(): \Iterator
+ {
+ return self::yieldFilesFromDirectory(__DIR__ . '/Fixtures', '*.typoscript.fixture');
+ }
+
+ public function provideConfigFilePath(): ?string
+ {
+ return __DIR__ . '/config/fractor.php';
+ }
+}
diff --git a/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/config/fractor.php b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/config/fractor.php
new file mode 100644
index 00000000..39d846b1
--- /dev/null
+++ b/packages/typo3-fractor/rules-tests/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor/config/fractor.php
@@ -0,0 +1,15 @@
+withOptions([
+ XmlProcessorOption::INDENT_CHARACTER => Indent::STYLE_TAB,
+ XmlProcessorOption::INDENT_SIZE => 1,
+ ])
+ ->withRules([MigratePluginContentElementAndPluginSubtypesFractor::class]);
diff --git a/packages/typo3-fractor/rules/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor.php b/packages/typo3-fractor/rules/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor.php
new file mode 100644
index 00000000..5a8c0a69
--- /dev/null
+++ b/packages/typo3-fractor/rules/TYPO3v13/TypoScript/MigratePluginContentElementAndPluginSubtypesFractor.php
@@ -0,0 +1,166 @@
+example
+}
+
+tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
+CODE_SAMPLE
+ ,
+ <<<'CODE_SAMPLE'
+tt_content.examples_pi1 =< lib.contentElement
+tt_content.examples_pi1 {
+ 20 = USER
+ 20 {
+ userFunc = MyVendor\Examples\Controller\ExampleController->example
+ }
+ templateName = Generic
+}
+
+tt_content.examples_pi1.20 < plugin.tx_examples_pi1
+CODE_SAMPLE
+ ), new CodeSample(
+ <<<'CODE_SAMPLE'
+tt_content.list.20.examples_pi1 < plugin.tx_examples_pi1
+CODE_SAMPLE
+ ,
+ <<<'CODE_SAMPLE'
+tt_content.examples_pi1.20 < plugin.tx_examples_pi1
+CODE_SAMPLE
+ )]);
+ }
+
+ public function refactor(Statement $statement): null|Statement|int
+ {
+ if ($this->shouldSkip($statement)) {
+ return null;
+ }
+
+ if ($statement instanceof NestedAssignment) {
+ $rootLine = ['tt_content', 'list', '20', '#'];
+ /** @var NestedAssignment|null $pluginStatement */
+ $pluginStatement = $this->findPluginStatement($statement, $rootLine);
+ if ($pluginStatement === null) {
+ return null;
+ }
+
+ $pluginStatement->object->absoluteName = str_replace(
+ 'list.20.',
+ '',
+ $pluginStatement->object->absoluteName
+ );
+ $pluginStatement->object->relativeName = str_replace(
+ 'list.20.',
+ '',
+ $pluginStatement->object->relativeName
+ );
+
+ if ($pluginStatement->object->absoluteName === $pluginStatement->object->relativeName) {
+ return $pluginStatement;
+ }
+ $statement->statements[0] = $pluginStatement;
+ } elseif ($statement instanceof Assignment) {
+ $statement->object->absoluteName = str_replace('list.20.', '', $statement->object->absoluteName);
+ $statement->object->relativeName = str_replace('list.20.', '', $statement->object->relativeName);
+
+ $objectPathLeft = $this->builder->path(
+ str_replace('list.20.', '', $statement->object->absoluteName),
+ str_replace('list.20.', '', $statement->object->relativeName)
+ );
+ $objectPathRight = $this->builder->path('tt_content.lib.contentElement', 'lib.contentElement');
+
+ $reference = new Reference($objectPathLeft, $objectPathRight, $statement->sourceLine);
+
+ } elseif ($statement instanceof Copy || $statement instanceof Reference) {
+ $statement->object->absoluteName = str_replace('list.20.', '', $statement->object->absoluteName);
+ $statement->object->relativeName = str_replace('list.20.', '', $statement->object->relativeName);
+
+ $statement->target->absoluteName = str_replace('list.20.', '', $statement->target->absoluteName);
+ $statement->target->relativeName = str_replace('list.20.', '', $statement->target->relativeName);
+ }
+
+ return $statement;
+ }
+
+ private function shouldSkip(Statement $statement): bool
+ {
+ if (! $statement instanceof NestedAssignment
+ && ! $statement instanceof Assignment
+ && ! $statement instanceof Copy
+ && ! $statement instanceof Reference
+ ) {
+ return true;
+ }
+
+ if (($statement instanceof Copy || $statement instanceof Reference)) {
+ return ! str_starts_with($statement->target->relativeName, 'tt_content')
+ && ! str_starts_with($statement->object->absoluteName, 'tt_content');
+ }
+
+ if (! str_starts_with($statement->object->absoluteName, 'tt_content')) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param array $rootLine
+ */
+ private function findPluginStatement(Statement $statement, array $rootLine): ?Statement
+ {
+ if (! $statement instanceof NestedAssignment) {
+ return null;
+ }
+
+ if (! isset($rootLine[0])) {
+ return $statement;
+ }
+
+ $objectPath = $statement->object->relativeName;
+ $parts = explode('.', $objectPath);
+ foreach ($parts as $part) {
+ $firstRootLineItem = $rootLine[0];
+ if ($firstRootLineItem === $part || $firstRootLineItem === '#') {
+ array_shift($rootLine);
+ }
+ }
+
+ if ($rootLine === []) {
+ // we found it!
+ return $statement;
+ }
+
+ return $this->findPluginStatement($statement->statements[0], $rootLine);
+ }
+}