Repository: nadar/quill-delta-parser (php)

Issue:
Nested lists are not rendered as nested
**Describe the bug**
Nested lists are not rendered as nested.

**The delta code which generates the Problem**

```
{"ops":[{"insert":"Level 1A"},{"attributes":{"list":"bullet"},"insert":"\n"},{"insert":"Level 2A"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"},{"insert":"Level 2B"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"},{"insert":"Level 1B"},{"attributes":{"list":"bullet"},"insert":"\n"}]}
```

**The expected html output the delta should produce**

```
<ul><li>Level 1A<ul><li>Level 2A</li><li>Level 2B</li></ul></li><li>Level 1B</li></ul>
```

**Expected behavior**
The expected behavior is to render nested lists

**Screenshots**
Editor: <img width="555" alt="Screen Shot 2022-10-31 at 3 18 45 PM" src="https://user-images.githubusercontent.com/4413670/199102581-cd2991e7-0085-4c46-b6ab-223ddf018d8f.png">

Rendered with nadar: <img width="282" alt="Screen Shot 2022-10-31 at 3 18 53 PM" src="https://user-images.githubusercontent.com/4413670/199102584-c3488a74-44d4-433a-bee9-5ba0fa778e92.png">


**Additional context**
DBlackborough sent me to this QuillJS render library because I needed support for nested lists and his didn't support it. I thought he was implying this library did support them.


