Skip to content

Commit 5a81252

Browse files
committed
Add snippet component
1 parent cdbce22 commit 5a81252

File tree

8 files changed

+345
-8
lines changed

8 files changed

+345
-8
lines changed

app/Http/Controllers/ShowDocumentationController.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ protected function getPageProperties($platform, $version, $page = null): array
101101
$pageProperties['version'] = $version;
102102
$pageProperties['pagePath'] = request()->path();
103103

104-
$pageProperties['content'] = CommonMark::convertToHtml($document->body());
104+
$pageProperties['content'] = CommonMark::convertToHtml($document->body(), [
105+
'user' => auth()->user(),
106+
]);
105107
$pageProperties['tableOfContents'] = $this->extractTableOfContents($document->body());
106108

107109
$navigation = $this->getNavigation($platform, $version);
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
3+
namespace App\Support\CommonMark;
4+
5+
use Illuminate\Support\Facades\Blade;
6+
use League\CommonMark\Environment\Environment;
7+
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
8+
use League\CommonMark\MarkdownConverter;
9+
use Torchlight\Commonmark\V2\TorchlightExtension;
10+
11+
class BladeMarkdownPreprocessor
12+
{
13+
/**
14+
* Process markdown content to render Blade components before markdown parsing.
15+
*
16+
* This extracts code blocks first (to prevent them from being processed),
17+
* renders any Blade components in the remaining content, then restores
18+
* the code blocks. Code blocks that end up inside HTML elements are
19+
* pre-converted to HTML to ensure proper syntax highlighting.
20+
*/
21+
public static function process(string $markdown, array $data = []): string
22+
{
23+
$codeBlocks = [];
24+
$placeholder = '___CODE_BLOCK_PLACEHOLDER_%d___';
25+
26+
// Extract fenced code blocks (``` or ~~~)
27+
$markdown = preg_replace_callback(
28+
'/^(`{3,}|~{3,})([^\n]*)\n(.*?)\n\1/ms',
29+
function ($matches) use (&$codeBlocks, $placeholder) {
30+
$index = count($codeBlocks);
31+
$codeBlocks[$index] = $matches[0];
32+
33+
return sprintf($placeholder, $index);
34+
},
35+
$markdown
36+
);
37+
38+
// Extract inline code (single backticks)
39+
$inlineCode = [];
40+
$inlinePlaceholder = '___INLINE_CODE_PLACEHOLDER_%d___';
41+
$markdown = preg_replace_callback(
42+
'/`[^`]+`/',
43+
function ($matches) use (&$inlineCode, $inlinePlaceholder) {
44+
$index = count($inlineCode);
45+
$inlineCode[$index] = $matches[0];
46+
47+
return sprintf($inlinePlaceholder, $index);
48+
},
49+
$markdown
50+
);
51+
52+
// Check if there are any Blade components to process
53+
if (static::containsBladeComponents($markdown)) {
54+
// Render the content as a Blade string
55+
$markdown = static::renderBladeContent($markdown, $data);
56+
}
57+
58+
// Restore inline code
59+
foreach ($inlineCode as $index => $code) {
60+
$markdown = str_replace(sprintf($inlinePlaceholder, $index), $code, $markdown);
61+
}
62+
63+
// Restore code blocks - convert to HTML if inside HTML elements
64+
foreach ($codeBlocks as $index => $block) {
65+
$placeholderStr = sprintf($placeholder, $index);
66+
67+
// Check if placeholder is inside an HTML element (after Blade rendering)
68+
if (static::isInsideHtmlElement($markdown, $placeholderStr)) {
69+
// Convert code block to HTML before restoring
70+
$block = static::convertCodeBlockToHtml($block);
71+
}
72+
73+
$markdown = str_replace($placeholderStr, $block, $markdown);
74+
}
75+
76+
return $markdown;
77+
}
78+
79+
/**
80+
* Check if a placeholder is inside an HTML element.
81+
*/
82+
protected static function isInsideHtmlElement(string $content, string $placeholder): bool
83+
{
84+
$pos = strpos($content, $placeholder);
85+
if ($pos === false) {
86+
return false;
87+
}
88+
89+
// Get content before placeholder and count open/close tags
90+
$before = substr($content, 0, $pos);
91+
92+
// Simple heuristic: check if there's an unclosed HTML tag before the placeholder
93+
// Look for patterns like <div...> that aren't closed
94+
$openTags = preg_match_all('/<(div|span|section|article|aside|p)[^>]*>(?!.*<\/\1>)/i', $before);
95+
$lastOpenTag = strrpos($before, '<');
96+
$lastCloseTag = strrpos($before, '>');
97+
98+
// If the last < is after the last >, we're likely inside a tag (shouldn't happen with placeholders)
99+
// More reliable: check if there's an HTML structure pattern before the placeholder
100+
return (bool) preg_match('/<[a-z][^>]*>\s*$/i', $before);
101+
}
102+
103+
/**
104+
* Convert a markdown code block to HTML using Torchlight.
105+
*/
106+
protected static function convertCodeBlockToHtml(string $codeBlock): string
107+
{
108+
try {
109+
$config = [
110+
'html_input' => 'allow',
111+
];
112+
113+
$environment = new Environment($config);
114+
$environment->addExtension(new CommonMarkCoreExtension);
115+
116+
if (class_exists(TorchlightExtension::class)) {
117+
$environment->addExtension(new TorchlightExtension);
118+
}
119+
120+
$converter = new MarkdownConverter($environment);
121+
122+
return trim($converter->convert($codeBlock)->getContent());
123+
} catch (\Throwable $e) {
124+
report($e);
125+
126+
return $codeBlock;
127+
}
128+
}
129+
130+
/**
131+
* Check if the content contains Blade components.
132+
*/
133+
protected static function containsBladeComponents(string $content): bool
134+
{
135+
// Look for <x-*> component tags or Blade directives/echoes
136+
return (bool) preg_match('/<x-[\w\-\.:]+|@[\w]+|{{\s*|{!!\s*/', $content);
137+
}
138+
139+
/**
140+
* Render Blade content, handling components and directives.
141+
*/
142+
protected static function renderBladeContent(string $content, array $data = []): string
143+
{
144+
try {
145+
return Blade::render($content, $data);
146+
} catch (\Throwable $e) {
147+
// If Blade rendering fails, return original content
148+
// This can happen if the content has invalid Blade syntax
149+
report($e);
150+
151+
return $content;
152+
}
153+
}
154+
}

