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:
1 parent
7bdf7cf018
commit
7c33482be7
19 files changed
+600
-241
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user