Skip to content

Commit 8c82ece

Browse files
simonhampclaude
andcommitted
Add GitHub-style markdown alert blocks to CommonMark renderer
Support [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], and [!CAUTION] blockquote syntax with styled alert rendering, SVG icons, and gradient backgrounds matching the existing aside design. Convert security docs page asides to use the new alert syntax. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 07f1e42 commit 8c82ece

6 files changed

Lines changed: 334 additions & 22 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
namespace App\Support\CommonMark;
4+
5+
use League\CommonMark\Extension\CommonMark\Node\Block\BlockQuote;
6+
use League\CommonMark\Node\Block\Paragraph;
7+
use League\CommonMark\Node\Inline\Newline;
8+
use League\CommonMark\Node\Inline\Text;
9+
use League\CommonMark\Node\Node;
10+
use League\CommonMark\Renderer\ChildNodeRendererInterface;
11+
use League\CommonMark\Renderer\NodeRendererInterface;
12+
use League\CommonMark\Util\HtmlElement;
13+
14+
class AlertBlockQuoteRenderer implements NodeRendererInterface
15+
{
16+
/**
17+
* @var array<string, array{label: string, icon: string}>
18+
*/
19+
protected const ALERT_TYPES = [
20+
'NOTE' => [
21+
'label' => 'Note',
22+
'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>',
23+
],
24+
'TIP' => [
25+
'label' => 'Tip',
26+
'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg>',
27+
],
28+
'IMPORTANT' => [
29+
'label' => 'Important',
30+
'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>',
31+
],
32+
'WARNING' => [
33+
'label' => 'Warning',
34+
'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>',
35+
],
36+
'CAUTION' => [
37+
'label' => 'Caution',
38+
'icon' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>',
39+
],
40+
];
41+
42+
public function render(Node $node, ChildNodeRendererInterface $childRenderer): ?HtmlElement
43+
{
44+
BlockQuote::assertInstanceOf($node);
45+
46+
$firstChild = $node->firstChild();
47+
48+
if (! $firstChild instanceof Paragraph) {
49+
return null;
50+
}
51+
52+
$firstInline = $firstChild->firstChild();
53+
54+
if (! $firstInline instanceof Text) {
55+
return null;
56+
}
57+
58+
$literal = $firstInline->getLiteral();
59+
60+
if (! preg_match('/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/', $literal, $matches)) {
61+
return null;
62+
}
63+
64+
$type = $matches[1];
65+
$alertConfig = self::ALERT_TYPES[$type];
66+
67+
$remaining = ltrim(substr($literal, strlen($matches[0])));
68+
69+
if ($remaining !== '') {
70+
$firstInline->setLiteral($remaining);
71+
} else {
72+
$nextSibling = $firstInline->next();
73+
74+
$firstInline->detach();
75+
76+
if ($nextSibling instanceof Newline) {
77+
$nextSibling->detach();
78+
}
79+
}
80+
81+
if (! $firstChild->hasChildren()) {
82+
$firstChild->detach();
83+
}
84+
85+
$innerHtml = $childRenderer->renderNodes($node->children());
86+
87+
$titleHtml = new HtmlElement(
88+
'p',
89+
['class' => 'markdown-alert-title'],
90+
$alertConfig['icon'].' '.$alertConfig['label'],
91+
);
92+
93+
$typeSlug = strtolower($type);
94+
95+
return new HtmlElement(
96+
'div',
97+
['class' => "markdown-alert markdown-alert-{$typeSlug}"],
98+
$titleHtml.$innerHtml,
99+
);
100+
}
101+
}

app/Support/CommonMark/CommonMark.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use App\Extensions\TorchlightWithCopyExtension;
66
use League\CommonMark\Environment\Environment;
77
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
8+
use League\CommonMark\Extension\CommonMark\Node\Block\BlockQuote;
89
use League\CommonMark\Extension\CommonMark\Node\Block\Heading;
910
use League\CommonMark\Extension\Embed\Bridge\OscaroteroEmbedAdapter;
1011
use League\CommonMark\Extension\Embed\Embed as EmbedNode;
@@ -53,6 +54,7 @@ protected static function getConverter(): MarkdownConverter
5354
$environment->addExtension(new GithubFlavoredMarkdownExtension);
5455
static::$headingRenderer = new HeadingRenderer;
5556
$environment->addRenderer(Heading::class, static::$headingRenderer);
57+
$environment->addRenderer(BlockQuote::class, new AlertBlockQuoteRenderer, 10);
5658
$environment->addExtension(new TableExtension);
5759

5860
$environment->addExtension(new EmbedExtension);

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

resources/css/app.css

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,108 @@ nav.docs-navigation li:has(.third-tier .exact-active) > .subsection-header {
393393
@apply relative z-0 mt-5 overflow-hidden rounded-2xl bg-gradient-to-tl from-transparent to-violet-100/75 px-5 ring-1 ring-black/5 dark:from-slate-900/30 dark:to-indigo-900/35;
394394
}
395395

