Skip to content

Commit 13fa1d0

Browse files
authored
Merge branch 'main' into remove-admin-plugin-links
2 parents b31d6f2 + 061691d commit 13fa1d0

5 files changed

Lines changed: 333 additions & 21 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);

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
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Support\CommonMark\CommonMark;
6+
use Tests\TestCase;
7+
8+
class AlertBlockQuoteRendererTest extends TestCase
9+
{
10+
public function test_note_alert_renders_with_correct_class_and_title(): void
11+
{
12+
$html = CommonMark::convertToHtml("> [!NOTE]\n> This is a note.");
13+
14+
$this->assertStringContainsString('class="markdown-alert markdown-alert-note"', $html);
15+
$this->assertStringContainsString('class="markdown-alert-title"', $html);
16+
$this->assertStringContainsString('Note', $html);
17+
$this->assertStringContainsString('This is a note.', $html);
18+
$this->assertStringNotContainsString('[!NOTE]', $html);
19+
}
20+
21+
public function test_tip_alert_renders_with_correct_class_and_title(): void
22+
{
23+
$html = CommonMark::convertToHtml("> [!TIP]\n> This is a tip.");
24+
25+
$this->assertStringContainsString('markdown-alert-tip', $html);
26+
$this->assertStringContainsString('Tip', $html);
27+
$this->assertStringContainsString('This is a tip.', $html);
28+
}
29+
30+
public function test_important_alert_renders_with_correct_class_and_title(): void
31+
{
32+
$html = CommonMark::convertToHtml("> [!IMPORTANT]\n> This is important.");
33+
34+
$this->assertStringContainsString('markdown-alert-important', $html);
35+
$this->assertStringContainsString('Important', $html);
36+
$this->assertStringContainsString('This is important.', $html);
37+
}
38+
39+
public function test_warning_alert_renders_with_correct_class_and_title(): void
40+
{
41+
$html = CommonMark::convertToHtml("> [!WARNING]\n> This is a warning.");
42+
43+
$this->assertStringContainsString('markdown-alert-warning', $html);
44+
$this->assertStringContainsString('Warning', $html);
45+
$this->assertStringContainsString('This is a warning.', $html);
46+
}
47+
48+
public function test_caution_alert_renders_with_correct_class_and_title(): void
49+
{
50+
$html = CommonMark::convertToHtml("> [!CAUTION]\n> This is a caution.");
51+
52+
$this->assertStringContainsString('markdown-alert-caution', $html);
53+
$this->assertStringContainsString('Caution', $html);
54+
$this->assertStringContainsString('This is a caution.', $html);
55+
}
56+
57+
public function test_regular_blockquote_still_renders_as_blockquote(): void
58+
{
59+
$html = CommonMark::convertToHtml('> This is a regular quote.');
60+
61+
$this->assertStringContainsString('<blockquote>', $html);
62+
$this->assertStringNotContainsString('markdown-alert', $html);
63+
}
64+
65+
public function test_multi_paragraph_alert_content(): void
66+
{
67+
$markdown = "> [!NOTE]\n> First paragraph.\n>\n> Second paragraph.";
68+
$html = CommonMark::convertToHtml($markdown);
69+
70+
$this->assertStringContainsString('markdown-alert-note', $html);
71+
$this->assertStringContainsString('First paragraph.', $html);
72+
$this->assertStringContainsString('Second paragraph.', $html);
73+
}
74+
75+
public function test_inline_formatting_within_alert(): void
76+
{
77+
$markdown = "> [!TIP]\n> Use **bold** and `code` and [links](https://example.com).";
78+
$html = CommonMark::convertToHtml($markdown);
79+
80+
$this->assertStringContainsString('markdown-alert-tip', $html);
81+
$this->assertStringContainsString('<strong>bold</strong>', $html);
82+
$this->assertStringContainsString('<code>code</code>', $html);
83+
$this->assertStringContainsString('href="https://example.com"', $html);
84+
}
85+
86+
public function test_lowercase_type_does_not_trigger_alert(): void
87+
{
88+
$html = CommonMark::convertToHtml("> [!note]\n> This should not be an alert.");
89+
90+
$this->assertStringContainsString('<blockquote>', $html);
91+
$this->assertStringNotContainsString('markdown-alert', $html);
92+
}
93+
94+
public function test_invalid_type_does_not_trigger_alert(): void
95+
{
96+
$html = CommonMark::convertToHtml("> [!DANGER]\n> This should not be an alert.");
97+
98+
$this->assertStringContainsString('<blockquote>', $html);
99+
$this->assertStringNotContainsString('markdown-alert', $html);
100+
}
101+
102+
public function test_alert_contains_svg_icon(): void
103+
{
104+
$html = CommonMark::convertToHtml("> [!NOTE]\n> Content here.");
105+
106+
$this->assertStringContainsString('<svg', $html);
107+
}
108+
109+
public function test_alert_with_text_on_same_line_as_marker(): void
110+
{
111+
$html = CommonMark::convertToHtml('> [!WARNING] Immediate text on same line.');
112+
113+
$this->assertStringContainsString('markdown-alert-warning', $html);
114+
$this->assertStringNotContainsString('[!WARNING]', $html);
115+
}
116+
}

0 commit comments

Comments
 (0)