✨ Rewrite for Kirby
This commit is contained in:
parent
07201d05de
commit
f854d60108
116 changed files with 4156 additions and 8875 deletions
0
site/accounts/index.html
Normal file
0
site/accounts/index.html
Normal file
37
site/blueprints/pages/article.yml
Normal file
37
site/blueprints/pages/article.yml
Normal file
|
@ -0,0 +1,37 @@
|
|||
title: Blog Article
|
||||
|
||||
num: '{{ page.date.toDate("YmdHi") }}'
|
||||
|
||||
create:
|
||||
fields:
|
||||
- date
|
||||
|
||||
columns:
|
||||
main:
|
||||
width: 2/3
|
||||
sections:
|
||||
fields:
|
||||
type: fields
|
||||
fields:
|
||||
text:
|
||||
type: textarea
|
||||
size: huge
|
||||
sidebar:
|
||||
width: 1/3
|
||||
sections:
|
||||
meta:
|
||||
type: fields
|
||||
fields:
|
||||
date:
|
||||
type: date
|
||||
label: Published on
|
||||
time: true
|
||||
required: true
|
||||
categories:
|
||||
type: tags
|
||||
labe: Categories
|
||||
tags:
|
||||
type: tags
|
||||
labe: Tags
|
||||
files:
|
||||
type: files
|
21
site/blueprints/pages/blog.yml
Normal file
21
site/blueprints/pages/blog.yml
Normal file
|
@ -0,0 +1,21 @@
|
|||
title: Blog Page
|
||||
|
||||
sections:
|
||||
fields:
|
||||
type: fields
|
||||
fields:
|
||||
text:
|
||||
type: textarea
|
||||
size: small
|
||||
drafts:
|
||||
extends: sections/articles
|
||||
label: Drafts
|
||||
status: draft
|
||||
unlisted:
|
||||
extends: sections/articles
|
||||
label: In Review
|
||||
status: unlisted
|
||||
listed:
|
||||
extends: sections/articles
|
||||
label: Published
|
||||
status: listed
|
21
site/blueprints/pages/default.yml
Normal file
21
site/blueprints/pages/default.yml
Normal file
|
@ -0,0 +1,21 @@
|
|||
title: Default Page
|
||||
|
||||
columns:
|
||||
main:
|
||||
width: 2/3
|
||||
sections:
|
||||
fields:
|
||||
type: fields
|
||||
fields:
|
||||
text:
|
||||
type: textarea
|
||||
size: huge
|
||||
sidebar:
|
||||
width: 1/3
|
||||
sections:
|
||||
pages:
|
||||
type: pages
|
||||
template: default
|
||||
files:
|
||||
type: files
|
||||
|
46
site/blueprints/pages/home.yml
Normal file
46
site/blueprints/pages/home.yml
Normal file
|
@ -0,0 +1,46 @@
|
|||
title: Home Page
|
||||
|
||||
columns:
|
||||
main:
|
||||
width: 2/3
|
||||
sections:
|
||||
fields:
|
||||
type: fields
|
||||
fields:
|
||||
subtitle:
|
||||
type: text
|
||||
text:
|
||||
type: textarea
|
||||
size: medium
|
||||
internal_menu:
|
||||
type: structure
|
||||
label: Internal Links
|
||||
fields:
|
||||
title:
|
||||
type: text
|
||||
label: Title
|
||||
link:
|
||||
type: link
|
||||
label: Link
|
||||
options:
|
||||
- page
|
||||
external_menu:
|
||||
type: structure
|
||||
label: External Links
|
||||
fields:
|
||||
title:
|
||||
type: text
|
||||
label: Title
|
||||
link:
|
||||
type: link
|
||||
label: Link
|
||||
options:
|
||||
- url
|
||||
sidebar:
|
||||
width: 1/3
|
||||
sections:
|
||||
pages:
|
||||
type: pages
|
||||
template: default
|
||||
files:
|
||||
type: files
|
29
site/blueprints/pages/quote.yml
Normal file
29
site/blueprints/pages/quote.yml
Normal file
|
@ -0,0 +1,29 @@
|
|||
title: Blog Article
|
||||
|
||||
num: '{{ page.date.toDate("YmdHi") }}'
|
||||
|
||||
create:
|
||||
fields:
|
||||
- date
|
||||
|
||||
columns:
|
||||
main:
|
||||
width: 2/3
|
||||
sections:
|
||||
fields:
|
||||
type: fields
|
||||
fields:
|
||||
text:
|
||||
type: textarea
|
||||
size: medium
|
||||
sidebar:
|
||||
width: 1/3
|
||||
sections:
|
||||
meta:
|
||||
type: fields
|
||||
fields:
|
||||
date:
|
||||
type: date
|
||||
label: Published on
|
||||
time: true
|
||||
required: true
|
21
site/blueprints/pages/quotes.yml
Normal file
21
site/blueprints/pages/quotes.yml
Normal file
|
@ -0,0 +1,21 @@
|
|||
title: Quotes Page
|
||||
|
||||
sections:
|
||||
fields:
|
||||
type: fields
|
||||
fields:
|
||||
text:
|
||||
type: textarea
|
||||
size: small
|
||||
quotes:
|
||||
type: pages
|
||||
search: true
|
||||
image: false
|
||||
template: quote
|
||||
sortBy: date desc
|
||||
empty: No quotes yet
|
||||
layout: table
|
||||
columns:
|
||||
date:
|
||||
label: Published on
|
||||
width: 1/6
|
17
site/blueprints/sections/articles.yml
Normal file
17
site/blueprints/sections/articles.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
type: pages
|
||||
search: true
|
||||
image: false
|
||||
template: article
|
||||
sortBy: date desc
|
||||
empty: No articles yet
|
||||
layout: table
|
||||
columns:
|
||||
date:
|
||||
label: Published on
|
||||
width: 1/6
|
||||
categories:
|
||||
label: Categories
|
||||
width: 1/6
|
||||
tags:
|
||||
label: Tags
|
||||
width: 1/6
|
56
site/blueprints/site.yml
Normal file
56
site/blueprints/site.yml
Normal file
|
@ -0,0 +1,56 @@
|
|||
title: Site
|
||||
|
||||
columns:
|
||||
main:
|
||||
width: 2/3
|
||||
sections:
|
||||
pages:
|
||||
type: pages
|
||||
fields:
|
||||
type: fields
|
||||
fields:
|
||||
menu:
|
||||
type: structure
|
||||
fields:
|
||||
title:
|
||||
type: text
|
||||
label: Title
|
||||
link:
|
||||
type: link
|
||||
label: Link
|
||||
options:
|
||||
- page
|
||||
- url
|
||||
sidebar:
|
||||
width: 1/3
|
||||
sections:
|
||||
articles_draft:
|
||||
extends: sections/articles
|
||||
label: Draft Articles
|
||||
status: draft
|
||||
parent: site.find("blog")
|
||||
layout: list
|
||||
limit: 5
|
||||
articles_review:
|
||||
extends: sections/articles
|
||||
label: In Review Articles
|
||||
status: unlisted
|
||||
parent: site.find("blog")
|
||||
layout: list
|
||||
limit: 5
|
||||
articles_published:
|
||||
extends: sections/articles
|
||||
label: Published Articles
|
||||
status: listed
|
||||
parent: site.find("blog")
|
||||
layout: list
|
||||
limit: 5
|
||||
metadata:
|
||||
type: fields
|
||||
fields:
|
||||
author:
|
||||
type: text
|
||||
description:
|
||||
type: text
|
||||
keywords:
|
||||
type: tags
|
0
site/cache/index.html
vendored
Normal file
0
site/cache/index.html
vendored
Normal file
37
site/config/config.php
Normal file
37
site/config/config.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'analytics' => [
|
||||
'goatcounter' => 'stats.mmk2410.org'
|
||||
],
|
||||
'routes' => [
|
||||
[
|
||||
'pattern' => 'feed',
|
||||
'action' => fn () => go('/blog.rss')
|
||||
],
|
||||
[
|
||||
'pattern' => 'index.xml',
|
||||
'action' => fn () => go('/blog.rss')
|
||||
],
|
||||
[
|
||||
'pattern' => '(:num)/(:num)/(:num)/(:any)',
|
||||
'action' => function ($year, $month, $day, $slug) {
|
||||
$page = page('blog/' . $slug);
|
||||
if (!$page) return site()->errorPage();
|
||||
return site()->visit($page);
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'blog/category/(:any)',
|
||||
'action' => function($category) {
|
||||
return page('blog')->render(['category' => $category]);
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'blog/tag/(:any)',
|
||||
'action' => function($tag) {
|
||||
return page('blog')->render(['tag' => $tag]);
|
||||
}
|
||||
]
|
||||
]
|
||||
];
|
17
site/controllers/blog.php
Normal file
17
site/controllers/blog.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
return function ($page, $tag, $category) {
|
||||
$articles = $page->children()->listed();
|
||||
|
||||
if ($tag) {
|
||||
$articles = $articles->filterBy('tags', $tag, ',');
|
||||
}
|
||||
|
||||
if ($category) {
|
||||
$articles = $articles->filterBy('categories', $category, ',');
|
||||
}
|
||||
|
||||
return [
|
||||
'articles' => $articles->flip()->paginate(20)
|
||||
];
|
||||
};
|
7
site/controllers/quotes.php
Normal file
7
site/controllers/quotes.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
return function ($page) {
|
||||
return [
|
||||
'quotes' => $page->children()->listed()->flip()->paginate(20)
|
||||
];
|
||||
};
|
30
site/models/article.php
Normal file
30
site/models/article.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
class ArticlePage extends Page
|
||||
{
|
||||
public function url($options = null): string
|
||||
{
|
||||
$date = $this->date()->toDate('Y/m/d');
|
||||
return '/' . $date .'/' . $this->slug();
|
||||
}
|
||||
|
||||
public function readingTime() {
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHtml(
|
||||
"<html><head><meta charset=\"UTF-8\"><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"></head><body>"
|
||||
. $this->text()->kirbytext()
|
||||
."</body></html>"
|
||||
);
|
||||
$pElems = $doc->getElementsByTagName('p');
|
||||
|
||||
$text = '';
|
||||
foreach ($pElems as $pElem) {
|
||||
$text .= $pElem->nodeValue . ' ';
|
||||
}
|
||||
|
||||
$wordCount = count(explode(' ', $text));
|
||||
$readingTime = (int)ceil($wordCount / 150);
|
||||
|
||||
return $wordCount . ' words, ~' . $readingTime . 'min reading time';
|
||||
}
|
||||
}
|
21
site/plugins/kirby-highlighter/LICENSE
Normal file
21
site/plugins/kirby-highlighter/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020-PRESENT Johann Schopplich
|
||||
|
||||
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.
|
135
site/plugins/kirby-highlighter/README.md
Normal file
135
site/plugins/kirby-highlighter/README.md
Normal file
|
@ -0,0 +1,135 @@
|
|||
# Kirby Highlighter
|
||||
|
||||
Server-side code highlighting available as [custom block](https://getkirby.com/docs/reference/panel/fields/blocks) and for [KirbyText](https://getkirby.com/docs/guide/content/text-formatting#kirbytext).
|
||||
|
||||
Built upon [highlight.php](http://www.highlightjs.org) which itself is a port of [highlight.js](http://www.highlightjs.org).
|
||||
|
||||
## Key Features
|
||||
|
||||
- 🏗 Works with Kirby's [`code` block](https://getkirby.com/docs/reference/panel/blocks/code)
|
||||
- 🏳️🌈 Supports 189 languages
|
||||
- 💫 94 styles available
|
||||
- ⛳️ Automatic language detection for KirbyText
|
||||
|
||||
## Requirements
|
||||
|
||||
- Kirby 3.8+
|
||||
|
||||
## Installation
|
||||
|
||||
### Composer
|
||||
|
||||
```
|
||||
composer require johannschopplich/kirby-highlighter
|
||||
```
|
||||
|
||||
### Download
|
||||
|
||||
Download and copy this repository to `/site/plugins/kirby-highlighter`.
|
||||
|
||||
## Usage
|
||||
|
||||
### With Kirby Blocks Field
|
||||
|
||||
This plugin overwrites Kirby's internal [`code` block](https://getkirby.com/docs/reference/panel/blocks/code). Thus, you won't have to change a thing.
|
||||
|
||||
Use the `code` block just like before, the output will be highlighted automatically:
|
||||
|
||||
```yaml
|
||||
fields:
|
||||
example:
|
||||
label: Paste code here
|
||||
type: blocks
|
||||
fieldsets:
|
||||
- code
|
||||
```
|
||||
|
||||
### Within KirbyText
|
||||
|
||||
Create a code block in your KirbyText field and optionally set the code language:
|
||||
|
||||
<pre lang="no-highlight"><code>```css
|
||||
.currentColor {
|
||||
color: currentColor;
|
||||
}
|
||||
```
|
||||
</code></pre>
|
||||
|
||||
Or use the new `code`-KirbyTag from this plugin with a base64 encoded code string:
|
||||
|
||||
```
|
||||
(code: LmN1cnJlbnRDb2xvciB7CiAgY29sb3I6IGN1cnJlbnRDb2xvcjsKfQ== lang: css)
|
||||
```
|
||||
|
||||
Which outputs:
|
||||
|
||||
```html
|
||||
<pre class="hljs"><code><span class="hljs-selector-class">.currentColor</span> {
|
||||
<span class="hljs-attribute">color</span>: currentColor;
|
||||
}</code></pre>
|
||||
```
|
||||
|
||||
The syntax highlighting functionality can be changed. You can choose between two highlighting modes:
|
||||
|
||||
1. Explicit mode (default)
|
||||
2. Automatic language detection mode (opt-in)
|
||||
|
||||
#### Explicit Mode
|
||||
|
||||
In explicit mode, you have to define which language the code block is. Otherwise highlighting will be skipped.
|
||||
|
||||
#### Automatic Language Detection
|
||||
|
||||
Alternatively you can use the automatic detection mode, which highlights your code with the language the library thinks is best. It is highly recommended you explicitly choose the language or limit the number of languages to automatically detect from. This reduces the number of inaccuracies and skips this extremely inefficient selection process.
|
||||
|
||||
To enable automatic language detection, set:
|
||||
|
||||
- `johannschopplich.highlighter.autodetect` to `true`
|
||||
- `johannschopplich.highlighter.languages` to an array of names from which languages should be chosen
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Default | Description |
|
||||
| --------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `johannschopplich.highlighter.class` | `hljs` | Style class for Highlight to be added to the `pre` element. |
|
||||
| `johannschopplich.highlighter.autodetect` | `false` | Indicates if the library should define which language thinks is best. Only applies when no language was set on the KirbyText code block. |
|
||||
| `johannschopplich.highlighter.languages` | `[]` | Array of language names to be auto-detected. If empty, every language will be auto-detectable. |
|
||||
| `johannschopplich.highlighter.line-numbering` | `false` | Indicates if the library should split up the highlighted code on each new line and wrap it in a `<span>` element. |
|
||||
| `johannschopplich.highlighter.line-numbering-class` | `hljs-code-line` | CSS class applied to highlighted code lines, respectively `<span>` elements. |
|
||||
|
||||
## Styling in the Frontend
|
||||
|
||||
Since this plugin handles highlighting code only and thus just wraps span's around code, you have to link styles in your frontend yourself. I recommend choosing one of the available themes directly from the highlight.js project: [highlight.js/src/styles/](https://github.com/highlightjs/highlight.js/tree/master/src/styles)
|
||||
|
||||
The CSS files over at the repository are maintained and new ones arrive from time to time, therefore it would be redundant to include a copy in this repository.
|
||||
|
||||
One of my favorite themes is [Night Owl by Sarah Drasner](https://github.com/highlightjs/highlight.js/blob/master/src/styles/night-owl.css).
|
||||
For example you could download the CSS file and save it in your Kirby project under `assets/css/hljs-night-owl.css`. Now you just have to include it in your template `<?= css('assets/css/hljs-night-owl.css') ?>`. Alternatively, use a CSS bundler of your choice.
|
||||
|
||||
### Line Numbering
|
||||
|
||||
If you choose to activate the line numbering option, you will need to include additional CSS style to display line numbering.
|
||||
|
||||
A basic example using [pseudo-elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements):
|
||||
|
||||
```css
|
||||
pre.hljs .hljs-code-line {
|
||||
counter-increment: line;
|
||||
}
|
||||
|
||||
pre.hljs .hljs-code-line::before {
|
||||
content: counter(line);
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
- Geert Bergman and contributors for the awesome [highlight.php](https://github.com/scrivo/highlight.php) port.
|
||||
- Martin Folkers for his [Kirby Highlight](https://github.com/S1SYPHOS/kirby3-highlight) plugin which built the base of this package.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE) License © 2020-PRESENT [Johann Schopplich](https://github.com/johannschopplich)
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace JohannSchopplich;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMNode;
|
||||
|
||||
class HTML5DOMDocument extends DOMDocument
|
||||
{
|
||||
/**
|
||||
* Name of temporary root element for the XML parser
|
||||
*/
|
||||
protected string $tempRoot = 'main';
|
||||
|
||||
/**
|
||||
* Create a new HTML5-compatible document parser
|
||||
*/
|
||||
public function __construct(string $version = '1.0', string $encoding = 'UTF-8')
|
||||
{
|
||||
// Silence libxml errors with HTML5 elements
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
// Call parent class
|
||||
parent::__construct($version, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load HTML from string, make UTF-8 compatible and add temporary root element
|
||||
*/
|
||||
public function loadHTML(string $source, int $options = LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD): bool
|
||||
{
|
||||
// `loadHTML` will treat the string as being in ISO-8859-1 unless
|
||||
// told otherwise, so translate anything above the ASCII range into
|
||||
// its html entity equivalent
|
||||
// @see https://stackoverflow.com/questions/39148170/utf-8-with-php-domdocument-loadhtml
|
||||
$convertedSource = htmlspecialchars_decode(htmlentities($source, ENT_COMPAT, 'UTF-8'), ENT_QUOTES);
|
||||
|
||||
// Add fake root element for XML parser because it assumes that the
|
||||
// first encountered tag is the root element
|
||||
// @see https://stackoverflow.com/questions/39479994/php-domdocument-savehtml-breaks-format
|
||||
return parent::loadHTML("<{$this->tempRoot}>" . $convertedSource . "</{$this->tempRoot}>", $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the temporarily added root element
|
||||
*/
|
||||
private function unwrapTempRoot(string $output): string
|
||||
{
|
||||
if ($this->firstChild->nodeName === $this->tempRoot) {
|
||||
return substr($output, strlen($this->tempRoot) + 2, -strlen($this->tempRoot) - 4);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the internal document into a HTML string
|
||||
*/
|
||||
#[\ReturnTypeWillChange]
|
||||
public function saveHTML(DOMNode|null $node = null, bool $entities = false): string|false
|
||||
{
|
||||
$html = parent::saveHTML($node);
|
||||
|
||||
if ($entities === false) {
|
||||
$html = html_entity_decode($html);
|
||||
}
|
||||
|
||||
if ($node === null) {
|
||||
$html = $this->unwrapTempRoot($html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
49
site/plugins/kirby-highlighter/composer.json
Normal file
49
site/plugins/kirby-highlighter/composer.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "johannschopplich/kirby-highlighter",
|
||||
"description": "Server-side syntax highlighting for Kirby CMS",
|
||||
"type": "kirby-plugin",
|
||||
"version": "3.1.0",
|
||||
"keywords": [
|
||||
"kirby",
|
||||
"highlight",
|
||||
"highlighter",
|
||||
"hljs"
|
||||
],
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/johannschopplich/kirby-highlighter#readme",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Johann Schopplich",
|
||||
"email": "pkg@johannschopplich.com",
|
||||
"homepage": "https://johannschopplich.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"getkirby/composer-installer": "^1.2",
|
||||
"scrivo/highlight.php": "^9.18"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "@stable",
|
||||
"getkirby/cms": "^4",
|
||||
"phpunit/phpunit": "^9.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"JohannSchopplich\\": "classes/JohannSchopplich/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"fix": "php-cs-fixer fix",
|
||||
"dist": "composer install --no-dev --optimize-autoloader",
|
||||
"test": "phpunit"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"allow-plugins": {
|
||||
"getkirby/composer-installer": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"kirby-cms-path": "tests/fixtures/kirby"
|
||||
}
|
||||
}
|
23
site/plugins/kirby-highlighter/extensions/fieldmethods.php
Normal file
23
site/plugins/kirby-highlighter/extensions/fieldmethods.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
if (!function_exists('is_base64_string_s')) {
|
||||
// https://stackoverflow.com/a/51877882
|
||||
function is_base64_string_s(string $str, $enc = ['UTF-8', 'ASCII'])
|
||||
{
|
||||
return !(($b = base64_decode($str, true)) === false) && in_array(mb_detect_encoding($b), $enc);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'fromBase64' => function ($field) {
|
||||
if ($field->isNotEmpty()) {
|
||||
$value = trim((string)$field->value());
|
||||
|
||||
if (is_base64_string_s($value)) {
|
||||
$field->value = base64_decode($value);
|
||||
}
|
||||
}
|
||||
|
||||
return $field;
|
||||
},
|
||||
];
|
86
site/plugins/kirby-highlighter/extensions/hooks.php
Normal file
86
site/plugins/kirby-highlighter/extensions/hooks.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
use Highlight\Highlighter;
|
||||
use JohannSchopplich\HTML5DOMDocument;
|
||||
use Kirby\Cms\App;
|
||||
|
||||
return [
|
||||
'kirbytext:after' => function (string|null $text) {
|
||||
$kirby = App::instance();
|
||||
|
||||
// Parse KirbyText input as HTML document
|
||||
$dom = new HTML5DOMDocument();
|
||||
$dom->loadHTML($text);
|
||||
|
||||
// Retrieve all `pre` elements inside newly created HTML document
|
||||
$preNodes = $dom->getElementsByTagName('pre');
|
||||
|
||||
// Bail if no `pre` elements have been found
|
||||
if ($preNodes->length === 0) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
// Loop through all `pre` elements
|
||||
foreach ($preNodes as $preNode) {
|
||||
// Ensure nothing but the `code` element exists
|
||||
if ($preNode->childNodes->length !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Select direct `code` child element of `pre` block
|
||||
$codeNode = $preNode->firstChild;
|
||||
|
||||
// Get language code if present
|
||||
$language = $codeNode->getAttribute('class');
|
||||
if (str_starts_with($language, 'language-')) {
|
||||
$language = preg_replace('/^language-/', '', $language);
|
||||
}
|
||||
|
||||
// Bail highlighting if language isn't set and auto detection is disabled
|
||||
if (empty($language) && !$kirby->option('johannschopplich.highlighter.autodetect', false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add `hljs` class to `pre` block
|
||||
$preNode->setAttribute('class', $kirby->option('johannschopplich.highlighter.class', 'hljs'));
|
||||
|
||||
// Get raw code data to highlight
|
||||
$code = $codeNode->nodeValue;
|
||||
|
||||
// Remove code element afterwards
|
||||
$preNode->removeChild($codeNode);
|
||||
|
||||
// Initiate `Highlighter` and use pre-defined language code, fall
|
||||
// back to language auto detection if enabled
|
||||
$highlighter = new Highlighter();
|
||||
|
||||
// Highlight code
|
||||
if (!empty($language)) {
|
||||
$highlightedCode = $highlighter->highlight($language, $code);
|
||||
} elseif ($kirby->option('johannschopplich.highlighter.autodetect', false)) {
|
||||
$languageSubset = $kirby->option('johannschopplich.highlighter.languages', []);
|
||||
if (!empty($languageSubset)) {
|
||||
$highlighter->setAutodetectLanguages($languageSubset);
|
||||
}
|
||||
|
||||
$highlightedCode = $highlighter->highlightAuto($code);
|
||||
}
|
||||
|
||||
// Line numbering
|
||||
if ($kirby->option('johannschopplich.highlighter.line-numbering', false)) {
|
||||
$lines = preg_split('/\R/', $highlightedCode->value);
|
||||
$lineClass = $kirby->option('johannschopplich.highlighter.line-numbering-class', 'hljs-code-line');
|
||||
$highlightedCode->value = '<span class="' . $lineClass . '">' . implode("</span>\n<span class=\"$lineClass\">", $lines) . '</span>';
|
||||
}
|
||||
|
||||
// Append highlighted wrapped in `code` block to parent `pre`
|
||||
$codeNode = $dom->createDocumentFragment();
|
||||
$codeNode->appendXML('<code data-language="' . $language . '">' . $highlightedCode->value . '</code>');
|
||||
$preNode->appendChild($codeNode);
|
||||
}
|
||||
|
||||
// Save all changes
|
||||
$text = $dom->saveHTML(null, true);
|
||||
return $text;
|
||||
}
|
||||
];
|
27
site/plugins/kirby-highlighter/extensions/kirbytags.php
Normal file
27
site/plugins/kirby-highlighter/extensions/kirbytags.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\App;
|
||||
|
||||
return [
|
||||
'code' => [
|
||||
'attr' => [
|
||||
'lang',
|
||||
'language',
|
||||
],
|
||||
// TODO: Type as `\Kirby\Text\KirbyTag` for Kirby 4
|
||||
'html' => function ($tag) {
|
||||
$kirby = App::instance();
|
||||
$code = $tag->value;
|
||||
$language = $tag->lang ?? $tag->language;
|
||||
$block = new \Kirby\Cms\Block([
|
||||
'type' => 'code',
|
||||
'content' => [
|
||||
'language' => $language ?? 'plaintext',
|
||||
'code' => is_base64_string_s($code) ? base64_decode($code) : $code,
|
||||
]
|
||||
]);
|
||||
|
||||
return $kirby->snippet('blocks/code', ['block' => $block], true);
|
||||
}
|
||||
]
|
||||
];
|
12
site/plugins/kirby-highlighter/index.php
Normal file
12
site/plugins/kirby-highlighter/index.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
@include_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
\Kirby\Cms\App::plugin('johannschopplich/highlighter', [
|
||||
'hooks' => require __DIR__ . '/extensions/hooks.php',
|
||||
'fieldMethods' => require __DIR__ . '/extensions/fieldmethods.php',
|
||||
'tags' => require __DIR__ . '/extensions/kirbytags.php',
|
||||
'snippets' => [
|
||||
'blocks/code' => __DIR__ . '/snippets/blocks/code.php'
|
||||
]
|
||||
]);
|
12
site/plugins/kirby-highlighter/phpunit.xml
Normal file
12
site/plugins/kirby-highlighter/phpunit.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
verbose="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="classes">
|
||||
<directory suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
22
site/plugins/kirby-highlighter/snippets/blocks/code.php
Normal file
22
site/plugins/kirby-highlighter/snippets/blocks/code.php
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/** @var \Kirby\Cms\Block $block */
|
||||
$highlighter = new \Highlight\Highlighter();
|
||||
$language = $block->language()->value();
|
||||
$code = $block->code()->fromBase64()->value();
|
||||
|
||||
if (empty($language) || !in_array($language, $highlighter->listRegisteredLanguages())) {
|
||||
$language = 'plaintext';
|
||||
}
|
||||
|
||||
$highlightedCode = $highlighter->highlight($language, $code)->value;
|
||||
|
||||
// Handle line numbering
|
||||
if (option('johannschopplich.highlighter.line-numbering', false)) {
|
||||
$lines = preg_split('/\R/', $highlightedCode);
|
||||
$lineClass = option('johannschopplich.highlighter.line-numbering-class', 'hljs-code-line');
|
||||
$highlightedCode = '<span class="' . $lineClass . '">' . implode("</span>\n<span class=\"$lineClass\">", $lines) . '</span>';
|
||||
}
|
||||
|
||||
?>
|
||||
<pre class="<?= option('johannschopplich.highlighter.class', 'hljs') ?>"><code data-language="<?= $language ?>"><?= $highlightedCode ?></code></pre>
|
0
site/sessions/index.html
Normal file
0
site/sessions/index.html
Normal file
28
site/snippets/article.php
Normal file
28
site/snippets/article.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<article>
|
||||
<h2><a href="<?= $article->url() ?>"><?= $article->title() ?></a></h2>
|
||||
<p id="date"><?= $article->date()->toDate('Y-m-d') ?></p>
|
||||
<p>
|
||||
<div class="tagories">
|
||||
<?php if ($article->categories()->isNotEmpty()): ?>
|
||||
<span id="categories">
|
||||
<?php foreach ($article->categories()->split() as $category): ?>
|
||||
<a href="/blog/category/<?= $category ?>"><?= $category ?></a>
|
||||
<?php endforeach ?>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
<?php if ($article->tags()->isNotEmpty()): ?>
|
||||
<span id="tags">
|
||||
<?php foreach ($article->tags()->split() as $tag): ?>
|
||||
<a href="/blog/tag/<?= $tag ?>"><?= $tag ?></a>
|
||||
<?php endforeach ?>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<?= $article->text()->excerpt(300) ?>
|
||||
</p>
|
||||
|
||||
<p><a href="<?= $article->url() ?>">Read more</a></p>
|
||||
</article>
|
0
site/snippets/index.html
Normal file
0
site/snippets/index.html
Normal file
54
site/snippets/layout.php
Normal file
54
site/snippets/layout.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>
|
||||
<?= $site->title() ?>
|
||||
<?php if (!$page->isHomePage()): ?>
|
||||
- <?= $page->title() ?>
|
||||
<?php endif ?>
|
||||
</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="author" content="<?= $site->author() ?>" />
|
||||
<meta name="description" content="<?= $site->description() ?>" />
|
||||
<meta name="keywords" content="<?= $site->keywords() ?>" />
|
||||
|
||||
<?= css('assets/build/main.css') ?>
|
||||
|
||||
<link rel="alternate" type="application/rss+xml" href="<?= url('feed') ?>" title="<?= $site->title() ?>" />
|
||||
|
||||
<link rel="apple-touch-icon-precomposed" sizes="256x256" href="/assets/img/favicon/favicon-256.png">
|
||||
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/assets/img/favicon/favicon-144.png">
|
||||
<link rel="apple-touch-icon-precomposed" sizes="128x128" href="/assets/img/favicon/favicon-128.png">
|
||||
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/assets/img/favicon/favicon-72.png">
|
||||
<link rel="apple-touch-icon-precomposed" href="/assets/img/favicon/favicon.png">
|
||||
<link rel="shortcut icon" href="/assets/img/favicon/favicon.png">
|
||||
|
||||
<script data-goatcounter="https://<?= option('analytics.goatcounter') ?>/count" async src="https://<?= option('analytics.goatcounter') ?>/count.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a id="title" href="/"><?= $site->title() ?></a>
|
||||
<label for="show-menu" class="show-menu">Menu</label>
|
||||
<input type="checkbox" id="show-menu" role="button">
|
||||
<nav>
|
||||
<ul>
|
||||
<?php foreach ($site->menu()->toStructure() as $item): ?>
|
||||
<li>
|
||||
<a href="<?= $item->link()->toUrl() ?>"><?= $item->title() ?></a>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<?= $slot ?>
|
||||
</main>
|
||||
<footer>
|
||||
<p><?= date('Y') ?> © <?= $site->author() ?></p>
|
||||
<p>
|
||||
<a href="<?= page('imprint')->url() ?>">Impressum und Datenschutz</a>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
16
site/templates/about.php
Normal file
16
site/templates/about.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php snippet('layout', slots: true) ?>
|
||||
<?php slot() ?>
|
||||
<h1><?= $page->title() ?></h1>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
<img src="<?= $page->image('profile.png')->url() ?>" style="width: 300px" />
|
||||
<div style="font-size: 40px">
|
||||
<strong><?= $page->intro_title() ?></strong>
|
||||
</div>
|
||||
<div style="font-size: 25px">
|
||||
<?= $page->intro_text() ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= $page->text()->kirbytext() ?>
|
||||
<?php endslot() ?>
|
33
site/templates/article.php
Normal file
33
site/templates/article.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php snippet('layout', slots: true) ?>
|
||||
<?php slot() ?>
|
||||
<h1><?= $page->title() ?></h1>
|
||||
|
||||
<p id="date"><?= $page->date()->toDate('Y-m-d') ?></p>
|
||||
<p id="readingtime"><?= $page->readingTime() ?></p>
|
||||
|
||||
<p>
|
||||
<div class="tagories">
|
||||
<?php if ($page->categories()->isNotEmpty()): ?>
|
||||
<span id="categories">
|
||||
<?php foreach ($page->categories()->split() as $category): ?>
|
||||
<a href="/blog/category/<?= $category ?>"><?= $category ?></a>
|
||||
<?php endforeach ?>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
<?php if ($page->tags()->isNotEmpty()): ?>
|
||||
<span id="tags">
|
||||
<?php foreach ($page->tags()->split() as $tag): ?>
|
||||
<a href="/blog/tag/<?= $tag ?>"><?= $tag ?></a>
|
||||
<?php endforeach ?>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<?= $page->text()->kirbytext() ?>
|
||||
|
||||
<div class="comment">
|
||||
<p>I would like to hear what you think about this post. Feel free to write me a mail!</p>
|
||||
<a class="btn" href="mailto:comment@mmk2410.org?subject=Reply to: <?= $page->title()?>">Reply by mail</a>
|
||||
</div>
|
||||
<?php endslot() ?>
|
27
site/templates/blog.php
Normal file
27
site/templates/blog.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php snippet('layout', slots: true) ?>
|
||||
<?php slot() ?>
|
||||
<h1><?= $page->title() ?></h1>
|
||||
|
||||
<?= $page->text()->kirbytext() ?>
|
||||
|
||||
<?php foreach($articles as $article): ?>
|
||||
<?php snippet('article', ['article' => $article]) ?>
|
||||
<?php endforeach ?>
|
||||
|
||||
<?php if ($articles->pagination()->hasPages()): ?>
|
||||
<nav class="pagination">
|
||||
<?php if ($articles->pagination()->hasNextPage()): ?>
|
||||
<a class="page-item next" href="<?= $articles->pagination()->nextPageURL() ?>">
|
||||
‹ older posts
|
||||
</a>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($articles->pagination()->hasPrevPage()): ?>
|
||||
<a class="page-item prev" href="<?= $articles->pagination()->prevPageURL() ?>">
|
||||
newer posts ›
|
||||
</a>
|
||||
<?php endif ?>
|
||||
</nav>
|
||||
<?php endif ?>
|
||||
|
||||
<?php endslot() ?>
|
31
site/templates/blog.rss.php
Normal file
31
site/templates/blog.rss.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
$feed = new SimpleXMLElement('<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"></rss>');
|
||||
|
||||
$feed->channel->title = $site->title()->toString();
|
||||
$feed->channel->description = $site->description()->toString();
|
||||
$feed->channel->link = url();
|
||||
$feed->channel->language = 'en-us';
|
||||
$feed->channel->lastBuildDate = date(DATE_RSS);
|
||||
$feed->channel->generator = 'Kirby';
|
||||
|
||||
$atomLink = $feed->channel->addChild('atom:link', null, 'atom');
|
||||
$atomLink->addAttribute('href', url('/feed'));
|
||||
$atomLink->addAttribute('rel', 'self');
|
||||
$atomLink->addAttribute('type', 'application/rss+xml');
|
||||
|
||||
$articles = $page->children()->template('article')->sortBy('date', 'desc')->limit(10);
|
||||
|
||||
foreach ($articles as $article) {
|
||||
$xmlArticle = $feed->channel->addChild('item');
|
||||
$xmlArticle->title = $article->title()->toString();
|
||||
$xmlArticle->link = url($article->url());
|
||||
$xmlArticle->description = Escape::xml($article->text()->kirbytext());
|
||||
$xmlArticle->pubDate = $article->date()->toDate(DATE_RSS);
|
||||
$xmlArticle->guid = url($article->url());
|
||||
}
|
||||
|
||||
$kirby->response()->type('application/rss+xml');
|
||||
|
||||
echo $feed->asXML();
|
6
site/templates/default.php
Normal file
6
site/templates/default.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php snippet('layout', slots: true) ?>
|
||||
<?php slot() ?>
|
||||
<h1><?= $page->title() ?></h1>
|
||||
|
||||
<?= $page->text()->kirbytext() ?>
|
||||
<?php endslot() ?>
|
29
site/templates/home.php
Normal file
29
site/templates/home.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php snippet('layout', slots: true) ?>
|
||||
<?php slot() ?>
|
||||
<header>
|
||||
<h1><?= $site->title() ?></h1>
|
||||
<h3><?= $page->subtitle() ?></h3>
|
||||
</header>
|
||||
|
||||
<?= $page->text()->kirbytext() ?>
|
||||
|
||||
<h4>Read more from me</h4>
|
||||
|
||||
<?php foreach ($page->internal_menu()->toStructure() as $item): ?>
|
||||
<a class="btn" href="<?= $item->link()->toUrl() ?>"><?= $item->title() ?></a>
|
||||
<?php endforeach ?>
|
||||
|
||||
<h4>Find me on other places</h4>
|
||||
|
||||
<?php foreach ($page->external_menu()->toStructure() as $item): ?>
|
||||
<a class="btn" href="<?= $item->link()->toUrl() ?>"><?= $item->title() ?></a>
|
||||
<?php endforeach ?>
|
||||
|
||||
<h2>Latest Posts</h2>
|
||||
|
||||
<?php foreach(page('blog')->children()->listed()->flip()->limit(3) as $article): ?>
|
||||
<?php snippet('article', ['article' => $article]) ?>
|
||||
<?php endforeach ?>
|
||||
|
||||
<a class="btn" href="<?= page('blog')->url() ?>">Read more posts</a>
|
||||
<?php endslot() ?>
|
34
site/templates/quotes.php
Normal file
34
site/templates/quotes.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php snippet('layout', slots: true) ?>
|
||||
<?php slot() ?>
|
||||
<h1><?= $page->title() ?></h1>
|
||||
|
||||
<?= $page->text()->kirbytext() ?>
|
||||
|
||||
<?php foreach($quotes as $quote): ?>
|
||||
<article>
|
||||
<h2><a href="<?= $quote->url() ?>"><?= $quote->title() ?></a></h2>
|
||||
<p id="date"><?= $quote->date()->toDate('Y-m-d') ?></p>
|
||||
|
||||
<p>
|
||||
<?= $quote->text()->kirbytext() ?>
|
||||
</p>
|
||||
</article>
|
||||
<?php endforeach ?>
|
||||
|
||||
<?php if ($quotes->pagination()->hasPages()): ?>
|
||||
<nav class="pagination">
|
||||
<?php if ($quotes->pagination()->hasNextPage()): ?>
|
||||
<a class="page-item next" href="<?= $quotes->pagination()->nextPageURL() ?>">
|
||||
‹ ältere Einträge
|
||||
</a>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($quotes->pagination()->hasPrevPage()): ?>
|
||||
<a class="page-item prev" href="<?= $quotes->pagination()->prevPageURL() ?>">
|
||||
neuere Einträge ›
|
||||
</a>
|
||||
<?php endif ?>
|
||||
</nav>
|
||||
<?php endif ?>
|
||||
|
||||
<?php endslot() ?>
|
Loading…
Add table
Add a link
Reference in a new issue