├── .releaserc.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin ├── build.js └── grammars.js ├── composer.json ├── config └── filament-knowledge-base.php ├── database ├── factories │ └── ModelFactory.php └── migrations │ └── create_filament_knowledge_base_table.php.stub ├── dist └── js │ ├── anchors-component.js │ └── modals-component.js ├── package-lock.json ├── package.json ├── pint.json ├── resources ├── js │ ├── anchors-component.js │ └── modals-component.js ├── lang │ ├── ar │ │ └── translations.php │ ├── cs │ │ └── translations.php │ ├── da │ │ └── translations.php │ ├── de │ │ └── translations.php │ ├── en │ │ └── translations.php │ ├── fa │ │ └── translations.php │ ├── fr │ │ └── translations.php │ ├── nl │ │ └── translations.php │ ├── pt_BR │ │ └── translations.php │ ├── pt_PT │ │ └── translations.php │ ├── ru │ │ └── translations.php │ └── uk │ │ └── translations.php └── views │ ├── .gitkeep │ ├── code-block.blade.php │ ├── components │ └── content.blade.php │ ├── documentation.blade.php │ ├── livewire │ ├── help-menu.blade.php │ └── modals.blade.php │ ├── modals.blade.php │ ├── pages │ └── section.blade.php │ └── sidebar-action.blade.php ├── src ├── Actions │ ├── Forms │ │ └── Components │ │ │ └── HelpAction.php │ └── HelpAction.php ├── Commands │ └── MakeDocumentationCommand.php ├── Concerns │ ├── CanDisableBackToDefaultPanelButton.php │ ├── CanDisableBreadcrumbs.php │ ├── CanDisableDefaultClasses.php │ ├── CanDisableKnowledgeBasePanelButton.php │ ├── CanDisableModalLinks.php │ ├── HasAnchorSymbol.php │ ├── HasArticleClass.php │ ├── HasDocumentable.php │ └── HasModalPreviews.php ├── Contracts │ ├── Documentable.php │ └── HasKnowledgeBase.php ├── Documentation.php ├── Enums │ └── TableOfContentsPosition.php ├── Facades │ └── KnowledgeBase.php ├── Filament │ ├── Pages │ │ └── ViewDocumentation.php │ ├── Panels │ │ └── KnowledgeBasePanel.php │ └── Resources │ │ └── DocumentationResource.php ├── KnowledgeBase.php ├── KnowledgeBasePlugin.php ├── KnowledgeBaseServiceProvider.php ├── Livewire │ ├── HelpMenu.php │ └── Modals.php ├── Markdown │ ├── MarkdownRenderer.php │ ├── Parsers │ │ ├── IncludeParser.php │ │ └── VariableParser.php │ └── Renderers │ │ ├── FencedCodeRenderer.php │ │ └── ImageRenderer.php ├── Models │ ├── FlatfileDocumentation.php │ └── RelationalDocumentation.php ├── Providers │ └── KnowledgeBasePanelProvider.php └── helpers.php └── stubs ├── documentation.php.stub └── markdown.md.stub /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@semantic-release/commit-analyzer", 5 | { 6 | "preset": "conventionalcommits", 7 | "releaseRules": [ 8 | { 9 | "type": "feat", 10 | "release": "minor" 11 | }, 12 | { 13 | "type": "fix", 14 | "release": "patch" 15 | }, 16 | { 17 | "type": "docs", 18 | "scope": "README", 19 | "release": "patch" 20 | }, 21 | { 22 | "type": "refactor", 23 | "release": "patch" 24 | }, 25 | { 26 | "type": "chore", 27 | "release": "patch" 28 | }, 29 | { 30 | "type": "style", 31 | "release": false 32 | }, 33 | { 34 | "type": "perf", 35 | "release": "patch" 36 | }, 37 | { 38 | "type": "test", 39 | "release": false 40 | }, 41 | { 42 | "scope": "no-release", 43 | "release": false 44 | } 45 | ] 46 | } 47 | ], 48 | [ 49 | "@semantic-release/release-notes-generator", 50 | { 51 | "preset": "conventionalcommits", 52 | "presetConfig": { 53 | "types": [ 54 | { 55 | "type": "feat", 56 | "section": "Features" 57 | }, 58 | { 59 | "type": "fix", 60 | "section": "Bug Fixes" 61 | }, 62 | { 63 | "type": "refactor", 64 | "section": "Refactor" 65 | }, 66 | { 67 | "type": "docs", 68 | "section": "Documentation" 69 | }, 70 | { 71 | "type": "chore", 72 | "section": "Chore" 73 | }, 74 | { 75 | "type": "style", 76 | "section": "Style" 77 | }, 78 | { 79 | "type": "perf", 80 | "section": "Performance" 81 | }, 82 | { 83 | "type": "test", 84 | "section": "Tests" 85 | } 86 | ] 87 | } 88 | } 89 | ], 90 | "@semantic-release/github" 91 | ], 92 | "branches": [ 93 | "+([0-9])?(.{+([0-9]),x}).x", 94 | "main", 95 | "master", 96 | { 97 | "name": "beta", 98 | "prerelease": true 99 | }, 100 | { 101 | "name": "alpha", 102 | "prerelease": true 103 | } 104 | ], 105 | "tagFormat": "${version}" 106 | } 107 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `filament-knowledge-base` will be documented in this file. 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Guava 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![filament-knowledge-base Banner](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/banner.jpg) 2 | 3 | # A filament plugin that adds a knowledge base and documentation to your filament panel(s). 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/guava/filament-knowledge-base.svg?style=flat-square)](https://packagist.org/packages/guava/filament-knowledge-base) 6 | [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/guavaCZ/filament-knowledge-base/run-tests.yml?branch=main&label=tests&style=flat-square)](https://github.com/guavaCZ/filament-knowledge-base/actions?query=workflow%3Arun-tests+branch%3Amain) 7 | [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/guavaCZ/filament-knowledge-base/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/guavaCZ/filament-knowledge-base/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/guava/filament-knowledge-base.svg?style=flat-square)](https://packagist.org/packages/guava/filament-knowledge-base) 9 | 10 | Did your filament panel ever get complex real quick? Ever needed a place to document all your features in one place? 11 | 12 | Filament Knowledge Base is here for exactly this reason! 13 | 14 | Using our Knowledge Base package, you can write markdown documentation files to document every feature of your package 15 | and give your users a comprehensive knowledge base tailored for your product. Right inside Filament! 16 | 17 | ## Showcase 18 | 19 | ![Showcase 01](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_01.jpeg) 20 | ![Showcase 02](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_02.jpeg) 21 | ![Showcase 03](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_03.png) 22 | ![Modal Slideover Example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_modal_slideovers.jpeg) 23 | ![Modal Previews Example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_modal_previews.jpeg) 24 | 25 | For a better understanding of how it works, please have a look at the video showcase: 26 | 27 | 30 | 31 | 32 | https://github.com/GuavaCZ/filament-knowledge-base/assets/10926334/cf9ebb59-aaf9-4e30-ad17-2832da4b9488 33 | 34 | ## Support us 35 | 36 | Your support is key to the continual advancement of our plugin. We appreciate every user who has contributed to our 37 | journey so far. 38 | 39 | While our plugin is available for all to use, if you are utilizing it for commercial purposes and believe it adds 40 | significant value to your business, we kindly ask you to consider supporting us through GitHub Sponsors. This 41 | sponsorship will assist us in continuous development and maintenance to keep our plugin robust and up-to-date. Any 42 | amount you contribute will greatly help towards reaching our goals. Join us in making this plugin even better and 43 | driving further innovation. 44 | 45 | ## Installation 46 | 47 | You can install the package via composer: 48 | 49 | ```bash 50 | composer require guava/filament-knowledge-base 51 | ``` 52 | 53 | Make sure to publish the package assets using: 54 | 55 | ```bash 56 | php artisan filament:assets 57 | ``` 58 | 59 | and translations with: 60 | 61 | ```bash 62 | php artisan vendor:publish --tag="filament-knowledge-base-translations" 63 | ``` 64 | 65 | You can publish the config file with: 66 | 67 | ```bash 68 | php artisan vendor:publish --tag="filament-knowledge-base-config" 69 | ``` 70 | 71 | This is the contents of the published config file: 72 | 73 | ```php 74 | return [ 75 | 'panel' => [ 76 | 'id' => env('FILAMENT_KB_ID', 'knowledge-base'), 77 | 'path' => env('FILAMENT_KB_PATH', 'kb'), 78 | ], 79 | 80 | 'docs-path' => env('FILAMENT_KB_DOCS_PATH', 'docs'), 81 | 82 | 'model' => \Guava\FilamentKnowledgeBase\Models\FlatfileDocumentation::class, 83 | 84 | 'cache' => [ 85 | 'prefix' => env('FILAMENT_KB_CACHE_PREFIX', 'filament_kb_'), 86 | 'ttl' => env('FILAMENT_KB_CACHE_TTL', 86400), 87 | ], 88 | ]; 89 | ``` 90 | 91 | ## Introduction 92 | 93 | ### Knowledge Base Panel 94 | 95 | We register a separate panel for your entire Knowledge Base. This way you have a single place where you can in detail 96 | document your functionalities. 97 | 98 | ### Modal Previews 99 | 100 | Instead of redirecting the user to the documentation immediately, the package offers `modal previews`, which render the 101 | markdown in a customizable modal with an optional button to open the full documentation page. 102 | 103 | You can learn how to enable this feature in the `Customizations` section. 104 | 105 | ### Global Search 106 | 107 | Knowledge base supports global search for all your markdown files and by default looks through the `title` and 108 | the `content` of the markdown file. This way your users can quickly find what they are looking for. 109 | 110 | ## Storage 111 | 112 | We currently support flat file (stored inside the source project) storage out of the box. 113 | 114 | You can choose to store your documentation in: 115 | 116 | - Markdown files (Preferred method) 117 | - PHP classes (for complex cases) 118 | 119 | In the future, we plan to also ship a Database Driver so you can store your documentation in the database. 120 | 121 | ## Usage 122 | 123 | ### Register plugin 124 | 125 | To begin, register the `KnowledgeBasePlugin` in all your Filament Panels from which you want to access your Knowledge 126 | Base / Documentation. 127 | 128 | ```php 129 | use Filament\Panel; 130 | use Guava\FilamentKnowledgeBase\KnowledgeBasePlugin; 131 | 132 | public function panel(Panel $panel): Panel 133 | { 134 | return $panel 135 | ->plugins([ 136 | // ... 137 | KnowledgeBasePlugin::make(), 138 | ]) 139 | } 140 | ``` 141 | 142 | ### Make sure you have a custom filament theme 143 | 144 | Check [here](https://filamentphp.com/docs/3.x/panels/themes#creating-a-custom-theme) how to create one. 145 | 146 | You can create one specifically for the knowledge base panel or if you want to have the same design as your main panel( 147 | s), you can simply reuse the vite theme from your panel. 148 | 149 | Then in the register method of your `AppServiceProvider`, configure the vite theme of the knowledge base panel using: 150 | 151 | ```php 152 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 153 | 154 | KnowledgeBasePanel::configureUsing( 155 | fn(KnowledgeBasePanel $panel) => $panel 156 | ->viteTheme('resources/css/filament/admin/theme.css') // your filament vite theme path here 157 | ); 158 | ``` 159 | 160 | ### Build CSS 161 | 162 | In every filament theme, make sure to include the plugin's php and blade files in the `tailwind.config.js`, so the CSS 163 | is correctly built: 164 | 165 | ```js 166 | { 167 | content: [ 168 | //... 169 | 170 | './vendor/guava/filament-knowledge-base/src/**/*.php', 171 | './vendor/guava/filament-knowledge-base/resources/**/*.blade.php', 172 | ] 173 | } 174 | ``` 175 | 176 | ### Create documentation 177 | 178 | To create your first documentation, simply run the `docs:make`, such as: 179 | 180 | ```bash 181 | php artisan docs:make prologue.getting-started 182 | ``` 183 | 184 | This will create a file in `/docs/en/prologue/getting-started.md`. 185 | 186 | If you want to create the file for a specific locale, you can do so using the `--locale` option (can be repeated for 187 | multiple locales): 188 | 189 | ```bash 190 | php artisan docs:make prologue.getting-started --locale=de --locale=en 191 | ``` 192 | 193 | This would create the file for both the `de` and `en` locale. 194 | 195 | If you **don't** pass any locale, it will automatically create the documentation file for every locale 196 | in `/docs/{locale}`. 197 | 198 | ### Markdown 199 | 200 | After you generate your documentation file, it's time to edit it. 201 | 202 | A markdown file consists of two sections, the `Front Matter` and `Content`. 203 | 204 | #### Front Matter 205 | 206 | In the front matter, you can customize the documentation page, such as the title, the icon and so on: 207 | 208 | ```md 209 | --- 210 | // Anything between these dashes is the front matter 211 | title: My example documentation 212 | icon: heroicon-o-book-open 213 | --- 214 | ``` 215 | 216 | #### Front Matter Options 217 | Below is a list of currently available options in the front matter. 218 | 219 | #### Title 220 | Allows you to modify the title of the documentation page. 221 | ```md 222 | --- 223 | title: My new title 224 | --- 225 | ``` 226 | 227 | #### Icon 228 | Allows you to modify the icon of the documentation page. 229 | 230 | You can use any name supported by Blade UI icons, that you have installed. 231 | 232 | ```md 233 | --- 234 | icon: heroicon-o-user 235 | --- 236 | ``` 237 | 238 | #### Order 239 | Allows you to modify the order of the documentation page within it's parent / group. 240 | 241 | A lower number will be displayed first. 242 | 243 | ```md 244 | --- 245 | order: 1 246 | --- 247 | ``` 248 | 249 | #### Group 250 | Allows you to define the group (and it's title) of the documentation page. 251 | 252 | ```md 253 | --- 254 | group: Getting Started 255 | --- 256 | ``` 257 | 258 | #### Parent 259 | Allows you to define the parent of the documentation page. 260 | 261 | ```md 262 | --- 263 | parent: my-parent 264 | --- 265 | ``` 266 | 267 | So for a file in `docs/en/prologue/getting-started/intro.md`, the parent would be `getting-started`. 268 | 269 | #### Content 270 | 271 | Anything after the front matter is your content, written in markdown: 272 | 273 | ```md 274 | --- 275 | // Front matter ... 276 | --- 277 | 278 | # Introduction 279 | 280 | Lorem ipsum dolor .... 281 | ``` 282 | 283 | And that's it! You've created a simple knowledge base inside Filament. 284 | 285 | ### Accessing the knowledge base 286 | 287 | In every panel you registered the Knowlege Base plugin, we automatically inject a documentation button at the very 288 | bottom of the sidebar. 289 | 290 | ![Documentation button example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_documentation_button.png) 291 | 292 | But we offer a deeper integration to your panels. 293 | 294 | #### Integrating into resources or pages 295 | 296 | You will most likely have a section in your knowledge base dedicated to each of your resources (at least to the more 297 | complex ones). 298 | 299 | To integrate your resource with the documentation, all you need to do is implement the `HasKnowledgeBase` contract in 300 | your resource or page. 301 | 302 | This will require you to implement the `getDocumentation` method, where you simply return the documentation pages you 303 | want to integrate. You can either return the `IDs` as strings (dot-separated path inside `/docs/{locale}/`) or use the 304 | helper to retrieve the model: 305 | 306 | ```php 307 | use Guava\FilamentKnowledgeBase\Contracts\HasKnowledgeBase; 308 | use Guava\FilamentKnowledgeBase\Facades\KnowledgeBase; 309 | class UserResource extends Resource implements HasKnowledgeBase 310 | { 311 | // ... 312 | 313 | // 314 | public static function getDocumentation(): array 315 | { 316 | return [ 317 | 'users.introduction', 318 | 'users.authentication', 319 | KnowledgeBase::model()::find('users.permissions'), 320 | ]; 321 | } 322 | } 323 | ``` 324 | 325 | This will render a `Help menu` button at the end of the top navbar. 326 | 327 | If you add more than one documentation file, it will render a dropdown menu, otherwise the `help` button will directly 328 | reference the documentation you linked. 329 | 330 | ![Documentation button example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_help_menu.png) 331 | 332 | #### Opening documentations in modals 333 | 334 | From any livewire component where you use the documentation pages (you have rendered the Help Menu), you can create links that will automatically open the documentation 335 | in a modal, by simply adding this fragment to the `href` attribute of the link: 336 | 337 | ```html 338 | #modal- 339 | 340 | ``` 341 | 342 | such as 343 | 344 | ```html 345 | Open Introduction 346 | ``` 347 | 348 | ### Modal Links 349 | 350 | To make it easy to access the documentation from anywhere, this plugin adds intercepts fragment links anywhere in the filament panel in order to open up a modal for a 351 | documentation page. 352 | 353 | To use modal links, simply add a link in **any place** in your panel with a fragment in the format `#modal-`, such as `#modal-intro.getting-started`, 354 | for example: 355 | 356 | ```html 357 | Open Introduction 358 | ``` 359 | 360 | As long as a documentation with that ID exists (/docs/en/intro/getting-started.md), it will automatically open a modal with the content of that documentation. 361 | 362 | You can even share the URL with someone and it will automatically open the modal upon opening! 363 | 364 | ![Modal links example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_modal_links.gif) 365 | 366 | #### Disabling Modal Links 367 | 368 | to disable modal links, simply call `disableModalLinks()` on the KnowledgeBasePlugin in your panel Service Provider.. 369 | 370 | ### Help Actions 371 | 372 | The plugin comes with a neat `HelpAction`, which can be linked to a specific markdown file or even a partial markdown 373 | file. 374 | 375 | For example, the `What is a slug?` help was added using the following: 376 | 377 | ```php 378 | use Guava\FilamentKnowledgeBase\Actions\Forms\Components\HelpAction; 379 | ->hintAction(HelpAction::forDocumentable('projects.creating-projects.slug') 380 | ->label('What is a slug?') 381 | ->slideOver(false) 382 | ), 383 | ``` 384 | 385 | ### Accessing the documentation models 386 | 387 | We use the `sushi` package in the background to store the documentations. This way, they behave almost like 388 | regular `Eloquent models`. 389 | 390 | #### Get model using our helper 391 | 392 | To get the model, simply use our helper `KnowledgeBase::model()`: 393 | 394 | ```php 395 | use \Guava\FilamentKnowledgeBase\KnowledgeBase; 396 | 397 | // find specific model 398 | KnowledgeBase::model()::find(''); 399 | // query models 400 | KnowledgeBase::model()::query()->where('title', 'Some title'); 401 | // etc. 402 | ``` 403 | 404 | ## Cache 405 | 406 | By default, the package caches all markdown files to ensure a smooth and fast user experience. If you don't see your 407 | changes, make sure to clear the cache: 408 | 409 | ```bash 410 | php artisan cache:clear 411 | ``` 412 | 413 | ## Customization 414 | 415 | A lot of the functionalities can be customized to a certain extent. 416 | 417 | ### Customize the knowledge base panel 418 | 419 | You can customize the knowledge base panel to your liking using: 420 | 421 | ```php 422 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 423 | 424 | KnowledgeBasePanel::configureUsing( 425 | fn(KnowledgeBasePanel $panel) => $panel 426 | // Your options here 427 | ); 428 | ``` 429 | 430 | #### Change brand name 431 | 432 | For example to change the default brand name/title (displayed in the top left) of the panel, you can do: 433 | 434 | ```php 435 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 436 | 437 | KnowledgeBasePanel::configureUsing( 438 | fn(KnowledgeBasePanel $panel) => $panel 439 | ->brandName('My Docs') 440 | ); 441 | ``` 442 | 443 | ### Custom classes on documentation article 444 | 445 | By default, the documentation article (the container where the markdown content is rendered) has a `gu-kb-article` class, which you can use to target and modify. You can 446 | also add your own class(es) using: 447 | 448 | ```php 449 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 450 | 451 | KnowledgeBasePanel::configureUsing( 452 | fn(KnowledgeBasePanel $panel) => $panel 453 | ->articleClass('max-w-2xl') 454 | ); 455 | ``` 456 | 457 | #### Disable default classes 458 | 459 | To disable the default styling altogether, you can use: 460 | 461 | ```php 462 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 463 | 464 | KnowledgeBasePanel::configureUsing( 465 | fn(KnowledgeBasePanel $panel) => $panel 466 | ->disableDefaultClasses() 467 | ); 468 | ``` 469 | 470 | ### Disable the knowledge base panel button 471 | 472 | When in a panel where the Knowledge Base plugin is enabled, we render by default in the bottom of the sidebar a button to go to the knowledge base panel. You can disable 473 | it if you like: 474 | 475 | ```php 476 | use \Filament\View\PanelsRenderHook; 477 | 478 | $plugin->disableKnowledgeBasePanelButton(); 479 | ``` 480 | 481 | ### Disable the back to default panel button 482 | 483 | When in the knowledge base panel, a similar button is rendered to go back to the default filament panel. You can disable it likewise: 484 | 485 | ```php 486 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 487 | 488 | KnowledgeBasePanel::configureUsing( 489 | fn(KnowledgeBasePanel $panel) => $panel 490 | ->disableBackToDefaultPanelButton() 491 | ); 492 | ``` 493 | 494 | ### Customize the help menu/button render hook 495 | 496 | If you want to place the help menu / button someplace else, you can override the render hook: 497 | 498 | ```php 499 | use \Filament\View\PanelsRenderHook; 500 | 501 | $plugin->helpMenuRenderHook(PanelsRenderHook::TOPBAR_START); 502 | ``` 503 | 504 | ### Table of contents 505 | 506 | By default, in each documentation article there is a table of contents sidebar on the right. 507 | 508 | #### Disabling table of contents 509 | 510 | ```php 511 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 512 | 513 | KnowledgeBasePanel::configureUsing( 514 | fn(KnowledgeBasePanel $panel) => $panel 515 | ->disableTableOfContents() 516 | ); 517 | ``` 518 | 519 | #### Changing the position of the table of contents 520 | 521 | ```php 522 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 523 | use Guava\FilamentKnowledgeBase\Enums\TableOfContentsPosition; 524 | KnowledgeBasePanel::configureUsing( 525 | fn(KnowledgeBasePanel $panel) => $panel 526 | ->tableOfContentsPosition(TableOfContentsPosition::Start) 527 | ); 528 | ``` 529 | 530 | ### Anchors 531 | 532 | #### Customizing the anchor symbol 533 | 534 | By default we use the `#` symbol. You can customize the symbol using: 535 | 536 | ```php 537 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 538 | 539 | KnowledgeBasePanel::configureUsing( 540 | fn(KnowledgeBasePanel $panel) => $panel 541 | ->anchorSymbol('¶') 542 | ); 543 | ``` 544 | 545 | #### Disabling anchors 546 | 547 | We render an anchor prefix (#) in front of every heading. To disable it, you can do: 548 | 549 | ```php 550 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 551 | 552 | KnowledgeBasePanel::configureUsing( 553 | fn(KnowledgeBasePanel $panel) => $panel 554 | ->disableAnchors() 555 | ); 556 | ``` 557 | 558 | ### Enable modal previews 559 | 560 | If you want to open documentations in modal previews instead of immediately redirecting to the full pages, you can 561 | enable it like this: 562 | 563 | ```php 564 | $plugin->modalPreviews(); 565 | ``` 566 | 567 | ![Modal Previews Example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_modal_previews.jpeg) 568 | 569 | #### Slide overs 570 | 571 | If you prefer to use slide overs, you can additionally also enable them: 572 | 573 | ```php 574 | $plugin->slideOverPreviews(); 575 | ``` 576 | 577 | ![Modal Slideover Example](/docs/images/screenshot_modal_slideovers.jpeg) 578 | 579 | ### Breadcrubs 580 | 581 | #### Disable breadcrumbs 582 | 583 | By default on each documentation page, there is a breadcrumb at the top. You can disable it if you wish: 584 | 585 | ```php 586 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 587 | 588 | KnowledgeBasePanel::configureUsing( 589 | fn(KnowledgeBasePanel $panel) => $panel 590 | ->disableBreadcrumbs() 591 | ); 592 | ``` 593 | 594 | #### Enable breadcrumbs in modal preview titles 595 | 596 | When using modal previews, by default the title shows just that, the title of the documentation page. 597 | 598 | If you'd rather show the full breadcrumb to the documentation page, you may enable it like so: 599 | 600 | ```php 601 | $plugin->modalTitleBreadcrumbs(); 602 | ``` 603 | 604 | ![Modal Breadcrumbs Example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_modal_breadcrumbs.jpeg) 605 | 606 | ### Open documentation links in new tab 607 | 608 | When you open a documentation, by default it will be opened in the same tab. 609 | 610 | To change this, you can customize your plugin: 611 | 612 | ```php 613 | $plugin->openDocumentationInNewTab() 614 | ``` 615 | 616 | ### Guest Access 617 | 618 | By default, the panel is only accessible to authenticated users. 619 | 620 | If you want the knowledge base to be publicly accessible, simply configure it like so: 621 | 622 | ```php 623 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 624 | 625 | KnowledgeBasePanel::configureUsing( 626 | fn(KnowledgeBasePanel $panel) => $panel 627 | ->guestAccess() 628 | ); 629 | ``` 630 | 631 | ## Markdown 632 | 633 | We use CommonMark as the markdown parser and the league/commonmark php implementation. Check their respective websites 634 | for a reference on how to use markdown. 635 | 636 | - [CommonMark](https://commonmark.org/) 637 | - [League CommonMark](https://commonmark.thephpleague.com/) 638 | 639 | We also added some custom parsers/extensions to the Markdown Parser, described below. 640 | 641 | ### Markers support 642 | 643 | In order to mark some words with your primary theme color, you can use the following syntax: 644 | 645 | ``` 646 | In this example, ==this text== will be marked. 647 | ``` 648 | 649 | The result looks like this, depending on your primary color: 650 | 651 | ![Marker example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_marker.png) 652 | 653 | ### Tables support 654 | 655 | You can use the regular markdown syntax to render tables styled to match filament tables. 656 | 657 | ```md 658 | | Syntax | Description (center) | Foo (right) | Bar (left) | 659 | |------------|:---------------------------------------------:|----------------:|:----------------| 660 | | Header | Title | Something | Else | 661 | | Paragraphs | First paragraph.

