Skip to content

Added a new setting to configure Mermaid JS version to use #5630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/Config/setting-defaults.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
'page-draft-color-dark' => '#a66ce8',
'app-custom-head' => false,
'registration-enabled' => false,
'enable-mermaid' => 'disabled',

// User-level default settings
'user' => [
Expand Down
63 changes: 63 additions & 0 deletions app/Plugins/MermaidProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace BookStack\Plugins;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;

class MermaidProvider
{
const MERMAID_REPOSITORY = 'https://api.github.com/repos/mermaid-js/mermaid/tags';
const MERMAID_CDN = 'https://cdn.jsdelivr.net/npm/mermaid@%s/dist/mermaid.min.js';

/**
* Retrieve the version from the Github of Mermaid
*/
public function getMermaidVersions()
{
$cacheKey = 'git_mermaid_versions';
$cachedVersions = Cache::get($cacheKey);

if (!is_null($cachedVersions)) {
return array_merge(['disabled', 'latest'], $cachedVersions);
}

$response = Http::withHeaders([
'Accept' => 'application/vnd.github+json',
'User-Agent' => 'BookStack',
])->get(MermaidProvider::MERMAID_REPOSITORY);

if ($response->successful()) {
$versions = collect($response->json())
->pluck('name')
->all();

Cache::put($cacheKey, $versions, now()->addHours(12));

return array_merge(['disabled', 'latest'], $versions);
}

return ['disabled', 'latest'];
}

/**
* Get the MermaidJS CDN URI to use.
*/
public function getMermaidJsCdnUri(): string
{
$mermaidJsVersion = setting('enable-mermaid');

if ($mermaidJsVersion === 'disabled') {
return '';
}

$localPath = public_path("mermaid/mermaid.min.js");

if (file_exists($localPath)) {
return asset("mermaid/mermaid.min.js");
}

return '';
}

}
57 changes: 57 additions & 0 deletions app/Settings/Plugins/MermaidController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace BookStack\Settings\Plugins;

use Illuminate\Http\Request;
use BookStack\Http\Controller;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use BookStack\Plugins\MermaidProvider;

class MermaidController extends Controller
{
public function store(Request $request)
{
$version = $request->input('version');

if (!$version || $version == 'disabled') {
return response()->json([
'success' => false,
'message' => 'No version specified.'
], Response::HTTP_BAD_REQUEST);
}

$remoteUrl = sprintf(MermaidProvider::MERMAID_CDN, $version);
$localDirectory = public_path('mermaid');
$localFilename = "mermaid.min.js";
$localPath = $localDirectory . '/' . $localFilename;

if (!File::exists($localDirectory)) {
File::makeDirectory($localDirectory, 0755, true);
}

try {
$response = Http::get($remoteUrl);

if ($response->successful()) {
File::put($localPath, $response->body());

return response()->json([
'success' => true,
'path' => asset("mermaid/$localFilename")
]);
} else {
return response()->json([
'success' => false,
'message' => 'Remote file not found.'
], Response::HTTP_NOT_FOUND);
}
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}
2 changes: 2 additions & 0 deletions lang/en/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
'app_disable_comments' => 'Disable Comments',
'app_disable_comments_toggle' => 'Disable comments',
'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',
'enable_mermaid' => 'Mermaid.js Version',
'enable_mermaid_desc' => 'Select the version of Mermaid.js to use when rendering diagrams. This allows compatibility with different diagram syntax versions and helps ensure consistent behavior across pages. Updating to a newer version may enable new features or fix rendering issues, but could also affect existing diagrams.',

// Color settings
'color_scheme' => 'Application Color Scheme',
Expand Down
24 changes: 24 additions & 0 deletions resources/views/exports/parts/mermaid-js.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@inject('mermaidProvider', 'BookStack\Plugins\MermaidProvider')

@if(setting('enable-mermaid') != 'disabled')
<!-- Mermaid JS external dependency initialization -->
<script src="{{ $mermaidProvider->getMermaidJsCdnUri() }}" nonce="{{ $cspNonce }}"></script>
<script nonce="{{ $cspNonce }}">
mermaid.initialize({ startOnLoad: true });
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('pre code.language-mermaid').forEach((block, i) => {
const parent = block.parentElement;
const graphCode = block.textContent;

const mermaidDiv = document.createElement('div');
mermaidDiv.classList.add('mermaid');
mermaidDiv.textContent = graphCode;

parent.replaceWith(mermaidDiv);
});

// Re-run Mermaid in case new diagrams were added
mermaid.init(undefined, document.querySelectorAll('.mermaid'));
});
</script>
@endif
3 changes: 3 additions & 0 deletions resources/views/layouts/base.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class="{{ setting()->getForCurrentUser('dark-mode-enabled') ? 'dark-mode ' : ''

<!-- Translations for JS -->
@stack('translations')

<!-- Mermaid JS -->
@include('layouts.parts.mermaid-js')
</head>
<body
@if(setting()->getForCurrentUser('ui-shortcuts-enabled', false))
Expand Down
2 changes: 2 additions & 0 deletions resources/views/layouts/export.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

@include('exports.parts.styles', ['format' => $format, 'engine' => $engine ?? ''])
@include('exports.parts.custom-head')

@include('layouts.parts.mermaid-js')
</head>
<body class="export export-format-{{ $format }} export-engine-{{ $engine ?? 'none' }}">
@include('layouts.parts.export-body-start')
Expand Down
25 changes: 25 additions & 0 deletions resources/views/layouts/parts/mermaid-js.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@inject('mermaidProvider', 'BookStack\Plugins\MermaidProvider')

@if(setting('enable-mermaid') != 'disabled' && !request()->routeIs('settings.category'))
<!-- Start: Mermaid JS external dependency initialization -->
<script src="{{ $mermaidProvider->getMermaidJsCdnUri() }}" nonce="{{ $cspNonce }}"></script>
<script nonce="{{ $cspNonce }}">
mermaid.initialize({ startOnLoad: true });
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('pre code.language-mermaid').forEach((block, i) => {
const parent = block.parentElement;
const graphCode = block.textContent;

const mermaidDiv = document.createElement('div');
mermaidDiv.classList.add('mermaid');
mermaidDiv.textContent = graphCode;

parent.replaceWith(mermaidDiv);
});

// Re-run Mermaid in case new diagrams were added
mermaid.init(undefined, document.querySelectorAll('.mermaid'));
});
</script>
<!-- End: Mermaid JS external dependency initialization -->
@endif
3 changes: 3 additions & 0 deletions resources/views/layouts/plain.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class="@yield('document-class')">
<!-- Custom Styles & Head Content -->
@include('layouts.parts.custom-styles')
@include('layouts.parts.custom-head')

