diff --git a/public/filter/activitynames/classes/text_filter.php b/public/filter/activitynames/classes/text_filter.php index 2939c828ad4..ca8e321d25d 100644 --- a/public/filter/activitynames/classes/text_filter.php +++ b/public/filter/activitynames/classes/text_filter.php @@ -54,12 +54,36 @@ class text_filter extends \core_filters\text_filter { } if ($filterslist) { - return $text = filter_phrases($text, $filterslist); + // Modify each filter's regex pattern to match any whitespace sequence where there are spaces. + // This allows matching activity names regardless of whether users type single spaces, double spaces, + // or non-breaking spaces (which HTML editors often insert). + $filterslist = filter_prepare_phrases_for_filtering($filterslist); + foreach ($filterslist as $filterobject) { + if ($filterobject->workregexp !== null) { + $filterobject->workregexp = self::replace_spaces_with_whitespace($filterobject->workregexp); + } + } + + return filter_phrases($text, $filterslist, null, null, false, true); } else { return $text; } } + /** + * Replace literal spaces in a regex with general whitespace match. + * + * @param string $regex The regex pattern containing literal spaces. + * @return string The regex pattern with spaces replaced. + */ + protected static function replace_spaces_with_whitespace($regex): string { + return preg_replace_callback('/ +/', function($matches): string { + $count = strlen($matches[0]); + // Matches regular space, non-breaking space (U+00A0), or other whitespace. + return '(?:[\s\xC2\xA0]{' . $count . '})'; + }, $regex); + } + /** * Get all the cached activity list for a course * @@ -115,6 +139,9 @@ class text_filter extends \core_filters\text_filter { foreach ($sortedactivities as $cm) { $title = s(trim(strip_tags($cm->name))); $currentname = trim($cm->name); + // Normalize whitespace in activity names to handle double spaces and non-breaking spaces. + // This ensures that activities with multiple consecutive spaces can still be matched + // even when users type the name with different whitespace (e.g., single space, NBSP). $entitisedname = s($currentname); // Avoid empty or unlinkable activity names. if (!empty($title)) { @@ -133,4 +160,4 @@ class text_filter extends \core_filters\text_filter { } return $activitylist; } -} +} \ No newline at end of file diff --git a/public/filter/activitynames/tests/text_filter_test.php b/public/filter/activitynames/tests/text_filter_test.php index b6713515ac1..a86bd2faa87 100644 --- a/public/filter/activitynames/tests/text_filter_test.php +++ b/public/filter/activitynames/tests/text_filter_test.php @@ -102,6 +102,71 @@ final class text_filter_test extends \advanced_testcase { $this->assertEquals($page->name, $matches[3][0]); } + /** + * Data provider for the test_links_with_whitespace. + * + * @return array + */ + public static function links_with_whitespace_provider(): array { + return [ + 'Regular spaces' => [ + 'Assignment 1', + '
Go to Assignment 1
', + true, + ], + 'Two regular spaces' => [ + 'Assignment 1', + 'Go to Assignment 1
', + true, + ], + 'NBSP + regular spaces' => [ + 'Assignment 1', + "Go to Assignment\xC2\xA0 1
", + true, + ], + 'Multiple spaces' => [ + 'Assignment 1 - History', + 'Go to Assignment 1 - History
', + true, + ], + 'Mismatched spaces 1' => [ + 'Assignment 1', + 'Go to Assignment 1
', + false, + ], + 'Mismatched spaces 2' => [ + 'Assignment 1', + 'Go to Assignment 1
', + false, + ], + ]; + } + + /** + * Test that links can be matched with various whitespace combinations. + * + * @dataProvider links_with_whitespace_provider + * @param string $activityname + * @param string $html + * @param bool $expectedresult + */ + public function test_links_with_whitespace(string $activityname, string $html, bool $expectedresult): void { + $this->resetAfterTest(true); + + $course = $this->getDataGenerator()->create_course(); + $context = \context_course::instance($course->id); + + $this->getDataGenerator()->create_module( + 'page', + ['course' => $course->id, 'name' => $activityname] + ); + + $filtered = format_text($html, FORMAT_HTML, ['context' => $context]); + $haslink = strpos($filtered, 'assertEquals($expectedresult, $haslink); + } + public function test_cache(): void { $this->resetAfterTest(true); @@ -166,4 +231,4 @@ final class text_filter_test extends \advanced_testcase { $this->assertEquals($page1->name, $matches[1][0]); $this->assertEquals($page2->name, $matches[1][1]); } -} +} \ No newline at end of file