Typecho 编辑器增强插件:一键复制文章与图片本地化 - Winmax Music

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' => '请求失败,请检查网络连接'
);

安装说明

  1. 创建 usr/plugins/RemoteImporter 目录
  2. 将上述文件按结构放入相应位置
  3. 在Typecho后台激活插件
  4. 配置插件设置(图片保存路径等)

插件特点

  1. 标准文件结构:符合Typecho插件开发规范
  2. 模块化设计:功能分离到不同类中,便于维护
  3. 多语言支持:使用语言文件管理文本
  4. 前端分离:CSS单独文件,便于样式定制
  5. 错误处理:完善的错误检测和提示机制

这个结构使得插件更易于维护和扩展,每个文件职责明确,符合Typecho插件开发的最佳实践。

标签: none



没事发点牢骚,评论几句?!Nothing to complain about, comment a few words.