Skip to content
This repository has been archived by the owner on Feb 27, 2024. It is now read-only.

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalbaljet committed Oct 18, 2023
1 parent 3c993a3 commit 1698615
Show file tree
Hide file tree
Showing 11 changed files with 263 additions and 46 deletions.
3 changes: 3 additions & 0 deletions app/resources/views/base-view.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<x-layout>
@include('included-view')
</x-layout>
10 changes: 10 additions & 0 deletions app/resources/views/included-view.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script setup>
const message = ref('Hello Included view!');
</script>

<div style="padding: 10px; background: #eee;">
<input v-model="message" />
<p>The message is: <span v-html="message"></span></p>

<x-two-way-binding />
</div>
10 changes: 10 additions & 0 deletions app/resources/views/regular-view.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<x-layout>
<script setup>
const message = ref('Hello World!');
</script>

<input v-model="message" />
<p>The message is: <span v-html="message"></span></p>

<x-two-way-binding />
</x-layout>
2 changes: 2 additions & 0 deletions app/routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
return view('welcome');
});

Route::view('/regular-view', 'regular-view');
Route::view('/base-view', 'base-view');
Route::view('/anonymous', 'anonymous');
Route::view('/blade-method-callbacks', 'blade-method-callbacks');
Route::view('/blade-method', 'blade-method');
Expand Down
4 changes: 3 additions & 1 deletion src/ComponentHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,12 @@ public function getTag(Component|View|string $component): ?string
$class = $this->getClass($component);

if (! $class) {
$isComponent = $component instanceof Component || Str::contains($component, 'components.');

return Str::of($this->getAlias($component))
->replace('.', ' ')
->studly()
->prepend('SpladeComponent')
->prepend($isComponent ? 'SpladeComponent' : 'Splade')
->toString();
}

Expand Down
122 changes: 111 additions & 11 deletions src/ExtractVueScriptFromBladeView.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
namespace ProtoneMedia\SpladeCore;

use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Js;
use Illuminate\Support\Str;

