Version 1.3.5 Consolidation

This commit is contained in:
trendschau 2020-04-20 19:21:56 +02:00
parent 5a8968efde
commit 55704bd69f
34 changed files with 726 additions and 426 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
system/Models/WriteMeta.php Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,14 +16,36 @@
{{ content }}
<div class="toc-nav">
<ul>
{% 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>
{% endfor %}
</ul>
</div>
{% elseif item.contains == 'posts' %}
<ul class="post">
{% 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>
{% 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 %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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