Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Highlighter.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Tempest\Highlight\Languages\Terraform\TerraformLanguage;
use Tempest\Highlight\Languages\Text\TextLanguage;
use Tempest\Highlight\Languages\Twig\TwigLanguage;
use Tempest\Highlight\Languages\TypeScript\TypeScriptLanguage;
use Tempest\Highlight\Languages\Xml\XmlLanguage;
use Tempest\Highlight\Languages\Yaml\YamlLanguage;
use Tempest\Highlight\Themes\CssTheme;
Expand Down Expand Up @@ -79,6 +80,7 @@ public function __construct(private readonly Theme $theme = new CssTheme())
->addLanguage(new SqlLanguage())
->addLanguage(new TerminalLanguage())
->addLanguage(new TerraformLanguage())
->addLanguage(new TypeScriptLanguage())
->addLanguage(new XmlLanguage())
->addLanguage(new YamlLanguage())
->addLanguage(new DotEnvLanguage())
Expand Down
24 changes: 24 additions & 0 deletions src/Languages/TypeScript/Patterns/TsBuiltInTypePattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Tempest\Highlight\Languages\TypeScript\Patterns;

use Tempest\Highlight\IsPattern;
use Tempest\Highlight\Pattern;
use Tempest\Highlight\Tokens\TokenTypeEnum;

final class TsBuiltInTypePattern implements Pattern
{
use IsPattern;

public function getPattern(): string
{
return '/\b(?<!\.)(?<match>string|number|boolean|any|unknown|never|bigint|symbol|object|undefined)\b/';
}

public function getTokenType(): TokenTypeEnum
{
return TokenTypeEnum::TYPE;
}
}
24 changes: 24 additions & 0 deletions src/Languages/TypeScript/Patterns/TsDecoratorPattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Tempest\Highlight\Languages\TypeScript\Patterns;

use Tempest\Highlight\IsPattern;
use Tempest\Highlight\Pattern;
use Tempest\Highlight\Tokens\TokenTypeEnum;

final class TsDecoratorPattern implements Pattern
{
use IsPattern;

public function getPattern(): string
{
return '/(?<match>@[A-Za-z_][\w]*)/';
}

public function getTokenType(): TokenTypeEnum
{
return TokenTypeEnum::ATTRIBUTE;
}
}
24 changes: 24 additions & 0 deletions src/Languages/TypeScript/Patterns/TsGenericPattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Tempest\Highlight\Languages\TypeScript\Patterns;

use Tempest\Highlight\IsPattern;
use Tempest\Highlight\Pattern;
use Tempest\Highlight\Tokens\TokenTypeEnum;

final class TsGenericPattern implements Pattern
{
use IsPattern;

public function getPattern(): string
{
return '/(?<=\w)(?<match><[A-Z][\w\s,\.\[\]]*>)/';
}

public function getTokenType(): TokenTypeEnum
{
return TokenTypeEnum::GENERIC;
}
}
24 changes: 24 additions & 0 deletions src/Languages/TypeScript/Patterns/TsTypeAnnotationPattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Tempest\Highlight\Languages\TypeScript\Patterns;

use Tempest\Highlight\IsPattern;
use Tempest\Highlight\Pattern;
use Tempest\Highlight\Tokens\TokenTypeEnum;

final class TsTypeAnnotationPattern implements Pattern
{
use IsPattern;

public function getPattern(): string
{
return '/(?<=[\w\)\]])\s*:\s*(?<match>[A-Z][\w]*(?:\.[A-Z][\w]*)*(?:\[\])?)/';
}

public function getTokenType(): TokenTypeEnum
{
return TokenTypeEnum::TYPE;
}
}
46 changes: 46 additions & 0 deletions src/Languages/TypeScript/TypeScriptLanguage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Tempest\Highlight\Languages\TypeScript;

use Override;
use Tempest\Highlight\Languages\JavaScript\JavaScriptLanguage;
use Tempest\Highlight\Languages\JavaScript\Patterns\CombinedJsKeywordPattern;
use Tempest\Highlight\Languages\TypeScript\Patterns\TsBuiltInTypePattern;
use Tempest\Highlight\Languages\TypeScript\Patterns\TsDecoratorPattern;
use Tempest\Highlight\Languages\TypeScript\Patterns\TsGenericPattern;
use Tempest\Highlight\Languages\TypeScript\Patterns\TsTypeAnnotationPattern;

class TypeScriptLanguage extends JavaScriptLanguage
{
private const array TS_KEYWORDS = [
'type', 'declare', 'readonly', 'namespace', 'keyof', 'infer',
'satisfies', 'abstract', 'is', 'module', 'override', 'asserts',
];

#[Override]
public function getName(): string
{
return 'ts';
}

#[Override]
public function getAliases(): array
{
return ['typescript'];
}

#[Override]
public function getPatterns(): array
{
return [
...parent::getPatterns(),
new CombinedJsKeywordPattern(self::TS_KEYWORDS),
new TsBuiltInTypePattern(),
new TsTypeAnnotationPattern(),
new TsDecoratorPattern(),
new TsGenericPattern(),
];
}
}
57 changes: 57 additions & 0 deletions tests/Bench/Fixtures/typescript.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* User service with basic CRUD operations.
*/
import { Injectable } from '@angular/core';

