diff --git a/src/Highlighter.php b/src/Highlighter.php index 843cf5b6..f5bdcbb0 100644 --- a/src/Highlighter.php +++ b/src/Highlighter.php @@ -16,6 +16,7 @@ use Tempest\Highlight\Languages\Dockerfile\DockerfileLanguage; use Tempest\Highlight\Languages\DotEnv\DotEnvLanguage; use Tempest\Highlight\Languages\Gdscript\GdscriptLanguage; +use Tempest\Highlight\Languages\Go\GoLanguage; use Tempest\Highlight\Languages\Graphql\GraphqlLanguage; use Tempest\Highlight\Languages\Html\HtmlLanguage; use Tempest\Highlight\Languages\Http\HttpLanguage; @@ -77,6 +78,7 @@ public function __construct( ->addLanguage(new DocCommentLanguage()) ->addLanguage(new DockerfileLanguage()) ->addLanguage(new GdscriptLanguage()) + ->addLanguage(new GoLanguage()) ->addLanguage(new GraphqlLanguage()) ->addLanguage(new HtmlLanguage()) ->addLanguage(new HttpLanguage()) diff --git a/src/Languages/Go/GoLanguage.php b/src/Languages/Go/GoLanguage.php new file mode 100644 index 00000000..5d9984e3 --- /dev/null +++ b/src/Languages/Go/GoLanguage.php @@ -0,0 +1,52 @@ +\/\/[^\r\n]*|\/\*[^*]*(?:\*(?!\/)[^*]*)*\*\/)/s'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::COMMENT; + } +} diff --git a/src/Languages/Go/Patterns/GoFunctionPattern.php b/src/Languages/Go/Patterns/GoFunctionPattern.php new file mode 100644 index 00000000..b33bb6df --- /dev/null +++ b/src/Languages/Go/Patterns/GoFunctionPattern.php @@ -0,0 +1,27 @@ +[A-Za-z_]\w*)(?=\s*(?:\[[^\[\]\r\n]*\]\s*)?\()'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::PROPERTY; + } +} diff --git a/src/Languages/Go/Patterns/GoKeywordPattern.php b/src/Languages/Go/Patterns/GoKeywordPattern.php new file mode 100644 index 00000000..bfbd56a7 --- /dev/null +++ b/src/Languages/Go/Patterns/GoKeywordPattern.php @@ -0,0 +1,27 @@ +break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::KEYWORD; + } +} diff --git a/src/Languages/Go/Patterns/GoLiteralPattern.php b/src/Languages/Go/Patterns/GoLiteralPattern.php new file mode 100644 index 00000000..cff01268 --- /dev/null +++ b/src/Languages/Go/Patterns/GoLiteralPattern.php @@ -0,0 +1,27 @@ +false|iota|nil|true)\b'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::LITERAL; + } +} diff --git a/src/Languages/Go/Patterns/GoNumberPattern.php b/src/Languages/Go/Patterns/GoNumberPattern.php new file mode 100644 index 00000000..38d65975 --- /dev/null +++ b/src/Languages/Go/Patterns/GoNumberPattern.php @@ -0,0 +1,27 @@ +(?:0[xX][\dA-Fa-f](?:_?[\dA-Fa-f])*(?:\.[\dA-Fa-f](?:_?[\dA-Fa-f])*)?(?:[pP][+-]?\d(?:_?\d)*)?|0[bB][01](?:_?[01])*|0[oO][0-7](?:_?[0-7])*|\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?)(?:i)?)(?![\w.])/'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::NUMBER; + } +} diff --git a/src/Languages/Go/Patterns/GoOperatorPattern.php b/src/Languages/Go/Patterns/GoOperatorPattern.php new file mode 100644 index 00000000..386b65c7 --- /dev/null +++ b/src/Languages/Go/Patterns/GoOperatorPattern.php @@ -0,0 +1,27 @@ +<-|:=|\+\+|--|<<=?|>>=?|&\^=?|&&|\|\||==|!=|<=|>=|\.\.\.|[-+*\/%&|^!<>=]=?|[~.,:;{}()\[\]])/'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::OPERATOR; + } +} diff --git a/src/Languages/Go/Patterns/GoStringPattern.php b/src/Languages/Go/Patterns/GoStringPattern.php new file mode 100644 index 00000000..6c664f77 --- /dev/null +++ b/src/Languages/Go/Patterns/GoStringPattern.php @@ -0,0 +1,28 @@ +`[^`]*`|"(?:\\\\.|[^"\\\\\r\n])*"|\'(?:\\\\.|[^\'\\\\\r\n])+\')/'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::VALUE; + } +} diff --git a/src/Languages/Go/Patterns/GoTypePattern.php b/src/Languages/Go/Patterns/GoTypePattern.php new file mode 100644 index 00000000..22d051ed --- /dev/null +++ b/src/Languages/Go/Patterns/GoTypePattern.php @@ -0,0 +1,27 @@ +(?<=\btype\s)[A-Za-z_]\w*|\b(?:any|bool|byte|comparable|complex64|complex128|error|float32|float64|int|int8|int16|int32|int64|rune|string|uint|uint8|uint16|uint32|uint64|uintptr)\b)'; + } + + public function getTokenType(): TokenTypeEnum + { + return TokenTypeEnum::TYPE; + } +} diff --git a/tests/Bench/Fixtures/go.txt b/tests/Bench/Fixtures/go.txt new file mode 100644 index 00000000..7b2c5599 --- /dev/null +++ b/tests/Bench/Fixtures/go.txt @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "fmt" +) + +type Result[T any] struct { + Value T + Err error +} + +func collect[T comparable](ctx context.Context, input <-chan T) []Result[T] { + results := make([]Result[T], 0, 16) + for value := range input { + select { + case <-ctx.Done(): + return append(results, Result[T]{Err: ctx.Err()}) + default: + results = append(results, Result[T]{Value: value}) + } + } + + fmt.Printf("collected %d values\n", len(results)) + return results +} diff --git a/tests/Bench/HighlighterBench.php b/tests/Bench/HighlighterBench.php index 154b03c3..316c8635 100644 --- a/tests/Bench/HighlighterBench.php +++ b/tests/Bench/HighlighterBench.php @@ -26,6 +26,7 @@ final class HighlighterBench 'dockerfile' => 'dockerfile.txt', 'dotenv' => 'dotenv.txt', 'gdscript' => 'gdscript.txt', + 'go' => 'go.txt', 'graphql' => 'graphql.txt', 'html' => 'html.txt', 'http' => 'http.txt', diff --git a/tests/Languages/Go/GoLanguageTest.php b/tests/Languages/Go/GoLanguageTest.php new file mode 100644 index 00000000..f387bed8 --- /dev/null +++ b/tests/Languages/Go/GoLanguageTest.php @@ -0,0 +1,103 @@ +assertContains('go', $highlighter->getSupportedLanguageNames()); + $this->assertContains('golang', $highlighter->getSupportedLanguageNames()); + + foreach (['go', 'golang'] as $name) { + $highlighter->parse('', $name); + + $this->assertInstanceOf(GoLanguage::class, $highlighter->getCurrentLanguage()); + } + } + + #[Test] + #[DataProvider('provide_it_highlights_representative_syntax_cases')] + public function it_highlights_representative_syntax(string $source, string $expected): void + { + $highlighted = (new Highlighter())->parse($source, 'go'); + + $this->assertStringContainsString($expected, $highlighted); + } + + public static function provide_it_highlights_representative_syntax_cases(): iterable + { + yield 'line comment' => ['// note', '// note']; + yield 'block comment' => ['/* note */', '/* note */']; + yield 'import' => ['import "fmt"', 'import']; + yield 'import path' => ['import "fmt"', '"fmt"']; + yield 'string' => ['"hello\\n"', '"hello\\n"']; + yield 'raw string' => ['`hello`', '`hello`']; + yield 'rune' => ["'λ'", '\'λ\'']; + yield 'hexadecimal float' => ['0x1.fp2', '0x1.fp2']; + yield 'imaginary number' => ['2i', '2i']; + yield 'keyword' => ['defer', 'defer']; + yield 'built-in type' => ['string', 'string']; + yield 'constant' => ['nil', 'nil']; + yield 'built-in function' => ['append(values, value)', 'append']; + yield 'declared type' => ['type Result struct{}', 'Result']; + yield 'declared function' => ['func render() {}', 'render']; + yield 'operator' => ['value <- input', '<-']; + yield 'punctuation' => ['map[string]int{}', '{']; + } + + #[Test] + public function it_escapes_html_and_strips_reserved_markers(): void + { + $source = "package main\r\n" + . "var raw = `<©> ❷img src=x onerror=alert(1)❸ ❶❹❿`\r" + . "var script = \"\"\r\n"; + + $highlighted = (new Highlighter())->parse($source, 'go'); + $renderedSource = html_entity_decode(strip_tags($highlighted), ENT_QUOTES | ENT_HTML5); + + $expectedSource = str_replace(["\r\n", "\r"], "\n", str_replace(['❶', '❷', '❸', '❹', '❿'], '', $source)); + + $this->assertSame($expectedSource, $renderedSource); + $this->assertStringNotContainsString('