396+
/* GitHub-style markdown alerts */
397+
.prose .markdown-alert {
398+
@apply relative z-0 my-4 overflow-hidden rounded-2xl border-l-4 px-5 py-3 ring-1 ring-black/5;
399+
}
400+
401+
.prose .markdown-alert > :first-child {
402+
@apply mt-0;
403+
}
404+
405+
.prose .markdown-alert > :last-child {
406+
@apply mb-0;
407+
}
408+
409+
.prose .markdown-alert-title {
410+
@apply flex items-center gap-2 font-semibold;
411+
}
412+
413+
.prose .markdown-alert-title svg {
414+
@apply shrink-0;
415+
}
416+
417+
.prose .markdown-alert-note {
418+
@apply border-blue-400 bg-gradient-to-tl from-transparent to-blue-100/75 text-blue-900;
419+
}
420+
421+
.prose .markdown-alert-note .markdown-alert-title {
422+
@apply text-blue-700;
423+
}
424+
425+
.prose .markdown-alert-tip {
426+
@apply border-green-400 bg-gradient-to-tl from-transparent to-green-100/75 text-green-900;
427+
}
428+
429+
.prose .markdown-alert-tip .markdown-alert-title {
430+
@apply text-green-700;
431+
}
432+
433+
.prose .markdown-alert-important {
434+
@apply border-purple-400 bg-gradient-to-tl from-transparent to-purple-100/75 text-purple-900;
435+
}
436+
437+
.prose .markdown-alert-important .markdown-alert-title {
438+
@apply text-purple-700;
439+
}
440+
441+
.prose .markdown-alert-warning {
442+
@apply border-amber-400 bg-gradient-to-tl from-transparent to-amber-100/75 text-amber-900;
443+
}
444+
445+
.prose .markdown-alert-warning .markdown-alert-title {
446+
@apply text-amber-700;
447+
}
448+
449+
.prose .markdown-alert-caution {
450+
@apply border-red-400 bg-gradient-to-tl from-transparent to-red-100/75 text-red-900;
451+
}
452+
453+
.prose .markdown-alert-caution .markdown-alert-title {
454+
@apply text-red-700;
455+
}
456+
457+
/* Dark mode markdown alerts */
458+
.dark .prose .markdown-alert-note {
459+
@apply border-blue-500 from-slate-900/30 to-blue-900/35 text-blue-200;
460+
}
461+
462+
.dark .prose .markdown-alert-note .markdown-alert-title {
463+
@apply text-blue-400;
464+
}
465+
466+
.dark .prose .markdown-alert-tip {
467+
@apply border-green-500 from-slate-900/30 to-green-900/35 text-green-200;
468+
}
469+
470+
.dark .prose .markdown-alert-tip .markdown-alert-title {
471+
@apply text-green-400;
472+
}
473+
474+
.dark .prose .markdown-alert-important {
475+
@apply border-purple-500 from-slate-900/30 to-purple-900/35 text-purple-200;
476+
}
477+
478+
.dark .prose .markdown-alert-important .markdown-alert-title {
479+
@apply text-purple-400;
480+
}
481+
482+
.dark .prose .markdown-alert-warning {
483+
@apply border-amber-500 from-slate-900/30 to-amber-900/35 text-amber-200;
484+
}
485+
486+
.dark .prose .markdown-alert-warning .markdown-alert-title {
487+
@apply text-amber-400;
488+
}
489+
490+
.dark .prose .markdown-alert-caution {
491+
@apply border-red-500 from-slate-900/30 to-red-900/35 text-red-200;
492+
}
493+
494+
.dark .prose .markdown-alert-caution .markdown-alert-title {
495+
@apply text-red-400;
496+
}
497+
396498
.images-two-up {
397499
@apply grid gap-8 mt-0 items-center;
398500

resources/views/docs/mobile/3/concepts/security.md

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,9 @@ This data is only accessible by your app and is persisted beyond the lifetime of
4242
the next time your app is opened.
4343

4444

45-
<aside>
46-
47-
Secure Storage is only meant for small amounts of text data, usually no more than a few KBs. If you need to store
48-
larger amounts of data or files, you should store this in a database or as a file.
49-
50-
</aside>
45+
> [!NOTE]
46+
> Secure Storage is only meant for small amounts of text data, usually no more than a few KBs. If you need to store
47+
> larger amounts of data or files, you should store this in a database or as a file.
5148
5249
### When to use the Laravel `Crypt` facade
5350

@@ -58,11 +55,8 @@ stored on the device.
5855
NativePHP securely reads the `APP_KEY` from secure storage and makes it available to Laravel. So you can safely use the
5956
`Crypt` facade to encrypt and decrypt data!
6057

61-
<aside>
62-
63-
Make sure you do not leak the `APP_KEY` or decrypted data inadvertently through error tracking or debug logging tools.
64-
65-
</aside>
58+
> [!WARNING]
59+
> Make sure you do not leak the `APP_KEY` or decrypted data inadvertently through error tracking or debug logging tools.
6660
6761
This is great for encrypting larger amounts of data that wouldn't easily fit in secure storage. You can encrypt values
6862
and store them in the file system or in the SQLite database, knowing that they are safe at rest:
@@ -85,14 +79,11 @@ $decryptedContents = Crypt::decryptString(
8579
);
8680
```
8781

88-
<aside>
89-
90-
Data encrypted with the `Crypt` facade should stay on the user's device with your app. Placing it encrypted anywhere
91-
else risks the chance that it will be unrecoverable. If the user loses their device or deletes your app,
92-
they will lose the encryption key and the data will be encrypted forever.
93-
94-
If you wish to share data, decrypt it first, transmit securely (e.g. over HTTPS) and re-encrypt it with a different key
95-
that is safely managed elsewhere.
96-
97-
</aside>
82+
> [!CAUTION]
83+
> Data encrypted with the `Crypt` facade should stay on the user's device with your app. Placing it encrypted anywhere
84+
> else risks the chance that it will be unrecoverable. If the user loses their device or deletes your app,
85+
> they will lose the encryption key and the data will be encrypted forever.
86+
>
87+
> If you wish to share data, decrypt it first, transmit securely (e.g. over HTTPS) and re-encrypt it with a different key
88+
> that is safely managed elsewhere.
9889

0 commit comments

Comments
 (0)