[TikiWiki-commits] [Git][tikiwiki/tiki][master] [FIX] Markdown: square brackets content in code block syntax gets parsed as link

"luci \(@luciash\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a74b9db9e464_3828c6cc89292c@gitlab-sidekiq-low-urgency-cpu-bound-v2-c84d8dfcb-lfk5h.mail>

luci pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
8007188b by Boss Ibrahim Mussa Gregoire at 2026-08-06T16:26:46+00:00
[FIX] Markdown: square brackets content in code block syntax gets parsed as link
---
* [ENH] Markdown Convert: add codeblock unit test.

* [FIX] Markdown conversion: code content get parse as link.

* [ENH] markdown code: replace markdown code to plugin code

See merge request tikiwiki/tiki!7403

- - - - -


5 changed files:

- lib/core/WikiParser/Parsable.php
- lib/core/WikiParser/ParsableMarkdown.php
- lib/test/Core/Search/Formatter/BaseTest.php
- lib/test/TikiLib/MarkdownParserTest.php
- lib/test/language/TranslationSanitizationTest.php


Changes:

=====================================
lib/core/WikiParser/Parsable.php
=====================================
@@ -435,9 +435,9 @@ if ( \$('#$id') ) {
         global $prefs;
 
         if ($this->option['is_markdown'] && $prefs['markdown_enabled'] === 'y') {
-            $parsable = new WikiParser_ParsableMarkdown($data);
+            $parsable = new WikiParser_ParsableMarkdown();
         } else {
-            $parsable = new WikiParser_ParsableWiki($data);
+            $parsable = new WikiParser_ParsableWiki();
         }
         $parsable->setOptions($this->option);
         return $parsable->wikiParse($data, $noparsed);


=====================================
lib/core/WikiParser/ParsableMarkdown.php
=====================================
@@ -84,11 +84,40 @@ class WikiParser_ParsableMarkdown extends ParserLib
             $data = $this->autolinks($data);
         }
 
+        /**
+         * extract Markdown code blocks to be processed later
+         * This regex matches code blocks that:
+         * - start with a fence of 3 or more backticks or tildes, also allows up to three spaces before opening,
+         * - language identifier (info string) is optional on the opening line and can contain anything,
+         * - end with a matching fence of the same character as at start,
+         * - allows up to three spaces before both opening and closing fences.
+         *
+         * Note: the first capturing group contains the opening fence itself.
+         * The fenced block content is matched by the middle non-capturing part
+         * and is preserved through the full match.
+         *
+         * Reference: https://spec.commonmark.org/0.31.2/#fenced-code-blocks
+         * */
+        $pattern = '/^(?: {0,3})([`~])\1{2,}([^`\r\n]*)?\r?\n(?:[\s\S]*?)(?:\r?\n(?: {0,3})\1{1,})/m';
+        preg_match_all($pattern, $data, $matches);
+        $list_code = [];
+        foreach ($matches[0] as $match) {
+            $hash = '§' . md5(uniqid()) . '§';
+            $data = str_replace($match, $hash, $data);
+            $list_code[] = [
+                'value' => $match,
+                'hash' => $hash
+            ];
+        }
         // wiki page links and external links are handled in Tiki-syntax to allow sister sites and other semantic linking
         $data = $this->parse_data_wikilinks($data, false, $this->option['wysiwyg']);
         $data = $this->parse_data_externallinks($data, false);
         $data = preg_replace(';~hc~(.*?)~/hc~;s', '<!-- $1 -->', $data);
 
+        // restore code blocks
+        foreach ($list_code as $code) {
+            $data = str_replace($code['hash'], $code['value'], $data);
+        }
         // converter/parser expects UTF-8, try to cleanup invalid characters
         $data = mb_convert_encoding($data, 'UTF-8', 'UTF-8');
 


