Files
CustomInlineTags/Plugin.php
2026-02-23 17:21:48 +08:00

872 lines
32 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 编辑器+短代码行内自定义标签
*
* @package CustomInlineTags
* @author 石头厝
* @version 1.0.0
* @link https://www.shitoucuo.com
*/
class CustomInlineTags_Plugin implements Typecho_Plugin_Interface
{
// 存储所有标签的静态变量
private static $_allTags = array();
/**
* 激活插件
*/
public static function activate()
{
// 添加文章保存时的钩子
Typecho_Plugin::factory('Widget_Contents_Post_Edit')->write = array('CustomInlineTags_Plugin', 'parseTagsOnSave');
Typecho_Plugin::factory('Widget_Contents_Page_Edit')->write = array('CustomInlineTags_Plugin', 'parseTagsOnSave');
// 添加内容解析钩子
Typecho_Plugin::factory('Widget_Abstract_Contents')->contentEx = array('CustomInlineTags_Plugin', 'parseContent');
Typecho_Plugin::factory('Widget_Abstract_Contents')->excerptEx = array('CustomInlineTags_Plugin', 'parseContent');
// 添加编辑器脚本 - 关键使用header钩子添加脚本
Typecho_Plugin::factory('admin/header.php')->header = array('CustomInlineTags_Plugin', 'addHeader');
Typecho_Plugin::factory('admin/write-post.php')->bottom = array('CustomInlineTags_Plugin', 'addEditorButton');
Typecho_Plugin::factory('admin/write-page.php')->bottom = array('CustomInlineTags_Plugin', 'addEditorButton');
return _t('插件已激活');
}
/**
* 禁用插件
*/
public static function deactivate()
{
return _t('插件已禁用');
}
/**
* 插件配置面板
*/
public static function config(Typecho_Widget_Helper_Form $form)
{
// 标签样式设置
$style = new Typecho_Widget_Helper_Form_Element_Textarea('tagStyle',
NULL,
"display: inline-block;\nbackground: #f0f0f0;\ncolor: #333;\npadding: 2px 8px;\nmargin: 0 4px;\nborder-radius: 12px;\nfont-size: 0.9em;\nfont-weight: normal;\ntext-decoration: none;",
_t('标签样式'),
_t('自定义标签的CSS样式'));
$form->addInput($style);
// 标签前缀
$prefix = new Typecho_Widget_Helper_Form_Element_Text('tagPrefix',
NULL,
'#',
_t('标签前缀'),
_t('在标签前显示的前缀符号'));
$form->addInput($prefix);
// 是否启用标签链接
$enableLink = new Typecho_Widget_Helper_Form_Element_Radio('enableLink',
array(
'1' => _t('启用'),
'0' => _t('禁用')
),
'1',
_t('启用标签链接'),
_t('点击标签是否跳转到标签页面'));
$form->addInput($enableLink);
// 伪静态标签链接格式
$tagUrlFormat = new Typecho_Widget_Helper_Form_Element_Text('tagUrlFormat',
NULL,
'/tag-{slug}.html',
_t('标签链接格式'),
_t('根据你的伪静态设置填写标签链接格式,{slug}会被替换为标签slug'));
$form->addInput($tagUrlFormat);
// 是否新窗口打开
$openNewWindow = new Typecho_Widget_Helper_Form_Element_Radio('openNewWindow',
array(
'1' => _t('是'),
'0' => _t('否')
),
'1',
_t('新窗口打开'),
_t('点击标签是否在新窗口打开'));
$form->addInput($openNewWindow);
}
/**
* 个人配置面板
*/
public static function personalConfig(Typecho_Widget_Helper_Form $form) {}
/**
* 获取所有标签
*/
private static function getAllTags()
{
if (!empty(self::$_allTags)) {
return self::$_allTags;
}
try {
$db = Typecho_Db::get();
$rows = $db->fetchAll($db->select('name')
->from('table.metas')
->where('type = ?', 'tag')
->order('name', Typecho_Db::SORT_ASC));
foreach ($rows as $row) {
self::$_allTags[] = $row['name'];
}
} catch (Exception $e) {
self::$_allTags = array();
}
return self::$_allTags;
}
/**
* 添加头部资源
*/
public static function addHeader()
{
// 只在写文章/页面页面添加
if (strpos($_SERVER['REQUEST_URI'], 'write-post') !== false ||
strpos($_SERVER['REQUEST_URI'], 'write-page') !== false) {
// 获取所有标签并转为JSON
$allTags = self::getAllTags();
$tagsJson = json_encode($allTags);
echo '<style>
.custom-tag-btn {
display: inline-block;
width: 24px;
height: 24px;
line-height: 24px;
text-align: center;
cursor: pointer;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 3px;
margin-left: 5px;
font-weight: bold;
}
.custom-tag-btn:hover {
background: #e5e5e5;
}
.custom-inline-tag {
transition: all 0.2s ease;
}
.custom-inline-tag:hover {
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
/* 标签选择弹窗样式 */
.custom-tag-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
z-index: 10000;
width: 400px;
max-width: 90%;
padding: 20px;
}
.custom-tag-modal h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
font-size: 16px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.dark .custom-tag-input{color:#000!important;}
.custom-tag-input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
margin-bottom: 15px;
}
.custom-tag-input:focus {
border-color: #467B96;
outline: none;
}
.custom-tag-suggestions {
max-height: 200px;
overflow-y: auto;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 15px;
}
.custom-tag-suggestion {
padding: 8px 12px;
cursor: pointer;
color:#000;
border-bottom: 1px solid #f5f5f5;
font-size: 14px;
}
.custom-tag-suggestion:hover {
background-color: #f5f5f5;
}
.custom-tag-suggestion.selected {
background-color: #e9f7fe;
}
.custom-tag-buttons {
text-align: right;
}
.custom-tag-btn-confirm,
.custom-tag-btn-cancel {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-left: 10px;
}
.custom-tag-btn-confirm {
background-color: #467B96;
color: white;
}
.custom-tag-btn-confirm:hover {
background-color: #3a6980;
}
.custom-tag-btn-cancel {
background-color: #f5f5f5;
color: #666;
}
.custom-tag-btn-cancel:hover {
background-color: #e5e5e5;
}
.custom-tag-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 9999;
}
.custom-tag-empty {
padding: 20px;
text-align: center;
color: #999;
font-style: italic;
}
.custom-tag-selected {
background-color: #f0f9ff;
border-left: 3px solid #467B96;
}
.dark .custom-tag-modal input[type=text]{background-color:#fff!important;color:#000!important;font-weight:none!important;}
.dark .title input[type=text]{background-color:#1f2937!important;color:#fff!important;font-weight:none!important;}
/* 深色模式适配 - 仅添加以下CSS */
.dark .custom-tag-btn {
background: #374151 !important;
border: 1px solid #4b5563 !important;
color: #d1d5db !important;
}
.dark .custom-tag-btn:hover {
background: #4b5563 !important;
}
.dark .custom-tag-modal {
background: #1f2937 !important;
border: 1px solid #374151 !important;
}
.dark .custom-tag-modal h3 {
color: #ffffff !important;
border-bottom: 1px solid #374151 !important;
}
.dark .custom-tag-input {
background: #101928 !important;
border: 1px solid #374151 !important;
color: #ffffff !important;
}
.dark .custom-tag-input:focus {
border-color: #467B96 !important;
outline: none !important;
}
.dark .custom-tag-suggestions {
background: #101928 !important;
border: 1px solid #374151 !important;
}
.dark .custom-tag-suggestion {
color: #d1d5db !important;
border-bottom: 1px solid #374151 !important;
}
.dark .custom-tag-suggestion:hover {
background-color: #1f2937 !important;
}
.dark .custom-tag-suggestion.selected {
background-color: rgba(70, 123, 150, 0.2) !important;
}
.dark .custom-tag-suggestion strong {
color: #467B96 !important;
}
.dark .custom-tag-btn-confirm {
background-color: #467B96 !important;
color: #ffffff !important;
}
.dark .custom-tag-btn-confirm:hover {
background-color: #3a6980 !important;
}
.dark .custom-tag-btn-cancel {
background-color: #374151 !important;
color: #d1d5db !important;
}
.dark .custom-tag-btn-cancel:hover {
background-color: #4b5563 !important;
}
.dark .custom-tag-empty {
color: #9ca3af !important;
}
.dark .custom-tag-selected {
background-color: rgba(70, 123, 150, 0.1) !important;
border-left: 3px solid #467B96 !important;
}
.dark #custom-tag-button span {
color: #d1d5db !important;
}
.dark .custom-tag-input::placeholder {
color: #9ca3af !important;
}
</style>';
// 在JavaScript中嵌入标签数据
echo '<script>var CUSTOM_TAGS_ALL_TAGS = ' . $tagsJson . ';</script>';
}
}
/**
* 添加编辑器按钮 - 带标签联想搜索(本地搜索版)
*/
public static function addEditorButton()
{
?>
<style>
.dark body{color:#000!important;}
</style>
<script>
(function() {
// 等待编辑器加载完成
var initInterval = setInterval(function() {
var wmdButtonRow = document.querySelector('.wmd-button-row');
if (wmdButtonRow) {
clearInterval(initInterval);
addTagButton(wmdButtonRow);
}
}, 300);
function addTagButton(toolbar) {
// 检查是否已经添加过按钮
if (document.getElementById('custom-tag-button')) {
return;
}
// 创建按钮容器
var buttonContainer = document.createElement('li');
buttonContainer.id = 'custom-tag-button';
buttonContainer.className = 'wmd-button';
buttonContainer.style.cssText = 'width: 20px; height: 20px; position: relative;';
buttonContainer.title = '插入标签';
// 创建按钮内容
var buttonSpan = document.createElement('span');
buttonSpan.style.cssText = 'display: block; width: 100%; height: 100%; text-align: center; line-height: 20px; font-weight: bold; color: #AAA; cursor: pointer;';
buttonSpan.textContent = '#';
// 添加到容器
buttonContainer.appendChild(buttonSpan);
// 添加到工具栏末尾
toolbar.appendChild(buttonContainer);
// 添加点击事件
buttonContainer.onclick = function(e) {
e.preventDefault();
showTagSelector();
return false;
};
}
// 在本地搜索标签
function searchTags(keyword) {
var allTags = window.CUSTOM_TAGS_ALL_TAGS || [];
if (!keyword) {
// 如果没有关键词返回最近使用的标签这里简化返回前20个
return allTags.slice(0, 20);
}
keyword = keyword.toLowerCase();
var results = [];
// 搜索包含关键词的标签
for (var i = 0; i < allTags.length; i++) {
if (allTags[i].toLowerCase().indexOf(keyword) !== -1) {
results.push(allTags[i]);
if (results.length >= 20) {
break;
}
}
}
return results;
}
// 显示标签选择器
function showTagSelector() {
// 创建遮罩层
var overlay = document.createElement('div');
overlay.className = 'custom-tag-modal-overlay';
// 创建弹窗
var modal = document.createElement('div');
modal.className = 'custom-tag-modal';
modal.innerHTML = '<h3>选择或输入标签</h3>' +
'<input type="text" class="custom-tag-input" placeholder="输入标签名称搜索..." autocomplete="off">' +
'<div class="custom-tag-suggestions"></div>' +
'<div class="custom-tag-buttons">' +
'<button type="button" class="custom-tag-btn-cancel">取消</button>' +
'<button type="button" class="custom-tag-btn-confirm">插入标签</button>' +
'</div>';
// 添加到页面
document.body.appendChild(overlay);
document.body.appendChild(modal);
// 获取DOM元素
var input = modal.querySelector('.custom-tag-input');
var suggestions = modal.querySelector('.custom-tag-suggestions');
var confirmBtn = modal.querySelector('.custom-tag-btn-confirm');
var cancelBtn = modal.querySelector('.custom-tag-btn-cancel');
var selectedTag = '';
var selectedIndex = -1;
var suggestionItems = [];
// 聚焦输入框
setTimeout(function() {
input.focus();
}, 100);
// 加载初始标签列表
updateSuggestions('');
// 输入框输入事件
var searchTimer;
input.addEventListener('input', function() {
clearTimeout(searchTimer);
searchTimer = setTimeout(function() {
updateSuggestions(input.value.trim());
}, 200);
});
// 输入框键盘事件
input.addEventListener('keydown', function(e) {
if (e.key === 'ArrowDown') {
e.preventDefault();
selectNext();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectPrev();
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0 && suggestionItems[selectedIndex]) {
selectTag(suggestionItems[selectedIndex].dataset.tag);
} else if (input.value.trim()) {
insertTag(input.value.trim());
}
} else if (e.key === 'Escape') {
e.preventDefault();
closeModal();
}
});
// 确认按钮点击
confirmBtn.addEventListener('click', function() {
if (selectedTag) {
insertTag(selectedTag);
} else if (input.value.trim()) {
insertTag(input.value.trim());
} else {
alert('请输入标签名称!');
}
});
// 取消按钮点击
cancelBtn.addEventListener('click', closeModal);
// 点击遮罩层关闭
overlay.addEventListener('click', closeModal);
// 更新建议列表
function updateSuggestions(keyword) {
var tags = searchTags(keyword);
suggestions.innerHTML = '';
suggestionItems = [];
selectedIndex = -1;
if (tags.length === 0) {
if (keyword) {
suggestions.innerHTML = '<div class="custom-tag-empty">没有找到匹配的标签</div>';
} else {
suggestions.innerHTML = '<div class="custom-tag-empty">暂无标签,请输入新标签</div>';
}
return;
}
tags.forEach(function(tag, index) {
var item = document.createElement('div');
item.className = 'custom-tag-suggestion';
item.textContent = tag;
item.dataset.tag = tag;
// 高亮匹配的关键字
if (keyword) {
var escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\\\$&');
var regex = new RegExp('(' + escapedKeyword + ')', 'gi');
item.innerHTML = tag.replace(regex, '<strong>$1</strong>');
}
item.addEventListener('click', function() {
selectTag(tag);
});
item.addEventListener('mouseenter', function() {
clearSelection();
selectedIndex = index;
item.classList.add('selected');
});
suggestions.appendChild(item);
suggestionItems.push(item);
});
// 如果有建议,默认选择第一个
if (suggestionItems.length > 0) {
selectedIndex = 0;
suggestionItems[0].classList.add('selected');
}
}
// 选择下一个
function selectNext() {
if (suggestionItems.length === 0) return;
clearSelection();
selectedIndex = (selectedIndex + 1) % suggestionItems.length;
suggestionItems[selectedIndex].classList.add('selected');
scrollToSelected();
}
// 选择上一个
function selectPrev() {
if (suggestionItems.length === 0) return;
clearSelection();
selectedIndex = selectedIndex <= 0 ? suggestionItems.length - 1 : selectedIndex - 1;
suggestionItems[selectedIndex].classList.add('selected');
scrollToSelected();
}
// 清除选择
function clearSelection() {
suggestionItems.forEach(function(item) {
item.classList.remove('selected');
});
}
// 滚动到选中的项目
function scrollToSelected() {
if (selectedIndex >= 0 && suggestionItems[selectedIndex]) {
suggestionItems[selectedIndex].scrollIntoView({
block: 'nearest',
behavior: 'smooth'
});
}
}
// 选择标签
function selectTag(tag) {
selectedTag = tag;
input.value = tag;
// 高亮显示选中的标签
suggestionItems.forEach(function(item) {
item.classList.remove('custom-tag-selected');
if (item.dataset.tag === tag) {
item.classList.add('custom-tag-selected');
}
});
}
// 插入标签
function insertTag(tagName) {
// 验证标签名称
tagName = tagName.trim();
if (tagName.length < 2 || tagName.length > 20) {
alert('标签名称长度应在2-20个字符之间');
return;
}
if (!/^[\u4e00-\u9fa5a-zA-Z0-9_\-\s]+$/.test(tagName)) {
alert('标签名称只能包含中文、英文、数字、下划线和减号!');
return;
}
// 创建短代码
var shortcode = '[tag]' + tagName + '[/tag]';
// 获取编辑器textarea
var textarea = document.getElementById('text');
if (!textarea) {
alert('无法找到编辑器!');
return;
}
// 获取光标位置
var startPos = textarea.selectionStart;
var endPos = textarea.selectionEnd;
// 检查光标前的内容避免与Markdown标题混淆
var beforeText = textarea.value.substring(0, startPos);
var lines = beforeText.split('\\n');
var currentLine = lines[lines.length - 1];
// 如果当前行以#开头且没有空格,提示用户
var trimmedLine = currentLine.trim();
if (trimmedLine.match(/^#+[^#\\s]/)) {
if (!confirm('检测到当前位置可能在Markdown标题中插入标签可能会影响标题显示。确定要插入吗')) {
return;
}
}
// 插入短代码
textarea.value = textarea.value.substring(0, startPos) +
shortcode +
textarea.value.substring(endPos);
// 移动光标到短代码后面
textarea.selectionStart = textarea.selectionEnd = startPos + shortcode.length;
textarea.focus();
// 触发input事件
var event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
// 如果是Markdown编辑器触发预览更新
if (typeof window.convertMarkdown === 'function') {
window.convertMarkdown();
}
// 关闭弹窗
closeModal();
}
// 关闭弹窗
function closeModal() {
if (overlay.parentNode) {
document.body.removeChild(overlay);
}
if (modal.parentNode) {
document.body.removeChild(modal);
}
}
}
// 添加快捷键支持
document.addEventListener('keydown', function(e) {
// Ctrl+Alt+T 插入标签
if (e.ctrlKey && e.altKey && e.keyCode === 84) { // T键
e.preventDefault();
showTagSelector();
}
});
// 重新检查是否加载了工具栏(有些主题可能动态加载)
setTimeout(function() {
var wmdButtonRow = document.querySelector('.wmd-button-row');
if (wmdButtonRow && !document.getElementById('custom-tag-button')) {
addTagButton(wmdButtonRow);
}
}, 1000);
})();
</script>
<?php
}
/**
* 解析内容中的标签短代码
*/
public static function parseContent($content, $widget, $lastResult)
{
$content = empty($lastResult) ? $content : $lastResult;
// 如果是在后台列表或编辑页面,不转换
if ($widget instanceof Widget_Archive && !$widget->is('single')) {
return $content;
}
// 获取插件配置
$options = Helper::options()->plugin('CustomInlineTags');
$prefix = $options->tagPrefix ?: '#';
$style = $options->tagStyle ?: "display: inline-block; background: #f0f0f0; color: #333; padding: 2px 8px; margin: 0 4px; border-radius: 12px; font-size: 0.9em; font-weight: normal; text-decoration: none;";
$className = 'custom-inline-tag';
$enableLink = isset($options->enableLink) ? $options->enableLink : '1';
$tagUrlFormat = isset($options->tagUrlFormat) ? $options->tagUrlFormat : '/tag-{slug}.html';
$openNewWindow = isset($options->openNewWindow) ? $options->openNewWindow : '1';
// 替换短代码为HTML
$content = preg_replace_callback(
'/\[tag\]([^\[\]]+?)\[\/tag\]/',
function($matches) use ($prefix, $style, $className, $enableLink, $tagUrlFormat, $openNewWindow, $widget) {
$tagName = htmlspecialchars(trim($matches[1]), ENT_QUOTES, 'UTF-8');
$displayName = $prefix . $tagName;
if ($enableLink == '1' && $widget instanceof Widget_Archive) {
// 获取标签链接
$tagUrl = '';
try {
$db = Typecho_Db::get();
$tag = $db->fetchRow($db->select()
->from('table.metas')
->where('type = ?', 'tag')
->where('name = ?', $tagName)
->limit(1));
if ($tag) {
// 使用Typecho的标签链接生成方法
if (method_exists($widget, 'permalink')) {
// 创建临时对象获取标签链接
$reflection = new ReflectionClass($widget);
$params = $reflection->getProperty('_params');
$params->setAccessible(true);
$widgetParams = $params->getValue($widget);
// 临时修改参数获取标签链接
$originalParams = $widgetParams;
$widgetParams['type'] = 'tag';
$widgetParams['slug'] = $tag['slug'];
// 尝试获取标签链接
try {
$tagUrl = $widget->permalink;
} catch (Exception $e) {
// 如果失败,使用自定义格式
$tagSlug = urlencode($tag['slug']);
$tagUrl = str_replace('{slug}', $tagSlug, $tagUrlFormat);
$tagUrl = Typecho_Common::url($tagUrl, Helper::options()->index);
}
// 恢复原始参数
$params->setValue($widget, $originalParams);
} else {
// 使用自定义格式
$tagSlug = urlencode($tag['slug']);
$tagUrl = str_replace('{slug}', $tagSlug, $tagUrlFormat);
$tagUrl = Typecho_Common::url($tagUrl, Helper::options()->index);
}
}
} catch (Exception $e) {
// 出错时不添加链接
error_log('CustomInlineTags Error: ' . $e->getMessage());
}
if ($tagUrl) {
// 构建链接属性
$linkAttributes = sprintf('href="%s" class="%s" data-tag="%s" style="%s" title="查看标签相关文章"',
htmlspecialchars($tagUrl),
$className,
$tagName,
$style
);
// 如果启用了新窗口打开添加target="_blank"
if ($openNewWindow == '1') {
$linkAttributes .= ' target="_blank"';
}
return sprintf('<a %s>%s</a>',
$linkAttributes,
$displayName
);
}
}
// 如果不启用链接或找不到标签
return sprintf(
'<span class="%s" data-tag="%s" style="%s">%s</span>',
$className,
$tagName,
$style,
$displayName
);
},
$content
);
return $content;
}
/**
* 保存文章时提取标签并保存到数据库
*/
public static function parseTagsOnSave($contents, $widget)
{
$content = $contents['text'];
$tags = array();
// 从短代码中提取标签
preg_match_all('/\[tag\]([^\[\]]+?)\[\/tag\]/', $content, $matches);
if (!empty($matches[1])) {
foreach ($matches[1] as $tag) {
$tag = trim($tag);
if (!empty($tag) && !in_array($tag, $tags)) {
$tags[] = $tag;
}
}
}
// 如果找到了标签,添加到文章的标签字段
if (!empty($tags)) {
// 获取现有的标签
$existingTags = isset($contents['tags']) ? $contents['tags'] : '';
$existingTagsArray = array_filter(array_map('trim', explode(',', $existingTags)));
// 合并标签并去重
$allTags = array_unique(array_merge($existingTagsArray, $tags));
// 更新标签字段
$contents['tags'] = implode(',', $allTags);
}
return $contents;
}
}