Creating a theme
A theme implements ThemeInterface. It maps Alto's semantic scopes to CSS
class names and returns the stylesheet that defines those classes.
Complete theme
The following theme covers every current Scope, styles the highlighter
container, and includes the structural line-number classes:
<?php
declare(strict_types=1);
namespace App\Highlight;
use Alto\Code\Highlight\Scope;
use Alto\Code\Highlight\ThemeInterface;
final class OceanTheme implements ThemeInterface
{
public function getName(): string
{
return 'Ocean Dark';
}
public function isDark(): bool
{
return true;
}
public function getCssClasses(): array
{
$classes = [];
foreach (Scope::cases() as $scope) {
$classes[$scope->value] = match ($scope) {
Scope::Comment,
Scope::CommentDocblock,
Scope::CommentTask => 'ocean-comment',
Scope::Keyword,
Scope::KeywordDeclaration,
Scope::KeywordOperator,
Scope::KeywordControl,
Scope::StorageModifier => 'ocean-keyword',
Scope::String,
Scope::StringInterpolated,
Scope::StringTemplateExpression,
Scope::RegExp,
Scope::AttributeValue,
Scope::TagAttributeValue => 'ocean-string',
Scope::Number,
Scope::Boolean,
Scope::Null,
Scope::Constant,
Scope::BuiltInConstant,
Scope::EnumCase,
Scope::SupportConstant => 'ocean-literal',
Scope::Variable,
Scope::VariableParameter,
Scope::VariableProperty,
Scope::VariableThis => 'ocean-variable',
Scope::Namespace,
Scope::TypeDefinition,
Scope::TypeReference,
Scope::BuiltInType,
Scope::SupportType => 'ocean-type',
Scope::FunctionDefinition,
Scope::FunctionCall,
Scope::FunctionBuiltin,
Scope::SupportFunction => 'ocean-function',
Scope::AttributeName,
Scope::TagAttributeName => 'ocean-attribute',
Scope::TagName,
Scope::SectionName => 'ocean-tag',
Scope::DiffAdded => 'ocean-added',
Scope::DiffRemoved,
Scope::DiagnosticError => 'ocean-error',
Scope::DiffChanged,
Scope::DiagnosticWarning => 'ocean-warning',
Scope::DiagnosticInfo => 'ocean-info',
Scope::Meta => 'ocean-meta',
Scope::Operator => 'ocean-operator',
Scope::Punctuation => 'ocean-punctuation',
Scope::MarkupText => 'ocean-markup',
Scope::Whitespace => 'ocean-default',
};
}
return $classes;
}
public function getStylesheet(): string
{
return <<<'CSS'
.alto-highlight {
margin: 0;
padding: 1rem;
overflow-x: auto;
border-radius: 0.5rem;
background: #0b1724;
color: #d8e6f3;
font: 0.875rem/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
tab-size: 4;
}
.alto-highlight code {
font: inherit;
}
.alto-highlight .ocean-default,
.alto-highlight .ocean-punctuation,
.alto-highlight .ocean-markup { color: #d8e6f3; }
.alto-highlight .ocean-comment { color: #7890a6; font-style: italic; }
.alto-highlight .ocean-keyword { color: #c792ea; }
.alto-highlight .ocean-string { color: #addb67; }
.alto-highlight .ocean-literal { color: #f78c6c; }
.alto-highlight .ocean-variable { color: #82aaff; }
.alto-highlight .ocean-type { color: #ffcb6b; }
.alto-highlight .ocean-function { color: #7fdbca; }
.alto-highlight .ocean-attribute { color: #89ddff; }
.alto-highlight .ocean-tag { color: #f07178; }
.alto-highlight .ocean-added { color: #addb67; }
.alto-highlight .ocean-error { color: #ff5370; }
.alto-highlight .ocean-warning { color: #ffcb6b; }
.alto-highlight .ocean-info { color: #82aaff; }
.alto-highlight .ocean-meta { color: #c792ea; }
.alto-highlight .ocean-operator { color: #89ddff; }
.alto-highlight .alto-line-number {
display: inline-block;
width: 3rem;
color: #60788e;
user-select: none;
}
.alto-highlight .alto-line-number.alto-highlighted {
color: #ffcb6b;
font-weight: 700;
}
CSS;
}
}Use it like a built-in theme:
<?php
use Alto\Code\Highlight\Highlighter;
use App\Highlight\OceanTheme;
$theme = new OceanTheme();
$highlighter = new Highlighter($theme);
echo '<style>'.$theme->getStylesheet().'</style>';
echo $highlighter->highlight($code, 'php', lineNumbers: true);Semantic scopes, not lexer tokens
A lexer token records an exact piece of source text. A semantic scope records
the role that text plays across languages. For example, PHP, Go, and Python
parsers can all emit Scope::FunctionDefinition even though their lexical
rules differ. Themes style the shared semantic role and do not need to know
which parser produced it.
Keep token class names theme-specific. A prefix such as ocean- avoids
collisions with application classes and lets two theme stylesheets coexist
without accidentally restyling generic names such as .string or .keyword.
Verify the implementation
At minimum, test these contracts:
<?php
$theme = new OceanTheme();
$classes = $theme->getCssClasses();
foreach (Scope::cases() as $scope) {
assert(isset($classes[$scope->value]));
}
$highlighter = new Highlighter($theme);
$html = $highlighter->highlight(
'<?php echo "<script>";',
'php',
lineNumbers: true,
highlightLines: [1],
);
assert(str_contains($html, '<?php'));
assert(str_contains($html, '<script>'));
assert(str_contains($html, 'alto-line-number'));
assert(str_contains($html, 'alto-highlighted'));Also review the theme in a browser:
- confirm readable contrast for every semantic color;
- test long lines and
.alto-highlighthorizontal overflow; - test line numbers and selected-line numbers;
- test markup, strings, comments, definitions, calls, and diff scopes;
- confirm that source text reconstructs exactly after stripping generated markup.
The canonical samples in Examples provide stable inputs for visual review.