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
87 changes: 86 additions & 1 deletion docs/commonmark.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,54 @@ echo "Hello, world!"; // [code! f]
echo "Hello, world!"; // [code! **]
```

#### Diff annotations (Insert/Remove)

To show code changes, you can add insert and remove annotations to highlight additions and deletions.

```php
$user = User::find(1); // [code! remove]
$user = User::findOrFail(1); // [code! insert]
```

This will add `insert` and `remove` classes to the corresponding line elements.

If you don't want to write out the full keywords, you can use these shorthands:

```php
$user = User::find(1); // [code! --]
$user = User::findOrFail(1); // [code! ++]
```

You can also use `add`, `del`, or `delete` as alternative keywords for insert and remove respectively.

##### **Gutter diff symbols**

When you have the gutter enabled, diff annotations will automatically replace line numbers with diff symbols:

- Insert lines show `+` instead of the line number
- Remove lines show `-` instead of the line number
- Regular lines continue to show normal line numbers

```php
$environment = new Environment;
$environment
->addExtension(new CommonMarkCoreExtension)
->addExtension(new PhikiExtension(Theme::GithubLight, withGutter: true));
```

This provides a visual indication in the gutter that matches standard diff format conventions.

#### Sample CSS

##### **Highlighted lines**

When you use an inline highlight annotation, Phiki will try to find an `editor.lineHighlightBackground` or `editor.selectionHighlightBackground` color inside of your chosen theme(s) and add a CSS variable to the `<pre>` element.
When you use an inline highlight annotation, Phiki will automatically try to find an `editor.lineHighlightBackground` or `editor.selectionHighlightBackground` color inside of your chosen theme(s) and add a CSS variable to the `<pre>` element.

If it can't find one, it will fallback to the default background color of the theme.

Phiki automatically adds the following CSS variable:
- `--phiki-line-highlight` - Background color for highlighted lines

Those CSS variables are then applied to the line element the same as other styled elements.

<Note>
Expand Down Expand Up @@ -211,3 +251,48 @@ pre.phiki.focus:hover .line {
filter: blur(0);
}
```

##### **Diff annotations**

When you use diff annotations (insert/remove), Phiki will automatically try to find `markup.inserted` and `markup.deleted` colors inside of your chosen theme(s) and add CSS variables to the `<pre>` element.

If it can't find them, it will fallback to the default background color of the theme.

Phiki automatically adds the following CSS variables to the `<pre>` element:

- `--phiki-diff-insert-bg` - Background color for inserted lines
- `--phiki-diff-insert-fg` - Text color for inserted lines
- `--phiki-diff-remove-bg` - Background color for removed lines
- `--phiki-diff-remove-fg` - Text color for removed lines

You can use these variables in your CSS:

```css
.phiki .line.insert {
background-color: var(--phiki-diff-insert-bg);
color: var(--phiki-diff-insert-fg);
}

.phiki .line.remove {
background-color: var(--phiki-diff-remove-bg);
color: var(--phiki-diff-remove-fg);
}
```

<Note>
If you're using multiple themes, Phiki will also add CSS variables for each theme so you can style them differently in dark mode, for example.
</Note>

```css
@media (prefers-color-scheme: dark) {
.phiki .line.insert {
background-color: var(--phiki-dark-diff-insert-bg) !important;
color: var(--phiki-dark-diff-insert-fg) !important;
}

.phiki .line.remove {
background-color: var(--phiki-dark-diff-remove-bg) !important;
color: var(--phiki-dark-diff-remove-fg) !important;
}
}
```
4 changes: 2 additions & 2 deletions meta/commonmark.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
use Phiki\Adapters\CommonMark\PhikiExtension;
use Phiki\Theme\Theme;

require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__.'/../vendor/autoload.php';

$environment = new Environment();
$environment = new Environment;
$environment->addExtension(new CommonMarkCoreExtension)->addExtension(new PhikiExtension(Theme::GithubLight));
$converter = new MarkdownConverter($environment);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ enum AnnotationType
{
case Highlight;
case Focus;
case Insert;
case Remove;

/**
* Get the keywords used to denote this annotation type.
*
*
* e.g. highlight can be denoted by `[code! highlight]`.
*/
public function keywords(): array
{
return match ($this) {
self::Highlight => ['highlight', 'hl', '~~'],
self::Focus => ['focus', 'f', '**'],
self::Insert => ['insert', 'add', '++'],
self::Remove => ['remove', 'del', 'delete', '--'],
};
}

Expand All @@ -28,6 +32,8 @@ public function getLineClasses(): array
return match ($this) {
self::Highlight => ['highlight'],
self::Focus => ['focus'],
self::Insert => ['insert'],
self::Remove => ['remove'],
};
}

Expand All @@ -50,6 +56,8 @@ public static function fromKeyword(string $keyword): self
return match ($keyword) {
'highlight', 'hl', '~~' => self::Highlight,
'focus', 'f', '**' => self::Focus,
'insert', 'add', '++' => self::Insert,
'remove', 'del', 'delete', '--' => self::Remove,
default => throw new \InvalidArgumentException("Unknown annotation keyword: {$keyword}"),
};
}
Expand Down
110 changes: 104 additions & 6 deletions src/Adapters/CommonMark/Transformers/AnnotationsTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
use Phiki\Contracts\RequiresThemesInterface;
use Phiki\Grammar\Grammar;
use Phiki\Phast\Element;
use Phiki\Phast\Text;
use Phiki\Support\Arr;
use Phiki\Theme\ThemeColorExtractor;
use Phiki\Transformers\AbstractTransformer;
use Phiki\Transformers\Concerns\RequiresGrammar;
use Phiki\Transformers\Concerns\RequiresThemes;
Expand Down Expand Up @@ -40,15 +42,15 @@ class AnnotationsTransformer extends AbstractTransformer implements RequiresGram

