From 4a1961b315aeeb7ac9eac5f866ca7b4448316da3 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Sun, 1 May 2022 20:07:04 +0300
Subject: [PATCH 01/16] Bump up dev version

---
 CHANGELOG.textile                | 2 ++
 src/Netcarver/Textile/Parser.php | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.textile b/CHANGELOG.textile
index 6f13c2ef..fb3a03a7 100644
--- a/CHANGELOG.textile
+++ b/CHANGELOG.textile
@@ -2,6 +2,8 @@ h1. Changelog
 
 Here's a summary of changes in each release. The list doesn't include some small changes or updates to test cases.
 
+h2. Version 3.7.8 - upcoming
+
 h2. "Version 3.7.7 - 2022/05/01":https://github.com/textile/php-textile/releases/tag/v3.7.7
 
 * Fix deprecation errors that appear on PHP >= 8.1 about preg_split limit argument's NULL value.
diff --git a/src/Netcarver/Textile/Parser.php b/src/Netcarver/Textile/Parser.php
index 0d052416..e94ffb6f 100644
--- a/src/Netcarver/Textile/Parser.php
+++ b/src/Netcarver/Textile/Parser.php
@@ -370,7 +370,7 @@ class Parser
      *
      * @var string
      */
-    protected $ver = '3.7.7';
+    protected $ver = '3.7.8-dev';
 
     /**
      * Regular expression snippets.

From 8b62935763d431deb0ff1324e1c72f35ff92b008 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Sun, 1 May 2022 22:29:16 +0300
Subject: [PATCH 02/16] Add setAlignClasses option

This allows enabling alignment classes independent
of the HTML5 document type. This avoid generating
the invalid align="center" attribute when
using XHTML and trying to center align images.

The generated align-center, align-left and align-right
classes can be styled using CSS to fit one's specific
usage case.

Fixes #218
Closes #219
---
 src/Netcarver/Textile/Parser.php          | 81 +++++++++++++++++++++--
 test/Netcarver/Textile/Test/BasicTest.php | 39 +++++++++++
 2 files changed, 114 insertions(+), 6 deletions(-)

diff --git a/src/Netcarver/Textile/Parser.php b/src/Netcarver/Textile/Parser.php
index e94ffb6f..d8088e8e 100644
--- a/src/Netcarver/Textile/Parser.php
+++ b/src/Netcarver/Textile/Parser.php
@@ -365,6 +365,9 @@
 
 class Parser
 {
+    const DOCTYPE_HTML5 = 'html5';
+    const DOCTYPE_XHTML = 'xhtml';
+
     /**
      * Version number.
      *
@@ -623,6 +626,13 @@ class Parser
      */
     protected $lineWrapEnabled = true;
 
+    /**
+     * Whether aligning with class selectors is enabled.
+     *
+     * @var bool|null
+     */
+    protected $isAlignClassesEnabled = null;
+
     /**
      * Pattern for punctation.
      *
@@ -843,8 +853,8 @@ class Parser
      * @since 3.6.0
      */
     protected $doctypes = array(
-        'xhtml',
-        'html5',
+        self::DOCTYPE_XHTML,
+        self::DOCTYPE_HTML5,
     );
 
     /**
@@ -1031,7 +1041,8 @@ class Parser
      * a whole, such as the output doctype. To instruct the parser to return
      * HTML5 markup instead of XHTML, set $doctype argument to 'html5'.
      *
-     * bc. $parser = new \Netcarver\Textile\Parser('html5');
+     * bc. use Netcarver\Textile\Parser;
+     * $parser = new Parser(Parser::DOCTYPE_HTML5);
      * echo $parser->parse('HTML(HyperText Markup Language)");
      *
      * @param string $doctype The output document type, either 'xhtml' or 'html5'
@@ -1039,6 +1050,8 @@ class Parser
      * @see Parser::configure()
      * @see Parser::parse()
      * @see Parser::setDocumentType()
+     * @see Parser::DOCTYPE_HTML5
+     * @see Parser::DOCTYPE_XHTML
      * @api
      */
     public function __construct($doctype = 'xhtml')
@@ -1137,15 +1150,18 @@ protected function configure()
     /**
      * Sets the output document type.
      *
-     * bc. $parser = new \Netcarver\Textile\Parser();
+     * bc. use Netcarver\Textile\Parser;
+     * $parser = new Parser();
      * echo $parser
-     *     ->setDocumentType('html5')
+     *     ->setDocumentType(Parser::DOCTYPE_HTML5)
      *     ->parse('HTML(HyperText Markup Language)");
      *
      * @param string $doctype Either 'xhtml' or 'html5'
      * @return Parser This instance
      * @since 3.6.0
      * @see Parser::getDocumentType()
+     * @see Parser::DOCTYPE_HTML5
+     * @see Parser::DOCTYPE_XHTML
      * @api
      */
     public function setDocumentType($doctype)
@@ -1477,6 +1493,59 @@ public function isRawBlocksEnabled()
         return (bool) $this->rawBlocksEnabled;
     }
 
