diff --git a/public/assets/css/stylesheet.css b/public/assets/css/stylesheet.print.css similarity index 100% rename from public/assets/css/stylesheet.css rename to public/assets/css/stylesheet.print.css diff --git a/public/composer.json b/public/composer.json index d3864ae..ca005e1 100644 --- a/public/composer.json +++ b/public/composer.json @@ -22,7 +22,8 @@ }, "require": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "getkirby/cms": "^5.0" + "getkirby/cms": "^5.0", + "sylvainjule/code-editor": "^1.1" }, "config": { "allow-plugins": { diff --git a/public/composer.lock b/public/composer.lock index 5d9b2c4..bc95da5 100644 --- a/public/composer.lock +++ b/public/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0b7fb803e22a45eb87e24172337208aa", + "content-hash": "82adb49b472cb54cd88e72b31f49ada3", "packages": [ { "name": "christian-riesen/base32", @@ -725,6 +725,44 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "sylvainjule/code-editor", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sylvainjule/kirby-code-editor.git", + "reference": "adbc2c8a728994cc57ea72a7f8628f27d202b8df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sylvainjule/kirby-code-editor/zipball/adbc2c8a728994cc57ea72a7f8628f27d202b8df", + "reference": "adbc2c8a728994cc57ea72a7f8628f27d202b8df", + "shasum": "" + }, + "require": { + "getkirby/composer-installer": "^1.2" + }, + "type": "kirby-plugin", + "extra": { + "installer-name": "code-editor" + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sylvain Julé", + "email": "contact@sylvain-jule.fr" + } + ], + "description": "Code editor field for Kirby 3, 4 and 5", + "support": { + "issues": "https://github.com/sylvainjule/kirby-code-editor/issues", + "source": "https://github.com/sylvainjule/kirby-code-editor/tree/1.1.0" + }, + "time": "2025-08-04T17:32:08+00:00" + }, { "name": "symfony/deprecation-contracts", "version": "v3.6.0", diff --git a/public/content/1_cohesion-des-mondes/5_test-avec-geoformat/narrative.txt b/public/content/1_cohesion-des-mondes/5_test-avec-geoformat/narrative.txt index 2c7e609..02fe167 100644 --- a/public/content/1_cohesion-des-mondes/5_test-avec-geoformat/narrative.txt +++ b/public/content/1_cohesion-des-mondes/5_test-avec-geoformat/narrative.txt @@ -14,4 +14,14 @@ Introduction:

Ah le Japon... Quel beau pays où nous trouvons des créatures ---- +Customcss: + +@page { + size: A4; + margin: 50mm 15mm 26mm 15mm; + background: rgba(255, 255, 255, 1); +} + +---- + Uuid: xi60pjkz5bp1nlwp \ No newline at end of file diff --git a/public/site/blueprints/pages/narrative.yml b/public/site/blueprints/pages/narrative.yml index c0a7001..88fda34 100644 --- a/public/site/blueprints/pages/narrative.yml +++ b/public/site/blueprints/pages/narrative.yml @@ -22,6 +22,13 @@ columns: introduction: label: Introduction type: writer + customCss: + label: Custom CSS + type: code-editor + language: css + help: Custom CSS styling for this narrative's print view + theme: monokai + size: large pages: label: Pages type: pages diff --git a/public/site/plugins/code-editor/.editorconfig b/public/site/plugins/code-editor/.editorconfig new file mode 100644 index 0000000..3c7a80f --- /dev/null +++ b/public/site/plugins/code-editor/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[*.php] +indent_size = 4 diff --git a/public/site/plugins/code-editor/.gitignore b/public/site/plugins/code-editor/.gitignore new file mode 100644 index 0000000..e68e503 --- /dev/null +++ b/public/site/plugins/code-editor/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.cache +node_modules +package-lock.json +yarn.lock +composer.lock \ No newline at end of file diff --git a/public/site/plugins/code-editor/LICENSE b/public/site/plugins/code-editor/LICENSE new file mode 100644 index 0000000..ee27a83 --- /dev/null +++ b/public/site/plugins/code-editor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Sylvain Julé + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/public/site/plugins/code-editor/README.md b/public/site/plugins/code-editor/README.md new file mode 100644 index 0000000..565c03f --- /dev/null +++ b/public/site/plugins/code-editor/README.md @@ -0,0 +1,111 @@ +# Kirby – Code editor + +Code editor field for Kirby 3, 4 and 5. + +![screenshot-code-editor](https://user-images.githubusercontent.com/14079751/109679014-7b043800-7b7b-11eb-8c4e-2ae25da8288d.png) + +
+ +## Overview + +> This plugin is completely free and published under the MIT license. However, if you are using it in a commercial project and want to help me keep up with maintenance, you can consider [making a donation of your choice](https://paypal.me/sylvainjl). + +- [1. Installation](#1-installation) +- [2. Setup](#2-setup) +- [3. Options](#3-options) +- [4. Available languages](#4-available-languages) +- [5. License](#5-license) +- [6. Credits](#6-credits) + +
+ +## 1. Installation + +Download and copy this repository to ```/site/plugins/code-editor``` + +Alternatively, you can install it with composer: ```composer require sylvainjule/code-editor``` + +
+ +## 2. Setup + +This field adds a code editor in the panel: + +```yaml +editor: + label: My code editor + type: code-editor +``` + +
+ +## 3. Options + +| Name | Type | Default | Options | Description | +| -------------------- | ------------------ | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| language | `String` | `'css'` | - | Syntax mode of the editor. See below for available languages | +| size | `String` | `'small'` | - | Min height of the editor. `small / medium / large / huge` | +| lineNumbers | `Boolean` | `true` | - | Whether to show line numbers. | +| tabSize | `number` | `4` | - | The number of characters to insert when pressing tab key. | +| insertSpaces | `boolean` | `true` | - | Whether to use spaces for indentation. If you set it to `false`, you might also want to set `tabSize` to `1` | +| ignoreTabKey | `boolean` | `false` | - | Whether the editor should ignore tab key presses so that keyboard users can tab past the editor. Users can toggle this behaviour using `Ctrl+Shift+M` (Mac) / `Ctrl+M` manually when this is `false`. | + + +Note that you can make the default height any height you want with some [custom panel CSS](https://getkirby.com/docs/reference/system/options/panel#custom-panel-css). First, set the `size` option to any string you'd like: + +```yaml +size: custom-size +``` + +Then in your `panel.css`: + +```css +.k-code-editor-input[data-size="custom-size"] { + min-height: 15rem; +} +``` + +### 3.1. Default options + +You can globally override the default options, instead of setting them on a per-field basis. In your `site/config/config.php`: + +```php +return [ + 'sylvainjule.code-editor.language' => 'css', + 'sylvainjule.code-editor.size' => 'small', + 'sylvainjule.code-editor.lineNumbers' => true, + 'sylvainjule.code-editor.tabSize' => 4, + 'sylvainjule.code-editor.insertSpaces' => true, + 'sylvainjule.code-editor.ignoreTabKey' => false, +]; +``` + +
+ +## 4. Available languages + +Currently supported languages are: + +* `css` +* `javascript` +* `json` +* `less` +* `php` +* `python` +* `ruby` +* `scss` +* `yaml` + +
+ +## 5. License + +MIT + +
+ +## 6. Credits + +**Code editor:** + +- [Vue Prism Editor](https://github.com/koca/vue-prism-editor) diff --git a/public/site/plugins/code-editor/composer.json b/public/site/plugins/code-editor/composer.json new file mode 100644 index 0000000..bc733d6 --- /dev/null +++ b/public/site/plugins/code-editor/composer.json @@ -0,0 +1,20 @@ +{ + "name": "sylvainjule/code-editor", + "description": "Code editor field for Kirby 3, 4 and 5", + "type": "kirby-plugin", + "license": "MIT", + "version": "1.1.0", + "authors": [ + { + "name": "Sylvain Julé", + "email": "contact@sylvain-jule.fr" + } + ], + "require": { + "getkirby/composer-installer": "^1.2" + }, + "extra": { + "installer-name": "code-editor" + }, + "minimum-stability": "beta" +} diff --git a/public/site/plugins/code-editor/eslint.config.mjs b/public/site/plugins/code-editor/eslint.config.mjs new file mode 100644 index 0000000..cfe049e --- /dev/null +++ b/public/site/plugins/code-editor/eslint.config.mjs @@ -0,0 +1,28 @@ +import js from "@eslint/js"; +import prettier from "eslint-config-prettier"; +import vue from "eslint-plugin-vue"; + +export default [ + js.configs.recommended, + ...vue.configs["flat/vue2-recommended"], + prettier, + { + rules: { + "vue/attributes-order": "error", + "vue/component-definition-name-casing": "off", + "vue/html-closing-bracket-newline": [ + "error", + { + singleline: "never", + multiline: "always" + } + ], + "vue/multi-word-component-names": "off", + "vue/require-default-prop": "off", + "vue/require-prop-types": "error" + }, + languageOptions: { + ecmaVersion: 2022 + } + } +]; diff --git a/public/site/plugins/code-editor/index.css b/public/site/plugins/code-editor/index.css new file mode 100644 index 0000000..e223c39 --- /dev/null +++ b/public/site/plugins/code-editor/index.css @@ -0,0 +1 @@ +.prism-editor-wrapper{width:100%;height:100%;display:flex;align-items:flex-start;overflow:auto;-o-tab-size:1.5em;tab-size:1.5em;-moz-tab-size:1.5em}@media (-ms-high-contrast:active),(-ms-high-contrast:none){.prism-editor-wrapper .prism-editor__textarea{color:transparent!important}.prism-editor-wrapper .prism-editor__textarea::-moz-selection{background-color:#accef7!important;color:transparent!important}.prism-editor-wrapper .prism-editor__textarea::selection{background-color:#accef7!important;color:transparent!important}}.prism-editor-wrapper .prism-editor__container{position:relative;text-align:left;box-sizing:border-box;padding:0;overflow:hidden;width:100%}.prism-editor-wrapper .prism-editor__line-numbers{height:100%;overflow:hidden;flex-shrink:0;padding-top:4px;margin-top:0;margin-right:10px}.prism-editor-wrapper .prism-editor__line-number{text-align:right;white-space:nowrap}.prism-editor-wrapper .prism-editor__textarea{position:absolute;top:0;left:0;height:100%;width:100%;resize:none;color:inherit;overflow:hidden;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;-webkit-text-fill-color:transparent}.prism-editor-wrapper .prism-editor__editor,.prism-editor-wrapper .prism-editor__textarea{margin:0;border:0;background:none;box-sizing:inherit;display:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-variant-ligatures:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;-moz-tab-size:inherit;-o-tab-size:inherit;tab-size:inherit;text-indent:inherit;text-rendering:inherit;text-transform:inherit;white-space:pre-wrap;word-wrap:keep-all;overflow-wrap:break-word;padding:0}.prism-editor-wrapper .prism-editor__textarea--empty{-webkit-text-fill-color:inherit!important}.prism-editor-wrapper .prism-editor__editor{position:relative;pointer-events:none}code[class*=language-],pre[class*=language-]{color:#ccc;background:none;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.comment,.token.block-comment,.token.prolog,.token.doctype,.token.cdata{color:#999}.token.punctuation{color:#ccc}.token.tag,.token.attr-name,.token.namespace,.token.deleted{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.number,.token.function{color:#f08d49}.token.property,.token.class-name,.token.constant,.token.symbol{color:#f8c555}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin{color:#cc99cd}.token.string,.token.char,.token.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc}.token.important,.token.bold{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}.k-code-editor-input{background:light-dark(var(--color-gray-950),var(--input-color-back));color:var(--color-gray-200);font-family:var(--font-mono);font-size:var(--text-sm);line-height:1.5;padding:var(--spacing-2);border-radius:var(--rounded)}.k-code-editor-input[data-size=small]{min-height:7.5rem}.k-code-editor-input[data-size=medium]{min-height:15rem}.k-code-editor-input[data-size=large]{min-height:30rem}.k-code-editor-input[data-size=huge]{min-height:45rem}.prism-editor__textarea:focus{outline:none} diff --git a/public/site/plugins/code-editor/index.js b/public/site/plugins/code-editor/index.js new file mode 100644 index 0000000..ff49136 --- /dev/null +++ b/public/site/plugins/code-editor/index.js @@ -0,0 +1,14 @@ +(function(){"use strict";function T(){return T=Object.assign||function(l){for(var e=1;e";return e},lineNumbersCount:function(){var e=this.codeData.split(/\r\n|\n/).length;return e}},mounted:function(){this._recordCurrentState(),this.styleLineNumbers()},methods:{setLineNumbersHeight:function(){this.lineNumbersHeight=getComputedStyle(this.$refs.pre).height},styleLineNumbers:function(){if(!(!this.lineNumbers||!this.autoStyleLineNumbers)){var e=this.$refs.pre,a=this.$el.querySelector(".prism-editor__line-numbers"),u=window.getComputedStyle(e);this.$nextTick(function(){var h="border-top-left-radius",y="border-bottom-left-radius";if(a){a.style[h]=u[h],a.style[y]=u[y],e.style[h]="0",e.style[y]="0";var c=["background-color","margin-top","padding-top","font-family","font-size","line-height"];c.forEach(function(r){a.style[r]=u[r]}),a.style["margin-bottom"]="-"+u["padding-top"]}})}},_recordCurrentState:function(){var e=this.$refs.textarea;if(e){var a=e.value,u=e.selectionStart,h=e.selectionEnd;this._recordChange({value:a,selectionStart:u,selectionEnd:h})}},_getLines:function(e,a){return e.substring(0,a).split(` +`)},_applyEdits:function(e){var a=this.$refs.textarea,u=this.history.stack[this.history.offset];u&&a&&(this.history.stack[this.history.offset]=T({},u,{selectionStart:a.selectionStart,selectionEnd:a.selectionEnd})),this._recordChange(e),this._updateInput(e)},_recordChange:function(e,a){a===void 0&&(a=!1);var u=this.history,h=u.stack,y=u.offset;if(h.length&&y>-1){this.history.stack=h.slice(0,y+1);var c=this.history.stack.length;if(c>Q){var r=c-Q;this.history.stack=h.slice(r,c),this.history.offset=Math.max(this.history.offset-r,0)}}var d=Date.now();if(a){var g=this.history.stack[this.history.offset];if(g&&d-g.timestamp=S&&K<=_&&k.startsWith(g)?k.substring(g.length):k}).join(` +`);if(c!==F){var A=x[S];this._applyEdits({value:F,selectionStart:A.startsWith(g)?r-g.length:r,selectionEnd:d-(c.length-F.length)})}}else if(r!==d){var $=this._getLines(c,r),z=$.length-1,P=this._getLines(c,d).length-1,n=$[z];this._applyEdits({value:c.split(` +`).map(function(k,K){return K>=z&&K<=P?g+k:k}).join(` +`),selectionStart:/\S/.test(n)?r+g.length:r,selectionEnd:d+g.length*(P-z+1)})}else{var t=r+g.length;this._applyEdits({value:c.substring(0,r)+g+c.substring(d),selectionStart:t,selectionEnd:t})}else if(e.keyCode===ie){var i=r!==d,s=c.substring(0,r);if(s.endsWith(g)&&!i){e.preventDefault();var o=r-g.length;this._applyEdits({value:c.substring(0,r-g.length)+c.substring(d),selectionStart:o,selectionEnd:o})}}else if(e.keyCode===re){if(r===d){var p=this._getLines(c,r).pop(),b=p==null?void 0:p.match(/^\s+/);if(b&&b[0]){e.preventDefault();var f=` +`+b[0],v=r+f.length;this._applyEdits({value:c.substring(0,r)+f+c.substring(d),selectionStart:v,selectionEnd:v})}}}else if(e.keyCode===W||e.keyCode===Y||e.keyCode===G||e.keyCode===U){var m;e.keyCode===W&&e.shiftKey?m=["(",")"]:e.keyCode===Y?e.shiftKey?m=["{","}"]:m=["[","]"]:e.keyCode===G?e.shiftKey?m=['"','"']:m=["'","'"]:e.keyCode===U&&!e.shiftKey&&(m=["`","`"]),r!==d&&m&&(e.preventDefault(),this._applyEdits({value:c.substring(0,r)+m[0]+c.substring(r,d)+m[1]+c.substring(d),selectionStart:r,selectionEnd:d+2}))}else(q?e.metaKey&&e.keyCode===B:e.ctrlKey&&e.keyCode===B)&&!e.shiftKey&&!e.altKey?(e.preventDefault(),this._undoEdit()):(q?e.metaKey&&e.keyCode===B&&e.shiftKey:de?e.ctrlKey&&e.keyCode===se:e.ctrlKey&&e.keyCode===B&&e.shiftKey)&&!e.altKey?(e.preventDefault(),this._redoEdit()):e.keyCode===oe&&e.ctrlKey&&(!q||e.shiftKey)&&(e.preventDefault(),this.capture=!this.capture)}}},render:function(e){var a=this,u=e("div",{attrs:{class:"prism-editor__line-width-calc",style:"height: 0px; visibility: hidden; pointer-events: none;"}},"999"),h=e("div",{staticClass:"prism-editor__line-numbers",style:{"min-height":this.lineNumbersHeight},attrs:{"aria-hidden":"true"}},[u,Array.from(Array(this.lineNumbersCount).keys()).map(function(d,g){return e("div",{attrs:{class:"prism-editor__line-number token comment"}},""+ ++g)})]),y=e("textarea",{ref:"textarea",on:{input:this.handleChange,keydown:this.handleKeyDown,click:function(g){a.$emit("click",g)},keyup:function(g){a.$emit("keyup",g)},focus:function(g){a.$emit("focus",g)},blur:function(g){a.$emit("blur",g)}},staticClass:"prism-editor__textarea",class:{"prism-editor__textarea--empty":this.isEmpty},attrs:{spellCheck:"false",autocapitalize:"off",autocomplete:"off",autocorrect:"off","data-gramm":"false",placeholder:this.placeholder,"data-testid":"textarea",readonly:this.readonly},domProps:{value:this.codeData}}),c=e("pre",{ref:"pre",staticClass:"prism-editor__editor",attrs:{"data-testid":"preview"},domProps:{innerHTML:this.content}}),r=e("div",{staticClass:"prism-editor__container"},[y,c]);return e("div",{staticClass:"prism-editor-wrapper"},[this.lineNumbers&&h,r])}},J=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},X={exports:{}};(function(l){var e=typeof window<"u"?window:typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope?self:{};/** + * Prism: Lightweight, robust, elegant syntax highlighting + * + * @license MIT + * @author Lea Verou + * @namespace + * @public + */var a=function(u){var h=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,y=0,c={},r={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function n(t){return t instanceof d?new d(t.type,n(t.content),t.alias):Array.isArray(t)?t.map(n):t.replace(/&/g,"&").replace(/"u")return null;if(document.currentScript&&document.currentScript.tagName==="SCRIPT")return document.currentScript;try{throw new Error}catch(s){var n=(/at [^(\r\n]*\((.*):[^:]+:[^:]+\)$/i.exec(s.stack)||[])[1];if(n){var t=document.getElementsByTagName("script");for(var i in t)if(t[i].src==n)return t[i]}return null}},isActive:function(n,t,i){for(var s="no-"+t;n;){var o=n.classList;if(o.contains(t))return!0;if(o.contains(s))return!1;n=n.parentElement}return!!i}},languages:{plain:c,plaintext:c,text:c,txt:c,extend:function(n,t){var i=r.util.clone(r.languages[n]);for(var s in t)i[s]=t[s];return i},insertBefore:function(n,t,i,s){s=s||r.languages;var o=s[n],p={};for(var b in o)if(o.hasOwnProperty(b)){if(b==t)for(var f in i)i.hasOwnProperty(f)&&(p[f]=i[f]);i.hasOwnProperty(b)||(p[b]=o[b])}var v=s[n];return s[n]=p,r.languages.DFS(r.languages,function(m,k){k===v&&m!=n&&(this[m]=p)}),p},DFS:function n(t,i,s,o){o=o||{};var p=r.util.objId;for(var b in t)if(t.hasOwnProperty(b)){i.call(t,b,t[b],s||b);var f=t[b],v=r.util.type(f);v==="Object"&&!o[p(f)]?(o[p(f)]=!0,n(f,i,null,o)):v==="Array"&&!o[p(f)]&&(o[p(f)]=!0,n(f,i,b,o))}}},plugins:{},highlightAll:function(n,t){r.highlightAllUnder(document,n,t)},highlightAllUnder:function(n,t,i){var s={callback:i,container:n,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};r.hooks.run("before-highlightall",s),s.elements=Array.prototype.slice.apply(s.container.querySelectorAll(s.selector)),r.hooks.run("before-all-elements-highlight",s);for(var o=0,p;p=s.elements[o++];)r.highlightElement(p,t===!0,s.callback)},highlightElement:function(n,t,i){var s=r.util.getLanguage(n),o=r.languages[s];r.util.setLanguage(n,s);var p=n.parentElement;p&&p.nodeName.toLowerCase()==="pre"&&r.util.setLanguage(p,s);var b=n.textContent,f={element:n,language:s,grammar:o,code:b};function v(k){f.highlightedCode=k,r.hooks.run("before-insert",f),f.element.innerHTML=f.highlightedCode,r.hooks.run("after-highlight",f),r.hooks.run("complete",f),i&&i.call(f.element)}if(r.hooks.run("before-sanity-check",f),p=f.element.parentElement,p&&p.nodeName.toLowerCase()==="pre"&&!p.hasAttribute("tabindex")&&p.setAttribute("tabindex","0"),!f.code){r.hooks.run("complete",f),i&&i.call(f.element);return}if(r.hooks.run("before-highlight",f),!f.grammar){v(r.util.encode(f.code));return}if(t&&u.Worker){var m=new Worker(r.filename);m.onmessage=function(k){v(k.data)},m.postMessage(JSON.stringify({language:f.language,code:f.code,immediateClose:!0}))}else v(r.highlight(f.code,f.grammar,f.language))},highlight:function(n,t,i){var s={code:n,grammar:t,language:i};if(r.hooks.run("before-tokenize",s),!s.grammar)throw new Error('The language "'+s.language+'" has no grammar.');return s.tokens=r.tokenize(s.code,s.grammar),r.hooks.run("after-tokenize",s),d.stringify(r.util.encode(s.tokens),s.language)},tokenize:function(n,t){var i=t.rest;if(i){for(var s in i)t[s]=i[s];delete t.rest}var o=new S;return _(o,o.head,n),x(n,o,t,o.head,0),A(o)},hooks:{all:{},add:function(n,t){var i=r.hooks.all;i[n]=i[n]||[],i[n].push(t)},run:function(n,t){var i=r.hooks.all[n];if(!(!i||!i.length))for(var s=0,o;o=i[s++];)o(t)}},Token:d};u.Prism=r;function d(n,t,i,s){this.type=n,this.content=t,this.alias=i,this.length=(s||"").length|0}d.stringify=function n(t,i){if(typeof t=="string")return t;if(Array.isArray(t)){var s="";return t.forEach(function(v){s+=n(v,i)}),s}var o={type:t.type,content:n(t.content,i),tag:"span",classes:["token",t.type],attributes:{},language:i},p=t.alias;p&&(Array.isArray(p)?Array.prototype.push.apply(o.classes,p):o.classes.push(p)),r.hooks.run("wrap",o);var b="";for(var f in o.attributes)b+=" "+f+'="'+(o.attributes[f]||"").replace(/"/g,""")+'"';return"<"+o.tag+' class="'+o.classes.join(" ")+'"'+b+">"+o.content+""};function g(n,t,i,s){n.lastIndex=t;var o=n.exec(i);if(o&&s&&o[1]){var p=o[1].length;o.index+=p,o[0]=o[0].slice(p)}return o}function x(n,t,i,s,o,p){for(var b in i)if(!(!i.hasOwnProperty(b)||!i[b])){var f=i[b];f=Array.isArray(f)?f:[f];for(var v=0;v=p.reach);C+=w.value.length,w=w.next){var D=w.value;if(t.length>n.length)return;if(!(D instanceof d)){var O=1,E;if(ee){if(E=g(te,C,n,K),!E||E.index>=n.length)break;var j=E.index,we=E.index+E[0].length,N=C;for(N+=w.value.length;j>=N;)w=w.next,N+=w.value.length;if(N-=w.value.length,C=N,w.value instanceof d)continue;for(var I=w;I!==t.tail&&(Np.reach&&(p.reach=Z);var R=w.prev;M&&(R=_(t,R,M),C+=M.length),F(t,R,O);var ke=new d(b,k?r.tokenize(L,k):L,me,L);if(w=_(t,R,ke),ne&&_(t,w,ne),O>1){var H={cause:b+","+v,reach:Z};x(n,t,i,w.prev,C,H),p&&H.reach>p.reach&&(p.reach=H.reach)}}}}}}function S(){var n={value:null,prev:null,next:null},t={value:null,prev:n,next:null};n.next=t,this.head=n,this.tail=t,this.length=0}function _(n,t,i){var s=t.next,o={value:i,prev:t,next:s};return t.next=o,s.prev=o,n.length++,o}function F(n,t,i){for(var s=t.next,o=0;o=y.length);d++){var g=r[d];if(typeof g=="string"||g.content&&typeof g.content=="string"){var x=y[h],S=a.tokenStack[x],_=typeof g=="string"?g:g.content,F=e(u,x),A=_.indexOf(F);if(A>-1){++h;var $=_.substring(0,A),z=new l.Token(u,l.tokenize(S,a.grammar),"language-"+u,S),P=_.substring(A+F.length),n=[];$&&n.push.apply(n,c([$])),n.push(z),P&&n.push.apply(n,c([P])),typeof g=="string"?r.splice.apply(r,[d,1].concat(n)):g.content=n}}else g.content&&c(g.content)}return r}c(a.tokens)}}})})(Prism),Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},function(l){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;l.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:"+/[^;{\s"']|\s+(?!\s)/.source+"|"+e.source+")*?"+/(?:;|(?=\s*\{))/.source),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp(`(^|[{}\\s])[^{}\\s](?:[^{};"'\\s]|\\s+(?![\\s{])|`+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},l.languages.css.atrule.inside.rest=l.languages.css;var a=l.languages.markup;a&&(a.tag.addInlined("style","css"),a.tag.addAttribute("style","css"))}(Prism),Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+(/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source)+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp(/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source+/\//.source+"(?:"+/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}/.source+"|"+/(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/.source+")"+/(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),Prism.languages.js=Prism.languages.javascript,Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json,Prism.languages.less=Prism.languages.extend("css",{comment:[/\/\*[\s\S]*?\*\//,{pattern:/(^|[^\\])\/\/.*/,lookbehind:!0}],atrule:{pattern:/@[\w-](?:\((?:[^(){}]|\([^(){}]*\))*\)|[^(){};\s]|\s+(?!\s))*?(?=\s*\{)/,inside:{punctuation:/[:()]/}},selector:{pattern:/(?:@\{[\w-]+\}|[^{};\s@])(?:@\{[\w-]+\}|\((?:[^(){}]|\([^(){}]*\))*\)|[^(){};@\s]|\s+(?!\s))*?(?=\s*\{)/,inside:{variable:/@+[\w-]+/}},property:/(?:@\{[\w-]+\}|[\w-])+(?:\+_?)?(?=\s*:)/,operator:/[+\-*\/]/}),Prism.languages.insertBefore("less","property",{variable:[{pattern:/@[\w-]+\s*:/,inside:{punctuation:/:/}},/@@?[\w-]+/],"mixin-usage":{pattern:/([{;]\s*)[.#](?!\d)[\w-].*?(?=[(;])/,lookbehind:!0,alias:"function"}}),function(l){var e=/\/\*[\s\S]*?\*\/|\/\/.*|#(?!\[).*/,a=[{pattern:/\b(?:false|true)\b/i,alias:"boolean"},{pattern:/(::\s*)\b[a-z_]\w*\b(?!\s*\()/i,greedy:!0,lookbehind:!0},{pattern:/(\b(?:case|const)\s+)\b[a-z_]\w*(?=\s*[;=])/i,greedy:!0,lookbehind:!0},/\b(?:null)\b/i,/\b[A-Z_][A-Z0-9_]*\b(?!\s*\()/],u=/\b0b[01]+(?:_[01]+)*\b|\b0o[0-7]+(?:_[0-7]+)*\b|\b0x[\da-f]+(?:_[\da-f]+)*\b|(?:\b\d+(?:_\d+)*\.?(?:\d+(?:_\d+)*)?|\B\.\d+)(?:e[+-]?\d+)?/i,h=/|\?\?=?|\.{3}|\??->|[!=]=?=?|::|\*\*=?|--|\+\+|&&|\|\||<<|>>|[?~]|[/^|%*&<>.+-]=?/,y=/[{}\[\](),:;]/;l.languages.php={delimiter:{pattern:/\?>$|^<\?(?:php(?=\s)|=)?/i,alias:"important"},comment:e,variable:/\$+(?:\w+\b|(?=\{))/,package:{pattern:/(namespace\s+|use\s+(?:function\s+)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,lookbehind:!0,inside:{punctuation:/\\/}},"class-name-definition":{pattern:/(\b(?:class|enum|interface|trait)\s+)\b[a-z_]\w*(?!\\)\b/i,lookbehind:!0,alias:"class-name"},"function-definition":{pattern:/(\bfunction\s+)[a-z_]\w*(?=\s*\()/i,lookbehind:!0,alias:"function"},keyword:[{pattern:/(\(\s*)\b(?:array|bool|boolean|float|int|integer|object|string)\b(?=\s*\))/i,alias:"type-casting",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|object|self|static|string)\b(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b(?:array(?!\s*\()|bool|callable|(?:false|null)(?=\s*\|)|float|int|iterable|mixed|never|object|self|static|string|void)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/\b(?:array(?!\s*\()|bool|float|int|iterable|mixed|object|string|void)\b/i,alias:"type-declaration",greedy:!0},{pattern:/(\|\s*)(?:false|null)\b|\b(?:false|null)(?=\s*\|)/i,alias:"type-declaration",greedy:!0,lookbehind:!0},{pattern:/\b(?:parent|self|static)(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(\byield\s+)from\b/i,lookbehind:!0},/\bclass\b/i,{pattern:/((?:^|[^\s>:]|(?:^|[^-])>|(?:^|[^:]):)\s*)\b(?:abstract|and|array|as|break|callable|case|catch|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|enum|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|match|namespace|never|new|or|parent|print|private|protected|public|readonly|require|require_once|return|self|static|switch|throw|trait|try|unset|use|var|while|xor|yield|__halt_compiler)\b/i,lookbehind:!0}],"argument-name":{pattern:/([(,]\s*)\b[a-z_]\w*(?=\s*:(?!:))/i,lookbehind:!0},"class-name":[{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self|\s+static))\s+|\bcatch\s*\()\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/(\|\s*)\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/\b[a-z_]\w*(?!\\)\b(?=\s*\|)/i,greedy:!0},{pattern:/(\|\s*)(?:\\?\b[a-z_]\w*)+\b/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(?:\\?\b[a-z_]\w*)+\b(?=\s*\|)/i,alias:"class-name-fully-qualified",greedy:!0,inside:{punctuation:/\\/}},{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self\b|\s+static\b))\s+|\bcatch\s*\()(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*\$)/i,alias:"type-declaration",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-declaration"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*::)/i,alias:["class-name-fully-qualified","static-context"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/([(,?]\s*)[a-z_]\w*(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-hint"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b[a-z_]\w*(?!\\)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:["class-name-fully-qualified","return-type"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:a,function:{pattern:/(^|[^\\\w])\\?[a-z_](?:[\w\\]*\w)?(?=\s*\()/i,lookbehind:!0,inside:{punctuation:/\\/}},property:{pattern:/(->\s*)\w+/,lookbehind:!0},number:u,operator:h,punctuation:y};var c={pattern:/\{\$(?:\{(?:\{[^{}]+\}|[^{}]+)\}|[^{}])+\}|(^|[^\\{])\$+(?:\w+(?:\[[^\r\n\[\]]+\]|->\w+)?)/,lookbehind:!0,inside:l.languages.php},r=[{pattern:/<<<'([^']+)'[\r\n](?:.*[\r\n])*?\1;/,alias:"nowdoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<'[^']+'|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<'?|[';]$/}}}},{pattern:/<<<(?:"([^"]+)"[\r\n](?:.*[\r\n])*?\1;|([a-z_]\w*)[\r\n](?:.*[\r\n])*?\2;)/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<(?:"[^"]+"|[a-z_]\w*)|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<"?|[";]$/}},interpolation:c}},{pattern:/`(?:\\[\s\S]|[^\\`])*`/,alias:"backtick-quoted-string",greedy:!0},{pattern:/'(?:\\[\s\S]|[^\\'])*'/,alias:"single-quoted-string",greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,alias:"double-quoted-string",greedy:!0,inside:{interpolation:c}}];l.languages.insertBefore("php","variable",{string:r,attribute:{pattern:/#\[(?:[^"'\/#]|\/(?![*/])|\/\/.*$|#(?!\[).*$|\/\*(?:[^*]|\*(?!\/))*\*\/|"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*')+\](?=\s*[a-z$#])/im,greedy:!0,inside:{"attribute-content":{pattern:/^(#\[)[\s\S]+(?=\]$)/,lookbehind:!0,inside:{comment:e,string:r,"attribute-class-name":[{pattern:/([^:]|^)\b[a-z_]\w*(?!\\)\b/i,alias:"class-name",greedy:!0,lookbehind:!0},{pattern:/([^:]|^)(?:\\?\b[a-z_]\w*)+/i,alias:["class-name","class-name-fully-qualified"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:a,number:u,operator:h,punctuation:y}},delimiter:{pattern:/^#\[|\]$/,alias:"punctuation"}}}}),l.hooks.add("before-tokenize",function(d){if(/<\?/.test(d.code)){var g=/<\?(?:[^"'/#]|\/(?![*/])|("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|(?:\/\/|#(?!\[))(?:[^?\n\r]|\?(?!>))*(?=$|\?>|[\r\n])|#\[|\/\*(?:[^*]|\*(?!\/))*(?:\*\/|$))*?(?:\?>|$)/g;l.languages["markup-templating"].buildPlaceholders(d,"php",g)}}),l.hooks.add("after-tokenize",function(d){l.languages["markup-templating"].tokenizePlaceholders(d,"php")})}(Prism),Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python,function(l){l.languages.ruby=l.languages.extend("clike",{comment:{pattern:/#.*|^=begin\s[\s\S]*?^=end/m,greedy:!0},"class-name":{pattern:/(\b(?:class|module)\s+|\bcatch\s+\()[\w.\\]+|\b[A-Z_]\w*(?=\s*\.\s*new\b)/,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:BEGIN|END|alias|and|begin|break|case|class|def|define_method|defined|do|each|else|elsif|end|ensure|extend|for|if|in|include|module|new|next|nil|not|or|prepend|private|protected|public|raise|redo|require|rescue|retry|return|self|super|then|throw|undef|unless|until|when|while|yield)\b/,operator:/\.{2,3}|&\.|===||[!=]?~|(?:&&|\|\||<<|>>|\*\*|[+\-*/%<>!^&|=])=?|[?:]/,punctuation:/[(){}[\].,;]/}),l.languages.insertBefore("ruby","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}});var e={pattern:/((?:^|[^\\])(?:\\{2})*)#\{(?:[^{}]|\{[^{}]*\})*\}/,lookbehind:!0,inside:{content:{pattern:/^(#\{)[\s\S]+(?=\}$)/,lookbehind:!0,inside:l.languages.ruby},delimiter:{pattern:/^#\{|\}$/,alias:"punctuation"}}};delete l.languages.ruby.function;var a="(?:"+[/([^a-zA-Z0-9\s{(\[<=])(?:(?!\1)[^\\]|\\[\s\S])*\1/.source,/\((?:[^()\\]|\\[\s\S]|\((?:[^()\\]|\\[\s\S])*\))*\)/.source,/\{(?:[^{}\\]|\\[\s\S]|\{(?:[^{}\\]|\\[\s\S])*\})*\}/.source,/\[(?:[^\[\]\\]|\\[\s\S]|\[(?:[^\[\]\\]|\\[\s\S])*\])*\]/.source,/<(?:[^<>\\]|\\[\s\S]|<(?:[^<>\\]|\\[\s\S])*>)*>/.source].join("|")+")",u=/(?:"(?:\\.|[^"\\\r\n])*"|(?:\b[a-zA-Z_]\w*|[^\s\0-\x7F]+)[?!]?|\$.)/.source;l.languages.insertBefore("ruby","keyword",{"regex-literal":[{pattern:RegExp(/%r/.source+a+/[egimnosux]{0,6}/.source),greedy:!0,inside:{interpolation:e,regex:/[\s\S]+/}},{pattern:/(^|[^/])\/(?!\/)(?:\[[^\r\n\]]+\]|\\.|[^[/\\\r\n])+\/[egimnosux]{0,6}(?=\s*(?:$|[\r\n,.;})#]))/,lookbehind:!0,greedy:!0,inside:{interpolation:e,regex:/[\s\S]+/}}],variable:/[@$]+[a-zA-Z_]\w*(?:[?!]|\b)/,symbol:[{pattern:RegExp(/(^|[^:]):/.source+u),lookbehind:!0,greedy:!0},{pattern:RegExp(/([\r\n{(,][ \t]*)/.source+u+/(?=:(?!:))/.source),lookbehind:!0,greedy:!0}],"method-definition":{pattern:/(\bdef\s+)\w+(?:\s*\.\s*\w+)?/,lookbehind:!0,inside:{function:/\b\w+$/,keyword:/^self\b/,"class-name":/^\w+/,punctuation:/\./}}}),l.languages.insertBefore("ruby","string",{"string-literal":[{pattern:RegExp(/%[qQiIwWs]?/.source+a),greedy:!0,inside:{interpolation:e,string:/[\s\S]+/}},{pattern:/("|')(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|(?!\1)[^\\#\r\n])*\1/,greedy:!0,inside:{interpolation:e,string:/[\s\S]+/}},{pattern:/<<[-~]?([a-z_]\w*)[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?[a-z_]\w*|\b[a-z_]\w*$/i,inside:{symbol:/\b\w+/,punctuation:/^<<[-~]?/}},interpolation:e,string:/[\s\S]+/}},{pattern:/<<[-~]?'([a-z_]\w*)'[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?'[a-z_]\w*'|\b[a-z_]\w*$/i,inside:{symbol:/\b\w+/,punctuation:/^<<[-~]?'|'$/}},string:/[\s\S]+/}}],"command-literal":[{pattern:RegExp(/%x/.source+a),greedy:!0,inside:{interpolation:e,command:{pattern:/[\s\S]+/,alias:"string"}}},{pattern:/`(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|[^\\`#\r\n])*`/,greedy:!0,inside:{interpolation:e,command:{pattern:/[\s\S]+/,alias:"string"}}}]}),delete l.languages.ruby.string,l.languages.insertBefore("ruby","number",{builtin:/\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Fixnum|Float|Hash|IO|Integer|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|Stat|String|Struct|Symbol|TMS|Thread|ThreadGroup|Time|TrueClass)\b/,constant:/\b[A-Z][A-Z0-9_]*(?:[?!]|\b)/}),l.languages.rb=l.languages.ruby}(Prism),Prism.languages.scss=Prism.languages.extend("css",{comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0},atrule:{pattern:/@[\w-](?:\([^()]+\)|[^()\s]|\s+(?!\s))*?(?=\s+[{;])/,inside:{rule:/@[\w-]+/}},url:/(?:[-a-z]+-)?url(?=\()/i,selector:{pattern:/(?=\S)[^@;{}()]?(?:[^@;{}()\s]|\s+(?!\s)|#\{\$[-\w]+\})+(?=\s*\{(?:\}|\s|[^}][^:{}]*[:{][^}]))/,inside:{parent:{pattern:/&/,alias:"important"},placeholder:/%[-\w]+/,variable:/\$[-\w]+|#\{\$[-\w]+\}/}},property:{pattern:/(?:[-\w]|\$[-\w]|#\{\$[-\w]+\})+(?=\s*:)/,inside:{variable:/\$[-\w]+|#\{\$[-\w]+\}/}}}),Prism.languages.insertBefore("scss","atrule",{keyword:[/@(?:content|debug|each|else(?: if)?|extend|for|forward|function|if|import|include|mixin|return|use|warn|while)\b/i,{pattern:/( )(?:from|through)(?= )/,lookbehind:!0}]}),Prism.languages.insertBefore("scss","important",{variable:/\$[-\w]+|#\{\$[-\w]+\}/}),Prism.languages.insertBefore("scss","function",{"module-modifier":{pattern:/\b(?:as|hide|show|with)\b/i,alias:"keyword"},placeholder:{pattern:/%[-\w]+/,alias:"selector"},statement:{pattern:/\B!(?:default|optional)\b/i,alias:"keyword"},boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"},operator:{pattern:/(\s)(?:[-+*\/%]|[=!]=|<=?|>=?|and|not|or)(?=\s)/,lookbehind:!0}}),Prism.languages.scss.atrule.inside.rest=Prism.languages.scss,function(l){var e=/[*&][^\s[\]{},]+/,a=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,u="(?:"+a.source+"(?:[ ]+"+e.source+")?|"+e.source+"(?:[ ]+"+a.source+")?)",h=/(?:[^\s\x00-\x08\x0e-\x1f!"#%&'*,\-:>?@[\]`{|}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]|[?:-])(?:[ \t]*(?:(?![#:])|:))*/.source.replace(//g,function(){return/[^\s\x00-\x08\x0e-\x1f,[\]{}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]/.source}),y=/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*'/.source;function c(r,d){d=(d||"").replace(/m/g,"")+"m";var g=/([:\-,[{]\s*(?:\s<>[ \t]+)?)(?:<>)(?=[ \t]*(?:$|,|\]|\}|(?:[\r\n]\s*)?#))/.source.replace(/<>/g,function(){return u}).replace(/<>/g,function(){return r});return RegExp(g,d)}l.languages.yaml={scalar:{pattern:RegExp(/([\-:]\s*(?:\s<>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\S[^\r\n]*(?:\2[^\r\n]+)*)/.source.replace(/<>/g,function(){return u})),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp(/((?:^|[:\-,[{\r\n?])[ \t]*(?:<>[ \t]+)?)<>(?=\s*:\s)/.source.replace(/<>/g,function(){return u}).replace(/<>/g,function(){return"(?:"+h+"|"+y+")"})),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:c(/\d{4}-\d\d?-\d\d?(?:[tT]|[ \t]+)\d\d?:\d{2}:\d{2}(?:\.\d*)?(?:[ \t]*(?:Z|[-+]\d\d?(?::\d{2})?))?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(?::\d{2}(?:\.\d*)?)?/.source),lookbehind:!0,alias:"number"},boolean:{pattern:c(/false|true/.source,"i"),lookbehind:!0,alias:"important"},null:{pattern:c(/null|~/.source,"i"),lookbehind:!0,alias:"important"},string:{pattern:c(y),lookbehind:!0,greedy:!0},number:{pattern:c(/[+-]?(?:0x[\da-f]+|0o[0-7]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|\.inf|\.nan)/.source,"i"),lookbehind:!0},tag:a,important:e,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},l.languages.yml=l.languages.yaml}(Prism);function pe(l,e,a,u,h,y,c,r){var d=typeof l=="function"?l.options:l;return e&&(d.render=e,d.staticRenderFns=a,d._compiled=!0),{exports:l,options:d}}const ge={components:{PrismEditor:ce},extends:"k-textarea-field",props:{size:String,language:String,lineNumbers:Boolean,tabSize:Number,insertSpaces:Boolean,ignoreTabKey:Boolean},data(){return{code:""}},mounted(){this.code=this.value},methods:{highlighter(){return V.highlight(this.code,V.languages[this.language])},onCodeInput(){this.$emit("input",this.code)}}};var fe=function(){var e=this,a=e._self._c;return a("k-field",e._b({staticClass:"k-code-editor-field",attrs:{input:e.uid}},"k-field",e.$props,!1),[a("prism-editor",{staticClass:"k-code-editor-input",attrs:{highlight:e.highlighter,"line-numbers":e.lineNumbers,"tab-size":e.tabSize,"insert-spaces":e.insertSpaces,"ignore-tab-key":e.ignoreTabKey,"data-size":e.size},on:{input:e.onCodeInput},model:{value:e.code,callback:function(u){e.code=u},expression:"code"}})],1)},he=[],be=pe(ge,fe,he);const ye=be.exports;window.panel.plugin("sylvainjule/code-editor",{fields:{"code-editor":ye}})})(); diff --git a/public/site/plugins/code-editor/index.php b/public/site/plugins/code-editor/index.php new file mode 100644 index 0000000..f4a3250 --- /dev/null +++ b/public/site/plugins/code-editor/index.php @@ -0,0 +1,15 @@ + array( + 'language' => 'css', + 'size' => 'small', + 'lineNumbers' => true, + 'tabSize' => 4, + 'insertSpaces' => true, + 'ignoreTabKey' => false, + ), + 'fields' => array( + 'code-editor' => require_once __DIR__ . '/lib/fields/code-editor.php', + ), +]); diff --git a/public/site/plugins/code-editor/lib/fields/code-editor.php b/public/site/plugins/code-editor/lib/fields/code-editor.php new file mode 100644 index 0000000..343b545 --- /dev/null +++ b/public/site/plugins/code-editor/lib/fields/code-editor.php @@ -0,0 +1,32 @@ +root('kirby') . '/config/fields/textarea.php'; + +/* Merge new properties +--------------------------------*/ + +$options = A::merge($options, [ + 'props' => [ + 'size' => function($size = null) { + return $size ?? option('sylvainjule.code-editor.size'); + }, + 'language' => function($language = null) { + return $language ?? option('sylvainjule.code-editor.language'); + }, + 'lineNumbers' => function($lineNumbers = null) { + return $lineNumbers ?? option('sylvainjule.code-editor.lineNumbers'); + }, + 'tabSize' => function($tabSize = null) { + return $tabSize ?? option('sylvainjule.code-editor.tabSize'); + }, + 'insertSpaces' => function($insertSpaces = null) { + return $tabSize ?? option('sylvainjule.code-editor.insertSpaces'); + }, + 'ignoreTabKey' => function($ignoreTabKey = null) { + return $tabSize ?? option('sylvainjule.code-editor.ignoreTabKey'); + }, + ] +]); + +// return the updated options +return $options; diff --git a/public/site/plugins/code-editor/package.json b/public/site/plugins/code-editor/package.json new file mode 100644 index 0000000..66753b0 --- /dev/null +++ b/public/site/plugins/code-editor/package.json @@ -0,0 +1,30 @@ +{ + "name": "kirby-code-editor", + "version": "1.0.3", + "description": "Code editor field for Kirby 3 and 4", + "main": "index.js", + "author": "Kirby Community", + "license": "MIT", + "repository": { + "type": "git", + "url": "git@github.com:sylvainjule/kirby-code-editor.git" + }, + "scripts": { + "dev": "kirbyup src/index.js --watch", + "build": "kirbyup src/index.js", + "lint": "eslint \"src/**/*.{js,vue}\"", + "lint:fix": "npm run lint -- --fix", + "format": "prettier --write \"src/**/*.{css,js,vue}\"", + "prepare": "node src/node/patchVuePrismEditor.mjs" + }, + "devDependencies": { + "consola": "^3.4.2", + "eslint": "^9.27.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-vue": "^9.33.0", + "kirbyup": "^3.3.0", + "prettier": "^3.5.3", + "prismjs": "^1.30.0", + "vue-prism-editor": "^1.3.0" + } +} diff --git a/public/site/plugins/code-editor/src/components/field/CodeEditor.vue b/public/site/plugins/code-editor/src/components/field/CodeEditor.vue new file mode 100644 index 0000000..3679339 --- /dev/null +++ b/public/site/plugins/code-editor/src/components/field/CodeEditor.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/public/site/plugins/code-editor/src/index.js b/public/site/plugins/code-editor/src/index.js new file mode 100644 index 0000000..89e4600 --- /dev/null +++ b/public/site/plugins/code-editor/src/index.js @@ -0,0 +1,7 @@ +import CodeEditor from "./components/field/CodeEditor.vue"; + +window.panel.plugin("sylvainjule/code-editor", { + fields: { + "code-editor": CodeEditor, + }, +}); diff --git a/public/site/plugins/code-editor/src/node/patchVuePrismEditor.mjs b/public/site/plugins/code-editor/src/node/patchVuePrismEditor.mjs new file mode 100644 index 0000000..3b80640 --- /dev/null +++ b/public/site/plugins/code-editor/src/node/patchVuePrismEditor.mjs @@ -0,0 +1,38 @@ +import { existsSync, readFileSync, writeFileSync } from "fs"; +import chalk from "chalk"; +import consola from "consola"; + +const srcPath = "node_modules/vue-prism-editor/dist/prismeditor.esm.js"; + +async function main() { + consola.start("Vue Prism Editor patcher"); + + if (!existsSync(srcPath)) { + consola.error( + `couldn't find ${chalk.cyan(srcPath)}, did you run ${chalk.green( + "npm install" + )}?` + ); + return; + } + + const source = readFileSync(srcPath, "utf8"); + + if (!source.includes("Vue.extend")) { + consola.success("already patched"); + return; + } + + consola.info("patching the source component..."); + + let output = source + .replace(/^import Vue from 'vue';/, "") + .replace("/*#__PURE__*/Vue.extend(", "") + .replace(/\}\)(;\s+export)/, "}$1"); + + writeFileSync(srcPath, output, "utf8"); + + consola.success("successfully patched"); +} + +main().catch((err) => consola.error(err)); diff --git a/public/site/plugins/web2print/index.php b/public/site/plugins/web2print/index.php new file mode 100644 index 0000000..a29dc63 --- /dev/null +++ b/public/site/plugins/web2print/index.php @@ -0,0 +1,116 @@ + [ + // POST: Save custom CSS + [ + 'pattern' => 'narratives/(:all)/css', + 'method' => 'POST', + 'action' => function ($pagePath) { + // Check authentication + if (!kirby()->user()) { + return Response::json([ + 'status' => 'error', + 'message' => 'Authentication required' + ], 401); + } + + // Verify CSRF token from header + $csrfToken = kirby()->request()->header('X-CSRF'); + if (!csrf($csrfToken)) { + return Response::json([ + 'status' => 'error', + 'message' => 'Invalid CSRF token' + ], 403); + } + + // Get page + $page = page($pagePath); + + if (!$page || $page->intendedTemplate()->name() !== 'narrative') { + return Response::json([ + 'status' => 'error', + 'message' => 'Narrative not found' + ], 404); + } + + // Get POST data + $data = kirby()->request()->data(); + $customCss = $data['customCss'] ?? null; + + if ($customCss === null) { + return Response::json([ + 'status' => 'error', + 'message' => 'No CSS content provided' + ], 400); + } + + try { + // Update page with new custom CSS + $page->update([ + 'customCss' => $customCss + ]); + + // Reload page to get updated modification time + $page = page($pagePath); + + // Return success with updated modified timestamp + return Response::json([ + 'status' => 'success', + 'data' => [ + 'modified' => $page->modified(), + 'modifiedFormatted' => $page->modified('d/m/Y H:i') + ] + ]); + } catch (Exception $e) { + return Response::json([ + 'status' => 'error', + 'message' => 'Failed to save CSS: ' . $e->getMessage() + ], 500); + } + } + ], + + // GET: Load custom CSS and last modified time + [ + 'pattern' => 'narratives/(:all)/css', + 'method' => 'GET', + 'action' => function ($pagePath) { + // Check authentication + if (!kirby()->user()) { + return Response::json([ + 'status' => 'error', + 'message' => 'Authentication required' + ], 401); + } + + // Get page + $page = page($pagePath); + + if (!$page || $page->intendedTemplate()->name() !== 'narrative') { + return Response::json([ + 'status' => 'error', + 'message' => 'Narrative not found' + ], 404); + } + + // Return custom CSS content and modified timestamp + return Response::json([ + 'status' => 'success', + 'data' => [ + 'customCss' => $page->customCss()->value() ?? '', + 'modified' => $page->modified(), + 'modifiedFormatted' => $page->modified('d/m/Y H:i') + ] + ]); + } + ] + ] +]); diff --git a/public/site/snippets/header.php b/public/site/snippets/header.php index 97dcaaa..e453a0b 100644 --- a/public/site/snippets/header.php +++ b/public/site/snippets/header.php @@ -14,6 +14,11 @@ + + user()): ?> + + + + + diff --git a/src/components/StylesheetViewer.vue b/src/components/StylesheetViewer.vue index 4cd72d6..c59dda0 100644 --- a/src/components/StylesheetViewer.vue +++ b/src/components/StylesheetViewer.vue @@ -1,30 +1,57 @@ @@ -79,18 +108,44 @@ watch(activeTab, (newTab) => { height: 100%; background: #282c34; color: #fff; + gap: 1rem; + overflow-y: auto; } -.header { +.css-section { + display: flex; + flex-direction: column; + background: #21252b; + border-radius: 0.25rem; + overflow: hidden; +} + +.custom-section { + flex: 1; + min-height: 300px; +} + +.section-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1rem; + padding: 0.75rem 1rem; + background: #2c313c; + cursor: pointer; + user-select: none; +} + +.css-section.custom-section .section-header { + cursor: default; } h3 { margin: 0; color: #fff; + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; } .toggle { @@ -144,11 +199,28 @@ h3 { transform: translateX(20px); } +.expand-icon { + width: 1.25rem; + height: 1.25rem; + color: #abb2bf; + transition: transform 0.2s ease; +} + +.expand-icon.expanded { + transform: rotate(180deg); +} + +.section-content { + display: flex; + flex-direction: column; + flex: 1; +} + .readonly { margin: 0; flex: 1; overflow-y: auto; - padding: 0.5rem; + padding: 1rem; background: #1e1e1e; font-family: 'Courier New', Courier, monospace; font-size: 0.875rem; @@ -162,14 +234,20 @@ h3 { textarea { width: 100%; flex: 1; + min-height: 300px; background: #1e1e1e; color: #abb2bf; border: none; - padding: 0.5rem; + padding: 1rem; font-family: 'Courier New', Courier, monospace; font-size: 0.875rem; line-height: 1.5; resize: none; outline: none; } + +textarea::placeholder { + color: #5c6370; + font-style: italic; +} diff --git a/src/components/editor/PageSettings.vue b/src/components/editor/PageSettings.vue index 138f6e1..3517ddb 100644 --- a/src/components/editor/PageSettings.vue +++ b/src/components/editor/PageSettings.vue @@ -354,10 +354,7 @@ const updateMargins = () => { `$1${marginValue}` ); - stylesheetStore.content = stylesheetStore.content.replace( - currentBlock, - updatedBlock - ); + stylesheetStore.replaceBlock(currentBlock, updatedBlock); }; // Watch margin values (number inputs) with debounce @@ -398,19 +395,13 @@ const updateBackground = () => { /(background:\s*)[^;]+/, `$1${background.value.value}` ); - stylesheetStore.content = stylesheetStore.content.replace( - currentBlock, - updatedBlock - ); + stylesheetStore.replaceBlock(currentBlock, updatedBlock); } else { const updatedBlock = currentBlock.replace( /(\s*})$/, ` background: ${background.value.value};\n$1` ); - stylesheetStore.content = stylesheetStore.content.replace( - currentBlock, - updatedBlock - ); + stylesheetStore.replaceBlock(currentBlock, updatedBlock); } }; @@ -457,7 +448,7 @@ watch(runningTitle, (enabled) => { }); const updatePageFooters = () => { - let currentCss = stylesheetStore.content; + let currentCss = stylesheetStore.customCss; // Remove existing @page:left and @page:right rules currentCss = currentCss.replace(/@page:left\s*\{[^}]*\}/g, ''); @@ -539,7 +530,7 @@ const updatePageFooters = () => { currentCss.slice(insertPosition); } - stylesheetStore.content = currentCss; + stylesheetStore.setCustomCss(currentCss); }; const syncFromStore = () => { diff --git a/src/stores/stylesheet.js b/src/stores/stylesheet.js index d2475e9..b394c3c 100644 --- a/src/stores/stylesheet.js +++ b/src/stores/stylesheet.js @@ -1,29 +1,48 @@ import { defineStore } from 'pinia'; -import { ref, watch } from 'vue'; +import { ref, computed, watch } from 'vue'; import cssParsingUtils from '../utils/css-parsing'; import prettier from 'prettier/standalone'; import parserPostcss from 'prettier/plugins/postcss'; +import { getCsrfToken } from '../utils/kirby-auth'; export const useStylesheetStore = defineStore('stylesheet', () => { - const content = ref(''); + // Base state + const baseCss = ref(''); + const customCss = ref(''); const isEditing = ref(false); let formatTimer = null; let isFormatting = false; + let isInitializing = false; - // Format CSS with Prettier - const formatContent = async () => { - if (isFormatting || !content.value) return; + // Save/load state + const isDirty = ref(false); + const isSaving = ref(false); + const lastSaved = ref(null); + const lastSavedFormatted = ref(''); + const saveError = ref(null); + const narrativeId = ref(null); + + // Computed: combined CSS for preview + const content = computed(() => { + if (!baseCss.value) return customCss.value; + if (!customCss.value) return baseCss.value; + return baseCss.value + '\n\n/* Custom CSS */\n' + customCss.value; + }); + + // Format custom CSS with Prettier + const formatCustomCss = async () => { + if (isFormatting || !customCss.value) return; try { isFormatting = true; - const formatted = await prettier.format(content.value, { + const formatted = await prettier.format(customCss.value, { parser: 'css', plugins: [parserPostcss], printWidth: 80, tabWidth: 2, useTabs: false, }); - content.value = formatted; + customCss.value = formatted; } catch (error) { console.error('CSS formatting error:', error); } finally { @@ -31,46 +50,195 @@ export const useStylesheetStore = defineStore('stylesheet', () => { } }; - // Watch content and format after 500ms of inactivity (only when not editing) - watch(content, () => { - if (isFormatting || isEditing.value) return; + // Watch customCss and format after 500ms of inactivity (only when not editing) + watch(customCss, () => { + if (isFormatting || isEditing.value || isInitializing) return; + + // Mark as dirty when customCss changes (unless we're saving) + if (!isSaving.value) { + isDirty.value = true; + } clearTimeout(formatTimer); formatTimer = setTimeout(() => { - formatContent(); + formatCustomCss(); }, 500); }); const loadStylesheet = async () => { - const response = await fetch('/assets/css/stylesheet.css'); - content.value = await response.text(); + const response = await fetch('/assets/css/stylesheet.print.css'); + baseCss.value = await response.text(); }; const updateProperty = (selector, property, value, unit) => { - content.value = cssParsingUtils.updateCssValue({ - css: content.value, + // Update custom CSS, not the combined content + customCss.value = cssParsingUtils.updateCssValue({ + css: customCss.value, selector, property, value, - unit + unit, }); }; const extractValue = (selector, property) => { - return cssParsingUtils.extractCssValue(content.value, selector, property); + // Try to extract from custom CSS first, then from base CSS + const customValue = cssParsingUtils.extractCssValue(customCss.value, selector, property); + if (customValue) return customValue; + return cssParsingUtils.extractCssValue(baseCss.value, selector, property); }; const extractBlock = (selector) => { - return cssParsingUtils.extractCssBlock(content.value, selector); + // Try to extract from custom CSS first, then from base CSS + const customBlock = cssParsingUtils.extractCssBlock(customCss.value, selector); + if (customBlock) return customBlock; + return cssParsingUtils.extractCssBlock(baseCss.value, selector); + }; + + // Replace a CSS block in custom CSS (handles blocks from base CSS too) + const replaceBlock = (oldBlock, newBlock) => { + // Check if the old block exists in custom CSS + if (customCss.value.includes(oldBlock)) { + // Replace in custom CSS + customCss.value = customCss.value.replace(oldBlock, newBlock); + } else { + // Block is from base CSS, append new block to custom CSS (will override via cascade) + customCss.value = customCss.value.trim() + '\n\n' + newBlock; + } + }; + + // Replace content in custom CSS (for more complex string replacements) + const replaceInCustomCss = (searchValue, replaceValue) => { + customCss.value = customCss.value.replace(searchValue, replaceValue); + }; + + // Set custom CSS directly (for complex transformations) + const setCustomCss = (newCss) => { + customCss.value = newCss; + }; + + // Load base CSS from stylesheet.print.css + const loadBaseCss = async () => { + const response = await fetch('/assets/css/stylesheet.print.css'); + baseCss.value = await response.text(); + return baseCss.value; + }; + + // Initialize from narrative data (base + custom CSS) + const initializeFromNarrative = async (narrativeData) => { + // Set initializing flag to prevent marking as dirty during init + isInitializing = true; + + try { + // Set narrative ID for API calls + narrativeId.value = narrativeData.id; + + // Load base CSS + await loadBaseCss(); + + // Get custom CSS if exists + customCss.value = narrativeData.customCss || ''; + + // Set last saved info + if (narrativeData.modified) { + lastSaved.value = narrativeData.modified; + lastSavedFormatted.value = narrativeData.modifiedFormatted || ''; + } + + // Mark as not dirty initially + isDirty.value = false; + } finally { + // Always clear initializing flag + isInitializing = false; + } + }; + + // Save custom CSS to Kirby + const saveCustomCss = async () => { + if (!narrativeId.value) { + saveError.value = 'No narrative ID available'; + return { status: 'error', message: saveError.value }; + } + + isSaving.value = true; + saveError.value = null; + + try { + // Get CSRF token + const csrfToken = getCsrfToken(); + if (!csrfToken) { + throw new Error( + 'No CSRF token available. Please log in to Kirby Panel.' + ); + } + + // Make POST request to save CSS (save customCss directly) + const response = await fetch(`/narratives/${narrativeId.value}/css`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF': csrfToken, + }, + body: JSON.stringify({ + customCss: customCss.value, + }), + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error( + result.message || `HTTP error! status: ${response.status}` + ); + } + + if (result.status === 'success') { + // Update last saved info + lastSaved.value = result.data.modified; + lastSavedFormatted.value = result.data.modifiedFormatted; + + // Mark as not dirty + isDirty.value = false; + + return { status: 'success' }; + } else { + throw new Error(result.message || 'Failed to save CSS'); + } + } catch (error) { + console.error('Error saving CSS:', error); + saveError.value = error.message; + return { status: 'error', message: error.message }; + } finally { + isSaving.value = false; + } }; return { - content, + // Core state + content, // computed: baseCss + customCss + baseCss, + customCss, isEditing, + + // Methods loadStylesheet, updateProperty, extractValue, extractBlock, - formatContent + replaceBlock, + replaceInCustomCss, + setCustomCss, + formatCustomCss, + loadBaseCss, + initializeFromNarrative, + + // Save/load + isDirty, + isSaving, + lastSaved, + lastSavedFormatted, + saveError, + narrativeId, + saveCustomCss, }; }); diff --git a/src/utils/kirby-auth.js b/src/utils/kirby-auth.js new file mode 100644 index 0000000..eb6e359 --- /dev/null +++ b/src/utils/kirby-auth.js @@ -0,0 +1,37 @@ +/** + * Kirby Authentication Utilities + * + * Helper functions for authentication and CSRF token management + */ + +/** + * Get CSRF token from meta tag + * @returns {string|null} CSRF token or null if not found + */ +export function getCsrfToken() { + // Check for meta tag (added by header.php when user is logged in) + const metaTag = document.querySelector('meta[name="csrf"]'); + if (metaTag) { + return metaTag.getAttribute('content'); + } + + // Alternatively, could be in a cookie + const cookies = document.cookie.split(';'); + for (let cookie of cookies) { + const [name, value] = cookie.trim().split('='); + if (name === 'kirby_csrf') { + return decodeURIComponent(value); + } + } + + return null; +} + +/** + * Check if user is authenticated (has Kirby session) + * @returns {boolean} True if authenticated, false otherwise + */ +export function isAuthenticated() { + // Check for kirby session cookie + return document.cookie.includes('kirby_session'); +}