Repository: DenverCoder1/readme-typing-svg (php)

Issue:
Support for text containing semicolons
Currently it isn't possible, as far as I know, to use semicolons in the lines parameter as text since it will be always be treated as a line separator.

Current workarounds include using a [Greek Question Mark `;`](https://www.fileformat.info/info/unicode/char/037e/index.htm) in place of the semicolon in your text.

## Possible ideas for solutions

**Option 1:** Allow specifying the separator character as another parameter (eg. `&sep=|`) to change which character is used for separation and allowing semicolons. On the demo site this can be hidden from the user and just use `;` if no semicolons are used in the text, and something else if there are (Possibly the separator could become `;;` if `;` appears in the text, `;;;` if `;;` appears in the text, etc. to allow for infinitely many possible separators depending on the text.)

**Option 2:** Allow a way of "escaping" semicolons (eg. `\;`) in the text which should get treated as text on the backend instead of as a separator. (Note may be a breaking change since it changes the way a line ending with a backslash is parsed.)

**Option 3:** Automatically convert semicolons in the text on the demo site to Greek Question Marks instead of making a backend solution. (This is not ideal since it doesn't provide an easy solution for those not using the demo site.)

I think **Option 1** would be the cleanest solution, I am open to other ideas if anyone has input on the subject.

Pull request description:
feat: Support for semicolons and custom separator
## Summary

<!-- Please include a summary of the change and which issue is fixed. -->

Fixes #148 

## Type of change

<!-- Please delete options that are not relevant. -->

- [ ] Bug fix (added a non-breaking change which fixes an issue)
- [x] New feature (added a non-breaking change which adds functionality)
- [ ] Updated documentation (updated the readme, templates, or other repo files)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

## How Has This Been Tested?

<!-- If you have changed or added a feature, please describe the tests you made to verify your changes. -->

- [x] Ran tests with `composer test`
- [x] Added or updated test cases to test new features


Patch:
diff --git a/README.md b/README.md
index 212cdff..3404cd4 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,7 @@ Feel free to [open a PR](https://github.com/DenverCoder1/readme-typing-svg/issue
 |  `duration`  | Duration of the printing of a single line in milliseconds (default: `5000`) | integer |         Any positive number         |
 |   `pause`    |     Duration of the pause between lines in milliseconds (default: `0`)      | integer |       Any non-negative number       |
 |   `repeat`   |  `true` to loop around to the first line after the last (default: `true`)   | boolean |          `true` or `false`          |
+| `separator`  |     Separator used between lines in the lines parameter (default: `;`)      | string  |        `;`, `;;`, `/`, etc.         |
 
 ## 📤 Deploying it on your own
 
diff --git a/src/demo/js/script.js b/src/demo/js/script.js
index d6a87cd..ef57034 100644
--- a/src/demo/js/script.js
+++ b/src/demo/js/script.js
@@ -14,6 +14,7 @@ let preview = {
     duration: "5000",
     pause: "0",
     repeat: "true",
+    separator: ";",
   },
   dummyText: [
     "The five boxing wizards jump quickly",
@@ -39,15 +40,16 @@ let preview = {
       return obj;
     }, {});
     const lineInputs = Array.from(document.querySelectorAll(".param[data-index]"));
-    // disable copy button if any line contains semicolon
-    if (lineInputs.some((el) => el.value.indexOf(";") >= 0)) {
-      return copyButtons.forEach((el) => (el.disabled = true));
+    // change separator if it's included in the lines
+    params.separator = ";";
+    while (lineInputs.some((el) => el.value.indexOf(params.separator) >= 0)) {
+      params.separator += ";";
     }
     // add lines to parameters
     params.lines = lineInputs
       .map((el) => el.value) // get values
       .filter((val) => val.length) // skip blank entries
-      .join(";"); // join lines with ';' delimiter
+      .join(params.separator); // join lines with separator
     // function to URI encode string but keep semicolons as ';' and spaces as '+'
     const encode = (str) => {
       return encodeURIComponent(str).replace(/%3B/g, ";").replace(/%20/g, "+");
@@ -95,8 +97,6 @@ let preview = {
     input.name = `line-${index}`;
     input.placeholder = "Enter text here";
     input.value = this.dummyText[(index - 1) % this.dummyText.length];
-    input.pattern = "^[^;]*$";
-    input.title = "Text cannot contain semicolons";
     input.dataset.index = index;
     // removal button
     const deleteButton = document.createElement("button");
diff --git a/src/models/RendererModel.php b/src/models/RendererModel.php
index 60746bd..3ca813a 100644
--- a/src/models/RendererModel.php
+++ b/src/models/RendererModel.php
@@ -49,6 +49,9 @@ class RendererModel
     /** @var bool $repeat Whether to loop around to the first line after the last */
     public $repeat;
 
+    /** @var string $separator Line separator */
+    public $separator;
+
     /** @var string $fontCSS CSS required for displaying the selected font */
     public $fontCSS;
 
@@ -70,6 +73,7 @@ class RendererModel
         "duration" => "5000",
         "pause" => "0",
         "repeat" => "true",
+        "separator" => ";",
     ];
 
     /**
@@ -81,6 +85,7 @@ class RendererModel
     public function __construct($template, $params)
     {
         $this->template = $template;
+        $this->separator = $params["separator"] ?? $this->DEFAULTS["separator"];
         $this->lines = $this->checkLines($params["lines"] ?? "");
         $this->font = $this->checkFont($params["font"] ?? $this->DEFAULTS["font"]);
         $this->weight = $this->checkNumberPositive($params["weight"] ?? $this->DEFAULTS["weight"], "Font weight");
@@ -109,8 +114,10 @@ class RendererModel
         if (!$lines) {
             throw new InvalidArgumentException("Lines parameter must be set.");
         }
-        $trimmed_lines = rtrim($lines, ";");
-        $exploded = explode(";", $trimmed_lines);
+        if (strlen($this->separator) === 1) {
+            $lines = rtrim($lines, $this->separator);
+        }
+        $exploded = explode($this->separator, $lines);
         // escape special characters to prevent code injection
         return array_map("htmlspecialchars", $exploded);
     }


Test patch:
diff --git a/tests/OptionsTest.php b/tests/OptionsTest.php
index 4e53612..697b8fe 100644
--- a/tests/OptionsTest.php
+++ b/tests/OptionsTest.php
@@ -316,4 +316,26 @@ final class OptionsTest extends TestCase
         $model = new RendererModel("src/templates/main.php", $params);
         $this->assertEquals(false, $model->repeat);
     }
+
+    /**
+     * Test separator set to something other than ";"
+     */
+    public function testSeparator(): void
+    {
+        $params = [
+            "lines" => "text;text2",
+            "separator" => ";;",
+        ];
+        $model = new RendererModel("src/templates/main.php", $params);
+        $this->assertEquals(";;", $model->separator);
+        $this->assertEquals(["text;text2"], $model->lines);
+
+        $params = [
+            "lines" => "text;;tex;t2",
+            "separator" => ";;",
+        ];
+        $model = new RendererModel("src/templates/main.php", $params);
+        $this->assertEquals(";;", $model->separator);
+        $this->assertEquals(["text", "tex;t2"], $model->lines);
+    }
 }
diff --git a/tests/RendererTest.php b/tests/RendererTest.php
index 160404f..7d122db 100644
--- a/tests/RendererTest.php
+++ b/tests/RendererTest.php
@@ -280,4 +280,28 @@ final class RendererTest extends TestCase
         $this->assertStringContainsString("begin='0s'", $actualSVG);
         $this->assertStringNotContainsString(";d3.end", $actualSVG);
     }
+
+    /**
+     * Test separator set to something other than ";"
+     */
+    public function testSeparator(): void
+    {
+        $params = [
+            "lines" => implode(";;", [
+                "Full-stack web and app developer",
+                "Self-taught UI/UX Designer",
+                "10+ years of coding experience",
+                "Always learning new things",
+            ]),
+            "center" => "true",
+            "vCenter" => "true",
+            "width" => "380",
+            "height" => "50",
+            "separator" => ";;",
+        ];
+        $controller = new RendererController($params);
+        $expectedSVG = file_get_contents("tests/svg/test_normal.svg");
+        $actualSVG = $controller->render();
+        $this->compareNoCommentsOrWhitespace($expectedSVG, $actualSVG);
+    }
 }