+    /**
+     * Sets class alignment mode independent of the document type.
+     *
+     * In HTML5 document type, img elements are generated with align-left,
+     * align-center and align-right class selectors rather than align
+     * attribute being added to the image.
+     *
+     * With this option you can enable that functionality in XHTML document type mode too.
+     *
+     * bc. $parser = new \Netcarver\Textile\Parser();
+     * $parser
+     *     ->setAlignClasses(true)
+     *     ->parse(!<image.png!);
+     *
+     * Generates:
+     *
+     * bc. <p><img alt="" class="align-left" src="image.png" /></p>
+     *
+     * @param bool $enabled TRUE to enable, FALSE to disable
+     * @return Parser This instance
+     * @since 3.8.0
+     * @api
+     */
+    public function setAlignClasses($enabled)
+    {
+        $this->isAlignClassesEnabled = (bool) $enabled;
+        return $this;
+    }
+
+    /**
+     * Whether class alignment mode is enabled.
+     *
+     * bc. $parser = new \Netcarver\Textile\Parser();
+     * if ($parser->isAlignClassesEnabled() === true) {
+     *     echo 'Images are aligned with class instead of align attribute';
+     * }
+     *
+     * @return bool TRUE if enabled, FALSE otherwise
+     * @since 3.8.0
+     * @see Parser::setAlignClasses()
+     * @api
+     */
+    public function isAlignClassesEnabled()
+    {
+        if ($this->isAlignClassesEnabled === null
+            && $this->getDocumentType() === self::DOCTYPE_HTML5
+        ) {
+            return true;
+        }
+
+        return (bool) $this->isAlignClassesEnabled;
+    }
+
     /**
      * Enables and disables block-level tags and formatting features.
      *
@@ -4623,7 +4692,7 @@ protected function fImage($m)
         );
 
         if (isset($alignments[$align])) {
-            if ($this->getDocumentType() === 'html5') {
+            if ($this->isAlignClassesEnabled()) {
                 $extras = 'align-'.$alignments[$align];
                 $align = '';
             } else {
diff --git a/test/Netcarver/Textile/Test/BasicTest.php b/test/Netcarver/Textile/Test/BasicTest.php
index 0d03b29f..1512a778 100644
--- a/test/Netcarver/Textile/Test/BasicTest.php
+++ b/test/Netcarver/Textile/Test/BasicTest.php
@@ -273,4 +273,43 @@ public function testLinkPrefix()
         $parser = new Textile();
         $this->assertEquals('test', $parser->setImagePrefix('test')->getImagePrefix());
     }
+
+    public function testAlignClasses()
+    {
+        $parser = new Textile();
+
+        $this->assertFalse(
+            $parser->isAlignClassesEnabled()
+        );
+
+        $parser->setDocumentType(Textile::DOCTYPE_HTML5);
+
+        $this->assertTrue(
+            $parser->isAlignClassesEnabled()
+        );
+
+        $parser->setAlignClasses(false);
+
+        $this->assertFalse(
+            $parser->isAlignClassesEnabled()
+        );
+
+        $parser->setDocumentType(Textile::DOCTYPE_XHTML);
+
+        $this->assertFalse(
+            $parser->isAlignClassesEnabled()
+        );
+
+        $parser->setAlignClasses(true);
+
+        $this->assertTrue(
+            $parser->isAlignClassesEnabled()
+        );
+
+        $parser->setAlignClasses(false);
+
+        $this->assertFalse(
+            $parser->isAlignClassesEnabled()
+        );
+    }
 }

From 631d839c136d0e93288aa9d1d02f9e97c94a31b0 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Sun, 1 May 2022 22:34:44 +0300
Subject: [PATCH 03/16] Update changelog

---
 CHANGELOG.textile | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.textile b/CHANGELOG.textile
index fb3a03a7..c226896d 100644
--- a/CHANGELOG.textile
+++ b/CHANGELOG.textile
@@ -2,7 +2,9 @@ h1. Changelog
 
 Here's a summary of changes in each release. The list doesn't include some small changes or updates to test cases.
 
-h2. Version 3.7.8 - upcoming
+h2. Version 3.8.0 - upcoming
+
+* Added @Parser::setAlignClasses()@ and @Parser::isAlignClassesEnabled@. This can be used to enable img alignment classes in XHTML output document mode, instead of the default align attribute.
 
 h2. "Version 3.7.7 - 2022/05/01":https://github.com/textile/php-textile/releases/tag/v3.7.7
 

From 997caf101220dcfd58efff308fd3fc30de179e14 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Sun, 1 May 2022 22:42:38 +0300
Subject: [PATCH 04/16] Document DOCTYPE constant addition

---
 CHANGELOG.textile                |  1 +
 src/Netcarver/Textile/Parser.php | 11 +++++++++++
 2 files changed, 12 insertions(+)

diff --git a/CHANGELOG.textile b/CHANGELOG.textile
index c226896d..4a5d795a 100644
--- a/CHANGELOG.textile
+++ b/CHANGELOG.textile
@@ -5,6 +5,7 @@ Here's a summary of changes in each release. The list doesn't include some small
 h2. Version 3.8.0 - upcoming
 
 * Added @Parser::setAlignClasses()@ and @Parser::isAlignClassesEnabled@. This can be used to enable img alignment classes in XHTML output document mode, instead of the default align attribute.
+* Added @Parser::DOCTYPE_HTML5@ and @Parser::DOCTYPE_XHTML@ constants. These can be used with @Parser::setDocumentType()@ to specify the output document type.
 
 h2. "Version 3.7.7 - 2022/05/01":https://github.com/textile/php-textile/releases/tag/v3.7.7
 
diff --git a/src/Netcarver/Textile/Parser.php b/src/Netcarver/Textile/Parser.php
index d8088e8e..4ae468e6 100644
--- a/src/Netcarver/Textile/Parser.php
+++ b/src/Netcarver/Textile/Parser.php
@@ -365,7 +365,18 @@
 
 class Parser
 {
+    /**
+     * HTML5 document type.
+     *
+     * @since 3.8.0
+     */
     const DOCTYPE_HTML5 = 'html5';
