diff --git a/.gitignore b/.gitignore
index 685efea..eb123e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ public/
typo3temp/
vendor/
composer.lock
+Tests/Testfiles/*.json
diff --git a/Classes/Common/XmlDocument.php b/Classes/Common/XmlDocument.php
index a305e63..d14b0d8 100644
--- a/Classes/Common/XmlDocument.php
+++ b/Classes/Common/XmlDocument.php
@@ -2,30 +2,173 @@
namespace Slub\LisztCommon\Common;
+use SimpleXMLElement;
+
class XmlDocument
{
-
protected string $xmlString;
+ // Set up configuration vars
+ protected bool $includeLiteralString;
+ protected bool $includeXmlId;
+ protected array $splitSymbols;
+
+ // Private helper vars
+ protected array $convertedArray;
+
public function __construct(string $xmlString)
{
$this->xmlString = $xmlString;
+
+ // Deal with xml-reserved symbols
+ $this->xmlString = str_replace('&', '&', $this->xmlString);
+
+ // Default config
+ $this->includeLiteralString = false;
+ $this->includeXmlId = true;
+ $this->splitSymbols = array();
}
- public static function from (string $xmlString): XmlDocument
+ // Set up config, if needed
+
+ public function setConfig(array $config)
+ {
+ $this->includeLiteralString = $config['literalString'];
+ $this->includeXmlId = $config['xmlId'];
+ $this->splitSymbols = $config['splitSymbols'];
+ return $this;
+ }
+
+ // Functions to set single config aspects
+
+ public function setXmlId(bool $xmlId)
+ {
+ $this->includeXmlId = $xmlId;
+ return $this;
+ }
+
+ public function setLiteralString(bool $literal)
+ {
+ $this->includeLiteralString = $literal;
+ return $this;
+ }
+
+ public function setSplitSymbols(array $splitSymbols)
+ {
+ $this->splitSymbols = $splitSymbols;
+ return $this;
+ }
+
+ public static function from(string $xmlString): XmlDocument
{
return new XmlDocument($xmlString);
}
public function toArray(): array
{
- // add function here
- return [];
+ // Check if array is already converted
+ if (isset($this->convertedArray)) {
+ return $this->convertedArray;
+ }
+
+ $this->convertedArray = [];
+ $xml = simplexml_load_string($this->xmlString);
+ $this->convertedArray[$this->getXmlId($xml)] = $this->convert($xml);
+ return $this->convertedArray;
+ }
+
+ public function toJson()
+ {
+ $result = [];
+ $xmlArray = $this->toArray();
+
+ /*
+ * Convert every key value pair to valid json,
+ * then decode it, so it stays valid when the whole
+ * array is encoded
+ */
+ foreach ($xmlArray as $id => $value) {
+ $result[$id] = json_encode($value);
+ $result[$id] = json_decode($result[$id], true);
+ }
+ return trim(json_encode($result));
+ }
+
+ protected function convert(SimpleXMLElement $node): array
+ {
+ $result = [];
+
+ // Parse attributes
+ $attrs = collect($node->attributes())->filter(function ($attrValue) {
+ return !empty(trim(strval($attrValue)));
+ })->mapWithKeys(function ($attrValue, $attrName) {
+ return [$attrName => trim((string) $attrValue)];
+ })->toArray();
+
+ // Merge parsed attributes with result array
+ if (!empty($attrs)) {
+ $result = array_merge_recursive($result, ['@attributes' => $attrs]);
+ }
+
+ // Parse value
+ $nodeValue = trim((string) $node);
+ if (!empty($nodeValue)) {
+ $result['@value'] = $nodeValue;
+ }
+
+ // Include xml:id attribute
+ if ($this->includeXmlId) {
+ $xmlId = $node->attributes('xml', true)->id;
+ $trimmedXmlId = trim(strval($xmlId));
+ if (!empty($trimmedXmlId)) {
+ $result['@xml:id'] = $trimmedXmlId;
+ }
+ }
+
+ // Check if node is a mixed-content element (if literalString is set to true)
+
+ if ($this->includeLiteralString) {
+ if ($node->getName() == 'p' && $node->count() > 0 && !empty($node)) {
+ // Add literal string, to store the node order
+ $literal = str_replace(array("\n", "\r"), '', trim($node->asXML()));
+ $literal = str_replace('', '', $literal);
+ $result['@literal'] = $literal;
+ }
+ }
+
+ $toParse = collect($node->children())->filter(function ($subject) use ($node, &$result) {
+ foreach ($this->splitSymbols as $symbol) {
+ if ($subject->getName() == $symbol) {
+ $result['@link'] = $this->getXmlId($subject);
+ $this->convertedArray[$this->getXmlId($subject)] = $this->convert($subject);
+ return false;
+ }
+ }
+ return true;
+ });
+
+ $toParse->each(function ($subject) use (&$result) {
+ $result = $this->parseChild($subject, $result);
+ });
+
+ return $result;
+ }
+
+ private function parseChild(SimpleXMLElement $child, array $result)
+ {
+ $childName = $child->getName();
+ $childData = $this->convert($child);
+ // Always parse child nodes as array
+ if (!isset($result[$childName])) {
+ $result[$childName] = [];
+ }
+ $result[$childName][] = $childData;
+
+ return $result;
}
- public function toJson(): string
+ private function getXmlId(SimpleXMLElement $xml)
{
- // add function here
- return '';
+ return strval($xml->attributes('xml', true)->id);
}
}
diff --git a/README.md b/README.md
index 4dbccb8..43c9f2c 100644
--- a/README.md
+++ b/README.md
@@ -13,19 +13,38 @@ This comprises the elasticsearch connection and translation of file formats.
You can obtain a Controller with easy access to elasticsearch by inheriting from ClientEnabledController.
- use Slub\LisztCommon\Controller\ClientEnabledController;
+```php
+use Slub\LisztCommon\Controller\ClientEnabledController;
- class ActionController extends ClientEnabledController
- {
+class ActionController extends ClientEnabledController
+{
- public function ExampleAction()
- {
- $this->initializeClient();
- $params = ...
- $entity = $this->elasticClient->search($params);
- ...
+ public function ExampleAction()
+ {
+ $this->initializeClient();
+ $params = ...
+ $entity = $this->elasticClient->search($params);
+ ...
- }
+ }
- }
+}
+```
+## Translation between XML and JSON
+
+You can read in an XML document and translate it to a PHP array or JSON.
+
+```php
+use Slub\LisztCommon\Common\XmlDocument;
+...
+
+$xmlDocument = XmlDocument::from($xmlString);
+$array = $xmlDocument->toArray();
+$json = $xmlDocument->toJson();
+```
+
+# Maintainer
+
+If you have any questions or encounter any problems, please do not hesitate to contact me.
+- [Matthias Richter](https://github.com/dikastes)
diff --git a/Tests/Functional/Common/Fixtures/minimal.json b/Tests/Functional/Common/Fixtures/minimal.json
index 8d1c8b6..5d49724 100644
--- a/Tests/Functional/Common/Fixtures/minimal.json
+++ b/Tests/Functional/Common/Fixtures/minimal.json
@@ -1 +1 @@
-
+{"minimal_root":{"@xml:id":"minimal_root","subroot":[{"subsubroot":[{"@value":"entry"}]}]}}
diff --git a/Tests/Functional/Common/Fixtures/minimal.xml b/Tests/Functional/Common/Fixtures/minimal.xml
index 8d1c8b6..a4bec39 100644
--- a/Tests/Functional/Common/Fixtures/minimal.xml
+++ b/Tests/Functional/Common/Fixtures/minimal.xml
@@ -1 +1,9 @@
-
+
+ Werkinformationen im Überblick Ach! nun taucht die Klosterzelle Ach! nun taucht die Klosterzelle Ach! nun taucht die Klosterzelle En ces lieux tout me parle d'elle En ces lieux tout me parle d'elle Schrift: Liszt (schwarze und sepiafarbene Tinte, Bleistift) Papier: Hochformat, 267 x 70-75 mm, 20 Notenzeilen, gedruckt Umfang: 2 Blatt zu 1 Bogen, 4 Seiten Notentext Der ursprüngliche Notentext entspricht der 2. Fassung (vgl. GSA 60/D 63), in dem Liszt zahlreiche Streichungen, Rasuren und Korrekturen vornimmt. Auf [S. 1] streicht er mit Sepia-Tinte und Bleistift das Klaviervorspiel, schreibt über den 1. Takt den Buchstaben A und notiert im 5. Takt Korrekturen mit Bleistift. Auf [S. 3] streicht er mit Sepia-Tinte die ersten beiden Takte und schreibt den Buchstaben B darüber, weiterhin streicht er die letzten beiden Takte der Seite. Auf [S. 4] streicht er bis auf 5 Takte im 3. System alle weiteren Takte, vor die letzten gestrichenen 9 Takte schreibt er den Buchstaben C. Ein weiteres Korrekturblatt liegt der Quelle nicht bei. Im gesamten Notentext finden sich ironische Bemerkungen und kommentierende Erklärungen, die an Emilie Genast adressiert sind. Das Autograph ist am Ende signiert und darunter schreibt Liszt: "an Fräulein Emilie Genast | (die Retterin meiner "ersten" und "letzten" Lieder)" etc. Nonnenwerth (ein nichts werthes | Erinnerungs Blatt!) Schrift: Liszt (schwarze und sepiafarbene Tinte, Bleistift) Papier: Hochformat, 267 x 70-75 mm, 20 Notenzeilen, gedruckt Umfang: 2 Blatt zu 1 Bogen, 4 Seiten Notentext Schrift: Liszt (schwarze und sepiafarbene Tinte, Bleistift) Papier: Hochformat, 267 x 70-75 mm, 20 Notenzeilen, gedruckt Umfang: 2 Blatt zu 1 Bogen, 4 Seiten Notentext Das Manuskript besteht aus einem Autograph von Liszt und einer Abschrift von [Weber 125165311], die Liszt miteinander verbunden hat, und die zusammen die Komposition ergeben. Die S. 1-3 stammen von Liszts Hand, ebenso die S. 7-12. Die S. 4-6 und 13-14 sind Abschriften von [Weber 125165311]. Das Manuskript enthält die gesamte Komposition. Die Vl notiert Liszt mit roter Tinte, Vlc und Kl mit schwarzer Tinte. Gelegentlich finden sich einzelne Zeichen auch in den übrigen Stimmen, die Liszt mit roter Tinte verbessert hat. Auf S. 7 hat Liszt das untere System überklebt und neu komponiert, während er die Überklebung auf S. 12 aus [Webers 1251653111] Abschrift herausgeschnitten hat. Die Zelle in Nonnenwerth. Schrift: Liszt (schwarze und rote Tinte), Wilhelm Weber (schwarze Tinte) Umfang: 7 Blatt zu 3 Bogen und eingeklebtes Einzelblatt, 14 Seiten Notentext, paginiert, Fadenbindung Papier: Hochformat, 353 x 267 mm, 12 Notenzeilen, gedruckt (B. und H. Nr. 1 C.); Überklebung S. 7a, Querformat, 161 x 265 mm, 6 Notenzeilen, gedruckt; Überklebung S. 12a, Querformat, 126-131 x 264 mm, 5 Notenzeilen, gedruckt Nonnenwerth Quellentyp: Abschrift Schrift: Wilhelm Weber (schwarze Tinte), zS (roter und blauer Stift) Umfang: 9 Blatt zu 3 Bogen und 3 Einzelblatt, 1 Titelseite, 17 Seiten Notentext, paginiert und foliiert, Fadenbindung Papier: Hochformat, 350 x 270-273 mm, 10 Notenzeilen, gedruckt (B. und H. Nr. 18. C) Eine sehr saubere Abschrift, die keine Eintragungen Liszts enthält. Der Klavierpart ist der letzten Fassung der Elegie ("Nonnenwerth") für Kl entlehnt, stimmt jedoch nicht durchgängig mit der Kl-Fassung überein. "Nonnenwerth" | Elegie. Schrift: Wilhelm Weber (schwarze Tinte), Liszt [?] (Bleistift) Umfang, vl: 2 Blatt zu 1 Bogen, 4 Seiten Notentext, paginiert und foliiert Umfang, vlc: 2 Blatt zu 1 Bogen, 4 Seiten Notentext, paginiert und foliiert Papier: Querformat, 180 x 270 mm, 8 Notenzeilen, gedruckt (B. und H. Nr. 19. C.) Eine sehr saubere Stimme mit minimalen Korrekturen (Bleistift), vielleicht von Liszt. "Die Zelle in Nonnenwerth" v. F. Liszt. Saubere Abschrift von der Hand [Wilhelm Webers 1251653111]. Es sind keine Eintragungen Liszts vorhanden. Das Manuskript schließt mit dem Auftrag "Bitte die erste Abschrift Herrn Dr. F. Liszt | wieder zu verabreichen". Datierung: W. Weber. | d. 21./8/83. Schrift: Wilhelm Weber (schwarze Tinte) Umfang: 1 Blatt, 2 Seiten Notentext, paginiert und foliiert Papier: Hochformat, 333 x 249 mm, 12 Notenzeilen, handrastriert Die Zelle in Nonnenwerth. | ELEGIE | für | Pianoforte | von | FRANZ LISZT. | 3. Auflage. Pr. 17 1/2 Neugr. | Mk. 1,75. | Eigenthum des Verlegers. | LEIPZIG, FRIEDRICH HOFMEISTER. | 6882. | Anst. f. Musikaliendruck v. Carl Schulze, Leipzig
+ Druck: Titelblatt, Leerseite, Notentext S. [3]-11, Leerseite
Enthält wenige Eintragungen auf dem Titelblatt.
+"Die Zelle in | Nonnenwerth" | (nach einem Gedichte des Fürsten | Felix Lichnowsky) | für Pianoforte | von | Franz Liszt. | (Letzte neu veränderte Ausgabe)
+D. Zelle in Nonnenwerth | Elegie | für Pianoforte | v. Fr. Liszt
+Die Zelle in Nonnenwerth – | Elegie, für Pianoforte | von F. Liszt (nach einem Gedicht | des Fürsten Felix | Lichnowsky)
+Schrift: Liszt (schwarze und bordeaux-rote Tinte, blauer und roter Stift)
+Umfang: 6 Blatt zu 1
Papier: Hochformat, 354 x 270 mm, 12 Notenzeilen, gedruckt (B. und H. Nr. 1. C.); Überklebung S. 2a, Querformat, 59-68 x 254 mm, 2 Notenzeilen, gedruckt, oben abgeschnitten
++
Die Version stimmt zum größten Teil mit der Druckfassung der Neuen Liszt-Ausgabe (Bd. I/17, Klavierversionen eigener Werke III, S. 117-121) überein. Erneut ein schönes Beispiel, an dem die unterschiedlichen Arbeitsschritte deutlich werden (Revisionen).
+file created with MerMEId
+I am mixed content
I am mixed <\/b> content <\/p>"';
+ self::assertStringContainsString($expected, $subject->toJson());
+ }
+
+ /**
+ * @test
+ */
+ public function testMixedContentIsNotIncluded()
+ {
+ $this->subject->setLiteralString(false);
+ self::assertFalse(str_contains($this->subject->toJson(), '@literal'));
+ }
+
+ /**
+ * @test
+ */
+ public function testConvertedAttribute()
+ {
+ $subject = XmlDocument::from('
I am plain text
'); + $expected = '{"testid": { + "@value": "I am plain text", + "@xml:id": "testid" } + }'; + + self::assertJsonStringEqualsJsonString($expected, $subject->toJson()); + } + + /** + * @test + */ + public function testSplitSymbols() + { + $xmlString = ' +