Forráskód Böngészése

Merge branch 'version135'

trendschau 5 éve
szülő
commit
6ff403b289
33 módosított fájl, 724 hozzáadás és 424 törlés
  1. 15 19
      system/Controllers/ArticleApiController.php
  2. 0 1
      system/Controllers/AuthController.php
  3. 12 11
      system/Controllers/BlockApiController.php
  4. 5 4
      system/Controllers/ContentController.php
  5. 3 2
      system/Controllers/Controller.php
  6. 32 3
      system/Controllers/MediaApiController.php
  7. 16 16
      system/Controllers/MetaApiController.php
  8. 20 41
      system/Controllers/PageController.php
  9. 9 8
      system/Controllers/SettingsController.php
  10. 1 1
      system/Controllers/SetupController.php
  11. 37 15
      system/Extensions/ParsedownExtension.php
  12. 24 3
      system/Extensions/TwigMetaExtension.php
  13. 6 1
      system/Models/ProcessAssets.php
  14. 24 15
      system/Models/ProcessImage.php
  15. 348 0
      system/Models/WriteMeta.php
  16. 0 233
      system/Models/WriteYaml.php
  17. 4 3
      system/Settings.php
  18. 13 3
      system/author/css/style.css
  19. 0 0
      system/author/js/typemillutils.js
  20. 8 1
      system/author/js/vue-blox-config.js
  21. 40 17
      system/author/js/vue-blox.js
  22. 3 0
      system/author/js/vue-navi.js
  23. 5 0
      system/author/js/vue-publishcontroller.js
  24. 1 1
      system/author/layouts/layoutBlox.twig
  25. 6 6
      system/author/partials/fields.twig
  26. 13 2
      system/author/settings/system.twig
  27. 29 7
      themes/typemill/chapter.twig
  28. 2 0
      themes/typemill/cover.twig
  29. 42 8
      themes/typemill/css/style.css
  30. 1 1
      themes/typemill/partials/layout.twig
  31. 3 0
      themes/typemill/partials/layoutCover.twig
  32. 1 1
      themes/typemill/partials/navigation.twig
  33. 1 1
      themes/typemill/typemill.yaml

+ 15 - 19
system/Controllers/ArticleApiController.php

@@ -7,6 +7,7 @@ use Slim\Http\Response;
 use Typemill\Models\Folder;
 use Typemill\Models\Write;
 use Typemill\Models\WriteYaml;
+use Typemill\Models\WriteMeta;
 use Typemill\Extensions\ParsedownExtension;
 use Typemill\Events\OnPagePublished;
 use Typemill\Events\OnPageUnpublished;
@@ -77,10 +78,15 @@ class ArticleApiController extends ContentController
 			# update the public structure
 			$this->setStructure($draft = false, $cache = false);
 
+			# complete the page meta if title or description not set
+			$writeMeta = new WriteMeta();
+			$meta = $writeMeta->completePageMeta($this->content, $this->settings, $this->item);
+
 			# dispatch event
-			$this->c->dispatcher->dispatch('onPagePublished', new OnPagePublished($this->item));
+			$page = ['content' => $this->content, 'meta' => $meta, 'item' => $this->item];
+			$this->c->dispatcher->dispatch('onPagePublished', new OnPagePublished($page));
 
-			return $response->withJson(['success'], 200);
+			return $response->withJson(['success' => true, 'meta' => $meta], 200);
 		}
 		else
 		{
@@ -260,7 +266,7 @@ class ArticleApiController extends ContentController
 		}
 		else
 		{
-			return $response->withJson(array('data' => $this->structure, 'errors' => $this->errors), 404); 
+			return $response->withJson(array('data' => $this->structure, 'errors' => $this->errors), 422); 
 		}
 	}
 	
@@ -465,13 +471,12 @@ class ArticleApiController extends ContentController
 		# create the url for the item
 		$urlWoF 	= $folder->urlRelWoF . '/' . $slug;
 
-		# add the navigation name to the item htmlspecialchars needed for frensh language
+		# add the navigation name to the item htmlspecialchars needed for french language
 		$extended[$urlWoF] = ['hide' => false, 'navtitle' => $name];
 
 		# store the extended structure
 		$write->updateYaml('cache', 'structure-extended.yaml', $extended);
 
-
 		# update the structure for editor
 		$this->setStructure($draft = true, $cache = false);
 
@@ -482,7 +487,6 @@ class ArticleApiController extends ContentController
 		
 		return $response->withJson(array('posts' => $folder, $this->structure, 'errors' => false, 'url' => $url));
 	}
-
 	
 	public function createArticle(Request $request, Response $response, $args)
 	{
@@ -546,14 +550,10 @@ class ArticleApiController extends ContentController
 		if($writeError){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); }
 
 		# add prefix number to the name
-#		$namePath 	= $index > 9 ? $index . '-' . $name : '0' . $index . '-' . $name;
 		$namePath 	= $index > 9 ? $index . '-' . $slug : '0' . $index . '-' . $slug;
 		$folderPath	= 'content' . $folder->path;
 		
-#		$title = implode(" ", $nameParts); 
-
 		# create default content
-#		$content = json_encode(['# ' . $title, 'Content']);
 		$content = json_encode(['# ' . $name, 'Content']);
 		
 		if($this->params['type'] == 'file')
@@ -576,21 +576,18 @@ class ArticleApiController extends ContentController
 
 		}
 		
-
 		# get extended structure
 		$extended = $write->getYaml('cache', 'structure-extended.yaml');
 
 		# create the url for the item
 		$urlWoF = $folder->urlRelWoF . '/' . $slug;
 
-		# add the navigation name to the item htmlspecialchars needed for frensh language
+		# add the navigation name to the item htmlspecialchars needed for french language
 		$extended[$urlWoF] = ['hide' => false, 'navtitle' => $name];
 
 		# store the extended structure
 		$write->updateYaml('cache', 'structure-extended.yaml', $extended);
 
-
-
 		# update the structure for editor
 		$this->setStructure($draft = true, $cache = false);
 
@@ -643,7 +640,7 @@ class ArticleApiController extends ContentController
 			# check, if the same name as new item, then return an error
 			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);
+				return $response->withJson(array('data' => $this->structure, 'errors' => 'There is already a page with this name. Please choose another name.', 'url' => $url), 422);
 			}
 			
 			if(!$write->moveElement($item, '', $index))
@@ -653,7 +650,7 @@ class ArticleApiController extends ContentController
 			$index++;
 		}
 
-		if($writeError){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 404); }
+		if($writeError){ return $response->withJson(array('data' => $this->structure, 'errors' => 'Something went wrong. Please refresh the page and check, if all folders and files are writable.', 'url' => $url), 422); }
 
 		# add prefix number to the name
 		$namePath 	= $index > 9 ? $index . '-' . $slug : '0' . $index . '-' . $slug;
@@ -667,14 +664,14 @@ class ArticleApiController extends ContentController
 		{
 			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);
+				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), 422);
 			}
 		}
 		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);
+				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), 422);
 			}
 			$write->writeFile($folderPath . DIRECTORY_SEPARATOR . $namePath, 'index.txt', $content);
 
@@ -695,7 +692,6 @@ class ArticleApiController extends ContentController
 		# store the extended structure
 		$write->updateYaml('cache', 'structure-extended.yaml', $extended);
 
-
 		# update the structure for editor
 		$this->setStructure($draft = true, $cache = false);
 

+ 0 - 1
system/Controllers/AuthController.php

@@ -24,7 +24,6 @@ class AuthController extends Controller
 		}
 	}
 