+
+    /**
+     * XHTML document type.
+     *
+     * @since 3.8.0
+     */
     const DOCTYPE_XHTML = 'xhtml';
 
     /**

From d03daef779e326d05264759645d0d7ef38714d14 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Tue, 3 May 2022 20:38:31 +0300
Subject: [PATCH 05/16] Add REPL using PsySH

---
 Makefile      | 8 +++++++-
 composer.json | 4 +++-
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/Makefile b/Makefile
index e41115ac..a77b2b3b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: all clean help lint lint-fix test test-static test-unit bump bump-dev process-reports
+.PHONY: all clean help lint lint-fix repl test test-static test-unit bump bump-dev process-reports
 
 IMAGE?=php_8_1
 PHP = docker-compose run --rm $(IMAGE)
@@ -26,6 +26,9 @@ test-static: vendor
 test-unit: vendor
 	$(PHP) composer test:unit
 
+repl: vendor
+	$(PHP) composer repl
+
 bump: vendor
 	$(PHP) composer bump
 
@@ -60,6 +63,9 @@ help:
 	@echo "  $$ make test-static"
 	@echo "  Run static tests"
 	@echo ""
+	@echo "  $$ make repl"
+	@echo "  Launch read-print-eval loop"
+	@echo ""
 	@echo "  $$ make bump"
 	@echo "  Bump version"
 	@echo ""
diff --git a/composer.json b/composer.json
index 0632b995..e958b85d 100644
--- a/composer.json
+++ b/composer.json
@@ -27,7 +27,8 @@
         "phpstan/phpstan": "1.6.3",
         "phpunit/phpunit": "^9.5.20",
         "squizlabs/php_codesniffer": "3.*",
-        "symfony/yaml": "^4.4.3"
+        "symfony/yaml": "^4.4.3",
+        "psy/psysh": "^0.11.2"
     },
     "extra": {
         "branch-alias": {
@@ -44,6 +45,7 @@
         "bump-dev": "@php ./scripts/release.php --dev",
         "lint": "phpcs",
         "lint-fix": "phpcbf",
+        "repl": "psysh",
         "test:static": "phpstan analyse --level 8 src",
         "test:unit": "XDEBUG_MODE=coverage phpunit"
     }

From 1c74a11f1942de8c782bf36ee97645dcdca572de Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Wed, 4 May 2022 22:04:21 +0300
Subject: [PATCH 06/16] Update multi-target image support

---
 Makefile                                     | 18 +++++++++++++++---
 docker-compose.yml                           |  4 ++--
 docker/image/{php/8_1 => php_8_1}/Dockerfile |  0
 3 files changed, 17 insertions(+), 5 deletions(-)
 rename docker/image/{php/8_1 => php_8_1}/Dockerfile (100%)

diff --git a/Makefile b/Makefile
index a77b2b3b..06aec936 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
-.PHONY: all clean help lint lint-fix repl test test-static test-unit bump bump-dev process-reports
+.PHONY: all clean docker-build docker-images help lint lint-fix repl test test-static test-unit bump bump-dev process-reports
 
 IMAGE?=php_8_1
-PHP = docker-compose run --rm $(IMAGE)
+PHP = docker-compose run --rm php
 
 all: test
 
@@ -38,6 +38,12 @@ bump-dev: vendor
 process-reports:
 	$(PHP) bash -c "test -e build/logs/clover.xml && sed -i 's/\/app\///' build/logs/clover.xml"
 
+docker-build:
+	docker-compose build php
+
+docker-images:
+	@$(PHP) bash -c "cd docker/image && ls ."
+
 help:
 	@echo "Manage project"
 	@echo ""
@@ -81,8 +87,14 @@ help:
 	@echo "  $$ make process-reports"
 	@echo "  Formats test reports to use relative local file paths"
 	@echo ""
+	@echo "  $$ make docker-images"
+	@echo "  Lists available Docker images"
+	@echo ""
+	@echo "  $$ make docker-build"
+	@echo "  Re-builds the Docker image"
+	@echo ""
 	@echo "Environment variables:"
 	@echo ""
 	@echo "  IMAGE"
-	@echo "  docker-compose service name that is used to run the command"
+	@echo "  Docker image that is used to run the command"
 	@echo ""
diff --git a/docker-compose.yml b/docker-compose.yml
index a2928fde..9797397c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,8 +1,8 @@
 version: '3'
 
 services:
-  php_8_1:
-    build: ./docker/image/php/8_1
+  php:
+    build: ./docker/image/${IMAGE:-php_8_1}
     volumes:
       - .:/app
       - ${COMPOSER_HOME:-$HOME/.composer}:/tmp
diff --git a/docker/image/php/8_1/Dockerfile b/docker/image/php_8_1/Dockerfile
similarity index 100%
rename from docker/image/php/8_1/Dockerfile
rename to docker/image/php_8_1/Dockerfile

From b2abb7b7aa2c6b5aad1760c54835151ab727b781 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Wed, 4 May 2022 22:34:52 +0300
Subject: [PATCH 07/16] Support slashes and brackets in class names

Fixes #216
---
 src/Netcarver/Textile/Parser.php | 24 ++++++++++++------------
 test/fixtures/issue-216.yaml     | 14 ++++++++++++++
 2 files changed, 26 insertions(+), 12 deletions(-)
 create mode 100644 test/fixtures/issue-216.yaml

diff --git a/src/Netcarver/Textile/Parser.php b/src/Netcarver/Textile/Parser.php
index 4ae468e6..fc59a718 100644
--- a/src/Netcarver/Textile/Parser.php
+++ b/src/Netcarver/Textile/Parser.php
@@ -2514,25 +2514,15 @@ protected function parseAttribsToArray($in, $element = '', $include_id = true, $
             $matched = str_replace($sty[0], '', $matched);
         }
 
-        if (preg_match("/\[([^]]+)\]/U", $matched, $lng)) {
-            // Consume entire lang block -- valid or invalid.
-            $matched = str_replace($lng[0], '', $matched);
-            if ($element === 'code' && preg_match("/\[([a-zA-Z0-9_-]+)\]/U", $lng[0], $lng1)) {
-                $lang = $lng1[1];
-            } elseif (preg_match("/\[([a-zA-Z]{2}(?:[\-\_][a-zA-Z]{2})?)\]/U", $lng[0], $lng2)) {
-                $lang = $lng2[1];
-            }
-        }
-
         if (preg_match("/\(([^()]+)\)/U", $matched, $cls)) {
-            $class_regex = "/^([-a-zA-Z 0-9_\.]*)$/";
+            $class_regex = "/^([-a-zA-Z 0-9_\.\/\[\]]*)$/";
 
             // Consume entire class block -- valid or invalid.
             $matched = str_replace($cls[0], '', $matched);
 
             // Only allow a restricted subset of the CSS standard characters for classes/ids.
             // No encoding markers allowed.
-            if (preg_match("/\(([-a-zA-Z 0-9_\.\:\#]+)\)/U", $cls[0], $cls)) {
+            if (preg_match("/\(([-a-zA-Z 0-9_\/\[\]\.\:\#]+)\)/U", $cls[0], $cls)) {
                 $hashpos = strpos($cls[1], '#');
                 // If a textile class block attribute was found with a '#' in it
                 // split it into the css class and css id...
@@ -2552,6 +2542,16 @@ protected function parseAttribsToArray($in, $element = '', $include_id = true, $
             }
         }
 
+        if (preg_match("/\[([^]]+)\]/U", $matched, $lng)) {
+            // Consume entire lang block -- valid or invalid.
+            $matched = str_replace($lng[0], '', $matched);
+            if ($element === 'code' && preg_match("/\[([a-zA-Z0-9_-]+)\]/U", $lng[0], $lng1)) {
+                $lang = $lng1[1];
+            } elseif (preg_match("/\[([a-zA-Z]{2}(?:[\-\_][a-zA-Z]{2})?)\]/U", $lng[0], $lng2)) {
+                $lang = $lng2[1];
+            }
+        }
+
         if (preg_match("/([(]+)/", $matched, $pl)) {
             $style[] = "padding-left:" . strlen($pl[1]) . "em";
             $matched = str_replace($pl[0], '', $matched);
diff --git a/test/fixtures/issue-216.yaml b/test/fixtures/issue-216.yaml
new file mode 100644
index 00000000..3a715349
--- /dev/null
+++ b/test/fixtures/issue-216.yaml
@@ -0,0 +1,14 @@
+Support slashes and brackets in class names:
+  input: |
+    p(m-auto w-6/12). Paragraph
+    
+    p(max-w-[140px]). Paragraph
+    
+    p(m-auto w-6/12 #unique). Paragraph
+
+  expect: |
+    <p class="m-auto w-6/12">Paragraph</p>
+    
+    <p class="max-w-[140px]">Paragraph</p>
+    
+    <p class="m-auto w-6/12" id="unique">Paragraph</p>

From b5c59225b3d9fe6cb74afa01c85e2b9f70032110 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Thu, 5 May 2022 11:25:17 +0300
Subject: [PATCH 08/16] Breakpoint support

---
 docker-compose.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/docker-compose.yml b/docker-compose.yml
index 9797397c..7375e787 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,6 +8,11 @@ services:
       - ${COMPOSER_HOME:-$HOME/.composer}:/tmp
     networks:
       - app
+    environment:
+      - XDEBUG_CONFIG
+      - XDEBUG_MODE
+      - XDEBUG_TRIGGER
+      - PHP_IDE_CONFIG
 
 networks:
   app:

From 94bc325e5cbec20a22c14b47f0e58df0ddb5dc58 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Thu, 5 May 2022 20:55:07 +0300
Subject: [PATCH 09/16] Clean composer package meta and contents

---
 .gitattributes | 6 +++++-
 composer.json  | 1 -
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/.gitattributes b/.gitattributes
index d95bec5c..3f83c868 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,10 +1,14 @@
+/docker export-ignore
 /scripts export-ignore
 /test export-ignore
 /.editorconfig export-ignore
 /.github export-ignore
 /.gitignore export-ignore
-/.travis.yml export-ignore
+/Makefile export-ignore
+/docker-compose.yml export-ignore
 /phpcs.xml export-ignore
+/phpstan.neon export-ignore
 /phpunit.xml export-ignore
+/sonar-project.properties export-ignore
 /textile-wordmark.png export-ignore
 /.gitattributes export-ignore
diff --git a/composer.json b/composer.json
index e958b85d..54041811 100644
--- a/composer.json
+++ b/composer.json
@@ -5,7 +5,6 @@
     "homepage": "https://github.com/textile/php-textile",
     "keywords": ["php-textile", "textile", "parser", "markup", "language", "html", "format", "plaintext", "document"],
     "support": {
-        "irc": "irc://irc.freenode.net/textile",
         "wiki": "https://github.com/textile/php-textile/wiki",
         "issues": "https://github.com/textile/php-textile/issues",
         "source": "https://github.com/textile/php-textile"

From e7a0fa8dec86b11a4c784df92a1f7b63a83c7c56 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Thu, 5 May 2022 21:10:21 +0300
Subject: [PATCH 10/16] Add development link to README

---
 README.textile | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/README.textile b/README.textile
index ef8db27d..b5504bfc 100644
--- a/README.textile
+++ b/README.textile
@@ -77,3 +77,7 @@ $parser
 h2. Getting in contact
 
 The PHP-Textile project welcomes constructive input and bug reports from users. Please "open an issue":https://github.com/textile/php-textile/issues on the repository for a comment, feature request or bug.
+
+h2. Development
+
+See "CONTRIBUTING.textile":https://github.com/textile/php-textile/blob/master/.github/CONTRIBUTING.textile.

From 88abb355555517187ecd5933d56c10debd05dfa5 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Thu, 5 May 2022 21:21:32 +0300
Subject: [PATCH 11/16] Bump version number, fix script composer.json handling

---
 composer.json                    | 2 +-
 scripts/release.php              | 4 ++--
 src/Netcarver/Textile/Parser.php | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/composer.json b/composer.json
index 54041811..6549133a 100644
--- a/composer.json
+++ b/composer.json
@@ -31,7 +31,7 @@
     },
     "extra": {
         "branch-alias": {
-            "dev-master": "3.7-dev"
+            "dev-master": "3.8-dev"
         }
     },
     "scripts": {
diff --git a/scripts/release.php b/scripts/release.php
index 8b9c71da..ec61315f 100644
--- a/scripts/release.php
+++ b/scripts/release.php
@@ -72,8 +72,8 @@
         },
     ],
     'composer.json' => [
-        '/("dev-master": ")([^"])(")/' => function ($m) use ($dev) {
-            return $m[1] . $dev . $m[2];
+        '/("dev-master": ")([^"]+)(")/' => function ($m) use ($dev) {
+            return $m[1] . $dev . $m[3];
         }
     ]
 ];
diff --git a/src/Netcarver/Textile/Parser.php b/src/Netcarver/Textile/Parser.php
index fc59a718..b4626b75 100644
--- a/src/Netcarver/Textile/Parser.php
+++ b/src/Netcarver/Textile/Parser.php
@@ -384,7 +384,7 @@ class Parser
      *
      * @var string
      */
-    protected $ver = '3.7.8-dev';
+    protected $ver = '3.8.0-dev';
 
     /**
      * Regular expression snippets.

From 79f9f903da25b17f203aca03f114fb83e25f18a5 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Fri, 20 May 2022 19:11:36 +0300
Subject: [PATCH 12/16] Split unit test files

---
 test/Netcarver/Textile/Test/BasicTest.php     | 315 -----------------
 .../Textile/Test/ParserFixtureTest.php        | 112 ++++++
 test/Netcarver/Textile/Test/ParserTest.php    | 324 ++++++++++++++++++
 test/Netcarver/Textile/Test/TagTest.php       |  21 ++
 4 files changed, 457 insertions(+), 315 deletions(-)
 delete mode 100644 test/Netcarver/Textile/Test/BasicTest.php
 create mode 100644 test/Netcarver/Textile/Test/ParserFixtureTest.php
 create mode 100644 test/Netcarver/Textile/Test/ParserTest.php
 create mode 100644 test/Netcarver/Textile/Test/TagTest.php

diff --git a/test/Netcarver/Textile/Test/BasicTest.php b/test/Netcarver/Textile/Test/BasicTest.php
deleted file mode 100644
index 1512a778..00000000
--- a/test/Netcarver/Textile/Test/BasicTest.php
+++ /dev/null
@@ -1,315 +0,0 @@
-<?php
-
-namespace Netcarver\Textile\Test;
-
-use PHPUnit\Framework\TestCase;
-use Symfony\Component\Yaml\Yaml;
-use Netcarver\Textile\Parser as Textile;
-use Netcarver\Textile\Tag;
-
-class BasicTest extends TestCase
-{
-    /**
-     * @dataProvider provider
-     */
-    public function testAdd($file, $name, $test)
-    {
-        if (isset($test['class'])) {
-            $class = $test['class'];
-        } else {
-            $class = '\Netcarver\Textile\Parser';
-        }
-
-        $textile = new $class;
-
-        if (isset($test['doctype'])) {
-            $textile->setDocumentType($test['doctype']);
-        }
-
-        if (isset($test['setup'])) {
-            foreach ($test['setup'] as $setup) {
-                foreach ($setup as $method => $value) {
-                    $textile = $textile->$method($value);
-                }
-            }
-        }
-
-        if (isset($test['method'])) {
-            $method = trim($test['method']);
-        } else {
-            $method = 'parse';
-        }
-
-        $args = array();
-
-        if (isset($test['arguments'])) {
-            foreach ($test['arguments'] as $argument) {
-                foreach ($argument as $value) {
-                    $args[] = $value;
-                }
-            }
-        }
-
-        foreach (array('expect', 'input') as $field) {
-            $test[$field] = strtr($test[$field], array(
-                '\x20' => ' ',
-            ));
-        }
-
-        $expect = rtrim($test['expect']);
-        array_unshift($args, $test['input']);
-        $input = rtrim(call_user_func_array(array($textile, $method), $args));
-
-        foreach (array('expect', 'input') as $variable) {
-            $$variable = preg_replace(
-                array(
-                    '/ id="(fn|note)[a-z0-9\-]*"/',
-                    '/ href="#(fn|note)[a-z0-9\-]*"/',
-                ),
-                '',
-                $$variable
-            );
-        }
-
-        $this->assertEquals($expect, $input, $name . ' in ' . $file);
-        $public = implode(', ', array_keys(get_object_vars($textile)));
-        $this->assertEquals('', $public, 'Leaking public class properties.');
-    }
-
-    public function testGetVersion()
-    {
-        $textile = new Textile();
-
-        $this->assertIsString(
-            $textile->getVersion()
-        );
-    }
-
-    public function testInvalidSymbol()
-    {
-        $this->expectException('\InvalidArgumentException');
-        $textile = new Textile();
-        $textile->getSymbol('invalidSymbolName');
-    }
-
-    public function testSetGetSymbol()
-    {
-        $textile = new Textile();
-        $this->assertEquals('TestValue', $textile->setSymbol('test', 'TestValue')->getSymbol('test'));
-        $this->assertArrayHasKey('test', $textile->getSymbol());
-    }
-
-    public function testSetRelativeImagePrefixChaining()
-    {
-        $this->expectError();
-        $textile = new Textile();
-        $symbol = $textile->setRelativeImagePrefix('abc')->setSymbol('test', 'TestValue')->getSymbol('test');
-        $this->assertEquals('TestValue', $symbol);
-    }
-
-    public function testSetGetDimensionlessImage()
-    {
-        $textile = new Textile();
-        $this->assertFalse($textile->getDimensionlessImages());
-        $this->assertTrue($textile->setDimensionlessImages(true)->getDimensionlessImages());
-    }
-
-    public function testEncode()
-    {
-        $textile = new Textile();
-        $encoded = $textile->textileEncode('& &amp; &#124; &#x0022 &#x0022;');
-        $this->assertEquals('&amp; &amp; &#124; &amp;#x0022 &#x0022;', $encoded);
-    }
-
-    public function provider()
-    {
-        chdir(dirname(dirname(dirname(__DIR__))));
-        $out = array();
-
-        if ($files = glob('*/*.yaml')) {
-            foreach ($files as $file) {
-                $yaml = Yaml::parseFile($file);
-
-                foreach ($yaml as $name => $test) {
-                    if (!is_array($test) || !isset($test['input']) || !isset($test['expect'])) {
-                        continue;
-                    }
-
-                    if (isset($test['assert']) && $test['assert'] === 'skip') {
-                        continue;
-                    }
-
-                    $out[] = array($file, $name, $test);
-                }
-            }
-        }
-
-        return $out;
-    }
-
-    public function testTagAttributesGenerator()
-    {
-        $attributes = new Tag(null, array('name' => 'value'));
-        $this->assertEquals(' name="value"', (string) $attributes);
-    }
-
-    public function testDeprecatedEncodingArgument()
-    {
-        $this->expectDeprecation();
-        $parser = new Textile();
-        $this->assertEquals('content', @$parser->textileThis('content', false, true));
-        $this->assertEquals('content', $parser->textileEncode('content'));
-        $parser->textileThis('content', false, true);
-    }
-
-    public function testDeprecatedTextileCommon()
-    {
-        $this->expectDeprecation();
-        $parser = new Parser\DeprecatedTextileCommon();
-        $this->assertEquals(' content', @$parser->testTextileCommon(' content', false));
-        $this->assertEquals(' content', @$parser->testTextileCommon(' content', true));
-        $parser->testTextileCommon('content', false);
-    }
-
-    public function testDeprecatedPrepare()
-    {
-        $this->expectDeprecation();
-        $parser = new Parser\DeprecatedPrepare();
-        $this->assertEquals(' content', @$parser->parse(' content'));
-        $parser->parse('content');
-    }
-
-    public function testDeprecatedTextileRestricted()
-    {
-        $this->expectDeprecation();
-        $parser = new Textile();
-        $this->assertEquals(' content', @$parser->textileRestricted(' content'));
-        $parser->textileRestricted('content');
-    }
-
-    public function testDeprecatedTextileThis()
-    {
-        $this->expectDeprecation();
-        $parser = new Textile();
-        $this->assertEquals(' content', @$parser->textileThis(' content'));
-        $parser->textileThis('content');
-    }
-
-    public function testDeprecatedSetRelativeImagePrefix()
-    {
-        $this->expectDeprecation();
-        $parser = new Textile();
-        @$parser->setRelativeImagePrefix('/1/');
-        $this->assertEquals(
-            ' <img alt="" src="/1/2.jpg" /> <a href="/1/2">1</a>',
-            $parser->parse(' !2.jpg! "1":2')
-        );
-        $parser->setRelativeImagePrefix('/1/');
-    }
-
-    public function testInvalidDocumentType()
-    {
-        $this->expectException('\InvalidArgumentException');
-        new Textile('InvalidDocumentType');
-    }
-
-    public function testInstanceSharingAndFootnoteIndex()
-    {
-        $parser = new Textile();
-        $previous = array('', '<p><strong>strong</strong></p>');
-
-        for ($i = 1; $i <= 100; $i++) {
-            $content = "Note[1]\n\nfn1. Footnote";
-            $parsed[0] = $parser->parse($content);
-            $parsed[1] = $parser->parse('*strong*');
-            $this->assertTrue($parsed[0] !== $previous[0]);
-            $this->assertEquals($previous[1], $parsed[1]);
-            $previous[0] = $parsed[0];
-            $previous[1] = $parsed[1];
-        }
-    }
-
-    public function testLineSpaceEscaping()
-    {
-        $parser = new Textile();
-        $this->assertEquals(' <strong>line</strong>', $parser->parse(' *line*'));
-    }
-
-    public function testDocumentRoot()
-    {
-        $parser = new Textile();
-        $parser->setDocumentRootDirectory(__DIR__);
-        $this->assertEquals(__DIR__, rtrim($parser->getDocumentRootDirectory(), '\\/'));
-    }
-
-    public function testDisallowImages()
-    {
-        $parser = new Textile();
-        $this->assertFalse($parser->setImages(false)->isImageTagEnabled());
-        $this->assertTrue($parser->setImages(true)->isImageTagEnabled());
-    }
-
-    public function testLinkRelationShip()
-    {
-        $parser = new Textile();
-        $this->assertEquals('test', $parser->setLinkRelationShip('test')->getLinkRelationShip());
-    }
-
-    public function testEnableRestrictedMode()
-    {
-        $parser = new Textile();
-        $this->assertTrue($parser->setRestricted(true)->isRestrictedModeEnabled());
-        $this->assertFalse($parser->setRestricted(false)->isRestrictedModeEnabled());
-    }
-
-    public function testImagePrefix()
-    {
-        $parser = new Textile();
-        $this->assertEquals('test', $parser->setLinkPrefix('test')->getLinkPrefix());
-    }
-
-    public function testLinkPrefix()
-    {
-        $parser = new Textile();
-        $this->assertEquals('test', $parser->setImagePrefix('test')->getImagePrefix());
-    }
-
-    public function testAlignClasses()
-    {
-        $parser = new Textile();
-
-        $this->assertFalse(
-            $parser->isAlignClassesEnabled()
-        );
-
-        $parser->setDocumentType(Textile::DOCTYPE_HTML5);
-
-        $this->assertTrue(
-            $parser->isAlignClassesEnabled()
-        );
-
-        $parser->setAlignClasses(false);
-
-        $this->assertFalse(
-            $parser->isAlignClassesEnabled()
-        );
-
-        $parser->setDocumentType(Textile::DOCTYPE_XHTML);
-
-        $this->assertFalse(
-            $parser->isAlignClassesEnabled()
-        );
-
-        $parser->setAlignClasses(true);
-
-        $this->assertTrue(
-            $parser->isAlignClassesEnabled()
-        );
-
-        $parser->setAlignClasses(false);
-
-        $this->assertFalse(
-            $parser->isAlignClassesEnabled()
-        );
-    }
-}
diff --git a/test/Netcarver/Textile/Test/ParserFixtureTest.php b/test/Netcarver/Textile/Test/ParserFixtureTest.php
new file mode 100644
index 00000000..bd8bbf14
--- /dev/null
+++ b/test/Netcarver/Textile/Test/ParserFixtureTest.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * Textile - A Humane Web Text Generator.
+ *
+ * @link https://github.com/textile/php-textile
+ */
+
+namespace Netcarver\Textile\Test;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Yaml\Yaml;
+
+class ParserFixtureTest extends TestCase
+{
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testFixture($test)
+    {
+        if (isset($test['class'])) {
+            $class = $test['class'];
+        } else {
+            $class = '\Netcarver\Textile\Parser';
+        }
+
+        $textile = new $class;
+
+        if (isset($test['doctype'])) {
+            $textile->setDocumentType($test['doctype']);
+        }
+
+        if (isset($test['setup'])) {
+            foreach ($test['setup'] as $setup) {
+                foreach ($setup as $method => $value) {
+                    $textile = $textile->$method($value);
+                }
+            }
+        }
+
+        if (isset($test['method'])) {
+            $method = trim($test['method']);
+        } else {
+            $method = 'parse';
+        }
+
+        $args = array();
+
+        if (isset($test['arguments'])) {
+            foreach ($test['arguments'] as $argument) {
+                foreach ($argument as $value) {
+                    $args[] = $value;
+                }
+            }
+        }
+
+        foreach (array('expect', 'input') as $field) {
+            $test[$field] = strtr($test[$field], array(
+                '\x20' => ' ',
+            ));
+        }
+
+        $expect = rtrim($test['expect']);
+        array_unshift($args, $test['input']);
+        $input = rtrim(call_user_func_array(array($textile, $method), $args));
+
+        foreach (array('expect', 'input') as $variable) {
+            $$variable = preg_replace(
+                array(
+                    '/ id="(fn|note)[a-z0-9\-]*"/',
+                    '/ href="#(fn|note)[a-z0-9\-]*"/',
+                ),
+                '',
+                $$variable
+            );
+        }
+
+        $this->assertSame($expect, $input);
+
+        $public = implode(', ', array_keys(get_object_vars($textile)));
+
+        $this->assertEmpty($public, 'Leaking public class properties.');
+    }
+
+    public function dataProvider()
+    {
+        chdir(dirname(dirname(dirname(__DIR__))));
+        $out = array();
+
+        if ($files = glob('*/*.yaml')) {
+            foreach ($files as $file) {
+                $yaml = Yaml::parseFile($file);
+
+                foreach ($yaml as $name => $test) {
+                    if (!is_array($test) || !isset($test['input']) || !isset($test['expect'])) {
+                        continue;
+                    }
+
+                    if (isset($test['assert']) && $test['assert'] === 'skip') {
+                        continue;
+                    }
+
+                    $out[$file . ':' . $name] = array(
+                        'test' => $test,
+                    );
+                }
+            }
+        }
+
+        return $out;
+    }
+}
diff --git a/test/Netcarver/Textile/Test/ParserTest.php b/test/Netcarver/Textile/Test/ParserTest.php
new file mode 100644
index 00000000..8c14b8e3
--- /dev/null
+++ b/test/Netcarver/Textile/Test/ParserTest.php
@@ -0,0 +1,324 @@
+<?php
+
+/**
+ * Textile - A Humane Web Text Generator.
+ *
+ * @link https://github.com/textile/php-textile
+ */
+
+namespace Netcarver\Textile\Test;
+
+use Netcarver\Textile\Test\Parser\DeprecatedPrepare;
+use Netcarver\Textile\Test\Parser\DeprecatedTextileCommon;
+use PHPUnit\Framework\TestCase;
+use Netcarver\Textile\Parser;
+
+class ParserTest extends TestCase
+{
+    /**
+     * @var Parser
+     */
+    private $parser;
+
+    protected function setUp(): void
+    {
+        $this->parser = new Parser();
+    }
+
+    public function testGetVersion()
+    {
+        $this->assertIsString(
+            $this->parser->getVersion()
+        );
+    }
+
+    public function testInvalidSymbol()
+    {
+        $this->expectException('\InvalidArgumentException');
+        $this->parser->getSymbol('invalidSymbolName');
+    }
+
+    public function testSetGetSymbol()
+    {
+        $value = 'TestValue';
+
+        $this->parser->setSymbol('test', $value);
+
+        $this->assertSame(
+            $value,
+            $this->parser->getSymbol('test')
+        );
+
+        $result = $this->parser->getSymbol();
+
+        $this->assertArrayHasKey(
+            'test',
+            $result
+        );
+
+        $this->assertSame(
+            $value,
+            $result['test']
+        );
+    }
+
+    public function testSetRelativeImagePrefixChaining()
+    {
+        $this->expectError();
+
+        $symbol = $this->parser
+            ->setRelativeImagePrefix('abc')
+            ->setSymbol('test', 'TestValue')
+            ->getSymbol('test');
+
+        $this->assertEquals('TestValue', $symbol);
+    }
+
+    public function testSetGetDimensionlessImage()
+    {
+        $this->assertFalse(
+            $this->parser->getDimensionlessImages()
+        );
+
+        $this->parser->setDimensionlessImages(true);
+
+        $this->assertTrue(
+            $this->parser->getDimensionlessImages()
+        );
+    }
+
+    public function testEncode()
+    {
+        $result = $this->parser->textileEncode('& &amp; &#124; &#x0022 &#x0022;');
+
+        $this->assertSame(
+            '&amp; &amp; &#124; &amp;#x0022 &#x0022;',
+            $result
+        );
+    }
+
+    public function testDeprecatedEncodingArgument()
+    {
+        $this->expectDeprecation();
+
+        $this->assertSame(
+            'content',
+            @$this->parser->textileThis('content', false, true)
+        );
+
+        $this->assertSame(
+            'content',
+            $this->parser->textileEncode('content')
+        );
+
+        $this->parser->textileThis('content', false, true);
+    }
+
+    public function testDeprecatedTextileCommon()
+    {
+        $this->expectDeprecation();
+
+        $parser = new DeprecatedTextileCommon();
+
+        $this->assertSame(
+            ' content',
+            @$parser->testTextileCommon(' content', false)
+        );
+
+        $this->assertSame(
+            ' content',
+            @$parser->testTextileCommon(' content', true)
+        );
+
+        $parser->testTextileCommon('content', false);
+    }
+
+    public function testDeprecatedPrepare()
+    {
+        $this->expectDeprecation();
+
+        $parser = new DeprecatedPrepare();
+
+        $this->assertSame(
+            ' content',
+            @$parser->parse(' content')
+        );
+
+        $parser->parse('content');
+    }
+
+    public function testDeprecatedTextileRestricted()
+    {
+        $this->expectDeprecation();
+
+        $this->assertSame(
+            ' content',
+            @$this->parser->textileRestricted(' content')
+        );
+
+        $this->parser->textileRestricted('content');
+    }
+
+    public function testDeprecatedTextileThis()
+    {
+        $this->expectDeprecation();
+
+        $this->assertSame(
+            ' content',
+            @$this->parser->textileThis(' content')
+        );
+
+        $this->parser->textileThis('content');
+    }
+
+    public function testDeprecatedSetRelativeImagePrefix()
+    {
+        $this->expectDeprecation();
+
+        @$this->parser->setRelativeImagePrefix('/1/');
+
+        $this->assertSame(
+            ' <img alt="" src="/1/2.jpg" /> <a href="/1/2">1</a>',
+            $this->parser->parse(' !2.jpg! "1":2')
+        );
+
+        $this->parser->setRelativeImagePrefix('/1/');
+    }
+
+    public function testInvalidDocumentType()
+    {
+        $this->expectException('\InvalidArgumentException');
+
+        new Parser('InvalidDocumentType');
+    }
+
+    public function testInstanceSharingAndFootnoteIndex()
+    {
+        $previous = array('', '<p><strong>strong</strong></p>');
+
+        for ($i = 1; $i <= 100; $i++) {
+            $content = "Note[1]\n\nfn1. Footnote";
+            $parsed[0] = $this->parser->parse($content);
+            $parsed[1] = $this->parser->parse('*strong*');
+            $this->assertTrue($parsed[0] !== $previous[0]);
+            $this->assertSame($previous[1], $parsed[1]);
+            $previous[0] = $parsed[0];
+            $previous[1] = $parsed[1];
+        }
+    }
+
+    public function testLineSpaceEscaping()
+    {
+        $this->assertSame(
+            ' <strong>line</strong>',
+            $this->parser->parse(' *line*')
+        );
+    }
+
+    public function testDocumentRoot()
+    {
+        $this->parser->setDocumentRootDirectory(__DIR__);
+
+        $this->assertSame(
+            __DIR__,
+            rtrim($this->parser->getDocumentRootDirectory(), '\\/')
+        );
+    }
+
+    public function testDisallowImages()
+    {
+        $this->parser->setImages(false);
+
+        $this->assertFalse(
+            $this->parser->isImageTagEnabled()
+        );
+
+        $this->parser->setImages(true);
+
+        $this->assertTrue(
+            $this->parser->isImageTagEnabled()
+        );
+    }
+
+    public function testLinkRelationShip()
+    {
+        $this->parser->setLinkRelationShip('test');
+
+        $this->assertSame(
+            'test',
+            $this->parser->getLinkRelationShip()
+        );
+    }
+
+    public function testEnableRestrictedMode()
+    {
+        $this->parser->setRestricted(true);
+
+        $this->assertTrue(
+            $this->parser->isRestrictedModeEnabled()
+        );
+
+        $this->parser->setRestricted(false);
+
+        $this->assertFalse(
+            $this->parser->isRestrictedModeEnabled()
+        );
+    }
+
+    public function testImagePrefix()
+    {
+        $this->parser->setLinkPrefix('test');
+
+        $this->assertSame(
+            'test',
+            $this->parser->getLinkPrefix()
+        );
+    }
+
+    public function testLinkPrefix()
+    {
+        $this->parser->setImagePrefix('test');
+
+        $this->assertSame(
+            'test',
+            $this->parser->getImagePrefix()
+        );
+    }
+
+    public function testAlignClasses()
+    {
+        $this->assertFalse(
+            $this->parser->isAlignClassesEnabled()
+        );
+
+        $this->parser->setDocumentType(Parser::DOCTYPE_HTML5);
+
+        $this->assertTrue(
+            $this->parser->isAlignClassesEnabled()
+        );
+
+        $this->parser->setAlignClasses(false);
+
+        $this->assertFalse(
+            $this->parser->isAlignClassesEnabled()
+        );
+
+        $this->parser->setDocumentType(Parser::DOCTYPE_XHTML);
+
+        $this->assertFalse(
+            $this->parser->isAlignClassesEnabled()
+        );
+
+        $this->parser->setAlignClasses(true);
+
+        $this->assertTrue(
+            $this->parser->isAlignClassesEnabled()
+        );
+
+        $this->parser->setAlignClasses(false);
+
+        $this->assertFalse(
+            $this->parser->isAlignClassesEnabled()
+        );
+    }
+}
diff --git a/test/Netcarver/Textile/Test/TagTest.php b/test/Netcarver/Textile/Test/TagTest.php
new file mode 100644
index 00000000..fb8ae789
--- /dev/null
+++ b/test/Netcarver/Textile/Test/TagTest.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * Textile - A Humane Web Text Generator.
+ *
+ * @link https://github.com/textile/php-textile
+ */
+
+namespace Netcarver\Textile\Test;
+
+use PHPUnit\Framework\TestCase;
+use Netcarver\Textile\Tag;
+
+class TagTest extends TestCase
+{
+    public function testTagAttributesGenerator()
+    {
+        $attributes = new Tag(null, array('name' => 'value'));
+        $this->assertEquals(' name="value"', (string) $attributes);
+    }
+}