class ExtractVueScriptFromBladeView
{
protected readonly string $originalScript;

protected array $viewRootLayoutTags = [];

protected string $viewWithoutScriptTag;

protected ScriptParser $scriptParser;
Expand All @@ -30,12 +35,57 @@ public static function from(string $originalView, array $data, string $bladePath
return new static($originalView, $data, $bladePath);
}

/**
* Check if the view is a Blade Component.
*/
protected function isComponent(): bool
{
return array_key_exists('spladeBridge', $this->data)
&& Str::contains($this->bladePath, '/components/');
}

/**
* Check if the view has a <script setup> tag.
*/
protected function hasScriptSetup(): bool
{
return str_contains($this->originalView, '<script setup>');
}

/**
* Check if the view is wrapped in a root layout.
*/
protected function extractWrappedViewInRootLayout(): void
{
$originalView = trim($this->originalView);

if (! Str::startsWith($originalView, '<x-')) {
return;
}

// find last closing html tag
$endTag = trim(Arr::last(explode(PHP_EOL, $originalView)));

if (! (Str::startsWith($endTag, '</') && Str::endsWith($endTag, '>'))) {
return;
}

$tag = Str::between($endTag, '</', '>');

preg_match_all('/(<'.$tag.'.*?>)(.*?)(<\/'.$tag.'>)/s', $originalView, $matches);

$this->viewRootLayoutTags = [
$matches[1][0], // <x-layout {{ $attributes }}>
$matches[3][0], // </x-layout>
];
}

/**
* Handle the extraction of the Vue script. Returns the view without the <script setup> tag.
*/
public function handle(Filesystem $filesystem): string
public function handle(Filesystem $filesystem): string|PendingView
{
if (! str_starts_with(trim($this->originalView), '<script setup>')) {
if (! $this->hasScriptSetup()) {
// The view does not contain a <script setup> tag, so we don't need to do anything.
return $this->originalView;
}
Expand Down Expand Up @@ -73,7 +123,27 @@ public function handle(Filesystem $filesystem): string
Process::path(base_path())->run("node_modules/.bin/eslint --fix {$vuePath}");
}

return $this->viewWithoutScriptTag;
return $this->isComponent()
? $this->viewWithoutScriptTag
: PendingView::from($this->viewWithoutScriptTag, $this->bladePath, $this->viewRootLayoutTags);

// $tag = app(ComponentHelper::class)->getTag($this->bladePath);

// $hash = Str::random(24);

// $bridge = Js::from(['tag' => $tag, 'template_hash' => $hash])->toHtml();

// $replacement = <<<HTML
// {$this->viewRootLayoutTags[0]}
// <generic-splade-component :bridge="{$bridge}"></generic-splade-component>
// {$this->viewRootLayoutTags[1]}
// HTML;

// return [
// 'hash' => $hash,
// 'toRender' => $this->viewWithoutScriptTag,
// 'replacement' => $replacement,
// ];
}

/**
Expand All @@ -89,23 +159,30 @@ protected function attributesAreCustomBound(): bool
*/
protected function getBladeFunctions(): array
{
return $this->data['spladeBridge']['functions'];
return $this->data['spladeBridge']['functions'] ?? [];
}

/**
* Get the properties that are passed from the Blade Component.
*/
protected function getBladeProperties(): array
{
return array_keys($this->data['spladeBridge']['data']);
return array_keys($this->data['spladeBridge']['data'] ?? []);
}

/**
* Get the 'Splade' tag of the Blade Component.
*/
protected function getTag(): string
{
return $this->data['spladeBridge']['tag'];
if ($this->isComponent()) {
return $this->data['spladeBridge']['tag'];
}

/** @var ComponentHelper */
$componentHelper = app(ComponentHelper::class);

return $componentHelper->getTag($this->bladePath);
}

/**
Expand All @@ -121,6 +198,10 @@ protected function isRefreshable(): bool
*/
protected function needsSpladeBridge(): bool
{
if (! $this->isComponent()) {
return false;
}

if (! empty($this->getBladeFunctions())) {
return true;
}
Expand Down Expand Up @@ -163,13 +244,29 @@ protected function splitOriginalView(): void
->replaceFirst("<script setup>{$this->originalScript}</script>", '')
->trim()
->toString();

$this->extractWrappedViewInRootLayout();

if (empty($this->viewRootLayoutTags)) {
return;
}

$this->viewWithoutScriptTag = Str::of($this->viewWithoutScriptTag)
->after($this->viewRootLayoutTags[0])
->beforeLast($this->viewRootLayoutTags[1])
->trim()
->toString();
}

/**
* Replace someMethod.loading with someMethod.loading.value
*/
protected function replaceComponentMethodLoadingStates(string $script): string
{
if (! $this->isComponent()) {
return $script;
}

$methods = ['refreshComponent', ...$this->getBladeFunctions()];

return preg_replace_callback('/(\w+)\.loading/', function ($matches) use ($methods) {
Expand All @@ -196,10 +293,9 @@ protected function replaceElementRefs(string $script): string
*/
protected function extractDefinePropsFromScript(): array
{
$defaultProps = Collection::make([
'spladeBridge' => 'Object',
'spladeTemplateId' => 'String',
])->when($this->viewUsesVModel(), fn (Collection $collection) => $collection->put('modelValue', '{}'));
$defaultProps = Collection::make(['spladeTemplateId' => 'String'])
->when($this->isComponent(), fn (Collection $collection) => $collection->put('spladeBridge', 'Object'))
->when($this->viewUsesVModel(), fn (Collection $collection) => $collection->put('modelValue', '{}'));

$defineProps = $this->scriptParser->getDefineProps($defaultProps->all());

Expand All @@ -222,7 +318,7 @@ protected function renderImports(): string
->push('h')
->when($this->needsSpladeBridge(), fn ($collection) => $collection->push('ref'))
->when($this->isRefreshable(), fn ($collection) => $collection->push('inject'))
->unless(empty($this->getBladeProperties()), fn ($collection) => $collection->push('computed'))
->when($this->isComponent() && ! empty($this->getBladeProperties()), fn ($collection) => $collection->push('computed'))
->unique()
->sort()
->implode(',');
Expand Down Expand Up @@ -285,6 +381,10 @@ protected static function renderBladePropertyAsComputedVueProperty(string $prope
*/
protected function renderBladePropertiesAsComputedVueProperties(): string
{
if (! $this->isRefreshable()) {
return '';
}

$lines = [];

foreach ($this->getBladeProperties() as $property) {
Expand Down
51 changes: 51 additions & 0 deletions src/PendingView.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace ProtoneMedia\SpladeCore;

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Js;

class PendingView
{
public readonly string $originalView;

public function __construct(
public readonly string $viewWithoutScript,
public readonly string $tag,
public readonly array $rootLayoutTags
) {

}

public function setOriginalView(string $originalView): self
{
$this->originalView = $originalView;

return $this;
}

public function render(string $hash): string
{
$bridge = Js::from(['tag' => $this->tag, 'template_hash' => $hash])->toHtml();

if (empty($this->rootLayoutTags)) {
return Blade::render(<<<HTML
<generic-splade-component :bridge="{$bridge}"></generic-splade-component>
HTML);
}

return Blade::render(<<<HTML
{$this->rootLayoutTags[0]}
<generic-splade-component :bridge="{$bridge}"></generic-splade-component>
{$this->rootLayoutTags[1]}
HTML);
}

public static function from(string $viewWithoutScript, string $path, array $rootLayoutTags)
{
/** @var ComponentHelper */
$componentHelper = app(ComponentHelper::class);

return new static($viewWithoutScript, $componentHelper->getTag($path), $rootLayoutTags);
}
}
4 changes: 0 additions & 4 deletions src/SpladeCoreServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,6 @@ protected function registerBladeCompiler()
$blade->component('dynamic-component', DynamicComponent::class);
});
});

BladeCompiler::beforeCompilingString(
fn ($value, $data, $path) => ExtractVueScriptFromBladeView::from($value, $data, $path)->handle(app('files'))
);
}

protected function registerFactory()
Expand Down
36 changes: 21 additions & 15 deletions src/View/BladeCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace ProtoneMedia\SpladeCore\View;

use Illuminate\Support\Str;
use Illuminate\View\Compilers\BladeCompiler as BaseBladeCompiler;
use ProtoneMedia\SpladeCore\ExtractVueScriptFromBladeView;

class BladeCompiler extends BaseBladeCompiler
{
Expand All @@ -11,30 +13,34 @@ class BladeCompiler extends BaseBladeCompiler
*/
protected array $data = [];

/**
* Callbacks that are called before compiling the string.
*/
protected static array $beforeCompilingStringCallbacks = [];
public array $hashes = [];

/**
* Registers a callback that is called before compiling the string.
*/
public static function beforeCompilingString(callable $callback): void
{
static::$beforeCompilingStringCallbacks[] = $callback;
}
public array $pendingViews = [];

/**
* Extract the Vue script from the given template.
*/
public function compileString($value): string
{
foreach (static::$beforeCompilingStringCallbacks as $callback) {
$callback = $callback->bindTo($this, static::class);
$value = $callback($value, $this->data, $this->getPath());
$service = ExtractVueScriptFromBladeView::from($value, $this->data, $this->getPath());

$result = $service->handle($this->files);

if (is_string($result)) {
return parent::compileString($result);
}

return parent::compileString($value);
// TODO: move to CompilerEngine::get()
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 9);

// prevent leaking the full path
$path = Str::after($trace[8]['file'], base_path());

$hash = md5($path.'.'.$trace[8]['line']);

$this->pendingViews[$hash] = $result->setOriginalView($value);

return parent::compileString($result->viewWithoutScript);
}

/**
Expand Down
Loading

0 comments on commit 1698615

Please sign in to comment.