Pull request:
Nested Lists
closes #74 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 583775c..48ba06e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,8 +4,9 @@ All notable changes to this project will be documented in this file.
 
 This project adheres to [Semantic Versioning](http://semver.org/).
 
-## 3.x
+## 3.2.1
 
++ [#74](https://github.com/nadar/quill-delta-parser/issues/74) Fixed missing support for nested lists
 + [#73](https://github.com/nadar/quill-delta-parser/pull/73) More automation and testing with rector and auto commit on PR's for csfixer and rector.
 
 ## 3.2.0 (7. August 2022)
diff --git a/src/Debug.php b/src/Debug.php
index 7f01fba..6e05911 100644
--- a/src/Debug.php
+++ b/src/Debug.php
@@ -16,6 +16,9 @@ namespace nadar\quill;
  * echo $debug->debugPrint();
  * ```
  *
+ * In order to push informations into the Debug Panel, you can use
+ * the `debugInfo($text)` method which is available on all `Line` objects.
+ *
  * @author Basil Suter <basil@nadar.io>
  * @since 1.0.0
  */
diff --git a/src/Line.php b/src/Line.php
index 3fcf672..9ecd861 100644
--- a/src/Line.php
+++ b/src/Line.php
@@ -302,7 +302,7 @@ class Line
      *
      * > This method wont return the line.
      *
-     * @param callable $condition The while condition until false is returned.
+     * @param callable $condition The while condition until false is returned. The callable receives the Line as argument.
      * @since 1.3.0
      * @return void
      */
@@ -322,9 +322,10 @@ class Line
      *
      * > This method wont return the line.
      *
-     * @param callable $condition The while condition until false is returned.
+     * @param callable $condition The while condition until false is returned. The callable receives the Line as argument.
      * @since 1.3.0
      * @return void
+     * @see see while() for better documentation
      */
     public function whilePrevious(callable $condition)
     {
@@ -393,7 +394,7 @@ class Line
      * if true is returned this line will be assigned.
      *
      * @param callable $fn A function in order to determined whether this is the next element or not, if not provided the first next element is returned.
-     * @return Line|boolean
+     * @return Line|boolean The method will return either a Line object or false, if there is no next Line.
      */
     public function next($fn = null)
     {
@@ -420,7 +421,7 @@ class Line
      * if true is returned this line will be assigned.
      *
      * @param callable $fn A function in order to determined whether this is the previous element or not, if not provided the first previous element is returned.
-     * @return Line|boolean
+     * @return Line|boolean The method will return either a Line object or false, if there is no next Line.
      */
     public function previous($fn = null)
     {
diff --git a/src/listener/Lists.php b/src/listener/Lists.php
index 785b01e..80e8dab 100644
--- a/src/listener/Lists.php
+++ b/src/listener/Lists.php
@@ -103,8 +103,30 @@ class Lists extends BlockListener
                 $isOpen = true;
             }
 
-            // write the li element.
-            $output.= '<li>' . $buffer .'</li>'.PHP_EOL;
+            // when the next line has a higher intened, add nested list
+            $nextIndent = 0;
+            $pick->line->whileNext(static function (Line $line) use (&$nextIndent) {
+                $indent = $line->getAttribute('indent', 0);
+                if ($line->getAttribute(self::ATTRIBUTE_LIST)) {
+                    $nextIndent = $indent;
+                    return false;
+                }
+            });
+
+            $output .= '<li>';
+            $output .= $buffer;
+
+            if ($nextIndent > $pick->line->getAttribute('indent', 0)) {
+                $output .= '<'.$this->getListAttribute($pick).'>'.PHP_EOL;
+            } elseif ($nextIndent < $pick->line->getAttribute('indent', 0)) {
+                $output .= '</li></'.$this->getListAttribute($pick).'></li>'.PHP_EOL;
+                $closeGap = $pick->line->getAttribute('indent', 0) - $nextIndent;
+                if ($closeGap > 1) {
+                    $output .= '</'.$this->getListAttribute($pick).'></li>'.PHP_EOL;
+                }
+            } else {
+                $output.= '</li>'.PHP_EOL;
+            }
 
             // close the opening OL/UL tag if:
             //   a. its the last element and the tag is opened.


Test patch:
diff --git a/tests/Issue74Test.php b/tests/Issue74Test.php
new file mode 100644
index 0000000..3a435e0
--- /dev/null
+++ b/tests/Issue74Test.php
@@ -0,0 +1,54 @@
+<?php
+namespace nadar\quill\tests;
+
+class Issue74Test extends DeltaTestCase
+{
+    public $json = <<<'JSON'
+{
+  "ops": [
+    {
+      "insert": "Level 1A"
+    },
+    {
+      "attributes": {
+        "list": "bullet"
+      },
+      "insert": "\n"
+    },
+    {
+      "insert": "Level 2A"
+    },
+    {
+      "attributes": {
+        "indent": 1,
+        "list": "bullet"
+      },
+      "insert": "\n"
+    },
+    {
+      "insert": "Level 2B"
+    },
+    {
+      "attributes": {
+        "indent": 1,
+        "list": "bullet"
+      },
+      "insert": "\n"
+    },
+    {
+      "insert": "Level 1B"
+    },
+    {
+      "attributes": {
+        "list": "bullet"
+      },
+      "insert": "\n"
+    }
+  ]
+}
+JSON;
+
+    public $html = <<<'EOT'
+    <ul><li>Level 1A<ul><li>Level 2A</li><li>Level 2B</li></ul></li><li>Level 1B</li></ul>
+EOT;
+}
diff --git a/tests/NestedList1Test.php b/tests/NestedList1Test.php
new file mode 100644
index 0000000..254a3dd
--- /dev/null
+++ b/tests/NestedList1Test.php
@@ -0,0 +1,22 @@
+<?php
+namespace nadar\quill\tests;
+
+class NestedList1Test extends DeltaTestCase
+{
+    public $json = <<<'JSON'
+{"ops":[{"insert":"Foo 1"},{"attributes":{"list":"bullet"},"insert":"\n"},{"insert":"Foo 2"},{"attributes":{"list":"bullet"},"insert":"\n"},{"insert":"Bar 1"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"},{"insert":"Bar 2"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"}]}
+JSON;
+
+    public $html = <<<'EOT'
+<ul>
+<li>Foo 1</li>
+<li>Foo 2
+<ul>
+<li>Bar 1</li>
+<li>Bar 2</li>
+</ul>
+</li>
+</ul>
+
+EOT;
+}
diff --git a/tests/NestedNestedList1Test.php b/tests/NestedNestedList1Test.php
new file mode 100644
index 0000000..741e070
--- /dev/null
+++ b/tests/NestedNestedList1Test.php
@@ -0,0 +1,29 @@
+<?php
+namespace nadar\quill\tests;
+
+class NestedNestedList1Test extends DeltaTestCase
+{
+    public $json = <<<'JSON'
+{"ops":[{"insert":"Foo 1"},{"attributes":{"list":"bullet"},"insert":"\n"},{"insert":"Sub 1"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"},{"insert":"Sub 2"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"},{"insert":"SubSub 1"},{"attributes":{"indent":2,"list":"bullet"},"insert":"\n"},{"insert":"SubSub 2"},{"attributes":{"indent":2,"list":"bullet"},"insert":"\n"},{"insert":"Foo 2"},{"attributes":{"list":"bullet"},"insert":"\n"},{"insert":"Bar 1"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"},{"insert":"Bar 2"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"}]}
+JSON;
+
+    public $html = <<<'EOT'
+<ul>
+<li>Foo 1
+<ul>
+<li>Sub 1</li>
+<li>Sub 2
+<ul>
+<li>SubSub 1</li>
+<li>SubSub 2</li>
+</ul>
+</li>
+</ul></li><li>Foo 2
+<ul>
+<li>Bar 1</li>
+<li>Bar 2</li>
+</ul>
+</li>
+</ul>
+EOT;
+}
diff --git a/tests/NestedNestedNestedList1Test.php b/tests/NestedNestedNestedList1Test.php
new file mode 100644
index 0000000..b557e87
--- /dev/null
+++ b/tests/NestedNestedNestedList1Test.php
@@ -0,0 +1,36 @@
+<?php
+namespace nadar\quill\tests;
+
+class NestedNestedNestedList1Test extends DeltaTestCase
+{
+    public $json = <<<'JSON'
+{"ops":[{"insert":"Foo 1"},{"attributes":{"list":"bullet"},"insert":"\n"},{"insert":"Sub 1"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"},{"attributes":{"italic":true},"insert":"Sub 2"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"},{"insert":"SubSub 1"},{"attributes":{"indent":2,"list":"bullet"},"insert":"\n"},{"attributes":{"bold":true},"insert":"X1"},{"attributes":{"indent":3,"list":"bullet"},"insert":"\n"},{"insert":"X2"},{"attributes":{"indent":3,"list":"bullet"},"insert":"\n"},{"insert":"X3"},{"attributes":{"indent":3,"list":"bullet"},"insert":"\n"},{"insert":"SubSub 2"},{"attributes":{"indent":2,"list":"bullet"},"insert":"\n"},{"insert":"Foo 2"},{"attributes":{"list":"bullet"},"insert":"\n"},{"insert":"Bar 1"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"},{"insert":"Bar 2"},{"attributes":{"indent":1,"list":"bullet"},"insert":"\n"}]}
+JSON;
+
+    public $html = <<<'EOT'
+<ul>
+<li>Foo 1
+<ul>
+<li>Sub 1</li>
+<li><em>Sub 2</em>
+<ul>
+<li>SubSub 1
+<ul>
+<li><strong>X1</strong></li>
+<li>X2</li>
+<li>X3</li>
+</ul>
+</li>
+<li>SubSub 2</li>
+</ul>
+</li>
+</ul></li><li>Foo 2
+<ul>
+<li>Bar 1</li>
+<li>Bar 2</li>
+</ul>
+</li>
+</ul>
+
+EOT;
+}