interface User {
id: number;
name: string;
email: string;
roles: readonly string[];
}

type UserId = number;

enum Role {
Admin = 'admin',
Member = 'member',
Guest = 'guest',
}

@Injectable({ providedIn: 'root' })
export class UserService<T extends User> {
private readonly users: Map<UserId, T> = new Map();

constructor(private readonly apiUrl: string) {}

public async findById(id: UserId): Promise<T | undefined> {
const user = this.users.get(id);
if (user === undefined) {
return undefined;
}
return user;
}

public async create(data: Omit<T, 'id'>): Promise<T> {
const id: number = Math.floor(Math.random() * 1_000_000);
const user = { id, ...data } as T;
this.users.set(id, user);
return user;
}

public delete(id: UserId): boolean {
return this.users.delete(id);
}

public list(): readonly T[] {
return Array.from(this.users.values());
}
}

const service = new UserService<User>('https://api.example.com');
const admin: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
roles: ['admin'],
};
1 change: 1 addition & 0 deletions tests/Bench/HighlighterBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ final class HighlighterBench
'sql' => 'sql.txt',
'terminal' => 'terminal.txt',
'terraform' => 'terraform.txt',
'typescript' => 'typescript.txt',
'twig' => 'twig.txt',
'xml' => 'xml.txt',
'yaml' => 'yaml.txt',
Expand Down
112 changes: 112 additions & 0 deletions tests/Languages/TypeScript/TypeScriptLanguageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types=1);

namespace Tempest\Highlight\Tests\Languages\TypeScript;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Tempest\Highlight\Highlighter;

class TypeScriptLanguageTest extends TestCase
{
#[DataProvider('provide_highlight_cases')]
public function test_highlight(string $content, string $expected): void
{
$highlighter = new Highlighter();

$this->assertSame(
$expected,
$highlighter->parse($content, 'ts'),
);

$this->assertSame(
$expected,
$highlighter->parse($content, 'typescript'),
);
}

public static function provide_highlight_cases(): iterable
{
return [
[
'type Alias = string;',
'<span class="hl-keyword">type</span> Alias = <span class="hl-type">string</span>;',
],
[
<<<'TXT'
interface User {
id: number;
name: string;
}
TXT,
<<<'TXT'
<span class="hl-keyword">interface</span> User {
<span class="hl-property">id</span>: <span class="hl-type">number</span>;
<span class="hl-property">name</span>: <span class="hl-type">string</span>;
}
TXT,
],
[
'const x: boolean = true;',
'<span class="hl-keyword">const</span> <span class="hl-property">x</span>: <span class="hl-type">boolean</span> = <span class="hl-keyword">true</span>;',
],
[
'readonly name: string;',
'<span class="hl-keyword">readonly</span> <span class="hl-property">name</span>: <span class="hl-type">string</span>;',
],
[
'function greet(name: string): void {}',
'<span class="hl-keyword">function</span> <span class="hl-property">greet</span>(<span class="hl-property">name</span>: <span class="hl-type">string</span>): <span class="hl-keyword">void</span> {}',
],
[
'let u: User = getUser();',
'<span class="hl-keyword">let</span> <span class="hl-property">u</span>: <span class="hl-type">User</span> = <span class="hl-property">getUser</span>();',
],
[
'function identity<T>(v: T): T {}',
'<span class="hl-keyword">function</span> identity<span class="hl-generic">&lt;T&gt;</span>(<span class="hl-property">v</span>: <span class="hl-type">T</span>): <span class="hl-type">T</span> {}',
],
[
'class Service<K, V extends Base> {}',
'<span class="hl-keyword">class</span> <span class="hl-type">Service</span><span class="hl-generic">&lt;K, V <span class="hl-keyword">extends</span> Base&gt;</span> {}',
],
[
<<<'TXT'
@Component({ selector: 'x' })
class Foo {}
TXT,
<<<'TXT'
<span class="hl-attribute">@<span class="hl-property">Component</span></span>({ <span class="hl-property">selector</span>: <span class="hl-value">'x'</span> })
<span class="hl-keyword">class</span> <span class="hl-type">Foo</span> {}
TXT,
],
[
<<<'TXT'
@Injectable
export class Bar {}
TXT,
<<<'TXT'
<span class="hl-attribute">@Injectable</span>
<span class="hl-keyword">export</span> <span class="hl-keyword">class</span> <span class="hl-type">Bar</span> {}
TXT,
],
[
<<<'TXT'
/**
* Greet a user.
* @param {string} name
*/
function greet(name: string): void {}
TXT,
<<<'TXT'
<span class="hl-comment">/**
* Greet a user.
* <span class="hl-value">@param</span> <span class="hl-type">{string}</span> <span class="hl-value">name</span>
*/</span>
<span class="hl-keyword">function</span> <span class="hl-property">greet</span>(<span class="hl-property">name</span>: <span class="hl-type">string</span>): <span class="hl-keyword">void</span> {}
TXT,
],
];
}
}
Loading