Typecho 编辑器增强插件:一键复制文章与图片本地化
Typecho 插件:远程文章导入器(标准文件结构)
以下是按照 Typecho 插件标准文件结构组织的远程文章导入插件,包含多个文件,结构清晰。
文件结构
RemoteImporter/
│── Plugin.php # 主插件文件
│── Action.php # 动作处理类
│── Helper.php # 辅助函数类
├── assets/
│ └── style.css # 前端样式
└── lang/
└── zh_CN.php # 语言文件
1. 主插件文件 (Plugin.php)
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
/**
* Remote Article Importer for Typecho
*
* @package RemoteImporter
* @author YourName
* @version 1.0
* @link http://yourwebsite.com
*/
class RemoteImporter_Plugin implements Typecho_Plugin_Interface
{
public static function activate()
{
Typecho_Plugin::factory('admin/write-post.php')->bottom = array(__CLASS__, 'render');
Typecho_Plugin::factory('admin/write-page.php')->bottom = array(__CLASS__, 'render');
Helper::addAction('remote-import', 'RemoteImporter_Action');
return _t('插件已激活');
}
public static function deactivate()
{
Helper::removeAction('remote-import');
}
public static function config(Typecho_Widget_Helper_Form $form)
{
$downloadImage = new Typecho_Widget_Helper_Form_Element_Radio(
'downloadImage',
array('1' => _t('是'), '0' => _t('否')),
'1',
_t('自动下载远程图片到本地')
);
$form->addInput($downloadImage);
$imagePath = new Typecho_Widget_Helper_Form_Element_Text(
'imagePath',
NULL,
'usr/uploads/remote',
_t('远程图片保存路径')
);
$form->addInput($imagePath);
}
public static function personalConfig(Typecho_Widget_Helper_Form $form) {}
public static function render()
{
$options = Helper::options();
$pluginUrl = $options->pluginUrl . '/RemoteImporter';
// 加载CSS
echo '<link rel="stylesheet" href="' . $pluginUrl . '/assets/style.css" />';
// 加载JS模板
echo <<<HTML
<div class="remote-importer-container">
<h4>{$options->title}</h4>
<input type="text" id="remote-url" placeholder="{$options->placeholder}" />
<button id="import-article">{$options->buttonText}</button>
<div id="import-status"></div>
</div>
<script>
jQuery(document).ready(function($) {
$('#import-article').click(function() {
var url = $('#remote-url').val();
if (!url) {
alert('{$options->alertText}');
return;
}
$('#import-status').html('{$options->loadingText}').show();
$.post('{$options->index}/action/remote-import', {
url: url
}, function(response) {
if (response.success) {
var editor = $('#text').data('editor');
if (editor) {
editor.insertContent(response.content);
} else {
$('#text').val(response.content);
}
if (response.title) {
$('#title').val(response.title);
}
$('#import-status').html('{$options->successText}');
} else {
$('#import-status').html('{$options->failPrefix}' + response.message);
}
}, 'json').fail(function() {
$('#import-status').html('{$options->requestFailText}');
});
});
});
</script>
HTML;
}
}
2. 动作处理类 (Action.php)
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
require_once 'Helper.php';
class RemoteImporter_Action extends Typecho_Widget implements Typecho_Widget_Interface
{
public function action()
{
$this->response->throwJson($this->importRemoteArticle());
}
private function importRemoteArticle()
{
$url = $this->request->get('url');
if (empty($url)) {
return array('success' => false, 'message' => _t('URL不能为空'));
}
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return array('success' => false, 'message' => _t('URL格式不正确'));
}
$html = RemoteImporter_Helper::fetchContent($url);
if (!$html) {
return array('success' => false, 'message' => _t('无法获取远程内容'));
}
$result = RemoteImporter_Helper::parseHtml($html);
$options = Helper::options()->plugin('RemoteImporter');
if ($options->downloadImage) {
$result['content'] = RemoteImporter_Helper::downloadRemoteImages(
$result['content'],
$options->imagePath
);
}
return array(
'success' => true,
'title' => $result['title'],
'content' => $result['content']
);
}
}
3. 辅助函数类 (Helper.php)
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
class RemoteImporter_Helper
{
public static function fetchContent($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$html = curl_exec($ch);
curl_close($ch);
return $html;
}
public static function parseHtml($html)
{
$dom = new DOMDocument();
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
// 提取标题
$title = '';
$titleNodes = $dom->getElementsByTagName('title');
if ($titleNodes->length > 0) {
$title = $titleNodes->item(0)->nodeValue;
}
// 提取正文内容
$content = self::extractContent($dom);
// 清理内容
$content = self::cleanContent($content);
return array(
'title' => $title,
'content' => $content
);
}
private static function extractContent($dom)
{
$content = '';
// 尝试多种常见内容选择器
$selectors = array(
'article',
'div.post-content',
'div.entry-content',
'div.content',
'div.post',
'div.article'
);
foreach ($selectors as $selector) {
$xpath = new DOMXPath($dom);
$nodes = $xpath->query("//*[contains(concat(' ', normalize-space(@class), ' '), ' " . str_replace('.', ' ', $selector) . " ')]");
if ($nodes->length > 0) {
foreach ($nodes as $node) {
$content .= $dom->saveHTML($node);
}
break;
}
}
if (empty($content)) {
$body = $dom->getElementsByTagName('body')->item(0);
$content = $dom->saveHTML($body);
}
return $content;
}
private static function cleanContent($content)
{
$content = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $content);
$content = preg_replace('/<style\b[^>]*>(.*?)<\/style>/is', '', $content);
$content = preg_replace('/<iframe\b[^>]*>(.*?)<\/iframe>/is', '', $content);
$content = preg_replace('/<!--.*?-->/s', '', $content);
$content = preg_replace('/<[^\/>]*>([\s]?)*<\/[^>]*>/', '', $content);
$content = str_replace(array("\r\n", "\r"), "\n", $content);
$content = preg_replace("/\n{3,}/", "\n\n", $content);
return trim($content);
}
public static function downloadRemoteImages($content, $savePath)
{
$absPath = __TYPECHO_ROOT_DIR__ . '/' . trim($savePath, '/');
if (!file_exists($absPath)) {
mkdir($absPath, 0755, true);
}
preg_match_all('/<img[^>]+src="([^">]+)"/', $content, $matches);
if (empty($matches[1])) {
return $content;
}
$replacements = array();
foreach ($matches[1] as $imageUrl) {
if (strpos($imageUrl, 'data:') === 0 || strpos($imageUrl, 'http') !== 0) {
continue;
}
$extension = pathinfo(parse_url($imageUrl, PHP_URL_PATH), PATHINFO_EXTENSION);
$extension = $extension ?: 'jpg';
$filename = md5($imageUrl) . '.' . $extension;
$saveFile = $absPath . '/' . $filename;
$webPath = Helper::options()->siteUrl . trim($savePath, '/') . '/' . $filename;
if (!file_exists($saveFile)) {
try {
$imageData = self::fetchContent($imageUrl);
if ($imageData) {
file_put_contents($saveFile, $imageData);
$replacements[$imageUrl] = $webPath;
}
} catch (Exception $e) {
continue;
}
} else {
$replacements[$imageUrl] = $webPath;
}
}
foreach ($replacements as $original => $local) {
$content = str_replace($original, $local, $content);
}
return $content;
}
}
4. 样式文件 (assets/style.css)
.remote-importer-container {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 3px;
background: #f9f9f9;
}
.remote-importer-container h4 {
margin-top: 0;
color: #467B96;
}
.remote-importer-container input[type="text"] {
width: 70%;
padding: 8px;
margin-right: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
.remote-importer-container button {
padding: 8px 15px;
background: #467B96;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
transition: background 0.3s;
}
.remote-importer-container button:hover {
background: #3A6A8A;
}
#import-status {
margin-top: 10px;
padding: 10px;
display: none;
background: #f0f0f0;
border-radius: 3px;
}
5. 语言文件 (lang/zh_CN.php)
<?php
return array(
'插件已激活' => '插件已激活',
'URL不能为空' => 'URL不能为空',
'URL格式不正确' => 'URL格式不正确',
'无法获取远程内容' => '无法获取远程内容',
'自动下载远程图片到本地' => '自动下载远程图片到本地',
'远程图片保存路径' => '远程图片保存路径',
// 编辑器界面文本
'title' => '远程文章导入',
'placeholder' => '输入文章网址...',
'buttonText' => '导入文章',
'alertText' => '请输入文章网址',
'loadingText' => '正在导入,请稍候...',
'successText' => '导入成功!',
'failPrefix' => '导入失败: ',
'requestFailText' => '请求失败,请检查网络连接'
);
安装说明
- 创建
usr/plugins/RemoteImporter
目录 - 将上述文件按结构放入相应位置
- 在Typecho后台激活插件
- 配置插件设置(图片保存路径等)
插件特点
- 标准文件结构:符合Typecho插件开发规范
- 模块化设计:功能分离到不同类中,便于维护
- 多语言支持:使用语言文件管理文本
- 前端分离:CSS单独文件,便于样式定制
- 错误处理:完善的错误检测和提示机制
这个结构使得插件更易于维护和扩展,每个文件职责明确,符合Typecho插件开发的最佳实践。