/**
* The collected list of annotations.
*
*
* @var array<int, array<Annotation>>
*/
protected array $annotations = [];

/**
* Create a new instance.
*
* @param string $prefix The prefix used to denote annotations, e.g. `code` for `[code! highlight]`.
*
* @param string $prefix The prefix used to denote annotations, e.g. `code` for `[code! highlight]`.
*/
public function __construct(protected string $prefix = 'code') {}

Expand Down Expand Up @@ -120,10 +122,10 @@ public function preprocess(string $code): string
// We store a list of these in a constant:
// - strings are characters for line comments
// - arrays are beginning and ending comment pairs (block comments)
[$l, $b] = Arr::partition($commentChars, fn(string|array $chars) => is_string($chars));
[$l, $b] = Arr::partition($commentChars, fn (string|array $chars) => is_string($chars));

// We'll first check for line comments.
$processedLineCommentRegex = sprintf(self::DANGLING_LINE_COMMENT_REGEX, implode('|', array_map(fn(string $char) => preg_quote($char, '/'), $l)));
$processedLineCommentRegex = sprintf(self::DANGLING_LINE_COMMENT_REGEX, implode('|', array_map(fn (string $char) => preg_quote($char, '/'), $l)));

// If we find a match, we can set the cutoff point and skip checking for block comments.
if (preg_match($processedLineCommentRegex, $trimmed, $lineCommentMatches, PREG_OFFSET_CAPTURE) === 1) {
Expand All @@ -133,7 +135,7 @@ public function preprocess(string $code): string

$processedBlockCommentRegex = sprintf(
'/%s$/',
implode('|', array_map(fn(array $chars) => sprintf('(%s\s*%s)', preg_quote($chars[0], '/'), preg_quote($chars[1], '/')), $b)),
implode('|', array_map(fn (array $chars) => sprintf('(%s\s*%s)', preg_quote($chars[0], '/'), preg_quote($chars[1], '/')), $b)),
);

// If we find a match, we can set the cutoff point.
Expand Down Expand Up @@ -199,6 +201,9 @@ public function pre(Element $pre): Element
}
}

// Add CSS variables for theme colors
$this->addThemeColorVariables($pre);

return $pre;
}

Expand All @@ -214,4 +219,97 @@ public function line(Element $span, array $tokens, int $index): Element

return $span;
}

public function gutter(Element $span, int $index): Element
{
if ($this->annotations === [] || ! isset($this->annotations[$index])) {
return $span;
}

// Check if this line has diff annotations
foreach ($this->annotations[$index] as $annotation) {
if ($annotation->type === AnnotationType::Insert) {
// Replace the line number with '+' symbol
$span->children = [new Text(' +')];
break;
} elseif ($annotation->type === AnnotationType::Remove) {
// Replace the line number with '-' symbol
$span->children = [new Text(' -')];
break;
}
}

return $span;
}

/**
* Add CSS variables for theme colors to the pre element.
*/
protected function addThemeColorVariables(Element $pre): void
{
if (empty($this->themes)) {
return;
}

$style = $pre->properties->get('style') ?? '';
$cssVariables = [];

// Collect all annotation types used
$usedTypes = [];
foreach ($this->annotations as $annotations) {
foreach ($annotations as $annotation) {
if (! in_array($annotation->type, $usedTypes, true)) {
$usedTypes[] = $annotation->type;
}
}
}

// Process each theme
foreach ($this->themes as $themeName => $theme) {
$extractor = new ThemeColorExtractor($theme);
$prefix = count($this->themes) > 1 ? "--phiki-{$themeName}-" : '--phiki-';

// Add CSS variables for each annotation type used
foreach ($usedTypes as $type) {
switch ($type) {
case AnnotationType::Highlight:
$colors = $extractor->getColorForType('highlight');
if ($colors && ! empty($colors['background'])) {
$cssVariables[] = "{$prefix}line-highlight: {$colors['background']}";
}
break;

case AnnotationType::Insert:
$colors = $extractor->getColorForType('insert');
if ($colors) {
if (! empty($colors['background'])) {
$cssVariables[] = "{$prefix}diff-insert-bg: {$colors['background']}";
}
if (! empty($colors['foreground'])) {
$cssVariables[] = "{$prefix}diff-insert-fg: {$colors['foreground']}";
}
}
break;

case AnnotationType::Remove:
$colors = $extractor->getColorForType('remove');
if ($colors) {
if (! empty($colors['background'])) {
$cssVariables[] = "{$prefix}diff-remove-bg: {$colors['background']}";
}
if (! empty($colors['foreground'])) {
$cssVariables[] = "{$prefix}diff-remove-fg: {$colors['foreground']}";
}
}
break;
}
}
}

if (! empty($cssVariables)) {
$newStyle = implode('; ', $cssVariables);
$style = $style ? "$style; $newStyle" : $newStyle;
$pre->properties->set('style', $style);
}
}
}
4 changes: 2 additions & 2 deletions src/Contracts/RequiresThemesInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ interface RequiresThemesInterface
{
/**
* Set the parsed themes for the transformer.
*
* @param array<string, ParsedTheme> $themes
*
* @param array<string, ParsedTheme> $themes
*/
public function withThemes(array $themes): void;
}
2 changes: 1 addition & 1 deletion src/Phiki.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public function grammar(string $name, string|ParsedGrammar $pathOrGrammar): stat
return $this;
}

public function alias(string $alias, string | Grammar $for): static
public function alias(string $alias, string|Grammar $for): static
{
$this->environment->grammars->alias($alias, $for instanceof Grammar ? $for->value : $for);

Expand Down
Loading