=====================================
lib/test/Core/Search/Formatter/BaseTest.php
=====================================
@@ -16,6 +16,17 @@ use TikiLib;
 
 class BaseTest extends TestCase
 {
+    protected function setUp(): void
+    {
+        // Simulate query string parameters
+        $_GET['foo'] = 'bar';
+
+        // Simulate POST parameters
+        $_POST['baz'] = 'qux';
+
+        // Now $_REQUEST will contain both
+        $_REQUEST = array_merge($_GET, $_POST, $_COOKIE);
+    }
     public function testBasicFormatter()
     {
         $plugin = new Search_Formatter_Plugin_WikiTemplate("* {display name=object_id} ({display name=object_type})\n");
@@ -406,4 +417,11 @@ OUT;
 
         $this->assertEquals("x", $output);
     }
+    protected function tearDown(): void
+    {
+        $_GET = [];
+        $_POST = [];
+        $_COOKIE = [];
+        $_REQUEST = [];
+    }
 }


=====================================
lib/test/TikiLib/MarkdownParserTest.php
=====================================
@@ -57,8 +57,8 @@ class TikiLib_MarkdownParserTest extends TikiTestCase
         $prefs['feature_wiki_argvariable'] = 'y';
         $heading_links_pref = $prefs['wiki_heading_links'];
         $prefs['wiki_heading_links'] = 'n';
-
-        $this->assertEquals($this->html(), TikiLib::lib('parser')->parse_data('{syntax type=markdown}' . $this->markdown()));
+        $parse_data = TikiLib::lib('parser')->parse_data('{syntax type=markdown}' . $this->markdown());
+        $this->assertEquals($this->html(), $parse_data);
 
         $prefs['wiki_heading_links'] = $heading_links_pref;
     }
@@ -78,6 +78,129 @@ class TikiLib_MarkdownParserTest extends TikiTestCase
         $prefs['wiki_heading_links'] = $heading_links_pref;
     }
 