-	
 	/**
 	* show login form
 	* 

+ 12 - 11
system/Controllers/BlockApiController.php

@@ -769,28 +769,29 @@ class BlockApiController extends ContentController
 			$imageData	= @file_get_contents($videoURL0, 0, $ctx);
 			if($imageData === false)
 			{
-				return $response->withJson(array('errors' => 'could not get the video image'));
+				return $response->withJson(['errors' => ['message' => 'We did not find that video or could not get a preview image.']], 500);
 			}
 		}
 		
-		$imageData64	= 'data:image/jpeg;base64,' . base64_encode($imageData);
-		$desiredSizes	= ['live' => ['width' => 560, 'height' => 315]];
-		$imageProcessor	= new ProcessImage($this->settings['images']);
-		if(!$imageProcessor->checkFolders())
+		$imageData64			= 'data:image/jpeg;base64,' . base64_encode($imageData);
+		$desiredSizes 			= $this->settings['images'];
+		$desiredSizes['live']	= ['width' => 560, 'height' => 315];
+		$imageProcessor			= new ProcessImage($desiredSizes);
+		
+		if(!$imageProcessor->checkFolders('images'))
 		{
 			return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
 		}
-
-		$tmpImage		= $imageProcessor->createImage($imageData64, $desiredSizes);
 		
-		if(!$tmpImage)
+		if(!$imageProcessor->createImage($imageData64, 'youtube-' . $videoID, $desiredSizes, $overwrite = true))
 		{
-			return $response->withJson(array('errors' => 'could not create temporary image'));			
+			return $response->withJson(['errors' => ['message' => 'We could not create the image.']], 500);
 		}
-		
-		$imageUrl 		= $imageProcessor->publishImage($desiredSizes, $videoID);
+
+		$imageUrl 		= $imageProcessor->publishImage();
 		if($imageUrl)
 		{
+
 			$this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}';
 
 			$request 	= $request->withParsedBody($this->params);

+ 5 - 4
system/Controllers/ContentController.php

@@ -60,6 +60,7 @@ abstract class ContentController
 		$this->structureDraftName 	= 'structure-draft.txt';
 	}
 	
+	# admin ui rendering
 	protected function render($response, $route, $data)
 	{
 		if(isset($_SESSION['old']))
@@ -68,7 +69,7 @@ abstract class ContentController
 		}
 
 		$response = $response->withoutHeader('Server');
-		$response = $response->withoutHeader('X-Powered-By');		
+		$response = $response->withoutHeader('X-Powered-By');
 		
 		if($this->c->request->getUri()->getScheme() == 'https')
 		{
@@ -376,7 +377,7 @@ abstract class ContentController
 		if(file_exists($path))
 		{
 			$files = array_diff(scandir($path), array('.', '..'));
-			
+
 			# check if there are published pages or folders inside, then stop the operation
 			foreach ($files as $file)
 			{
@@ -385,9 +386,9 @@ abstract class ContentController
 					$this->errors = ['message' => 'Please delete the sub-folder first.'];
 				}
 
-				if(substr($file, -3) == '.md' )
+				if(substr($file, -3) == '.md' && $file != 'index.md')
 				{
-					$this->errors = ['message' => 'Please unpublish all pages in the folder first.'];					
+					$this->errors = ['message' => 'Please unpublish all pages in the folder first.'];		
 				}
 			}
 

+ 3 - 2
system/Controllers/Controller.php

@@ -18,6 +18,7 @@ abstract class Controller
 		$this->c = $c;
 	}
 	
+	# frontend rendering
 	protected function render($response, $route, $data)
 	{
 		# why commented this out??
@@ -29,8 +30,6 @@ abstract class Controller
 		}
 		
 		$response = $response->withoutHeader('Server');
-		$response = $response->withoutHeader('X-Powered-By');		
-		
 		if($this->c->request->getUri()->getScheme() == 'https')
 		{
 			$response = $response->withAddedHeader('Strict-Transport-Security', 'max-age=63072000');
@@ -40,6 +39,8 @@ abstract class Controller
 		$response = $response->withAddedHeader('X-Frame-Options', 'SAMEORIGIN');
 		$response = $response->withAddedHeader('X-XSS-Protection', '1;mode=block');
 		$response = $response->withAddedHeader('Referrer-Policy', 'no-referrer-when-downgrade');
+		$response = $response->withAddedHeader('X-Powered-By', 'Typemill');
+		
 
 		return $this->c->view->render($response, $route, $data);
 	}

+ 32 - 3
system/Controllers/MediaApiController.php

@@ -320,19 +320,34 @@ class MediaApiController extends ContentController
 		return $response->withJson(array('errors' => 'could not store the preview image'));	
 	}
 
+	# https://www.sitepoint.com/mime-types-complete-list/
+	# https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
 	private function getAllowedMtypes()
 	{
 		return array(
 		   	'application/zip',
 		   	'application/gzip',
+		   	'application/x-gzip',
+		   	'application/x-compressed',
+		   	'application/x-zip-compressed',
 		   	'application/vnd.rar',
+		   	'application/x-7z-compressed',
+			'application/x-visio',
 			'application/vnd.visio',
+			'application/excel',
+			'application/x-excel',
+			'application/x-msexcel',
 			'application/vnd.ms-excel',
-			'application/vnd.ms-powerpoint',
-			'application/vnd.ms-word.document.macroEnabled.12',
-			'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
 			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+			'application/powerpoint',
+			'application/mspowerpoint',
+			'application/x-mspowerpoint',
+			'application/vnd.ms-powerpoint',
 			'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+			'application/msword',
+			'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+			'application/x-project',
+			'application/vnd.ms-project',			
 			'application/vnd.apple.keynote',
 			'application/vnd.apple.mpegurl',
 			'application/vnd.apple.numbers',
@@ -340,17 +355,31 @@ class MediaApiController extends ContentController
 			'application/vnd.amazon.mobi8-ebook',
 			'application/epub+zip',
 			'application/pdf',
+			'application/x-latex',
 		   	'image/png',
 		   	'image/jpeg',
 		   	'image/gif',
+		   	'image/tiff',
+		   	'image/x-tiff',
 		   	'image/svg+xml',
+		   	'image/x-icon',
+		   	'text/plain',
+		   	'application/plain',
+		   	'text/richtext',
+		   	'text/vnd.rn-realtext',
+		   	'application/rtf',
+		   	'application/x-rtf',
 		   	'font/*',
 		   	'audio/mpeg',
 		   	'audio/mp4',
 		   	'audio/ogg',
+		   	'audio/3gpp',
+		   	'audio/3gpp2',
 		   	'video/mpeg',
 		   	'video/mp4',
 		   	'video/ogg',
+		   	'video/3gpp',
+		   	'video/3gpp2',
 		);
 	}	
 }

+ 16 - 16
system/Controllers/MetaApiController.php

@@ -5,6 +5,7 @@ namespace Typemill\Controllers;
 use Slim\Http\Request;
 use Slim\Http\Response;
 use Typemill\Models\WriteYaml;
+use Typemill\Models\WriteMeta;
 use Typemill\Models\Folder;
 
 class MetaApiController extends ContentController
@@ -24,6 +25,7 @@ class MetaApiController extends ContentController
 
 		$metatabs = $writeYaml->getYaml('system' . DIRECTORY_SEPARATOR . 'author', 'metatabs.yaml');
 
+		# add radio buttons to choose posts or pages for folder.
 		if($folder)
 		{
 			$metatabs['meta']['fields']['contains'] = [
@@ -70,22 +72,22 @@ class MetaApiController extends ContentController
 		# set item 
 		if(!$this->setItem()){ return $response->withJson($this->errors, 404); }
 
-		$writeYaml = new writeYaml();
+		$writeMeta = new writeMeta();
 
-		$pagemeta = $writeYaml->getPageMeta($this->settings, $this->item);
+		$pagemeta = $writeMeta->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);
+			$pagemeta = $writeMeta->getPageMetaBlank($this->content, $this->settings, $this->item);
 		}
 
 		# if item is a folder
@@ -118,7 +120,7 @@ class MetaApiController extends ContentController
 		}
 
 		# store the metascheme in cache for frontend
-		$writeYaml->updateYaml('cache', 'metatabs.yaml', $metascheme);
+		$writeMeta->updateYaml('cache', 'metatabs.yaml', $metascheme);
 
 		return $response->withJson(array('metadata' => $metadata, 'metadefinitions' => $metadefinitions, 'item' => $this->item, 'errors' => false));
 	}
@@ -187,13 +189,13 @@ class MetaApiController extends ContentController
 		# return validation errors
 		if($errors){ return $response->withJson(array('errors' => $errors),422); }
 		
-		$writeYaml = new writeYaml();
+		$writeMeta = new writeMeta();
 
 		# get existing metadata for page
-		$metaPage = $writeYaml->getYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml');
+		$metaPage = $writeMeta->getYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml');
 		
 		# get extended structure
-		$extended = $writeYaml->getYaml('cache', 'structure-extended.yaml');
+		$extended = $writeMeta->getYaml('cache', 'structure-extended.yaml');
 
 		# flag for changed structure
 		$structure = false;
@@ -218,7 +220,7 @@ class MetaApiController extends ContentController
 					$pathWithoutFile 	= str_replace($this->item->originalName, "", $this->item->path);
 					$newPathWithoutType	= $pathWithoutFile . $datetime . '-' . $this->item->slug;
 
-					$writeYaml->renamePost($this->item->pathWithoutType, $newPathWithoutType);
+					$writeMeta->renamePost($this->item->pathWithoutType, $newPathWithoutType);
 
 					# recreate the draft structure
 					$this->setStructure($draft = true, $cache = false);
@@ -235,11 +237,11 @@ class MetaApiController extends ContentController
 
 				if($metaInput['contains'] == "posts")
 				{
-					$writeYaml->transformPagesToPosts($this->item);
+					$writeMeta->transformPagesToPosts($this->item);
 				}
 				if($metaInput['contains'] == "pages")
 				{
-					$writeYaml->transformPostsToPages($this->item);
+					$writeMeta->transformPostsToPages($this->item);
 				}
 			}
 
@@ -273,12 +275,12 @@ class MetaApiController extends ContentController
 		$meta[$tab] = $metaInput;
 
 		# store the metadata
-		$writeYaml->updateYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml', $meta);
+		$writeMeta->updateYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml', $meta);
 
 		if($structure)
 		{
 			# store the extended file
-			$writeYaml->updateYaml('cache', 'structure-extended.yaml', $extended);
+			$writeMeta->updateYaml('cache', 'structure-extended.yaml', $extended);
 
 			# recreate the draft structure
 			$this->setStructure($draft = true, $cache = false);
@@ -309,6 +311,4 @@ class MetaApiController extends ContentController
 		}
 		return true;
 	}
-}
-
-# check models -> writeYaml for getPageMeta and getPageMetaDefaults.
+}

+ 20 - 41
system/Controllers/PageController.php

@@ -6,6 +6,7 @@ use Typemill\Models\Folder;
 use Typemill\Models\WriteCache;
 use Typemill\Models\WriteSitemap;
 use Typemill\Models\WriteYaml;
+use Typemill\Models\WriteMeta;
 use \Symfony\Component\Yaml\Yaml;
 use Typemill\Models\VersionCheck;
 use Typemill\Models\Helpers;
@@ -30,7 +31,6 @@ class PageController extends Controller
 		$item			= false;
 		$home			= false;
 		$breadcrumb 	= false;
-		$description	= '';
 		$settings		= $this->c->get('settings');
 		$pathToContent	= $settings['rootPath'] . $settings['contentFolder'];
 		$cache 			= new WriteCache();
@@ -61,10 +61,6 @@ class PageController extends Controller
 					/* update sitemap */
 					$sitemap = new WriteSitemap();
 					$sitemap->updateSitemap('cache', 'sitemap.xml', 'lastSitemap.txt', $structure, $uri->getBaseUrl());
-
-					/* check and update the typemill-version in the user settings */
-					# this version check is not needed 
-					# $this->updateVersion($uri->getBaseUrl());
 				}
 			}
 			
@@ -89,7 +85,7 @@ class PageController extends Controller
 		if(empty($args))
 		{
 			$home = true;
-			$item = Folder::getItemForUrl($structure, $uri->getBasePath(), $uri->getBasePath());
+			$item = Folder::getItemForUrl($navigation, $uri->getBasePath(), $uri->getBasePath());
 		}
 		else
 		{
@@ -110,10 +106,10 @@ class PageController extends Controller
 			$breadcrumb = $this->c->dispatcher->dispatch('onBreadcrumbLoaded', new OnBreadcrumbLoaded($breadcrumb))->getData();
 
 			# set pages active for navigation again
-			Folder::getBreadcrumb($navigation, $item->keyPathArray);
+			Folder::getBreadcrumb($structure, $item->keyPathArray);
 			
 			/* add the paging to the item */
-			$item = Folder::getPagingForItem($structure, $item);
+			$item = Folder::getPagingForItem($navigation, $item);
 		}
 
 		# dispatch the item
@@ -126,6 +122,9 @@ class PageController extends Controller
 		if($item->elementType == 'folder')
 		{
 			$filePath 	= $filePath . DIRECTORY_SEPARATOR . 'index.md';
+
+			# use navigation instead of structure to get
+			$item = Folder::getItemForUrl($navigation, $urlRel, $uri->getBasePath());
 		}
 
 		# read the content of the file
@@ -135,13 +134,10 @@ class PageController extends Controller
 		$this->c->dispatcher->dispatch('onOriginalLoaded', new OnOriginalLoaded($contentMD));
 		
 		# get meta-Information
-		$writeYaml 		= new WriteYaml();
-		$metatabs 		= $writeYaml->getPageMeta($settings, $item);
+		$writeMeta 		= new WriteMeta();
 
-		if(!$metatabs)
-		{
-			$metatabs 	= $writeYaml->getPageMetaDefaults($contentMD, $settings, $item);
-		}
+		# makes sure that you always have the full meta with title, description and all the rest.
+		$metatabs 		= $writeMeta->completePageMeta($contentMD, $settings, $item);
 
 		# dispatch meta 
 		$metatabs 		= $this->c->dispatcher->dispatch('onMetaLoaded', new OnMetaLoaded($metatabs))->getData();
@@ -149,20 +145,20 @@ class PageController extends Controller
 		# dispatch content
 		$contentMD 		= $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
 
+		$itemUrl 		= isset($item->urlRel) ? $item->urlRel : false;
+
 		/* initialize parsedown */
-		$parsedown 		= new ParsedownExtension();
+		$parsedown 		= new ParsedownExtension($settings['headlineanchors']);
 		
 		/* set safe mode to escape javascript and html in markdown */
 		$parsedown->setSafeMode(true);
 
 		/* parse markdown-file to content-array */
-		$contentArray 	= $parsedown->text($contentMD);
+		$contentArray 	= $parsedown->text($contentMD, $itemUrl);
 		$contentArray 	= $this->c->dispatcher->dispatch('onContentArrayLoaded', new OnContentArrayLoaded($contentArray))->getData();
 		
 		/* get the first image from content array */
 		$firstImage		= $this->getFirstImage($contentArray);
-
-		$itemUrl 		= isset($item->urlRel) ? $item->urlRel : false;
 		
 		/* parse markdown-content-array to content-string */
 		$contentHTML	= $parsedown->markup($contentArray, $itemUrl);
@@ -172,25 +168,7 @@ class PageController extends Controller
 		$contentParts	= explode("</h1>", $contentHTML);
 		$title			= isset($contentParts[0]) ? strip_tags($contentParts[0]) : $settings['title'];
 		
-		$contentHTML	=  isset($contentParts[1]) ? $contentParts[1] : $contentHTML;
-
-		# if there is not meta description 
-		if(!isset($metatabs['meta']['description']) or !$metatabs['meta']['description'])
-		{
-			# 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);
-			}
-		}
+		$contentHTML	= isset($contentParts[1]) ? $contentParts[1] : $contentHTML;
 
 		/* get url and alt-tag for first image, if exists */
 		if($firstImage)
@@ -208,7 +186,7 @@ class PageController extends Controller
 		$route = empty($args) && isset($settings['themes'][$theme]['cover']) ? '/cover.twig' : '/index.twig';
 
 		# check if there is a custom theme css
-		$customcss = $writeYaml->checkFile('cache', $theme . '-custom.css');
+		$customcss = $writeMeta->checkFile('cache', $theme . '-custom.css');
 		if($customcss)
 		{
 			$this->c->assets->addCSS($base_url . '/cache/' . $theme . '-custom.css');
@@ -262,10 +240,10 @@ class PageController extends Controller
 		$yaml = new writeYaml();
 		$extended = $yaml->getYaml('cache', 'structure-extended.yaml');
 
-		/* create an array of object with the whole content of the folder */
+		# create an array of object with the whole content of the folder
 		$structure = Folder::getFolderContentDetails($structure, $extended, $uri->getBaseUrl(), $uri->getBasePath());
 
-		/* cache structure */
+		# cache structure
 		$cache->updateCache('cache', 'structure.txt', 'lastCache.txt', $structure);
 
 		if($extended && $this->containsHiddenPages($extended))
@@ -282,7 +260,8 @@ class PageController extends Controller
 			$cache->deleteFileWithPath('cache' . DIRECTORY_SEPARATOR . 'navigation.txt');
 		}
 		
-		return $structure;
+		# load and return the cached structure, because might be manipulated with navigation....
+		return 	$this->getCachedStructure($cache);
 	}
 	
 	protected function containsHiddenPages($extended)

+ 9 - 8
system/Controllers/SettingsController.php

@@ -57,15 +57,16 @@ class SettingsController extends Controller
 			{
 				/* make sure only allowed fields are stored */
 				$newSettings = array(
-					'title' 		=> $newSettings['title'],
-					'author' 		=> $newSettings['author'],
-					'copyright' 	=> $newSettings['copyright'],
-					'year'			=> $newSettings['year'],
-					'language'		=> $newSettings['language'],
-					'editor' 		=> $newSettings['editor'], 
-					'formats'		=> $newSettings['formats'],
+					'title' 			=> $newSettings['title'],
+					'author' 			=> $newSettings['author'],
+					'copyright' 		=> $newSettings['copyright'],
+					'year'				=> $newSettings['year'],
+					'language'			=> $newSettings['language'],
+					'editor' 			=> $newSettings['editor'], 
+					'formats'			=> $newSettings['formats'],
+					'headlineanchors'	=> isset($newSettings['headlineanchors']) ? $newSettings['headlineanchors'] : null,
 				);
-				
+
 				# https://www.slimframework.com/docs/v3/cookbook/uploading-files.html; 
 
 				$copyright 			= $this->getCopyright();

+ 1 - 1
system/Controllers/SetupController.php

@@ -52,7 +52,7 @@ class SetupController extends Controller
 		$setuperrors = empty($systemcheck) ? false : 'Some system requirements for Typemill are missing.';
 		$systemcheck = empty($systemcheck) ? false : $systemcheck;
 
-		return $this->render($response, 'auth/setup.twig', array( 'messages' => $setuperror, 'systemcheck' => $systemcheck ));
+		return $this->render($response, 'auth/setup.twig', array( 'messages' => $setuperrors, 'systemcheck' => $systemcheck ));
 	}
 
 	public function create($request, $response, $args)

+ 37 - 15
system/Extensions/ParsedownExtension.php

@@ -6,10 +6,13 @@ use \URLify;
 
 class ParsedownExtension extends \ParsedownExtra
 {
-	function __construct()
+	function __construct($showAnchor = NULL)
     {
 		parent::__construct();
 
+        # show anchor next to headline? 
+        $this->showAnchor = $showAnchor;
+
         # math support
         $this->BlockTypes['\\'][] = 'Math';
         $this->BlockTypes['$'][] = 'Math';
@@ -30,8 +33,10 @@ class ParsedownExtension extends \ParsedownExtra
         $this->visualMode = true;
     }
 
-	public function text($text)
+	public function text($text, $relurl = null)
 	{
+        $this->relurl = $relurl ? $relurl : '';
+
         $Elements = $this->textElements($text);
 		
 		return $Elements;
@@ -117,20 +122,37 @@ class ParsedownExtension extends \ParsedownExtra
             {
                 return;
             }
-    
+
             $text = trim($text, ' ');
-    						
-			$Block = array(
-				'element' => array(
-					'name' => 'h' . min(6, $level),
-					'text' => $text,
-					'handler' => 'line',
-					'attributes' => array(
-						'id' => "$headline"
-					)
-				)
-			);
-			
+
+            $Block = array(
+                'element' => array(
+                    'name' => 'h' . min(6, $level),
+                    'text' => $text,
+                    'handler' => 'line',
+                    'attributes' => array(
+                        'id' => "$headline"
+                    )
+                )
+            );
+
+            if($this->showAnchor && $level > 1)
+            {
+                $Block['element']['elements'] = array(
+                            array(
+                                'name' => 'a',
+                                'attributes' => array(
+                                    'href' => $this->relurl . "#" . $headline,
+                                    'class' => 'tm-heading-anchor',
+                                ),
+                                'text' => '#',
+                            ),
+                            array(
+                                'text' => $text,
+                            )
+                        );
+            }
+
 			$this->headlines[]	= array('level' => $level, 'name' => $Block['element']['name'], 'attribute' => $Block['element']['attributes']['id'], 'text' => $text);
 
             return $Block;

+ 24 - 3
system/Extensions/TwigMetaExtension.php

@@ -2,7 +2,7 @@
 
 namespace Typemill\Extensions;
 
-use Typemill\Models\WriteYaml;
+use Typemill\Models\WriteMeta;
 
 class TwigMetaExtension extends \Twig_Extension
 {
@@ -15,9 +15,30 @@ class TwigMetaExtension extends \Twig_Extension
 		
 	public function getMeta($settings, $item)
 	{
-		$write = new WriteYaml();
+		$writeMeta = new WriteMeta();
 		
-		$meta = $write->getPageMeta($settings, $item);
+		$meta = $writeMeta->getPageMeta($settings, $item);
+
+		if(!$meta OR $meta['meta']['title'] == '' OR $meta['meta']['description'] == '')
+		{
+			# create path to the file
+			$filePath	= $settings['rootPath'] . $settings['contentFolder'] . $item->path;
+			
+			# check if url is a folder and add index.md 
+			if($item->elementType == 'folder')
+			{
+				$filePath 	= $filePath . DIRECTORY_SEPARATOR . 'index.md';
+			}
+
+			if(file_exists($filePath))
+			{
+				# get content
+				$content = file_get_contents($filePath);
+
+				# completes title and description or generate default meta values
+				$meta = $writeMeta->completePageMeta($content, $settings, $item);
+			}
+		}
 		
 		return $meta;
 	}

+ 6 - 1
system/Models/ProcessAssets.php

@@ -98,7 +98,7 @@ class ProcessAssets
 		return (count(scandir($dir)) == 2);
 	}
 
-	public function setFileName($originalname, $type, $overwrite = null)
+	public function setFileName($originalname, $type, $overwrite = NULL)
 	{
 		$pathinfo			= pathinfo($originalname);
 		
@@ -135,6 +135,11 @@ class ProcessAssets
 		return $this->filename;
 	}
 
+	public function setExtension($extension)
+	{
+		$this->extension = $extension;
+	}
+
 	public function getExtension()
 	{
 		return $this->extension;

+ 24 - 15
system/Models/ProcessImage.php

@@ -5,7 +5,7 @@ use Typemill\Models\Helpers;
 
 class ProcessImage extends ProcessAssets
 {
-	public function createImage(string $image, string $name, array $desiredSizes)
+	public function createImage(string $image, string $name, array $desiredSizes, $overwrite = NULL)
 	{
 		# fix error from jpeg-library
 		ini_set ('gd.jpeg_ignore_warning', 1);
@@ -15,13 +15,15 @@ class ProcessImage extends ProcessAssets
 		$this->clearTempFolder();
 
 		# set the name of the image 
-		$this->setFileName($name, 'image');
+		$this->setFileName($name, 'image', $overwrite);
 
 		# decode the image from base64-string
 		$imageDecoded	= $this->decodeImage($image);
 		$imageData		= $imageDecoded["image"];
 		$imageType		= $imageDecoded["type"];
 		
+		$this->setExtension($imageType);
+
 		# transform image-stream into image
 		$image 			= imagecreatefromstring($imageData);
 		
@@ -38,7 +40,7 @@ class ProcessImage extends ProcessAssets
 		$tmpname = fopen($this->tmpFolder . $this->getName() . '.' . $imageType .  ".txt", "w");
 
 		$this->saveOriginal($this->tmpFolder, $imageData, $name = 'original', $imageType);
-			
+
 		# temporary store resized images
 		foreach($resizedImages as $key => $resizedImage)
 		{
@@ -71,8 +73,8 @@ class ProcessImage extends ProcessAssets
 		{
 			$tmpname = str_replace('.txt', '', basename($imagename));
 
-			# set extension and sanitize name
-			$this->setFileName($tmpname, 'image');
+			# set extension and sanitize name. Overwrite because this has been checked before
+			$this->setFileName($tmpname, 'image', $overwrite = true);
 
 			unlink($imagename);
 		}
@@ -80,7 +82,7 @@ class ProcessImage extends ProcessAssets
 		$name 			=  uniqid();
 
 		if($this->filename && $this->extension)
-		{			
+		{
 			$name 		= $this->filename;
 		}
 
@@ -110,7 +112,7 @@ class ProcessImage extends ProcessAssets
 		
 		if($success)
 		{
-			return true;
+			# return true;
 			return 'media/live/' . $name . '.' . $tmpfilename[1];
 		}
 		
@@ -201,7 +203,9 @@ class ProcessImage extends ProcessAssets
 
 	# save resized images in temporary folder
 	public function saveImage($folder, $image, $name, $type)
-	{		
+	{
+		$type = strtolower($type);
+
 		if($type == "png")
 		{
 			$result = imagepng( $image, $folder . $name . '.png' );
@@ -210,10 +214,14 @@ class ProcessImage extends ProcessAssets
 		{
 			$result = imagegif( $image, $folder . $name . '.gif' );
 		}
+		elseif($type == "jpg" OR $type == "jpeg")
+		{
+			$result = imagejpeg( $image, $folder . $name . '.' . $type );
+		}
 		else
 		{
-			$result = imagejpeg( $image, $folder . $name . '.jpeg' );
-			$type = 'jpeg';
+			# image type not supported
+			return false;
 		}
 		
 		imagedestroy($image);
@@ -339,12 +347,13 @@ class ProcessImage extends ProcessAssets
 	{
 		$this->setFileName($filename, 'image', $overwrite = true);
 
-		if($this->extension == 'jpeg') $this->extension = 'jpg';
+		# if($this->extension == 'jpg') $this->extension = 'jpeg';
 		
 		switch($this->extension)
 		{
 			case 'gif': $image = imagecreatefromgif($this->liveFolder . $filename); break;
-			case 'jpg': $image = imagecreatefromjpeg($this->liveFolder . $filename); break;
+			case 'jpg' :
+			case 'jpeg': $image = imagecreatefromjpeg($this->liveFolder . $filename); break;
 			case 'png': $image = imagecreatefrompng($this->liveFolder . $filename); break;
 			default: return 'image type not supported';
 		}
@@ -367,12 +376,13 @@ class ProcessImage extends ProcessAssets
 	{
 		$this->setFileName($filename, 'image');
 
-		if($this->extension == 'jpeg') $this->extension = 'jpg';
+		if($this->extension == 'jpg') $this->extension = 'jpeg';
 		
 		switch($this->extension)
 		{
 			case 'gif': $image = imagecreatefromgif($image); break;
-			case 'jpg': $image = imagecreatefromjpeg($image); break;
+			case 'jpg' :
+			case 'jpeg': $image = imagecreatefromjpeg($image); break;
 			case 'png': $image = imagecreatefrompng($image); break;
 			default: return 'image type not supported';
 		}
@@ -383,5 +393,4 @@ class ProcessImage extends ProcessAssets
 
 		return $resizedImages;
 	}
-
 }

+ 348 - 0
system/Models/WriteMeta.php

@@ -0,0 +1,348 @@
+<?php
+
+namespace Typemill\Models;
+
+use Typemill\Extensions\ParsedownExtension;
+
+class WriteMeta extends WriteYaml
+{
+	# used by contentApiController (backend) and pageController (frontend) and TwigMetaExtension (list pages)
+	public function getPageMeta($settings, $item)
+	{
+		$meta = $this->getYaml($settings['contentFolder'], $item->pathWithoutType . '.yaml');
+
+		if(!$meta)
+		{
+			return false;
+		}
+
+		# compare with meta that are in use right now (e.g. changed theme, disabled plugin)
+		$metascheme = $this->getYaml('cache', 'metatabs.yaml');
+
+		if($metascheme)
+		{
+			$meta = $this->whitelistMeta($meta,$metascheme);
+		}
+
+		$meta = $this->addFileTimeToMeta($meta, $item, $settings);
+
+		return $meta;
+	}
+
+	# cases are rare: updates from old version prior 1.3.4 or if content-files are added manually, e.g. by ftp
+	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 = $this->generateDescription($content, $parsedown, $item);
+
+		$author = $settings['author'];
+
+		if(isset($_SESSION))
+		{
+			if(isset($_SESSION['firstname']) && $_SESSION['firstname'] !='' && isset($_SESSION['lastname']) && $_SESSION['lastname'] != '')
+			{
+				$author = $_SESSION['firstname'] . ' ' . $_SESSION['lastname'];
+			}
+			elseif(isset($_SESSION['user']))
+			{
+				$author = $_SESSION['user'];
+			}
+		}
+
+		# create new meta-file
+		$meta = [
+			'meta' => [
+				'title' 		=> $title,
+				'description' 	=> $description,
+				'author' 		=> $author,
+				'created'		=> date("Y-m-d"),
+				'time'			=> date("H-i-s"),
+				'navtitle'		=> $item->name,
+			]
+		];
+
+		$meta = $this->addFileTimeToMeta($meta, $item, $settings);
+
+		$this->updateYaml($settings['contentFolder'], $item->pathWithoutType . '.yaml', $meta);
+		
+		return $meta;
+	}
+
+	# used by MetaApiController. Do not set title or description in defaults if page is not published yet
+	public function getPageMetaBlank($content, $settings, $item)
+	{
+		$author = $settings['author'];
+
+		if(isset($_SESSION))
+		{
+			if(isset($_SESSION['firstname']) && $_SESSION['firstname'] !='' && isset($_SESSION['lastname']) && $_SESSION['lastname'] != '')
+			{
+				$author = $_SESSION['firstname'] . ' ' . $_SESSION['lastname'];
+			}
+			elseif(isset($_SESSION['user']))
+			{
+				$author = $_SESSION['user'];
+			}
+		}
+
+		# create new meta-file
+		$meta = [
+			'meta' => [
+				'title' 		=> '',
+				'description' 	=> '',
+				'author' 		=> $author,
+				'created'		=> date("Y-m-d"),
+				'time'			=> date("H-i-s"),
+				'navtitle'		=> $item->name
+			]
+		];
+
+		$meta = $this->addFileTimeToMeta($meta, $item, $settings);
+
+		$this->updateYaml($settings['contentFolder'], $item->pathWithoutType . '.yaml', $meta);
+		
+		return $meta;
+	}
+
+	public function getNavtitle($url)
+	{
+		# get the extended structure where the navigation title is stored
+		$extended = $this->getYaml('cache', 'structure-extended.yaml');
+		
+		if(isset($extended[$url]['navtitle']))
+		{ 
+			return $extended[$url]['navtitle'];
+		}
+		return '';
+	}
+
+	# used by articleApiController and pageController to add title and description if an article is published
+	public function completePageMeta($content, $settings, $item)
+	{
+		$meta = $this->getPageMeta($settings, $item);
+
+		if(!$meta)
+		{
+			return $this->getPageMetaDefaults($content, $settings, $item);
+		}
+
+		$title = (isset($meta['meta']['title']) AND $meta['meta']['title'] !== '') ? true : false;
+		$description = (isset($meta['meta']['description']) AND $meta['meta']['description'] !== '') ? true : false;
+
+		if($title && $description)
+		{
+			return $meta;
+		}
+
+		# 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);
+		}
+
+		# delete markdown from title
+		if(!$title && isset($content[0]))
+		{
+			$meta['meta']['title'] = trim($content[0], "# ");
+		}
+
+		if(!$description && isset($content[1]))
+		{
+			$meta['meta']['description'] = $this->generateDescription($content, $parsedown, $item);
+		}
+
+		$this->updateYaml($settings['contentFolder'], $item->pathWithoutType . '.yaml', $meta);
+		
+		return $meta;
+	}
+
+	private function whitelistMeta($meta, $metascheme)
+	{
+		# we have only 2 dimensions, so no recursive needed
+		foreach($meta as $tab => $values)
+		{
+			if(!isset($metascheme[$tab]))
+			{
+				unset($meta[$tab]);
+			}
+			foreach($values as $key => $value)
+			{
+				if(!isset($metascheme[$tab][$key]))
+				{
+					unset($meta[$tab][$key]);
+				}
+			}
+		}
+		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)) : date("Y-m-d");
+
+		return $meta;
+	}
+
+	public function generateDescription($content, $parsedown, $item)
+	{
+		$description = isset($content[1]) ? $content[1] : '';
+
+		# create description or abstract from content
+		if($description !== '')
+		{
+			$firstLineArray = $parsedown->text($description);
+			$description 	= strip_tags($parsedown->markup($firstLineArray, $item->urlAbs));
+
+			# if description is very short
+			if(strlen($description) < 100 && isset($content[2]))
+			{
+				$secondLineArray = $parsedown->text($content[2]);
+				$description 	.= ' ' . strip_tags($parsedown->markup($secondLineArray, $item->urlAbs));
+			}
+
+			# if description is too long
+			if(strlen($description) > 300)
+			{
+				$description	= substr($description, 0, 300);
+				$lastSpace 		= strrpos($description, ' ');
+				$description 	= substr($description, 0, $lastSpace);
+			}
+		}
+		return $description;
+	}
+
+	public function transformPagesToPosts($folder){
+
+		$filetypes			= array('md', 'txt', 'yaml');
+
+		foreach($folder->folderContent as $page)
+		{
+			# create old filename without filetype
+			$oldFile 	= $this->basePath . 'content' . $page->pathWithoutType;
+
+			# set default date
+			$date 		= date('Y-m-d', time());
+			$time		= date('H-i', time());
+
+			$meta 		= $this->getYaml('content', $page->pathWithoutType . '.yaml');
+
+			if($meta)
+			{
+				# get dates from meta
+				if(isset($meta['meta']['manualdate'])){ $date = $meta['meta']['manualdate']; }
+				elseif(isset($meta['meta']['created'])){ $date = $meta['meta']['created']; }
+				elseif(isset($meta['meta']['modified'])){ $date = $meta['meta']['modified']; }
+
+				# set time
+				if(isset($meta['meta']['time']))
+				{
+					$time = $meta['meta']['time'];
+				}
+			}
+
+			$datetime 	= $date . '-' . $time;
+			$datetime 	= implode(explode('-', $datetime));
+			$datetime	= substr($datetime,0,12);
+
+			# create new file-name without filetype
+			$newFile 	= $this->basePath . 'content' . $folder->path . DIRECTORY_SEPARATOR . $datetime . '-' . $page->slug;
+
+			$result 	= true;
+
+			foreach($filetypes as $filetype)
+			{
+				$oldFilePath = $oldFile . '.' . $filetype;
+				$newFilePath = $newFile . '.' . $filetype;
+				
+				#check if file with filetype exists and rename
+				if($oldFilePath != $newFilePath && file_exists($oldFilePath))
+				{
+					if(@rename($oldFilePath, $newFilePath))
+					{
+						$result = $result;
+					}
+					else
+					{
+						$result = false;
+					}
+				}
+			}
+		}
+	}
+
+	public function transformPostsToPages($folder){
+
+		$filetypes			= array('md', 'txt', 'yaml');
+		$index				= 0;
+
+		foreach($folder->folderContent as $page)
+		{
+			# create old filename without filetype
+			$oldFile 	= $this->basePath . 'content' . $page->pathWithoutType;
+
+			$order 		= $index;
+
+			if($index < 10)
+			{
+				$order = '0' . $index;
+			}
+
+			# create new file-name without filetype
+			$newFile 	= $this->basePath . 'content' . $folder->path . DIRECTORY_SEPARATOR . $order . '-' . $page->slug;
+
+			$result 	= true;
+
+			foreach($filetypes as $filetype)
+			{
+				$oldFilePath = $oldFile . '.' . $filetype;
+				$newFilePath = $newFile . '.' . $filetype;
+				
+				#check if file with filetype exists and rename
+				if($oldFilePath != $newFilePath && file_exists($oldFilePath))
+				{
+					if(@rename($oldFilePath, $newFilePath))
+					{
+						$result = $result;
+					}
+					else
+					{
+						$result = false;
+					}
+				}
+			}
+
+			$index++;
+		}
+	}
+}

+ 0 - 233
system/Models/WriteYaml.php

@@ -2,8 +2,6 @@
 
 namespace Typemill\Models;
 
-use Typemill\Extensions\ParsedownExtension;
-
 class WriteYaml extends Write
 {
 	/**
@@ -37,235 +35,4 @@ 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;
-		}
-
-		# compare with meta that are in use right now (e.g. changed theme, disabled plugin)
-		$metascheme = $this->getYaml('cache', 'metatabs.yaml');
-
-		if($metascheme)
-		{
-			$meta = $this->whitelistMeta($meta,$metascheme);
-		}
-
-		$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);
-		}
-
-		$author = $settings['author'];
-
-		if(isset($_SESSION))
-		{
-			if(isset($_SESSION['firstname']) && $_SESSION['firstname'] !='' && isset($_SESSION['lastname']) && $_SESSION['lastname'] != '')
-			{
-				$author = $_SESSION['firstname'] . ' ' . $_SESSION['lastname'];
-			}
-			elseif(isset($_SESSION['user']))
-			{
-				$author = $_SESSION['user'];
-			}
-		}
-
-		# create new meta-file
-		$meta = [
-			'meta' => [
-				'title' 		=> $title,
-				'description' 	=> $description,
-				'author' 		=> $author,
-				'created'		=> date("Y-m-d"),
-				'time'			=> date("H-i-s"),
-			]
-		];
-
-		$this->updateYaml($settings['contentFolder'], $item->pathWithoutType . '.yaml', $meta);
-		
-		$meta = $this->addFileTimeToMeta($meta, $item, $settings);
-
-		return $meta;
-	}
-
-
-	private function whitelistMeta($meta, $metascheme)
-	{
-		# we have only 2 dimensions, so no recursive needed
-		foreach($meta as $tab => $values)
-		{
-			if(!isset($metascheme[$tab]))
-			{
-				unset($meta[$tab]);
-			}
-			foreach($values as $key => $value)
-			{
-				if(!isset($metascheme[$tab][$key]))
-				{
-					unset($meta[$tab][$key]);
-				}
-			}
-		}
-		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;
-	}
-
-
-	public function transformPagesToPosts($folder){
-
-		$filetypes			= array('md', 'txt', 'yaml');
-
-		foreach($folder->folderContent as $page)
-		{
-			# create old filename without filetype
-			$oldFile 	= $this->basePath . 'content' . $page->pathWithoutType;
-
-			# set default date
-			$date 		= date('Y-m-d', time());
-			$time		= date('H-i', time());
-
-			$meta 		= $this->getYaml('content', $page->pathWithoutType . '.yaml');
-
-			if($meta)
-			{
-				# get dates from meta
-				if(isset($meta['meta']['manualdate'])){ $date = $meta['meta']['manualdate']; }
-				elseif(isset($meta['meta']['created'])){ $date = $meta['meta']['created']; }
-				elseif(isset($meta['meta']['modified'])){ $date = $meta['meta']['modified']; }
-
-				# set time
-				if(isset($meta['meta']['time']))
-				{
-					$time = $meta['meta']['time'];
-				}
-			}
-
-			$datetime 	= $date . '-' . $time;
-			$datetime 	= implode(explode('-', $datetime));
-			$datetime	= substr($datetime,0,12);
-
-			# create new file-name without filetype
-			$newFile 	= $this->basePath . 'content' . $folder->path . DIRECTORY_SEPARATOR . $datetime . '-' . $page->slug;
-
-			$result 	= true;
-
-			foreach($filetypes as $filetype)
-			{
-				$oldFilePath = $oldFile . '.' . $filetype;
-				$newFilePath = $newFile . '.' . $filetype;
-				
-				#check if file with filetype exists and rename
-				if($oldFilePath != $newFilePath && file_exists($oldFilePath))
-				{
-					if(@rename($oldFilePath, $newFilePath))
-					{
-						$result = $result;
-					}
-					else
-					{
-						$result = false;
-					}
-				}
-			}
-		}
-	}
-
-	public function transformPostsToPages($folder){
-
-		$filetypes			= array('md', 'txt', 'yaml');
-		$index				= 0;
-
-		foreach($folder->folderContent as $page)
-		{
-			# create old filename without filetype
-			$oldFile 	= $this->basePath . 'content' . $page->pathWithoutType;
-
-			$order 		= $index;
-
-			if($index < 10)
-			{
-				$order = '0' . $index;
-			}
-
-			# create new file-name without filetype
-			$newFile 	= $this->basePath . 'content' . $folder->path . DIRECTORY_SEPARATOR . $order . '-' . $page->slug;
-
-			$result 	= true;
-
-			foreach($filetypes as $filetype)
-			{
-				$oldFilePath = $oldFile . '.' . $filetype;
-				$newFilePath = $newFile . '.' . $filetype;
-				
-				#check if file with filetype exists and rename
-				if($oldFilePath != $newFilePath && file_exists($oldFilePath))
-				{
-					if(@rename($oldFilePath, $newFilePath))
-					{
-						$result = $result;
-					}
-					else
-					{
-						$result = false;
-					}
-				}
-			}
-
-			$index++;
-		}
-	}
 }

+ 4 - 3
system/Settings.php

@@ -27,14 +27,14 @@ class Settings
 			$themes = array_diff(scandir($themefolder), array('..', '.'));
 			$firsttheme = reset($themes);
 
-			# if there is a theme with valid theme settings-file
-			if($firsttheme && self::getObjectSettings('themes', $firsttheme))
+			# if there is a theme with an index.twig-file
+			if($firsttheme && file_exists($themefolder . $firsttheme . DIRECTORY_SEPARATOR . 'index.twig'))
 			{
 				$settings['theme'] = $firsttheme;
 			}
 			else
 			{
-				die('There is no theme in the theme-folder. Please add a theme from https://themes.typemill.net');
+				die('You need at least one theme with an index.twig-file in your theme-folder.');
 			}
 		}
 
@@ -166,6 +166,7 @@ class Settings
 									'startpage' => true,
 									'author' => true,
 									'year' => true,
+									'headlineanchors' => true,
 									'theme' => true,
 									'editor' => true,
 									'formats' => true,

+ 13 - 3
system/author/css/style.css

@@ -141,18 +141,28 @@ a.tm-download::before{
 	width: 30px;
 	height: 30px;
 	line-height: 30px;
-	font-family: "Comic Sans MS",cursive,sans-serif;	
+	font-family: "Comic Sans MS",cursive,sans-serif;
+	font-size: 1.3em;
+	font-weight: 900;	
 	border: 2px solid #e0474c;
 	border-radius: 50%;
 	text-align: center;
-	text-decoration: underline;
+	text-decoration: none;
 }
 a.tm-download:hover::before{
-	text-decoration:underline;
+	text-decoration: none;
 	color: #fff;
 	background: #e0474c;
 }
 
+a.tm-heading-anchor {
+    display: none;
+    position: absolute;
+    top: 0;
+    left: -1em;
+    width: 1em;
+    opacity: 0;
+}
 
 /********************
 *  	COMMONS 	    *

+ 0 - 0
system/author/js/lazy-video.js → system/author/js/typemillutils.js


+ 8 - 1
system/author/js/vue-blox-config.js

@@ -35,8 +35,15 @@ let determiner = {
 		}
 		return false;
 	},
+	video: function(block,lines,firstChar,secondChar,thirdChar){
+		if( (firstChar == '!' && secondChar == '[' && lines[0].indexOf('.youtube') != -1) || (firstChar == '[' && secondChar == '!' && lines[0].indexOf('.youtube') != -1) )
+		{
+			return "video-component";
+		}
+		return false;
+	},
 	image: function(block,lines,firstChar,secondChar,thirdChar){
-		if( (firstChar == '!' && secondChar == '[') || (firstChar == '[' && secondChar == '!' && thirdChar == '[') )
+		if( (firstChar == '!' && secondChar == '[' ) || (firstChar == '[' && secondChar == '!' && thirdChar == '[') )
 		{
 			return "image-component";
 		}

+ 40 - 17
system/author/js/vue-blox.js

@@ -253,6 +253,16 @@ const contentComponent = Vue.component('content-block', {
 					{
 						params.new = true;
 					}
+					else
+					{
+						var oldVideoID = this.$root.$data.blockMarkdown.match(/#.*? /);
+						if(this.compmarkdown.indexOf(oldVideoID[0].substring(1).trim()) !== -1)
+						{
+							this.activatePage();
+							this.switchToPreviewMode();	
+							return;
+						}
+					}
 				}
 				else if(this.componentType == 'file-component')
 				{
@@ -282,7 +292,7 @@ const contentComponent = Vue.component('content-block', {
 						self.activatePage();
 						publishController.errors.message = "Looks like you are logged out. Please login and try again.";
 					}
-					else if(response)					
+					else if(response)			
 					{
 						self.activatePage();
 
@@ -342,9 +352,14 @@ const contentComponent = Vue.component('content-block', {
 							self.$root.checkMath(result.id);
 
 							/* check youtube here */
-							if(thisBlockType == "video-component" || thisBlockType == "image-component")
+							if(thisBlockType == "video-component")
 							{
-								self.$root.checkVideo(result.id);
+								setTimeout(function(){ 
+									self.$nextTick(function () 
+									{
+										self.$root.checkVideo(result.id);
+									});
+								}, 300);
 							}
 
 							/* update the navigation and mark navigation item as modified */
@@ -1192,7 +1207,6 @@ const definitionComponent = Vue.component('definition-component', {
 	},
 })
 
-
 const videoComponent = Vue.component('video-component', {
 	props: ['compmarkdown', 'disabled', 'load'],
 	template: '<div class="video dropbox">' +
@@ -1200,6 +1214,20 @@ const videoComponent = Vue.component('video-component', {
 				'<label for="video">{{ $t(\'Link to video\') }}: </label><input type="url" ref="markdown" placeholder="https://www.youtube.com/watch?v=" :value="compmarkdown" :disabled="disabled" @input="updatemarkdown">' +
 				'<div v-if="load" class="loadwrapper"><span class="load"></span></div>' +
 				'</div>',
+	mounted: function(){
+
+		this.$refs.markdown.focus();
+
+		if(this.compmarkdown)
+		{
+			var videoid = this.compmarkdown.match(/#.*? /);
+			if(videoid)
+			{
+				var event = { 'target': { 'value': 'https://www.youtube.com/watch?v=' + videoid[0].trim().substring(1) }};
+				this.updatemarkdown(event);
+			}
+		}
+	},				
 	methods: {
 		updatemarkdown: function(event)
 		{
@@ -1208,7 +1236,6 @@ const videoComponent = Vue.component('video-component', {
 	},
 })
 
-
 const imageComponent = Vue.component('image-component', {
 	props: ['compmarkdown', 'disabled'],
 	template: '<div class="dropbox">' +
@@ -1231,7 +1258,7 @@ const imageComponent = Vue.component('image-component', {
 				'<label for="imgtitle">{{ $t(\'Title\') }}: </label><input name="imgtitle" type="text" placeholder="title" v-model="imgtitle" @input="createmarkdown" max="64" />' +
 				'<label for="imgcaption">{{ $t(\'Caption\') }}: </label><input title="imgcaption" type="text" placeholder="caption" v-model="imgcaption" @input="createmarkdown" max="140" />' +
 				'<label for="imgurl">{{ $t(\'Link\') }}: </label><input title="imgurl" type="url" placeholder="url" v-model="imglink" @input="createmarkdown" />' +
-				'<label for="imgclass">{{ $t(\'Class\') }}: </label><select title="imgclass" v-model="imgclass" @change="createmarkdown"><option value="center">{{ $t(\'Center\') }}</option><option value="left">{{ $t(\'Left\') }}</option><option value="right">{{ $t(\'Right\') }}</option><option value="youtube">Youtube</option><option value="vimeo">Vimeo</option></select>' +
+				'<label for="imgclass">{{ $t(\'Class\') }}: </label><select title="imgclass" v-model="imgclass" @change="createmarkdown"><option value="center">{{ $t(\'Center\') }}</option><option value="left">{{ $t(\'Left\') }}</option><option value="right">{{ $t(\'Right\') }}</option></select>' +
 				'<input title="imgid" type="hidden" placeholder="id" v-model="imgid" @input="createmarkdown" max="140" />' +
 				'</div></div>',
 	data: function(){
@@ -2266,7 +2293,6 @@ let editor = new Vue({
 			}
 			if(response)
 			{
-
 				var result = JSON.parse(response);
 				
 				if(result.errors)
@@ -2277,7 +2303,7 @@ let editor = new Vue({
 				{
 					self.markdown = result.data;
 					
-					/* make math plugin working */
+					/* activate math plugin */
 					
 					if (typeof renderMathInElement === "function") { 
 						self.$nextTick(function () {
@@ -2419,29 +2445,26 @@ let editor = new Vue({
 		},
 		initiateVideo()
 		{
-			/* check for youtube videos */
+			/* check for youtube videos on first page load */
 			if (typeof typemillUtilities !== "undefined")
 			{
 				this.$nextTick(function () {
-						typemillUtilities.start();
+					typemillUtilities.start();
 				});
 			}
 		},
 		checkVideo(elementid)
 		{
-			/* check for youtube videos */
+			/* check for youtube videos for new blox */
 			var element = document.getElementById("blox-"+elementid);
+
 			if(element && typeof typemillUtilities !== "undefined")
 			{
 				imageElement = element.getElementsByClassName("youtube");
+
 				if(imageElement[0])
 				{
-					setTimeout(function(){ 
-						self.$nextTick(function () 
-						{
-								typemillUtilities.addYoutubePlayButton(imageElement[0]);
-						});
-					}, 300);
+					typemillUtilities.addYoutubePlayButton(imageElement[0]);
 				}
 			}
 		}

+ 3 - 0
system/author/js/vue-navi.js

@@ -178,6 +178,7 @@ const navcomponent = Vue.component('navigation', {
 					{
 						// evt.item.classList.remove("load");
 						self.$root.$data.items = result.data;						
+						self.newItem = '';
 						self.showForm = false;
 					}
 				}
@@ -280,6 +281,7 @@ let navi = new Vue({
 					if(result.data)
 					{
 						self.items = result.data;						
+						self.newItem = '';
 						self.showForm = false;
 					}
 				}
@@ -312,6 +314,7 @@ let navi = new Vue({
 					if(result.data)
 					{
 						self.items = result.data;
+						self.newItem = '';
 						self.homepage = result.homepage;						
 					}
 				}

+ 5 - 0
system/author/js/vue-publishcontroller.js

@@ -69,6 +69,11 @@ let publishController = new Vue({
 					}
 					else
 					{
+						if(result.meta)
+						{
+							meta.formData = result.meta;
+						}
+
 						self.draftDisabled = "disabled";
 						self.publishResult = "success";
 						self.publishStatus = false;

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

@@ -196,6 +196,7 @@
 			const myaxios = axios.create();
 			myaxios.defaults.baseURL =  "{{ base_url }}";
 		</script>
+		<script src="{{ base_url }}/system/author/js/typemillutils.js?20200405"></script>
 		<script src="{{ base_url }}/system/author/js/vue.min.js?20200405"></script>
     	<script src="{{ base_url }}/system/author/js/vue-i18n.min.js?20200405"></script>
 		<script src="{{ base_url }}/system/author/js/autosize.min.js?20200405"></script>
@@ -217,7 +218,6 @@
 		<script src="{{ base_url }}/system/author/js/vuedraggable.umd.min.js?20200405"></script>
 		<script src="{{ base_url }}/system/author/js/vue-navi.js?20200405"></script>
 		<script src="{{ base_url }}/system/author/js/vue-meta.js?20200405"></script>
-		<script src="{{ base_url }}/system/author/js/lazy-video.js?20200405"></script>
 
 		{{ assets.renderJS() }}
 

+ 6 - 6
system/author/partials/fields.twig

@@ -7,7 +7,7 @@
 
 	{% if field.type == 'textarea' %}
 
-		<textarea name="{{ itemName }}[{{ field.name }}]"{{field.getAttributeValues() }}{{ field.getAttributes() }}>{{ field.getContent() }}</textarea>
+		<textarea id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{field.getAttributeValues() }}{{ field.getAttributes() }}>{{ field.getContent() }}</textarea>
 
 	{% elseif field.type == 'paragraph' %}
 		
@@ -16,7 +16,7 @@
 	{% elseif field.type == 'checkbox' %}
 			
 		<label class="control-group">{{ __( field.getCheckboxLabel() ) }}
-			<input type="checkbox" name="{{ itemName}}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
+			<input type="checkbox" id="{{ itemName}}[{{ field.name }}]" name="{{ itemName}}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
 			<span class="checkmark"></span>
 		</label>
 
@@ -27,7 +27,7 @@
 		{% for value,label in options %}
 			
 			<label class="control-group">{{ __( label ) }}
-				<input type="checkbox" name="{{ itemName }}[{{ field.name }}][{{value}}]" {{ settings[object][itemName][field.name][value] ? ' checked' : '' }}>
+				<input type="checkbox" id="{{ itemName }}[{{ field.name }}][{{value}}]" name="{{ itemName }}[{{ field.name }}][{{value}}]" {{ settings[object][itemName][field.name][value] ? ' checked' : '' }}>
 				<span class="checkmark"></span>
 			</label>
 
@@ -37,7 +37,7 @@
 
 		{% set options = field.getOptions() %}
 
-		<select name="{{ itemName }}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
+		<select id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
 			{% for value,label in options %}
 				<option value="{{ value }}" {{ (value == field.getAttributeValue('value')) ? ' selected' : '' }}>{{ label }}</option>
 			{% endfor %}
@@ -50,7 +50,7 @@
 		{% for value,label in options %}
 			
 			<label class="control-group">{{ label }} 
-				<input type="radio" name="{{ itemName }}[{{ field.name }}]" value="{{ value }}" {{ (value == settings[object][itemName][field.name]) ? ' checked' : '' }}>
+				<input type="radio" id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]" value="{{ value }}" {{ (value == settings[object][itemName][field.name]) ? ' checked' : '' }}>
 				<span class="radiomark"></span>
 			</label>
 
@@ -58,7 +58,7 @@
 		
 	{% else %}
 
-		<input name="{{itemName}}[{{ field.name }}]" type="{{ field.type }}"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
+		<input id="{{itemName}}[{{ field.name }}]" name="{{itemName}}[{{ field.name }}]" type="{{ field.type }}"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
 
 	{% endif %}
 

+ 13 - 2
system/author/settings/system.twig

@@ -63,7 +63,12 @@
 					</div><div class="medium">
 						<label for="settings[sitemap]">Google Sitemap <small>({{ __('Readonly') }})</small></label>
 						<input type="text" name="settings[sitemap]" id="sitemap" readonly value="{{ base_url }}/cache/sitemap.xml" />
-					</div><div class="medium{{ errors.settings.logo ? ' error' : '' }}">
+					</div>
+					<hr>
+					<header class="headline">
+						<h2>{{ __('General Presentation') }}</h2>
+					</header>
+					<div class="medium{{ errors.settings.logo ? ' error' : '' }}">
 						<label for="settings[logo]">Logo <small>(jpg,jpeg,png,svg)</small></label>
 						<div class="flex fileinput">
 							<button class="deletefilebutton w-10 bg-tm-gray bn hover-bg-tm-red hover-white">x</button>
@@ -91,6 +96,12 @@
 						{% if errors.settings.favicon %}
 							<span class="error">{{ errors.settings.favicon | first }}</span>
 						{% endif %}
+					</div><div class="medium{{ errors.settings.headlineanchors ? ' error' : '' }}">
+						<label for="settings[headlineanchors]">{{ __('Headline Anchors') }} *</label>
+						<label class="control-group">{{ __('Show anchors next to headlines') }}
+							<input name="settings[headlineanchors]" type="checkbox" {% if (old.settings.headlineanchors) %} checked {% endif %}>
+							<span class="checkmark"></span>
+						</label>
 					</div>
 					<hr>
 					<header class="headline">
@@ -130,4 +141,4 @@
 		</form>
 	
 	</div>
-{% endblock %}
+{% endblock %}

+ 29 - 7
themes/typemill/chapter.twig

@@ -16,14 +16,36 @@
 	{{ content }}
 
 
-	<div class="toc-nav">
-		
-		<ul>
-			{% for element in item.folderContent %}
+	{% if item.contains == 'pages' %}
+		<div class="toc-nav">
+			
+			<ul>
+				{% for element in item.folderContent %}
 
-				<li class="level-2"><a href="{{ element.urlAbs }}">{% if settings.themes.typemill.chapnum %}{{ element.chapter }} {% endif %}{{ element.name }}</a></li>
+					<li class="level-2"><a href="{{ element.urlAbs }}">{% if settings.themes.typemill.chapnum %}{{ element.chapter }} {% endif %}{{ element.name }}</a></li>
+				
+				{% endfor %}
+			</ul>
 			
+		</div>
+
+	{% elseif item.contains == 'posts' %}
+
+		<ul class="post">
+
+			{% for element in item.folderContent %}
+
+				{% set post = getPageMeta(settings, element) %}
+				{% set date = element.order[0:4] ~ '-' ~ element.order[4:2] ~ '-' ~ element.order[6:2] %}
+
+				<li class="post-entry">
+					<a href="{{ element.urlAbs }}"><h2>{{ post.meta.title }}</h2></a>
+					<small><time datetime="{{date}}">{{ date | date("d.m.Y") }}</time> | {{ post.meta.author }}</small>
+					<p>{{ post.meta.description }}</p>
+				</li>
+
 			{% endfor %}
+			
 		</ul>
-		
-	</div>
+
+	{% endif %}	

+ 2 - 0
themes/typemill/cover.twig

@@ -1,5 +1,7 @@
 {% extends '/partials/layoutCover.twig' %}
 
+{% block title %}{{ metatabs.meta.title | default(title) }} | {{ settings.title }}{% endblock %}
+
 {% block content %}
 
 	{% if logo and settings.themes.typemill.coverlogo %}

+ 42 - 8
themes/typemill/css/style.css

@@ -67,7 +67,7 @@ pre,code{
 *  	   HEADLINES    *
 ********************/
 
-h1, h2, h3, h4, h5, h6{ font-weight: 700; line-height: 1em; }
+h1, h2, h3, h4, h5, h6{ font-weight: 700; line-height: 1em; position: relative;}
 h1{ font-size: 2.2em; margin: 1.4em 0 0.6em; }
 h2{ font-size: 1.6em; margin: 1.3em 0 0.6em; }
 h3{	font-size: 1.3em; margin: 1.2em 0 0.6em; }
@@ -350,11 +350,11 @@ article img.youtube{
 	position: relative;
 	max-width: 560px;
 }
-article .video-container{
+.video-container{
 	position: relative;
 	text-align: center;
 }
-article button.play-video { 
+button.play-video { 
 	position: absolute;
 	top: 50%;
 	left: 50%;
@@ -369,11 +369,11 @@ article button.play-video {
 	padding: 0;
 	text-align: center;
 }
-article button.play-video:hover {
+button.play-video:hover {
 	background: #cc4146;
 	cursor: pointer;
 }
-article button.play-video::after {
+button.play-video::after {
 	position: absolute;
 	top: 50%;
 	margin: -20px 0 0 -15px;
@@ -548,6 +548,11 @@ ul,ol{
 	padding-left: 0px;
 	margin-left: 18px;
 }
+ul.post{
+	list-style: none;
+	padding: 0 0 0 0;
+	margin: 0 0 0 0;
+}
 blockquote{
 	border-left: 4px solid #e0474c;
 	background: #f9f8f6;
@@ -600,17 +605,43 @@ a.tm-download::before{
 	width: 30px;
 	height: 30px;
 	line-height: 30px;
-	font-family: "Comic Sans MS",cursive,sans-serif;	
+	font-family: "Comic Sans MS",cursive,sans-serif;
+	font-size: 1.3em;
+	font-weight: 900;	
 	border: 2px solid #e0474c;
 	border-radius: 50%;
 	text-align: center;
-	text-decoration: underline;
+	text-decoration: none;
 }
 a.tm-download:hover::before{
-	text-decoration:underline;
+	text-decoration: none;
 	color: #fff;
 	background: #e0474c;
 }
+a.tm-heading-anchor {
+    display: none;
+    position: absolute;
+    top: 0;
+    left: -1em;
+    width: 1em;
+    opacity: 0;
+}
+a.tm-heading-anchor:hover,a.tm-heading-anchor:focus {
+    opacity: 1;
+    text-decoration: none;
+}
+h2:focus > .tm-heading-anchor, 
+h2:hover > .tm-heading-anchor, 
+h3:focus > .tm-heading-anchor, 
+h3:hover > .tm-heading-anchor, 
+h4:focus > .tm-heading-anchor, 
+h4:hover > .tm-heading-anchor,
+h5:focus > .tm-heading-anchor, 
+h5:hover > .tm-heading-anchor,
+h6:focus > .tm-heading-anchor, 
+h6:hover > .tm-heading-anchor{
+	opacity: .75;
+}
 
 
 /************************
@@ -735,6 +766,9 @@ img.myClass{
 	.cover h1{ 
 		font-size: 4em; 
 	}
+	a.tm-heading-anchor{
+    	display: block;
+    }
 	.github{
 		position:absolute;
 		display:block;

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

@@ -74,7 +74,7 @@
 		{% block javascripts %}
 
 			<script src="{{ base_url }}/themes/typemill/js/script.js"></script>
-			<script src="{{ base_url }}/system/author/js/lazy-video.js?20190602"></script>
+			<script src="{{ base_url }}/system/author/js/typemillutils.js?20200418"></script>
 			<script>typemillUtilities.start();</script>
 
 			{{ assets.renderJS() }}

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

@@ -51,6 +51,9 @@
 		</div>
 
 		{% block javascripts %}
+			<script src="{{ base_url }}/themes/typemill/js/script.js"></script>		
+			<script src="{{ base_url }}/system/author/js/typemillutils.js?20200418"></script>
+			<script>typemillUtilities.start();</script>
 		
 			{{ assets.renderJS() }}
 		

+ 1 - 1
themes/typemill/partials/navigation.twig

@@ -15,7 +15,7 @@
 		{% endif %}
             {% if (element.elementType == 'folder') %}
 				<a href="{{ element.urlAbs }}">{% if chapnum %}{{ element.chapter }}. {% endif %}{{ element.name }}</a>
-		{% if (element.folderContent|length > 0) %}	
+		{% if ( element.folderContent|length > 0 ) and (element.contains == 'pages') %}	
                 <ul>
                     {{ macros.loop_over(element.folderContent,chapnum) }}
                 </ul>

+ 1 - 1
themes/typemill/typemill.yaml

@@ -1,5 +1,5 @@
 name: Typemill Theme
-version: 1.2.3
+version: 1.2.4
 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