app/Support/CommonMark/CommonMark.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace App\Support\CommonMark;
44

55
use App\Extensions\TorchlightWithCopyExtension;
6-
use League\CommonMark\CommonMarkConverter;
76
use League\CommonMark\Environment\Environment;
87
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
98
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
@@ -21,8 +20,11 @@ class CommonMark
2120
{
2221
protected static ?MarkdownConverter $converter = null;
2322

24-
public static function convertToHtml(string $markdown): string
23+
public static function convertToHtml(string $markdown, array $data = []): string
2524
{
25+
// Pre-process to render any Blade components in the markdown
26+
$markdown = BladeMarkdownPreprocessor::process($markdown, $data);
27+
2628
return static::getConverter()->convert($markdown)->getContent();
2729
}
2830

@@ -45,6 +47,7 @@ protected static function getConverter(): MarkdownConverter
4547
$environment->addExtension(new GithubFlavoredMarkdownExtension);
4648
$environment->addRenderer(Heading::class, new HeadingRenderer);
4749
$environment->addExtension(new TableExtension);
50+
4851
$environment->addExtension(new EmbedExtension);
4952

5053
$environment->addRenderer(

resources/css/app.css

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,34 @@ nav.docs-navigation > ul > li > ul {
317317
@apply absolute inset-0 w-full h-full;
318318
}
319319
}
320+
321+
/* Snippet component with tabbed code blocks */
322+
.snippet {
323+
@apply rounded-xl shadow-lg;
324+
}
325+
326+
.snippet-content {
327+
@apply relative;
328+
}
329+
330+
.snippet-tab pre {
331+
@apply m-0 rounded-none bg-transparent p-0;
332+
}
333+
334+
.snippet-tab pre code {
335+
@apply block min-w-max py-4 text-sm;
336+
}
337+
338+
.snippet-tab pre code .line {
339+
@apply px-4;
340+
}
341+
342+
/* When snippet has a single tab, round the top corners */
343+
.snippet-content:first-child {
344+
@apply rounded-t-xl;
345+
}
346+
347+
/* Hide Torchlight's default copy button inside snippets */
348+
.snippet .torchlight-with-copy > div:first-child {
349+
@apply hidden;
350+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
@props([
2+
'title' => null,
3+
])
4+
<div {{ $attributes->merge(['class' => 'snippet not-prose my-6']) }} x-data="{
5+
activeTab: null,
6+
tabs: [],
7+
init() {
8+
const container = this.$el.querySelector('.snippet-content');
9+
if (container) {
10+
this.tabs = Array.from(container.querySelectorAll('[data-snippet-tab]'));
11+
if (this.tabs.length > 0) {
12+
this.activeTab = this.tabs[0].dataset.snippetTab;
13+
}
14+
}
15+
},
16+
setActiveTab(tab) {
17+
this.activeTab = tab;
18+
},
19+
isActive(tab) {
20+
return this.activeTab === tab;
21+
}
22+
}">
23+
<div x-show="tabs.length > 1" x-cloak class="flex gap-1 rounded-t-xl bg-slate-800 px-4 pt-3">
24+
<template x-for="tab in tabs" :key="tab.dataset.snippetTab">
25+
<button type="button" @click="setActiveTab(tab.dataset.snippetTab)" :class="{'bg-slate-700 text-white': isActive(tab.dataset.snippetTab), 'text-slate-400 hover:text-slate-300 hover:bg-slate-700/50': !isActive(tab.dataset.snippetTab)}" class="rounded-t-lg px-4 py-2 text-sm font-medium transition-colors" x-text="tab.dataset.snippetTab"></button>
26+
</template>
27+
</div>
28+
@if ($title)<div x-show="tabs.length <= 1" x-cloak class="rounded-t-xl bg-slate-800 px-4 py-2 text-sm font-medium text-slate-400">{{ $title }}</div>@endif
29+
<div class="snippet-content overflow-hidden bg-slate-900 @if(!$title) rounded-t-xl @endif rounded-b-xl">
30+
{{ $slot }}
31+
</div>
32+
</div>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@props([
2+
'name',
3+
])
4+
<div {{ $attributes->merge(['class' => 'snippet-tab']) }} data-snippet-tab="{{ $name }}" x-show="isActive('{{ $name }}')" x-cloak>
5+
<div class="relative">
6+
<div x-data="{ copied: false }" class="absolute right-0 top-0 flex items-center gap-2 p-2">
7+
<span x-show="copied" x-transition class="text-sm font-bold text-indigo-400">Copied!</span>
8+
<button type="button" title="Copy to clipboard" class="hidden p-2 text-white/20 transition duration-300 hover:text-white/60 sm:block" :class="{ 'text-indigo-400 hover:text-indigo-400': copied }" @click="const code = $el.closest('.snippet-tab').querySelector('pre code, .torchlight-copy-target'); if (code) { navigator.clipboard.writeText(code.textContent.trim()); copied = true; setTimeout(() => copied = false, 2000); }">
9+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="block size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0118 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3l1.5 1.5 3-3.75" /></svg>
10+
</button>
11+
</div>
12+
<div class="overflow-x-auto">
13+
{{ $slot }}
14+
</div>
15+
</div>
16+
</div>

resources/views/docs/mobile/2/getting-started/changelog.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,19 @@ Features:
2626
## Fluent Pending API (PHP)
2727
All [Asynchronous Methods](../the-basics/events#understanding-async-vs-sync) now implement a fluent API for better IDE support and ease of use.
2828

29-
### PHP
29+
<x-snippet title="Fluent APIs">
30+
31+
<x-snippet.tab name="PHP">
32+
3033
```php
3134
Dialog::alert('Confirm', 'Delete this?', ['Cancel', 'Delete'])
3235
->remember()
3336
->show();
3437
```
3538

36-
### JS
39+
</x-snippet.tab>
40+
<x-snippet.tab name="Vue">
41+
3742
```js
3843
import { dialog, on, off, Events } from '#nativephp';
3944
const label = ref('');
@@ -54,6 +59,9 @@ onMounted(() => {
5459
});
5560
```
5661

62+
</x-snippet.tab>
63+
</x-snippet>
64+
5765
## `#[OnNative]` Livewire Attribute
5866
Forget the silly string concatenation of yesterday; get into today's fashionable attribute usage with this drop-in
5967
replacement:
@@ -92,7 +100,9 @@ Just update your config and record audio even while the device is locked!
92100
## Push Notifications API
93101
New fluent API for push notification enrollment:
94102

95-
### PHP
103+
<x-snippet title="Push Notifications">
104+
105+
<x-snippet.tab name="PHP">
96106
```php
97107
use Native\Mobile\Facades\PushNotifications;
98108
use Native\Mobile\Events\PushNotification\TokenGenerated;
@@ -105,8 +115,8 @@ public function handlePushNotificationsToken($token)
105115
$this->token = $token;
106116
}
107117
```
108-
109-
### JS
118+
</x-snippet.tab>
119+
<x-snippet.tab name="Vue">
110120
```js
111121
import { pushNotifications, on, off, Events } from '#nativephp';
112122

@@ -128,6 +138,8 @@ onUnmounted(() => {
128138
off(Events.PushNotification.TokenGenerated, handlePushNotificationsToken);
129139
});
130140
```
141+
</x-snippet.tab>
142+
</x-snippet>
131143

132144
**Deprecated Methods:**
133145
- `enrollForPushNotifications()` → use `enroll()`

0 commit comments

Comments
 (0)