Procházet zdrojové kódy

Version 1.3.0: Meta-Information

trendschau před 5 roky
rodič
revize
7d41c4894c
85 změnil soubory, kde provedl 961 přidání a 1356 odebrání
  1. 7 16
      .gitignore
  2. 1 1
      cache/lastCache.txt
  3. 0 47
      plugins/analytics/analytics.php
  4. 0 38
      plugins/analytics/analytics.yaml
  5. 0 5
      plugins/analytics/templates/googleanalytics.twig
  6. 0 11
      plugins/analytics/templates/piwikanalytics.twig
  7. 0 38
      plugins/cookieconsent/cookieconsent.php
  8. 0 88
      plugins/cookieconsent/cookieconsent.yaml
  9. 0 6
      plugins/cookieconsent/public/cookieconsent.min.css
  10. 0 0
      plugins/cookieconsent/public/cookieconsent.min.js
  11. 0 22
      plugins/cookieconsent/templates/cookieconsent.twig
  12. 0 28
      plugins/highlight/highlight.php
  13. 0 6
      plugins/highlight/highlight.yaml
  14. 0 24
      plugins/highlight/public/LICENSE
  15. 0 99
      plugins/highlight/public/default.css
  16. 0 1
      plugins/highlight/public/highlight.pack.js
  17. 0 56
      plugins/math/math.php
  18. 0 20
      plugins/math/math.yaml
  19. 0 140
      plugins/math/public/README.md
  20. 0 0
      plugins/math/public/auto-render.min.js
  21. binární
      plugins/math/public/fonts/KaTeX_AMS-Regular.woff
  22. binární
      plugins/math/public/fonts/KaTeX_Caligraphic-Bold.woff
  23. binární
      plugins/math/public/fonts/KaTeX_Caligraphic-Regular.woff
  24. binární
      plugins/math/public/fonts/KaTeX_Fraktur-Bold.woff
  25. binární
      plugins/math/public/fonts/KaTeX_Fraktur-Regular.woff
  26. binární
      plugins/math/public/fonts/KaTeX_Main-Bold.woff
  27. binární
      plugins/math/public/fonts/KaTeX_Main-BoldItalic.woff
  28. binární
      plugins/math/public/fonts/KaTeX_Main-Italic.woff
  29. binární
      plugins/math/public/fonts/KaTeX_Main-Regular.woff
  30. binární
      plugins/math/public/fonts/KaTeX_Math-BoldItalic.woff
  31. binární
      plugins/math/public/fonts/KaTeX_Math-Italic.woff
  32. binární
      plugins/math/public/fonts/KaTeX_SansSerif-Bold.woff
  33. binární
      plugins/math/public/fonts/KaTeX_SansSerif-Italic.woff
  34. binární
      plugins/math/public/fonts/KaTeX_SansSerif-Regular.woff
  35. binární
      plugins/math/public/fonts/KaTeX_Script-Regular.woff
  36. binární
      plugins/math/public/fonts/KaTeX_Size1-Regular.woff
  37. binární
      plugins/math/public/fonts/KaTeX_Size2-Regular.woff
  38. binární
      plugins/math/public/fonts/KaTeX_Size3-Regular.woff
  39. binární
      plugins/math/public/fonts/KaTeX_Size4-Regular.woff
  40. binární
      plugins/math/public/fonts/KaTeX_Typewriter-Regular.woff
  41. 0 0
      plugins/math/public/katex.min.css
  42. 0 0
      plugins/math/public/katex.min.js
  43. 0 54
      plugins/math/public/math.js
  44. 0 87
      plugins/search/index.php
  45. 0 19
      plugins/search/public/lunr-licence.md
  46. 0 5
      plugins/search/public/lunr.min.js
  47. 0 64
      plugins/search/public/search.css
  48. 0 115
      plugins/search/public/search.js
  49. 0 128
      plugins/search/search.php
  50. 0 6
      plugins/search/search.yaml
  51. 1 1
      system/Assets.php
  52. 40 22
      system/Controllers/ContentApiController.php
  53. 23 27
      system/Controllers/ContentController.php
  54. 158 0
      system/Controllers/MetaApiController.php
  55. 61 44
      system/Controllers/PageController.php
  56. 14 0
      system/Events/OnMetaLoaded.php
  57. 24 0
      system/Extensions/TwigMetaExtension.php
  58. 37 12
      system/Models/Folder.php
  59. 24 1
      system/Models/Validation.php
  60. 1 1
      system/Models/Write.php
  61. 83 0
      system/Models/WriteYaml.php
  62. 6 1
      system/Routes/Api.php
  63. 1 1
      system/Settings.php
  64. 1 1
      system/author/auth/welcome.twig
  65. 58 2
      system/author/css/style.css
  66. 24 3
      system/author/editor/editor-blox.twig
  67. 70 53
      system/author/js/author.js
  68. 0 2
      system/author/js/sortable.min.js
  69. 1 19
      system/author/js/vue-blox.js
  70. 220 0
      system/author/js/vue-meta.js
  71. 38 16
      system/author/js/vue-navi.js
  72. 0 0
      system/author/js/vuedraggable.umd.min.js
  73. 6 1
      system/author/layouts/layoutBlox.twig
  74. 21 0
      system/author/metatabs.yaml
  75. 20 13
      system/author/partials/editorNavi.twig
  76. 1 0
      system/author/settings/plugins.twig
  77. 1 1
      system/author/settings/system.twig
  78. 1 0
      system/author/settings/themes.twig
  79. 1 0
      system/system.php
  80. 7 1
      themes/typemill/css/style.css
  81. 1 1
      themes/typemill/index.twig
  82. 2 2
      themes/typemill/page.twig
  83. 3 3
      themes/typemill/partials/layout.twig
  84. 3 3
      themes/typemill/partials/layoutCover.twig
  85. 1 1
      themes/typemill/typemill.yaml

