1
0
mirror of https://github.com/typemill/typemill.git synced 2025-04-20 20:11:53 +02:00

Version 1.3.0: Meta-Information

This commit is contained in:
trendschau 2019-12-31 15:57:45 +01:00
parent ead51d6540
commit 7d41c4894c
85 changed files with 964 additions and 1364 deletions

23
.gitignore vendored
View File

@ -1,22 +1,13 @@
cache
plugins/admin
plugins/contactform
plugins/demo
plugins/disqus
plugins/download
plugins/finalwords
plugins/hyer
plugins/joblistings
plugins/landingpage
plugins/mail
plugins/newsletter
plugins/textadds
plugins/version
content/index.yaml
content/00-Welcome/index.yaml
content/00-Welcome/00-Setup.yaml
content/00-Welcome/01-Write-Content.yaml
content/00-Welcome/02-Get-Help.yaml
content/00-Welcome/03-Markdown-Test.yaml
settings/settings.yaml
settings/formdata.yaml
settings/users
system/vendor
themes/learn
tests
plugins/demo
zips
build.php

2
cache/lastCache.txt vendored
View File

@ -1 +1 @@
1574585614
1577803430

View File

@ -1,47 +0,0 @@
<?php
namespace Plugins\Analytics;
use \Typemill\Plugin;
class Analytics extends Plugin
{
protected $settings;
public static function getSubscribedEvents()
{
return array(
'onSettingsLoaded' => 'onSettingsLoaded',
'onTwigLoaded' => 'onTwigLoaded'
);
}
public function onSettingsLoaded($settings)
{
$this->settings = $settings->getData();
}
public function onTwigLoaded()
{
/* get Twig Instance and add the cookieconsent template-folder to the path */
$twig = $this->getTwig();
$loader = $twig->getLoader();
$loader->addPath(__DIR__ . '/templates');
$analyticSettings = $this->settings['settings']['plugins']['analytics'];
if(isset($analyticSettings['tool']))
{
/* fetch the template, render it with twig and add javascript with settings */
if($analyticSettings['tool'] == 'piwik')
{
$this->addInlineJS($twig->fetch('/piwikanalytics.twig', $this->settings));
}
elseif($analyticSettings['tool'] == 'google')
{
$this->addJS('https://www.googletagmanager.com/gtag/js?id=' . $analyticSettings['google_id']);
$this->addInlineJS($twig->fetch('/googleanalytics.twig', $analyticSettings));
}
}
}
}

View File

@ -1,38 +0,0 @@
name: Analytics
version: 1.0.0
description: Integrate Piwik or Google Analytics Script
author: Sebastian Schürmanns
homepage: http://typemill.net
licence: MIT
settings:
tool: none
forms:
fields:
tool:
type: radio
label: Choose Your Tool
options:
none: None
piwik: Piwik
google: Google Analytics
piwik_url:
type: text
label: Piwik URL
help: Add the URL to your piwik installation without protocol like this: my-site.com.
placeholder: 'url like my-piwik-installation.com'
piwik_id:
type: number
label: Piwik Site-ID
help: You can find the id in Piwik under configuration and tracking code.
placeholder: 'simple number like 8'
google_id:
type: text
label: Google Tracking ID
help: You can find the tracking id in google under property. It starts with UA-
placeholder: 'UA-12345-6'

View File

@ -1,5 +0,0 @@
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ google_id }}');

View File

@ -1,11 +0,0 @@
var _paq = _paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function(){
var u="//{{ settings.plugins.analytics.piwik_url }}/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '{{ settings.plugins.analytics.piwik_id }}']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();

View File

@ -1,38 +0,0 @@
<?php
namespace Plugins\CookieConsent;
use \Typemill\Plugin;
class CookieConsent extends Plugin
{
protected $settings;
public static function getSubscribedEvents()
{
return array(
'onSettingsLoaded' => 'onSettingsLoaded',
'onTwigLoaded' => 'onTwigLoaded'
);
}
public function onSettingsLoaded($settings)
{
$this->settings = $settings->getData();
}
public function onTwigLoaded()
{
/* add external CSS and JavaScript */
$this->addCSS('/cookieconsent/public/cookieconsent.min.css');
$this->addJS('/cookieconsent/public/cookieconsent.min.js');
/* get Twig Instance and add the cookieconsent template-folder to the path */
$twig = $this->getTwig();
$loader = $twig->getLoader();
$loader->addPath(__DIR__ . '/templates');
/* fetch the template, render it with twig and add it as inline-javascript */
$this->addInlineJS($twig->fetch('/cookieconsent.twig', $this->settings));
}
}

View File

@ -1,88 +0,0 @@
name: Cookie Consent
version: 1.0.1
description: Enables a cookie consent for websites
author: Sebastian Schürmanns
homepage: https://cookieconsent.insites.com/
licence: MIT
settings:
popup_background: '#70c1b3'
popup_text: '#ffffff'
button_background: '#66b0a3'
button_text: '#ffffff'
theme: 'edgeless'
position: 'bottom'
message: 'This website uses cookies to ensure you get the best experience on our website.'
link: 'Learn More'
href: 'https://cookiesandyou.com/'
dismiss: 'Got It'
forms:
fields:
theme:
type: select
label: Theme
placeholder: 'Add name of theme'
required: true
options:
edgeless: Edgeless
block: Block
classic: Classic
position:
type: select
label: Position of Cookie Banner
options:
bottom: Bottom
top: Top
bottom-left: Bottom left
bottom-right: Bottom right
message:
type: textarea
label: Message
placeholder: 'Message for cookie-popup'
required: true
href:
type: url
label: Link to more informations
placeholder: 'https://cookiesandyou.com/'
required: true
link:
type: text
label: Label for Link
placeholder: 'Link-Lable like More infos'
required: true
dismiss:
type: text
label: Label for Button
placeholder: 'Got it'
required: true
popup_background:
type: color
label: Background Color of Popup
placeholder: 'Add hex color value like #ffffff'
required: true
popup_text:
type: color
label: Text Color of Popup
placeholder: 'Add hex color value like #ffffff'
required: true
button_background:
type: color
label: Background Color of Button
placeholder: 'Add hex color value like #ffffff'
required: true
button_text:
type: color
label: Text Color of Button
placeholder: 'Add hex color value like #ffffff'
required: true

View File

@ -1,6 +0,0 @@
.cc-window{opacity:1;transition:opacity 1s ease}.cc-window.cc-invisible{opacity:0}.cc-animate.cc-revoke{transition:transform 1s ease}.cc-animate.cc-revoke.cc-top{transform:translateY(-2em)}.cc-animate.cc-revoke.cc-bottom{transform:translateY(2em)}.cc-animate.cc-revoke.cc-active.cc-bottom,.cc-animate.cc-revoke.cc-active.cc-top,.cc-revoke:hover{transform:translateY(0)}.cc-grower{max-height:0;overflow:hidden;transition:max-height 1s}
.cc-link,.cc-revoke:hover{text-decoration:underline}.cc-revoke,.cc-window{position:fixed;overflow:hidden;box-sizing:border-box;font-family:Helvetica,Calibri,Arial,sans-serif;font-size:16px;line-height:1.5em;display:-ms-flexbox;display:flex;-ms-flex-wrap:nowrap;flex-wrap:nowrap;z-index:9999}.cc-window.cc-static{position:static}.cc-window.cc-floating{padding:2em;max-width:24em;-ms-flex-direction:column;flex-direction:column}.cc-window.cc-banner{padding:1em 1.8em;width:100%;-ms-flex-direction:row;flex-direction:row}.cc-revoke{padding:.5em}.cc-header{font-size:18px;font-weight:700}.cc-btn,.cc-close,.cc-link,.cc-revoke{cursor:pointer}.cc-link{opacity:.8;display:inline-block;padding:.2em}.cc-link:hover{opacity:1}.cc-link:active,.cc-link:visited{color:initial}.cc-btn{display:block;padding:.4em .8em;font-size:.9em;font-weight:700;border-width:2px;border-style:solid;text-align:center;white-space:nowrap}.cc-highlight .cc-btn:first-child{background-color:transparent;border-color:transparent}.cc-highlight .cc-btn:first-child:focus,.cc-highlight .cc-btn:first-child:hover{background-color:transparent;text-decoration:underline}.cc-close{display:block;position:absolute;top:.5em;right:.5em;font-size:1.6em;opacity:.9;line-height:.75}.cc-close:focus,.cc-close:hover{opacity:1}
.cc-revoke.cc-top{top:0;left:3em;border-bottom-left-radius:.5em;border-bottom-right-radius:.5em}.cc-revoke.cc-bottom{bottom:0;left:3em;border-top-left-radius:.5em;border-top-right-radius:.5em}.cc-revoke.cc-left{left:3em;right:unset}.cc-revoke.cc-right{right:3em;left:unset}.cc-top{top:1em}.cc-left{left:1em}.cc-right{right:1em}.cc-bottom{bottom:1em}.cc-floating>.cc-link{margin-bottom:1em}.cc-floating .cc-message{display:block;margin-bottom:1em}.cc-window.cc-floating .cc-compliance{-ms-flex:1 0 auto;flex:1 0 auto}.cc-window.cc-banner{-ms-flex-align:center;align-items:center}.cc-banner.cc-top{left:0;right:0;top:0}.cc-banner.cc-bottom{left:0;right:0;bottom:0}.cc-banner .cc-message{display:block;-ms-flex:1 1 auto;flex:1 1 auto;max-width:100%;margin-right:1em}.cc-compliance{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-line-pack:justify;align-content:space-between}.cc-floating .cc-compliance>.cc-btn{-ms-flex:1;flex:1}.cc-btn+.cc-btn{margin-left:.5em}
@media print{.cc-revoke,.cc-window{display:none}}@media screen and (max-width:900px){.cc-btn{white-space:normal}}@media screen and (max-width:414px) and (orientation:portrait),screen and (max-width:736px) and (orientation:landscape){.cc-window.cc-top{top:0}.cc-window.cc-bottom{bottom:0}.cc-window.cc-banner,.cc-window.cc-floating,.cc-window.cc-left,.cc-window.cc-right{left:0;right:0}.cc-window.cc-banner{-ms-flex-direction:column;flex-direction:column}.cc-window.cc-banner .cc-compliance{-ms-flex:1 1 auto;flex:1 1 auto}.cc-window.cc-floating{max-width:none}.cc-window .cc-message{margin-bottom:1em}.cc-window.cc-banner{-ms-flex-align:unset;align-items:unset}.cc-window.cc-banner .cc-message{margin-right:0}}
.cc-floating.cc-theme-classic{padding:1.2em;border-radius:5px}.cc-floating.cc-type-info.cc-theme-classic .cc-compliance{text-align:center;display:inline;-ms-flex:none;flex:none}.cc-theme-classic .cc-btn{border-radius:5px}.cc-theme-classic .cc-btn:last-child{min-width:140px}.cc-floating.cc-type-info.cc-theme-classic .cc-btn{display:inline-block}
.cc-theme-edgeless.cc-window{padding:0}.cc-floating.cc-theme-edgeless .cc-message{margin:2em 2em 1.5em}.cc-banner.cc-theme-edgeless .cc-btn{margin:0;padding:.8em 1.8em;height:100%}.cc-banner.cc-theme-edgeless .cc-message{margin-left:1em}.cc-floating.cc-theme-edgeless .cc-btn+.cc-btn{margin-left:0}

File diff suppressed because one or more lines are too long

View File

@ -1,22 +0,0 @@
window.addEventListener("load", function(){
window.cookieconsent.initialise({
"palette": {
"popup": {
"background": "{{ settings.plugins.cookieconsent.popup_background }}",
"text": "{{ settings.plugins.cookieconsent.popup_text }}"
},
"button": {
"background": "{{ settings.plugins.cookieconsent.button_background }}",
"text": "{{ settings.plugins.cookieconsent.button_text }}"
}
},
"theme": "{{ settings.plugins.cookieconsent.theme }}",
"position": "{{ settings.plugins.cookieconsent.position }}",
"content": {
"message": "{{ settings.plugins.cookieconsent.message }}",
"dismiss": "{{ settings.plugins.cookieconsent.dismiss }}",
"link": "{{ settings.plugins.cookieconsent.link }}",
"href": "{{ settings.plugins.cookieconsent.href }}"
}
})
});

View File

@ -1,28 +0,0 @@
<?php
namespace Plugins\Highlight;
use \Typemill\Plugin;
class Highlight extends Plugin
{
protected $settings;
public static function getSubscribedEvents()
{
return array(
'onTwigLoaded' => 'onTwigLoaded'
);
}
public function onTwigLoaded()
{
/* add external CSS and JavaScript */
$this->addCSS('/highlight/public/default.css');
$this->addJS('/highlight/public/highlight.pack.js');
/* initialize the script */
$this->addInlineJS('hljs.initHighlightingOnLoad();');
}
}

View File

@ -1,6 +0,0 @@
name: Highlight
version: 1.0.0
description: Adds the famous javascript syntax highlighter.
author: Sebastian Schürmanns
homepage: https://highlightjs.org/
licence: BSD

View File

@ -1,24 +0,0 @@
Copyright (c) 2006, Ivan Sagalaev
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of highlight.js nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,99 +0,0 @@
/*
Original highlight.js style (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #F0F0F0;
}
/* Base color: saturation 0; */
.hljs,
.hljs-subst {
color: #444;
}
.hljs-comment {
color: #888888;
}
.hljs-keyword,
.hljs-attribute,
.hljs-selector-tag,
.hljs-meta-keyword,
.hljs-doctag,
.hljs-name {
font-weight: bold;
}
/* User color: hue: 0 */
.hljs-type,
.hljs-string,
.hljs-number,
.hljs-selector-id,
.hljs-selector-class,
.hljs-quote,
.hljs-template-tag,
.hljs-deletion {
color: #880000;
}
.hljs-title,
.hljs-section {
color: #880000;
font-weight: bold;
}
.hljs-regexp,
.hljs-symbol,
.hljs-variable,
.hljs-template-variable,
.hljs-link,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #BC6060;
}
/* Language color: hue: 90; */
.hljs-literal {
color: #78A960;
}
.hljs-built_in,
.hljs-bullet,
.hljs-code,
.hljs-addition {
color: #397300;
}
/* Meta color: hue: 200 */
.hljs-meta {
color: #1f7199;
}
.hljs-meta-string {
color: #4d99bf;
}
/* Misc effects */
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

