diff --git a/src/Enums/TimestampStyle.php b/src/Enums/TimestampStyle.php new file mode 100644 index 0000000..d8fa874 --- /dev/null +++ b/src/Enums/TimestampStyle.php @@ -0,0 +1,37 @@ +` to its users and nothing says why. The shapes + * live here so they are spelled correctly once. + * + * @see https://discord.com/developers/docs/reference#message-formatting + */ +final class Format +{ + public static function user(string $userId): string + { + return '<@' . $userId . '>'; + } + + public static function role(string $roleId): string + { + return '<@&' . $roleId . '>'; + } + + public static function channel(string $channelId): string + { + return '<#' . $channelId . '>'; + } + + /** + * A slash command, rendered as a link that fills it in when clicked. + * + * The name is the full path as the user types it, so a subcommand is given + * as "voice room limit". + */ + public static function command(string $name, string $commandId): string + { + return ''; + } + + public static function emoji(string $name, string $emojiId, bool $animated = false): string + { + return '<' . ($animated ? 'a' : '') . ':' . $name . ':' . $emojiId . '>'; + } + + /** + * A moment, drawn in each reader's own timezone. + * + * A plain integer is a Unix timestamp in seconds, which is what Discord + * takes; anything else is asked for one. + */ + public static function timestamp( + DateTimeInterface|int $moment, + TimestampStyle $style = TimestampStyle::ShortDateTime, + ): string { + $seconds = $moment instanceof DateTimeInterface ? $moment->getTimestamp() : $moment; + + return 'value . '>'; + } + + public static function code(string $text): string + { + return '`' . $text . '`'; + } + + /** + * @param string|null $language a syntax highlighting hint, such as "php" + */ + public static function codeBlock(string $text, ?string $language = null): string + { + return '```' . ($language ?? '') . "\n" . $text . "\n" . '```'; + } +} diff --git a/tests/FormatTest.php b/tests/FormatTest.php new file mode 100644 index 0000000..4a84cd5 --- /dev/null +++ b/tests/FormatTest.php @@ -0,0 +1,111 @@ +assertSame('<@254766810296090626>', Format::user('254766810296090626')); + } + + public function testMentionsARole(): void + { + $this->assertSame('<@&979052732218503178>', Format::role('979052732218503178')); + } + + public function testMentionsAChannel(): void + { + $this->assertSame('<#979052732218503179>', Format::channel('979052732218503179')); + } + + /** + * A subcommand is mentioned by the whole path the user types, not just the + * leaf, and carries the id of the top level command it belongs to. + */ + public function testMentionsASubcommandByItsFullPath(): void + { + $this->assertSame('', Format::command('voice room limit', '12345')); + } + + public function testFormatsACustomEmoji(): void + { + $this->assertSame('<:apex:12345>', Format::emoji('apex', '12345')); + } + + public function testAnAnimatedEmojiIsMarkedAsOne(): void + { + $this->assertSame('', Format::emoji('apex', '12345', animated: true)); + } + + public function testATimestampIsTakenAsSeconds(): void + { + $this->assertSame('', Format::timestamp(1618928400)); + } + + public function testATimestampCanBeGivenAsADate(): void + { + $this->assertSame( + '', + Format::timestamp(new DateTimeImmutable('@1618928400')), + ); + } + + /** + * Discord's own default when a style is left off, so leaving it off here + * has to mean the same thing. + */ + public function testTheDefaultStyleIsShortDateTime(): void + { + $this->assertSame( + Format::timestamp(1618928400, TimestampStyle::ShortDateTime), + Format::timestamp(1618928400), + ); + } + + #[DataProvider('stylesProvider')] + public function testATimestampCarriesItsStyle(TimestampStyle $style, string $expected): void + { + $this->assertSame($expected, Format::timestamp(1618928400, $style)); + } + + public static function stylesProvider(): array + { + return [ + 'short time' => [TimestampStyle::ShortTime, ''], + 'long time' => [TimestampStyle::LongTime, ''], + 'short date' => [TimestampStyle::ShortDate, ''], + 'long date' => [TimestampStyle::LongDate, ''], + 'short date and time' => [TimestampStyle::ShortDateTime, ''], + 'long date and time' => [TimestampStyle::LongDateTime, ''], + 'relative' => [TimestampStyle::Relative, ''], + ]; + } + + public function testFormatsInlineCode(): void + { + $this->assertSame('`/warn`', Format::code('/warn')); + } + + /** + * The closing fence has to be on its own line, or a block whose content + * ends without a newline swallows it. + */ + public function testFormatsACodeBlock(): void + { + $this->assertSame("```\nechoed\n```", Format::codeBlock('echoed')); + } + + public function testACodeBlockCanNameItsLanguage(): void + { + $this->assertSame("```php\n\$x = 1;\n```", Format::codeBlock('$x = 1;', 'php')); + } +}