xref: /dokuwiki/_test/tests/Parsing/Helpers/EscapeTest.php (revision 13a62f810fbd091d15ab734b467eaec0a6bf829a)
1<?php
2
3namespace dokuwiki\test\Parsing\Helpers;
4
5use dokuwiki\Parsing\Helpers\Escape;
6
7/**
8 * Tests for the GFM backslash-escape post-hoc helper.
9 *
10 * The lexer-mode coverage is in {@see \dokuwiki\test\Parsing\ParserMode\GfmEscapeTest};
11 * this class exercises the helper that GfmLink and GfmCode call on text
12 * the lexer never reached.
13 */
14class EscapeTest extends \DokuWikiTest
15{
16    /**
17     * Every ASCII punctuation char is escapable per GFM §6.1.
18     *
19     * @dataProvider provideEscapableChars
20     */
21    function testUnescapesEscapablePunctuation(string $char)
22    {
23        $this->assertSame(
24            "before{$char}after",
25            Escape::unescapeBackslashes("before\\{$char}after")
26        );
27    }
28
29    public static function provideEscapableChars(): array
30    {
31        $chars = str_split('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~');
32        return array_combine(
33            array_map(static fn($c) => 'char_' . bin2hex($c), $chars),
34            array_map(static fn($c) => [$c], $chars),
35        );
36    }
37
38    /**
39     * Backslash before any non-punctuation char stays as-is — the helper
40     * must not touch it. Mirrors the lexer mode's pattern, which also
41     * doesn't match these.
42     *
43     * @dataProvider provideNonEscapableTails
44     */
45    function testKeepsBackslashBeforeNonPunctuation(string $tail)
46    {
47        $input = "x\\{$tail}y";
48        $this->assertSame($input, Escape::unescapeBackslashes($input));
49    }
50
51    public static function provideNonEscapableTails(): array
52    {
53        return [
54            'letter_upper' => ['A'],
55            'letter_lower' => ['a'],
56            'digit'        => ['3'],
57            'multibyte'    => ['α'],
58            'space'        => [' '],
59            'tab'          => ["\t"],
60            'newline'      => ["\n"],
61        ];
62    }
63
64    function testDoubleBackslashCollapsesOnce()
65    {
66        // `\\` → `\`. The collapse is a single replacement; the surviving
67        // backslash does NOT consume the next char.
68        $this->assertSame('a\\*b', Escape::unescapeBackslashes('a\\\\*b'));
69    }
70
71    function testTripleBackslashLeavesOneEscape()
72    {
73        // `\\\*` → `\` + `*` (first pair collapses to `\`, the surviving
74        // standalone `\*` then unescapes to `*` because preg_replace
75        // processes all non-overlapping matches in one pass).
76        $this->assertSame('a\\*b', Escape::unescapeBackslashes('a\\\\\\*b'));
77    }
78
79    function testMultipleEscapesInOnePass()
80    {
81        $this->assertSame(
82            '/path*with|special#chars',
83            Escape::unescapeBackslashes('/path\\*with\\|special\\#chars')
84        );
85    }
86
87    function testStringWithoutBackslashesIsUnchanged()
88    {
89        $this->assertSame('plain text', Escape::unescapeBackslashes('plain text'));
90    }
91
92    function testEmptyStringRoundTrips()
93    {
94        $this->assertSame('', Escape::unescapeBackslashes(''));
95    }
96
97    function testTrailingLoneBackslashSurvives()
98    {
99        // A backslash with nothing after it can't form an escape — it
100        // stays literal.
101        $this->assertSame('x\\', Escape::unescapeBackslashes('x\\'));
102    }
103}
104