+ 7 - 16
.gitignore

@@ -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

+ 1 - 1
cache/lastCache.txt

@@ -1 +1 @@
-1574585614
+1577803430

+ 0 - 47
plugins/analytics/analytics.php

@@ -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));
-			}			
-		}
-	}
-}

+ 0 - 38
plugins/analytics/analytics.yaml

@@ -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'

+ 0 - 5
plugins/analytics/templates/googleanalytics.twig

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

+ 0 - 11
plugins/analytics/templates/piwikanalytics.twig

@@ -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);
-})();

+ 0 - 38
plugins/cookieconsent/cookieconsent.php

@@ -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));
-	}
-}

+ 0 - 88
plugins/cookieconsent/cookieconsent.yaml

@@ -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

+ 0 - 6
plugins/cookieconsent/public/cookieconsent.min.css

@@ -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}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
plugins/cookieconsent/public/cookieconsent.min.js


+ 0 - 22
plugins/cookieconsent/templates/cookieconsent.twig

@@ -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 }}"
-		}
-	})
-});

+ 0 - 28
plugins/highlight/highlight.php

@@ -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();');
-	}
-}

+ 0 - 6
plugins/highlight/highlight.yaml

@@ -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

+ 0 - 24
plugins/highlight/public/LICENSE

@@ -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.

+ 0 - 99
plugins/highlight/public/default.css

@@ -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;
-}

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 1
plugins/highlight/public/highlight.pack.js


+ 0 - 56
plugins/math/math.php

@@ -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>');
-	}
-
-}

+ 0 - 20
plugins/math/math.yaml

@@ -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

+ 0 - 140
plugins/math/public/README.md

@@ -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:** KaTeX’s layout is based on Donald Knuth’s 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).

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
plugins/math/public/auto-render.min.js


binární
plugins/math/public/fonts/KaTeX_AMS-Regular.woff


binární
plugins/math/public/fonts/KaTeX_Caligraphic-Bold.woff


binární
plugins/math/public/fonts/KaTeX_Caligraphic-Regular.woff


binární
plugins/math/public/fonts/KaTeX_Fraktur-Bold.woff


binární
plugins/math/public/fonts/KaTeX_Fraktur-Regular.woff


binární
plugins/math/public/fonts/KaTeX_Main-Bold.woff


binární
plugins/math/public/fonts/KaTeX_Main-BoldItalic.woff


binární
plugins/math/public/fonts/KaTeX_Main-Italic.woff


binární
plugins/math/public/fonts/KaTeX_Main-Regular.woff


binární
plugins/math/public/fonts/KaTeX_Math-BoldItalic.woff


binární
plugins/math/public/fonts/KaTeX_Math-Italic.woff


binární
plugins/math/public/fonts/KaTeX_SansSerif-Bold.woff


