authorod / svitylo-anatomy-laravel
Laravel and Livewire integration of the Svitylo 3D Anatomy Atlas: Blade components, ```anatomy embeds for league/commonmark and stored HTML, Symfony HtmlSanitizer rules and Livewire commands/events.
Package info
github.com/authorOd/svitylo-anatomy-laravel
pkg:composer/authorod/svitylo-anatomy-laravel
Requires
- php: ^8.2
- illuminate/support: ^11.0 || ^12.0 || ^13.0
- illuminate/view: ^11.0 || ^12.0 || ^13.0
- league/commonmark: ^2.6
Requires (Dev)
- livewire/livewire: ^4.0
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- phpunit/phpunit: ^11.5 || ^12.0
- symfony/html-sanitizer: ^7.1 || ^8.0
Suggests
- livewire/livewire: Commands to the atlas and its events in Livewire components (^4.0)
- symfony/html-sanitizer: Sanitize notes before turning ```anatomy blocks into embeds
Provides
None
Conflicts
None
Replaces
None
README
Integration of the Svitylo 3D atlas with Laravel and Livewire:
- Blade components
<x-svitylo-anatomy>(the full atlas) and<x-svitylo-anatomy-embed>(an embed); - a league/commonmark extension: ```anatomy blocks and "Share" links become embeds (the same HTML as the markdown-it plugin produces, checked on shared fixtures);
- transformation of stored HTML (rich text editors, other Markdown renderers):
<pre><code class="language-anatomy">→ embed; - rules for Symfony HtmlSanitizer;
- Livewire 4: atlas commands from PHP and atlas events in components. Embeds and the atlas
carry
wire:ignore, so component updates do not touch the 3D scene;wire:navigateand@persistare covered by browser tests.
Requirements: PHP 8.2+, Laravel 11–13, Livewire 4 (optional), league/commonmark 2.6+.
Works with
@authorod/svitylo-3d-anatomy-atlas@1.0.0. The package lives in the atlas monorepo (packages/laravel); for Packagist it has to be split into a repository of its own. Terms: LICENSE.md.
Installation
composer require authorod/svitylo-anatomy-laravel
npm install @authorod/svitylo-3d-anatomy-atlas
npx svitylo-anatomy export-assets public/anatomy-data
php artisan vendor:publish --tag=svitylo-anatomy-config # optional
resources/js/app.js (Vite):
import '@authorod/svitylo-3d-anatomy-atlas'; // Only with Livewire: the JS bridge ships with this Composer package (an ES module without dependencies, types alongside). import { installLivewireBridge } from '../../vendor/authorod/svitylo-anatomy-laravel/resources/js/livewire.js'; installLivewireBridge();
The atlas library knows nothing about Laravel and Livewire. The protocol between PHP and the browser (event names, allowed commands) lives in one package: this one.
Theme
The atlas has a light and a dark theme (the theme attribute: auto follows the system, light,
dark; the 3D scene is dark in both). On a site where the dark class on <html> switches the
theme (Tailwind, Svitylo), a module of this package keeps the theme of every atlas on the page in
step. It is an ES module without dependencies, with types alongside:
import { syncAnatomyTheme } from '../../vendor/authorod/svitylo-anatomy-laravel/resources/js/theme.js'; syncAnatomyTheme(); // { root, darkClass } — another element or class; returns a function that stops it
It watches both the class and the atlases that appear later (Livewire updates, wire:navigate,
embeds in notes, editor previews and dialogs).
config/svitylo-anatomy.php: data_url (default: /anatomy-data/<data version>/), lang (names
language of embeds and of <x-svitylo-anatomy> when they do not set one; default uk),
share_urls (atlas pages whose share links become embeds), livewire (wire:ignore on embeds,
default true).
Notes
{{-- Markdown (CommonMark + GFM): raw HTML is escaped, unsafe links are dropped --}} @anatomyMarkdown($note->body) {{-- Stored HTML: the sanitizer first, then the embeds --}} @anatomyHtml($sanitizedHtml)
The same in PHP: app(SvityloAnatomy::class)->markdown($text), ->html($html),
->embed("structure: cardiovascular.heart\nlabel: Heart").
With your own league/commonmark environment or Str::markdown():
use Authorod\SvityloAnatomy\CommonMark\AnatomyExtension; $html = Str::markdown($text, [], [app(SvityloAnatomy::class)->extension()]); // or new AnatomyExtension(['data_url' => '/anatomy-data/1.0.0/', 'share_urls' => [...]])
The block syntax is in the Markdown package README. An invalid block stays an ordinary code block.
Sanitizer (Symfony HtmlSanitizer)
use Authorod\SvityloAnatomy\Sanitizer\AnatomySanitizer; use Symfony\Component\HtmlSanitizer\{HtmlSanitizer, HtmlSanitizerConfig}; $config = AnatomySanitizer::allowBlocks((new HtmlSanitizerConfig())->allowSafeElements()); $clean = (new HtmlSanitizer($config))->sanitize($storedHtml); echo app(SvityloAnatomy::class)->html($clean); // ```anatomy blocks → embeds
The order "sanitize, then transform" means that only the package creates embed markup: the
sanitizer removes an embed written into a note by hand (with a foreign data-url). allowEmbeds()
is for pipelines that sanitize embeds that are already rendered: it allows only their attributes,
always sets layout="embed", and sets data-url to the folder you pass
(AnatomySanitizer::allowEmbeds($config, config('svitylo-anatomy.data_url'))) or drops it, so an
embed in the input can never load data from another host.
Blade components
<x-svitylo-anatomy id="atlas" class="h-[80vh]" ui-lang="uk" /> <x-svitylo-anatomy-embed structure="cardiovascular.heart" :surroundings="2" label="Heart" caption="The heart in its surroundings" />
<x-svitylo-anatomy> passes its other attributes to the <svitylo-anatomy> element
(API). <x-svitylo-anatomy-embed> takes the fields of an ```anatomy block as
attributes (structure, `system`, `surroundings`, `view`, `state`, `label`, `caption`, `height`,
`lang`, `latin`) and renders nothing when a value is invalid. `:surroundings="2"` selects the
structure and shows the second level of its surroundings with the default transparency (78%).
Livewire
use Authorod\SvityloAnatomy\Livewire\InteractsWithAnatomy; use Livewire\Attributes\On; class Lesson extends Component { use InteractsWithAnatomy; public ?string $selected = null; public function showHeart(): void { // Only the heart, selected; then the first level of its surroundings at 78% transparency. $this->anatomy('atlas')->showStructure('cardiovascular.heart')->showSurroundings('cardiovascular.heart'); } public function widerView(): void { $this->anatomy('atlas')->setSurroundingsLevel(2)->setTransparency(0.5); } #[On('anatomy-select')] public function onSelect(?string $primary = null, ?string $atlas = null): void { $this->selected = $primary; } }
- Commands (
showStructure,showSystem,showSurroundings,setSurroundingsLevel,setTransparency,exitSurroundings,select,focusOn,setView,setState,reset,loadAll,isolate,clearIsolation,hide,show,showHidden) go to the browser as theanatomy-commandevent. The bridge (resources/js/livewire.js) runs only these methods; a test checks that its list matchesAnatomyCommands::METHODS. - Surroundings:
showSurroundings(?string $id)with an id places the structure on the scene, adds it to the selection and frames it; then it sets the surroundings level (the chosen one, else 1) and the transparency (the current one, else 0.78), and ends an isolation.setSurroundingsLevel(?int $level): 0 = only the selection, 1 = the nearest group of every selected structure … the last level = the whole body;null= the automatic level (everything placed on the scene is shown).setTransparency(float $value): transparency of everything that is not selected, from 0 (opaque) to 0.95.exitSurroundings(): back to the automatic level, everything opaque. - Atlas events arrive as the Livewire events
anatomy-select,anatomy-surroundingsandanatomy-statechange, with the fields of the atlas event plusatlas(the element id).anatomy-surroundingscarrieslevel(0 … the number of levels; null without a selection),explicit(the level was chosen),levels(group IDs of levels 1 … N;null= the whole body),transparency(0 = opaque) andsource. - Laravel does not drive the scene frame by frame: the scene changes only through commands.
Tests
composer install && composer test # PHPUnit + Testbench (Laravel 11–13, Livewire 4) pnpm test:laravel:e2e # from the root: Livewire browser tests on the workbench app
workbench/ is the test application: a note in a Livewire component, an atlas with commands and
events, and pages with @persist.
Licence and contributions
The project's own code is licensed under CPAL-1.0. Independent code in a Larger Work may remain closed; CPAL source and attribution obligations apply to the covered atlas code and its modifications. Data and dependencies retain their own licences. See the repository's licensing guide and language contribution guide. Partial translation drafts can be proposed through a PR; a new language also needs runtime integration.