From 561eeb6328fdf6cb9e9a6aacfee2e8d63251f93f Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Fri, 10 Jun 2022 18:41:19 +0300
Subject: [PATCH 13/16] Reduce code smells

---
 src/Netcarver/Textile/Parser.php           | 48 +++++++++++-----------
 test/Netcarver/Textile/Test/ParserTest.php |  2 +-
 2 files changed, 25 insertions(+), 25 deletions(-)

diff --git a/src/Netcarver/Textile/Parser.php b/src/Netcarver/Textile/Parser.php
index b4626b75..e8b7fea5 100644
--- a/src/Netcarver/Textile/Parser.php
+++ b/src/Netcarver/Textile/Parser.php
@@ -2500,10 +2500,10 @@ protected function parseAttribsToArray($in, $element = '', $include_id = true, $
             }
         }
 
-        if ($element == 'td' or $element == 'tr') {
-            if (preg_match("/^($this->vlgn)/", $matched, $vert)) {
-                $style[] = "vertical-align:" . $this->vAlign($vert[1]);
-            }
+        if (($element === 'td' || $element === 'tr')
+            && preg_match("/^($this->vlgn)/", $matched, $vert)
+        ) {
+            $style[] = "vertical-align:" . $this->vAlign($vert[1]);
         }
 
         if (preg_match("/\{([^}]*)\}/", $matched, $sty)) {
@@ -2552,12 +2552,12 @@ protected function parseAttribsToArray($in, $element = '', $include_id = true, $
             }
         }
 
-        if (preg_match("/([(]+)/", $matched, $pl)) {
+        if (preg_match("/(\(+)/", $matched, $pl)) {
             $style[] = "padding-left:" . strlen($pl[1]) . "em";
             $matched = str_replace($pl[0], '', $matched);
         }
 
-        if (preg_match("/([)]+)/", $matched, $pr)) {
+        if (preg_match("/(\)+)/", $matched, $pr)) {
             $style[] = "padding-right:" . strlen($pr[1]) . "em";
             $matched = str_replace($pr[0], '', $matched);
         }
@@ -2566,11 +2566,11 @@ protected function parseAttribsToArray($in, $element = '', $include_id = true, $
             $style[] = "text-align:" . $this->hAlign($horiz[1]);
         }
 
-        if ($element == 'col') {
-            if (preg_match("/(?:\\\\([0-9]+))?{$this->regex_snippets['space']}*([0-9]+)?/", $matched, $csp)) {
-                $span = isset($csp[1]) ? $csp[1] : '';
-                $width = isset($csp[2]) ? $csp[2] : '';
-            }
+        if ($element == 'col'
+            && preg_match("/(?:\\\\([0-9]+))?{$this->regex_snippets['space']}*([0-9]+)?/", $matched, $csp)
+        ) {
+            $span = isset($csp[1]) ? $csp[1] : '';
+            $width = isset($csp[2]) ? $csp[2] : '';
         }
 
         if ($this->isRestrictedModeEnabled()) {
@@ -2894,7 +2894,7 @@ protected function fRedclothList($m)
         $out = array();
 
         /** @var array<int, string> $text */
-        $text = preg_split('/\n(?=[-])/m', $in);
+        $text = preg_split('/\n(?=-)/m', $in);
 
         foreach ($text as $line) {
             $m = array();
@@ -4265,12 +4265,12 @@ protected function fLink($m)
         // eg. "text":url][otherstuff... will have "[otherstuff" popped back out.
         //     "text":url?q[]=x][123]    will have "[123]" popped off the back, the remaining closing square brackets
         //                               will later be tested for balance
-        if ($counts[']']) {
-            if (1 === preg_match('@(?P<url>^.*\])(?P<tight>\[.*?)$@' . $this->regex_snippets['mod'], $url, $m)) {
-                $url = $m['url'];
-                $tight = $m['tight'];
-                $m = array();
-            }
+        if ($counts[']']
+            && 1 === preg_match('@(?P<url>^.*\])(?P<tight>\[.*?)$@' . $this->regex_snippets['mod'], $url, $m)
+        ) {
+            $url = $m['url'];
+            $tight = $m['tight'];
+            $m = array();
         }
 
         // Split off any trailing text that isn't part of an array assignment.
@@ -4278,12 +4278,12 @@ protected function fLink($m)
         // "text":...?q[]=value1]following  ... would have "following"
         // popped back out and the remaining square bracket
         // will later be tested for balance
-        if ($counts[']']) {
-            if (1 === preg_match('@(?P<url>^.*\])(?!=)(?P<end>.*?)$@' . $this->regex_snippets['mod'], $url, $m)) {
-                $url = $m['url'];
-                $tight = $m['end'] . $tight;
-                $m = array();
-            }
+        if ($counts[']']
+            && 1 === preg_match('@(?P<url>^.*\])(?!=)(?P<end>.*?)$@' . $this->regex_snippets['mod'], $url, $m)
+        ) {
+            $url = $m['url'];
+            $tight = $m['end'] . $tight;
+            $m = array();
         }
 
         // Does this need to be mb_ enabled? We are only searching for text in the ASCII charset anyway
diff --git a/test/Netcarver/Textile/Test/ParserTest.php b/test/Netcarver/Textile/Test/ParserTest.php
index 8c14b8e3..dcbbd27a 100644
--- a/test/Netcarver/Textile/Test/ParserTest.php
+++ b/test/Netcarver/Textile/Test/ParserTest.php
@@ -200,7 +200,7 @@ public function testInstanceSharingAndFootnoteIndex()
             $content = "Note[1]\n\nfn1. Footnote";
             $parsed[0] = $this->parser->parse($content);
             $parsed[1] = $this->parser->parse('*strong*');
-            $this->assertTrue($parsed[0] !== $previous[0]);
+            $this->assertNotSame($parsed[0], $previous[0]);
             $this->assertSame($previous[1], $parsed[1]);
             $previous[0] = $parsed[0];
             $previous[1] = $parsed[1];

From a11ce072735008daf425642200e2bea959055e8c Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Sat, 19 Nov 2022 02:30:32 +0200
Subject: [PATCH 14/16] Update GitHub actions

---
 .github/workflows/ci.yml      |  2 +-
 .github/workflows/release.yml | 18 +++++-------------
 2 files changed, 6 insertions(+), 14 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 828d548f..a7bdffa9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,7 +12,7 @@ jobs:
     steps:
 
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 0
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2a68188d..59c557de 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,25 +13,17 @@ jobs:
     steps:
 
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
 
       - name: Changelog
         id: changelog
         run: |
-          contents="$(sed -e '1,/h2./d' -e '/h2./,$d' CHANGELOG.textile)"
-          contents="${contents//'%'/'%25'}"
-          contents="${contents//$'\n'/'%0A'}"
-          contents="${contents//$'\r'/'%0D'}"
-          echo "::set-output name=contents::$contents"
+          echo "contents<<CHANGELOGEOF" >> $GITHUB_OUTPUT
+          sed -e '1,/h2./d' -e '/h2./,$d' CHANGELOG.textile >> $GITHUB_OUTPUT
+          echo "CHANGELOGEOF" >> $GITHUB_OUTPUT
 
       - name: Create Release
         id: create_release
-        uses: actions/create-release@master
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        uses: shogo82148/actions-create-release@v1
         with:
-          tag_name: ${{ github.ref }}
-          release_name: ${{ github.ref }}
           body: ${{ steps.changelog.outputs.contents }}
-          draft: false
-          prerelease: false

From 226ab8d3ebe162b7972c7e28fef3193b47dff879 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Sat, 3 Dec 2022 19:17:41 +0200
Subject: [PATCH 15/16] Update pull-request template

---
 .github/PULL_REQUEST_TEMPLATE.md | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 6f57fa64..8f2e5d52 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -18,6 +18,5 @@
 <!--- Put an `x` in all the boxes that apply. -->
 - [ ] I documented my additions and changes using PHPdoc.
 - [ ] I wrote fixtures to cover my additions.
-- [ ] `$ composer update`
-- [ ] `$ composer test`
-- [ ] `$ composer cs`
+- [ ] `$ make clean`
+- [ ] `$ make test`

From 02ed0cbe6832c2100342dabb6d01d7ba558cb8e7 Mon Sep 17 00:00:00 2001
From: Jukka Svahn <void@rah.pw>
Date: Sat, 3 Dec 2022 20:19:42 +0200
Subject: [PATCH 16/16] Marks 3.8.0

---
 CHANGELOG.textile                | 4 ++--
 src/Netcarver/Textile/Parser.php | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.textile b/CHANGELOG.textile
index 4a5d795a..20f36462 100644
--- a/CHANGELOG.textile
+++ b/CHANGELOG.textile
@@ -2,9 +2,9 @@ h1. Changelog
 
 Here's a summary of changes in each release. The list doesn't include some small changes or updates to test cases.
 
-h2. Version 3.8.0 - upcoming
+h2. "Version 3.8.0 - 2022/12/03":https://github.com/textile/php-textile/releases/tag/v3.8.0
 
-* Added @Parser::setAlignClasses()@ and @Parser::isAlignClassesEnabled@. This can be used to enable img alignment classes in XHTML output document mode, instead of the default align attribute.
+* Added @Parser::setAlignClasses()@ and @Parser::isAlignClassesEnabled()@. This can be used to enable img alignment classes in XHTML output document mode, instead of the default align attribute.
 * Added @Parser::DOCTYPE_HTML5@ and @Parser::DOCTYPE_XHTML@ constants. These can be used with @Parser::setDocumentType()@ to specify the output document type.
 
 h2. "Version 3.7.7 - 2022/05/01":https://github.com/textile/php-textile/releases/tag/v3.7.7
diff --git a/src/Netcarver/Textile/Parser.php b/src/Netcarver/Textile/Parser.php
index e8b7fea5..e9f0e29a 100644
--- a/src/Netcarver/Textile/Parser.php
+++ b/src/Netcarver/Textile/Parser.php
@@ -384,7 +384,7 @@ class Parser
      *
      * @var string
      */
-    protected $ver = '3.8.0-dev';
+    protected $ver = '3.8.0';
 
     /**
      * Regular expression snippets.