Skip to content

Commit 8f46ed8

Browse files
committed
get close to release
Signed-off-by: Andy Miller <rhuk@mac.com>
1 parent b6cdae3 commit 8f46ed8

File tree

3 files changed

+314
-1
lines changed

3 files changed

+314
-1
lines changed

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# v1.0.0
2+
## 12/14/2024
3+
4+
1. [](#new)
5+
* Initial release of CodeSh plugin
6+
* Server-side syntax highlighting using [Phiki](https://phiki.dev) (PHP port of Shiki)
7+
* Support for 245+ languages via TextMate grammars
8+
* Support for 60+ VS Code themes out of the box
9+
* Automatic light/dark theme switching based on site theme mode
10+
* CSS variable-based dual-theme support for system preference detection
11+
* Custom `[codesh]` shortcode for inline code blocks
12+
* Automatic markdown fenced code block processing (configurable)
13+
* Line numbers with optional custom start line
14+
* Line highlighting with `highlight` or `hl` attribute
15+
* Line focus to dim non-focused lines with `focus` attribute
16+
* Title/filename display in header with `title` attribute
17+
* Language badge with `show-lang` attribute
18+
* Minimal mode to hide header with `header="false"`
19+
* Copy button with visual feedback
20+
* Per-block theme override with `theme` attribute
21+
* `[codesh-group]` shortcode for tabbed code examples
22+
* Tab synchronization across groups with `sync` attribute
23+
* LocalStorage persistence for tab preferences
24+
* Twig filter `codesh` for template-based highlighting with caching
25+
* Admin theme gallery at Admin > CodeSh Themes
26+
* Theme picker field with modal selector and visual previews
27+
* Theme import for VS Code compatible JSON themes
28+
* Theme deletion with confirmation dialog
29+
* Custom `helios-dark` and `helios-light` themes

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,51 @@ Codesh automatically detects your theme's light/dark mode setting:
259259

260260
When you specify an explicit `theme` attribute on a code block, that theme is used regardless of mode.
261261

262+
## Theme Management
263+
264+
The plugin includes a full-featured theme management system in the Grav Admin panel.
265+
266+
### Theme Picker
267+
268+
The plugin settings page features a custom theme picker for selecting dark and light themes:
269+
270+
- **Visual Preview** - Each theme shows a live syntax-highlighted code preview
271+
- **Search** - Filter themes by name
272+
- **Filters** - Filter by All, Dark, Light, or Custom themes
273+
- **One-Click Selection** - Click any theme card to select it
274+
275+
### Theme Gallery
276+
277+
Access the full theme gallery via **Admin > CodeSh Themes** in the sidebar. This provides a read-only view of all available themes with full-size previews.
278+
279+
### Importing Custom Themes
280+
281+
Import VS Code compatible themes directly from JSON files:
282+
283+
1. Click the **Import** button in the theme picker modal
284+
2. Select a `.json` theme file (VS Code theme format)
285+
3. The theme is automatically:
286+
- Validated for required structure
287+
- Normalized (short hex colors like `#fff` are expanded to `#ffffff`)
288+
- Type-detected (light/dark) if not specified
289+
- Saved to `user/data/codesh/themes/`
290+
291+
Imported themes appear with a **Custom** badge and can be filtered using the Custom filter.
292+
293+
### Deleting Custom Themes
294+
295+
Custom/imported themes can be deleted:
296+
297+
1. Click the red trash icon on any custom theme card
298+
2. Confirm the deletion in the dialog
299+
300+
Note: Built-in themes cannot be deleted.
301+
302+
### Theme Storage
303+
304+
- **Built-in themes**: `user/plugins/codesh/themes/`
305+
- **Custom themes**: `user/data/codesh/themes/`
306+
262307
## Available Themes (60+)
263308

264309
### Dark Themes

codesh.php

Lines changed: 240 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,12 +595,251 @@ public function onShortcodeHandlers(Event $e): void
595595
}
596596

597597
/**
598-
* Add CSS and JS assets
598+
* Add CSS and JS assets and Twig extensions
599599
*/
600600
public function onTwigSiteVariables(): void
601601
{
602602
$this->grav['assets']->addCss('plugin://codesh/css/codesh.css');
603603
$this->grav['assets']->addJs('plugin://codesh/js/codesh.js', ['group' => 'bottom', 'defer' => true]);
604+
605+
// Add Twig filter for use in templates
606+
$twig = $this->grav['twig']->twig();
607+
$twig->addFilter(new \Twig\TwigFilter('codesh', [$this, 'codeshFilter'], ['is_safe' => ['html']]));
608+
}
609+
610+
/**
611+
* Twig filter to highlight code using codesh
612+
*
613+
* Usage in templates:
614+
* {{ code_string|codesh('json') }}
615+
* {{ code_string|codesh('php', {title: 'example.php', 'line-numbers': true}) }}
616+
*
617+
* @param string $content The code to highlight
618+
* @param string $lang The language (default: 'txt')
619+
* @param array $options Options: theme, line-numbers, start, highlight, focus, class, show-lang, title, header
620+
* @return string The highlighted HTML
621+
*/
622+
public function codeshFilter(string $content, string $lang = 'txt', array $options = []): string
623+
{
624+
$mergedOptions = array_merge([
625+
'theme' => $options['theme'] ?? null,
626+
'line-numbers' => $options['line-numbers'] ?? false,
627+
'start' => $options['start'] ?? 1,
628+
'highlight' => $options['highlight'] ?? '',
629+
'focus' => $options['focus'] ?? '',
630+
'class' => $options['class'] ?? '',
631+
'show-lang' => $options['show-lang'] ?? true,
632+
'title' => $options['title'] ?? '',
633+
'header' => $options['header'] ?? true,
634+
], $options);
635+
636+
// Generate cache key based on content, language, options, and theme
637+
$themeConfig = $this->config->get('themes.helios.appearance.theme', 'system');
638+
$cacheKey = 'codesh_' . md5($content . $lang . serialize($mergedOptions) . $themeConfig);
639+
640+
// Try to get from cache
641+
$cache = $this->grav['cache'];
642+
$cached = $cache->fetch($cacheKey);
643+
644+
if ($cached !== false) {
645+
return $cached;
646+
}
647+
648+
// Generate highlighted code
649+
$result = $this->highlightCodeFull($content, $lang, $mergedOptions);
650+
651+
// Cache the result (1 hour TTL)
652+
$cache->save($cacheKey, $result, 3600);
653+
654+
return $result;
655+
}
656+
657+
/**
658+
* Highlight code using Phiki (shared implementation for filter and page processing)
659+
*/
660+
protected function highlightCodeFull(string $code, string $lang, array $options): string
661+
{
662+
$config = $this->config->get('plugins.codesh');
663+
664+
// Detect theme mode from Helios theme config
665+
$themeConfig = $this->config->get('themes.helios.appearance.theme', 'system');
666+
667+
// Use custom helios themes by default (with diff backgrounds)
668+
$themeDark = $config['theme_dark'] ?? 'helios-dark';
669+
$themeLight = $config['theme_light'] ?? 'helios-light';
670+
671+
// Get theme - explicit theme parameter overrides mode-based themes
672+
$explicitTheme = $options['theme'] ?? null;
673+
if ($explicitTheme) {
674+
$theme = $explicitTheme;
675+
} elseif ($themeConfig === 'system') {
676+
$theme = [
677+
'light' => $themeLight,
678+
'dark' => $themeDark,
679+
];
680+
} else {
681+
$theme = ($themeConfig === 'dark') ? $themeDark : $themeLight;
682+
}
683+
684+
$lineNumbers = $this->toBool($options['line-numbers'] ?? $config['show_line_numbers'] ?? false);
685+
$startLine = (int) ($options['start'] ?? 1);
686+
$highlight = $options['highlight'] ?? '';
687+
$focus = $options['focus'] ?? '';
688+
$class = $options['class'] ?? '';
689+
$showLang = $this->toBool($options['show-lang'] ?? true);
690+
$title = $options['title'] ?? '';
691+
$showHeader = $this->toBool($options['header'] ?? true);
692+
693+
// Clean up the content
694+
$code = html_entity_decode($code, ENT_QUOTES | ENT_HTML5, 'UTF-8');
695+
$code = trim($code, "\n\r");
696+
697+
if (empty(trim($code))) {
698+
return '';
699+
}
700+
701+
try {
702+
$phiki = $this->getPhiki();
703+
$output = $phiki->codeToHtml($code, strtolower($lang), $theme);
704+
705+
// Add 'no-highlight' class to prevent Prism.js from reprocessing
706+
$output = $output->decoration(
707+
PreDecoration::make()->class('no-highlight')
708+
);
709+
710+
// Add line numbers if enabled
711+
if ($lineNumbers) {
712+
$output = $output->withGutter();
713+
if ($startLine !== 1) {
714+
$output = $output->startingLine($startLine);
715+
}
716+
}
717+
718+
// Add line decorations for highlighting
719+
if (!empty($highlight)) {
720+
$highlightLines = $this->parseLineSpec($highlight);
721+
foreach ($highlightLines as $line) {
722+
$output = $output->decoration(
723+
\Phiki\Transformers\Decorations\LineDecoration::forLine($line - 1)->class('highlight')
724+
);
725+
}
726+
}
727+
728+
// Add line decorations for focus
729+
if (!empty($focus)) {
730+
$focusLines = $this->parseLineSpec($focus);
731+
foreach ($focusLines as $line) {
732+
$output = $output->decoration(
733+
\Phiki\Transformers\Decorations\LineDecoration::forLine($line - 1)->class('focus')
734+
);
735+
}
736+
}
737+
738+
$html = $output->toString();
739+
740+
// Wrap in container with optional class
741+
$classes = ['codesh-block'];
742+
if (is_array($theme)) {
743+
$classes[] = 'codesh-dual-theme';
744+
}
745+
if (!empty($class)) {
746+
$classes[] = htmlspecialchars($class);
747+
}
748+
if (!empty($highlight)) {
749+
$classes[] = 'has-highlights';
750+
}
751+
if (!empty($focus)) {
752+
$classes[] = 'has-focus';
753+
}
754+
if (!$showHeader) {
755+
$classes[] = 'no-header';
756+
}
757+
758+
// Build the complete HTML output
759+
$result = '<div class="' . implode(' ', $classes) . '" data-language="' . htmlspecialchars($lang) . '">';
760+
761+
// Add header with language/title and copy button
762+
if ($showHeader) {
763+
$result .= '<div class="codesh-header">';
764+
765+
// Display title or language
766+
if (!empty($title)) {
767+
$result .= '<span class="codesh-title">' . htmlspecialchars($title) . '</span>';
768+
} elseif ($showLang && !empty($lang)) {
769+
$result .= '<span class="codesh-lang">' . htmlspecialchars(strtoupper($lang)) . '</span>';
770+
} else {
771+
$result .= '<span class="codesh-lang"></span>';
772+
}
773+
774+
// Copy button
775+
$result .= '<button class="codesh-copy" type="button" title="Copy code">';
776+
$result .= '<svg class="codesh-copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">';
777+
$result .= '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>';
778+
$result .= '</svg>';
779+
$result .= '<span class="codesh-copy-text">Copy</span>';
780+
$result .= '</button>';
781+
782+
$result .= '</div>';
783+
}
784+
785+
// Add the code
786+
$result .= '<div class="codesh-code">' . $html . '</div>';
787+
$result .= '</div>';
788+
789+
return $result;
790+
791+
} catch (\Exception $e) {
792+
// Fallback to plain text on error
793+
return '<div class="codesh-block codesh-error" data-error="' . htmlspecialchars($e->getMessage()) . '"><pre><code>' . htmlspecialchars($code) . '</code></pre></div>';
794+
}
795+
}
796+
797+
/**
798+
* Parse line specification like "1,3-5,7" into array of line numbers
799+
*/
800+
protected function parseLineSpec(string $spec): array
801+
{
802+
$lines = [];
803+
$parts = explode(',', $spec);
804+
805+
foreach ($parts as $part) {
806+
$part = trim($part);
807+
if (empty($part)) {
808+
continue;
809+
}
810+
811+
if (str_contains($part, '-')) {
812+
[$start, $end] = explode('-', $part, 2);
813+
$start = (int) trim($start);
814+
$end = (int) trim($end);
815+
if ($start > 0 && $end >= $start) {
816+
for ($i = $start; $i <= $end; $i++) {
817+
$lines[] = $i;
818+
}
819+
}
820+
} else {
821+
$lineNum = (int) $part;
822+
if ($lineNum > 0) {
823+
$lines[] = $lineNum;
824+
}
825+
}
826+
}
827+
828+
return array_unique($lines);
829+
}
830+
831+
/**
832+
* Convert various values to boolean
833+
*/
834+
protected function toBool($value): bool
835+
{
836+
if (is_bool($value)) {
837+
return $value;
838+
}
839+
if (is_string($value)) {
840+
return in_array(strtolower($value), ['true', '1', 'yes', 'on'], true);
841+
}
842+
return (bool) $value;
604843
}
605844

606845
/**

0 commit comments

Comments
 (0)