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