Skip to content

Commit 75bf1a4

Browse files
authored
feat: add JNI HTML minify web filter and harden plugin config defaults (#90)
1 parent 551a2b9 commit 75bf1a4

23 files changed

Lines changed: 1326 additions & 182 deletions

CONTRIBUTING.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ pnpm dev
2424
./gradlew build
2525
```
2626

27+
## 配置约定
28+
29+
- 默认值设定由 `src/main/resources/extensions/settings.yaml` 统一定义。
30+
- 配置类、`fallbackConfig()`、配置提供方(Supplier)和运行时初始化逻辑中,不要重复写同一份业务默认值。
31+
- 设置缺失、读取失败或对象为空时,可以返回最小可用的兜底配置;但兜底配置不应再维护一份和 `settings.yaml` 并行的业务默认值。
32+
- 如果某个配置项在运行时可能出现非法值,并且会导致初始化失败,或者旧数据、脏数据可能绕过界面约束,应在消费前校验,必要时修正为可用值,而不是在配置类中再补一套默认值。
33+
2734
### 资源处理任务
2835

2936
- __processUiResources__ - 处理UI前端资源,将ui子项目的构建输出复制到console目录
@@ -44,6 +51,19 @@ pnpm dev
4451

4552
构建完成后,可以在 `build/libs` 目录找到插件 jar 文件。
4653

54+
## 已知问题
55+
56+
以下问题属于当前架构和上游依赖形态带来的限制,开发时需要明确认知。
57+
58+
### HTML 页面压缩的响应体改写不是流式的
59+
60+
- 相关实现:
61+
- `src/main/java/top/howiehz/halo/plugin/extra/api/service/interop/web/filter/htmlminify/HtmlMinifyWebFilter.java`
62+
- 当前 HTML 页面压缩功能依赖 Halo 的附加 Web 过滤器扩展点(`AdditionalWebFilter`)完整读取并重写响应体。
63+
- 这意味着在压缩前必须先聚合完整的 HTML 响应内容,再交给 `minify-html` 处理。
64+
- 因此该功能天然会带来一次额外的内存占用和复制成本,无法像真正的流式转换那样边读边压。
65+
- 这不是当前实现的疏漏,而是 Halo 附加 Web 过滤器扩展点(`AdditionalWebFilter`)的接入方式和 `minify-html` 接口形态共同决定的限制。
66+
4767
## 如何添加新的嵌入式 JS 模块
4868

4969
本项目将 JavaScript 工具嵌入到 Java 运行时中,并将其预加载到 Javet V8 运行时中。按照以下步骤添加对新 JS 模块的支持。
@@ -70,13 +90,13 @@ MARKED("marked", "marked.umd.js", JsModuleType.UMD),
7090
- 文件:`src/main/java/top/howiehz/halo/plugin/extra/api/service/interop/runtime/engine/CustomJavetEngine.java`
7191
- 引擎当前在 `preloadModules()` 中预加载 `Shiki`
7292
- 使用 `JsModule.MARKED.getSourceCode()` 读取资源,使用 `v8Runtime.getExecutor(code).executeVoid()` 执行。
73-
- 加载后,验证预期的函数是否暴露在 `globalThis`(或其他入口点)上。保持预加载对错误的容忍性,避免引擎创建失败。
93+
- 加载后,验证预期的函数是否暴露在 `globalThis` 这类全局入口上。保持预加载对错误的容忍性,避免引擎创建失败。
7494

7595
### 暴露 Java 服务来调用模块
7696

7797
-`service/interop/runtime/adapters/<module>` 下创建服务接口(示例 `service/interop/runtime/adapters/marked/MarkedService.java`),定义您需要的操作。
7898
- 使用 Spring `@Service` 类实现接口,该类使用现有的 `V8EnginePoolService` 对运行时执行调用,类似于 `ShikiHighlightServiceImpl`
79-
- 优先读取 `globalThis` 函数(例如 `parseMarkdown`)或 `globalThis.<lib>.parse`
99+
- 优先读取 `globalThis` 上的方法(例如 `parseMarkdown`)或 `globalThis.<lib>.parse`
80100

81101
### 验证
82102

@@ -85,7 +105,7 @@ MARKED("marked", "marked.umd.js", JsModuleType.UMD),
85105

86106
### 模块类型和自定义解析器
87107

88-
- JsModuleType.UMD:直接执行脚本(UMD 通常附加到 globalThis)。
108+
- JsModuleType.UMD:直接执行脚本(UMD 通常会挂到 `globalThis`)。
89109
- JsModuleType.ESM:`CustomV8ModuleResolver` 编译并为 ESM 模块返回 IV8Module。
90110
- JsModuleType.CJS:CommonJS 模块使用模拟的 `module.exports` 对象执行,导出作为模块对象返回。
91111

@@ -103,5 +123,5 @@ gradlew.bat clean assemble -x test
103123

104124
- 保持嵌入的 JS 文件相对较小,以保持 jar 大小可管理。
105125
- 优先选择精简或压缩的 UMD 构建版本进行嵌入。
106-
- 如果库暴露异步 API(promises),Java 实现应该使用 Javet Promise 助手或 V8ValuePromise 轮询来等待 promise 结果
126+
- 如果库暴露异步接口(Promise),Java 实现应使用 Javet Promise 辅助工具或 `V8ValuePromise` 轮询等待结果
107127
- 添加单元测试,使用模拟或引擎池来验证解析/高亮行为。

README.md

Lines changed: 72 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -50,65 +50,20 @@
5050
- 无需主题适配即可使用的功能:
5151
- [中英文混排格式化处理器](#中英文混排格式化处理器)
5252
- [代码高亮处理器](#代码高亮处理器)(仅全量版可用)
53+
- [HTML 页面压缩处理器](#html-页面压缩处理器)(仅全量版可用)
5354
- 提供给主题开发者使用的 Finder API:
5455
- [插件本体信息相关 API](#插件本体信息相关-api)
5556
- [文章字数统计 API(单篇/全站)](#文章字数统计-api)
5657
- [HTML 内容字数统计 API](#html-内容字数统计-api)
5758
- [中英文混排格式化 API](#中英文混排格式化-api)
5859
- [代码高亮 API](#代码高亮-api)(仅全量版可用)
5960

60-
未来将实现的功能:[TODO](#todo)
61-
6261
欢迎为此插件提 [Issue](https://github.com/HowieHz/halo-plugin-extra-api/issues/new),任何你需要的功能都可以在此处提出,我将在能力范围内尽力实现。
6362

6463
💖 支持本项目: 如果你觉得这个插件有用,点个 [Star](https://github.com/HowieHz/halo-plugin-extra-api) 就是对我最大的鼓励!
6564

6665
感谢所有支持本项目的用户和开发者,也特别感谢 Halo CMS 团队为插件生态提供的优秀平台。
6766

68-
## 文档目录
69-
70-
- [API 扩展包](#api-扩展包)
71-
- [简介](#简介)
72-
- [核心理念](#核心理念)
73-
- [功能介绍](#功能介绍)
74-
- [文档目录](#文档目录)
75-
- [Finder API 文档](#finder-api-文档)
76-
- [文档类型定义](#文档类型定义)
77-
- [插件本体信息相关 API](#插件本体信息相关-api)
78-
- [检测本插件是否启用](#检测本插件是否启用)
79-
- [插件版本检测 API](#插件版本检测-api)
80-
- [统计信息 API](#统计信息-api)
81-
- [文章字数统计 API](#文章字数统计-api)
82-
- [HTML 内容字数统计 API](#html-内容字数统计-api)
83-
- [渲染 API](#渲染-api)
84-
- [中英文混排格式化 API](#中英文混排格式化-api)
85-
- [渲染 API(需要 JS 运行时)](#渲染-api需要-js-运行时)
86-
- [代码高亮 API](#代码高亮-api)
87-
- [处理器文档](#处理器文档)
88-
- [中英文混排格式化处理器](#中英文混排格式化处理器)
89-
- [功能说明](#功能说明)
90-
- [配置选项](#配置选项)
91-
- [补充说明](#补充说明)
92-
- [代码高亮处理器](#代码高亮处理器)
93-
- [特点](#特点)
94-
- [配置选项](#配置选项-1)
95-
- [支持的主题](#支持的主题)
96-
- [补充说明](#补充说明-1)
97-
- [版本说明](#版本说明)
98-
- [轻量版的优势](#轻量版的优势)
99-
- [轻量版本缺少的功能](#轻量版本缺少的功能)
100-
- [全量版使用须知](#全量版使用须知)
101-
- [基本使用要求](#基本使用要求)
102-
- [全量版已知问题](#全量版已知问题)
103-
- [问题一解决方案](#问题一解决方案)
104-
- [下载和安装](#下载和安装)
105-
- [稳定版](#稳定版)
106-
- [开发版](#开发版)
107-
- [下载步骤](#下载步骤)
108-
- [开发指南/贡献指南](#开发指南贡献指南)
109-
- [TODO](#todo)
110-
- [许可证](#许可证)
111-
11267
## Finder API 文档
11368

11469
### 文档类型定义
@@ -202,7 +157,7 @@ extraApiPluginInfoFinder.isJavaScriptAvailable()
202157
- 解释:轻量版时返回 true,全量版时返回 false
203158
- `getVersionType()`
204159
- 类型:`string`
205-
- 解释:返回 "full""lite"
160+
- 解释:返回 `"full"`(全量版)或 `"lite"`(轻量版)
206161
- `isJavaScriptAvailable()`
207162
- 类型:`boolean`
208163
- 解释:JavaScript 功能可用时返回 true
@@ -212,7 +167,7 @@ extraApiPluginInfoFinder.isJavaScriptAvailable()
212167
- 检测原理
213168
- 通过检查 `V8EnginePoolService` 类是否存在来判断版本类型:
214169
- 全量版:包含 JavaScript 运行时,V8EnginePoolService 类存在
215-
- 轻量版:构建时排除 interop 包下所有类,V8EnginePoolService 类不存在
170+
- 轻量版:构建时排除原生运行时相关包(`interop` 包)下所有类,`V8EnginePoolService` 类不存在
216171
- 应用场景
217172
- 主题兼容性:主题可以根据插件版本提供不同的功能体验
218173
- 用户提示:向用户说明当前版本的功能限制
@@ -638,7 +593,7 @@ extraApiJsRenderFinder.highlightCodeInHtml(htmlContent)
638593
此处理器默认启用。开箱即用,无需额外配置。
639594

640595
在“插件设置 - 中英文混排格式化”提供以下配置项:
641-
- 自动渲染: 启用之后会自动处理文章和单页中段落标签的中英文混排格式,在中日韩字符与英文、数字、符号之间自动插入空格。注:Finder API 渲染不受此配置项影响
596+
- 自动渲染:启用后会自动处理文章和单页中段落标签的中英文混排格式,在中日韩字符与英文、数字、符号之间自动插入空格。通过 Finder 接口调用时不受此配置项影响
642597

643598
#### 补充说明
644599

@@ -680,19 +635,19 @@ extraApiJsRenderFinder.highlightCodeInHtml(htmlContent)
680635
#### 配置选项
681636

682637
在“插件设置 - 代码高亮(仅全量版可用)”提供以下配置项:
683-
- 自动渲染: 启用之后会自动渲染文章和单页中的代码块。注:Finder API 渲染不受此配置项影响
684-
- 自定义注入 CSS 样式:启用自动渲染时将在页面 head 注入样式以优化 Shiki 渲染效果默认值提供了边距调整/行号显示/基于媒体查询的明暗切换功能
685-
- 额外注入规则: 指定额外的页面路径规则,支持通配符。
686-
- 默认包含: `/moments/**`, `/docs/**`
638+
- 自动渲染:启用后会自动渲染文章和单页中的代码块。通过 Finder 接口调用时不受此配置项影响
639+
- 自定义注入 CSS 样式:启用自动渲染时,会在页面 `head` 标签中注入样式,以优化 Shiki 渲染效果默认值提供了边距调整行号显示,以及基于媒体查询的明暗切换功能
640+
- 额外注入规则指定额外的页面路径规则,支持通配符。
641+
- 默认包含`/moments/**`, `/docs/**`
687642
- 支持自定义路径,如 `/custom/**`
688643
- 明暗双倍渲染模式切换:
689-
- 单主题模式:
690-
- 主题: 选择单个代码高亮主题
691-
- 双主题模式:
692-
- 亮色主题: 浅色模式使用的主题
693-
- 暗色主题: 深色模式使用的主题
694-
- 亮色主题代码块类名: 浅色代码块的 CSS 类名
695-
- 暗色主题代码块类名: 深色代码块的 CSS 类名
644+
- 单主题模式
645+
- 主题选择单个代码高亮主题
646+
- 双主题模式
647+
- 亮色主题浅色模式使用的主题
648+
- 暗色主题深色模式使用的主题
649+
- 亮色主题代码块类名浅色代码块的 CSS 类名
650+
- 暗色主题代码块类名深色代码块的 CSS 类名
696651

697652
#### 支持的主题
698653

@@ -731,11 +686,66 @@ extraApiJsRenderFinder.highlightCodeInHtml(htmlContent)
731686
- 补充说明:
732687
- 双主题模式会生成两个并列的 div 元素
733688

689+
### HTML 页面压缩处理器
690+
691+
插件提供了自动化的 HTML 页面压缩处理器,无需在模板中手动调用,即可在服务端对前台 HTML 响应进行整体压缩。
692+
693+
此功能通过 [minify-html](https://github.com/wilsonzlin/minify-html) 的 Java JNI 绑定实现,仅在[全量版](#版本说明)中可用。
694+
695+
#### 特点
696+
697+
- 压缩范围:
698+
- 仅处理 `GET` 请求返回的 HTML 页面响应
699+
- 仅处理 `text/html` 且未经过 `gzip/br` 等编码的响应
700+
- 默认跳过后台、API、静态资源等路径,避免误处理非页面响应
701+
- 安全边界:
702+
- 非 UTF-8 响应会自动跳过,避免字符集被破坏
703+
- 已编码响应会自动跳过,避免破坏上游压缩结果
704+
- 压缩失败时自动回退原始 HTML,不影响页面正常返回
705+
- 性能说明:
706+
- 处理器会完整读取并重写 HTML 响应体,因此会带来一定 CPU 与内存开销
707+
- 压缩工作会切换到 Reactor 的弹性线程池(`boundedElastic`),避免阻塞处理请求的线程
708+
- 更适合体积较大、访问量稳定、希望进一步压缩 HTML 传输体积的站点
709+
710+
#### 配置选项
711+
712+
在“插件设置 - HTML 页面压缩(仅全量版可用)”提供以下配置项:
713+
714+
- 自动压缩:启用之后会在服务端对前台 HTML 页面响应做整体压缩。
715+
- 排除路径规则:
716+
- 支持 Ant 风格路径匹配,支持 `*` 和 `**`
717+
- 默认包含:`/console/**`、`/uc/**`、`/login/**`、`/signup/**`、`/logout/**`、`/themes/**`、`/plugins/**`、`/actuator/**`、`/api/**`、`/apis/**`、`/upload/**`
718+
- 常规安全压缩选项:
719+
- 压缩内联 CSS
720+
- 压缩内联 JavaScript
721+
- 保留花括号模板语法
722+
- 保留 `<% %>` 模板语法
723+
- 保留可省略闭合标签
724+
- 保留 HTML 注释
725+
- 保留 `html/head` 起始标签
726+
- 保留 `input[type=text]` 属性
727+
- 保留 SSI 注释
728+
- 移除 Bang 声明
729+
- 移除处理指令
730+
- 更激进的压缩选项(默认关闭),这些选项可能生成无法通过严格 HTML 校验、但仍可被主流浏览器按 WHATWG 解析规范正确渲染的输出:
731+
- 压缩 DOCTYPE
732+
- 允许非规范无引号属性值
733+
- 允许更激进的实体优化
734+
- 允许移除属性间空格
735+
736+
关于配置项的原始文档可参考 [minify_html](https://docs.rs/minify-html/latest/minify_html/struct.Cfg.html)。
737+
738+
#### 补充说明
739+
740+
- 此功能使用 [minify-html](https://github.com/wilsonzlin/minify-html) 库实现
741+
- 处理器只影响页面响应输出,不会修改数据库中的原始内容
742+
- 如果站点已由 CDN、反向代理或上游网关统一处理 HTML 压缩,请避免重复启用
743+
734744
## 版本说明
735745

736746
插件提供两个版本:
737747

738-
- **全量版**:包含所有功能,包括代码高亮等依赖 JS 的相关功能
748+
- **全量版**:包含所有功能,包括代码高亮、HTML 页面压缩等依赖原生运行时的功能
739749
- **轻量版**:轻量级版本,不包含 JS 相关功能和相关依赖。
740750

741751
### 轻量版的优势
@@ -749,6 +759,7 @@ extraApiJsRenderFinder.highlightCodeInHtml(htmlContent)
749759
### 轻量版本缺少的功能
750760

751761
- 代码高亮(Shiki.js 渲染)
762+
- HTML 页面压缩(minify-html JNI)
752763

753764
<!-- - 图表渲染(Mermaid)
754765
- 公式渲染(KaTeX) -->
@@ -912,18 +923,6 @@ ERROR - JavetException: Javet library is not loaded because <null>
912923
913924
参见 [CONTRIBUTING.md](./CONTRIBUTING.md)
914925
915-
## TODO
916-
917-
<details><summary>展开折叠内容</summary>
918-
919-
- [ ] 提供随机文章 API
920-
- [ ] 提供预计阅读时间 API,及相关配置项
921-
- [ ] 提供图表渲染 API
922-
- [ ] 提供公式渲染 API
923-
- [ ] 分离 Node.js 环境支持为可选前置插件(预计 4.0 版本实现)
924-
925-
</details>
926-
927926
## 许可证
928927
929928
[AGPL-3.0](./LICENSE) © HowieHz

build.gradle

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,35 @@ def platforms = [
4343
'Windows-x86_64': ['windows-x86_64']
4444
]
4545

46+
def minifyHtmlNativeLibsByVariant = [
47+
'Linux-arm64' : 'linux-aarch64.nativelib',
48+
'Linux-x86_64' : 'linux-x64.nativelib',
49+
'Macos-arm64' : 'mac-aarch64.nativelib',
50+
'Macos-x86_64' : 'mac-x64.nativelib',
51+
'Windows-x86_64': 'win-x64.nativelib'
52+
]
53+
54+
def runtimeClasspathTrees = { configuration, String selectedMinifyHtmlNativeLib = null ->
55+
configuration.resolvedConfiguration.resolvedArtifacts.collect { artifact ->
56+
def artifactFile = artifact.file
57+
if (artifactFile.isDirectory()) {
58+
return artifactFile
59+
}
60+
def artifactTree = zipTree(artifactFile)
61+
if (artifact.moduleVersion.id.group == 'in.wilsonl.minifyhtml'
62+
&& artifact.name == 'minify-html'
63+
&& selectedMinifyHtmlNativeLib != null) {
64+
return artifactTree.matching {
65+
exclude { details ->
66+
details.path.endsWith('.nativelib')
67+
&& details.path != selectedMinifyHtmlNativeLib
68+
}
69+
}
70+
}
71+
return artifactTree
72+
}
73+
}
74+
4675
// 为每个平台创建专用配置,继承 runtimeClasspath 以保证依赖一致性
4776
platforms.each { variant, platformList ->
4877
configurations.create("javet${variant}") {
@@ -64,6 +93,11 @@ configurations {
6493
javetAllPlatforms {
6594
extendsFrom configurations.runtimeClasspath
6695
}
96+
liteRuntimeClasspath {
97+
extendsFrom configurations.runtimeClasspath
98+
exclude group: 'com.caoccao.javet'
99+
exclude group: 'in.wilsonl.minifyhtml', module: 'minify-html'
100+
}
67101
}
68102

69103
dependencies.add('javetAllPlatforms', 'com.caoccao.javet:javet:5.0.6')
@@ -80,6 +114,7 @@ dependencies {
80114

81115
// JavaScript 引擎 - 为什么选择 Javet:支持 Node.js 模块,性能好,维护活跃
82116
implementation 'com.caoccao.javet:javet:5.0.6'
117+
implementation 'in.wilsonl.minifyhtml:minify-html:0.18.1'
83118

84119
// Pangu - 自动在中日韩字符和英文、数字、符号之间添加空格,提升可读性
85120
implementation 'ws.vinta:pangu:1.1.0'
@@ -245,10 +280,8 @@ afterEvaluate {
245280
exclude 'js/**'
246281
exclude 'extensions/extension-definitions*.yaml' // 变体扩展定义由专门任务生成
247282
}
248-
// 排除 Javet 依赖 - 轻量版不需要 interop 运行时,减少 JAR 大小
249-
from configurations.runtimeClasspath.filter {
250-
!it.name.contains('javet')
251-
}.collect {
283+
// 排除部分依赖 - 轻量版不需要 interop 运行时,减少 JAR 大小
284+
from configurations.liteRuntimeClasspath.collect {
252285
it.isDirectory() ? it : zipTree(it)
253286
}
254287
into('extensions') {
@@ -276,9 +309,7 @@ afterEvaluate {
276309
exclude 'extensions/extension-definitions-core.yaml'
277310
exclude 'extensions/extension-definitions-interop.yaml'
278311
}
279-
from configurations.javetAllPlatforms.collect {
280-
it.isDirectory() ? it : zipTree(it)
281-
}
312+
from(runtimeClasspathTrees(configurations.javetAllPlatforms))
282313
into('extensions') {
283314
from generateFullExtensionDefinitionsTask
284315
rename { 'extension-definitions.yaml' }
@@ -299,6 +330,7 @@ afterEvaluate {
299330
def configName = "javet${variant}"
300331
def archiveName = "extra-api-full-${variant.toLowerCase()}-${project.version}.jar"
301332
def indexTaskName = "generatePluginComponentsIdx${taskName.capitalize()}"
333+
def minifyHtmlNativeLib = minifyHtmlNativeLibsByVariant[variant]
302334

303335
tasks.register(taskName, Jar) {
304336
group = 'build'
@@ -309,9 +341,7 @@ afterEvaluate {
309341
exclude 'extensions/extension-definitions-core.yaml'
310342
exclude 'extensions/extension-definitions-interop.yaml'
311343
}
312-
from configurations.getByName(configName).collect {
313-
it.isDirectory() ? it : zipTree(it)
314-
}
344+
from(runtimeClasspathTrees(configurations.getByName(configName), minifyHtmlNativeLib))
315345
into('extensions') {
316346
from generateFullExtensionDefinitionsTask
317347
rename { 'extension-definitions.yaml' }

0 commit comments

Comments
 (0)