Second paragraph. | First paragraph | First paragraph | 662 | ``` 663 | 664 | ![Tables example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_tables.png) 665 | 666 | ### Quotes support 667 | 668 | Using the regular markdown syntax for quotes, you can render neat banners such as: 669 | 670 | ```md 671 | > ⚠️ **Warning:** Make sure that the slug is unique! 672 | ``` 673 | 674 | ![Quotes example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_quotes.png) 675 | 676 | ### Syntax Highlighting 677 | 678 | We offer syntax highlighting through shiki (requires NodeJS on the server) 679 | 680 | - [ShikiJS](https://shiki.style/) 681 | - [Spatie ShikiPHP](https://github.com/spatie/shiki-php) 682 | 683 | **Note:** Because of the additional installation steps, syntax highlighting is disabled by default. 684 | 685 | To enable it, you MUST have both the npm package `shiki` and `spatie/shiki-php` installed. 686 | 687 | Which versions of the shiki packages to choose depends on you. I **highly recommend going with the latest versions**, 688 | but if you encounter some issues due to incompatibility with other packages, you might need to downgrade. 689 | 690 | Check the table below for compatible versions. 691 | 692 | | Shiki PHP Version | Shiki JS Version | 693 | |-------------------|------------------| 694 | | ^2.0 | ^1.0 | 695 | | ^1.3 | ^0.14 | 696 | 697 | #### Installing spatie/shiki-php: 698 | 699 | ```bash 700 | composer require spatie/shiki-php:"^2.0" 701 | ``` 702 | 703 | #### Installing shiki: 704 | 705 | ```bash 706 | npm install shiki@^1.0 707 | ``` 708 | 709 | #### When using a Node Version Manager: 710 | 711 | If you use Herd or another NVM, you will most likely need to create a symlink to your node version. Please follow the 712 | instructions [here](https://github.com/spatie/shiki-php?tab=readme-ov-file#using-node-version-manager). 713 | 714 | Then you can enable syntax highlighting using: 715 | 716 | ```php 717 | use Guava\FilamentKnowledgeBase\Filament\Panels\KnowledgeBasePanel; 718 | 719 | KnowledgeBasePanel::configureUsing( 720 | fn(KnowledgeBasePanel $panel) => $panel 721 | ->syntaxHighlighting() 722 | ); 723 | ``` 724 | 725 | ![Syntax highlighting example](https://github.com/GuavaCZ/filament-knowledge-base/raw/main/docs/images/screenshot_syntax_highlighting.png) 726 | 727 | ### Vite assets support 728 | 729 | You can use the default image syntax to include vite assets, as long as you provide the full path from your root project 730 | directory: 731 | 732 | ```md 733 | ![my image](/resources/img/my-image.png) 734 | ``` 735 | 736 | ### Including other files 737 | 738 | We support including markdown files within other files. This is especially useful if you want to organize your markdown 739 | or display snippets of a whole documentation as a help button without duplicating your markdown files. 740 | 741 | The syntax is as follows: 742 | 743 | ```md 744 | @include(prologue.getting-started) 745 | ``` 746 | 747 | This is extremely helpful when you want to display help buttons for a concrete component or field, but don't want to 748 | deal with duplicated information. 749 | 750 | You can simply extract parts of your markdown into smaller markdown files and include them in your main file. That way 751 | you can only display the partials in your `Help Actions`. 752 | 753 | ## Testing 754 | 755 | ```bash 756 | composer test 757 | ``` 758 | 759 | ## Changelog 760 | 761 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 762 | 763 | ## Contributing 764 | 765 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 766 | 767 | ## Security Vulnerabilities 768 | 769 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 770 | 771 | ## Credits 772 | 773 | - [Lukas Frey](https://github.com/GuavaCZ) 774 | - [All Contributors](../../contributors) 775 | - Spatie - Our package skeleton is a modified version 776 | of [Spatie's Package Tools](https://github.com/spatie/laravel-package-tools) 777 | - Spatie shiki and markdown packages 778 | 779 | ## License 780 | 781 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 782 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild' 2 | 3 | const isDev = process.argv.includes('--dev') 4 | 5 | async function compile(options) { 6 | const context = await esbuild.context(options) 7 | 8 | if (isDev) { 9 | await context.watch() 10 | } else { 11 | await context.rebuild() 12 | await context.dispose() 13 | } 14 | } 15 | 16 | const defaultOptions = { 17 | define: { 18 | 'process.env.NODE_ENV': isDev ? `'development'` : `'production'`, 19 | }, 20 | bundle: true, 21 | mainFields: ['module', 'main'], 22 | platform: 'neutral', 23 | sourcemap: isDev ? 'inline' : false, 24 | sourcesContent: isDev, 25 | treeShaking: true, 26 | target: ['es2020'], 27 | minify: !isDev, 28 | plugins: [{ 29 | name: 'watchPlugin', 30 | setup: function (build) { 31 | build.onStart(() => { 32 | console.log(`Build started at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`) 33 | }) 34 | 35 | build.onEnd((result) => { 36 | if (result.errors.length > 0) { 37 | console.log(`Build failed at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`, result.errors) 38 | } else { 39 | console.log(`Build finished at ${new Date(Date.now()).toLocaleTimeString()}: ${build.initialOptions.outfile}`) 40 | } 41 | }) 42 | } 43 | }], 44 | } 45 | 46 | compile({ 47 | ...defaultOptions, 48 | entryPoints: ['./resources/js/anchors-component.js'], 49 | outfile: './dist/js/anchors-component.js', 50 | }) 51 | 52 | compile({ 53 | ...defaultOptions, 54 | entryPoints: ['./resources/js/modals-component.js'], 55 | outfile: './dist/js/modals-component.js', 56 | }) 57 | -------------------------------------------------------------------------------- /bin/grammars.js: -------------------------------------------------------------------------------- 1 | const args = process.argv.slice(2); 2 | 3 | async function main(args) { 4 | const {grammars} = await import('tm-grammars'); 5 | const language = args[0]; 6 | const meta = grammars 7 | .find(obj => obj.name === language || 8 | (obj.hasOwnProperty('aliases') && obj.aliases.includes(language)) 9 | ); 10 | process.stdout.write(meta ? meta.displayName : 'Text'); 11 | } 12 | 13 | 14 | main(args) 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guava/filament-knowledge-base", 3 | "description": "A filament plugin that adds a knowledge base and help to your filament panel(s).", 4 | "keywords": [ 5 | "Guava", 6 | "laravel", 7 | "filament-knowledge-base" 8 | ], 9 | "homepage": "https://github.com/guava/filament-knowledge-base", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Lukas Frey", 14 | "email": "lukas.frey@guava.cz", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1|^8.2", 20 | "calebporzio/sushi": "^2.5", 21 | "filament/filament": "^3.2|^3.3", 22 | "illuminate/contracts": "^10.0|^11.0|^12.0", 23 | "league/commonmark": "^2.4", 24 | "n0sz/commonmark-marker-extension": "^1.0", 25 | "spatie/laravel-package-tools": "^1.14.0", 26 | "spatie/php-structure-discoverer": "^2.1", 27 | "symfony/yaml": "^7.0" 28 | }, 29 | "suggest": { 30 | "spatie/shiki-php": "Required to support syntax highlighting" 31 | }, 32 | "require-dev": { 33 | "laravel/pint": "^1.0", 34 | "nunomaduro/collision": "^8.0", 35 | "nunomaduro/larastan": "^2.0.1", 36 | "orchestra/testbench": "^9.0|^10.0", 37 | "pestphp/pest": "^2.0", 38 | "pestphp/pest-plugin-arch": "^2.0", 39 | "pestphp/pest-plugin-laravel": "^2.0", 40 | "phpstan/extension-installer": "^1.1", 41 | "phpstan/phpstan-deprecation-rules": "^1.0", 42 | "phpstan/phpstan-phpunit": "^1.0" 43 | }, 44 | "autoload": { 45 | "files": [ 46 | "src/helpers.php" 47 | ], 48 | "psr-4": { 49 | "Guava\\FilamentKnowledgeBase\\": "src/", 50 | "Guava\\FilamentKnowledgeBase\\Database\\Factories\\": "database/factories/" 51 | } 52 | }, 53 | "autoload-dev": { 54 | "psr-4": { 55 | "Guava\\FilamentKnowledgeBase\\Tests\\": "tests/", 56 | "Workbench\\App\\": "workbench/app/" 57 | } 58 | }, 59 | "scripts": { 60 | "post-autoload-dump": "@composer run prepare", 61 | "clear": "@php vendor/bin/testbench package:purge-filament-knowledge-base --ansi", 62 | "prepare": "@php vendor/bin/testbench package:discover --ansi", 63 | "build": [ 64 | "@composer run prepare", 65 | "@php vendor/bin/testbench workbench:build --ansi" 66 | ], 67 | "start": [ 68 | "Composer\\Config::disableProcessTimeout", 69 | "@composer run build", 70 | "@php vendor/bin/testbench serve" 71 | ], 72 | "analyse": "vendor/bin/phpstan analyse", 73 | "test": "vendor/bin/pest", 74 | "test-coverage": "vendor/bin/pest --coverage", 75 | "format": "vendor/bin/pint" 76 | }, 77 | "config": { 78 | "sort-packages": true, 79 | "allow-plugins": { 80 | "pestphp/pest-plugin": true, 81 | "phpstan/extension-installer": true 82 | } 83 | }, 84 | "extra": { 85 | "laravel": { 86 | "providers": [ 87 | "Guava\\FilamentKnowledgeBase\\KnowledgeBaseServiceProvider" 88 | ], 89 | "aliases": { 90 | "KnowledgeBase": "Guava\\FilamentKnowledgeBase\\Facades\\KnowledgeBase" 91 | } 92 | } 93 | }, 94 | "minimum-stability": "dev", 95 | "prefer-stable": true 96 | } 97 | -------------------------------------------------------------------------------- /config/filament-knowledge-base.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'id' => env('FILAMENT_KB_ID', 'knowledge-base'), 7 | 'path' => env('FILAMENT_KB_PATH', 'kb'), 8 | ], 9 | 10 | 'docs-path' => env('FILAMENT_KB_DOCS_PATH', 'docs'), 11 | 12 | 'model' => \Guava\FilamentKnowledgeBase\Models\FlatfileDocumentation::class, 13 | 14 | 'cache' => [ 15 | 'prefix' => env('FILAMENT_KB_CACHE_PREFIX', 'filament_kb_'), 16 | 'ttl' => env('FILAMENT_KB_CACHE_TTL', 'forever'), 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /database/factories/ModelFactory.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | // add fields 15 | 16 | $table->timestamps(); 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /dist/js/anchors-component.js: -------------------------------------------------------------------------------- 1 | // resources/js/anchors-component.js 2 | function anchorsComponent() { 3 | return { 4 | init: async function() { 5 | let anchors = document.querySelectorAll(".gu-kb-anchor"); 6 | let settings = { 7 | root: null, 8 | rootMargin: "-15% 0px -65% 0px", 9 | threshold: 0.1 10 | }; 11 | let observer = new IntersectionObserver(this.callback, settings); 12 | anchors.forEach((anchor) => observer.observe(anchor)); 13 | }, 14 | callback: function(entries, observer) { 15 | let classes = [ 16 | "transition", 17 | "duration-300", 18 | "ease-out", 19 | "text-primary-600", 20 | "dark:text-primary-400", 21 | "translate-x-1" 22 | ]; 23 | entries.forEach((entry) => { 24 | if (entry.isIntersecting) { 25 | let section = "#" + entry.target.id; 26 | document.querySelectorAll(".fi-sidebar-item-button .fi-sidebar-item-label").forEach((el2) => el2.classList.remove(...classes)); 27 | let el = document.querySelector(".fi-sidebar-item-button[href='" + section + "'] .fi-sidebar-item-label"); 28 | el.classList.add(...classes); 29 | } 30 | }); 31 | } 32 | }; 33 | } 34 | export { 35 | anchorsComponent as default 36 | }; 37 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vLi4vcmVzb3VyY2VzL2pzL2FuY2hvcnMtY29tcG9uZW50LmpzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJleHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBhbmNob3JzQ29tcG9uZW50KCkge1xuICAgIHJldHVybiB7XG5cbiAgICAgICAgaW5pdDogYXN5bmMgZnVuY3Rpb24gKCkge1xuICAgICAgICAgICAgbGV0IGFuY2hvcnMgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yQWxsKCcuZ3Uta2ItYW5jaG9yJyk7XG5cbiAgICAgICAgICAgIGxldCBzZXR0aW5ncyA9IHtcbiAgICAgICAgICAgICAgICByb290OiBudWxsLFxuICAgICAgICAgICAgICAgIHJvb3RNYXJnaW46ICctMTUlIDBweCAtNjUlIDBweCcsXG4gICAgICAgICAgICAgICAgdGhyZXNob2xkOiAwLjEsXG4gICAgICAgICAgICB9O1xuXG4gICAgICAgICAgICBsZXQgb2JzZXJ2ZXIgPSBuZXcgSW50ZXJzZWN0aW9uT2JzZXJ2ZXIodGhpcy5jYWxsYmFjaywgc2V0dGluZ3MpO1xuXG4gICAgICAgICAgICBhbmNob3JzLmZvckVhY2goYW5jaG9yID0+IG9ic2VydmVyLm9ic2VydmUoYW5jaG9yKSk7XG4gICAgICAgIH0sXG5cbiAgICAgICAgY2FsbGJhY2s6IGZ1bmN0aW9uIChlbnRyaWVzLCBvYnNlcnZlcikge1xuICAgICAgICAgICAgbGV0IGNsYXNzZXMgPSBbXG4gICAgICAgICAgICAgICAgJ3RyYW5zaXRpb24nLCAnZHVyYXRpb24tMzAwJywgJ2Vhc2Utb3V0JywgJ3RleHQtcHJpbWFyeS02MDAnLCAnZGFyazp0ZXh0LXByaW1hcnktNDAwJywgJ3RyYW5zbGF0ZS14LTEnXG4gICAgICAgICAgICBdO1xuXG4gICAgICAgICAgICBlbnRyaWVzLmZvckVhY2goZW50cnkgPT4ge1xuICAgICAgICAgICAgICAgIGlmIChlbnRyeS5pc0ludGVyc2VjdGluZykge1xuICAgICAgICAgICAgICAgICAgICBsZXQgc2VjdGlvbiA9ICcjJyArIGVudHJ5LnRhcmdldC5pZDtcbiAgICAgICAgICAgICAgICAgICAgZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbCgnLmZpLXNpZGViYXItaXRlbS1idXR0b24gLmZpLXNpZGViYXItaXRlbS1sYWJlbCcpXG4gICAgICAgICAgICAgICAgICAgICAgICAuZm9yRWFjaCgoZWwpID0+IGVsLmNsYXNzTGlzdC5yZW1vdmUoLi4uY2xhc3NlcykpO1xuICAgICAgICAgICAgICAgICAgICBsZXQgZWwgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCcuZmktc2lkZWJhci1pdGVtLWJ1dHRvbltocmVmPVxcJycgKyBzZWN0aW9uICsgJ1xcJ10gLmZpLXNpZGViYXItaXRlbS1sYWJlbCcpO1xuICAgICAgICAgICAgICAgICAgICBlbC5jbGFzc0xpc3QuYWRkKC4uLmNsYXNzZXMpO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH0pO1xuICAgICAgICB9XG4gICAgfVxuXG59O1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFlLFNBQVIsbUJBQW9DO0FBQ3ZDLFNBQU87QUFBQSxJQUVILE1BQU0saUJBQWtCO0FBQ3BCLFVBQUksVUFBVSxTQUFTLGlCQUFpQixlQUFlO0FBRXZELFVBQUksV0FBVztBQUFBLFFBQ1gsTUFBTTtBQUFBLFFBQ04sWUFBWTtBQUFBLFFBQ1osV0FBVztBQUFBLE1BQ2Y7QUFFQSxVQUFJLFdBQVcsSUFBSSxxQkFBcUIsS0FBSyxVQUFVLFFBQVE7QUFFL0QsY0FBUSxRQUFRLFlBQVUsU0FBUyxRQUFRLE1BQU0sQ0FBQztBQUFBLElBQ3REO0FBQUEsSUFFQSxVQUFVLFNBQVUsU0FBUyxVQUFVO0FBQ25DLFVBQUksVUFBVTtBQUFBLFFBQ1Y7QUFBQSxRQUFjO0FBQUEsUUFBZ0I7QUFBQSxRQUFZO0FBQUEsUUFBb0I7QUFBQSxRQUF5QjtBQUFBLE1BQzNGO0FBRUEsY0FBUSxRQUFRLFdBQVM7QUFDckIsWUFBSSxNQUFNLGdCQUFnQjtBQUN0QixjQUFJLFVBQVUsTUFBTSxNQUFNLE9BQU87QUFDakMsbUJBQVMsaUJBQWlCLGdEQUFnRCxFQUNyRSxRQUFRLENBQUNBLFFBQU9BLElBQUcsVUFBVSxPQUFPLEdBQUcsT0FBTyxDQUFDO0FBQ3BELGNBQUksS0FBSyxTQUFTLGNBQWMsbUNBQW9DLFVBQVUsMkJBQTRCO0FBQzFHLGFBQUcsVUFBVSxJQUFJLEdBQUcsT0FBTztBQUFBLFFBQy9CO0FBQUEsTUFDSixDQUFDO0FBQUEsSUFDTDtBQUFBLEVBQ0o7QUFFSjsiLAogICJuYW1lcyI6IFsiZWwiXQp9Cg== 38 | -------------------------------------------------------------------------------- /dist/js/modals-component.js: -------------------------------------------------------------------------------- 1 | // resources/js/modals-component.js 2 | function modalsComponent() { 3 | return { 4 | init: async function() { 5 | this.hashChanged(); 6 | window.addEventListener("hashchange", () => this.hashChanged()); 7 | }, 8 | hashChanged: function() { 9 | const fragment = location.hash.substring(1); 10 | const prefix = "modal-"; 11 | if (fragment.startsWith(prefix)) { 12 | const modal = fragment.substring(fragment.indexOf(prefix) + prefix.length); 13 | console.log("Open modal via wire: ", modal); 14 | this.$wire.showDocumentation(modal); 15 | history.replaceState(null, null, " "); 16 | } 17 | } 18 | }; 19 | } 20 | export { 21 | modalsComponent as default 22 | }; 23 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vLi4vcmVzb3VyY2VzL2pzL21vZGFscy1jb21wb25lbnQuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIG1vZGFsc0NvbXBvbmVudCgpIHtcbiAgICByZXR1cm4ge1xuXG4gICAgICAgIGluaXQ6IGFzeW5jIGZ1bmN0aW9uICgpIHtcbiAgICAgICAgICAgIHRoaXMuaGFzaENoYW5nZWQoKTtcbiAgICAgICAgICAgIHdpbmRvdy5hZGRFdmVudExpc3RlbmVyKCdoYXNoY2hhbmdlJywgKCk9PnRoaXMuaGFzaENoYW5nZWQoKSk7XG4gICAgICAgIH0sXG5cbiAgICAgICAgaGFzaENoYW5nZWQ6IGZ1bmN0aW9uKCkge1xuICAgICAgICAgICAgY29uc3QgZnJhZ21lbnQgPSBsb2NhdGlvbi5oYXNoLnN1YnN0cmluZygxKTtcbiAgICAgICAgICAgIGNvbnN0IHByZWZpeCA9ICdtb2RhbC0nO1xuXG4gICAgICAgICAgICBpZiAoZnJhZ21lbnQuc3RhcnRzV2l0aChwcmVmaXgpKSB7XG4gICAgICAgICAgICAgICAgY29uc3QgbW9kYWwgPSBmcmFnbWVudC5zdWJzdHJpbmcoZnJhZ21lbnQuaW5kZXhPZihwcmVmaXgpICsgcHJlZml4Lmxlbmd0aCk7XG5cbiAgICAgICAgICAgICAgICBjb25zb2xlLmxvZygnT3BlbiBtb2RhbCB2aWEgd2lyZTogJywgbW9kYWwpO1xuICAgICAgICAgICAgICAgIHRoaXMuJHdpcmUuc2hvd0RvY3VtZW50YXRpb24obW9kYWwpO1xuICAgICAgICAgICAgICAgIC8vIHdpbmRvdy5kaXNwYXRjaEV2ZW50KG5ldyBDdXN0b21FdmVudCgnb3Blbi1tb2RhbCcsIHtkZXRhaWw6IHtpZDogbW9kYWx9fSkpO1xuICAgICAgICAgICAgICAgIGhpc3RvcnkucmVwbGFjZVN0YXRlKG51bGwsIG51bGwsICcgJyk7XG4gICAgICAgICAgICB9XG4gICAgICAgIH0sXG5cbiAgICB9XG5cbn07XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQWUsU0FBUixrQkFBbUM7QUFDdEMsU0FBTztBQUFBLElBRUgsTUFBTSxpQkFBa0I7QUFDcEIsV0FBSyxZQUFZO0FBQ2pCLGFBQU8saUJBQWlCLGNBQWMsTUFBSSxLQUFLLFlBQVksQ0FBQztBQUFBLElBQ2hFO0FBQUEsSUFFQSxhQUFhLFdBQVc7QUFDcEIsWUFBTSxXQUFXLFNBQVMsS0FBSyxVQUFVLENBQUM7QUFDMUMsWUFBTSxTQUFTO0FBRWYsVUFBSSxTQUFTLFdBQVcsTUFBTSxHQUFHO0FBQzdCLGNBQU0sUUFBUSxTQUFTLFVBQVUsU0FBUyxRQUFRLE1BQU0sSUFBSSxPQUFPLE1BQU07QUFFekUsZ0JBQVEsSUFBSSx5QkFBeUIsS0FBSztBQUMxQyxhQUFLLE1BQU0sa0JBQWtCLEtBQUs7QUFFbEMsZ0JBQVEsYUFBYSxNQUFNLE1BQU0sR0FBRztBQUFBLE1BQ3hDO0FBQUEsSUFDSjtBQUFBLEVBRUo7QUFFSjsiLAogICJuYW1lcyI6IFtdCn0K 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "conventional-changelog-conventionalcommits": "^7.0.2", 4 | "esbuild": "^0.21.1", 5 | "semantic-release": "^22.0.5" 6 | }, 7 | "dependencies": { 8 | "shiki": "^1.2.3", 9 | "tm-grammars": "^1.7.2" 10 | }, 11 | "type": "module" 12 | } 13 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel", 3 | "rules": { 4 | "method_argument_space": true, 5 | "multiline_whitespace_before_semicolons": { 6 | "strategy": "new_line_for_chained_calls" 7 | }, 8 | "types_spaces": { 9 | "space": "single" 10 | }, 11 | "concat_space": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /resources/js/anchors-component.js: -------------------------------------------------------------------------------- 1 | export default function anchorsComponent() { 2 | return { 3 | 4 | init: async function () { 5 | let anchors = document.querySelectorAll('.gu-kb-anchor'); 6 | 7 | let settings = { 8 | root: null, 9 | rootMargin: '-15% 0px -65% 0px', 10 | threshold: 0.1, 11 | }; 12 | 13 | let observer = new IntersectionObserver(this.callback, settings); 14 | 15 | anchors.forEach(anchor => observer.observe(anchor)); 16 | }, 17 | 18 | callback: function (entries, observer) { 19 | let classes = [ 20 | 'transition', 'duration-300', 'ease-out', 'text-primary-600', 'dark:text-primary-400', 'translate-x-1' 21 | ]; 22 | 23 | entries.forEach(entry => { 24 | if (entry.isIntersecting) { 25 | let section = '#' + entry.target.id; 26 | document.querySelectorAll('.fi-sidebar-item-button .fi-sidebar-item-label') 27 | .forEach((el) => el.classList.remove(...classes)); 28 | let el = document.querySelector('.fi-sidebar-item-button[href=\'' + section + '\'] .fi-sidebar-item-label'); 29 | el.classList.add(...classes); 30 | } 31 | }); 32 | } 33 | } 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /resources/js/modals-component.js: -------------------------------------------------------------------------------- 1 | export default function modalsComponent() { 2 | return { 3 | 4 | init: async function () { 5 | this.hashChanged(); 6 | window.addEventListener('hashchange', ()=>this.hashChanged()); 7 | }, 8 | 9 | hashChanged: function() { 10 | const fragment = location.hash.substring(1); 11 | const prefix = 'modal-'; 12 | 13 | if (fragment.startsWith(prefix)) { 14 | const modal = fragment.substring(fragment.indexOf(prefix) + prefix.length); 15 | 16 | console.log('Open modal via wire: ', modal); 17 | this.$wire.showDocumentation(modal); 18 | // window.dispatchEvent(new CustomEvent('open-modal', {detail: {id: modal}})); 19 | history.replaceState(null, null, ' '); 20 | } 21 | }, 22 | 23 | } 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /resources/lang/ar/translations.php: -------------------------------------------------------------------------------- 1 | 'التوثيق', 5 | 'help' => 'مساعدة', 6 | 'open-documentation' => 'فتح التوثيق', 7 | 'close' => 'إغلاق', 8 | 'back-to-default-panel' => 'رجوع', 9 | ]; 10 | -------------------------------------------------------------------------------- /resources/lang/cs/translations.php: -------------------------------------------------------------------------------- 1 | 'Dokumentace', 5 | 'help' => 'Nápověda', 6 | 'open-documentation' => 'Otevřít dokumentaci', 7 | 'close' => 'Zavřít', 8 | 'back-to-default-panel' => 'Zpět', 9 | 'url-copied' => 'URL adresa byla zkopírována do schránky.', 10 | 'code-copied' => 'Kód byl zkopírován do schránky.', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/lang/da/translations.php: -------------------------------------------------------------------------------- 1 | 'Dokumentation', 5 | 'help' => 'Vejledning', 6 | 'open-documentation' => 'Åben dokumentation', 7 | 'close' => 'Luk', 8 | 'back-to-default-panel' => 'Tilbage', 9 | 'url-copied' => 'URL blev kopieret.', 10 | 'code-copied' => 'Kode blev kopieret.', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/lang/de/translations.php: -------------------------------------------------------------------------------- 1 | 'Dokumentation', 5 | 'help' => 'Hilfe', 6 | 'open-documentation' => 'Dokumentation öffnen', 7 | 'close' => 'Schließen', 8 | 'back-to-default-panel' => 'Zurück', 9 | 'url-copied' => 'URL wurde in die Zwischenablage kopiert.', 10 | 'code-copied' => 'Code wurde in die Zwischenablage kopiert.', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/lang/en/translations.php: -------------------------------------------------------------------------------- 1 | 'Documentation', 5 | 'help' => 'Help', 6 | 'open-documentation' => 'Open documentation', 7 | 'close' => 'Close', 8 | 'back-to-default-panel' => 'Back', 9 | 'url-copied' => 'URL was copied to your clipboard.', 10 | 'code-copied' => 'Code copied to your clipboard.', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/lang/fa/translations.php: -------------------------------------------------------------------------------- 1 | 'پایگاه دانش', 5 | 'help' => 'راهنما', 6 | 'open-documentation' => 'گشودن مستند', 7 | 'close' => 'بستن', 8 | ]; 9 | -------------------------------------------------------------------------------- /resources/lang/fr/translations.php: -------------------------------------------------------------------------------- 1 | 'Base de connaissances', 5 | 'help' => 'Aide', 6 | 'open-documentation' => 'Ouvrir la documentation', 7 | 'close' => 'Fermer', 8 | 'back-to-default-panel' => 'Retour', 9 | 'url-copied' => 'URL copiée dans votre presse-papiers.', 10 | 'code-copied' => 'Code copié dans votre presse-papiers.', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/lang/nl/translations.php: -------------------------------------------------------------------------------- 1 | 'Documentatie', 5 | 'help' => 'Help', 6 | 'open-documentation' => 'Open documentatie', 7 | 'close' => 'Sluit', 8 | 'back-to-default-panel' => 'Terug', 9 | 'url-copied' => 'De URL is gekopieerd naar je klembord.', 10 | 'code-copied' => 'De code is gekopieerd naar je klembord.', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/lang/pt_BR/translations.php: -------------------------------------------------------------------------------- 1 | 'Documentação', 5 | 'help' => 'Ajuda', 6 | 'open-documentation' => 'Abrir documentação', 7 | 'close' => 'fechar', 8 | 'back-to-default-panel' => 'Voltar', 9 | 'url-copied' => 'URL foi copiado para sua área de transferência.', 10 | 'code-copied' => 'Código copiado para sua área de transferência.', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/lang/pt_PT/translations.php: -------------------------------------------------------------------------------- 1 | 'Documentação', 5 | 'help' => 'Ajuda', 6 | 'open-documentation' => 'Abrir documentação', 7 | 'close' => 'Fechar', 8 | 'back-to-default-panel' => 'Voltar', 9 | 'url-copied' => 'URL copiado para a área de transferência.', 10 | 'code-copied' => 'Código copiado para a área de transferência.', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/lang/ru/translations.php: -------------------------------------------------------------------------------- 1 | 'Документация', 5 | 'help' => 'Помощь', 6 | 'open-documentation' => 'Открыть документацию', 7 | 'close' => 'Закрыть', 8 | 'back-to-default-panel' => 'Назад', 9 | 'url-copied' => 'URL скопирован в буфер обмена.', 10 | 'code-copied' => 'Код скопирован в буфер обмена.', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/lang/uk/translations.php: -------------------------------------------------------------------------------- 1 | 'Документація', 5 | 'help' => 'Допомога', 6 | 'open-documentation' => 'Відкрити документацію', 7 | 'close' => 'Закрити', 8 | 'back-to-default-panel' => 'Назад', 9 | 'url-copied' => 'URL скопійовано у буфер обміну.', 10 | 'code-copied' => 'Код скопійовано у буфер обміну.', 11 | ]; 12 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuavaCZ/filament-knowledge-base/6f88712a7b1336f8cee9c82054f9dd951cae17cb/resources/views/.gitkeep -------------------------------------------------------------------------------- /resources/views/code-block.blade.php: -------------------------------------------------------------------------------- 1 | @pushonce('styles') 2 | 12 | @endpushonce 13 | 14 |
19 |
20 | {{$language}} 21 | 33 | 34 | Copy 35 | Select 36 | 37 |
38 |
39 | {!! $code !!} 40 |
41 |
42 | -------------------------------------------------------------------------------- /resources/views/components/content.blade.php: -------------------------------------------------------------------------------- 1 | @use(Guava\FilamentKnowledgeBase\Facades\KnowledgeBase) 2 |
class([ 4 | 'gu-kb-article', 5 | '[&_ul]:list-[revert] [&_ol]:list-[revert] [&_ul]:ml-4 [&_ol]:ml-4' => ! KnowledgeBase::panel()->shouldDisableDefaultClasses(), 6 | ]) }} 7 | x-ignore 8 | ax-load 9 | ax-load-src="{{ \Filament\Support\Facades\FilamentAsset::getAlpineComponentSrc('anchors-component', 'guava/filament-knowledge-base') }}" 10 | x-data="anchorsComponent()" 11 | > 12 | {{ $slot }} 13 |
14 | -------------------------------------------------------------------------------- /resources/views/documentation.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | $sidebar = $this->getSubNavigationPosition(); 3 | $articleClass = \Guava\FilamentKnowledgeBase\Facades\KnowledgeBase::panel()->getArticleClass(); 4 | @endphp 5 | 6 | @push('scripts') 7 | 20 | @endpush 21 | {{-- $sidebar === \Filament\Pages\SubNavigationPosition::End, 26 | "[&_.fi-page-sub-navigation-sidebar]:pr-4 [&_.fi-page-sub-navigation-sidebar]:mr-4 [&_.fi-page-sub-navigation-sidebar]:border-r [&_.fi-page-sub-navigation-sidebar]:border-r-gray-600/10 [&_.fi-page-sub-navigation-sidebar]:dark:border-r-gray-600/30" => $sidebar === \Filament\Pages\SubNavigationPosition::Start, 27 | ]) 28 | :full-height="true" 29 | > 30 | ! empty($articleClass), 33 | ])> 34 | {!! $this->record->getHtml() !!} 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /resources/views/livewire/help-menu.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | use Filament\Facades\Filament; 3 | 4 | $hasModalPreviews = Filament::getPlugin('guava::filament-knowledge-base')->hasModalPreviews(); 5 | $hasSlideOverPreviews = Filament::getPlugin('guava::filament-knowledge-base')->hasSlideOverPreviews(); 6 | $hasModalTitleBreadcrumbs = Filament::getPlugin('guava::filament-knowledge-base')->hasModalTitleBreadcrumbs(); 7 | $target = Filament::getPlugin('guava::filament-knowledge-base')->shouldOpenDocumentationInNewTab() ? '_blank' : '_self'; 8 | $articleClass = \Guava\FilamentKnowledgeBase\Facades\KnowledgeBase::panel()->getArticleClass(); 9 | @endphp 10 | 11 |
! $documentation, 13 | ]) 14 | > 15 | @if($documentation) 16 | @if($this->shouldShowAsMenu()) 17 | {{$this->getMenuAction()}} 18 | @else 19 | {{ $this->getSingleAction() }} 20 | @endif 21 | @endif 22 | 23 | 24 | 25 | @push('scripts') 26 | 69 | @endpush 70 |
71 | -------------------------------------------------------------------------------- /resources/views/livewire/modals.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | use Filament\Facades\Filament; 3 | 4 | $hasModalPreviews = Filament::getPlugin('guava::filament-knowledge-base')->hasModalPreviews(); 5 | $hasSlideOverPreviews = Filament::getPlugin('guava::filament-knowledge-base')->hasSlideOverPreviews(); 6 | $hasModalTitleBreadcrumbs = Filament::getPlugin('guava::filament-knowledge-base')->hasModalTitleBreadcrumbs(); 7 | $target = Filament::getPlugin('guava::filament-knowledge-base')->shouldOpenDocumentationInNewTab() ? '_blank' : '_self'; 8 | $articleClass = \Guava\FilamentKnowledgeBase\Facades\KnowledgeBase::panel()->getArticleClass(); 9 | @endphp 10 | 11 |
17 | @if($documentable) 18 | 27 | 28 | 29 | @if($hasModalTitleBreadcrumbs && !empty($documentable->getBreadcrumbs())) 30 | {{ KnowledgeBase::breadcrumbs($documentable) }} 31 | @else 32 | {{ $documentable->getTitle() }} 33 | @endif 34 | 35 | 36 | ! empty($articleClass), 39 | ])> 40 | {!! $documentable->getSimpleHtml() !!} 41 | 42 | 43 | 46 | {{ __('filament-knowledge-base::translations.open-documentation') }} 47 | 48 | 50 | 51 | {{ __('filament-knowledge-base::translations.close') }} 52 | 53 | 54 | 55 | @endif 56 |
57 | -------------------------------------------------------------------------------- /resources/views/modals.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | use Filament\Facades\Filament; 3 | 4 | $hasModalPreviews = Filament::getPlugin('guava::filament-knowledge-base')->hasModalPreviews(); 5 | $hasSlideOverPreviews = Filament::getPlugin('guava::filament-knowledge-base')->hasSlideOverPreviews(); 6 | $hasModalTitleBreadcrumbs = Filament::getPlugin('guava::filament-knowledge-base')->hasModalTitleBreadcrumbs(); 7 | $target = Filament::getPlugin('guava::filament-knowledge-base')->shouldOpenDocumentationInNewTab() ? '_blank' : '_self'; 8 | $articleClass = \Guava\FilamentKnowledgeBase\Facades\KnowledgeBase::panel()->getArticleClass(); 9 | @endphp 10 | 11 |
17 | 26 | 27 | 28 | @if($hasModalTitleBreadcrumbs && !empty($documentable->getBreadcrumbs())) 29 | {{ KnowledgeBase::breadcrumbs($documentable) }} 30 | @else 31 | {{ $documentable->getTitle() }} 32 | @endif 33 | 34 | 35 | ! empty($articleClass), 38 | ])> 39 | {!! $documentable->getSimpleHtml() !!} 40 | 41 | 42 | 45 | {{ __('filament-knowledge-base::translations.open-documentation') }} 46 | 47 | 49 | 50 | {{ __('filament-knowledge-base::translations.close') }} 51 | 52 | 53 | 54 |
55 | -------------------------------------------------------------------------------- /resources/views/pages/section.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | $sidebar = $this->getSubNavigationPosition(); 3 | @endphp 4 | $sidebar === \Filament\Pages\SubNavigationPosition::End, 7 | "[&_.fi-page-sub-navigation-sidebar]:pr-4 [&_.fi-page-sub-navigation-sidebar]:border-r [&_.fi-page-sub-navigation-sidebar]:border-r-gray-600/10 [&_.fi-page-sub-navigation-sidebar]:dark:border-r-gray-600/30" => $sidebar === \Filament\Pages\SubNavigationPosition::Start, 8 | ]) 9 | :full-height="true" 10 | > 11 |
12 | {!! $this->html !!} 13 |
14 | 15 |
16 | -------------------------------------------------------------------------------- /resources/views/sidebar-action.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 7 | 14 | {{ $label }} 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/Actions/Forms/Components/HelpAction.php: -------------------------------------------------------------------------------- 1 | getSimpleHtml(); 17 | $articleClass = KnowledgeBase::panel()->getArticleClass(); 18 | 19 | $classes = Arr::toCssClasses([ 20 | 'gu-kb-article-modal', 21 | $articleClass => ! empty($articleClass), 22 | ]); 23 | 24 | $replacementStringId = \Str::random(); 25 | 26 | $parsed = \Blade::render(<< 28 | $replacementStringId 29 | 30 | blade); 31 | 32 | return new HtmlString(\Str::replace($replacementStringId, $html, $parsed)); 33 | } 34 | 35 | public static function forDocumentable(Documentable | string $documentable): static 36 | { 37 | $documentable = KnowledgeBase::documentable($documentable); 38 | 39 | return static::make("help.{$documentable->getId()}") 40 | ->label($documentable->getTitle()) 41 | // ->icon($documentable->getIcon()) 42 | ->icon('heroicon-o-question-mark-circle') 43 | ->when( 44 | Filament::getPlugin('guava::filament-knowledge-base')->hasModalPreviews(), 45 | fn (HelpAction $action) => $action 46 | ->modalContent(fn () => static::getContentView($documentable)) 47 | ->modalHeading($documentable->getTitle()) 48 | ->modalSubmitAction(false) 49 | ->modalCancelActionLabel(__('filament-knowledge-base::translations.close')) 50 | ->when( 51 | Filament::getPlugin('guava::filament-knowledge-base')->hasSlideOverPreviews(), 52 | fn (HelpAction $action) => $action->slideOver() 53 | ), 54 | fn (HelpAction $action) => $action->url($documentable->getUrl()) 55 | ) 56 | ; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Actions/HelpAction.php: -------------------------------------------------------------------------------- 1 | label(__('filament-knowledge-base::translations.help')) 17 | ->icon('heroicon-o-question-mark-circle') 18 | ->iconSize('lg') 19 | ->color('gray') 20 | ->button() 21 | ; 22 | } 23 | 24 | public static function forDocumentable(Documentable | string $documentable): HelpAction 25 | { 26 | $documentable = KnowledgeBase::documentable($documentable); 27 | 28 | return HelpAction::make("help.{$documentable->getId()}") 29 | ->label($documentable->getTitle()) 30 | ->icon($documentable->getIcon()) 31 | ->when( 32 | Filament::getPlugin('guava::filament-knowledge-base')->hasModalPreviews(), 33 | fn (HelpAction $action) => $action 34 | ->alpineClickHandler('$dispatch("open-modal", {id: "' . $documentable->getId() . '"})') 35 | ->when( 36 | Filament::getPlugin('guava::filament-knowledge-base')->hasSlideOverPreviews(), 37 | fn (HelpAction $action) => $action->slideOver() 38 | ), 39 | fn (HelpAction $action) => $action->url($documentable->getUrl()) 40 | ) 41 | ; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Commands/MakeDocumentationCommand.php: -------------------------------------------------------------------------------- 1 | option('class') 25 | ? __DIR__ . '/../../stubs/documentation.php.stub' 26 | : __DIR__ . '/../../stubs/markdown.md.stub'; 27 | } 28 | 29 | protected function qualifyClass($name) 30 | { 31 | return $this->option('class') 32 | ? parent::qualifyClass($name) 33 | : $name; 34 | } 35 | 36 | protected function getPath($name) 37 | { 38 | return $this->option('class') 39 | ? parent::getPath($name) 40 | : str(base_path(config('filament-knowledge-base.docs-path'))) 41 | ->rtrim('/') 42 | ->append( 43 | '/', 44 | str($name) 45 | ->replaceEnd('.md', '') 46 | ->append('.md') 47 | ) 48 | ; 49 | } 50 | 51 | protected function getNameInput() 52 | { 53 | return str(parent::getNameInput()) 54 | ->trim('/') 55 | ->replace('.', '/') 56 | ->when( 57 | ! $this->option('class'), 58 | fn (Stringable $str) => $str->prepend($this->currentLocale, '/') 59 | ) 60 | ->toString() 61 | ; 62 | } 63 | 64 | protected string $currentLocale; 65 | 66 | public function handle() 67 | { 68 | if ($this->option('class')) { 69 | return parent::handle(); 70 | } 71 | 72 | $path = str(base_path(config('filament-knowledge-base.docs-path'))) 73 | ->rtrim('/') 74 | ->append('/') 75 | ->toString() 76 | ; 77 | if (! File::exists($path)) { 78 | File::makeDirectory($path); 79 | } 80 | $locales = $this->option('locale'); 81 | $locales = empty($locales) 82 | ? File::directories($path) 83 | : $locales; 84 | 85 | if (empty($locales)) { 86 | $locales[] = App::getLocale(); 87 | } 88 | 89 | foreach ($locales as $locale) { 90 | $this->currentLocale = str($locale)->afterLast('/')->toString(); 91 | if (parent::handle() === false) { 92 | return false; 93 | } 94 | } 95 | 96 | return true; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Concerns/CanDisableBackToDefaultPanelButton.php: -------------------------------------------------------------------------------- 1 | disableBackToDefaultPanelButton = $condition; 12 | 13 | return $this; 14 | } 15 | 16 | public function shouldDisableBackToDefaultPanelButton(): bool 17 | { 18 | return $this->disableBackToDefaultPanelButton; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Concerns/CanDisableBreadcrumbs.php: -------------------------------------------------------------------------------- 1 | disableBreadcrumbs = $condition; 12 | 13 | return $this; 14 | } 15 | 16 | public function shouldDisableBreadcrumbs(): bool 17 | { 18 | return $this->evaluate($this->disableBreadcrumbs); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Concerns/CanDisableDefaultClasses.php: -------------------------------------------------------------------------------- 1 | disableDefaultClasses = $condition; 12 | 13 | return $this; 14 | } 15 | 16 | public function shouldDisableDefaultClasses(): bool 17 | { 18 | return $this->evaluate($this->disableDefaultClasses); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Concerns/CanDisableKnowledgeBasePanelButton.php: -------------------------------------------------------------------------------- 1 | disableKnowledgeBasePanelButton = $condition; 12 | 13 | return $this; 14 | } 15 | 16 | public function shouldDisableKnowledgeBasePanelButton(): bool 17 | { 18 | return $this->disableKnowledgeBasePanelButton; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Concerns/CanDisableModalLinks.php: -------------------------------------------------------------------------------- 1 | disableModalLinks = $condition; 12 | 13 | return $this; 14 | } 15 | 16 | public function shouldDisableModalLinks(): bool 17 | { 18 | return $this->disableModalLinks; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Concerns/HasAnchorSymbol.php: -------------------------------------------------------------------------------- 1 | anchorSymbol = $symbol; 12 | 13 | return $this; 14 | } 15 | 16 | public function getAnchorSymbol(): ?string 17 | { 18 | return $this->evaluate($this->anchorSymbol); 19 | } 20 | 21 | public function disableAnchors(): static 22 | { 23 | $this->anchorSymbol = null; 24 | 25 | return $this; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Concerns/HasArticleClass.php: -------------------------------------------------------------------------------- 1 | articleClass = $class; 14 | 15 | return $this; 16 | } 17 | 18 | public function getArticleClass(): ?string 19 | { 20 | return $this->evaluate($this->articleClass); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Concerns/HasDocumentable.php: -------------------------------------------------------------------------------- 1 | documentable = $documentable; 14 | 15 | return $this; 16 | } 17 | 18 | public function getDocumentable(): Documentable 19 | { 20 | return $this->documentable; 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Concerns/HasModalPreviews.php: -------------------------------------------------------------------------------- 1 | modalPreviews = $condition; 14 | 15 | return $this; 16 | } 17 | 18 | public function slideOverPreviews(bool $condition = true): static 19 | { 20 | $this->slideOverPreviews = $condition; 21 | 22 | return $this; 23 | } 24 | 25 | public function hasModalPreviews(): bool 26 | { 27 | return $this->modalPreviews; 28 | } 29 | 30 | public function hasSlideOverPreviews(): bool 31 | { 32 | return $this->slideOverPreviews; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Contracts/Documentable.php: -------------------------------------------------------------------------------- 1 | after('App\\Docs') 14 | ->trim('\\') 15 | ->replace('\\', '.') 16 | ->lower() 17 | ; 18 | } 19 | 20 | public function getOrder(): int 21 | { 22 | return 0; 23 | } 24 | 25 | public function getGroup(): ?string 26 | { 27 | return null; 28 | } 29 | 30 | public function getParent(): ?string 31 | { 32 | return null; 33 | } 34 | 35 | public function getIcon(): ?string 36 | { 37 | return null; 38 | } 39 | 40 | public function isRegistered(): bool 41 | { 42 | return true; 43 | } 44 | 45 | public function __toString(): string 46 | { 47 | return $this->getId(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Enums/TableOfContentsPosition.php: -------------------------------------------------------------------------------- 1 | SubNavigationPosition::Start, 17 | self::End => SubNavigationPosition::End, 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Facades/KnowledgeBase.php: -------------------------------------------------------------------------------- 1 | getTableOfContentsPosition()->toSubNavigationPosition(); 27 | } 28 | 29 | public function getBreadcrumbs(): array 30 | { 31 | if (KnowledgeBase::panel()->shouldDisableBreadcrumbs()) { 32 | return []; 33 | } 34 | 35 | return $this->record->getBreadcrumbs(); 36 | } 37 | 38 | public function mount(int | string $record): void 39 | { 40 | parent::mount($record); // TODO: Change the autogenerated stub 41 | } 42 | 43 | // public function mount(FlatfileDocumentation $documentation) { 44 | // $this->record = $documentation; 45 | // } 46 | 47 | // public function getTitle(): string|Htmlable 48 | // { 49 | // return $this->record->title ?? parent::getTitle(); 50 | // } 51 | // 52 | // /** 53 | // * @return string|null 54 | // */ 55 | // public static function getNavigationLabel(): string 56 | // { 57 | // return self::$navigationLabel; 58 | // } 59 | 60 | // public static function getRoutePath(): string 61 | // { 62 | // return '/{documentation?}'; 63 | // } 64 | 65 | // public static function routes(Panel $panel): void 66 | // { 67 | // Route::get(static::getRoutePath(), static::class) 68 | // ->middleware(static::getRouteMiddleware($panel)) 69 | // ->withoutMiddleware(static::getWithoutRouteMiddleware($panel)) 70 | // ->name(static::getRelativeRouteName()) 71 | // ->where('documentation', '.*') 72 | // ; 73 | // } 74 | 75 | public static function route(string $path): PageRegistration 76 | { 77 | 78 | // Route::get(static::getRoutePath(), static::class) 79 | // ->middleware(static::getRouteMiddleware($panel)) 80 | // ->withoutMiddleware(static::getWithoutRouteMiddleware($panel)) 81 | // ->name(static::getRelativeRouteName()) 82 | // ->where('documentation', '.*') 83 | // ; 84 | return new PageRegistration( 85 | page: static::class, 86 | route: fn (Panel $panel): Route => RouteFacade::get($path, static::class) 87 | ->middleware(static::getRouteMiddleware($panel)) 88 | ->withoutMiddleware(static::getWithoutRouteMiddleware($panel)) 89 | ->where('record', '.*'), 90 | ); 91 | } 92 | 93 | public function getSubNavigation(): array 94 | { 95 | if (KnowledgeBase::panel()->shouldDisableTableOfContents()) { 96 | return []; 97 | } 98 | 99 | $pages = []; 100 | foreach ($this->record->getAnchors() as $label => $anchor) { 101 | $pages[] = NavigationItem::make($anchor) 102 | ->url("#$anchor") 103 | ->label($label) 104 | ; 105 | } 106 | 107 | return $pages; 108 | } 109 | 110 | public function getTitle(): string | Htmlable 111 | { 112 | return $this->record->getTitle(); 113 | } 114 | 115 | #[On('documentation.anchor.copy')] 116 | public function copyAnchorToClipboard(string $url) 117 | { 118 | $this->js(<< { 121 | (new FilamentNotification()).title(filamentKnowledgeBaseTranslations.urlCopied) 122 | .success() 123 | .send(); 124 | }).catch((err) => { 125 | console.error('Failed to copy text: ', err); 126 | }); 127 | } 128 | JS); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Filament/Panels/KnowledgeBasePanel.php: -------------------------------------------------------------------------------- 1 | id( 61 | config('filament-knowledge-base.panel.id', 'knowledge-base') 62 | ); 63 | } 64 | 65 | public function guestAccess(bool $condition = true): static 66 | { 67 | $this->guestAccess = $condition; 68 | 69 | return $this; 70 | } 71 | 72 | public function hasGuestAccess(): bool 73 | { 74 | return $this->evaluate($this->guestAccess); 75 | } 76 | 77 | public function disableTableOfContents(bool $condition = true): static 78 | { 79 | $this->disableTableOfContents = $condition; 80 | 81 | return $this; 82 | } 83 | 84 | public function shouldDisableTableOfContents(): bool 85 | { 86 | return $this->evaluate($this->disableTableOfContents); 87 | } 88 | 89 | public function tableOfContentsPosition(TableOfContentsPosition $position): static 90 | { 91 | $this->tableOfContentsPosition = $position; 92 | 93 | return $this; 94 | } 95 | 96 | public function getTableOfContentsPosition(): TableOfContentsPosition 97 | { 98 | return $this->evaluate($this->tableOfContentsPosition); 99 | } 100 | 101 | public function syntaxHighlighting(bool $condition = true): static 102 | { 103 | static::$syntaxHighlighting = $condition; 104 | 105 | if (static::$syntaxHighlighting) { 106 | if (! InstalledVersions::isInstalled('spatie/shiki-php')) { 107 | throw new Exception('You need to install shiki and spatie/shiki-php in order to use the syntax highlighting feature. Please check the documentation for installation instructions.'); 108 | } 109 | } 110 | 111 | return $this; 112 | } 113 | 114 | public static function hasSyntaxHighlighting(): bool 115 | { 116 | return static::$syntaxHighlighting; 117 | } 118 | 119 | public function getBrandName(): string | Htmlable 120 | { 121 | return $this->evaluate($this->brandName) 122 | ?? __('filament-knowledge-base::translations.knowledge-base') 123 | ?? config('app.name'); 124 | } 125 | 126 | public function getTheme(): Theme 127 | { 128 | if (! isset($this->viteTheme)) { 129 | throw new Exception('The knowledge base panel needs to be registered with a custom vite theme.'); 130 | } 131 | 132 | return parent::getTheme(); 133 | } 134 | 135 | public function getPath(): string 136 | { 137 | if (! empty($path = parent::getPath())) { 138 | return $path; 139 | } 140 | 141 | return config('filament-knowledge-base.panel.path', 'kb'); 142 | } 143 | 144 | public function getPages(): array 145 | { 146 | return array_unique([ 147 | ...parent::getPages(), 148 | Dashboard::class, 149 | ]); 150 | } 151 | 152 | public function getResources(): array 153 | { 154 | return array_unique([ 155 | ...parent::getResources(), 156 | DocumentationResource::class, 157 | ]); 158 | } 159 | 160 | public function getMiddleware(): array 161 | { 162 | return [ 163 | ...parent::getMiddleware(), 164 | 165 | EncryptCookies::class, 166 | AddQueuedCookiesToResponse::class, 167 | StartSession::class, 168 | AuthenticateSession::class, 169 | ShareErrorsFromSession::class, 170 | VerifyCsrfToken::class, 171 | SubstituteBindings::class, 172 | DisableBladeIconComponents::class, 173 | DispatchServingFilamentEvent::class, 174 | ]; 175 | } 176 | 177 | public function getGlobalSearchKeyBindings(): array 178 | { 179 | if (! empty($keyBindings = parent::getGlobalSearchKeyBindings())) { 180 | return $keyBindings; 181 | } 182 | 183 | return ['mod+k']; 184 | } 185 | 186 | public function getGlobalSearchFieldSuffix(): ?string 187 | { 188 | if ($suffix = parent::getGlobalSearchFieldSuffix()) { 189 | return $suffix; 190 | } 191 | 192 | return match (Platform::detect()) { 193 | Platform::Windows, Platform::Linux => 'CTRL+K', 194 | Platform::Mac => '⌘K', 195 | Platform::Other => null, 196 | }; 197 | } 198 | 199 | protected function setUp(): void 200 | { 201 | $this 202 | ->when( 203 | ! $this->hasGuestAccess(), 204 | fn (Panel $panel) => $panel 205 | ->widgets([ 206 | AccountWidget::class, 207 | ]) 208 | ->authMiddleware([ 209 | Authenticate::class, 210 | ]) 211 | ) 212 | 213 | ->when( 214 | ! $this->shouldDisableBackToDefaultPanelButton(), 215 | fn (Panel $panel) => $panel 216 | ->renderHook( 217 | PanelsRenderHook::SIDEBAR_FOOTER, 218 | fn (): string => view('filament-knowledge-base::sidebar-action', [ 219 | 'label' => __('filament-knowledge-base::translations.back-to-default-panel'), 220 | 'icon' => 'heroicon-o-arrow-uturn-left', 221 | 'url' => KnowledgeBase::url(Filament::getDefaultPanel()), 222 | 'shouldOpenUrlInNewTab' => false, 223 | ]) 224 | ) 225 | ) 226 | 227 | // TODO: Replace with ->navigationItems and ->navigationGroups to support custom pages 228 | ->navigation($this->makeNavigation(...)) 229 | ; 230 | } 231 | 232 | protected function buildNavigationItem(Documentable $documentable) 233 | { 234 | return NavigationItem::make($documentable->getTitle()) 235 | ->group($documentable->getGroup()) 236 | ->icon($documentable->getIcon()) 237 | ->sort($documentable->getOrder()) 238 | ->childItems( 239 | KnowledgeBase::model()::query() 240 | ->where('parent', $documentable->getTitle()) 241 | ->get() 242 | ->filter(fn (Documentable $documentable) => $documentable->isRegistered()) 243 | ->sort(fn (Documentable $d1, Documentable $d2) => $d1->getOrder() <=> $d2->getOrder()) 244 | ->map(fn (Documentable $documentable) => $this->buildNavigationItem($documentable)) 245 | ->toArray() 246 | ) 247 | ->parentItem($documentable->getParent()) 248 | ->url($documentable->getUrl()) 249 | ->isActiveWhen(fn () => url()->current() === $documentable->getUrl()) 250 | ; 251 | } 252 | 253 | protected function makeNavigation(NavigationBuilder $builder): NavigationBuilder 254 | { 255 | $documentables = KnowledgeBase::model()::all(); 256 | 257 | if (File::exists(app_path('Docs'))) { 258 | $documentables 259 | ->push( 260 | ...collect(Discover::in(app_path('Docs')) 261 | ->extending(Documentation::class) 262 | ->get()) 263 | ->map(fn ($class) => new $class) 264 | ->all() 265 | ) 266 | ; 267 | } 268 | 269 | $documentables 270 | ->filter(fn (Documentable $documentable) => $documentable->isRegistered()) 271 | ->filter(fn (Documentable $documentable) => $documentable->getParent() === null) 272 | ->groupBy(fn (Documentable $documentable) => $documentable->getGroup()) 273 | ->map( 274 | fn (Collection $items, string $key) => empty($key) 275 | ? $items 276 | ->sort(fn (Documentable $d1, Documentable $d2) => $d1->getOrder() <=> $d2->getOrder()) 277 | ->map(fn (Documentable $documentation) => $this->buildNavigationItem($documentation)) 278 | : NavigationGroup::make($key) 279 | ->items( 280 | $items 281 | ->sort(fn (Documentable $d1, Documentable $d2) => $d1->getOrder() <=> $d2->getOrder()) 282 | ->map(fn (Documentable $documentable) => $this->buildNavigationItem($documentable)) 283 | ->toArray() 284 | ) 285 | ) 286 | ->flatten() 287 | ->each(fn ($item) => match (true) { 288 | $item instanceof NavigationItem => $builder->item($item), 289 | $item instanceof NavigationGroup => $builder->group($item), 290 | }) 291 | ; 292 | 293 | return $builder; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/Filament/Resources/DocumentationResource.php: -------------------------------------------------------------------------------- 1 | ViewDocumentation::route('/{record?}'), 29 | ]; 30 | } 31 | 32 | protected static bool $shouldRegisterNavigation = false; 33 | 34 | public static function getRoutePrefix(): string 35 | { 36 | return ''; 37 | } 38 | 39 | public static function getGlobalSearchResultUrl(Model $record): ?string 40 | { 41 | return ViewDocumentation::getUrl(['record' => $record], panel: KnowledgeBase::panelId()); 42 | } 43 | 44 | public static function getGlobalSearchResultTitle(Model $record): string | Htmlable 45 | { 46 | return str($record->slug) 47 | ->replace('/', ' -> ') 48 | ; 49 | } 50 | 51 | public static function resolveRecordRouteBinding(int | string $key): ?Model 52 | { 53 | // TODO: First try to load it from a standalone (App/Docs) class 54 | $record = parent::resolveRecordRouteBinding($key); 55 | 56 | if (! $record?->isRegistered()) { 57 | return null; 58 | } 59 | 60 | return $record; 61 | } 62 | 63 | public static function getPluralModelLabel(): string 64 | { 65 | return __('filament-knowledge-base::translations.knowledge-base'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/KnowledgeBase.php: -------------------------------------------------------------------------------- 1 | slug) 28 | ->label($documentation->title) 29 | ->icon($documentation->icon) 30 | ->action(fn () => dd('test')) 31 | ->requiresConfirmation() 32 | ; 33 | } 34 | 35 | public function model(): Documentable | string 36 | { 37 | return config('filament-knowledge-base.model'); 38 | } 39 | 40 | public function panel(): KnowledgeBasePanel 41 | { 42 | $panel = Filament::getPanel($this->panelId()); 43 | 44 | if (! ($panel instanceof KnowledgeBasePanel)) { 45 | throw new Exception('Panel must be an Knowledge Base Panel!'); 46 | } 47 | 48 | return $panel; 49 | } 50 | 51 | public function panelId(): string 52 | { 53 | return config('filament-knowledge-base.panel.id', 'knowledge-base'); 54 | } 55 | 56 | public function url(Panel $panel): ?string 57 | { 58 | $oldPanel = Filament::getCurrentPanel(); 59 | 60 | Filament::setCurrentPanel($panel); 61 | $url = $panel->getUrl(); 62 | Filament::setCurrentPanel($oldPanel); 63 | 64 | return $url; 65 | } 66 | 67 | public function parseMarkdown(string $path): array 68 | { 69 | $converter = app(MarkdownRenderer::class); 70 | 71 | $result = $converter->convert(file_get_contents($path)); 72 | 73 | $frontMatter = []; 74 | if ($result instanceof RenderedContentWithFrontMatter) { 75 | $frontMatter = $result->getFrontMatter(); 76 | } 77 | 78 | return ['html' => $result->getContent(), 'front-matter' => $frontMatter]; 79 | } 80 | 81 | public function documentable(Documentable | string $documentable): Documentable 82 | { 83 | if (is_string($documentable) && class_exists($documentable)) { 84 | $documentable = new $documentable; 85 | } 86 | 87 | if ($documentable instanceof Documentable) { 88 | return $documentable; 89 | } 90 | 91 | if (! is_string($documentable)) { 92 | throw new Exception('The class you provided is not a \`Documentable\`.'); 93 | } 94 | 95 | if ($model = $this->model()::find(str($documentable)->replace('/', '.'))) { 96 | return $model; 97 | } else { 98 | throw new Exception("'The provided documentable \"$documentable\" could not be found.'"); 99 | } 100 | } 101 | 102 | public function markdown(Documentable | string $documentable) 103 | { 104 | return new HtmlString($this->documentable($documentable)->getContent()); 105 | if (is_string($documentable) && class_exists($documentable)) { 106 | $documentable = new $documentable; 107 | } 108 | 109 | if ($documentable instanceof Documentable) { 110 | return $documentable->getContent(); 111 | } 112 | 113 | if (! is_string($documentable)) { 114 | throw new Exception('The class you provided is not a \`Documentable\`.'); 115 | } 116 | 117 | $path = str(base_path(config('filament-knowledge-base.docs-path'))) 118 | ->rtrim('/') 119 | ->append('/', App::getLocale(), '/') 120 | ; 121 | 122 | $documentable = str($documentable)->ltrim('/') 123 | ->replace('.', '/') 124 | ->prepend('/') 125 | ; 126 | $directory = $documentable->beforeLast('/')->ltrim('/')->append('/'); 127 | $file = $documentable->afterLast('/') 128 | ->beforeLast('.md') 129 | ->append('.md') 130 | ; 131 | 132 | $fullPath = match (true) { 133 | File::exists($fullPath = str($path)->append($directory, '_partials/', $file)) => $fullPath, 134 | File::exists($fullPath = str($path)->append('_partials/', $directory, $file)) => $fullPath, 135 | File::exists($fullPath = str($path)->append($directory, $file)) => $fullPath, 136 | }; 137 | 138 | $converter = app(MarkdownRenderer::class); 139 | 140 | return $converter->convertToHtml(file_get_contents($fullPath)); 141 | } 142 | 143 | public function breadcrumbs(Documentable $documentable): HtmlString 144 | { 145 | return new HtmlString(view('filament::components.breadcrumbs', [ 146 | 'breadcrumbs' => $documentable->getBreadcrumbs(), 147 | ])); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/KnowledgeBasePlugin.php: -------------------------------------------------------------------------------- 1 | helpMenuRenderHook = $renderHook; 28 | 29 | return $this; 30 | } 31 | 32 | public function getHelpMenuRenderHook(): string 33 | { 34 | return $this->helpMenuRenderHook; 35 | } 36 | 37 | public function modalTitleBreadcrumbs(bool $condition = true): static 38 | { 39 | $this->modalTitleBreadcrumbs = $condition; 40 | 41 | return $this; 42 | } 43 | 44 | public function openDocumentationInNewTab(bool $condition = true): static 45 | { 46 | $this->openDocumentationInNewTab = $condition; 47 | 48 | return $this; 49 | } 50 | 51 | public function hasModalTitleBreadcrumbs(): bool 52 | { 53 | return $this->modalTitleBreadcrumbs; 54 | } 55 | 56 | public function shouldOpenDocumentationInNewTab(): bool 57 | { 58 | return $this->openDocumentationInNewTab; 59 | } 60 | 61 | public function getId(): string 62 | { 63 | return 'guava::filament-knowledge-base'; 64 | } 65 | 66 | public function register(Panel $panel): void 67 | { 68 | $panel 69 | ->renderHook( 70 | $this->getHelpMenuRenderHook(), 71 | fn (): string => Blade::render('@livewire(\'help-menu\')'), 72 | ) 73 | ->when( 74 | ! $this->shouldDisableModalLinks(), 75 | fn (Panel $panel) => $panel->renderHook( 76 | PanelsRenderHook::BODY_END, 77 | fn (): string => Blade::render('@livewire(\'modals\')'), 78 | ) 79 | ) 80 | ->when( 81 | ! $this->shouldDisableKnowledgeBasePanelButton(), 82 | fn (Panel $panel) => $panel 83 | ->renderHook( 84 | PanelsRenderHook::SIDEBAR_FOOTER, 85 | fn (): string => view('filament-knowledge-base::sidebar-action', [ 86 | 'label' => __('filament-knowledge-base::translations.knowledge-base'), 87 | 'icon' => 'heroicon-o-book-open', 88 | 'url' => \Guava\FilamentKnowledgeBase\Facades\KnowledgeBase::url( 89 | \Guava\FilamentKnowledgeBase\Facades\KnowledgeBase::panel() 90 | ), 91 | 'shouldOpenUrlInNewTab' => $this->shouldOpenDocumentationInNewTab(), 92 | ]) 93 | ) 94 | ) 95 | ; 96 | } 97 | 98 | public function boot(Panel $panel): void {} 99 | 100 | public static function make(): static 101 | { 102 | return app(static::class); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/KnowledgeBaseServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('filament-knowledge-base') 26 | ->hasViews() 27 | ->hasConfigFile() 28 | ->hasTranslations() 29 | ->hasMigration('create_filament-knowledge-base_table') 30 | ->hasCommand(MakeDocumentationCommand::class) 31 | ; 32 | } 33 | 34 | public function packageRegistered(): void 35 | { 36 | $this->app->register(KnowledgeBasePanelProvider::class); 37 | } 38 | 39 | public function packageBooted(): void 40 | { 41 | Livewire::component('help-menu', HelpMenu::class); 42 | Livewire::component('modals', Modals::class); 43 | 44 | FilamentAsset::register( 45 | assets: [ 46 | AlpineComponent::make( 47 | 'anchors-component', 48 | __DIR__ . '/../dist/js/anchors-component.js', 49 | ), 50 | AlpineComponent::make( 51 | 'modals-component', 52 | __DIR__ . '/../dist/js/modals-component.js', 53 | ), 54 | ], 55 | package: 'guava/filament-knowledge-base' 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Livewire/HelpMenu.php: -------------------------------------------------------------------------------- 1 | route()->controller; 31 | 32 | $this->shouldOpenDocumentationInNewTab = Filament::getPlugin('guava::filament-knowledge-base')->shouldOpenDocumentationInNewTab(); 33 | 34 | $this->documentation = Arr::wrap(match (true) { 35 | $controller instanceof HasKnowledgeBase => $controller::getDocumentation(), 36 | $controller instanceof Page && in_array(HasKnowledgeBase::class, class_implements($controller::getResource())) => $controller::getResource()::getDocumentation(), 37 | default => [], 38 | }); 39 | } 40 | 41 | public function getDocumentation() 42 | { 43 | return collect($this->documentation) 44 | ->map(fn ($documentable) => KnowledgeBase::documentable($documentable)) 45 | ; 46 | } 47 | 48 | public function actions(): array 49 | { 50 | return $this->getDocumentation() 51 | // ->map(fn (string $class) => KnowledgeBasePanel::getDocumentationAction($class)) 52 | ->map( 53 | fn (Documentable $documentable) => HelpAction::forDocumentable($documentable) 54 | ->openUrlInNewTab($this->shouldOpenDocumentationInNewTab) 55 | ) 56 | ->toArray() 57 | ; 58 | } 59 | 60 | public function shouldShowAsMenu(): bool 61 | { 62 | return count($this->documentation) > 1; 63 | } 64 | 65 | public function getSingleAction(): HelpAction 66 | { 67 | return HelpAction::forDocumentable($this->getDocumentation()->first()) 68 | ->generic() 69 | ->openUrlInNewTab($this->shouldOpenDocumentationInNewTab) 70 | ; 71 | } 72 | 73 | public function getMenuAction(): ActionGroup 74 | { 75 | return ActionGroup::make($this->actions()) 76 | ->label(__('filament-knowledge-base::translations.help')) 77 | ->icon('heroicon-o-question-mark-circle') 78 | ->iconSize('lg') 79 | ->color('gray') 80 | ->button() 81 | ; 82 | } 83 | 84 | public function render() 85 | { 86 | return view('filament-knowledge-base::livewire.help-menu'); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Livewire/Modals.php: -------------------------------------------------------------------------------- 1 | shouldOpenDocumentationInNewTab = Filament::getPlugin('guava::filament-knowledge-base')->shouldOpenDocumentationInNewTab(); 27 | } 28 | 29 | #[On('close-modal')] 30 | public function onClose($id) 31 | { 32 | if ($id !== 'kb-custom-modal') { 33 | return; 34 | } 35 | $this->js(<<<'JS' 36 | $nextTick(() => { 37 | $wire.resetDocumentation(); 38 | }); 39 | JS); 40 | } 41 | 42 | public function showDocumentation(string $id) 43 | { 44 | $this->documentable = KnowledgeBase::documentable($id); 45 | 46 | $this->js(<<<'JS' 47 | $nextTick(() => { 48 | $dispatch('open-modal', { id: 'kb-custom-modal' }); 49 | }); 50 | JS); 51 | } 52 | 53 | public function resetDocumentation() 54 | { 55 | $this->documentable = null; 56 | } 57 | 58 | public function render() 59 | { 60 | return view('filament-knowledge-base::livewire.modals'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Markdown/MarkdownRenderer.php: -------------------------------------------------------------------------------- 1 | minimal = $minimal; 39 | 40 | return $this; 41 | } 42 | 43 | public function isMinimal(): bool 44 | { 45 | return $this->minimal; 46 | } 47 | 48 | protected function getOptions(): array 49 | { 50 | $anchorSymbol = KnowledgeBase::panel()->getAnchorSymbol(); 51 | $shouldDisableDefaultClasses = KnowledgeBase::panel()->shouldDisableDefaultClasses(); 52 | 53 | return [ 54 | 'default_attributes' => [ 55 | Heading::class => [ 56 | 'class' => $shouldDisableDefaultClasses 57 | ? 'relative' 58 | : static fn (Heading $node) => match ($node->getLevel()) { 59 | 1 => 'text-3xl mb-2 [&:first-child]:mt-0 mt-10', 60 | 2 => 'text-xl mb-2 [&:first-child]:mt-0 mt-2', 61 | 3 => 'text-lg mb-1 [&:first-child]:mt-0 mt-2', 62 | default => null, 63 | } . ' relative', 64 | ], 65 | Paragraph::class => [ 66 | 'class' => $shouldDisableDefaultClasses ? '' : 'mb-4 [&:last-child]:mb-0 leading-relaxed', 67 | ], 68 | ListBlock::class => [ 69 | 'class' => $shouldDisableDefaultClasses ? '' : 'mb-4 [&:last-child]:mb-0 leading-relaxed', 70 | ], 71 | Marker::class => [ 72 | 'class' => 'bg-primary-500/20 dark:bg-primary-400/40 text-inherit rounded-md py-0.5 px-1.5', 73 | ], 74 | BlockQuote::class => [ 75 | 'class' => $shouldDisableDefaultClasses ? '' : 'bg-white dark:bg-gray-900 mt-2 mb-4 p-4 rounded-md ring-1 ring-gray-950/5 dark:ring-white/10', 76 | ], 77 | ], 78 | 'heading_permalink' => [ 79 | 'id_prefix' => '', 80 | 'symbol' => $anchorSymbol ?? '', 81 | 'html_class' => Arr::toCssClasses([ 82 | 'gu-kb-anchor md:absolute md:-left-8 mr-2 md:mr-0 text-primary-600 dark:text-primary-500 font-bold', 83 | 'hidden' => ! $anchorSymbol, 84 | ]), 85 | ], 86 | 'table' => [ 87 | 'wrap' => [ 88 | 'enabled' => true, 89 | 'tag' => 'div', 90 | 'attributes' => [ 91 | 'class' => $shouldDisableDefaultClasses ? '' : Arr::toCssClasses([ 92 | 'divide-y divide-gray-200 overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:divide-white/10 dark:bg-gray-900 dark:ring-white/10', 93 | 'fi-ta-content relative divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10 !border-t-0', 94 | 'mb-4 [&:last-child]:mb-0 leading-relaxed', 95 | '[&_table]:fi-ta-table [&_table]:w-full [&_table]:table-auto [&_table]:divide-y [&_table]:divide-gray-200 [&_table]:text-start [&_table]:dark:divide-white/5', 96 | '[&_thead]:divide-y [&_thead]:divide-gray-200 [&_thead]:dark:divide-white/5', 97 | '[&_thead_tr]:bg-gray-50 [&_thead_tr]:dark:bg-white/5', 98 | '[&_thead_th]:text-start', 99 | '[&_th]:px-3 [&_th]:py-3.5', 100 | '[&_td]:px-3 [&_td]:py-3.5', 101 | '[&_tbody]:divide-y [&_tbody]:divide-gray-200 [&_tbody]:whitespace-nowrap [&_tbody]:dark:divide-white/5', 102 | ]), 103 | ], 104 | ], 105 | 'alignment_attributes' => [ 106 | 'left' => ['class' => '!text-start'], 107 | 'center' => ['class' => '!text-center'], 108 | 'right' => ['class' => '!text-end'], 109 | ], 110 | ], 111 | ]; 112 | } 113 | 114 | protected function configureEnvironment(EnvironmentBuilderInterface $environment): EnvironmentInterface 115 | { 116 | // Extensions 117 | $environment 118 | ->addExtension(new CommonMarkCoreExtension) 119 | ->addExtension(new DefaultAttributesExtension) 120 | ->addExtension(new AttributesExtension) 121 | ->addExtension(new FrontMatterExtension) 122 | ->addExtension(new MarkerExtension) 123 | ->addExtension(new TableExtension) 124 | ; 125 | if (! $this->isMinimal()) { 126 | $environment 127 | ->addExtension(new HeadingPermalinkExtension) 128 | ; 129 | } 130 | 131 | // Parsers 132 | $environment->addInlineParser(new IncludeParser($this)); 133 | 134 | // Renderers 135 | $environment 136 | ->addRenderer(Image::class, new ImageRenderer, 5) 137 | ; 138 | 139 | if (KnowledgeBasePanel::hasSyntaxHighlighting()) { 140 | $environment 141 | ->addRenderer(FencedCode::class, new FencedCodeRenderer, 5) 142 | ; 143 | } 144 | 145 | return $environment; 146 | } 147 | 148 | protected function getEnvironment(): EnvironmentInterface 149 | { 150 | return $this->configureEnvironment( 151 | environment: new Environment( 152 | config: $this->getOptions() 153 | ) 154 | ); 155 | } 156 | 157 | protected function getMarkdownConverter(): MarkdownConverter 158 | { 159 | return new MarkdownConverter( 160 | environment: $this->getEnvironment() 161 | ); 162 | } 163 | 164 | public function convert(string $input): RenderedContentInterface 165 | { 166 | $ttl = config('filament-knowledge-base.cache.ttl'); 167 | 168 | if ($ttl === 'forever') { 169 | return cache()->rememberForever($this->getCacheKey($input), fn () => $this->getMarkdownConverter()->convert($input)); 170 | } 171 | 172 | if (! is_int($ttl) || $ttl < 1) { 173 | throw new InvalidArgumentException('The cache.ttl configuration must be an integer greater than 0 or the string "forever".'); 174 | } 175 | 176 | return cache()->remember($this->getCacheKey($input), $ttl, fn () => $this->getMarkdownConverter()->convert($input)); 177 | } 178 | 179 | protected function getCacheKey(string $input): string 180 | { 181 | $options = json_encode([ 182 | 'minimal' => $this->isMinimal(), 183 | ]); 184 | 185 | return config('filament-knowledge-base.cache.prefix') . md5("kb.$input.$options"); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Markdown/Parsers/IncludeParser.php: -------------------------------------------------------------------------------- 1 | getCursor(); 22 | // The @ symbol must not have any other characters immediately prior 23 | $previousChar = $cursor->peek(-1); 24 | if ($previousChar !== null && $previousChar !== ' ' && $previousChar !== "\n") { 25 | // peek() doesn't modify the cursor, so no need to restore state first 26 | return false; 27 | } 28 | 29 | // This seems to be a valid match 30 | // Advance the cursor to the end of the match 31 | $cursor->advanceBy($inlineContext->getFullMatchLength()); 32 | 33 | // Grab the Twitter handle 34 | [$path] = $inlineContext->getSubMatches(); 35 | 36 | $result = $this->renderer->convert( 37 | file_get_contents( 38 | str(base_path(config('filament-knowledge-base.docs-path'))) 39 | ->rtrim('/') 40 | ->append( 41 | '/', 42 | \App::getLocale(), 43 | '/', 44 | str($path) 45 | ->trim('/') 46 | ->replaceEnd('.md', '') 47 | ->replace('.', '/') 48 | ->append('.md') 49 | ), 50 | ) 51 | ); 52 | 53 | $inlineContext->getContainer()->replaceWith($result->getDocument()); 54 | 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Markdown/Parsers/VariableParser.php: -------------------------------------------------------------------------------- 1 | record) { 25 | return false; 26 | } 27 | 28 | $cursor = $inlineContext->getCursor(); 29 | // The @ symbol must not have any other characters immediately prior 30 | $previousChar = $cursor->peek(-1); 31 | if ($previousChar !== null && $previousChar !== ' ') { 32 | // peek() doesn't modify the cursor, so no need to restore state first 33 | return false; 34 | } 35 | 36 | // This seems to be a valid match 37 | // Advance the cursor to the end of the match 38 | $cursor->advanceBy($inlineContext->getFullMatchLength()); 39 | 40 | // Grab the Twitter handle 41 | [$variable] = $inlineContext->getSubMatches(); 42 | 43 | if ($content = $this->record->$variable) { 44 | $inlineContext->getContainer()->appendChild(new Text($content)); 45 | 46 | return true; 47 | } 48 | 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Markdown/Renderers/FencedCodeRenderer.php: -------------------------------------------------------------------------------- 1 | shiki = new Shiki; 26 | $this->baseRenderer = new BaseRenderer; 27 | } 28 | 29 | private function getLanguage($language): string 30 | { 31 | $home = getenv('HOME'); 32 | $command = [ 33 | (new ExecutableFinder)->find('node', 'node', [ 34 | '/usr/local/bin', 35 | '/opt/homebrew/bin', 36 | $home . '/n/bin', // support https://github.com/tj/n 37 | ]), 38 | 'grammars.js', 39 | $language, 40 | ]; 41 | 42 | $path = realpath(__DIR__ . '/../../../bin'); 43 | 44 | $process = new Process( 45 | $command, 46 | $path, 47 | null, 48 | ); 49 | 50 | $process->run(); 51 | 52 | if (! $process->isSuccessful()) { 53 | throw new ProcessFailedException($process); 54 | } 55 | 56 | return $process->getOutput(); 57 | } 58 | 59 | public function render( 60 | Node $node, 61 | ChildNodeRendererInterface $childRenderer 62 | ): string { 63 | /** @var HtmlElement $element */ 64 | $element = $this->baseRenderer->render($node, $childRenderer); 65 | 66 | $languageId = $this->getSpecifiedLanguage($node) ?? 'text'; 67 | $languageName = $this->getLanguage($languageId); 68 | 69 | $pattern = '/]*>(.*)<\/code>/is'; 70 | $replacement = '$1'; 71 | 72 | $result = preg_replace($pattern, $replacement, $element->getContents()); 73 | $result = htmlspecialchars_decode($result); 74 | $code = $this->shiki->highlightCode( 75 | $result, 76 | $languageId, 77 | 'github-dark', 78 | ); 79 | $element->setContents( 80 | $code 81 | ); 82 | 83 | return view('filament-knowledge-base::code-block', [ 84 | 'code' => $element->getContents(), 85 | 'language' => $languageName, 86 | ]); 87 | } 88 | 89 | protected function getSpecifiedLanguage(FencedCode $block): ?string 90 | { 91 | $infoWords = $block->getInfoWords(); 92 | 93 | if (empty($infoWords) || empty($infoWords[0])) { 94 | return null; 95 | } 96 | 97 | return Xml::escape($infoWords[0]); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Markdown/Renderers/ImageRenderer.php: -------------------------------------------------------------------------------- 1 | data->get('attributes'); 37 | 38 | $forbidUnsafeLinks = ! $this->config->get('allow_unsafe_links'); 39 | if ($forbidUnsafeLinks && RegexHelper::isLinkPotentiallyUnsafe($node->getUrl())) { 40 | $attrs['src'] = ''; 41 | } else { 42 | $attrs['src'] = Str::startsWith($url = $node->getUrl(), ['http://', 'https://']) 43 | ? $url 44 | : Vite::asset(str($url)->ltrim('/')->toString()); 45 | } 46 | 47 | $attrs['alt'] = $this->getAltText($node); 48 | 49 | if (($title = $node->getTitle()) !== null) { 50 | $attrs['title'] = $title; 51 | } 52 | 53 | if (str($node->getUrl())->endsWith('.mov')) { 54 | return new HtmlElement('video', ['muted' => 'muted', 'autoplay' => 'autoplay', 'loop' => 'loop', 55 | 'class' => 'rounded-md ring-1 ring-gray-950/5 dark:ring-white/10', 56 | ], new HtmlElement('source', $attrs), true); 57 | } else { 58 | return new HtmlElement('img', $attrs, '', true); 59 | } 60 | } 61 | 62 | public function setConfiguration(ConfigurationInterface $configuration): void 63 | { 64 | $this->config = $configuration; 65 | } 66 | 67 | public function getXmlTagName(Node $node): string 68 | { 69 | return 'image'; 70 | } 71 | 72 | /** 73 | * @param Image $node 74 | * @return array 75 | * 76 | * @psalm-suppress MoreSpecificImplementedParamType 77 | */ 78 | public function getXmlAttributes(Node $node): array 79 | { 80 | Image::assertInstanceOf($node); 81 | 82 | return [ 83 | 'destination' => $node->getUrl(), 84 | 'title' => $node->getTitle() ?? '', 85 | ]; 86 | } 87 | 88 | private function getAltText(Image $node): string 89 | { 90 | $altText = ''; 91 | 92 | foreach ((new NodeIterator($node)) as $n) { 93 | if ($n instanceof StringContainerInterface) { 94 | $altText .= $n->getLiteral(); 95 | } elseif ($n instanceof Newline) { 96 | $altText .= "\n"; 97 | } 98 | } 99 | 100 | return $altText; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Models/FlatfileDocumentation.php: -------------------------------------------------------------------------------- 1 | 'string', 27 | 'slug' => 'string', 28 | 'path' => 'string', 29 | 'content' => 'text', 30 | 'title' => 'string', 31 | 'group' => 'string', 32 | 'icon' => 'string', 33 | 'parent' => 'string', 34 | 'order' => 'integer', 35 | ]; 36 | 37 | public $incrementing = false; 38 | 39 | public function getLocale(): string 40 | { 41 | return App::getLocale(); 42 | } 43 | 44 | public function getFallbackLocale(): string 45 | { 46 | return App::getFallbackLocale(); 47 | } 48 | 49 | public function getRows() 50 | { 51 | $path = base_path( 52 | str(config('filament-knowledge-base.docs-path')) 53 | ->append('/') 54 | ->append($this->getLocale()) 55 | ); 56 | 57 | if (! File::exists($path)) { 58 | $path = base_path( 59 | str(config('filament-knowledge-base.docs-path')) 60 | ->append('/') 61 | ->append($this->getFallbackLocale()) 62 | ); 63 | } 64 | 65 | return collect(File::allFiles($path)) 66 | ->map(function (\SplFileInfo $file) use ($path) { 67 | $data = KnowledgeBase::parseMarkdown($file->getRealPath()); 68 | 69 | $id = str($file->getPathname()) 70 | ->afterLast($path) 71 | ->beforeLast($file->getExtension()) 72 | ->replace(DIRECTORY_SEPARATOR, '.') 73 | ->trim('.') 74 | ; 75 | 76 | $parts = $id->explode('.', 3); 77 | $group = data_get($data, 'front-matter.group'); 78 | $parent = data_get($data, 'front-matter.parent'); 79 | if (count($parts) >= 2) { 80 | $group ??= Str::headline($parts[0]); 81 | } 82 | if (count($parts) >= 3) { 83 | $parent ??= Str::headline($parts[1]); 84 | } 85 | 86 | return [ 87 | 'id' => $id, 88 | 'slug' => $id->replace('.', '/')->toString(), 89 | 'path' => $file->getRealPath(), 90 | 'content' => data_get($data, 'html'), 91 | 'title' => data_get($data, 'front-matter.title', $id->afterLast('.')->headline()), 92 | 'group' => $group, 93 | 'icon' => data_get($data, 'front-matter.icon'), 94 | 'parent' => $parent, 95 | 'order' => data_get($data, 'front-matter.order'), 96 | ]; 97 | }) 98 | ->toArray() 99 | ; 100 | } 101 | 102 | public function getFrontMatter(): array 103 | { 104 | $result = $this->getHtml(); 105 | 106 | if ($result instanceof RenderedContentWithFrontMatter) { 107 | return $result->getFrontMatter(); 108 | } 109 | 110 | return []; 111 | } 112 | 113 | public function getHtml(): RenderedContentInterface 114 | { 115 | $converter = app(MarkdownRenderer::class); 116 | 117 | return $converter->convert(file_get_contents($this->path)); 118 | } 119 | 120 | public function getSimpleHtml(): RenderedContentInterface 121 | { 122 | $converter = app(MarkdownRenderer::class)->minimal(); 123 | 124 | return $converter->convert(file_get_contents($this->path)); 125 | } 126 | 127 | public function getUrl(): string 128 | { 129 | return ViewDocumentation::getUrl(parameters: [ 130 | 'record' => $this, 131 | ], panel: KnowledgeBase::panelId()); 132 | } 133 | 134 | public function getAnchors() 135 | { 136 | $walker = $this->getHtml()->getDocument()->walker(); 137 | $anchors = []; 138 | while ($event = $walker->next()) { 139 | $node = $event->getNode(); 140 | 141 | if ($event->isEntering() && $node instanceof HeadingPermalink) { 142 | $slug = $node->getSlug(); 143 | $next = $node->next(); 144 | if (! method_exists($next, 'getLiteral')) { 145 | continue; 146 | } 147 | // dd($node, $node->next()); 148 | // } 149 | $anchors[$next->getLiteral()] = $slug; 150 | } 151 | } 152 | 153 | return $anchors; 154 | } 155 | 156 | public function getRouteKeyName() 157 | { 158 | return 'slug'; 159 | } 160 | 161 | public function getId(): string 162 | { 163 | return $this->id; 164 | } 165 | 166 | public function getTitle(): ?string 167 | { 168 | return $this->title; 169 | } 170 | 171 | public function getContent(): string 172 | { 173 | return $this->content; 174 | } 175 | 176 | public function getParent(): ?string 177 | { 178 | return $this->parent; 179 | } 180 | 181 | public function getParentId(): ?string 182 | { 183 | $parts = str($this->getId())->explode('.', 3); 184 | if (count($parts) >= 3) { 185 | return $parts[0] . '.' . $parts[1]; 186 | } elseif (count($parts) == 2) { 187 | return $parts[0]; 188 | } 189 | 190 | return null; 191 | } 192 | 193 | public function getGroup(): ?string 194 | { 195 | return $this->group; 196 | } 197 | 198 | public function getOrder(): int 199 | { 200 | return $this->order ?? 0; 201 | } 202 | 203 | public function getIcon(): ?string 204 | { 205 | return $this->icon ?? 'heroicon-o-document'; 206 | } 207 | 208 | public function isRegistered(): bool 209 | { 210 | return ! empty($this->getTitle()); 211 | } 212 | 213 | public function getBreadcrumbs(): array 214 | { 215 | return collect([ 216 | KnowledgeBase::panel()->getUrl() => __('filament-knowledge-base::translations.knowledge-base'), 217 | ]) 218 | ->when( 219 | $group = $this->getGroup(), 220 | fn (Collection $collection) => $collection->put($this->getGroupUrl() . '#', $group) 221 | ) 222 | ->when( 223 | $parent = $this->getParent(), 224 | fn (Collection $collection) => $collection->put( 225 | KnowledgeBase::documentable($this->getParentId())->getUrl(), 226 | $parent, 227 | ) 228 | ) 229 | ->put($this->getUrl(), $this->getTitle()) 230 | ->toArray() 231 | ; 232 | } 233 | 234 | public function getGroupUrl() 235 | { 236 | $group = collect(KnowledgeBase::panel()->getNavigation()) 237 | ->first(function ($item) { 238 | return $item instanceof NavigationGroup && $item->getLabel() === $this->getGroup(); 239 | }) 240 | ; 241 | 242 | if ($group) { 243 | return Arr::first($group->getItems())->getUrl(); 244 | } 245 | 246 | return null; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/Models/RelationalDocumentation.php: -------------------------------------------------------------------------------- 1 | append('/') 27 | ->append(App::getLocale()) 28 | ); 29 | 30 | return collect(File::allFiles($path)) 31 | ->map(function (\SplFileInfo $file) use ($path) { 32 | $data = KnowledgeBase::parseMarkdown($file->getRealPath()); 33 | 34 | $id = str($file->getPathname()) 35 | ->afterLast($path) 36 | ->beforeLast($file->getExtension()) 37 | ->replace('/', '.') 38 | ->trim('.') 39 | ; 40 | 41 | return [ 42 | // 'id' => $id, 43 | 'slug' => $id->replace('.', '/')->toString(), 44 | 'path' => $file->getRealPath(), 45 | 'content' => data_get($data, 'html'), 46 | 'title' => data_get($data, 'front-matter.title'), 47 | 'group' => data_get($data, 'front-matter.group'), 48 | 'icon' => data_get($data, 'front-matter.icon'), 49 | 'parent' => data_get($data, 'front-matter.parent'), 50 | 'order' => data_get($data, 'front-matter.order'), 51 | ]; 52 | }) 53 | ->toArray() 54 | ; 55 | collect(Discover::in(app_path('KnowledgeBasePanel')) 56 | ->extending(\Guava\FilamentKnowledgeBase\Pages\Documentation::class) 57 | ->get()); 58 | } 59 | 60 | public function getFrontMatter(): array 61 | { 62 | $result = $this->getHtml(); 63 | 64 | if ($result instanceof RenderedContentWithFrontMatter) { 65 | return $result->getFrontMatter(); 66 | } 67 | 68 | return []; 69 | } 70 | 71 | public function getHtml(): RenderedContentInterface 72 | { 73 | $converter = app(MarkdownRenderer::class) 74 | ->record($this) 75 | ; 76 | 77 | return $converter->convertToHtml(file_get_contents($this->path)); 78 | } 79 | 80 | public function getSimpleHtml(): RenderedContentInterface 81 | { 82 | $converter = app(MarkdownRenderer::class)->minimal(); 83 | 84 | return $converter->convertToHtml(file_get_contents($this->path)); 85 | } 86 | 87 | public function getPart(string $id) 88 | { 89 | $walker = $this->getHtml()->getDocument()->walker(); 90 | 91 | while ($event = $walker->next()) { 92 | $node = $event->getNode(); 93 | if ($node instanceof Heading) { 94 | } 95 | } 96 | 97 | } 98 | 99 | public function getUrl(): string 100 | { 101 | return ViewDocumentation::getUrl(parameters: [ 102 | 'record' => $this, 103 | ], panel: config('filament-knowledge-base.panel.id')); 104 | } 105 | 106 | public function getAnchors() 107 | { 108 | $walker = $this->getHtml()->getDocument()->walker(); 109 | $anchors = []; 110 | while ($event = $walker->next()) { 111 | $node = $event->getNode(); 112 | 113 | if ($event->isEntering() && $node instanceof HeadingPermalink) { 114 | $slug = $node->getSlug(); 115 | $anchors[$node->next()->getLiteral()] = $slug; 116 | } 117 | } 118 | 119 | return $anchors; 120 | } 121 | 122 | public function getRouteKeyName() 123 | { 124 | return 'slug'; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Providers/KnowledgeBasePanelProvider.php: -------------------------------------------------------------------------------- 1 |