Skip to content

Commit

Permalink
Merge pull request #478 from hydephp/add-blade-support-for-markdown
Browse files Browse the repository at this point in the history
Add inline Blade support to markdown
  • Loading branch information
caendesilva authored May 31, 2022
2 parents 8b185ed + a0a28cb commit fd41cf7
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 0 deletions.
53 changes: 53 additions & 0 deletions .github/dev-docs/advanced-markdown.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
label: "Advanced Markdown"
priority: 26
category: "Digging Deeper"
---

# Advanced Markdown

## Introduction

Since HydePHP makes heavy use of Markdown there are some extra features and helpers
created just for Hyde to make using Markdown even easier!

## Blade Support

Since Hyde v0.30.x you can use Laravel Blade in Markdown files!

### Using Blade in Markdown

To use Blade in your Markdown files, simply use the Blade shortcode directive,
followed by your desired Blade string.

#### Standard syntax

```markdown
[Blade]: {{ "Hello World!" }} // Will render: 'Hello World!'
```

#### Blade includes

Only single-line shortcode directives are supported. If you need to use multi-line Blade code,
use an `@include` directive to render a more complex Blade template.
You can pass data to includes by specifying an array to the second argument.

```markdown
[Blade]: @include("hello-world")
[Blade]: @include("hello", ["name" => "World"])
```

### Enabling Blade-supported Markdown
It's disabled by default since it allows arbitrary PHP to run, which could be a security risk,
depending on your setup. However, if your Markdown is trusted, and you know it's safe,
you can enable it in the `config/markdown.php` file.

```php
// torchlight! {"lineNumbers": false}
'enable_blade' => true,
```

#### Limitations

All shortcodes must be the first word on a new line.
For example, using a space before the `[Blade]:` will intentionally cause it to not render.
16 changes: 16 additions & 0 deletions config/markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,20 @@
//
],

/*
|--------------------------------------------------------------------------
| Blade-supported Markdown
|--------------------------------------------------------------------------
|
| Since Hyde v0.30.x you can use Laravel Blade in Markdown files.
|
| It's disabled by default since can be a security risk as it allows
| arbitrary PHP to run. But if your Markdown is trusted, try it out!
|
| To see the syntax and usage, see the documentation:
| @see https://hydephp.com/docs/master/advanced-markdown#blade-support
|
*/

'enable_blade' => false,
];
73 changes: 73 additions & 0 deletions src/Services/BladeDownService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Hyde\Framework\Services;

use Illuminate\Support\Facades\Blade;

/**
* Markdown Processor to render Laravel Blade within Markdown files.
*
* Works on a line-by-line basis by searching for a line starting with the directive.
* The preprocessor expands the directive to an HTML comment. The post-processor parses it.
*
* @example: [Blade]: {{ time() }}
* @example: [Blade]: @include('path/to/view.blade.php')
*
* @see \Tests\Feature\Services\BladeDownServiceTest
*/
class BladeDownService
{
protected string $html;
protected string $output;

protected array $pageData = [];

public function __construct(string $html, ?array $pageData = [])
{
$this->html = $html;
$this->pageData = $pageData;
}

public function process(): self
{
$this->output = implode("\n", array_map(function ($line) {
return $this->lineStartsWithDirective($line)
? $this->processLine($line)
: $line;
}, explode("\n", $this->html)));

return $this;
}

public function get(): string
{
return $this->output;
}

public static function render(string $html, ?array $pageData = []): string
{
return (new static(static::preprocess($html), $pageData))->process()->get();
}

public static function preprocess(string $markdown): string
{
return implode("\n", array_map(function ($line) {
return str_starts_with(strtolower($line), strtolower('[Blade]:'))
? '<!-- HYDE'.trim(htmlentities($line)).' -->'
: $line;
}, explode("\n", $markdown)));
}

protected function lineStartsWithDirective(string $line): bool
{
return str_starts_with(strtolower($line), '<!-- hyde[blade]:');
}

protected function processLine(string $line): string
{
return Blade::render(
substr(substr(html_entity_decode($line), 18), 0, -4),
$this->pageData
);
}
}
14 changes: 14 additions & 0 deletions src/Services/MarkdownConverterService.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public function parse(): string
{
$this->setupConverter();

$this->runPreprocessing();

$this->html = $this->converter->convert($this->markdown);

$this->runPostProcessing();
Expand Down Expand Up @@ -93,12 +95,24 @@ protected function setupConverter(): void
}
}