+    /**
+    * /**
+     * Test Markdown fenced code block parsing with standard triple fences.
+     *
+     * This method validates that the Markdown parser correctly handles:
+     * - Triple backtick fences with a language specifier (e.g. ```php)
+     * - Triple tilde fences with a language specifier (e.g. ~~~html)
+     *
+     * The test ensures that:
+     * - Code blocks are wrapped in <pre><code> tags
+     * - The appropriate language class is applied when specified
+     * - Special characters inside the code block are properly escaped
+     *
+     *
+     * @return void
+     * @throws Exception
+     */
+    public function testEscapeMarkdownCode(): void
+    {
+        global $prefs, $user;
+
+        $user = 'admin';
+        $prefs['markdown_enabled'] = 'y';
+
+        $data = '```php
+\$rules=[
+"name"=>John,
+"email"=>[email protected],
+"link"=>http://example.com,
+]
+```';
+        $expected_html = '<div class="codelisting_container"><div class="icon_copy_code far fa-clipboard" tabindex="0" data-clipboard-target="#md-codebox1"><span class="copy_code_tooltiptext">Copy to clipboard</span></div><pre class="codelisting" id="md-codebox1" dir="ltr" style="white-space:pre-wrap; overflow-wrap: break-word; word-wrap: break-word;" data-syntax="php"><div class="code">\$rules=[
+&quot;name&quot;=&gt;John,
+&quot;email&quot;=&gt;[email protected],
+&quot;link&quot;=&gt;http://example.com,
+]
+</div></pre></div>
+';
+        $parse_data = TikiLib::lib('parser')->parse_data('{syntax type=markdown}' . $data);
+        $this->assertEquals($expected_html, $parse_data);
+
+        // title test example
+        $title_example = '~~~html
+<div class="container">
+<h1>My First Bootstrap Page</h1>
+<p>This is some text.</p>
+</div>
+~~~';
+        $tilde_data = "{syntax type=markdown}" . $title_example;
+        $tilde_output = '<div class="codelisting_container"><div class="icon_copy_code far fa-clipboard" tabindex="0" data-clipboard-target="#md-codebox2"><span class="copy_code_tooltiptext">Copy to clipboard</span></div><pre class="codelisting" id="md-codebox2" dir="ltr" style="white-space:pre-wrap; overflow-wrap: break-word; word-wrap: break-word;" data-syntax="html"><div class="code">&lt;div class=&quot;container&quot;&gt;
+&lt;h1&gt;My First Bootstrap Page&lt;/h1&gt;
+&lt;p&gt;This is some text.&lt;/p&gt;
+&lt;/div&gt;
+</div></pre></div>
+';
+        $parse_tilde_data = TikiLib::lib('parser')->parse_data($tilde_data);
+
+        $this->assertEquals($tilde_output, $parse_tilde_data);
+    }
+
+    /**
+     * Test markdown fenced code block parsing with variable fence lengths and syntax variations.
+     *
+     * This method ensures that the Markdown parser correctly handles:
+     * - Backtick fences of length greater than 3 (e.g. 4 backticks)
+     * - Tilde fences of length greater than 3 (e.g. 5 tildes)
+     * - Indented fences (up to 3 spaces before the opening fence)
+     * - Fences with and without language specifiers
+     *
+     * Each case validates that the parser produces the expected HTML output
+     * with proper <pre><code> wrapping, language class assignment when present,
+     * and correct escaping of special characters.
+     * @return void
+     * @throws Exception
+     */
+    public function testMarkdownCodeFencesVariations(): void
+    {
+        global $prefs;
+        $prefs['markdown_enabled'] = 'y';
+
+        // Case 1: 4 backticks with language
+        $data4 = '````js
+console.log("Hello");
+````';
+        $expected4 = '<div class="codelisting_container"><div class="icon_copy_code far fa-clipboard" tabindex="0" data-clipboard-target="#md-codebox1"><span class="copy_code_tooltiptext">Copy to clipboard</span></div><pre class="codelisting" id="md-codebox1" dir="ltr" style="white-space:pre-wrap; overflow-wrap: break-word; word-wrap: break-word;" data-syntax="js"><div class="code">console.log(&quot;Hello&quot;);
+</div></pre></div>
+';
+        $this->assertEquals($expected4, TikiLib::lib('parser')->parse_data('{syntax type=markdown}' . $data4));
+
+        // Case 2: 5 tildes, no language
+        $data5 = '~~~~~
+Plain text block
+~~~~~';
+        $expected5 = '<div class="codelisting_container"><div class="icon_copy_code far fa-clipboard" tabindex="0" data-clipboard-target="#md-codebox2"><span class="copy_code_tooltiptext">Copy to clipboard</span></div><pre class="codelisting" id="md-codebox2" dir="ltr" style="white-space:pre-wrap; overflow-wrap: break-word; word-wrap: break-word;"><div class="code">Plain text block
+</div></pre></div>
+';
+        $this->assertEquals($expected5, TikiLib::lib('parser')->parse_data('{syntax type=markdown}' . $data5));
+
+        // Case 3: Indented fence (3 spaces before backticks)
+        $dataIndented = "   ```python\nprint(\"Indented\")\n   ```";
+        $expectedIndented = '<div class="codelisting_container"><div class="icon_copy_code far fa-clipboard" tabindex="0" data-clipboard-target="#md-codebox3"><span class="copy_code_tooltiptext">Copy to clipboard</span></div><pre class="codelisting" id="md-codebox3" dir="ltr" style="white-space:pre-wrap; overflow-wrap: break-word; word-wrap: break-word;" data-syntax="python"><div class="code">print(&quot;Indented&quot;)
+</div></pre></div>
+';
+        $this->assertEquals($expectedIndented, TikiLib::lib('parser')->parse_data('{syntax type=markdown}' . $dataIndented));
+
+        // Case 4: Fence with empty language specifier
+        $dataEmptyLang = '``` 
+No language here
+```';
+        $expectedEmptyLang = '<div class="codelisting_container"><div class="icon_copy_code far fa-clipboard" tabindex="0" data-clipboard-target="#md-codebox4"><span class="copy_code_tooltiptext">Copy to clipboard</span></div><pre class="codelisting" id="md-codebox4" dir="ltr" style="white-space:pre-wrap; overflow-wrap: break-word; word-wrap: break-word;"><div class="code">No language here
+</div></pre></div>
+';
+        $this->assertEquals($expectedEmptyLang, TikiLib::lib('parser')->parse_data('{syntax type=markdown}' . $dataEmptyLang));
+
+        // Case 5: Negative test - only 2 backticks should NOT be parsed as a code block
+        $dataInvalid = "``js
+console.log('Not valid');
+``";
+        // Expectation: parser should leave it untouched, since it's not a valid fence
+        $expectedInvalid = "<p><code>js console.log('Not valid'); </code></p>\n";
+        $this->assertEquals($expectedInvalid, TikiLib::lib('parser')->parse_data('{syntax type=markdown}' . $dataInvalid));
+    }
+
     public function testReplaceLinks(): void
     {
         $template = "Link to ((%s)) in wiki syntax, as a ((%s|description)) and in [markdown](%s) syntax.";
@@ -179,6 +302,14 @@ Footnote 2 link[^second].
 
 Inline footnote^[Text of inline footnote] definition.
 
+```php
+\$rules=[
+\"name\"=>\$validate->string()->required()->min(3)->max(30)->check(),
+\"email\"=>\$validate->string()->required()->min(3)->max(60)->email()->check(),
+\"link\"=>\$validate->string()->required()->min(3)->max(60)->url()->check(),
+\"age\"=>\$validate->number()->required()->positive()->check()
+];
+```
 Duplicated footnote reference[^second].
 
 [^first]: Footnote **can have markup**
@@ -308,6 +439,13 @@ line 3 of code
 <p>Footnote 1 link<sup id="fnref:first"><a class="footnote-ref" href="#fn:first" role="doc-noteref">1</a></sup>.</p>
 <p>Footnote 2 link<sup id="fnref:second"><a class="footnote-ref" href="#fn:second" role="doc-noteref">2</a></sup>.</p>
 <p>Inline footnote<sup id="fnref:text-of-inline-footn"><a class="footnote-ref" href="#fn:text-of-inline-footn" role="doc-noteref">3</a></sup> definition.</p>
+<div class="codelisting_container"><div class="icon_copy_code far fa-clipboard" tabindex="0" data-clipboard-target="#md-codebox2"><span class="copy_code_tooltiptext">Copy to clipboard</span></div><pre class="codelisting" id="md-codebox2" dir="ltr" style="white-space:pre-wrap; overflow-wrap: break-word; word-wrap: break-word;" data-syntax="php"><div class="code">$rules=[
+&quot;name&quot;=&gt;$validate-&gt;string()-&gt;required()-&gt;min(3)-&gt;max(30)-&gt;check(),
+&quot;email&quot;=&gt;$validate-&gt;string()-&gt;required()-&gt;min(3)-&gt;max(60)-&gt;email()-&gt;check(),
+&quot;link&quot;=&gt;$validate-&gt;string()-&gt;required()-&gt;min(3)-&gt;max(60)-&gt;url()-&gt;check(),
+&quot;age&quot;=&gt;$validate-&gt;number()-&gt;required()-&gt;positive()-&gt;check()
+];
+</div></pre></div>
 <p>Duplicated footnote reference<sup id="fnref:second__2"><a class="footnote-ref" href="#fn:second" role="doc-noteref">2</a></sup>.</p>
 <div class="footnotes" role="doc-endnotes"><hr /><ol><li class="footnote" id="fn:first" role="doc-endnote"><p>Footnote <strong>can have markup</strong></p>
 <p>and multiple paragraphs.&nbsp;<a class="footnote-backref" rev="footnote" href="#fnref:first" role="doc-backlink">↩</a></p></li>


=====================================
lib/test/language/TranslationSanitizationTest.php
=====================================
@@ -31,7 +31,7 @@ class TranslationSanitizationTest extends \TikiTestCase
         $this->lang = 'ts_' . $testCounter++;
         $this->langDir = $this->tikiroot . 'lang/' . $this->lang;
         $this->customFile = $this->langDir . '/custom.php';
-
+        $_SERVER['REQUEST_URI'] = '/test/language/TranslationSanitizationTest.php';
         chdir($this->tikiroot);
 
         // Enable database translations
@@ -61,6 +61,8 @@ $lang = array(
         $reflection = new \ReflectionClass(LanguageTranslator::class);
         $instancesProperty = $reflection->getProperty('instances');
         $instancesProperty->setValue(null, []);
+        // Clean up to avoid leaking state between tests
+        unset($_SERVER['REQUEST_URI']);
     }
 
     protected function tearDown(): void



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/8007188b613a14e10450e28bdaf56149309cce6b

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/8007188b613a14e10450e28bdaf56149309cce6b
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.