File diff suppressed because one or more lines are too long

View File

@ -1,56 +0,0 @@
<?php
namespace Plugins\Math;
use \Typemill\Plugin;
class Math extends Plugin
{
protected $settings;
public static function getSubscribedEvents()
{
return array(
'onSettingsLoaded' => 'onSettingsLoaded',
'onTwigLoaded' => 'onTwigLoaded'
);
}
public function onSettingsLoaded($settings)
{
$this->settings = $settings->getData();
}
public function onTwigLoaded()
{
$mathSettings = $this->settings['settings']['plugins']['math'];
if($mathSettings['tool'] == 'mathjax')
{
/* add external CSS and JavaScript */
$this->addJS('https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/latest.js?config=TeX-MML-AM_CHTML');
}
if($mathSettings['tool'] == 'katex')
{
$this->addJS('/math/public/katex.min.js');
$this->addJS('/math/public/auto-render.min.js');
$this->addCSS('/math/public/katex.min.css');
/* initialize autorendering of page only in frontend */
if (strpos($this->getPath(), 'tm/content') === false)
{
$this->addInlineJs('renderMathInElement(document.body);');
}
}
# add math to the blox editor configuration
$this->addEditorJS('/math/public/math.js');
$this->addSvgSymbol('<symbol id="icon-omega" viewBox="0 0 32 32">
<title>omega</title>
<path d="M22 28h8l2-4v8h-12v-6.694c4.097-1.765 7-6.161 7-11.306 0-6.701-4.925-11.946-11-11.946s-11 5.245-11 11.946c0 5.144 2.903 9.541 7 11.306v6.694h-12v-8l2 4h8v-1.018c-5.863-2.077-10-7.106-10-12.982 0-7.732 7.163-14 16-14s16 6.268 16 14c0 5.875-4.137 10.905-10 12.982v1.018z"></path>
</symbol>');
}
}

View File

@ -1,20 +0,0 @@
name: Math
version: 1.1.0
description: Adds support for katex and mathjax.
author: Sebastian Schürmanns
homepage: https://mathjax.org/
licence: Apache 2.0 / MIT
settings:
tool: none
forms:
fields:
tool:
type: radio
label: Choose Your Tool
options:
none: None
katex: KaTex
mathjax: MathJax

View File

@ -1,140 +0,0 @@
# [<img src="https://khan.github.io/KaTeX/katex-logo.svg" width="130" alt="KaTeX">](https://khan.github.io/KaTeX/)
[![Build Status](https://travis-ci.org/Khan/KaTeX.svg?branch=master)](https://travis-ci.org/Khan/KaTeX)
[![codecov](https://codecov.io/gh/Khan/KaTeX/branch/master/graph/badge.svg)](https://codecov.io/gh/Khan/KaTeX)
[![Join the chat at https://gitter.im/Khan/KaTeX](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Khan/KaTeX?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web.
* **Fast:** KaTeX renders its math synchronously and doesn't need to reflow the page. See how it compares to a competitor in [this speed test](http://www.intmath.com/cg5/katex-mathjax-comparison.php).
* **Print quality:** KaTeXs layout is based on Donald Knuths TeX, the gold standard for math typesetting.
* **Self contained:** KaTeX has no dependencies and can easily be bundled with your website resources.
* **Server side rendering:** KaTeX produces the same output regardless of browser or environment, so you can pre-render expressions using Node.js and send them as plain HTML.
KaTeX supports all major browsers, including Chrome, Safari, Firefox, Opera, Edge, and IE 9 - IE 11. More information can be found on the [list of supported commands](https://khan.github.io/KaTeX/function-support.html) and on the [wiki](https://github.com/khan/katex/wiki).
## Usage
You can [download KaTeX](https://github.com/khan/katex/releases) and host it on your server or include the `katex.min.js` and `katex.min.css` files on your page directly from a CDN:
```html
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/katex.min.css" integrity="sha384-VEnyslhHLHiYPca9KFkBB3CMeslnM9CzwjxsEbZTeA21JBm7tdLwKoZmCt3cZTYD" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/katex.min.js" integrity="sha384-O4hpKqcplNCe+jLuBVEXC10Rn1QEqAmX98lKAIFBEDxZI0a+6Z2w2n8AEtQbR4CD" crossorigin="anonymous"></script>
```
#### In-browser rendering
Call `katex.render` with a TeX expression and a DOM element to render into:
```js
katex.render("c = \\pm\\sqrt{a^2 + b^2}", element);
```
If KaTeX can't parse the expression, it throws a `katex.ParseError` error.
#### Server side rendering or rendering to a string
To generate HTML on the server or to generate an HTML string of the rendered math, you can use `katex.renderToString`:
```js
var html = katex.renderToString("c = \\pm\\sqrt{a^2 + b^2}");
// '<span class="katex">...</span>'
```
Make sure to include the CSS and font files, but there is no need to include the JavaScript. Like `render`, `renderToString` throws if it can't parse the expression.
#### Security
Any HTML generated by KaTeX *should* be safe from `<script>` or other code
injection attacks.
(See `maxSize` below for preventing large width/height visual affronts.)
Of course, it is always a good idea to sanitize the HTML, though you will need
a rather generous whitelist (including some of SVG and MathML) to support
all of KaTeX.
#### Handling errors
If KaTeX encounters an error (invalid or unsupported LaTeX), then it will
throw an exception of type `katex.ParseError`. The message in this error
includes some of the LaTeX source code, so needs to be escaped if you want
to render it to HTML. In particular, you should convert `&`, `<`, `>`
characters to `&amp;`, `&lt;`, `&gt;` (e.g., using `_.escape`)
before including either LaTeX source code or exception messages in your
HTML/DOM. (Failure to escape in this way makes a `<script>` injection
attack possible if your LaTeX source is untrusted.)
#### Rendering options
You can provide an object of options as the last argument to `katex.render` and `katex.renderToString`. Available options are:
- `displayMode`: `boolean`. If `true` the math will be rendered in display mode, which will put the math in display style (so `\int` and `\sum` are large, for example), and will center the math on the page on its own line. If `false` the math will be rendered in inline mode. (default: `false`)
- `throwOnError`: `boolean`. If `true`, KaTeX will throw a `ParseError` when it encounters an unsupported command. If `false`, KaTeX will render the unsupported command as text in the color given by `errorColor`. (default: `true`)
- `errorColor`: `string`. A color string given in the format `"#XXX"` or `"#XXXXXX"`. This option determines the color which unsupported commands are rendered in. (default: `#cc0000`)
- `macros`: `object`. A collection of custom macros. Each macro is a property with a name like `\name` (written `"\\name"` in JavaScript) which maps to a string that describes the expansion of the macro. Single-character keys can also be included in which case the character will be redefined as the given macro (similar to TeX active characters).
- `colorIsTextColor`: `boolean`. If `true`, `\color` will work like LaTeX's `\textcolor`, and take two arguments (e.g., `\color{blue}{hello}`), which restores the old behavior of KaTeX (pre-0.8.0). If `false` (the default), `\color` will work like LaTeX's `\color`, and take one argument (e.g., `\color{blue}hello`). In both cases, `\textcolor` works as in LaTeX (e.g., `\textcolor{blue}{hello}`).
- `maxSize`: `number`. If non-zero, all user-specified sizes, e.g. in `\rule{500em}{500em}`, will be capped to `maxSize` ems. Otherwise, users can make elements and spaces arbitrarily large (the default behavior).
For example:
```js
katex.render("c = \\pm\\sqrt{a^2 + b^2}\\in\\RR", element, {
displayMode: true,
macros: {
"\\RR": "\\mathbb{R}"
}
});
```
#### Automatic rendering of math on a page
Math on the page can be automatically rendered using the auto-render extension. See [the Auto-render README](contrib/auto-render/README.md) for more information.
#### Font size and lengths
By default, KaTeX math is rendered in a 1.21× larger font than the surrounding
context, which makes super- and subscripts easier to read. You can control
this using CSS, for example:
```css
.katex { font-size: 1.1em; }
```
KaTeX supports all TeX units, including absolute units like `cm` and `in`.
Absolute units are currently scaled relative to the default TeX font size of
10pt, so that `\kern1cm` produces the same results as `\kern2.845275em`.
As a result, relative and absolute units are both uniformly scaled relative
to LaTeX with a 10pt font; for example, the rectangle `\rule{1cm}{1em}` has
the same aspect ratio in KaTeX as in LaTeX. However, because most browsers
default to a larger font size, this typically means that a 1cm kern in KaTeX
will appear larger than 1cm in browser units.
### Common Issues
- Many Markdown preprocessors, such as the one that Jekyll and GitHub Pages use,
have a "smart quotes" feature. This changes `'` to `` which is an issue for
math containing primes, e.g. `f'`. This can be worked around by defining a
single character macro which changes them back, e.g. `{"", "'"}`.
- KaTeX follows LaTeX's rendering of `aligned` and `matrix` environments unlike
MathJax. When displaying fractions one above another in these vertical
layouts there may not be enough space between rows for people who are used to
MathJax's rendering. The distance between rows can be adjusted by using
`\\[0.1em]` instead of the standard line separator distance.
- KaTeX does not support the `align` environment because LaTeX doesn't support
`align` in math mode. The `aligned` environment offers the same functionality
but in math mode, so use that instead or define a macro that maps `align` to
`aligned`.
## Libraries
### Angular2+
- [ng-katex](https://github.com/garciparedes/ng-katex) Angular module to write beautiful math expressions with TeX syntax boosted by KaTeX library
### Ruby
- [katex-ruby](https://github.com/glebm/katex-ruby) Provides server-side rendering and integration with popular Ruby web frameworks (Rails, Hanami, and anything that uses Sprockets).
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)
## License
KaTeX is licensed under the [MIT License](http://opensource.org/licenses/MIT).

View File

@ -1 +0,0 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,function(e){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=1)}([function(t,r){t.exports=e},function(e,t,r){"use strict";r.r(t);var n=r(0),o=r.n(n),a=function(e,t,r){for(var n=r,o=0,a=e.length;n<t.length;){var i=t[n];if(o<=0&&t.slice(n,n+a)===e)return n;"\\"===i?n++:"{"===i?o++:"}"===i&&o--,n++}return-1},i=function(e,t,r,n){for(var o=[],i=0;i<e.length;i++)if("text"===e[i].type){var l=e[i].data,d=!0,s=0,f=void 0;for(-1!==(f=l.indexOf(t))&&(s=f,o.push({type:"text",data:l.slice(0,s)}),d=!1);;){if(d){if(-1===(f=l.indexOf(t,s)))break;o.push({type:"text",data:l.slice(s,f)}),s=f}else{if(-1===(f=a(r,l,s+t.length)))break;o.push({type:"math",data:l.slice(s+t.length,f),rawData:l.slice(s,f+r.length),display:n}),s=f+r.length}d=!d}o.push({type:"text",data:l.slice(s)})}else o.push(e[i]);return o},l=function(e,t){for(var r=function(e,t){for(var r=[{type:"text",data:e}],n=0;n<t.length;n++){var o=t[n];r=i(r,o.left,o.right,o.display||!1)}return r}(e,t.delimiters),n=document.createDocumentFragment(),a=0;a<r.length;a++)if("text"===r[a].type)n.appendChild(document.createTextNode(r[a].data));else{var l=document.createElement("span"),d=r[a].data;t.displayMode=r[a].display;try{t.preProcess&&(d=t.preProcess(d)),o.a.render(d,l,t)}catch(e){if(!(e instanceof o.a.ParseError))throw e;t.errorCallback("KaTeX auto-render: Failed to parse `"+r[a].data+"` with ",e),n.appendChild(document.createTextNode(r[a].rawData));continue}n.appendChild(l)}return n};t.default=function(e,t){if(!e)throw new Error("No element provided to render");var r={};for(var n in t)t.hasOwnProperty(n)&&(r[n]=t[n]);r.delimiters=r.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\[",right:"\\]",display:!0}],r.ignoredTags=r.ignoredTags||["script","noscript","style","textarea","pre","code"],r.ignoredClasses=r.ignoredClasses||[],r.errorCallback=r.errorCallback||console.error,r.macros=r.macros||{},function e(t,r){for(var n=0;n<t.childNodes.length;n++){var o=t.childNodes[n];if(3===o.nodeType){var a=l(o.textContent,r);n+=a.childNodes.length-1,t.replaceChild(a,o)}else 1===o.nodeType&&function(){var t=" "+o.className+" ";-1===r.ignoredTags.indexOf(o.nodeName.toLowerCase())&&r.ignoredClasses.every(function(e){return-1===t.indexOf(" "+e+" ")})&&e(o,r)}()}}(e,r)}}]).default});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,54 +0,0 @@
determiner.math = function(block,lines,firstChar,secondChar,thirdChar){
if( (firstChar == '\\' && secondChar == '[') || (firstChar == '$' && secondChar == '$') )
{
return "math-component";
}
return false;
};
bloxFormats.math = { label: '<svg class="icon icon-omega"><use xlink:href="#icon-omega"></use></svg>', title: 'Math', component: 'math-component' };
formatConfig.push('math');
const mathComponent = Vue.component('math-component', {
props: ['compmarkdown', 'disabled'],
template: '<div>' +
'<input type="hidden" ref="markdown" :value="compmarkdown" :disabled="disabled" @input="updatemarkdown" />' +
'<div class="contenttype"><svg class="icon icon-omega"><use xlink:href="#icon-omega"></use></svg></div>' +
'<textarea class="mdcontent" ref="markdown" v-model="mathblock" :disabled="disabled" @input="createmarkdown"></textarea>' +
'</div>',
data: function(){
return {
mathblock: ''
}
},
mounted: function(){
this.$refs.markdown.focus();
if(this.compmarkdown)
{
var dollarMath = new RegExp(/^\$\$[\S\s]+\$\$$/m);
var bracketMath = new RegExp(/^\\\[[\S\s]+\\\]$/m);
if(dollarMath.test(this.compmarkdown) || bracketMath.test(this.compmarkdown))
{
var mathExpression = this.compmarkdown.substring(2,this.compmarkdown.length-2);
this.mathblock = mathExpression.trim();
}
}
this.$nextTick(function () {
autosize(document.querySelectorAll('textarea'));
});
},
methods: {
createmarkdown: function(event)
{
this.codeblock = event.target.value;
var codeblock = '$$\n' + event.target.value + '\n$$';
this.updatemarkdown(codeblock);
},
updatemarkdown: function(codeblock)
{
this.$emit('updatedMarkdown', codeblock);
},
},
})

View File

@ -1,87 +0,0 @@
<?php
namespace Plugins\search;
use \Typemill\Plugin;
use \Typemill\Models\Write;
use \Typemill\Models\WriteCache;
class Index extends Plugin
{
public static function getSubscribedEvents(){}
public function index()
{
$write = new Write();
$index = $write->getFile('cache', 'index.json');
if(!$index)
{
$this->createIndex();
$index = $write->getFile('cache', 'index.json');
}
return $this->returnJson($index);
}
private function createIndex()
{
$write = new WriteCache();
# get content structure
$structure = $write->getCache('cache', 'structure.txt');
# get data for search-index
$index = $this->getAllContent($structure, $write);
# store the index file here
$write->writeFile('cache', 'index.json', json_encode($index, JSON_UNESCAPED_SLASHES));
}
private function getAllContent($structure, $write, $index = NULL)
{
foreach($structure as $item)
{
if($item->elementType == "folder")
{
if($item->fileType == 'md')
{
$page = $write->getFileWithPath('content' . $item->path . DIRECTORY_SEPARATOR . 'index.md');
$pageArray = $this->getPageContentArray($page, $item->urlAbs);
$index[$pageArray['url']] = $pageArray;
}
$index = $this->getAllContent($item->folderContent, $write, $index);
}
else
{
$page = $write->getFileWithPath('content' . $item->path);
$pageArray = $this->getPageContentArray($page, $item->urlAbs);
$index[$pageArray['url']] = $pageArray;
}
}
return $index;
}
private function getPageContentArray($page, $url)
{
$parts = explode("\n", $page, 2);
# get the title / headline
$title = trim($parts[0], '# ');
$title = str_replace(["\r\n", "\n", "\r"],' ', $title);
# get and cleanup the content
$content = $parts[1];
$content = strip_tags($content);
$content = str_replace(["\r\n", "\n", "\r"],' ', $content);
$pageContent = [
'title' => $title,
'content' => $content,
'url' => $url
];
return $pageContent;
}
}

View File

@ -1,19 +0,0 @@
Copyright (C) 2013 by Oliver Nightingale
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.

File diff suppressed because one or more lines are too long

View File

@ -1,64 +0,0 @@
.searchContainer{
overflow: hidden;
width: 100%;
vertical-align: middle;
white-space: nowrap;
}
.searchContainer input{
width: 100%;
height: 50px;
border: 1px solid #ddd;
font-size: 1rem;
float: left;
padding-left: 15px;
border-radius: 2px;
box-sizing:border-box;
}
.searchContainer button{
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border: none;
background: #232833;
height: 50px;
width: 50px;
color: #fff;
font-size: 10pt;
margin-left: -50px;
}
.searchContainer button:hover,.searchContainer button:focus, .searchContainer button:active{
cursor: pointer;
}
#searchresult{
}
.resultwrapper{
}
button#closeSearchResult{
position: absolute;
right: 0px;
top: 0px;
margin: 10px;
border: none;
border-radius: 2px;
font-size: 1rem;
color: #fff;
background: #000;
padding: 15px;
}
button#closeSearchResult:hover,#closeSearchResult:focus{
cursor: pointer;
}
.resultlist{
margin: 0px;
padding: 0px;
list-style:none;
}
.resultitem{
}
.resultheader{
}
.resultsnippet{
}

View File

@ -1,115 +0,0 @@
var searchField = document.getElementById("searchField");
var searchButton = document.getElementById("searchButton");
if(searchField && searchButton)
{
var searchIndex = false;
var documents = false;
var holdcontent = false;
var contentwrapper = false;
searchField.addEventListener("focus", function(event){
if(!searchIndex)
{
myaxios.get('/indexrs51gfe2o2')
.then(function (response) {
documents = JSON.parse(response.data);
searchIndex = lunr(function() {
this.ref("id");
this.field("title", { boost: 10 });
this.field("content");
for (var key in documents){
this.add({
"id": documents[key].url,
"title": documents[key].title,
"content": documents[key].content
});
}
});
})
.catch(function (error) {});
}
});
searchButton.addEventListener("click", function(event){
event.preventDefault();
var term = document.getElementById('searchField').value;
var results = searchIndex.search(term);
var resultPages = results.map(function (match) {
return documents[match.ref];
});
resultsString = "<div class='resultwrapper'><h1>Result for " + term + "</h1>";
resultsString += "<button id='closeSearchResult'>close</button>";
resultsString += "<ul class='resultlist'>";
resultPages.forEach(function (r) {
resultsString += "<li class='resultitem'>";
resultsString += "<a class='resultheader' href='" + r.url + "?q=" + term + "'><h3>" + r.title + "</h3></a>";
resultsString += "<div class='resultsnippet'>" + r.content.substring(0, 200) + " ...</div>";
resultsString += "</li>"
});
resultsString += "</ul></div>";
if(!holdcontent)
{
contentwrapper = document.getElementById("searchresult").parentNode;
holdcontent = contentwrapper.innerHTML;
}
contentwrapper.innerHTML = resultsString;
document.getElementById("closeSearchResult").addEventListener("click", function(event){
contentwrapper.innerHTML = holdcontent;
});
}, false);
}
/*
var searchIndex = lunr(function() {
this.ref("id");
this.field("title", { boost: 10 });
this.field("content");
for (var key in window.pages) {
this.add({
"id": key,
"title": pages[key].title,
"content": pages[key].content
});
}
});
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if (pair[0] === variable) {
return decodeURIComponent(pair[1].replace(/\+/g, "%20"));
}
}
}
var searchTerm = getQueryVariable("q");
// creation of searchIndex from earlier example
var results = searchIndex.search(searchTerm);
var resultPages = results.map(function (match) {
return pages[match.ref];
});
// resultPages from previous example
resultsString = "";
resultPages.forEach(function (r) {
resultsString += "<li>";
resultsString += "<a class='result' href='" + r.url + "?q=" + searchTerm + "'><h3>" + r.title + "</h3></a>";
resultsString += "<div class='snippet'>" + r.content.substring(0, 200) + "</div>";
resultsString += "</li>"
});
document.querySelector("#search-results").innerHTML = resultsString;
*/

View File

@ -1,128 +0,0 @@
<?php
namespace Plugins\search;
use \Typemill\Plugin;
use \Typemill\Models\Write;
class Search extends index
{
protected $item;
public static function getSubscribedEvents()
{
return array(
'onSettingsLoaded' => 'onsettingsLoaded',
'onContentArrayLoaded' => 'onContentArrayLoaded',
'onPageReady' => 'onPageReady',
'onPagePublished' => 'onPagePublished',
'onPageUnpublished' => 'onPageUnpublished',
'onPageSorted' => 'onPageSorted',
'onPageDeleted' => 'onPageDeleted',
);
}
# get search.json with route
# update search.json on publish
public static function addNewRoutes()
{
# the route for the api calls
return array(
array(
'httpMethod' => 'get',
'route' => '/indexrs51gfe2o2',
'class' => 'Plugins\search\index:index'
),
);
}
public function onSettingsLoaded($settings)
{
$this->settings = $settings->getData();
}
# at any of theses events, delete the old search index
public function onPagePublished($item)
{
$this->deleteSearchIndex();
}
public function onPageUnpublished($item)
{
$this->deleteSearchIndex();
}
public function onPageSorted($inputParams)
{
$this->deleteSearchIndex();
}
public function onPageDeleted($item)
{
$this->deleteSearchIndex();
}
private function deleteSearchIndex()
{
$write = new Write();
# store the index file here
$write->deleteFileWithPath('cache' . DIRECTORY_SEPARATOR . 'index.json');
}
public function onContentArrayLoaded($contentArray)
{
# get content array
$content = $contentArray->getData();
$settings = $this->getPluginSettings('search');
$salt = "asPx9Derf2";
# activate axios and vue in frontend
$this->activateAxios();
$this->activateVue();
# add the css and vue application
$this->addCSS('/search/public/search.css');
$this->addJS('/search/public/lunr.min.js');
$this->addJS('/search/public/search.js');
# simple security for first request
$secret = time();
$secret = substr($secret,0,-1);
$secret = md5($secret . $salt);
# simple csrf protection with a session for long following requests
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$length = 32;
$token = substr(base_convert(sha1(uniqid(mt_rand())), 16, 36), 0, $length);
$_SESSION['search'] = $token;
$_SESSION['search-expire'] = time() + 1300; # 60 seconds * 30 minutes
# create div for vue app
$search = '<div data-access="' . $secret . '" data-token="' . $token . '" id="searchresult"></div>';
# create content type
$search = Array
(
'rawHtml' => $search,
'allowRawHtmlInSafeMode' => true,
'autobreak' => 1
);
$content[] = $search;
$contentArray->setData($content);
}
public function onPageReady($page)
{
$pageData = $page->getData($page);
$pageData['widgets']['search'] = '<div class="searchContainer" id="searchForm">'.
'<input id="searchField" type="text" placeholder="search ..." />'.
'<button id="searchButton" type="button">GO</button>'.
'</div>';
$page->setData($pageData);
}
}

View File

@ -1,6 +0,0 @@
name: Search
version: 1.0.0
description: Adds a search to typemill with lunr.js.
author: Sebastian Schürmanns
homepage: https://typemill.net
licence: MIT

View File

@ -17,7 +17,7 @@ class Assets
$this->editorInlineJS = array();
$this->svgSymbols = array();
}
public function addCSS($CSS)
{
$CSSfile = $this->getFileUrl($CSS);

View File

@ -6,6 +6,7 @@ use Slim\Http\Request;
use Slim\Http\Response;
use Typemill\Models\Folder;
use Typemill\Models\Write;
use Typemill\Models\WriteYaml;
use Typemill\Models\ProcessImage;
use Typemill\Extensions\ParsedownExtension;
use Typemill\Events\OnPagePublished;
@ -220,7 +221,7 @@ class ContentApiController extends ContentController
if($this->item->elementType == 'file')
{
$delete = $this->deleteContentFiles(['md','txt']);
$delete = $this->deleteContentFiles(['md','txt', 'yaml']);
}
elseif($this->item->elementType == 'folder')
{
@ -323,13 +324,19 @@ class ContentApiController extends ContentController
$parentKeyFrom = explode('.', $this->params['parent_id_from']);
$parentKeyTo = explode('.', $this->params['parent_id_to']);
/*
echo '<pre>';
print_r(array($itemKeyPath 0,$parentKeyFrom navi,$parentKeyTo 2));
die();
*/
# get the item from structure
$item = Folder::getItemWithKeyPath($this->structure, $itemKeyPath);
if(!$item){ return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not find this page. Please refresh and try again.', 'url' => $url), 404); }
# if a folder is moved on the first level
if($this->params['parent_id_from'] == 'navi')
# if an item is moved to the first level
if($this->params['parent_id_to'] == 'navi')
{
# create empty and default values so that the logic below still works
$newFolder = new \stdClass();
@ -388,7 +395,7 @@ class ContentApiController extends ContentController
# get item for url and set it active again
if(isset($this->params['url']))
{
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url']);
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
}
# keep the internal structure for response
@ -434,7 +441,7 @@ class ContentApiController extends ContentController
$nameParts = Folder::getStringParts($this->params['item_name']);
$name = implode("-", $nameParts);
$slug = $name;
# initialize index
$index = 0;
@ -465,8 +472,10 @@ class ContentApiController extends ContentController
$namePath = $index > 9 ? $index . '-' . $name : '0' . $index . '-' . $name;
$folderPath = 'content' . $folder->path;
$title = implode(" ", $nameParts);
# create default content
$content = json_encode(['# Add Title', 'Add Content']);
$content = json_encode(['# ' . $title, 'Content']);
if($this->params['type'] == 'file')
{
@ -490,7 +499,7 @@ class ContentApiController extends ContentController
# get item for url and set it active again
if(isset($this->params['url']))
{
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url']);
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
}
# activate this if you want to redirect after creating the page...
@ -499,7 +508,7 @@ class ContentApiController extends ContentController
return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url));
}
public function createBaseFolder(Request $request, Response $response, $args)
public function createBaseItem(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
@ -512,7 +521,7 @@ class ContentApiController extends ContentController
if(!$this->setStructure($draft = true)){ return $response->withJson(array('data' => false, 'errors' => $this->errors, 'url' => $url), 404); }
# validate input
#if(!$this->validateBaseFolder()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 20 chars.', 'url' => $url), 422); }
if(!$this->validateBaseNaviItem()){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Special Characters not allowed. Length between 1 and 20 chars.', 'url' => $url), 422); }
# create the name for the new item
$nameParts = Folder::getStringParts($this->params['item_name']);
@ -527,16 +536,16 @@ class ContentApiController extends ContentController
# iterate through the whole content of the new folder
$writeError = false;
foreach($this->structure as $folder)
foreach($this->structure as $item)
{
# check, if the same name as new item, then return an error
if($folder->slug == $slug)
if($item->slug == $slug)
{
return $response->withJson(array('data' => $this->structure, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 404);
}
if(!$write->moveElement($folder, '', $index))
if(!$write->moveElement($item, '', $index))
{
$writeError = true;
}
@ -549,23 +558,32 @@ class ContentApiController extends ContentController
$namePath = $index > 9 ? $index . '-' . $name : '0' . $index . '-' . $name;
$folderPath = 'content';
if(!$write->checkPath($folderPath . DIRECTORY_SEPARATOR . $namePath))
{
return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the folder. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404);
}
# create default content
$content = json_encode(['# Add Title', 'Add Content']);
$write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content);
if($this->params['type'] == 'file')
{
if(!$write->writeFile($folderPath, $namePath . '.txt', $content))
{
return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the file. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404);
}
}
elseif($this->params['type'] == 'folder')
{
if(!$write->checkPath($folderPath . DIRECTORY_SEPARATOR . $namePath))
{
return $response->withJson(array('data' => $this->structure, 'errors' => 'We could not create the folder. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404);
}
$write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content);
}
# update the structure for editor
$this->setStructure($draft = true, $cache = false);
# get item for url and set it active again
if(isset($this->params['url']))
{
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url']);
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
}
return $response->withJson(array('data' => $this->structure, 'errors' => false, 'url' => $url));
@ -586,7 +604,7 @@ class ContentApiController extends ContentController
# get item for url and set it active again
if(isset($this->params['url']))
{
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url']);
$activeItem = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBaseUrl());
}
return $response->withJson(array('data' => $this->structure, 'homepage' => $this->homepage, 'errors' => false));

View File

@ -92,6 +92,11 @@ abstract class ContentController
return $this->c->view->render($response->withStatus(404), '/intern404.twig', $data);
}
protected function getValidator()
{
return new Validation();
}
protected function validateEditorInput()
{
$validate = new Validation();
@ -151,6 +156,21 @@ abstract class ContentController
}
return true;
}
protected function validateBaseNaviItem()
{
$validate = new Validation();
$vResult = $validate->navigationBaseItem($this->params);
if(is_array($vResult))
{
$message = reset($vResult);
$this->errors = ['errors' => $vResult];
if(isset($message[0])){ $this->errors['errors']['message'] = $message[0]; }
return false;
}
return true;
}
protected function setStructure($draft = false, $cache = true)
{
@ -221,35 +241,11 @@ abstract class ContentController
protected function setItem()
{
# if it is the homepage
if($this->params['url'] == $this->uri->getBasePath() OR $this->params['url'] == '/')
{
$item = new \stdClass;
$item->elementType = 'folder';
$item->path = '';
$item->urlRel = '/';
}
else
{
# search for the url in the structure
$item = Folder::getItemForUrl($this->structure, $this->params['url']);
}
# search for the url in the structure
$item = Folder::getItemForUrl($this->structure, $this->params['url'], $this->uri->getBasePath());
if($item)
{
if($item->elementType == 'file')
{
$pathParts = explode('.', $item->path);
$fileType = array_pop($pathParts);
$pathWithoutType = implode('.', $pathParts);
$item->pathWithoutType = $pathWithoutType;
}
elseif($item->elementType == 'folder')
{
$item->pathWithoutItem = $item->path;
$item->path = $item->path . DIRECTORY_SEPARATOR . 'index';
$item->pathWithoutType = $item->path;
}
$this->item = $item;
return true;
}
@ -315,7 +311,7 @@ abstract class ContentController
protected function deleteContentFolder()
{
$basePath = $this->settings['rootPath'] . $this->settings['contentFolder'];
$path = $basePath . $this->item->pathWithoutItem;
$path = $basePath . $this->item->path;
if(file_exists($path))
{

View File

@ -0,0 +1,158 @@
<?php
namespace Typemill\Controllers;
use Slim\Http\Request;
use Slim\Http\Response;
use Typemill\Models\WriteYaml;
class MetaApiController extends ContentController
{
# get the standard meta-definitions and the meta-definitions from plugins (same for all sites)
public function getMetaDefinitions(Request $request, Response $response, $args)
{
$metatabs = $this->aggregateMetaDefinitions();
return $response->withJson(array('definitions' => $metatabs, 'errors' => false));
}
# get the standard meta-definitions and the meta-definitions from plugins (same for all sites)
public function aggregateMetaDefinitions()
{
$writeYaml = new writeYaml();
$metatabs = $writeYaml->getYaml('system' . DIRECTORY_SEPARATOR . 'author', 'metatabs.yaml');
# load cached metadefinitions
# check if valid
# if not, refresh cache
# loop through all plugins
foreach($this->settings['plugins'] as $name => $plugin)
{
$pluginSettings = \Typemill\Settings::getObjectSettings('plugins', $name);
if($pluginSettings && isset($pluginSettings['metatabs']))
{
$metatabs = array_merge_recursive($metatabs, $pluginSettings['metatabs']);
}
}
return $metatabs;
}
public function getArticleMetaObject(Request $request, Response $response, $args)
{
/* get params from call */
$this->params = $request->getParams();
$this->uri = $request->getUri();
# set structure
if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
# set item
if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
$writeYaml = new writeYaml();
$pagemeta = $writeYaml->getPageMeta($this->settings, $this->item);
if(!$pagemeta)
{
# set the status for published and drafted
$this->setPublishStatus();
# set path
$this->setItemPath($this->item->fileType);
# read content from file
if(!$this->setContent()){ return $response->withJson(array('data' => false, 'errors' => $this->errors), 404); }
$pagemeta = $writeYaml->getPageMetaDefaults($this->content, $this->settings, $this->item);
}
# get global metadefinitions
$metadefinitions = $this->aggregateMetaDefinitions();
$metadata = [];
foreach($metadefinitions as $tabname => $tab )
{
$metadata[$tabname] = [];
foreach($tab['fields'] as $fieldname => $fielddefinitions)
{
$metadata[$tabname][$fieldname] = isset($pagemeta[$tabname][$fieldname]) ? $pagemeta[$tabname][$fieldname] : null;
}
}
return $response->withJson(array('metadata' => $metadata, 'metadefinitions' => $metadefinitions, 'errors' => false));
}
public function updateArticleMeta(Request $request, Response $response, $args)
{
/* get params from call */
$this->params = $request->getParams();
$this->uri = $request->getUri();
$tab = isset($this->params['tab']) ? $this->params['tab'] : false;
$metaData = isset($this->params['data']) ? $this->params['data'] : false ;
$objectName = 'meta';
$errors = false;
if(!$tab or !$metaData)
{
return $response->withJson($this->errors, 404);
}
# load metadefinitions
$metaDefinitions = $this->aggregateMetaDefinitions();
# create validation object
$validate = $this->getValidator();
# take the user input data and iterate over all fields and values
foreach($metaData as $fieldName => $fieldValue)
{
# get the corresponding field definition from original plugin settings */
$fieldDefinition = isset($metaDefinitions[$tab]['fields'][$fieldName]) ? $metaDefinitions[$tab]['fields'][$fieldName] : false;
if(!$fieldDefinition)
{
$errors[$tab][$fieldName] = 'This field is not defined';
}
else
{
# validate user input for this field
$result = $validate->objectField($fieldName, $fieldValue, $objectName, $fieldDefinition);
if($result !== true)
{
$errors[$tab][$fieldName] = $result[$fieldName][0];
}
}
}
# return validation errors
if($errors){ return $response->withJson(array('errors' => $errors),422); }
# set structure
if(!$this->setStructure($draft = true)){ return $response->withJson($this->errors, 404); }
# set item
if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
$writeYaml = new writeYaml();
# get existing metadata for page
$meta = $writeYaml->getYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml');
# add the new/edited metadata
$meta[$tab] = $metaData;
# store the metadata
$writeYaml->updateYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml', $meta);
# return with the new metadata
return $response->withJson(array('metadata' => $metaData, 'errors' => false));
}
}

View File

@ -14,6 +14,7 @@ use Typemill\Events\OnPagetreeLoaded;
use Typemill\Events\OnBreadcrumbLoaded;
use Typemill\Events\OnItemLoaded;
use Typemill\Events\OnOriginalLoaded;
use Typemill\Events\OnMetaLoaded;
use Typemill\Events\OnMarkdownLoaded;
use Typemill\Events\OnContentArrayLoaded;
use Typemill\Events\OnHtmlLoaded;
@ -27,6 +28,7 @@ class PageController extends Controller
$structure = false;
$contentHTML = false;
$item = false;
$home = false;
$breadcrumb = false;
$description = '';
$settings = $this->c->get('settings');
@ -34,7 +36,7 @@ class PageController extends Controller
$cache = new WriteCache();
$uri = $request->getUri();
$base_url = $uri->getBaseUrl();
try
{
/* if the cached structure is still valid, use it */
@ -74,56 +76,68 @@ class PageController extends Controller
exit(1);
}
/* if the user is on startpage */
# if the user is on startpage
if(empty($args))
{
/* check, if there is an index-file in the root of the content folder */
$contentMD = file_exists($pathToContent . DIRECTORY_SEPARATOR . 'index.md') ? file_get_contents($pathToContent . DIRECTORY_SEPARATOR . 'index.md') : NULL;
{
$home = true;
$item = Folder::getItemForUrl($structure, $uri->getBasePath(), $uri->getBasePath());
}
else
{
/* get the request url */
$urlRel = $uri->getBasePath() . '/' . $args['params'];
$urlRel = $uri->getBasePath() . '/' . $args['params'];
/* find the url in the content-item-tree and return the item-object for the file */
$item = Folder::getItemForUrl($structure, $urlRel);
$item = Folder::getItemForUrl($structure, $urlRel, $uri->getBasePath());
/* if there is still no item, return a 404-page */
if(!$item)
{
return $this->render404($response, array( 'navigation' => $structure, 'settings' => $settings, 'base_url' => $base_url ));
}
/* get breadcrumb for page */
$breadcrumb = Folder::getBreadcrumb($structure, $item->keyPathArray);
$breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData();
/* add the paging to the item */
$item = Folder::getPagingForItem($structure, $item);
$item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData();
/* check if url is a folder. If so, check if there is an index-file in that folder */
if($item->elementType == 'folder')
{
$filePath = $pathToContent . $item->path . DIRECTORY_SEPARATOR . 'index.md';
}
elseif($item->elementType == 'file')
{
$filePath = $pathToContent . $item->path;
}
/* add the modified date for the file */
$item->modified = isset($filePath) ? filemtime($filePath) : false;
/* read the content of the file */
$contentMD = isset($filePath) ? file_get_contents($filePath) : false;
}
# dispatch the item
$item = $this->c->dispatcher->dispatch('onItemLoaded', new OnItemLoaded($item))->getData();
# set the filepath
$filePath = $pathToContent . $item->path;
# check if url is a folder and add index.md
if($item->elementType == 'folder')
{
$filePath = $filePath . DIRECTORY_SEPARATOR . 'index.md';
}
# read the content of the file
$contentMD = file_exists($filePath) ? file_get_contents($filePath) : false;
# dispatch the original content without plugin-manipulations for case anyone wants to use it
$this->c->dispatcher->dispatch('onOriginalLoaded', new OnOriginalLoaded($contentMD));
$contentMD = $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
# get meta-Information
$writeYaml = new WriteYaml();
$metatabs = $writeYaml->getPageMeta($settings, $item);
if(!$metatabs)
{
$metatabs = $writeYaml->getPageMetaDefaults($contentMD, $settings, $item);
}
# dispatch meta
$metatabs = $this->c->dispatcher->dispatch('onMetaLoaded', new OnMetaLoaded($metatabs))->getData();
# dispatch content
$contentMD = $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
/* initialize parsedown */
$parsedown = new ParsedownExtension();
@ -148,18 +162,23 @@ class PageController extends Controller
$title = isset($contentParts[0]) ? strip_tags($contentParts[0]) : $settings['title'];
$contentHTML = isset($contentParts[1]) ? $contentParts[1] : $contentHTML;
/* create excerpt from content */
$excerpt = substr($contentHTML,0,500);
/* create description from excerpt */
$description = isset($excerpt) ? strip_tags($excerpt) : false;
if($description)
# if there is not meta description
if(!isset($metatabs['meta']['description']) or !$metatabs['meta']['description'])
{
$description = trim(preg_replace('/\s+/', ' ', $description));
$description = substr($description, 0, 300);
$lastSpace = strrpos($description, ' ');
$description = substr($description, 0, $lastSpace);
# create excerpt from html
$excerpt = substr($contentHTML,0,500);
# create description from excerpt
$description = isset($excerpt) ? strip_tags($excerpt) : false;
if($description)
{
$description = trim(preg_replace('/\s+/', ' ', $description));
$description = substr($description, 0, 300);
$lastSpace = strrpos($description, ' ');
$metatabs['meta']['description'] = substr($description, 0, $lastSpace);
}
}
/* get url and alt-tag for first image, if exists */
@ -174,20 +193,18 @@ class PageController extends Controller
}
}
$home = empty($args) ? true : false;
$theme = $settings['theme'];
$route = empty($args) && isset($settings['themes'][$theme]['cover']) ? '/cover.twig' : '/index.twig';
return $this->render($response, $route, [
'home' => $home,
'navigation' => $structure,
'title' => $title,
'content' => $contentHTML,
'item' => $item,
'breadcrumb' => $breadcrumb,
'settings' => $settings,
'title' => $title,
'description' => $description,
'settings' => $settings,
'metatabs' => $metatabs,
'base_url' => $base_url,
'image' => $firstImage ]);
}

View File

@ -0,0 +1,14 @@
<?php
namespace Typemill\Events;
use Symfony\Component\EventDispatcher\Event;
/**
* Event for markdown.
*/
class OnMetaLoaded extends BaseEvent
{
}

View File

@ -0,0 +1,24 @@
<?php
namespace Typemill\Extensions;
use Typemill\Models\WriteYaml;
class TwigMetaExtension extends \Twig_Extension
{
public function getFunctions()
{
return [
new \Twig_SimpleFunction('getPageMeta', array($this, 'getMeta' ))
];
}
public function getMeta($settings, $item)
{
$write = new WriteYaml();
$meta = $write->getPageMeta($settings, $item);
return $meta;
}
}

View File

@ -106,18 +106,18 @@ class Folder
$fileType = '';
if(in_array('index.md', $name))
{
$fileType = 'md';
$status = 'published';
$fileType = 'md';
$status = 'published';
}
if(in_array('index.txt', $name))
{
$fileType = 'txt';
$status = 'unpublished';
$fileType = 'txt';
$status = 'unpublished';
}
if(in_array('index.txtmd', $name))
{
$fileType = 'txt';
$status = 'modified';
$fileType = 'txt';
$status = 'modified';
}
$item->originalName = $key;
@ -130,6 +130,7 @@ class Folder
$item->slug = implode("-",$nameParts);
$item->slug = URLify::filter(iconv(mb_detect_encoding($item->slug, mb_detect_order(), true), "UTF-8", $item->slug));
$item->path = $fullPath . DIRECTORY_SEPARATOR . $key;
$item->pathWithoutType = $fullPath . DIRECTORY_SEPARATOR . $key . DIRECTORY_SEPARATOR . 'index';
$item->urlRelWoF = $fullSlugWithoutFolder . '/' . $item->slug;
$item->urlRel = $fullSlugWithFolder . '/' . $item->slug;
$item->urlAbs = $baseUrl . $fullSlugWithoutFolder . '/' . $item->slug;
@ -139,20 +140,21 @@ class Folder
$item->chapter = $chapter ? $chapter . '.' . $chapternr : $chapternr;
$item->active = false;
$item->activeParent = false;
$item->folderContent = self::getFolderContentDetails($name, $baseUrl, $item->urlRel, $item->urlRelWoF, $item->path, $item->keyPath, $item->chapter);
}
else
{
# do not use files in base folder (only folders are allowed)
if(!isset($keyPath)) continue;
# if(!isset($keyPath)) continue;
# do not use index files
if($name == 'index.md' || $name == 'index.txt' || $name == 'index.txtmd' ) continue;
$nameParts = self::getStringParts($name);
$fileType = array_pop($nameParts);
$nameWithoutType = self::getNameWithoutType($name);
if($fileType == 'md')
{
$status = 'published';
@ -177,8 +179,9 @@ class Folder
$item->slug = implode("-",$nameParts);
$item->slug = URLify::filter(iconv(mb_detect_encoding($item->slug, mb_detect_order(), true), "UTF-8", $item->slug));
$item->path = $fullPath . DIRECTORY_SEPARATOR . $name;
$item->pathWithoutType = $fullPath . DIRECTORY_SEPARATOR . $nameWithoutType;
$item->key = $iteration;
$item->keyPath = $keyPath . '.' . $iteration;
$item->keyPath = isset($keyPath) ? $keyPath . '.' . $iteration : $iteration;
$item->keyPathArray = explode('.',$item->keyPath);
$item->chapter = $chapter . '.' . $chapternr;
$item->urlRelWoF = $fullSlugWithoutFolder . '/' . $item->slug;
@ -187,6 +190,7 @@ class Folder
$item->active = false;
$item->activeParent = false;
}
$iteration++;
$chapternr++;
$contentDetails[] = $item;
@ -194,8 +198,22 @@ class Folder
return $contentDetails;
}
public static function getItemForUrl($folderContentDetails, $url, $result = NULL)
public static function getItemForUrl($folderContentDetails, $url, $baseUrl, $result = NULL)
{
# if we are on the homepage
if($url == '/' OR $url == $baseUrl)
{
# return a standard item-object
$item = new \stdClass;
$item->elementType = 'folder';
$item->path = '';
$item->urlRel = '/';
$item->pathWithoutType = DIRECTORY_SEPARATOR . 'index';
return $item;
}
foreach($folderContentDetails as $key => $item)
{
if($item->urlRel === $url)
@ -206,7 +224,7 @@ class Folder
}
elseif($item->elementType === "folder")
{
$result = self::getItemForUrl($item->folderContent, $url, $result);
$result = self::getItemForUrl($item->folderContent, $url, $baseUrl, $result);
}
}
return $result;
@ -357,6 +375,7 @@ class Folder
/* get breadcrumb as copied array, set elements active in original and mark parent element in original */
public static function getBreadcrumb($content, $searchArray, $i = NULL, $breadcrumb = NULL)
{
# if it is the first round, create an empty array
if(!$i){ $i = 0; $breadcrumb = array();}
while($i < count($searchArray))
@ -431,4 +450,10 @@ class Folder
$parts = preg_split('/\./',$fileName);
return $parts;
}
public static function getNameWithoutType($fileName)
{
$parts = preg_split('/\./',$fileName);
return $parts[0];
}
}

View File

@ -291,6 +291,25 @@ class Validation
return $v->errors();
}
}
public function navigationBaseItem(array $params)
{
$v = new Validator($params);
$v->rule('required', ['item_name', 'type', 'url']);
$v->rule('noSpecialChars', 'item_name');
$v->rule('lengthBetween', 'item_name', 1, 40);
$v->rule('in', 'type', ['file', 'folder']);
if($v->validate())
{
return true;
}
else
{
return $v->errors();
}
}
/**
* validation for dynamic fields ( settings for themes and plugins)
@ -387,7 +406,11 @@ class Validation
}
else
{
if($name)
if($name == 'meta')
{
return $v->errors();
}
elseif($name)
{
if(isset($_SESSION['errors'][$name]))
{

View File

@ -112,7 +112,7 @@ class Write
public function moveElement($item, $folderPath, $index)
{
$filetypes = array('md', 'txt');
$filetypes = array('md', 'txt', 'yaml');
# set new order as string
$newOrder = ($index < 10) ? '0' . $index : $index;

View File

@ -2,6 +2,8 @@
namespace Typemill\Models;
use Typemill\Extensions\ParsedownExtension;
class WriteYaml extends Write
{
/**
@ -35,4 +37,85 @@ class WriteYaml extends Write
}
return false;
}
# used by contentApiController (backend) and pageController (frontend)
public function getPageMeta($settings, $item)
{
$meta = $this->getYaml($settings['contentFolder'], $item->pathWithoutType . '.yaml');
if(!$meta)
{
return false;
}
$meta = $this->addFileTimeToMeta($meta, $item, $settings);
return $meta;
}
# used by contentApiController (backend) and pageController (frontend)
public function getPageMetaDefaults($content, $settings, $item)
{
# initialize parsedown extension
$parsedown = new ParsedownExtension();
# if content is not an array, then transform it
if(!is_array($content))
{
# turn markdown into an array of markdown-blocks
$content = $parsedown->markdownToArrayBlocks($content);
}
$title = false;
# delete markdown from title
if(isset($content[0]))
{
$title = trim($content[0], "# ");
}
$description = false;
# delete markdown from title
if(isset($content[1]))
{
$firstLineArray = $parsedown->text($content[1]);
$description = strip_tags($parsedown->markup($firstLineArray, $item->urlAbs));
$description = substr($description, 0, 300);
$lastSpace = strrpos($description, ' ');
$description = substr($description, 0, $lastSpace);
}
# create new meta-file
$meta = [
'meta' => [
'title' => $title,
'description' => $description,
'author' => $settings['author'], # change to session, extend userdata
]
];
$this->updateYaml($settings['contentFolder'], $item->pathWithoutType . '.yaml', $meta);
$meta = $this->addFileTimeToMeta($meta, $item, $settings);
return $meta;
}
private function addFileTimeToMeta($meta, $item, $settings)
{
$filePath = $settings['contentFolder'] . $item->path;
$fileType = isset($item->fileType) ? $item->fileType : 'md';
# check if url is a folder.
if($item->elementType == 'folder')
{
$filePath = $settings['contentFolder'] . $item->path . DIRECTORY_SEPARATOR . 'index.'. $fileType;
}
# add the modified date for the file
$meta['meta']['modified'] = file_exists($filePath) ? date("Y-m-d",filemtime($filePath)) : false;
return $meta;
}
}

View File

@ -3,6 +3,7 @@
use Typemill\Controllers\SettingsController;
use Typemill\Controllers\ContentController;
use Typemill\Controllers\ContentApiController;
use Typemill\Controllers\MetaApiController;
use Typemill\Middleware\RestrictApiAccess;
$app->get('/api/v1/themes', SettingsController::class . ':getThemeSettings')->setName('api.themes')->add(new RestrictApiAccess($container['router']));
@ -16,8 +17,12 @@ $app->post('/api/v1/article', ContentApiController::class . ':createArticle')->s
$app->put('/api/v1/article', ContentApiController::class . ':updateArticle')->setName('api.article.update')->add(new RestrictApiAccess($container['router']));
$app->delete('/api/v1/article', ContentApiController::class . ':deleteArticle')->setName('api.article.delete')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/article/sort', ContentApiController::class . ':sortArticle')->setName('api.article.sort')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/basefolder', ContentApiController::class . ':createBaseFolder')->setName('api.basefolder.create')->add(new RestrictApiAccess($container['router']));
$app->get('/api/v1/article/metaobject', MetaApiController::class . ':getArticleMetaobject')->setName('api.articlemetaobject.get')->add(new RestrictApiAccess($container['router']));
$app->get('/api/v1/article/metadata', MetaApiController::class . ':getArticleMeta')->setName('api.articlemeta.get')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/article/metadata', MetaApiController::class . ':updateArticleMeta')->setName('api.articlemeta.update')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/baseitem', ContentApiController::class . ':createBaseItem')->setName('api.baseitem.create')->add(new RestrictApiAccess($container['router']));
$app->get('/api/v1/navigation', ContentApiController::class . ':getNavigation')->setName('api.navigation.get')->add(new RestrictApiAccess($container['router']));
$app->get('/api/v1/metadefinitions', MetaApiController::class . ':getMetaDefinitions')->setName('api.metadefinitions.get')->add(new RestrictApiAccess($container['router']));
$app->post('/api/v1/block', ContentApiController::class . ':addBlock')->setName('api.block.add')->add(new RestrictApiAccess($container['router']));
$app->put('/api/v1/block', ContentApiController::class . ':updateBlock')->setName('api.block.update')->add(new RestrictApiAccess($container['router']));

View File

@ -80,7 +80,7 @@ class Settings
return $objectSettings;
}
public static function createSettings()
{
$yaml = new Models\WriteYaml();

View File

@ -11,7 +11,7 @@
<h1>Hurra!</h1>
<p>Your account has been created and you are logged in now.</p>
<p><strong>Next step:</strong> Visit the author panel and setup your new website. You can configure the system, choose themes and add plugins.</p>
<p><strong>New:</strong> You never use code-examples on your pages? Then disable the code-button and adjust the whole format-bar of the editor exactly to your needs.</p>
<p><strong>New:</strong>Hurra! Version 1.3.0 is out and now you can edit meta-information like title and description.</p>
<p><strong>Get help:</strong> If you have any questions, please consult the <a target="_blank" href="https://typemill.net/typemill"><i class="icon-link-ext"></i> docs</a> or open a new issue on <a target="_blank" href="https://github.com/typemill/typemill"><i class="icon-link-ext"></i> github</a>.</p>
</div>
<a class="button" href="{{ path_for('settings.show') }}">Configure your website</a>

View File

@ -2,7 +2,7 @@
* TRANSITION *
**********************/
a, a:link, a:visited, a:focus, a:hover, a:active, button, .button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{
a, a:link, a:visited, a:focus, a:hover, a:active, button, .button, .tab-button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{
-webkit-transition: color 0.2s ease;
-moz-transition: color 0.2s ease;
-o-transition: color 0.2s ease;
@ -283,6 +283,9 @@ span.level-3{ padding-left: 50px; }
span.level-4{ padding-left: 60px; }
span.level-5{ padding-left: 70px; }
.addBaseItem{
margin-left: -10px;
}
.addNaviItem{
padding: 5px;
display: block;
@ -630,7 +633,41 @@ header.headline
{
padding: 0px 0px;
}
header a.button{
float: right;
margin: 1.5em 0.5em 0 0;
}
/********************
* Meta-Tabs *
********************/
.metanav{
position: absolute;
width: 100%;
margin-top: -35px;
z-index: 1;
}
.tab-button{
font-size: 0.9em;
background: transparent;
padding: 10px 10px;
margin: 0px 5px 0px 0px;
min-width: 120px;
border: 0px;
}
.tab-button:focus,
.tab-button:hover,
.tab-button:active,
.tab-button.active{
background: #fff;
box-shadow: -2px -2px 4px #eee;
outline: none;
}
section.tab{
z-index: -1;
padding: 40px;
}
/********************
* Forms *
********************/
@ -972,7 +1009,7 @@ ul.cardInfo{
.error p{
color: #e0474c;
}
.errors .cardHead header{
.errors .cardHead header{
background: #e0474c;
color: #fff;
}
@ -985,6 +1022,25 @@ span.error{
margin-top: -8px;
margin-bottom: 8px;
}
.metaLarge{
box-sizing: border-box;
width: 100%;
padding: 18px 20px 0px;
}
.metaErrors,.metaSuccess{
width: 100%;
padding: 4px 18px;
color: #fff;
border-radius: 1px;
text-align:center;
box-sizing: border-box;
}
.metaErrors{
background: #e0474c;
}
.metaSuccess{
background: #70c1b3;
}
/********************

View File

@ -5,8 +5,29 @@
<div class="formWrapper">
<section id="blox">
<div id="metanav" class="metanav" v-cloak>
<button
v-for="tab in tabs"
v-bind:key="tab"
v-bind:class="['tab-button', { active: currentTab === tab }]"
v-on:click="currentTab = tab"
>${tab}</button>
<component
class="tab"
v-bind:is="currentTabComponent"
:saved="saved"
:errors="formErrors[currentTab]"
:schema="formDefinitions[currentTab]"
:formdata="formData[currentTab]"
v-on:saveform="saveForm">
</component>
</div>
<section id="blox" :class="showBlox">
<div class="blox-body">
<transition name="fade" v-cloak>
@ -55,7 +76,7 @@
</section>
{% include 'editor/publish-controller.twig' %}
<input id="path" type="hidden" value="{{ item.urlRel }}" required readonly />
{{ csrf_field() | raw }}

View File

@ -154,7 +154,7 @@
pluginList += value[i].id + ',';
}
url += pluginList;
url = 'https://plugins.typemill.net/api/v1/checkversion?' + pluginList;
}
if(name == 'theme')
@ -165,7 +165,7 @@
themeList += value[i].id + ',';
}
url += themeList;
url = 'https://themes.typemill.net/api/v1/checkversion?' + themeList;
}
sendJson(function(response)
@ -176,15 +176,15 @@
if(name == 'system' && versions.system)
{
updateVersions(versions.system);
updateVersions(versions.system, 'system');
}
if(name == 'plugins' && versions.plugins)
{
updateVersions(versions.plugins);
updateVersions(versions.plugins, 'plugins');
}
if(name == 'theme' && versions.themes)
{
updateVersions(versions.themes);
updateVersions(versions.themes, 'themes');
}
}
else
@ -194,7 +194,7 @@
}, getPost, url, false, true);
}
function updateVersions(elementVersions)
function updateVersions(elementVersions,type)
{
for (var key in elementVersions)
{
@ -204,7 +204,20 @@
if(elementVersions[key] && singleElement && cmpVersions(elementVersions[key], singleElement.innerHTML) > 0)
{
singleElement.innerHTML = "<span>update<br/>to " + elementVersions[key] + "</span>";
if(type == 'themes')
{
var html = '<a href="https://themes.typemill.net/' + key + '" target="blank"><span>update<br/>to ' + elementVersions[key] + '</span></a>';
}
else if (type == 'plugins')
{
var html = '<a href="https://plugins.typemill.net/' + key + '" target="blank"><span>update<br/>to ' + elementVersions[key] + '</span></a>';
}
else
{
var html = '<a href="https://typemill.net" target="blank"><span>update<br/>to ' + elementVersions[key] + '</span></a>';
}
singleElement.innerHTML = html;
singleElement.classList.add("show-banner");
}
}
@ -262,60 +275,64 @@
var target = document.querySelectorAll('input[type=color]');
// set hooks for each target element
for (var i = 0, len = target.length; i < len; ++i)
{
var thisTarget = target[i];
(function(thisTarget){
if(target)
{
for (var i = 0, len = target.length; i < len; ++i)
{
var thisTarget = target[i];
/* hide the input field and show color box instead */
var box = document.createElement('div');
(function(thisTarget){
/* hide the input field and show color box instead */
var box = document.createElement('div');
box.className = 'color-box';
box.style.backgroundColor = thisTarget.value;
box.setAttribute('data-color', thisTarget.value);
thisTarget.parentNode.insertBefore(box, thisTarget);
thisTarget.type = 'hidden';
box.className = 'color-box';
box.style.backgroundColor = thisTarget.value;
box.setAttribute('data-color', thisTarget.value);
thisTarget.parentNode.insertBefore(box, thisTarget);
thisTarget.type = 'hidden';
var picker = new CP(box),
code = document.createElement('input');
picker.target.onclick = function(e)
{
e.preventDefault();
};
code.className = 'color-code';
code.pattern = '^#[A-Fa-f0-9]{6}$';
code.type = 'text';
picker.on("enter", function() {
code.value = '#' + CP._HSV2HEX(this.get());
});
var picker = new CP(box),
code = document.createElement('input');
picker.target.onclick = function(e)
{
e.preventDefault();
};
code.className = 'color-code';
code.pattern = '^#[A-Fa-f0-9]{6}$';
code.type = 'text';
picker.on("enter", function() {
code.value = '#' + CP._HSV2HEX(this.get());
});
picker.on("change", function(color) {
thisTarget.value = '#' + color;
this.target.style.backgroundColor = '#' + color;
code.value = '#' + color;
});
picker.picker.firstChild.appendChild(code);
picker.on("change", function(color) {
thisTarget.value = '#' + color;
this.target.style.backgroundColor = '#' + color;
code.value = '#' + color;
});
picker.picker.firstChild.appendChild(code);
function update() {
if (this.value.length) {
picker.set(this.value);
picker.trigger("change", [this.value.slice(1)]);
function update() {
if (this.value.length) {
picker.set(this.value);
picker.trigger("change", [this.value.slice(1)]);
}
}
}
code.oncut = update;
code.onpaste = update;
code.onkeyup = update;
code.oninput = update;
})(thisTarget);
code.oncut = update;
code.onpaste = update;
code.onkeyup = update;
code.oninput = update;
})(thisTarget);
}
}
/**

File diff suppressed because one or more lines are too long

View File

@ -1441,25 +1441,6 @@ const imageComponent = Vue.component('image-component', {
}
})
/*
let componentList = {
'content-component': contentComponent,
'markdown-component': markdownComponent,
'hr-component': hrComponent,
'toc-component': tocComponent,
'title-component': titleComponent,
'headline-component': headlineComponent,
'image-component': imageComponent,
'code-component': codeComponent,
'quote-component': quoteComponent,
'ulist-component': ulistComponent,
'olist-component': olistComponent,
'table-component': tableComponent,
'definition-component': definitionComponent,
'math-component': mathComponent
}
*/
let activeFormats = [];
for(var i = 0; i < formatConfig.length; i++)
@ -1489,6 +1470,7 @@ let editor = new Vue({
draftDisabled: true,
bloxOverlay: false,
sortdisabled: false,
showBlox: 'show',
formats: activeFormats
},
mounted: function(){

View File

@ -0,0 +1,220 @@
const FormBus = new Vue();
Vue.component('component-text', {
props: ['class', 'placeholder', 'label', 'name', 'type', 'size', 'value', 'errors'],
template: '<div class="large">' +
'<label>{{ label }}</label>' +
'<input type="text" :name="name" :placeholder="placeholder" :value="value" @input="update($event, name)">' +
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
'</div>',
methods: {
update: function($event, name)
{
FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
},
},
})
Vue.component('component-date', {
props: ['class', 'placeholder', 'readonly', 'label', 'name', 'type', 'size', 'value', 'errors'],
template: '<div class="large">' +
'<label>{{ label }}</label>' +
'<input type="date" :readonly="readonly" :name="name" :placeholder="placeholder" :value="value" @input="update($event, name)">' +
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
'</div>',
methods: {
update: function($event, name)
{
FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
},
},
})
Vue.component('component-textarea', {
props: ['class', 'placeholder', 'label', 'name', 'type', 'size', 'value', 'errors'],
template: '<div class="large">' +
'<label>{{label}}</label>' +
'<textarea :name="name" v-model="value" @input="update($event, name)"></textarea>' +
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
'</div>',
methods: {
update: function($event, name)
{
FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
},
},
})
Vue.component('component-select', {
props: ['class', 'placeholder', 'label', 'name', 'type', 'size', 'options', 'value', 'errors'],
template: '<div class="large">' +
'<label>{{label}}</label>' +
'<select v-model="value" @change="update($event,name)">' +
'<option v-for="option,optionkey in options" v-bind:value="optionkey">{{option}}</option>' +
'</select>' +
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
'</div>',
methods: {
update: function($event, name)
{
FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
},
},
})
Vue.component('component-checkbox', {
props: ['class', 'label', 'checkboxlabel', 'name', 'type', 'value', 'errors'],
template: '<div class="large">' +
'<label>{{ label }}</label>' +
'<label class="control-group">{{ checkboxlabel }}' +
'<input type="checkbox" :name="name" v-model="value" @change="update($event, value, name)">' +
'<span class="checkmark"></span>' +
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
'</label>' +
'</div>',
methods: {
update: function($event, value, name)
{
FormBus.$emit('forminput', {'name': name, 'value' : value});
},
},
})
Vue.component('component-radio', {
props: ['label', 'options', 'name', 'type', 'value', 'errors'],
template: '<div class="medium">' +
'<label>{{ label }}</label>' +
'<label v-for="option,optionvalue in options" class="control-group">{{ option }}' +
'<input type="radio" :name="name" :value="optionvalue" v-model="value" @change="update($event, value, name)">' +
'<span class="radiomark"></span>' +
'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
'</label>' +
'</div>',
methods: {
update: function($event, value, name)
{
FormBus.$emit('forminput', {'name': name, 'value' : value});
},
},
})
Vue.component('tab-meta', {
props: ['saved', 'errors', 'formdata', 'schema'],
template: '<section><form>' +
'<component v-for="(field, index) in schema.fields"' +
':key="index"' +
':is="selectComponent(field)"' +
':errors="errors"' +
':name="index"' +
'v-model="formdata[index]"' +
'v-bind="field">' +
'</component>' +
'<div v-if="saved" class="metaLarge"><div class="metaSuccess">Saved successfully</div></div>' +
'<div v-if="errors" class="metaLarge"><div class="metaErrors">Please correct the errors above</div></div>' +
'<div class="large"><input type="submit" @click.prevent="saveInput" value="save"></input></div>' +
'</form></section>',
methods: {
selectComponent: function(field)
{
return 'component-'+field.type;
},
saveInput: function()
{
this.$emit('saveform');
},
}
})
let meta = new Vue({
delimiters: ['${', '}'],
el: '#metanav',
data: function () {
return {
root: document.getElementById("main").dataset.url, /* get url of current page */
currentTab: 'Content',
tabs: ['Content'],
formDefinitions: [],
formData: [],
formErrors: {},
formErrorsReset: {},
saved: false,
}
},
computed: {
currentTabComponent: function () {
if(this.currentTab == 'Content')
{
editor.showBlox = 'show';
}
else
{
editor.showBlox = 'hidden';
}
return 'tab-' + this.currentTab.toLowerCase()
}
},
mounted: function(){
var self = this;
myaxios.get('/api/v1/article/metaobject',{
params: {
'url': document.getElementById("path").value,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
}
})
.then(function (response) {
var formdefinitions = response.data.metadefinitions;
for (var key in formdefinitions) {
if (formdefinitions.hasOwnProperty(key)) {
self.tabs.push(key);
self.formErrors[key] = false;
}
}
self.formErrorsReset = self.formErrors;
self.formDefinitions = formdefinitions;
self.formData = response.data.metadata;
})
.catch(function (error)
{
if(error.response)
{
}
});
FormBus.$on('forminput', formdata => {
this.$set(this.formData[this.currentTab], formdata.name, formdata.value);
});
},
methods: {
saveForm: function()
{
this.saved = false;
self = this;
myaxios.post('/api/v1/article/metadata',{
'url': document.getElementById("path").value,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'tab': self.currentTab,
'data': self.formData[self.currentTab]
})
.then(function (response) {
self.saved = true;
self.formErrors = self.formErrorsReset;
})
.catch(function (error)
{
if(error.response)
{
self.formErrors = error.response.data.errors;
}
});
},
}
});

View File

@ -1,9 +1,10 @@
const navcomponent = Vue.component('navigation', {
template: '#navigation-template',
props: ['homepage', 'name', 'newItem', 'parent', 'active', 'filetype', 'status', 'elementtype', 'element', 'folder', 'level', 'url', 'root', 'freeze'],
props: ['homepage', 'showForm', 'name', 'newItem', 'parent', 'active', 'filetype', 'status', 'elementtype', 'element', 'folder', 'level', 'url', 'root', 'freeze'],
data: function () {
return {
showForm: false,
revert: false,
}
},
methods: {
@ -11,7 +12,7 @@ const navcomponent = Vue.component('navigation', {
{
if(evt.dragged.classList.contains('folder') && evt.from.parentNode.id != evt.to.parentNode.id)
{
return false;
return false;
}
if(evt.dragged.firstChild.className == 'active' && !editor.draftDisabled)
{
@ -21,7 +22,7 @@ const navcomponent = Vue.component('navigation', {
return true;
},
onStart : function(evt)
{
{
/* delete error messages if exist */
publishController.errors.message = false;
},
@ -29,14 +30,14 @@ const navcomponent = Vue.component('navigation', {
{
var locator = {
'item_id': evt.item.id,
'parent_id_from': evt.from.parentNode.id,
'parent_id_to': evt.to.parentNode.id,
'parent_id_from': evt.from.parentNode.id,
'parent_id_to': evt.to.parentNode.id,
'index_old': evt.oldIndex,
'index_new': evt.newIndex,
'active': evt.item.getElementsByTagName('a')[0].className,
'url': document.getElementById("path").value,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'csrf_value': document.getElementById("csrf_value").value,
};
if(locator.parent_id_from == locator.parent_id_to && locator.index_old == locator.index_new)
@ -193,11 +194,26 @@ let navi = new Vue({
modalWindow: false,
format: /[!@#$%^&*()_+=\[\]{};':"\\|,.<>\/?]/,
folderName: '',
showForm: false,
newItem: '',
}
},
methods:{
checkMove: function(evt){
/* this.$refs.draggit[0].checkMove(evt); */
if(evt.dragged.classList.contains('folder') && evt.from.parentNode.id != evt.to.parentNode.id)
{
return false;
}
if(evt.dragged.firstChild.className == 'active' && !editor.draftDisabled)
{
publishController.errors.message = "Please save your changes before you move the file";
return false;
}
return true;
},
onStart: function(evt){
this.$refs.draggit[0].onStart(evt);
this.$refs.draggit[0].onStart(evt);
},
onEnd: function(evt){
this.$refs.draggit[0].onEnd(evt);
@ -208,29 +224,34 @@ let navi = new Vue({
hideModal: function(e){
this.modalWindow = false;
},
addFolder: function()
toggleForm : function()
{
this.showForm = !this.showForm;
},
addFile : function(type)
{
publishController.errors.message = false;
if(this.format.test(this.folderName) || this.folderName < 1 || this.folderName.length > 20)
{
publishController.errors.message = 'Special Characters are not allowed. Length between 1 and 20.';
if(this.format.test(this.newItem) || !this.newItem || this.newItem.length > 40)
{
publishController.errors.message = 'Special Characters are not allowed. Length between 1 and 40.';
return;
}
var newFolder = {
'item_name': this.folderName,
var newItem = {
'item_name': this.newItem,
'type': type,
'url': document.getElementById("path").value,
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
};
var self = this;
self.freeze = true;
self.errors = {title: false, content: false, message: false};
var url = this.root + '/api/v1/basefolder';
var url = this.root + '/api/v1/baseitem';
var method = 'POST';
sendJson(function(response, httpStatus)
@ -251,9 +272,10 @@ let navi = new Vue({
if(result.data)
{
self.items = result.data;
self.showForm = false;
}
}
}, method, url, newFolder );
}, method, url, newItem );
},
getNavi: function()
{

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,6 @@
<link rel="icon" type="image/png" href="{{ base_url }}/system/author/img/favicon-16x16.png" sizes="16x16" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="{{ base_url }}/system/author/img/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{{ base_url }}/system/author/img/apple-touch-icon-152x152.png" />
<link rel="stylesheet" href="{{ base_url }}/system/author/css/normalize.css" />
<link rel="stylesheet" href="{{ base_url }}/system/author/css/style.css?20191124" />
@ -160,6 +159,11 @@
</article>
<footer></footer>
</div>
<script src="{{ base_url }}/system/author/js/axios.min.js?20191124"></script>
<script>
const myaxios = axios.create();
myaxios.defaults.baseURL = "{{ base_url }}";
</script>
<script src="{{ base_url }}/system/author/js/vue.min.js?20191124"></script>
<script src="{{ base_url }}/system/author/js/autosize.min.js?20191124"></script>
<script src="{{ base_url }}/system/author/js/sortable.min.js?20191124"></script>
@ -174,6 +178,7 @@
{{ assets.renderEditorJS() }}
<script src="{{ base_url }}/system/author/js/vue-blox.js?20191124"></script>
<script src="{{ base_url }}/system/author/js/vue-meta.js?20191124"></script>
<script src="{{ base_url }}/system/author/js/vue-navi.js?20191124"></script>
<script src="{{ base_url }}/system/author/js/lazy-video.js?2019124"></script>

View File

@ -0,0 +1,21 @@
meta:
fields:
title:
type: text
label: Meta title
size: 60
class: large
description:
type: textarea
label: Meta description
size: 160
class: large
author:
type: text
label: author
class: large
modified:
type: date
label: Last modified at (readonly)
readonly: readonly
class: large

View File

@ -5,17 +5,17 @@
<div class="navi-list">
<div class="navi-item folder">
<div class="status" :class="homepage.status"></div>
<a href="{{ base_url }}/tm/content/{{ settings.editor }}" :class="homepage.active"><span><span class="iconwrapper"><svg class="icon icon-plus"><use xlink:href="#icon-plus"></use></svg></span><span class="level-1">Homepage</span></a>
<a href="{{ base_url }}/tm/content/{{ settings.editor }}" :class="homepage.active"><span><span class="iconwrapper"><svg class="icon icon-home"><use xlink:href="#icon-home"></use></svg></span><span class="level-1">Homepage</span></a>
</div>
</div>
<draggable class="navi-list"
<draggable class="navi-list" tag="ul"
@start="onStart"
@end="onEnd"
tag="ul"
:list="items"
group="folder"
:move="checkMove"
group="file"
animation="150"
:disabled="freeze">
:disabled="freeze">
<navigation
v-for="item in items"
ref="draggit"
@ -34,13 +34,20 @@
:folder="item.folderContent"
></navigation>
</draggable>
<ul class="navi-list addBaseFolder">
<ul class="navi-list addBaseItem">
<li class="navi-item file">
<span class="iconwrapper"><svg class="icon icon-plus"><use xlink:href="#icon-plus"></use></svg></span>
<div class="addNaviForm">
<input type="text" v-model="folderName" />
<button class="fullWidth" @click="addFolder">add folder to base level</button>
</div>
<span class="iconwrapper">
<svg class="icon icon-plus"><use xlink:href="#icon-plus"></use></svg>
</span>
<span class="addNaviItem">
<a class="addNaviLink" href="#" @click.prevent="toggleForm">add item</a>
</span>
<transition name="fade">
<div v-if="showForm" class="addNaviForm">
<input v-model="newItem">
<button class="b-left" @click="addFile('file')">add file</button><button class="b-right" @click="addFile('folder')">add folder</button>
</div>
</transition>
</li>
</ul>
</div>
@ -55,10 +62,10 @@
@start="onStart"
@end="onEnd"
:list="folder"
:move="checkMove"
:move="checkMove"
group="file"
animation="150"
:disabled="freeze">
:disabled="freeze">
<navigation
v-for="item in folder"
ref="draggit"

View File

@ -12,6 +12,7 @@
<header class="headline">
<h1>Plugins</h1>
<a class="button" target="_blank" href="https://plugins.typemill.net">Plugin Store</a>
</header>
{% for pluginName,plugin in plugins %}

View File

@ -51,7 +51,7 @@
<span class="error">{{ errors.settings.year | first }}</span>
{% endif %}
</div><div class="medium{{ errors.settings.language ? ' error' : '' }}">
<label for="settings[language]">Language</label>
<label for="settings[language]">Language-Attribute <small>(HTML)</small></label>
<select name="settings[language]" id="language">
{% for key,lang in languages %}
<option value="{{ key }}"{% if (key == old.settings.language or key == mylang) %} selected{% endif %}>{{ lang }}</option>

View File

@ -10,6 +10,7 @@
<header class="headline">
<h1>Themes</h1>
<a class="button" target="_blank" href="https://themes.typemill.net">Theme Store</a>
</header>
{% for themeName, theme in themes %}

View File

@ -201,6 +201,7 @@ $container['view'] = function ($container)
$view->addExtension(new Twig_Extension_Debug());
$view->addExtension(new Typemill\Extensions\TwigUserExtension());
$view->addExtension(new Typemill\Extensions\TwigMarkdownExtension());
$view->addExtension(new Typemill\Extensions\TwigMetaExtension());
/* use {{ base_url() }} in twig templates */
$view['base_url'] = $container['request']->getUri()->getBaseUrl();

View File

@ -193,11 +193,17 @@ header p{
.main-menu li.folder.level-1{
padding-left: 15px;
}
.main-menu li.file.level-1{
padding-left: 20px;
}
.main-menu > ul > li
{
font-size: 0.8em;
}
.main-menu > ul > li.folder
{
text-transform: uppercase;
margin: 15px 0 5px;
font-size: 0.8em;
font-weight: 700;
}

View File

@ -1,6 +1,6 @@
{% extends '/partials/layout.twig' %}
{% block title %}{{ title }} | {{ settings.title }}{% endblock %}
{% block title %}{{ metatabs.meta.title | default(title) }} | {{ settings.title }}{% endblock %}
{% block content %}

View File

@ -12,7 +12,7 @@
<small>{{ settings.themes.typemill.authorIntro }}: {{ settings.author }}</small>
{% endif %}
{% if settings.themes.typemill.modifiedPosition.top %}
<small>{{ settings.themes.typemill.modifiedText }}: {{ item.modified|date(settings.themes.typemill.modifiedFormat) }}</small>
<small>{{ settings.themes.typemill.modifiedText }}: {{ metatabs.meta.modified|date(settings.themes.typemill.modifiedFormat) }}</small>
{% endif %}
{% if settings.themes.typemill.socialPosition.top %}
<div id="share-icons" class="share-icons hide">
@ -37,7 +37,7 @@
<small>{{ settings.themes.typemill.authorIntro }}: {{ settings.author }}</small>
{% endif %}
{% if settings.themes.typemill.modifiedPosition.bottom %}
<small>{{ settings.themes.typemill.modifiedText }}: {{ item.modified|date(settings.themes.typemill.modifiedFormat) }}</small>
<small>{{ settings.themes.typemill.modifiedText }}: {{ metatabs.meta.modified|date(settings.themes.typemill.modifiedFormat) }}</small>
{% endif %}
{% if settings.themes.typemill.socialPosition.bottom %}
<div id="share-icons-bottom" class="share-icons hide">

View File

@ -7,7 +7,7 @@
<base href="{{ base_url }}/">
<meta name="description" content="{{ description }}" />
<meta name="description" content="{{ metatabs.meta.description }}" />
<meta name="author" content="{{ settings.author }}" />
<meta name="generator" content="TYPEMILL" />
<meta name="msapplication-TileColor" content="#F9F8F6" />
@ -21,8 +21,8 @@
<link rel="canonical" href="{{ item.urlAbs }}" />
<meta property="og:site_name" content="{{ settings.title }}">
<meta property="og:title" content="{{ title }}">
<meta property="og:description" content="{{ description }}">
<meta property="og:title" content="{{ metatabs.meta.title }}">
<meta property="og:description" content="{{ metatabs.meta.description }}">
<meta property="og:type" content="article">
<meta property="og:url" content="{{ item.urlAbs }}">
<meta property="og:image" content="{{ image.img_url }}">

View File

@ -7,7 +7,7 @@
<base href="{{ base_url }}/">
<meta name="description" content="{{ description }}" />
<meta name="description" content="{{ metatabs.meta.description }}" />
<meta name="author" content="{{ settings.author }}" />
<meta name="generator" content="TYPEMILL" />
<meta name="msapplication-TileColor" content="#F9F8F6" />
@ -21,8 +21,8 @@
<link rel="canonical" href="{{ item.urlAbs }}" />
<meta property="og:site_name" content="{{ settings.title }}">
<meta property="og:title" content="{{ title }}">
<meta property="og:description" content="{{ description }}">
<meta property="og:title" content="{{ metatabs.meta.title }}">
<meta property="og:description" content="{{ metatabs.meta.description }}">
<meta property="og:type" content="article">
<meta property="og:url" content="{{ item.urlAbs }}">
<meta property="og:image" content="{{ image.img_url }}">

View File

@ -1,5 +1,5 @@
name: Typemill Theme
version: 1.1.8
version: 1.1.9
description: The standard theme for Typemill. Responsive, minimal and without any dependencies. It uses the system fonts Calibri and Helvetica. No JavaScript is used.
author: Sebastian Schürmanns
homepage: https://typemill.net