binární
plugins/math/public/fonts/KaTeX_SansSerif-Italic.woff


binární
plugins/math/public/fonts/KaTeX_SansSerif-Regular.woff


binární
plugins/math/public/fonts/KaTeX_Script-Regular.woff


binární
plugins/math/public/fonts/KaTeX_Size1-Regular.woff


binární
plugins/math/public/fonts/KaTeX_Size2-Regular.woff


binární
plugins/math/public/fonts/KaTeX_Size3-Regular.woff


binární
plugins/math/public/fonts/KaTeX_Size4-Regular.woff


binární
plugins/math/public/fonts/KaTeX_Typewriter-Regular.woff


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
plugins/math/public/katex.min.css


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
plugins/math/public/katex.min.js


+ 0 - 54
plugins/math/public/math.js

@@ -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);
-		},
-	},
-})

+ 0 - 87
plugins/search/index.php

@@ -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;
-    }
-}

+ 0 - 19
plugins/search/public/lunr-licence.md

@@ -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.

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 5
plugins/search/public/lunr.min.js


+ 0 - 64
plugins/search/public/search.css

@@ -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{
-
-}

+ 0 - 115
plugins/search/public/search.js

@@ -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;
-*/

+ 0 - 128
plugins/search/search.php

@@ -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);
-	}
-}

+ 0 - 6
plugins/search/search.yaml

@@ -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

+ 1 - 1
system/Assets.php

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

+ 40 - 22
system/Controllers/ContentApiController.php

@@ -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));

+ 23 - 27
system/Controllers/ContentController.php

@@ -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))
 		{

+ 158 - 0
system/Controllers/MetaApiController.php

@@ -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));
+	}
+}

+ 61 - 44
system/Controllers/PageController.php

@@ -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 ]);
 	}

+ 14 - 0
system/Events/OnMetaLoaded.php

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

+ 24 - 0
system/Extensions/TwigMetaExtension.php

@@ -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;
+	}
+}

+ 37 - 12
system/Models/Folder.php

@@ -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];
+	}
+
 }

+ 24 - 1
system/Models/Validation.php

@@ -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]))
 				{

+ 1 - 1
system/Models/Write.php

@@ -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;

+ 83 - 0
system/Models/WriteYaml.php

@@ -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;
+	}
 }

+ 6 - 1
system/Routes/Api.php

@@ -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']));

+ 1 - 1
system/Settings.php

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

+ 1 - 1
system/author/auth/welcome.twig

@@ -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>

+ 58 - 2
system/author/css/style.css

@@ -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;
+}
 
 
 /********************

+ 24 - 3
system/author/editor/editor-blox.twig

@@ -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 }}
 		

+ 70 - 53
system/author/js/author.js

@@ -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);
 
-			function update() {
-				if (this.value.length) {
-					picker.set(this.value);
-					picker.trigger("change", [this.value.slice(1)]);
+				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)]);
+					}
 				}
-			}
 
-			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);		
+	    }
     }
 	
 	/**

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 2
system/author/js/sortable.min.js


+ 1 - 19
system/author/js/vue-blox.js

@@ -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(){

+ 220 - 0
system/author/js/vue-meta.js

@@ -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;
+	            }
+	        });
+		},
+	}
+});

+ 38 - 16
system/author/js/vue-navi.js

@@ -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()
 		{

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
system/author/js/vuedraggable.umd.min.js


+ 6 - 1
system/author/layouts/layoutBlox.twig

@@ -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>
 

+ 21 - 0
system/author/metatabs.yaml

@@ -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

+ 20 - 13
system/author/partials/editorNavi.twig

@@ -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" 

+ 1 - 0
system/author/settings/plugins.twig

@@ -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 %}

+ 1 - 1
system/author/settings/system.twig

@@ -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>

+ 1 - 0
system/author/settings/themes.twig

@@ -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 %}

+ 1 - 0
system/system.php

@@ -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();

+ 7 - 1
themes/typemill/css/style.css

@@ -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;
 }
 

+ 1 - 1
themes/typemill/index.twig

@@ -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 %}
 

+ 2 - 2
themes/typemill/page.twig

@@ -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">

+ 3 - 3
themes/typemill/partials/layout.twig

@@ -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 }}">

+ 3 - 3
themes/typemill/partials/layoutCover.twig

@@ -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 }}">

+ 1 - 1
themes/typemill/typemill.yaml

@@ -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

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů