diff --git a/content/00-welcome/01-write-content.md b/content/00-welcome/01-write-content.md index 0f24144..140d0d7 100644 --- a/content/00-welcome/01-write-content.md +++ b/content/00-welcome/01-write-content.md @@ -6,7 +6,7 @@ Typemill is a simple Flat File Content Management System (CMS). We (the communit You can create, structure and reorder all pages with the navigation on the left. To structure your content, you can create new folders and files with the "add item" button. To reorder the pages, just drag an item and drop it wherever you want. Play around with it and you will notice, that it works pretty similar to the folder- and file-system of your laptop. And in fact, this is exactly what Typemill does in the background: It stores your content in files and folders on the server. -However, there are some limitations when you try to reorder elements. For example, you cannot move a complete folder to another folder. Click on the question-mark at the top of the navigation for detailed information. +However, there are some limitations when you try to reorder elements. For example, you cannot move a complete folder to another folder, because this would change all the urls of the pages inside that folder, which is a nightmare for readers and search engines. ## The Editor diff --git a/system/Controllers/ArticleApiController.php b/system/Controllers/ArticleApiController.php index 5ed5e19..034260a 100644 --- a/system/Controllers/ArticleApiController.php +++ b/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); - # dispatch event - $this->c->dispatcher->dispatch('onPagePublished', new OnPagePublished($this->item)); + # complete the page meta if title or description not set + $writeMeta = new WriteMeta(); + $meta = $writeMeta->completePageMeta($this->content, $this->settings, $this->item); - return $response->withJson(['success'], 200); + # dispatch event + $page = ['content' => $this->content, 'meta' => $meta, 'item' => $this->item]; + $this->c->dispatcher->dispatch('onPagePublished', new OnPagePublished($page)); + + 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); diff --git a/system/Controllers/AuthController.php b/system/Controllers/AuthController.php index b5fa700..b8c8921 100644 --- a/system/Controllers/AuthController.php +++ b/system/Controllers/AuthController.php @@ -24,7 +24,6 @@ class AuthController extends Controller } } - /** * show login form * diff --git a/system/Controllers/BlockApiController.php b/system/Controllers/BlockApiController.php index 9c77ff4..3e5a0af 100644 --- a/system/Controllers/BlockApiController.php +++ b/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); diff --git a/system/Controllers/ContentController.php b/system/Controllers/ContentController.php index 78ea990..87dc47e 100644 --- a/system/Controllers/ContentController.php +++ b/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.']; } } diff --git a/system/Controllers/Controller.php b/system/Controllers/Controller.php index 57e54cf..6dcad45 100644 --- a/system/Controllers/Controller.php +++ b/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); } diff --git a/system/Controllers/MediaApiController.php b/system/Controllers/MediaApiController.php index a721197..5b0ec9b 100644 --- a/system/Controllers/MediaApiController.php +++ b/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', ); } } \ No newline at end of file diff --git a/system/Controllers/MetaApiController.php b/system/Controllers/MetaApiController.php index ab997a1..30c6df9 100644 --- a/system/Controllers/MetaApiController.php +++ b/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. \ No newline at end of file +} \ No newline at end of file diff --git a/system/Controllers/PageController.php b/system/Controllers/PageController.php index b721941..a05c9e2 100644 --- a/system/Controllers/PageController.php +++ b/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("", $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) diff --git a/system/Controllers/SettingsController.php b/system/Controllers/SettingsController.php index bad290f..23af2e4 100644 --- a/system/Controllers/SettingsController.php +++ b/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(); diff --git a/system/Controllers/SetupController.php b/system/Controllers/SetupController.php index e149ed1..35e10a3 100644 --- a/system/Controllers/SetupController.php +++ b/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) diff --git a/system/Extensions/ParsedownExtension.php b/system/Extensions/ParsedownExtension.php index 34771a2..77d305a 100644 --- a/system/Extensions/ParsedownExtension.php +++ b/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; diff --git a/system/Extensions/TwigMetaExtension.php b/system/Extensions/TwigMetaExtension.php index 82da060..0d33399 100644 --- a/system/Extensions/TwigMetaExtension.php +++ b/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; } diff --git a/system/Models/ProcessAssets.php b/system/Models/ProcessAssets.php index 98bed4d..7494388 100644 --- a/system/Models/ProcessAssets.php +++ b/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; diff --git a/system/Models/ProcessImage.php b/system/Models/ProcessImage.php index f3c4b0a..e6a1d18 100644 --- a/system/Models/ProcessImage.php +++ b/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; } - } \ No newline at end of file diff --git a/system/Models/WriteMeta.php b/system/Models/WriteMeta.php new file mode 100644 index 0000000..f04d7af --- /dev/null +++ b/system/Models/WriteMeta.php @@ -0,0 +1,348 @@ +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++; + } + } +} \ No newline at end of file diff --git a/system/Models/WriteYaml.php b/system/Models/WriteYaml.php index 283ee2d..6473dcc 100644 --- a/system/Models/WriteYaml.php +++ b/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++; - } - } } \ No newline at end of file diff --git a/system/Settings.php b/system/Settings.php index 7dde0a9..9bf141c 100644 --- a/system/Settings.php +++ b/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, diff --git a/system/author/css/style.css b/system/author/css/style.css index 6197987..b4f6c00 100644 --- a/system/author/css/style.css +++ b/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 * diff --git a/system/author/js/lazy-video.js b/system/author/js/typemillutils.js similarity index 100% rename from system/author/js/lazy-video.js rename to system/author/js/typemillutils.js diff --git a/system/author/js/vue-blox-config.js b/system/author/js/vue-blox-config.js index 170b501..8368a4a 100644 --- a/system/author/js/vue-blox-config.js +++ b/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"; } diff --git a/system/author/js/vue-blox.js b/system/author/js/vue-blox.js index a64347b..e2006e1 100644 --- a/system/author/js/vue-blox.js +++ b/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: '
' + @@ -1200,6 +1214,20 @@ const videoComponent = Vue.component('video-component', { '' + '
' + '
', + 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: '
' + @@ -1231,7 +1258,7 @@ const imageComponent = Vue.component('image-component', { '' + '' + '' + - '' + + '' + '' + '
', 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]); } } } diff --git a/system/author/js/vue-navi.js b/system/author/js/vue-navi.js index 8aed908..9295876 100644 --- a/system/author/js/vue-navi.js +++ b/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; } } diff --git a/system/author/js/vue-publishcontroller.js b/system/author/js/vue-publishcontroller.js index 02ca228..f171e76 100644 --- a/system/author/js/vue-publishcontroller.js +++ b/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; diff --git a/system/author/layouts/layoutBlox.twig b/system/author/layouts/layoutBlox.twig index c7eebfb..382265a 100644 --- a/system/author/layouts/layoutBlox.twig +++ b/system/author/layouts/layoutBlox.twig @@ -196,6 +196,7 @@ const myaxios = axios.create(); myaxios.defaults.baseURL = "{{ base_url }}"; + @@ -217,7 +218,6 @@ - {{ assets.renderJS() }} diff --git a/system/author/partials/fields.twig b/system/author/partials/fields.twig index 8da6b93..bd81206 100644 --- a/system/author/partials/fields.twig +++ b/system/author/partials/fields.twig @@ -7,7 +7,7 @@ {% if field.type == 'textarea' %} - + {% elseif field.type == 'paragraph' %} @@ -16,7 +16,7 @@ {% elseif field.type == 'checkbox' %} @@ -27,7 +27,7 @@ {% for value,label in options %} @@ -37,7 +37,7 @@ {% set options = field.getOptions() %} - {% for value,label in options %} {% endfor %} @@ -50,7 +50,7 @@ {% for value,label in options %} @@ -58,7 +58,7 @@ {% else %} - + {% endif %} diff --git a/system/author/settings/system.twig b/system/author/settings/system.twig index 424a954..fba46a6 100644 --- a/system/author/settings/system.twig +++ b/system/author/settings/system.twig @@ -63,7 +63,12 @@
-
+
+
+

{{ __('General Presentation') }}

+
+ -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/themes/typemill/chapter.twig b/themes/typemill/chapter.twig index 745a331..c7da37e 100644 --- a/themes/typemill/chapter.twig +++ b/themes/typemill/chapter.twig @@ -16,14 +16,36 @@ {{ content }} -
- -
\ No newline at end of file + + {% endif %} diff --git a/themes/typemill/cover.twig b/themes/typemill/cover.twig index 3e6c3eb..356644c 100644 --- a/themes/typemill/cover.twig +++ b/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 %} diff --git a/themes/typemill/css/style.css b/themes/typemill/css/style.css index cb01334..0a603a9 100644 --- a/themes/typemill/css/style.css +++ b/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; diff --git a/themes/typemill/partials/layout.twig b/themes/typemill/partials/layout.twig index 3478689..46c4e62 100644 --- a/themes/typemill/partials/layout.twig +++ b/themes/typemill/partials/layout.twig @@ -74,7 +74,7 @@ {% block javascripts %} - + {{ assets.renderJS() }} diff --git a/themes/typemill/partials/layoutCover.twig b/themes/typemill/partials/layoutCover.twig index e0b8883..896c728 100644 --- a/themes/typemill/partials/layoutCover.twig +++ b/themes/typemill/partials/layoutCover.twig @@ -51,6 +51,9 @@ {% block javascripts %} + + + {{ assets.renderJS() }} diff --git a/themes/typemill/partials/navigation.twig b/themes/typemill/partials/navigation.twig index 7c81b31..7d8e0e7 100644 --- a/themes/typemill/partials/navigation.twig +++ b/themes/typemill/partials/navigation.twig @@ -15,7 +15,7 @@ {% endif %} {% if (element.elementType == 'folder') %} {% if chapnum %}{{ element.chapter }}. {% endif %}{{ element.name }} - {% if (element.folderContent|length > 0) %} + {% if ( element.folderContent|length > 0 ) and (element.contains == 'pages') %} diff --git a/themes/typemill/typemill.yaml b/themes/typemill/typemill.yaml index 829b7bf..9a1cd56 100644 --- a/themes/typemill/typemill.yaml +++ b/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