diff options
-rw-r--r-- | engine/Skin.php | 1 | ||||
-rw-r--r-- | handler/Auto.php | 4 | ||||
-rw-r--r-- | handler/admin/AutoAddOrEdit.php | 2 | ||||
-rw-r--r-- | handler/admin/AutoEdit.php | 15 | ||||
-rw-r--r-- | handler/admin/PageAdd.php | 4 | ||||
-rw-r--r-- | htdocs/scss/app/blog.scss | 70 | ||||
-rw-r--r-- | htdocs/scss/app/common.scss | 5 | ||||
-rw-r--r-- | htdocs/scss/app/mobile.scss | 3 | ||||
-rw-r--r-- | htdocs/scss/vars.scss | 1 | ||||
-rw-r--r-- | lang/en.php | 2 | ||||
-rw-r--r-- | lib/MyParsedown.php | 9 | ||||
-rw-r--r-- | lib/markup.php | 13 | ||||
-rw-r--r-- | model/Post.php | 10 | ||||
-rw-r--r-- | mysql_schema.sql | 4 | ||||
-rw-r--r-- | skin/admin.skin.php | 2 | ||||
-rw-r--r-- | skin/base.skin.php | 8 | ||||
-rw-r--r-- | skin/main.skin.php | 44 |
17 files changed, 172 insertions, 25 deletions
diff --git a/engine/Skin.php b/engine/Skin.php index a8924d4..b1523e6 100644 --- a/engine/Skin.php +++ b/engine/Skin.php @@ -9,6 +9,7 @@ class Skin { protected array $langKeys = []; protected array $options = [ 'full_width' => false, + 'wide' => false, 'dynlogo_enabled' => true, 'logo_path_map' => [], 'logo_link_map' => [], diff --git a/handler/Auto.php b/handler/Auto.php index 361cc17..0656c44 100644 --- a/handler/Auto.php +++ b/handler/Auto.php @@ -65,10 +65,14 @@ class Auto extends RequestHandler { $s->title = $post->title; + if ($post->toc) + $s->setOptions(['wide' => true]); + return $s->renderPage('main/post', title: $post->title, id: $post->id, unsafe_html: $post->getHtml($this->isRetina(), \themes::getUserTheme()), + unsafe_toc_html: $post->getToc(), date: $post->getFullDate(), tags: $tags, visible: $post->visible, diff --git a/handler/admin/AutoAddOrEdit.php b/handler/admin/AutoAddOrEdit.php index 027c827..1627642 100644 --- a/handler/admin/AutoAddOrEdit.php +++ b/handler/admin/AutoAddOrEdit.php @@ -39,6 +39,7 @@ abstract class AutoAddOrEdit extends AdminRequestHandler { string $text = '', ?array $tags = null, bool $visible = false, + bool $toc = false, string $short_name = '', ?string $error_code = null, bool $saved = false, @@ -53,6 +54,7 @@ abstract class AutoAddOrEdit extends AdminRequestHandler { text: $text, tags: $tags ? implode(', ', $tags) : '', visible: $visible, + toc: $toc, saved: $saved, short_name: $short_name, error_code: $error_code diff --git a/handler/admin/AutoEdit.php b/handler/admin/AutoEdit.php index 9d70c5b..ba6a7d8 100644 --- a/handler/admin/AutoEdit.php +++ b/handler/admin/AutoEdit.php @@ -16,12 +16,13 @@ class AutoEdit extends AutoAddOrEdit { if ($post) { $tags = $post->getTags(); return $this->_get_postEdit($post, - tags: $post->getTags(), - saved: $saved, title: $post->title, text: $post->md, + tags: $post->getTags(), visible: $post->visible, + toc: $post->toc, short_name: $post->shortName, + saved: $saved, ); } @@ -30,8 +31,8 @@ class AutoEdit extends AutoAddOrEdit { return $this->_get_pageEdit($page, title: $page->title, text: $page->md, - visible: $page->visible, saved: $saved, + visible: $page->visible, ); } @@ -45,8 +46,8 @@ class AutoEdit extends AutoAddOrEdit { if ($post) { csrf::check('editpost'.$post->id); - list($text, $title, $tags, $visible, $short_name) - = $this->input('text, title, tags, b:visible, new_short_name'); + list($text, $title, $tags, $visible, $toc, $short_name) + = $this->input('text, title, tags, b:visible, b:toc, new_short_name'); $tags = posts::splitStringToTags($tags); $error_code = null; @@ -63,10 +64,11 @@ class AutoEdit extends AutoAddOrEdit { if ($error_code) $this->_get_postEdit($post, - text: $text, title: $title, + text: $text, tags: $tags, visible: $visible, + toc: $toc, short_name: $short_name, error_code: $error_code ); @@ -75,6 +77,7 @@ class AutoEdit extends AutoAddOrEdit { 'title' => $title, 'md' => $text, 'visible' => (int)$visible, + 'toc' => (int)$toc, 'short_name' => $short_name ]); $tag_ids = posts::getTagIds($tags); diff --git a/handler/admin/PageAdd.php b/handler/admin/PageAdd.php index 8754f0f..42a9911 100644 --- a/handler/admin/PageAdd.php +++ b/handler/admin/PageAdd.php @@ -40,8 +40,8 @@ class PageAdd extends AutoAddOrEdit { if ($error_code) { return $this->_get_pageAdd( name: $name, - text: $text, title: $title, + text: $text, error_code: $error_code ); } @@ -53,8 +53,8 @@ class PageAdd extends AutoAddOrEdit { ])) { return $this->_get_pageAdd( name: $name, - text: $text, title: $title, + text: $text, error_code: 'db_err' ); } diff --git a/htdocs/scss/app/blog.scss b/htdocs/scss/app/blog.scss index da640f9..30540ef 100644 --- a/htdocs/scss/app/blog.scss +++ b/htdocs/scss/app/blog.scss @@ -91,6 +91,74 @@ margin-top: 3px; } +.blog-post-wrap2 { + display: table; + table-layout: fixed; + border: none; + border-collapse: collapse; +} +.blog-post-wrap1 { + display: table-row; +} +.blog-post { + display: table-cell; + vertical-align: top; +} +.blog-post-toc { + display: table-cell; + vertical-align: top; + font-size: $fs - 2px; + + &-wrap { + position: sticky; + top: 0; + padding: 10px 0 0 20px; + overflow-y: auto; + max-height: 100vh; + box-sizing: border-box; + } + + &-inner-wrap { + border-left: 1px $border-color solid; + padding-left: 20px; + margin-bottom: 10px; + + ul { + list-style-type: none; + margin: 5px 0; + padding-left: 18px; + } + > ul { + padding-left: 0 !important; + } + + li { + margin: 2px 0; + line-height: 150%; + > a { + display: inline-block; + } + } + } + + &-title { + font-weight: bold; + padding: 6px 0; + } +} +body.wide .blog-post { + width: $base_width; +} + +@media screen and (max-width: 1150px) { + .blog-post-toc { + display: none; + } + body.wide .blog-post { + width: auto; + } +} + .blog-post-title { margin: 0 0 16px; } @@ -174,7 +242,7 @@ } blockquote { - border-left: 3px #e0e0e0 solid; + border-left: 3px $border-color solid; margin-left: 0; padding: 5px 0 5px 12px; color: $grey; diff --git a/htdocs/scss/app/common.scss b/htdocs/scss/app/common.scss index 074c1aa..723c1ff 100644 --- a/htdocs/scss/app/common.scss +++ b/htdocs/scss/app/common.scss @@ -34,6 +34,11 @@ body.full-width .base-width { margin-left: auto; margin-right: auto; } +body.wide .base-width { + max-width: $wide_width; + margin-left: auto; + margin-right: auto; +} input[type="text"], input[type="password"], diff --git a/htdocs/scss/app/mobile.scss b/htdocs/scss/app/mobile.scss index 27d250c..4eae019 100644 --- a/htdocs/scss/app/mobile.scss +++ b/htdocs/scss/app/mobile.scss @@ -46,3 +46,6 @@ a.head-item:last-child > span { .blog-list.withtags { margin-right: 0; } +//.blog-post-toc { +// display: none; +//}
\ No newline at end of file diff --git a/htdocs/scss/vars.scss b/htdocs/scss/vars.scss index cc67f04..71a5f3f 100644 --- a/htdocs/scss/vars.scss +++ b/htdocs/scss/vars.scss @@ -4,6 +4,7 @@ $ff: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif; $ffMono: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace; $base-width: 900px; +$wide_width: 1240px; $side-padding: 25px; $base-padding: 18px; $footer-height: 64px; diff --git a/lang/en.php b/lang/en.php index 5736104..7d17205 100644 --- a/lang/en.php +++ b/lang/en.php @@ -19,6 +19,7 @@ return [ 'edit' => 'edit', 'delete' => 'delete', 'info_saved' => 'Information saved.', + 'toc' => 'Table of Contents', // theme switcher 'theme_auto' => 'auto', @@ -53,6 +54,7 @@ return [ 'blog_write_form_enter_title' => 'Enter title..', 'blog_write_form_tags' => 'Tags', 'blog_write_form_visible' => 'Visible', + 'blog_write_form_toc' => 'ToC', 'blog_write_form_short_name' => 'Short name', 'blog_write_form_toggle_wrap' => 'Toggle wrap', 'blog_write_form_options' => 'Options', diff --git a/lib/MyParsedown.php b/lib/MyParsedown.php index cd537bc..85ed9c4 100644 --- a/lib/MyParsedown.php +++ b/lib/MyParsedown.php @@ -3,13 +3,18 @@ class MyParsedown extends ParsedownExtended { public function __construct( + ?array $opts = null, protected bool $useImagePreviews = false ) { - parent::__construct([ + $parsedown_opts = [ 'tables' => [ 'tablespan' => true ] - ]); + ]; + if (!is_null($opts)) { + $parsedown_opts = array_merge($parsedown_opts, $opts); + } + parent::__construct($parsedown_opts); $this->InlineTypes['{'][] = 'FileAttach'; $this->InlineTypes['{'][] = 'Image'; diff --git a/lib/markup.php b/lib/markup.php index 2f25c6c..f6ddd0f 100644 --- a/lib/markup.php +++ b/lib/markup.php @@ -7,6 +7,19 @@ class markup { return $pd->text($md); } + public static function toc(string $md): string { + $pd = new MyParsedown([ + 'toc' => [ + 'lowercase' => true, + 'transliterate' => true, + 'urlencode' => false, + 'headings' => ['h1', 'h2', 'h3'] + ] + ]); + $pd->text($md); + return $pd->contentsList(); + } + public static function htmlToText(string $html): string { $text = html_entity_decode(strip_tags($html)); $lines = explode("\n", $text); diff --git a/model/Post.php b/model/Post.php index b0360ac..6f3f1ab 100644 --- a/model/Post.php +++ b/model/Post.php @@ -8,10 +8,12 @@ class Post extends Model { public string $title; public string $md; public string $html; + public string $tocHtml; public string $text; public int $ts; public int $updateTs; public bool $visible; + public bool $toc; public string $shortName; public function edit(array $data) { @@ -26,6 +28,10 @@ class Post extends Model { $data['text'] = markup::htmlToText($data['html']); } + if ((isset($data['toc']) && $data['toc']) || $this->toc) { + $data['toc_html'] = markup::toc($data['md']); + } + parent::edit($data); $this->updateImagePreviews(); } @@ -87,6 +93,10 @@ class Post extends Model { return $html; } + public function getToc(): ?string { + return $this->toc ? $this->tocHtml : null; + } + public function isUpdated(): bool { return $this->updateTs && $this->updateTs != $this->ts; } diff --git a/mysql_schema.sql b/mysql_schema.sql index fa5459b..8246db5 100644 --- a/mysql_schema.sql +++ b/mysql_schema.sql @@ -43,7 +43,9 @@ CREATE TABLE `posts` ( PRIMARY KEY (`id`), UNIQUE KEY `short_name` (`short_name`), KEY ` visible_ts_idx` (`visible`,`ts`) -) ENGINE=InnoDB AUTO_INCREMENT=66 DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +ALTER TABLE `posts` ADD `toc` TINYINT(1) NOT NULL DEFAULT '0' AFTER `short_name`; +ALTER TABLE `posts` ADD `toc_html` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' AFTER `toc`; CREATE TABLE `posts_tags` ( `id` int(11) NOT NULL, diff --git a/skin/admin.skin.php b/skin/admin.skin.php index 5619dd0..b2d9bb4 100644 --- a/skin/admin.skin.php +++ b/skin/admin.skin.php @@ -129,6 +129,7 @@ function postForm($ctx, $error_code = null, ?bool $saved = null, ?bool $visible = null, + ?bool $toc = null, string|Stringable|null $post_url = null, ?int $post_id = null): array { $form_url = !$is_edit ? '/write/' : $post_url.'edit/'; @@ -173,6 +174,7 @@ $html = <<<HTML <div class="form-field-label">{$ctx->lang('blog_write_form_options')}</div> <div class="form-field"> <label for="visible_cb"><input type="checkbox" id="visible_cb" name="visible"{$ctx->if_true($visible, ' checked="checked"')}> {$ctx->lang('blog_write_form_visible')}</label> + <label for="toc_cb"><input type="checkbox" id="toc_cb" name="toc"{$ctx->if_true($toc, ' checked="checked"')}> {$ctx->lang('blog_write_form_toc')}</label> </div> </div> </td> diff --git a/skin/base.skin.php b/skin/base.skin.php index 3374555..d39a0a8 100644 --- a/skin/base.skin.php +++ b/skin/base.skin.php @@ -13,6 +13,12 @@ $app_config = json_encode([ 'cookieHost' => $config['cookie_host'], ]); +$body_class = []; +if ($opts['full_width']) + $body_class = 'full-width'; +else if ($opts['wide']) + $body_class = 'wide'; + return <<<HTML <!doctype html> <html lang="en"> @@ -26,7 +32,7 @@ return <<<HTML {$ctx->renderMeta($meta)} {$ctx->renderStatic($static, $theme)} </head> - <body{$ctx->if_true($opts['full_width'], ' class="full-width"')}> + <body{$ctx->if_true($body_class, ' class="'.$body_class.'"')}> {$ctx->renderHeader($theme, renderLogo($ctx, $opts['logo_path_map'], $opts['logo_link_map']))} <div class="page-content base-width"> <div class="page-content-inner">{$unsafe_body}</div> diff --git a/skin/main.skin.php b/skin/main.skin.php index a1a4910..c0ab40a 100644 --- a/skin/main.skin.php +++ b/skin/main.skin.php @@ -144,22 +144,28 @@ HTML; // post page // --------- -function post($ctx, $id, $title, $unsafe_html, $date, $visible, $url, $tags, $email, $urlencoded_reply_subject) { +function post($ctx, $id, $title, $unsafe_html, $unsafe_toc_html, $date, $visible, $url, $tags, $email, $urlencoded_reply_subject) { $html = <<<HTML -<div class="blog-post"> - <div class="blog-post-title"> - <h1>{$title}</h1> - <div class="blog-post-date"> - {$ctx->if_not($visible, '<b>'.$ctx->lang('blog_post_hidden').'</b> |')} - {$date} - {$ctx->if_admin($ctx->postAdminLinks, $url, $id)} - </div> - <div class="blog-post-tags clearfix"> - {$ctx->for_each($tags, fn($tag) => $ctx->postTag($tag->getUrl(), $tag->tag))} +<div class="blog-post-wrap2"> + <div class="blog-post-wrap1"> + <div class="blog-post"> + <div class="blog-post-title"> + <h1>{$title}</h1> + <div class="blog-post-date"> + {$ctx->if_not($visible, '<b>'.$ctx->lang('blog_post_hidden').'</b> |')} + {$date} + {$ctx->if_admin($ctx->postAdminLinks, $url, $id)} + </div> + <div class="blog-post-tags clearfix"> + {$ctx->for_each($tags, fn($tag) => $ctx->postTag($tag->getUrl(), $tag->tag))} + </div> + </div> + <div class="blog-post-text">{$unsafe_html}</div> </div> + {$ctx->if_true($unsafe_toc_html, $ctx->postToc, $unsafe_toc_html)} </div> - <div class="blog-post-text">{$unsafe_html}</div> </div> + <div class="blog-post-comments"> {$ctx->langRaw('blog_comments_text', $email, $urlencoded_reply_subject)} </div> @@ -168,6 +174,20 @@ HTML; return [$html, markdownThemeChangeListener()]; } +function postToc($ctx, $unsafe_toc_html) { +return <<<HTML +<div class="blog-post-toc"> + <div class="blog-post-toc-wrap"> + <div class="blog-post-toc-inner-wrap"> + <div class="blog-post-toc-title">{$ctx->lang('toc')}</div> + {$unsafe_toc_html} + </div> + </div> +</div> +HTML; + +} + function postAdminLinks($ctx, $url, $id) { return <<<HTML <a href="{$url}edit/">{$ctx->lang('edit')}</a> |