blog: support ToC

This commit is contained in:
Evgeny Zinoviev 2023-03-04 01:46:45 +03:00
parent 917d2622aa
commit eeb84c5be1
17 changed files with 172 additions and 25 deletions

View File

@ -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' => [],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -46,3 +46,6 @@ a.head-item:last-child > span {
.blog-list.withtags {
margin-right: 0;
}
//.blog-post-toc {
// display: none;
//}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -144,9 +144,11 @@ 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-wrap2">
<div class="blog-post-wrap1">
<div class="blog-post">
<div class="blog-post-title">
<h1>{$title}</h1>
<div class="blog-post-date">
@ -159,7 +161,11 @@ $html = <<<HTML
</div>
</div>
<div class="blog-post-text">{$unsafe_html}</div>
</div>
{$ctx->if_true($unsafe_toc_html, $ctx->postToc, $unsafe_toc_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>