protected function runPreprocessing(): void
{
// Run any pre-processing actions
if (config('markdown.enable_blade', false)) {
$this->markdown = BladeDownService::preprocess($this->markdown);
}
}

protected function runPostProcessing(): void
{
// Run any post-processing actions
if ($this->determineIfTorchlightAttributionShouldBeInjected()) {
$this->html .= $this->injectTorchlightAttribution();
}

if (config('markdown.enable_blade', false)) {
$this->html = (new BladeDownService($this->html))->process()->get();
}
}

// Helper to inspect the currently enabled extensions
Expand Down
14 changes: 14 additions & 0 deletions tests/Feature/MarkdownConverterServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,18 @@ public function test_torchlight_integration_injects_attribution()
$this->assertStringContainsString('Syntax highlighting by <a href="https://torchlight.dev/" '
.'rel="noopener nofollow">Torchlight.dev</a>', $html);
}

public function test_bladedown_is_not_enabled_by_default()
{
$service = new MarkdownConverterService('[Blade]: {{ "Hello World!" }}');
$this->assertEquals("<p>[Blade]: {{ &quot;Hello World!&quot; }}</p>\n", $service->parse());
}

public function test_bladedown_can_be_enabled()
{
config(['markdown.enable_blade' => true]);
$service = new MarkdownConverterService('[Blade]: {{ "Hello World!" }}');
$service->addFeature('bladedown')->parse();
$this->assertEquals("Hello World!\n", $service->parse());
}
}
73 changes: 73 additions & 0 deletions tests/Feature/Services/BladeDownServiceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Tests\Feature\Services;

use Hyde\Framework\Services\BladeDownService;
use Tests\TestCase;

/**
* Class BladeDownServiceTest.
*
* @covers \Hyde\Framework\Services\BladeDownService
*/
class BladeDownServiceTest extends TestCase
{
// Test it renders Blade echo syntax
public function test_it_renders_blade_echo_syntax()
{
$this->assertEquals('Hello World!', BladeDownService::render('[Blade]: {{ "Hello World!" }}'));
}

// Test it renders Blade within multiline Markdown
public function test_it_renders_blade_within_multiline_markdown()
{
$this->assertEquals(
"Foo\nHello World!\nBar",

BladeDownService::render("Foo\n[Blade]: {{ 'Hello World!' }}\nBar")
);
}

// Test it renders Blade views
public function test_it_renders_blade_views()
{
file_put_contents(resource_path(
'views/hello.blade.php'
), 'Hello World!');

$this->assertEquals('Hello World!', BladeDownService::render('[Blade]: @include("hello")'));

unlink(resource_path('views/hello.blade.php'));
}

// Test directive is case-insensitive
public function test_directive_is_case_insensitive()
{
$this->assertEquals('Hello World!', BladeDownService::render('[blade]: {{ "Hello World!" }}'));
}

// Test directive is ignored if it's not at the start of a line
public function test_directive_is_ignored_if_it_is_not_at_the_start_of_a_line()
{
$this->assertEquals('Example: [Blade]: {{ "Hello World!" }}',
BladeDownService::render('Example: [Blade]: {{ "Hello World!" }}'));
}

// Test it renders Blade echo syntax with variables
public function test_it_renders_blade_echo_syntax_with_variables()
{
$this->assertEquals('Hello World!', BladeDownService::render('[Blade]: {{ $foo }}', ['foo' => 'Hello World!']));
}

// Test it renders Blade views with variables
public function test_it_renders_blade_views_with_variables()
{
file_put_contents(resource_path(
'views/hello.blade.php'
), 'Hello {{ $name }}!');

$this->assertEquals('Hello John!', BladeDownService::render('[Blade]: @include("hello", ["name" => "John"])'));

unlink(resource_path('views/hello.blade.php'));
}
}

0 comments on commit fd41cf7

Please sign in to comment.