aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2023-03-04 01:46:45 +0300
committerEvgeny Zinoviev <me@ch1p.io>2023-03-04 01:46:45 +0300
commiteeb84c5be16ecca239adae9c851bc0f7db0875a1 (patch)
tree9aa1056e643212e4c6133d90f38a1966f0fa35ca
parent917d2622aa5fe748c1cda914eae94c12be743c42 (diff)
blog: support ToC
-rw-r--r--engine/Skin.php1
-rw-r--r--handler/Auto.php4
-rw-r--r--handler/admin/AutoAddOrEdit.php2
-rw-r--r--handler/admin/AutoEdit.php15
-rw-r--r--handler/admin/PageAdd.php4
-rw-r--r--htdocs/scss/app/blog.scss70
-rw-r--r--htdocs/scss/app/common.scss5
-rw-r--r--htdocs/scss/app/mobile.scss3
-rw-r--r--htdocs/scss/vars.scss1
-rw-r--r--lang/en.php2
-rw-r--r--lib/MyParsedown.php9
-rw-r--r--lib/markup.php13
-rw-r--r--model/Post.php10
-rw-r--r--mysql_schema.sql4
-rw-r--r--skin/admin.skin.php2
-rw-r--r--skin/base.skin.php8
-rw-r--r--skin/main.skin.php44
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>