<!-- Mermaid JS -->
@include('layouts.parts.mermaid-js')
</head>
<body>
@yield('content')
Expand Down
84 changes: 83 additions & 1 deletion resources/views/settings/categories/customization.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
@extends('settings.layout')

@inject('mermaidProvider', 'BookStack\Plugins\MermaidProvider')

<style>
.spinner-border {
display: inline-block;
width: 1em;
height: 1em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.75s linear infinite;
vertical-align: middle;
}
@keyframes spin {
100% { transform: rotate(360deg); }
}
.hide {
display: none !important;
}
</style>


@section('card')
<h1 id="customization" class="list-heading">{{ trans('settings.app_customization') }}</h1>
<form action="{{ url("/settings/customization") }}" method="POST" enctype="multipart/form-data">
Expand Down Expand Up @@ -165,12 +187,72 @@ class="simple-code-input">{{ setting('app-custom-head', '') }}</textarea>
</div>


<div class="grid half gap-xl items-center">
<div>
<label for="setting-enable-mermaid" class="setting-list-label">{{ trans('settings.enable_mermaid') }}</label>
<p class="small">{{ trans('settings.enable_mermaid_desc') }}</p>
</div>
<div class="mt-m">
<select name="setting-enable-mermaid" id="setting-enable-mermaid">
@foreach($mermaidProvider->getMermaidVersions() as $version)
<option value="{{ $version }}" @if(setting('enable-mermaid') === $version) selected @endif>
{{ $version }}
</option>
@endforeach
</select>
</div>
</div>


