feat(creator-studio): icon toolbar with block tools, drop author strip

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-06 22:30:08 +08:00
1 parent 7bdf7cf018
commit 7c33482be7
19 files changed
+600 -241

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
<?php
/**
* 正文扩展块的渲染:目录、脚注、公式。
*
* 编辑器插入的都是 HTML/文本占位,读者端由这里转换成真正的渲染结果:
* - 目录:占位替换成前台 section-navs 识别的 .posts-nav-box 卡片;
* - 脚注:[^1] 引用与 [^1]: 定义转换成脚注列表并重新编号;
* - 公式:.jio-math 内的 ^ / _ 转成上标下标,给出可读的数学排版。
*
* @package Jio
*/
namespace Jio\Content;
defined('ABSPATH') || exit;
class ContentBlocks {
/** 编辑器插入的目录占位 class。 */
public const TOC_CLASS = 'jio-toc-placeholder';
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_filter('the_content', [$this, 'render_blocks'], 9);
}
/**
* 正文里是否插入了目录占位(用于决定是否加载目录脚本)。
*/
public static function has_toc_placeholder(string $content): bool {
return strpos($content, self::TOC_CLASS) !== false;
}
public function render_blocks(string $content): string {
if ($content === '' || is_admin()) {
return $content;
}
$content = $this->render_toc($content);
$content = $this->render_footnotes($content);
return $this->render_math($content);
}
/**
* 目录占位 → 文章目录卡片。
*/
private function render_toc(string $content): string {
if (!self::has_toc_placeholder($content)) {
return $content;
}
$box = '<div class="widget-container"><div class="posts-nav-box" data-title="'
. esc_attr__('本文目录', 'jio')
. '"></div></div>';
$pattern = '/<div[^>]*class="[^"]*' . preg_quote(self::TOC_CLASS, '/') . '[^"]*"[^>]*>\s*<\/div>\s*(?:<p>\s*<\/p>)?/i';
$replaced = preg_replace($pattern, $box, $content, 1);
return is_string($replaced) ? $replaced : $content;
}
/**
* Markdown 脚注:[^1] 引用与 [^1]: 定义。
*/
private function render_footnotes(string $content): string {
if (strpos($content, '[^') === false) {
return $content;
}
if (!preg_match_all('/\[\^([^\]]+)\]:[ \t]*([^\n<]+)/u', $content, $defs, PREG_SET_ORDER)) {
return $content;
}
$notes = [];
foreach ($defs as $def) {
$notes[(string) $def[1]] = trim((string) $def[2]);
$content = str_replace($def[0], '', $content);
}
if ($notes === []) {
return $content;
}
// 按首次引用顺序编号,而不是按定义顺序。
$order = [];
$result = preg_replace_callback('/\[\^([^\]]+)\]/u', static function (array $match) use (&$order): string {
$key = (string) $match[1];
if (!isset($order[$key])) {
$order[$key] = count($order) + 1;
}
$index = $order[$key];
return '<sup class="jio-footnote-ref"><a href="#jio-fn-' . $index . '" id="jio-fnref-' . $index . '">'
. $index
. '</a></sup>';
}, $content);
if (!is_string($result)) {
return $content;
}
$content = $result;
$items = '';
foreach ($order as $key => $index) {
if (!isset($notes[$key])) {
continue;
}
$items .= '<li id="jio-fn-' . $index . '">'
. esc_html($notes[$key])
. ' <a class="jio-footnote-back" href="#jio-fnref-' . $index . '" aria-label="'
. esc_attr__('返回正文', 'jio')
. '">↩</a></li>';
}
if ($items === '') {
return $content;
}
return $content
. '<section class="jio-footnotes"><h4 class="jio-footnotes__title">'
. esc_html__('注释', 'jio')
. '</h4><ol class="jio-footnotes__list">' . $items . '</ol></section>';
}
/**
* 公式:把 .jio-math 片段里的 ^ 与 _ 转成真正可见的上标下标。
*
* 主题没有内置 TeX 引擎,这里只做确定性的公式排版,不猜测其它语法。
*/
private function render_math(string $content): string {
if (strpos($content, 'jio-math') === false) {
return $content;
}
$result = preg_replace_callback(
'#<span([^>]*\bclass="[^"]*jio-math[^"]*"[^>]*)>(.*?)</span>#is',
static function (array $match): string {
$inner = esc_html(html_entity_decode($match[2], ENT_QUOTES, 'UTF-8'));
$inner = (string) preg_replace('/\^\{([^{}]*)\}|\^(\S)/u', '<sup>$1$2</sup>', $inner);
$inner = (string) preg_replace('/_\{([^{}]*)\}|_(\S)/u', '<sub>$1$2</sub>', $inner);
return '<span' . $match[1] . '>' . $inner . '</span>';
},
$content
);
return is_string($result) ? $result : $content;
}
}
-33
View File
@@ -153,8 +153,6 @@ final class CreatorRest {
'titlePlaceholder' => (string) $settings['title_placeholder'],
'contentPlaceholder' => (string) $settings['content_placeholder'],
],
// 作者行数据由服务端给出,前端不自行拼装昵称/头像。
'author' => $this->author_payload($editable, $user_id),
'privateUploads' => $this->private_upload_rows($user_id, $post_id),
'privateStorage' => PrivateAssets::is_available(),
'media' => [
@@ -566,37 +564,6 @@ final class CreatorRest {
];
}
/**
* 作者行数据:昵称、头像与身份说明。
*
* @return array{name:string,avatar:string,meta:string}
*/
private function author_payload(?\WP_Post $editable, int $user_id): array {
$author_id = $editable instanceof \WP_Post ? (int) $editable->post_author : $user_id;
$user = $author_id > 0 ? get_userdata($author_id) : null;
if (!$user) {
return ['name' => '', 'avatar' => '', 'meta' => ''];
}
$avatar = function_exists('jio_get_avatar_url') ? (string) jio_get_avatar_url($author_id, ['size' => 64]) : '';
if ($avatar === '' && function_exists('get_avatar_url')) {
$avatar = (string) get_avatar_url($author_id, ['size' => 64]);
}
$meta = '';
if ($author_id !== $user_id) {
$meta = __('原作者', 'jio');
} elseif (function_exists('jio_get_user_vip_level') && (int) jio_get_user_vip_level($user_id) > 0) {
$meta = sprintf(__('VIP%s', 'jio'), (string) max(1, min(2, (int) jio_get_user_vip_level($user_id))));
}
return [
'name' => (string) $user->display_name,
'avatar' => $avatar,
'meta' => $meta,
];
}
/**
* @param array<string, mixed> $post_meta
* @return array<string, mixed>
+4 -1
View File
@@ -103,7 +103,10 @@ trait ScriptsPart1c {
}
$requirements['is_article'] = $is_article;
$requirements['has_toc'] = $is_article && $article_nav_enabled;
// 用户在正文里插入了目录块时,即使关闭了侧栏目录树也要加载目录脚本。
$has_inline_toc = class_exists('\Jio\Content\ContentBlocks')
&& \Jio\Content\ContentBlocks::has_toc_placeholder($content);
$requirements['has_toc'] = $is_article && ($article_nav_enabled || $has_inline_toc);
$requirements['has_video'] = $has_video;
$requirements['has_hidden_content'] = $has_hidden_content || $has_yiyan || $has_inserted_content;
$requirements['needs_pay_box'] = $is_paid_content_single || (bool) preg_match('/\[(?:payshow|hidecontent)\b|wp:jio\/paid-fragment/i', $content);