Skip to content
Merged
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 @@ -21,6 +21,7 @@
use Tempest\Highlight\Languages\Markdown\MarkdownLanguage;
use Tempest\Highlight\Languages\Php\PhpLanguage;
use Tempest\Highlight\Languages\Python\PythonLanguage;
use Tempest\Highlight\Languages\Scss\ScssLanguage;
use Tempest\Highlight\Languages\Sql\SqlLanguage;
use Tempest\Highlight\Languages\Text\TextLanguage;
use Tempest\Highlight\Languages\Twig\TwigLanguage;
Expand Down Expand Up @@ -64,6 +65,7 @@ public function __construct(private readonly Theme $theme = new CssTheme())
->addLanguage(new MarkdownLanguage())
->addLanguage(new PhpLanguage())
->addLanguage(new PythonLanguage())
->addLanguage(new ScssLanguage())
->addLanguage(new SqlLanguage())
->addLanguage(new XmlLanguage())
->addLanguage(new YamlLanguage())
Expand Down
27 changes: 27 additions & 0 deletions src/Languages/Scss/Patterns/ScssCommentPattern.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Tempest\Highlight\Languages\Scss\Patterns;

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

#[PatternTest(input: '// this is a comment', output: '// this is a comment')]
#[PatternTest(input: 'color: red; // inline comment', output: '// inline comment')]
final readonly class ScssCommentPattern implements Pattern
{
use IsPattern;

public function getPattern(): string
{
return '/(?<match>\/\/.*)/';
}

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

declare(strict_types=1);

namespace Tempest\Highlight\Languages\Scss\Patterns;

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

#[PatternTest(input: 'content: "#{$name}";', output: '#{$name}')]
#[PatternTest(input: '.#{$class}-item {', output: '#{$class}')]
final readonly class ScssInterpolationPattern implements Pattern
{
use IsPattern;

public function getPattern(): string
{
return '(?<match>#\{\$[\w\-]+\})';
}

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

declare(strict_types=1);

namespace Tempest\Highlight\Languages\Scss\Patterns;

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

#[PatternTest(input: '@mixin rounded {', output: '@mixin')]
#[PatternTest(input: '@include rounded;', output: '@include')]
#[PatternTest(input: '@extend .base;', output: '@extend')]
#[PatternTest(input: '@if $dark {', output: '@if')]
#[PatternTest(input: '@each $color in $colors {', output: '@each')]
#[PatternTest(input: "@use 'colors';", output: '@use')]
#[PatternTest(input: '@function double($value) {', output: '@function')]
#[PatternTest(input: '@return $value * 2;', output: '@return')]
final readonly class ScssKeywordPattern implements Pattern
{
use IsPattern;

public function getPattern(): string
{
return '(?<match>@(?:mixin|include|extend|function|return|if|else|for|each|while|use|forward|at-root|debug|warn|error))\b';
}

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

declare(strict_types=1);

namespace Tempest\Highlight\Languages\Scss\Patterns;

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

#[PatternTest(input: '&:hover {', output: '&:hover ')]
#[PatternTest(input: '%placeholder {', output: '%placeholder ')]
#[PatternTest(input: '& .child {', output: '& .child ')]
final readonly class ScssSelectorPattern implements Pattern
{
use IsPattern;

public function getPattern(): string
{
return '(?<match>[\[\]\'\"\=\@\-\#\.\w\s,\n\+\:\(\)\*\&\%]+)\{';
}

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

declare(strict_types=1);

namespace Tempest\Highlight\Languages\Scss\Patterns;

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

#[PatternTest(input: '$primary-color: #333;', output: '$primary-color')]
#[PatternTest(input: 'color: $primary-color;', output: '$primary-color')]
#[PatternTest(input: 'border: 1px solid $border-color;', output: '$border-color')]
final readonly class ScssVariablePattern implements Pattern
{
use IsPattern;

public function getPattern(): string
{
return '(?<match>\$[\w\-]+)';
}

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

declare(strict_types=1);

namespace Tempest\Highlight\Languages\Scss;

use Override;
use Tempest\Highlight\Languages\Css\CssLanguage;
use Tempest\Highlight\Languages\Scss\Patterns\ScssCommentPattern;
use Tempest\Highlight\Languages\Scss\Patterns\ScssInterpolationPattern;
use Tempest\Highlight\Languages\Scss\Patterns\ScssKeywordPattern;
use Tempest\Highlight\Languages\Scss\Patterns\ScssSelectorPattern;
use Tempest\Highlight\Languages\Scss\Patterns\ScssVariablePattern;

class ScssLanguage extends CssLanguage
{
public function getName(): string
{
return 'scss';
}

#[Override]
public function getInjections(): array
{
return [
...parent::getInjections(),
];
}

#[Override]
public function getPatterns(): array
{
return [
...parent::getPatterns(),
new ScssSelectorPattern(),
new ScssCommentPattern(),
new ScssVariablePattern(),
new ScssKeywordPattern(),
new ScssInterpolationPattern(),
];
}
}
100 changes: 100 additions & 0 deletions tests/Bench/Fixtures/scss.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
$primary-color: #3b82f6;
$secondary-color: #6366f1;
$spacing-unit: 0.25rem;
$font-sans: 'Inter', system-ui, -apple-system, sans-serif;
$breakpoint-md: 768px;

@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}

@mixin responsive($breakpoint) {
@media (max-width: $breakpoint) {
@content;
}
}

@function spacing($multiplier) {
@return $spacing-unit * $multiplier;
}

%card-base {
background: white;
border-radius: 0.75rem;
padding: spacing(6);
}

body {
font-family: $font-sans;
line-height: 1.6;
color: #1f2937;
background-color: #f9fafb;
}

.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 spacing(4);
}

.card {
@extend %card-base;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
transition: transform 0.2s ease-in-out;

&:hover {
transform: translateY(-2px);
}

&__title {
font-size: 1.25rem;
color: $primary-color;
}

&__body {
padding: spacing(4);
}
}

.btn {
@include flex-center;
padding: spacing(2) spacing(4);
border-radius: 0.375rem;
cursor: pointer;

&-primary {
background-color: $primary-color;
color: white;

&:hover {
background-color: darken($primary-color, 10%);
}
}
}

// Responsive utilities
@each $size in sm, md, lg {
.hide-#{$size} {
@include responsive($breakpoint-md) {
display: none;
}
}
}

@if $primary-color == #3b82f6 {
.themed {
color: $primary-color;
}
} @else {
.themed {
color: $secondary-color;
}
}

/* Multi-line comment
still works in SCSS */
.fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
1 change: 1 addition & 0 deletions tests/Bench/HighlighterBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ final class HighlighterBench
'markdown' => 'markdown.txt',
'php' => 'php.txt',
'python' => 'python.txt',
'scss' => 'scss.txt',
'sql' => 'sql.txt',
'twig' => 'twig.txt',
'xml' => 'xml.txt',
Expand Down
79 changes: 79 additions & 0 deletions tests/Languages/Scss/ScssLanguageTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Tempest\Highlight\Tests\Languages\Scss;

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

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

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

public static function provide_highlight_cases(): iterable
{
return [
// SCSS variable
[
'$primary: #333;',
'<span class="hl-variable">$primary</span>: #333;',
],
// Single-line comment
[
'// this is a comment',
'<span class="hl-comment">// this is a comment</span>',
],
// Mixin definition
[
'@mixin rounded {',
'<span class="hl-keyword">@mixin rounded </span>{',
],
// @include
[
'@include rounded;',
'<span class="hl-keyword">@include</span> rounded;',
],
// Parent selector (& is HTML-encoded)
[
'&:hover {',
'<span class="hl-keyword">&amp;:hover </span>{',
],
// Placeholder selector
[
'%placeholder {',
'<span class="hl-keyword">%placeholder </span>{',
],
// @each keyword with variables
[
'@each $color in $colors {',
'<span class="hl-keyword">@each</span> <span class="hl-variable">$color</span> in <span class="hl-variable">$colors</span> {',
],
// CSS features still work
[
'@media only screen and (max-width: 500px) {}',
'<span class="hl-keyword">@media only screen and (max-width: 500px) </span>{}',
],
// CSS property
[
'color: $primary;',
'<span class="hl-property">color</span>: <span class="hl-variable">$primary</span>;',
],
// SCSS function
[
'background: darken($color, 10%);',
'<span class="hl-property">background</span>: <span class="hl-keyword">darken</span>(<span class="hl-variable">$color</span>, 10%);',
],
];
}
}
Loading