{"id":31706595,"url":"https://github.com/howiehz/halo-plugin-extra-api","last_synced_at":"2026-03-14T10:15:12.873Z","repository":{"id":315758498,"uuid":"1060646577","full_name":"HowieHz/halo-plugin-extra-api","owner":"HowieHz","description":"A lightweight plugin providing extra data for Halo CMS","archived":false,"fork":false,"pushed_at":"2025-10-01T16:41:07.000Z","size":300,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-06T03:11:18.237Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/HowieHz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-20T09:58:25.000Z","updated_at":"2025-10-01T16:41:10.000Z","dependencies_parsed_at":"2025-09-20T15:53:52.848Z","dependency_job_id":null,"html_url":"https://github.com/HowieHz/halo-plugin-extra-api","commit_stats":null,"previous_names":["howiehz/halo-plugin-extra-api"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/HowieHz/halo-plugin-extra-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HowieHz%2Fhalo-plugin-extra-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HowieHz%2Fhalo-plugin-extra-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HowieHz%2Fhalo-plugin-extra-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HowieHz%2Fhalo-plugin-extra-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/HowieHz","download_url":"https://codeload.github.com/HowieHz/halo-plugin-extra-api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/HowieHz%2Fhalo-plugin-extra-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279000722,"owners_count":26082862,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-10-08T23:17:01.443Z","updated_at":"2026-03-14T10:15:12.865Z","avatar_url":"https://github.com/HowieHz.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# API 扩展包\n\n![GitHub](https://img.shields.io/github/license/HowieHz/halo-plugin-extra-api)\n![GitHub all releases](https://img.shields.io/github/downloads/HowieHz/halo-plugin-extra-api/total)\n![GitHub release (latest by date)](https://img.shields.io/github/downloads/HowieHz/halo-plugin-extra-api/latest/total)\n![GitHub repo size](https://img.shields.io/github/repo-size/HowieHz/halo-plugin-extra-api)\n[![Halo Version](https://img.shields.io/badge/Halo-2.22.0+-brightgreen.svg)](https://halo.run)\n\n## 简介\n\n一个为 Halo CMS 提供额外 API 的轻量级插件。\n\n快捷跳转：[版本说明](#版本说明)/[全量版使用须知](#全量版使用须知)/[文档目录](#文档目录)\n\n## 核心理念\n\n\u003e CMS 的价值就是在服务端管理和处理内容，为什么要把简单的数据处理推到前端去增加复杂度呢？\n\n让 CMS 回归\"内容即数据\"的本质，减少**不必要**的前端异步请求和动态渲染。\n\n这个插件正是基于这个理念：\n\n- 让复杂的逻辑在后端处理\n- 前端模板只负责展示\n- 为主题提供简洁的 Finder API\n- 减少不必要的 JavaScript 依赖\n\n\u003cdetails\u003e\u003csummary\u003e前端动态加载方式 vs 后端服务端渲染方式\u003c/summary\u003e\n\n| 对比维度                   | 前端动态加载方式                 | 后端服务端渲染方式              |\n|------------------------|--------------------------|------------------------|\n| **全页无刷兼容性（Pjax/Swup）** | ❌ 需要额外处理动态内容加载           | ✅ 模板渲染，天然支持无刷          |\n| **性能表现**               | ❌ 需要额外 HTTP 请求，增加延迟      | ✅ 服务端渲染，一次性输出          |\n| **用户体验**               | ❌ 页面闪烁，先显示占位符后填充数据       | ✅ 内容立即可见，无加载状态         |\n| **SEO 友好**             | ❌ 搜索引擎难以抓取动态内容           | ✅ 服务端渲染，完全 SEO 友好      |\n| **错误处理**               | ❌ 需要处理网络失败、超时等异常         | ✅ 服务端统一异常处理，减轻主题作者心智负担 |\n| **开发复杂度**              | ❌ 需要编写 JS 代码、状态管理、DOM 操作 | ✅ 模板中直接调用，代码简洁         |\n| **缓存策略**               | ❌ 需要前端缓存逻辑或重复请求          | ✅ 可利用模板缓存和服务端缓存        |\n| **首屏渲染 (FCP)**         | ❌ 需要等待 JS 执行和 API 响应     | ✅ HTML 直接包含内容，渲染更快     |\n| **最大内容绘制 (LCP)**       | ❌ 动态内容加载延迟主要内容显示         | ✅ 关键内容随页面一起渲染          |\n| **累积布局偏移 (CLS)**       | ❌ 内容异步加载可能导致页面跳动         | ✅ 静态布局，无意外的布局变化        |\n| **交互响应 (INP)**         | ❌ JS 执行和 DOM 操作影响交互性能    | ✅ 减少 JS 负担，交互更流畅       |\n\n\u003c/details\u003e\n\n## 功能介绍\n\n本插件现版本已提供以下功能：\n\n- 无需主题适配即可使用的功能：\n    - [中英文混排格式化处理器](#中英文混排格式化处理器)\n    - [代码高亮处理器](#代码高亮处理器)（仅全量版可用）\n- 提供给主题开发者使用的 Finder API：\n    - [插件本体信息相关 API](#插件本体信息相关-api)\n    - [文章字数统计 API（单篇/全站）](#文章字数统计-api)\n    - [HTML 内容字数统计 API](#html-内容字数统计-api)\n    - [中英文混排格式化 API](#中英文混排格式化-api)\n    - [代码高亮 API](#代码高亮-api)（仅全量版可用）\n\n未来将实现的功能：[TODO](#todo)\n\n欢迎为此插件提 [Issue](https://github.com/HowieHz/halo-plugin-extra-api/issues/new)，任何你需要的功能都可以在此处提出，我将在能力范围内尽力实现。\n\n💖 支持本项目: 如果你觉得这个插件有用，点个 [Star](https://github.com/HowieHz/halo-plugin-extra-api) 就是对我最大的鼓励!\n\n感谢所有支持本项目的用户和开发者，也特别感谢 Halo CMS 团队为插件生态提供的优秀平台。\n\n## 文档目录\n\n- [API 扩展包](#api-扩展包)\n  - [简介](#简介)\n  - [核心理念](#核心理念)\n  - [功能介绍](#功能介绍)\n  - [文档目录](#文档目录)\n  - [Finder API 文档](#finder-api-文档)\n    - [文档类型定义](#文档类型定义)\n    - [插件本体信息相关 API](#插件本体信息相关-api)\n      - [检测本插件是否启用](#检测本插件是否启用)\n      - [插件版本检测 API](#插件版本检测-api)\n    - [统计信息 API](#统计信息-api)\n      - [文章字数统计 API](#文章字数统计-api)\n      - [HTML 内容字数统计 API](#html-内容字数统计-api)\n    - [渲染 API](#渲染-api)\n      - [中英文混排格式化 API](#中英文混排格式化-api)\n    - [渲染 API（需要 JS 运行时）](#渲染-api需要-js-运行时)\n      - [代码高亮 API](#代码高亮-api)\n  - [处理器文档](#处理器文档)\n    - [中英文混排格式化处理器](#中英文混排格式化处理器)\n      - [功能说明](#功能说明)\n      - [配置选项](#配置选项)\n      - [补充说明](#补充说明)\n    - [代码高亮处理器](#代码高亮处理器)\n      - [特点](#特点)\n      - [配置选项](#配置选项-1)\n      - [支持的主题](#支持的主题)\n      - [补充说明](#补充说明-1)\n  - [版本说明](#版本说明)\n    - [轻量版的优势](#轻量版的优势)\n    - [轻量版本缺少的功能](#轻量版本缺少的功能)\n  - [全量版使用须知](#全量版使用须知)\n    - [基本使用要求](#基本使用要求)\n    - [全量版已知问题](#全量版已知问题)\n      - [问题一解决方案](#问题一解决方案)\n  - [下载和安装](#下载和安装)\n    - [稳定版](#稳定版)\n    - [开发版](#开发版)\n      - [下载步骤](#下载步骤)\n  - [开发指南/贡献指南](#开发指南贡献指南)\n  - [TODO](#todo)\n  - [许可证](#许可证)\n\n## Finder API 文档\n\n### 文档类型定义\n\n- `string`： 字符串类型\n- `int`：整数类型（实现为 BigInteger 即无限精度整数）\n- `boolean`：布尔类型（true/false）\n- `map`：映射类型（键值对）\n\n### 插件本体信息相关 API\n\n#### 检测本插件是否启用\n\n**描述**\n\n检测 ExtraAPI 插件是否已安装并启用。建议在主题中使用本插件 API 前先进行检测，以确保插件可用性。\n官方文档：[插件 Finder API](https://docs.halo.run/developer-guide/theme/finder-apis/plugin#availablepluginname-requiresversion)\n\n**参数**\n\n- `extra-api`\n- 解释：本插件的标识符（`metadata.name`）\n\n**返回值**\n\n- 类型：`boolean`\n- 解释：插件可用时返回 true，否则返回 false\n\n**说明**\n\n使用 `pluginFinder.available('extra-api')`\n可以优雅地处理插件依赖，避免在插件未安装时出现模板错误，提升主题的兼容性和用户体验。   \n注：在此基础上可以使用 `pluginFinder.available('extra-api', '3.*')` 锁定大版本号，避免 API 破坏性更新时导致主题渲染报错。\n\n**示例**\n\n```html\n\u003c!--/* 先检测插件可用性，再使用 API */--\u003e\n\u003cth:block th:if=\"${pluginFinder.available('extra-api')}\"\u003e\n    \u003cspan\n            th:text=\"|总字数：${extraApiStatsFinder.getPostWordCount()}|\"\n    \u003e\u003c/span\u003e\n\u003c/th:block\u003e\n\n\u003c!--/* 写在一个标签内也可以，th:if 的优先级比 th:text 高 */--\u003e\n\u003cspan\n        th:if=\"${pluginFinder.available('extra-api')}\"\n        th:text=\"|总字数：${extraApiStatsFinder.getPostWordCount()}|\"\n\u003e\u003c/span\u003e\n\n\u003c!--/* 自然模板写法 */--\u003e\n\u003cspan th:if=\"${pluginFinder.available('extra-api')}\"\u003e总字数：[[${extraApiStatsFinder.getPostWordCount()}]]\u003c/span\u003e\n```\n\n#### 插件版本检测 API\n\n**Finder 名称：** `extraApiPluginInfoFinder`\n\n**描述**\n\n提供插件版本类型检测功能，让主题或其他代码能够检测当前运行的是轻量版还是全量版插件，以便有条件地使用高级功能。\n（注：以下四个 API 本质上是**同一个 API**，您可以选择使用其中**任何一个**进行主题编写。）\n\n**API 方法**\n\n```javascript\n// 检查是否为全量版\nextraApiPluginInfoFinder.isFullVersion()\n\n// 检查是否为轻量版  \nextraApiPluginInfoFinder.isLiteVersion()\n\n// 获取版本类型字符串\nextraApiPluginInfoFinder.getVersionType()\n\n// 检查 JavaScript 功能是否可用\nextraApiPluginInfoFinder.isJavaScriptAvailable()\n```\n\n**参数**\n\n- 无\n\n**返回值**\n\n- `isFullVersion()`\n    - 类型：`boolean`\n    - 解释：全量版时返回 true，轻量版时返回 false\n- `isLiteVersion()`\n    - 类型：`boolean`\n    - 解释：轻量版时返回 true，全量版时返回 false\n- `getVersionType()`\n    - 类型：`string`\n    - 解释：返回 \"full\" 或 \"lite\"\n- `isJavaScriptAvailable()`\n    - 类型：`boolean`\n    - 解释：JavaScript 功能可用时返回 true\n\n**补充说明**\n\n- 检测原理\n    - 通过检查 `V8EnginePoolService` 类是否存在来判断版本类型：\n        - 全量版：包含 JavaScript 运行时，V8EnginePoolService 类存在\n        - 轻量版：构建时排除 js 包下所有类，V8EnginePoolService 类不存在\n- 应用场景\n    - 主题兼容性：主题可以根据插件版本提供不同的功能体验\n    - 用户提示：向用户说明当前版本的功能限制\n    - 条件渲染：仅在支持的版本中启用特定功能（主题请求不存在的 Finder API 会报错并阻止页面渲染）\n- 性能说明\n    - 这些方法单次调用开销极小，适合在模板中频繁使用\n    - 首次调用性能开销比后续调用稍高，但仍在毫秒级别（首次通过反射检查，后续直接读取缓存）\n\n**使用示例**\n\n```html\n\u003c!--/* 根据插件版本条件性显示 */--\u003e\n\u003cth:block th:unless=\"${extraApiPluginInfoFinder.isFullVersion()}\"\u003e\n    \u003cp\u003e当前使用轻量版插件，代码高亮功能不可用\u003c/p\u003e\n\u003c/th:block\u003e\n\n\u003c!--/* 显示当前版本类型 */--\u003e\n\u003cspan\u003e插件版本：[[${extraApiPluginInfoFinder.getVersionType()}]]\u003c/span\u003e\n\n\u003c!--/* 检查 JavaScript 功能可用性 */--\u003e\n\u003cdiv th:if=\"${extraApiPluginInfoFinder.isJavaScriptAvailable()}\"\u003e\n    \u003cp\u003eJavaScript 功能可用，支持高级渲染功能\u003c/p\u003e\n\u003c/div\u003e\n\n\u003c!--/* 结合其他条件使用，下面这段代码可直接用于 /templates/post.html */--\u003e\n\u003cth:block th:if=\"${pluginFinder.available('extra-api') and extraApiPluginInfoFinder.isFullVersion()}\"\u003e\n    \u003c!-- 只有在插件可用且为全量版时才显示 --\u003e\n    \u003cdiv th:utext=\"${extraApiRenderFinder.highlightCodeInHtml(post.content?.content)}\"\u003e\u003c/div\u003e\n\u003c/th:block\u003e\n```\n\n### 统计信息 API\n\n**Finder 名称：** `extraApiStatsFinder`\n\n#### 文章字数统计 API\n\n**描述**\n\n提供文章字数统计功能，支持统计单篇文章或全站文章的字数总和。可选择统计已发布版本或最新版本（含草稿）。适用于显示文章字数、阅读时间估算等场景。\n\n**API 方法（无参数）**\n\n```javascript\n// 统计全部文章已发布版本的总字数\nextraApiStatsFinder.getPostWordCount()\n```\n\n**参数**\n\n- 无\n\n**返回值**\n\n- 类型：`int` \n- 解释：字数统计结果（非负），不存在或参数缺失时返回 0\n\n**API 方法（传入映射形式参数）**\n\n```javascript\n// 传入映射形式参数\nextraApiStatsFinder.getPostWordCount({\n  name: 'post-metadata-name',  // 可选，未传入则统计全部文章字数总和\n  version: 'release' | 'draft'  // 可选，默认 'release'\n})\n```\n\n**参数**\n\n- 映射形式参数：\n    - `name`\n        - 类型：`string`\n        - 解释：文章的 `metadata.name`，可选参数。未传入时统计全部文章字数总和。\n    - `version`\n        - 类型：`string`\n        - 解释：统计版本，可选参数。`release`（未传入时默认值）或 `draft`。\n\n**返回值**\n\n- 类型：`int` \n- 解释：字数统计结果（非负），不存在或参数缺失时返回 0\n\n**补充说明**\n\n- 计数规则：\n    - 中文、日文、韩文等 CJK 字符按每个字符计 1。\n    - ASCII 连续字母/数字按 1 个单词计数。\n    - 标点符号和空格不计入统计。\n- 错误处理：\n    - 输入为空或文章不存在时返回 0，不会抛出异常。\n- 性能说明：\n    - 单次调用开销较小，适合在模板中直接使用。\n    - 启动时自动计算并缓存，仅在文章内容更新时重新计算。\n\n**使用示例**\n\n```html\n\u003c!--/* 统计文章已发布版本的字，下面这段代码可直接用于 /templates/post.html */--\u003e\n\u003cspan th:text=\"${extraApiStatsFinder.getPostWordCount({name: post.metadata.name})}\"\u003e\u003c/span\u003e\n\n\u003c!--/* 统计文章最新版本的字数（含草稿），下面这段代码可直接用于 /templates/post.html */--\u003e\n\u003cspan th:text=\"${extraApiStatsFinder.getPostWordCount({name: post.metadata.name, version: 'draft'})}\"\u003e\u003c/span\u003e\n\n\u003c!--/* 统计全站已发布文章的总字数，下面这段代码可直接用于全部模板 */--\u003e\n\u003cspan th:text=\"${extraApiStatsFinder.getPostWordCount()}\"\u003e\u003c/span\u003e\n\u003c!--/* 与下方写法等价 */--\u003e\n\u003cspan th:text=\"${extraApiStatsFinder.getPostWordCount({})}\"\u003e\u003c/span\u003e\n\n\u003c!--/* 统计全站所有文章最新版本的总字数（含草稿），下面这段代码可直接用于全部模板 */--\u003e\n\u003cspan th:text=\"${extraApiStatsFinder.getPostWordCount({version: 'draft'})}\"\u003e\u003c/span\u003e\n```\n\n#### HTML 内容字数统计 API\n\n**描述**\n\n提供 HTML 内容字数统计功能，支持统计任意 HTML 字符串的字数。适用于统计文章内容、瞬间内容或自定义 HTML 片段的字数。\n\n**API 方法（传入字符串形式参数）**\n\n```javascript\n// 直接传入 HTML 内容字符串进行统计\nextraApiStatsFinder.getContentWordCount(htmlContent)\n```\n\n**参数**\n\n- `htmlContent`\n    - 类型：`string`\n    - 解释：HTML 内容字符串（必需）。即要统计字数的 HTML 内容，支持标准 HTML 格式。\n\n**返回值**\n\n- 类型：`int` \n- 解释：字数统计结果（非负），输入为空时返回 0\n\n**API 方法（传入映射形式参数）**\n\n```javascript\n// 传入映射形式参数\nextraApiStatsFinder.getContentWordCount({\n  htmlContent: '\u003cp\u003eHTML 内容\u003c/p\u003e'  // 必需，要统计字数的 HTML 内容\n})\n```\n\n**参数**\n\n- 映射形式参数：\n    - `htmlContent`\n        - 类型：`string`\n        - 解释：HTML 内容字符串（必需）\n\n**返回值**\n\n- 类型：`int`\n- 解释：字数统计结果（非负），输入为空时返回 0\n\n**描述**\n\n- 计数规则：\n    - 自动移除 HTML 标签（包括 `\u003cscript\u003e` 和 `\u003cstyle\u003e` 标签）\n    - 中文、日文、韩文等 CJK 字符按每个字符计 1\n    - ASCII 连续字母/数字按 1 个单词计数\n    - 标点符号和空格不计入统计\n- 错误处理：\n    - 输入为空或 null 时返回 0，不会抛出异常\n- 性能说明：\n    - 单次调用开销较小，适合在模板中直接使用\n    - 纯计算操作，不涉及缓存和数据库查询\n\n**使用示例**\n\n```html\n\u003c!--/* 统计任意 HTML 内容的字数 */--\u003e\n\u003c!--/* 传入映射形式参数的写法 */--\u003e\n\u003cspan th:text=\"${extraApiStatsFinder.getContentWordCount({htmlContent: '\u003cp\u003e这是一段测试文本\u003c/p\u003e'})}\"\u003e\u003c/span\u003e\n\u003c!--/* 传入 HTML 内容字符串的写法 */--\u003e\n\u003cspan th:text=\"${extraApiStatsFinder.getContentWordCount('\u003cp\u003e这是一段测试文本\u003c/p\u003e')}\"\u003e\u003c/span\u003e\n\n\u003c!--/* 统计文章内容的字数，下面这段代码可直接用于 /templates/post.html */--\u003e\n\u003cspan th:text=\"${extraApiStatsFinder.getContentWordCount(post.content?.content)}\"\u003e\u003c/span\u003e\n\n\u003c!--/* 统计瞬间内容的字数，下面这段代码可直接用于 /templates/moment.html */--\u003e\n\u003cspan th:text=\"${extraApiStatsFinder.getContentWordCount(moment.spec.content?.html)}\"\u003e\u003c/span\u003e\n\n\u003c!--/* 在变量中使用 */--\u003e\n\u003cdiv th:with=\"wordCount=${extraApiStatsFinder.getContentWordCount(customContent)}\"\u003e\n    \u003cspan\u003e字数：[[${wordCount}]]\u003c/span\u003e\n\u003c/div\u003e\n```\n\n### 渲染 API\n\n**Finder 名称：** `extraApiRenderFinder`\n\n#### 中英文混排格式化 API\n\n**描述**\n\n提供中英文混排自动空格功能，自动在中日韩（CJK）字符与英文字母、数字、符号之间插入空格，提升内容可读性。\n\n**处理纯文本内容的 API 方法**\n\n```javascript\n// 对纯文本应用 Pangu 空格处理\nextraApiRenderFinder.applySpacingInText(text)\n```\n\n**参数**\n\n- `text`\n    - 类型：`string`\n    - 解释：要处理的纯文本内容\n\n**返回值**\n\n- 类型：`string`\n- 解释：处理后的内容，失败时返回原始内容\n\n**描述**\n\n- 处理规则\n  - 在中日韩字符和英文字母之间添加空格\n  - 在中日韩字符和数字之间添加空格\n  - 在中日韩字符和常见符号之间添加空格\n- 性能说明\n  - 纯 Java 实现，处理速度快，适合在模板中直接使用\n  - 不涉及缓存，每次调用都会重新处理\n- 错误处理\n  - 输入为空或 null 时返回空字符串\n  - HTML 解析失败时返回原始内容\n  - 不会抛出异常，保证页面渲染稳定性\n\n**使用示例**\n\n```html\n\u003c!--/* 对纯文本应用 Pangu 处理 */--\u003e\n\u003cspan th:text=\"${extraApiRenderFinder.applySpacingInText('请问Jackie的鼻子有几个？123个！')}\"\u003e\u003c/span\u003e\n\u003c!--/* 输出：请问 Jackie 的鼻子有几个？123 个！ */--\u003e\n```\n\n**处理 HTML 内容的 API 方法（传入字符串形式参数）**\n\n```javascript\n// 对整个 HTML 内容应用 Pangu 空格处理\nextraApiRenderFinder.applySpacingInHtml(htmlContent)\n```\n\n**参数**\n\n- `htmlContent`\n    - 类型：`string`\n    - 解释：包含 HTML 标签的内容\n\n**返回值**\n\n- 类型：`string`\n- 解释：处理后的内容，失败时返回原始内容\n\n**描述**\n\n- 处理规则\n  - 在中日韩字符和英文字母之间添加空格\n  - 在中日韩字符和数字之间添加空格\n  - 在中日韩字符和常见符号之间添加空格\n  - 跳过规则：自动跳过 `\u003ccode\u003e`、`\u003cpre\u003e`、`\u003cscript\u003e`、`\u003cstyle\u003e`、`\u003ctextarea\u003e` 等标签，保留其原始格式\n- 性能说明\n  - 纯 Java 实现，处理速度快，适合在模板中直接使用\n  - 不涉及缓存，每次调用都会重新处理\n- 错误处理\n  - 输入为空或 null 时返回空字符串\n  - HTML 解析失败时返回原始内容\n  - 不会抛出异常，保证页面渲染稳定性\n\n**使用示例**\n\n```html\n\u003c!--/* 处理整个 HTML 内容（自动跳过 code、pre 等标签） */--\u003e\n\u003cdiv th:utext=\"${extraApiRenderFinder.applySpacingInHtml(post.content?.content)}\"\u003e\u003c/div\u003e\n\n\u003c!--/* 在变量中使用 */--\u003e\n\u003cdiv th:with=\"processedContent=${extraApiRenderFinder.applySpacingInHtml(moment.spec.content?.html)}\"\u003e\n    \u003cdiv th:utext=\"${processedContent}\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n```\n\n**处理 HTML 内容的 API 方法（传入映射形式参数）**\n\n```javascript\n// 使用灵活参数对 HTML 内容应用 Pangu 空格处理\nextraApiRenderFinder.applySpacingInHtml({\n  htmlContent: '\u003cp\u003eHTML 内容\u003c/p\u003e',  // 必需\n  selector: 'p'  // 可选，CSS 选择器\n})\n```\n\n**参数**\n\n- `htmlContent`\n    - 类型：`string`\n    - 解释：包含 HTML 标签的内容（必需）\n- `selector`\n    - 类型：`string`\n    - 解释：CSS 选择器，用于定位特定元素（可选）\n    - 支持所有 JSoup CSS 选择器，包括：\n        - 标签选择器：`p`, `div`, `span`\n        - ID 选择器：`#main`, `#content`\n        - Class 选择器：`.article`, `.comment`\n        - 属性选择器：`[attr]`, `[attr=value]`\n        - 后代选择器：`div p`, `.article .content`\n        - 子选择器：`div \u003e p`\n        - 伪类选择器：`:first-child`, `:nth-child(n)`\n\n**返回值**\n\n- 类型：`string`\n- 解释：处理后的内容，失败时返回原始内容\n\n**描述**\n\n**描述**\n\n- 处理规则\n  - 在中日韩字符和英文字母之间添加空格\n  - 在中日韩字符和数字之间添加空格\n  - 在中日韩字符和常见符号之间添加空格\n  - 省略 `selector` 时应用跳过规则：自动跳过 `\u003ccode\u003e`、`\u003cpre\u003e`、`\u003cscript\u003e`、`\u003cstyle\u003e`、`\u003ctextarea\u003e` 等标签，保留其原始格式\n- 性能说明\n  - 纯 Java 实现，处理速度快，适合在模板中直接使用\n  - 不涉及缓存，每次调用都会重新处理\n- 错误处理\n  - 输入为空或 null 时返回空字符串\n  - HTML 解析失败时返回原始内容\n  - 不会抛出异常，保证页面渲染稳定性\n\n**使用示例**\n\n```html\n\u003c!--/* 使用 CSS 选择器仅处理段落标签 */--\u003e\n\u003cdiv th:utext=\"${extraApiRenderFinder.applySpacingInHtml({htmlContent: post.content?.content, selector: 'p'})}\"\u003e\u003c/div\u003e\n\n\u003c!--/* 使用 class 选择器处理指定 class 的元素 */--\u003e\n\u003cdiv th:utext=\"${extraApiRenderFinder.applySpacingInHtml({htmlContent: content, selector: '.article-content'})}\"\u003e\u003c/div\u003e\n\n\u003c!--/* 使用 ID 选择器处理指定 ID 的元素 */--\u003e\n\u003cdiv th:utext=\"${extraApiRenderFinder.applySpacingInHtml({htmlContent: content, selector: '#main'})}\"\u003e\u003c/div\u003e\n\n\u003c!--/* 使用复合选择器处理 */--\u003e\n\u003cdiv th:utext=\"${extraApiRenderFinder.applySpacingInHtml({htmlContent: content, selector: 'div.article \u003e p'})}\"\u003e\u003c/div\u003e\n\n\u003c!--/* 处理瞬间内容中的特定元素 */--\u003e\n\u003cdiv th:with=\"processedContent=${extraApiRenderFinder.applySpacingInHtml({htmlContent: moment.spec.content?.html, selector: '.moment-text'})}\"\u003e\n    \u003cdiv th:utext=\"${processedContent}\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n```\n\n### 渲染 API（需要 JS 运行时）\n\n此功能仅在全量版中可用。\n\n**Finder 名称：** `extraApiJsRenderFinder`\n\n#### 代码高亮 API\n\n```javascript\nextraApiJsRenderFinder.highlightCodeInHtml(htmlContent)\n```\n\n**参数**\n\n- `htmlContent`\n    - 类型：`string`\n    - 解释：包含代码块的 HTML 内容，通常是文章或页面的内容字段。\n\n**返回值**\n\n- 类型：`string`\n- 解释：渲染后的 HTML 内容，渲染样式已内联，渲染失败时返回原始内容\n\n**描述**\n\n- 功能特性：\n    - 同 [处理器文档 - 代码高亮（通过 Shiki.js 渲染）](#代码高亮处理器) 中描述的功能特性一致。也受同样的配置项影响。\n\n**使用示例**\n\n```html\n\u003c!--/* 渲染文章内容中的代码块，下面这段代码可直接用于 /templates/post.html */--\u003e\n\u003cdiv th:utext=\"${extraApiJsRenderFinder.highlightCodeInHtml(post.content?.content)}\"\u003e\u003c/div\u003e\n\n\u003c!--/* 在模板中使用，下面这段代码可直接用于 /templates/moment.html */--\u003e\n\u003cdiv th:with=\"renderedContent=${extraApiJsRenderFinder.highlightCodeInHtml(moment.spec.content?.html)}\"\u003e\n    \u003cdiv th:utext=\"${renderedContent}\"\u003e\u003c/div\u003e\n\u003c/div\u003e\n```\n\n## 处理器文档\n\n### 中英文混排格式化处理器\n\n插件提供了自动化的中英文混排格式化处理器，无需在模板中手动调用，即可对文章和页面内容自动应用 Pangu 空格处理。\n\n#### 功能说明\n\n- 自动处理范围：\n    - 处理器会自动处理文章（post）和页面（page）内容中的段落标签（`\u003cp\u003e`）\n    - 在中日韩字符与英文、数字、符号之间自动插入空格\n- 处理规则：\n    - 自动在 CJK 字符和英文字母之间添加空格\n    - 自动在 CJK 字符和数字之间添加空格\n    - 自动在 CJK 字符和常见符号之间添加空格\n    - 智能跳过 `\u003ccode\u003e`、`\u003cpre\u003e`、`\u003cscript\u003e`、`\u003cstyle\u003e`、`\u003ctextarea\u003e` 等标签\n- 性能特点：\n    - 纯 Java 实现，无需 JavaScript 运行时\n    - 处理速度快，对页面加载影响极小\n    - 不涉及缓存，每次渲染时实时处理\n- 错误处理：\n    - 处理失败时保持原始内容不变\n    - 不会因格式化问题影响页面正常渲染\n\n#### 配置选项\n\n此处理器默认启用。开箱即用，无需额外配置。\n\n在“插件设置 - 中英文混排格式化”提供以下配置项：\n- 自动渲染: 启用之后会自动处理文章和单页中段落标签的中英文混排格式，在中日韩字符与英文、数字、符号之间自动插入空格。注：Finder API 渲染不受此配置项影响。\n\n#### 补充说明\n\n- 此功能使用 [Pangu.java](https://github.com/vinta/pangu.java) 库实现\n- 仅处理可见文本内容，不影响 HTML 结构\n- 递归处理嵌套元素，确保完整覆盖\n- 与本插件提供的其他处理器兼容，互不影响\n\n### 代码高亮处理器\n\n插件提供了自动化的代码高亮处理器，无需在模板中手动调用，即可对文章和页面内容中的代码块进行语法高亮渲染。\n\n此功能通过 Shiki.js 渲染，仅在[全量版](#版本说明)中可用。\n\n#### 特点\n\n- 双主题支持：\n    - 可同时渲染浅色和深色主题，便于主题切换\n- 语言自动检测：\n    - 从 `class` 属性中提取语言标识（如 `language-java`、`lang-python`）\n- 容错处理：\n    - 渲染失败时保持原始代码块不变\n- 默认自动渲染范围：\n    - 处理器会自动处理以下页面内容并在页面 `head` 注入自定义 CSS 样式：\n        - 文章内容 (`post`)\n        - 页面内容 (`page`)\n- 性能说明：\n    - 命中缓存时响应速度极快（微秒级），首次渲染需要 1-3 秒初始化 JS 环境\n    - 批量处理策略：\n        - 智能分组：根据 V8 引擎池大小动态分配任务（例如 14 个任务 + 5 个引擎 → 5 组，每组 2-3 个任务）\n        - 并行执行：多个任务组并行处理，充分利用多核性能\n        - 批量渲染：同一组内的任务在单个引擎中通过一次 JS 通信批量处理，减少引擎切换开销\n        - 优先缓存：先检查缓存，只对未命中的请求进行实际渲染\n    - 缓存策略：\n        - LRU + TTL 双重策略：最多缓存 10,000 个代码块，每个条目 24 小时自动过期\n        - 智能去重：相同代码内容+语言+主题的重复渲染会自动去重，避免重复计算\n        - 缓存键：基于代码内容的 SHA-256 哈希值，避免长代码占用过多内存\n\n#### 配置选项\n\n在“插件设置 - 代码高亮（仅全量版可用）”提供以下配置项：\n- 自动渲染: 启用之后会自动渲染文章和单页中的代码块。注：Finder API 渲染不受此配置项影响。\n- 自定义注入 CSS 样式：启用自动渲染时将在页面 head 注入样式以优化 Shiki 渲染效果，默认值提供了边距调整/行号显示/基于媒体查询的明暗切换功能。\n    - 额外注入规则: 指定额外的页面路径规则，支持通配符。\n        - 默认包含: `/moments/**`, `/docs/**`\n        - 支持自定义路径，如 `/custom/**`\n- 明暗双倍渲染模式切换：\n    - 单主题模式:\n        - 主题: 选择单个代码高亮主题\n    - 双主题模式:\n        - 亮色主题: 浅色模式使用的主题\n        - 暗色主题: 深色模式使用的主题\n        - 亮色主题代码块类名: 浅色代码块的 CSS 类名\n        - 暗色主题代码块类名: 深色代码块的 CSS 类名\n\n#### 支持的主题\n\n插件内置了 118 个 Shiki 主题，包括：\n\n- GitHub Light/Dark 系列\n- One Light/Dark Pro\n- Nord, Dracula, Monokai\n- Material Theme 系列\n- Catppuccin 系列\n\n完整主题列表: 请在 Halo 管理后台的插件设置页面查看所有可用主题。或前往[官方文档](https://shiki.zhcndoc.com/themes)在线预览。\n\n#### 补充说明\n\n- 工作原理：\n    1. 内容处理阶段：在内容渲染时，处理器会扫描 HTML 中的 `\u003cpre\u003e\u003ccode\u003e` 结构\n    2. 语言检测：从 `class` 属性中提取语言标识（如 `language-java`、`lang-python`）\n    ```html\n    \u003c!-- 标准 Markdown 格式 --\u003e\n    \u003cpre\u003e\u003ccode class=\"language-java\"\u003epublic class Hello { }\u003c/code\u003e\u003c/pre\u003e\n    \n    \u003c!-- 简写格式 --\u003e\n    \u003cpre\u003e\u003ccode class=\"lang-python\"\u003eprint(\"Hello World\")\u003c/code\u003e\u003c/pre\u003e\n    \n    \u003c!-- 直接在 pre 标签上指定 --\u003e\n    \u003cpre class=\"language-javascript\"\u003e\u003ccode\u003econsole.log(\"Hello\");\u003c/code\u003e\u003c/pre\u003e\n    ```\n    3. 主题应用：根据配置应用相应的 Shiki 主题进行高亮\n    4. 样式注入：在页面头部注入配置的 CSS\n- 错误处理：\n    - 不支持的语言/主题会被跳过渲染\n    - 渲染失败时保留原始格式\n- 性能说明：\n    - 使用 V8 引擎池和异步处理，提升渲染效率\n- 补充说明：\n    - 双主题模式会生成两个并列的 div 元素\n\n## 版本说明\n\n插件提供两个版本：\n\n- **全量版**：包含所有功能，包括代码高亮等依赖 JS 的相关功能。\n- **轻量版**：轻量级版本，不包含 JS 相关功能和相关依赖。\n\n### 轻量版的优势\n\n- 更小的插件体积\n- 更快的启动速度\n- 更低的内存占用\n- 更低的系统性能要求\n- 支持全平台（全量版仅支持以下平台：Linux ARM64、Linux x86_64、macOS ARM64、macOS x86_64、Windows x86_64）\n\n### 轻量版本缺少的功能\n\n- 代码高亮（Shiki.js 渲染）\n\n\u003c!-- - 图表渲染（Mermaid）\n- 公式渲染（KaTeX） --\u003e\n\n- 其他 JS 运行时相关功能\n\n如果您需要上述功能，请使用全量版。\n\n## 全量版使用须知\n\n⏱️ **首次使用提示**: 全量版的 JavaScript 功能（如 Shiki 代码高亮）在首次调用时需要 1~3 秒进行环境初始化。这是正常现象，之后的调用速度会非常快。\n\n\u003e ⚠️ **重要**: 全量版依赖 Javet 加载 Node.js 原生库（基于 JNI），受 Halo 插件架构限制，存在[已知问题](#全量版已知问题)。\n\n### 基本使用要求\n\n1. **请勿热重载更新**: 请勿直接覆盖更新（如使用应用商店/插件列表的快捷更新操作）\n2. **正确的更新流程**:\n    - 方法一：停止插件 → 卸载插件 → **重启 Halo** → 安装新版本 → 启动插件\n    - 方法二：停止插件 → 卸载插件 → 安装新版本 → **重启 Halo** → 启动插件\n3. **卸载推荐做法**: 在卸载全量版后**重启 Halo** 确保原生库资源完全释放。\n\n### 全量版已知问题\n\n- 问题一：卸载后重新安装全量版插件，不重启就启用。调用 JS 相关 API 时会出现错误：\n    - **首次安装并启动**: 正常工作\n    - **禁用后重新启用**: 正常工作\n    - **卸载后重新安装**: ❌ 会出现 `JavetException: Javet library is not loaded` 错误\n    - **重启 Halo 后**: 恢复正常工作\n\n#### 问题一解决方案\n\n安装好新版本插件后**先别启用**！   \n在启用新版本插件之前，请**先重启 Halo CMS**。\n\n未来版本会将引擎作为前置依赖插件，更新本插件不再需要重启。\n\n\u003cdetails\u003e\n\u003csummary\u003e📖 技术原因分析（展开查看详细说明）\u003c/summary\u003e\n\n根据错误日志和 Halo 架构分析:\n\n**问题根源**:\n\n1. **Halo 插件架构**: 根据 [Halo 开发文档 - 插件架构](https://docs.halo.run/developer-guide/core/framework), Halo 使用\n   Spring Plugin Framework 实现插件隔离\n    - 每个插件拥有独立的 Spring ApplicationContext\n    - 每个插件使用独立的 classloader 加载资源\n    - 插件间通过 ExtensionPoint 机制通信\n    - classloader 完全隔离,无法跨插件共享类或资源\n\n2. **Javet 原生库加载机制**:\n    - Javet 需要从 JAR 中提取原生库文件(`.dll`/`.so`/`.dylib`)到临时目录\n    - JVM 通过 JNI 加载这些原生库文件\n    - 原生库文件一旦加载,会被 JVM 锁定\n\n3. **冲突发生过程**:\n    - 安装插件时,Javet 提取原生库文件到 `C:\\Users\\用户名\\AppData\\Local\\Temp\\javet\\进程ID\\`（以 Windows 举例）\n    - JVM 加载这些文件并锁定\n    - 卸载插件时,虽然 classloader 被销毁,但原生库文件仍被 JVM 锁定,无法删除\n    - 重新安装时,Javet 尝试提取新文件到同一位置\n    - 因为文件被锁定,提取失败\n    - Javet 初始化失败,报错 `Javet library is not loaded because \u003cnull\u003e`\n\n**典型错误日志解读**:\n\n```\nWARN - Failed to write to ...libjavet-node-windows-x86_64.v.4.1.7.dll because it is locked\nERROR - Native Library already loaded in another classloader\nERROR - JavetException: Javet library is not loaded because \u003cnull\u003e\n```\n\n这三条日志清晰地展示了整个失败过程:\n\n1. 尝试写入文件失败(文件被锁定)\n2. 检测到库已在其他 classloader 中加载\n3. 初始化失败，因为无法提取必要的库文件\n\n**为什么重启有效**:\n\n- 重启 Halo 会终止 JVM 进程\n- JVM 终止时会释放所有文件锁\n- 临时目录中的原生库文件会被清理\n- 新的 Halo 进程启动后，Javet 可以正常提取和加载原生库\n\n**为什么 Javet 文档中提到的 JVM 参数无效**:\n\n`-Djavet.lib.loading.suppress.error=true` 这个参数的作用是:\n\n- 抑制 Javet 在检测到\"already loaded in another classloader\"时的错误日志\n- **但无法解决文件锁定问题**\n- 当库文件无法提取时，Javet 根本无法完成初始化\n- 因此该参数在这个场景下无效\n\n**架构层面的限制**:\n\n这个问题是 JVM/JNI + 插件 classloader 隔离的固有矛盾:\n\n- Halo 的插件隔离设计保证了安全性和稳定性\n- 但也导致原生库这类 JVM 级别资源难以管理\n- 类似问题在所有使用 classloader 隔离的插件系统中都存在\n- 这不是 Javet 或本插件的 bug，而是架构层面的限制\n\n**相关技术文档**:\n\n- [Javet - Load and Unload](https://www.caoccao.com/Javet/reference/resource_management/load_and_unload.html)\n- [Javet Issue #124 - Classloader Reload](https://github.com/caoccao/Javet/issues/124)\n- [Halo - Plugin Framework](https://docs.halo.run/developer-guide/core/framework)\n\n\u003c/details\u003e\n\n## 下载和安装\n\n全量版和轻量版的区别请看：[版本说明](#版本说明)\n使用全量版前请看：[全量版使用须知](#全量版使用须知)\n\n### 稳定版\n\n稳定版通过 GitHub Releases 发布，建议生产环境使用。\n\n1. 访问 [Releases 页面](https://github.com/HowieHz/halo-plugin-extra-api/releases)\n2. 下载最新版本的 JAR 文件：\n    - `extra-api-lite-版本号.jar`：轻量版（适用于所有平台）\n    - `extra-api-full-all-platforms-版本号.jar`：全量版（包含所有平台依赖，体积较大，不推荐下载）\n    - 如需使用全量版，推荐下载平台特定版本：\n        - `extra-api-full-linux-arm64-版本号.jar`：适用于 Linux ARM64 平台的版本\n        - `extra-api-full-linux-x86_64-版本号.jar`：适用于 Linux x86_64 平台的版本\n        - `extra-api-full-macos-arm64-版本号.jar`：适用于 macOS ARM64 平台的版本\n        - `extra-api-full-macos-x86_64-版本号.jar`：适用于 macOS x86_64 平台的版本\n        - `extra-api-full-windows-x86_64-版本号.jar`：适用于 Windows x86_64 平台的版本\n3. 将下载的 JAR 文件上传到 Halo 的插件管理页面安装\n\n### 开发版\n\n插件的开发版 JAR 文件通过 GitHub Actions 自动构建，仅用于测试和开发。\n\n- [GitHub Actions Workflow](https://github.com/HowieHz/halo-plugin-extra-api/actions/workflows/workflow.yaml)\n\n#### 下载步骤\n\n1. 访问上述链接，选择最新的成功运行的 workflow。\n2. 在 \"Artifacts\" 部分，下载 `extra-api` 压缩包。\n3. 解压后，您将找到以下 JAR 文件：\n    - `extra-api-lite-版本号-SNAPSHOT.jar`：轻量版（适用于所有平台）\n    - `extra-api-full-all-platforms-版本号-SNAPSHOT.jar`：全量版（包含所有平台依赖）\n    - `extra-api-full-linux-arm64-版本号-SNAPSHOT.jar`：全量版（适用于 Linux ARM64 平台）\n    - `extra-api-full-linux-x86_64-版本号-SNAPSHOT.jar`：全量版（适用于 Linux x86_64 平台）\n    - `extra-api-full-macos-arm64-版本号-SNAPSHOT.jar`：全量版（适用于 macOS ARM64 平台）\n    - `extra-api-full-macos-x86_64-版本号-SNAPSHOT.jar`：全量版（适用于 macOS x86_64 平台）\n    - `extra-api-full-windows-x86_64-版本号-SNAPSHOT.jar`：全量版（适用于 Windows x86_64 平台）\n\n选择适合您系统的 JAR 文件安装到 Halo。\n\n## 开发指南/贡献指南\n\n参见 [CONTRIBUTING.md](./CONTRIBUTING.md)\n\n## TODO\n\n\u003cdetails\u003e\u003csummary\u003e展开折叠内容\u003c/summary\u003e\n\n- [ ] 提供随机文章 API\n- [ ] 提供预计阅读时间 API，及相关配置项\n- [ ] 提供图表渲染 API\n- [ ] 提供公式渲染 API\n- [ ] 分离 Node.js 环境支持为可选前置插件（预计 4.0 版本实现）\n\n\u003c/details\u003e\n\n## 许可证\n\n[AGPL-3.0](./LICENSE) © HowieHz\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhowiehz%2Fhalo-plugin-extra-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhowiehz%2Fhalo-plugin-extra-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhowiehz%2Fhalo-plugin-extra-api/lists"}