</div>

<div class="form-group text-right">
<button type="submit" class="button">{{ trans('settings.settings_save') }}</button>
<button type="submit" class="button" id="save-button">
<span class="spinner-border spinner-border-sm mr-xs hide" role="status" aria-hidden="true" id="save-spinner"></span>
{{ trans('settings.settings_save') }}
</button>
</div>
</form>

<script nonce="{{ $cspNonce }}">
document.addEventListener('DOMContentLoaded', function () {
const select = document.getElementById('setting-enable-mermaid');
const saveButton = document.getElementById('save-button');
const spinner = document.getElementById('save-spinner');
select.addEventListener('change', function () {

const selectedVersion = this.value;

if (selectedVersion === 'disabled')
return

saveButton.disabled = true;
spinner.classList.remove('hide');

fetch(`{{ route("settings.plugins.mermaid.download") }}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
body: JSON.stringify({ version: selectedVersion })
})
.then(res => res.json())
.then(data => {
if (!data.success) {
console.error(data);
}
})
.catch(err => {
console.error(err);
})
.finally(() => {
spinner.classList.add('hide');
saveButton.disabled = false;
});
});
});
</script>
@endsection

@section('after-content')
Expand Down
30 changes: 16 additions & 14 deletions routes/web.php
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
<?php

use BookStack\Access\Controllers as AccessControllers;
use BookStack\Activity\Controllers as ActivityControllers;
use BookStack\Api\ApiDocsController;
use BookStack\Api\UserApiTokenController;
use BookStack\App\HomeController;
use BookStack\App\MetaController;
use BookStack\Entities\Controllers as EntityControllers;
use BookStack\Exports\Controllers as ExportControllers;
use BookStack\Http\Middleware\VerifyCsrfToken;
use BookStack\Permissions\PermissionsController;
use BookStack\References\ReferenceController;
use BookStack\Api\ApiDocsController;
use Illuminate\Support\Facades\Route;
use BookStack\Search\SearchController;
use BookStack\Settings as SettingControllers;
use BookStack\Sorting as SortingControllers;
use BookStack\Theming\ThemeController;
use BookStack\Uploads\Controllers as UploadControllers;
use BookStack\Users\Controllers as UserControllers;
use BookStack\Api\UserApiTokenController;
use BookStack\Sorting as SortingControllers;
use BookStack\References\ReferenceController;
use BookStack\Settings as SettingControllers;
use BookStack\Http\Middleware\VerifyCsrfToken;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Route;
use BookStack\Permissions\PermissionsController;
use SettingControllers\Plugins\MermaidController;
use BookStack\Users\Controllers as UserControllers;
use BookStack\Access\Controllers as AccessControllers;
use Illuminate\View\Middleware\ShareErrorsFromSession;
use BookStack\Exports\Controllers as ExportControllers;
use BookStack\Uploads\Controllers as UploadControllers;
use BookStack\Entities\Controllers as EntityControllers;
use BookStack\Activity\Controllers as ActivityControllers;

// Status & Meta routes
Route::get('/status', [SettingControllers\StatusController::class, 'show']);
Expand Down Expand Up @@ -308,6 +309,7 @@
Route::get('/settings', [SettingControllers\SettingController::class, 'index'])->name('settings');
Route::get('/settings/{category}', [SettingControllers\SettingController::class, 'category'])->name('settings.category');
Route::post('/settings/{category}', [SettingControllers\SettingController::class, 'update']);
Route::post('/settings/plugins/mermaid/download', [SettingControllers\Plugins\MermaidController::class, 'store'])->name('settings.plugins.mermaid.download');
});

// MFA routes
Expand Down
Loading