diff --git a/src/Foundation/Concerns/ImplementsStringHelpers.php b/src/Foundation/Concerns/ImplementsStringHelpers.php index a5910e8b..91096048 100644 --- a/src/Foundation/Concerns/ImplementsStringHelpers.php +++ b/src/Foundation/Concerns/ImplementsStringHelpers.php @@ -38,6 +38,19 @@ public static function makeTitle(string $value): string )); } + public static function makeSlug(string $value): string + { + // Expand camelCase and PascalCase to separate words + $value = preg_replace('/([a-z])([A-Z])/', '$1 $2', $value); + + // Transliterate international characters to ASCII + $value = Str::transliterate($value); + + // Todo: In v2.0 we will use the following dictionary: ['@' => 'at', '&' => 'and'] + + return Str::slug($value); + } + public static function normalizeNewlines(string $string): string { return str_replace("\r\n", "\n", $string); diff --git a/src/Framework/Actions/CreatesNewMarkdownPostFile.php b/src/Framework/Actions/CreatesNewMarkdownPostFile.php index 3f1f4ea9..f524ded7 100644 --- a/src/Framework/Actions/CreatesNewMarkdownPostFile.php +++ b/src/Framework/Actions/CreatesNewMarkdownPostFile.php @@ -6,9 +6,9 @@ use Hyde\Framework\Exceptions\FileConflictException; use Hyde\Facades\Filesystem; +use Hyde\Hyde; use Hyde\Pages\MarkdownPost; use Illuminate\Support\Carbon; -use Illuminate\Support\Str; /** * Offloads logic for the make:post command. @@ -48,7 +48,7 @@ public function __construct(string $title, ?string $description, ?string $catego $this->customContent = $customContent; $this->date = Carbon::make($date ?? Carbon::now())->format('Y-m-d H:i'); - $this->identifier = Str::slug($title); + $this->identifier = Hyde::makeSlug($title); } /** diff --git a/src/Framework/Actions/CreatesNewPageSourceFile.php b/src/Framework/Actions/CreatesNewPageSourceFile.php index ccb0fb6f..aa9355a6 100644 --- a/src/Framework/Actions/CreatesNewPageSourceFile.php +++ b/src/Framework/Actions/CreatesNewPageSourceFile.php @@ -81,7 +81,7 @@ protected function fileName(string $title): string } // And return a slug made from just the title without the subdirectory - return Str::slug(basename($title)); + return Hyde::makeSlug(basename($title)); } protected function normalizeSubdirectory(string $title): string diff --git a/src/Framework/Features/Navigation/DocumentationSidebar.php b/src/Framework/Features/Navigation/DocumentationSidebar.php index 919b9343..f9fc088a 100644 --- a/src/Framework/Features/Navigation/DocumentationSidebar.php +++ b/src/Framework/Features/Navigation/DocumentationSidebar.php @@ -11,7 +11,6 @@ use Hyde\Support\Facades\Render; use Hyde\Support\Models\Route; use Illuminate\Support\Collection; -use Illuminate\Support\Str; use function collect; @@ -48,14 +47,15 @@ public function getGroups(): array public function getItemsInGroup(?string $group): Collection { return $this->items->filter(function (NavItem $item) use ($group): bool { - return ($item->getGroup() === $group) || ($item->getGroup() === Str::slug($group)); + return ($item->getGroup() === $group) || ($item->getGroup() === Hyde::makeSlug($group)); })->sortBy('navigation.priority')->values(); } public function isGroupActive(string $group): bool { - return Str::slug(Render::getPage()->navigationMenuGroup()) === $group - || $this->isPageIndexPage() && $this->shouldIndexPageBeActive($group); + $normalized = Hyde::makeSlug(Render::getPage()->navigationMenuGroup() ?? 'other'); + + return ($normalized === $group) || ($this->isPageIndexPage() && $this->shouldIndexPageBeActive($group)); } public function makeGroupTitle(string $group): string diff --git a/src/Framework/Features/Navigation/NavItem.php b/src/Framework/Features/Navigation/NavItem.php index 7a57b554..a3fe8932 100644 --- a/src/Framework/Features/Navigation/NavItem.php +++ b/src/Framework/Features/Navigation/NavItem.php @@ -7,7 +7,6 @@ use Hyde\Foundation\Facades\Routes; use Hyde\Hyde; use Hyde\Support\Models\Route; -use Illuminate\Support\Str; use Stringable; /** @@ -133,6 +132,6 @@ protected static function getRouteGroup(Route $route): ?string protected static function normalizeGroupKey(?string $group): ?string { - return $group ? Str::slug($group) : null; + return $group ? Hyde::makeSlug($group) : null; } } diff --git a/tests/Feature/HydeKernelTest.php b/tests/Feature/HydeKernelTest.php index d2e58f58..ea18c156 100644 --- a/tests/Feature/HydeKernelTest.php +++ b/tests/Feature/HydeKernelTest.php @@ -32,6 +32,7 @@ * @covers \Hyde\Hyde * * @see \Hyde\Framework\Testing\Unit\HydeHelperFacadeMakeTitleTest + * @see \Hyde\Framework\Testing\Unit\HydeHelperFacadeMakeSlugTest * @see \Hyde\Framework\Testing\Feature\HydeExtensionFeatureTest */ class HydeKernelTest extends TestCase @@ -108,6 +109,11 @@ public function testMakeTitleHelperReturnsTitleFromPageSlug() $this->assertSame('Foo Bar', Hyde::makeTitle('foo-bar')); } + public function testMakeSlugHelperReturnsSlugFromTitle() + { + $this->assertSame('foo-bar', Hyde::makeSlug('Foo Bar')); + } + public function testNormalizeNewlinesReplacesCarriageReturnsWithUnixEndings() { $this->assertSame("foo\nbar\nbaz", Hyde::normalizeNewlines("foo\nbar\r\nbaz")); diff --git a/tests/Feature/InternationalizationTest.php b/tests/Feature/InternationalizationTest.php new file mode 100644 index 00000000..04e6d6fa --- /dev/null +++ b/tests/Feature/InternationalizationTest.php @@ -0,0 +1,117 @@ +save(); + + $this->assertSame("_posts/$expectedSlug.md", $path); + $this->assertSame($expectedSlug, $creator->getIdentifier()); + $this->assertSame($creator->getIdentifier(), Hyde::makeSlug($title)); + $this->assertFileExists($path); + + $contents = file_get_contents($path); + + if (str_contains($title, ' ')) { + $expectedTitle = "'$expectedTitle'"; + } + + if (str_contains($description, ' ')) { + $description = "'$description'"; + } + + $this->assertStringContainsString("title: $expectedTitle", $contents); + $this->assertSame(<< $title, + 'description' => $description, + 'category' => 'blog', + 'author' => 'default', + 'date' => '2024-12-22 10:45', + ]); + + $path = StaticPageBuilder::handle($page); + + $this->assertSame(Hyde::path("_site/posts/$expectedSlug.html"), $path); + $this->assertFileExists($path); + + $contents = file_get_contents($path); + + $this->assertStringContainsString("HydePHP - $expectedTitle", $contents); + $this->assertStringContainsString("

$expectedTitle

", $contents); + $this->assertStringContainsString("", $contents); + + Filesystem::unlink($path); + } + + public static function internationalCharacterSetsProvider(): array + { + return [ + 'Chinese (Simplified)' => [ + '你好世界', + '简短描述', + 'ni-hao-shi-jie', + '你好世界', + ], + 'Japanese' => [ + 'こんにちは世界', + '短い説明', + 'konnichihashi-jie', + 'こんにちは世界', + ], + 'Korean' => [ + '안녕하세요 세계', + '짧은 설명', + 'annyeonghaseyo-segye', + '안녕하세요 세계', + ], + ]; + } +} diff --git a/tests/Unit/HydeHelperFacadeMakeSlugTest.php b/tests/Unit/HydeHelperFacadeMakeSlugTest.php new file mode 100644 index 00000000..0d1279cc --- /dev/null +++ b/tests/Unit/HydeHelperFacadeMakeSlugTest.php @@ -0,0 +1,131 @@ +assertSame('hello-world', Hyde::makeSlug('Hello World')); + } + + public function testMakeSlugHelperConvertsKebabCaseToSlug() + { + $this->assertSame('hello-world', Hyde::makeSlug('hello-world')); + } + + public function testMakeSlugHelperConvertsSnakeCaseToSlug() + { + $this->assertSame('hello-world', Hyde::makeSlug('hello_world')); + } + + public function testMakeSlugHelperConvertsCamelCaseToSlug() + { + $this->assertSame('hello-world', Hyde::makeSlug('helloWorld')); + } + + public function testMakeSlugHelperConvertsPascalCaseToSlug() + { + $this->assertSame('hello-world', Hyde::makeSlug('HelloWorld')); + } + + public function testMakeSlugHelperHandlesMultipleSpaces() + { + $this->assertSame('hello-world', Hyde::makeSlug('Hello World')); + } + + public function testMakeSlugHelperHandlesSpecialCharacters() + { + $this->assertSame('hello-world', Hyde::makeSlug('Hello & World!')); + } + + public function testMakeSlugHelperConvertsUppercaseToLowercase() + { + $this->assertSame('hello-world', Hyde::makeSlug('HELLO WORLD')); + $this->assertSame('hello-world', Hyde::makeSlug('HELLO_WORLD')); + } + + public function testMakeSlugHelperHandlesNumbers() + { + $this->assertSame('hello-world-123', Hyde::makeSlug('Hello World 123')); + } + + public function testMakeSlugHelperTransliteratesChineseCharacters() + { + $this->assertSame('ni-hao-shi-jie', Hyde::makeSlug('你好世界')); + } + + public function testMakeSlugHelperTransliteratesJapaneseCharacters() + { + $this->assertSame('konnichihashi-jie', Hyde::makeSlug('こんにちは世界')); + } + + public function testMakeSlugHelperTransliteratesKoreanCharacters() + { + $this->assertSame('annyeongsegye', Hyde::makeSlug('안녕세계')); + } + + public function testMakeSlugHelperTransliteratesArabicCharacters() + { + $this->assertSame('mrhb-bllm', Hyde::makeSlug('مرحبا بالعالم')); + } + + public function testMakeSlugHelperTransliteratesRussianCharacters() + { + $this->assertSame('privet-mir', Hyde::makeSlug('Привет мир')); + } + + public function testMakeSlugHelperTransliteratesAccentedLatinCharacters() + { + $this->assertSame('hello-world', Hyde::makeSlug('hèllô wórld')); + $this->assertSame('uber-strasse', Hyde::makeSlug('über straße')); + } + + public function testMakeSlugHelperHandlesMixedScripts() + { + $this->assertSame('hello-ni-hao-world', Hyde::makeSlug('Hello 你好 World')); + $this->assertSame('privet-world', Hyde::makeSlug('Привет World')); + } + + public function testMakeSlugHelperHandlesEmojis() + { + $this->assertSame('hello-world', Hyde::makeSlug('Hello 👋 World')); + $this->assertSame('world', Hyde::makeSlug('😊 World')); + } + + public function testMakeSlugHelperHandlesComplexMixedInput() + { + $this->assertSame( + 'hello-ni-hao-privet-bonjour-world-123', + Hyde::makeSlug('Hello 你好 Привет Bonjóur World 123!') + ); + } + + public function testMakeSlugHelperHandlesEdgeCases() + { + $this->assertSame('', Hyde::makeSlug('')); + $this->assertSame('at', Hyde::makeSlug('!@#$%^&*()')); + $this->assertSame('', Hyde::makeSlug('... ...')); + $this->assertSame('multiple-dashes', Hyde::makeSlug('multiple---dashes')); + } + + public function testMakeSlugHelperPreservesValidCharacters() + { + $this->assertSame('abc-123', Hyde::makeSlug('abc-123')); + $this->assertSame('test-slug', Hyde::makeSlug('test-slug')); + } + + public function testMakeSlugHelperHandlesWhitespace() + { + $this->assertSame('trim-spaces', Hyde::makeSlug(' trim spaces ')); + $this->assertSame('newline-test', Hyde::makeSlug("newline\ntest")); + $this->assertSame('tab-test', Hyde::makeSlug("tab\ttest")); + } +}