Skip to content

Commit 6f12bf4

Browse files
committed
feat: Add HTML content word count API
1 parent 5741b12 commit 6f12bf4

3 files changed

Lines changed: 219 additions & 52 deletions

File tree

README.md

Lines changed: 167 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
- [代码高亮处理器](#代码高亮处理器)(仅全量版可用)
4646
- 需要主题适配的 Finder API:
4747
- [文章字数统计 API](#文章字数统计-api)
48+
- [HTML 内容字数统计 API](#html-内容字数统计-api)
4849
- [代码高亮 API](#代码高亮-api)(仅全量版可用)
4950

5051
未来将实现的功能:[TODO](#todo)
@@ -199,38 +200,40 @@ ERROR - JavetException: Javet library is not loaded because <null>
199200
## 文档目录
200201

201202
- [halo-plugin-extra-api](#halo-plugin-extra-api)
202-
- [简介](#简介)
203-
- [核心理念](#核心理念)
204-
- [功能介绍](#功能介绍)
205-
- [版本说明](#版本说明)
206-
- [轻量版的优势](#轻量版的优势)
207-
- [轻量版本缺少的功能](#轻量版本缺少的功能)
208-
- [全量版使用须知](#全量版使用须知)
209-
- [基本使用要求](#基本使用要求)
210-
- [全量版已知问题](#全量版已知问题)
211-
- [问题一解决方案](#问题一解决方案)
212-
- [TODO](#todo)
213-
- [文档目录](#文档目录)
214-
- [处理器文档](#处理器文档)
215-
- [代码高亮处理器](#代码高亮处理器)
216-
- [特点](#特点)
217-
- [配置选项](#配置选项)
218-
- [支持的主题](#支持的主题)
219-
- [补充说明](#补充说明)
220-
- [Finder API 文档](#finder-api-文档)
221-
- [插件本体信息相关 API](#插件本体信息相关-api)
222-
- [检测本插件是否启用](#检测本插件是否启用)
223-
- [插件版本检测 API](#插件版本检测-api)
224-
- [统计信息 API](#统计信息-api)
225-
- [文章字数统计 API](#文章字数统计-api)
226-
- [渲染 API](#渲染-api)
227-
- [代码高亮 API](#代码高亮-api)
228-
- [下载和安装](#下载和安装)
229-
- [稳定版](#稳定版)
230-
- [开发版](#开发版)
231-
- [下载步骤](#下载步骤)
232-
- [开发指南/贡献指南](#开发指南贡献指南)
233-
- [许可证](#许可证)
203+
- [简介](#简介)
204+
- [核心理念](#核心理念)
205+
- [功能介绍](#功能介绍)
206+
- [版本说明](#版本说明)
207+
- [轻量版的优势](#轻量版的优势)
208+
- [轻量版本缺少的功能](#轻量版本缺少的功能)
209+
- [全量版使用须知](#全量版使用须知)
210+
- [基本使用要求](#基本使用要求)
211+
- [全量版已知问题](#全量版已知问题)
212+
- [问题一解决方案](#问题一解决方案)
213+
- [TODO](#todo)
214+
- [文档目录](#文档目录)
215+
- [处理器文档](#处理器文档)
216+
- [代码高亮处理器](#代码高亮处理器)
217+
- [特点](#特点)
218+
- [配置选项](#配置选项)
219+
- [支持的主题](#支持的主题)
220+
- [补充说明](#补充说明)
221+
- [Finder API 文档](#finder-api-文档)
222+
- [文档类型定义](#文档类型定义)
223+
- [插件本体信息相关 API](#插件本体信息相关-api)
224+
- [检测本插件是否启用](#检测本插件是否启用)
225+
- [插件版本检测 API](#插件版本检测-api)
226+
- [统计信息 API](#统计信息-api)
227+
- [文章字数统计 API](#文章字数统计-api)
228+
- [HTML 内容字数统计 API](#html-内容字数统计-api)
229+
- [渲染 API](#渲染-api)
230+
- [代码高亮 API](#代码高亮-api)
231+
- [下载和安装](#下载和安装)
232+
- [稳定版](#稳定版)
233+
- [开发版](#开发版)
234+
- [下载步骤](#下载步骤)
235+
- [开发指南/贡献指南](#开发指南贡献指南)
236+
- [许可证](#许可证)
234237

235238
## 处理器文档
236239

@@ -319,6 +322,13 @@ ERROR - JavetException: Javet library is not loaded because <null>
319322

320323
## Finder API 文档
321324

325+
### 文档类型定义
326+
327+
- `string`: 字符串类型
328+
- `int`:整数类型(实现为 BigInteger 即无限精度整数)
329+
- `boolean`:布尔类型(true/false)
330+
- `map`:映射类型(键值对)
331+
322332
### 插件本体信息相关 API
323333

324334
#### 检测本插件是否启用
@@ -330,15 +340,18 @@ ERROR - JavetException: Javet library is not loaded because <null>
330340

331341
**参数**
332342

333-
- `extra-api` - 本插件的标识符(`metadata.name`)
343+
- `extra-api`
344+
- 解释:本插件的标识符(`metadata.name`)
334345

335346
**返回值**
336347

337-
- `boolean` - 插件可用时返回 true,否则返回 false
348+
- 类型:`boolean`
349+
- 解释:插件可用时返回 true,否则返回 false
338350

339351
**说明**
340352

341-
使用 `pluginFinder.available('extra-api')` 可以优雅地处理插件依赖,避免在插件未安装时出现模板错误,提升主题的兼容性和用户体验。
353+
使用 `pluginFinder.available('extra-api')`
354+
可以优雅地处理插件依赖,避免在插件未安装时出现模板错误,提升主题的兼容性和用户体验。
342355
注:在此基础上可以使用 `pluginFinder.available('extra-api', '2.*')` 锁定大版本号,避免 API 破坏性更新时导致主题渲染报错。
343356

344357
**示例**
@@ -392,12 +405,20 @@ extraApiPluginInfoFinder.isJavaScriptAvailable()
392405

393406
**返回值**
394407

395-
- `isFullVersion()``boolean` - 全量版时返回 true,轻量版时返回 false
396-
- `isLiteVersion()``boolean` - 轻量版时返回 true,全量版时返回 false
397-
- `getVersionType()``string` - 返回 "full" 或 "lite"
398-
- `isJavaScriptAvailable()``boolean` - JavaScript 功能可用时返回 true
399-
400-
**描述**
408+
- `isFullVersion()`
409+
- 类型:`boolean`
410+
- 解释:全量版时返回 true,轻量版时返回 false
411+
- `isLiteVersion()`
412+
- 类型:`boolean`
413+
- 解释:轻量版时返回 true,全量版时返回 false
414+
- `getVersionType()`
415+
- 类型:`string`
416+
- 解释:返回 "full" 或 "lite"
417+
- `isJavaScriptAvailable()`
418+
- 类型:`boolean`
419+
- 解释:JavaScript 功能可用时返回 true
420+
421+
**补充说明**
401422

402423
- 检测原理
403424
- 通过检查 `V8EnginePoolService` 类是否存在来判断版本类型:
@@ -407,6 +428,9 @@ extraApiPluginInfoFinder.isJavaScriptAvailable()
407428
- 主题兼容性:主题可以根据插件版本提供不同的功能体验
408429
- 用户提示:向用户说明当前版本的功能限制
409430
- 条件渲染:仅在支持的版本中启用特定功能(主题请求不存在的 Finder API 会报错并阻止页面渲染)
431+
- 性能说明
432+
- 这些方法单次调用开销极小,适合在模板中频繁使用
433+
- 首次调用性能开销比后续调用稍高,但仍在毫秒级别(首次通过反射检查,后续直接读取缓存)
410434

411435
**使用示例**
412436

@@ -441,31 +465,42 @@ extraApiPluginInfoFinder.isJavaScriptAvailable()
441465

442466
#### 文章字数统计 API
443467

468+
**描述**
469+
470+
提供文章字数统计功能,支持统计单篇文章或全站文章的字数总和。可选择统计已发布版本或最新版本(含草稿)。适用于显示文章字数、阅读时间估算等场景。
471+
472+
**API 方法**
473+
444474
```javascript
475+
// 传入映射形式参数
445476
extraApiStatsFinder.getPostWordCount({
446477
name: 'post-metadata-name', // 可选,未传入则统计全部文章字数总和
447478
version: 'release' | 'draft' // 可选,默认 'release'
448479
});
449480
```
450481

451482
```javascript
483+
// 统计全部文章已发布版本的总字数
452484
extraApiStatsFinder.getPostWordCount();
453485
```
454486

455487
**参数**
456488

457-
- `name:string` – 文章 `metadata.name`(可选,不传则统计全站)
458-
- `version:string` – 统计版本,可选 `release`(默认)或 `draft`
489+
- 映射形式参数:
490+
- `name`
491+
- 类型:`string`
492+
- 解释:文章的 `metadata.name`,可选参数。未传入时统计全部文章字数总和。
493+
- `version`
494+
- 类型:`string`
495+
- 解释:统计版本,可选参数。`release`(未传入时默认值)或 `draft`
459496

460497
**返回值**
461498

462-
- `int` – 字数统计结果(非负),不存在或参数缺失时返回 0
499+
- 类型:`int`
500+
- 解释:字数统计结果(非负),不存在或参数缺失时返回 0
463501

464-
**描述**
502+
**补充说明**
465503

466-
- 参数说明:
467-
- `name`:文章的 `metadata.name`,可选参数。未传入时统计全部文章字数总和。
468-
- `version`:统计版本,可选 `release`(默认)或 `draft`
469504
- 计数规则:
470505
- 中文、日文、韩文等 CJK 字符按每个字符计 1。
471506
- ASCII 连续字母/数字按 1 个单词计数。
@@ -494,6 +529,85 @@ extraApiStatsFinder.getPostWordCount();
494529
<span th:text="${extraApiStatsFinder.getPostWordCount({version: 'draft'})}"></span>
495530
```
496531

532+
#### HTML 内容字数统计 API
533+
534+
**描述**
535+
536+
提供 HTML 内容字数统计功能,支持统计任意 HTML 字符串的字数。适用于统计文章内容、瞬间内容或自定义 HTML 片段的字数。
537+
538+
**API 方法**
539+
540+
```javascript
541+
// 传入映射形式参数
542+
extraApiStatsFinder.getContentWordCount({
543+
htmlContent: '<p>HTML 内容</p>' // 必需,要统计字数的 HTML 内容
544+
});
545+
```
546+
547+
**参数**
548+
549+
- 映射形式参数:
550+
- `htmlContent`
551+
- 类型:`string`
552+
- 解释:HTML 内容字符串(必需)
553+
554+
**返回值**
555+
556+
- 类型:`int`
557+
- 解释:字数统计结果(非负),输入为空时返回 0
558+
559+
**API 方法**
560+
561+
```javascript
562+
// 直接传入 HTML 内容字符串进行统计
563+
extraApiStatsFinder.getContentWordCount(htmlContent);
564+
```
565+
566+
**参数**
567+
568+
- `htmlContent`
569+
- 类型:`string`
570+
- 解释:HTML 内容字符串(必需)。即要统计字数的 HTML 内容,支持标准 HTML 格式。
571+
572+
**返回值**
573+
574+
- 类型:`int`
575+
- 解释:字数统计结果(非负),输入为空时返回 0
576+
577+
**描述**
578+
579+
- 计数规则:
580+
- 自动移除 HTML 标签(包括 `<script>``<style>` 标签)
581+
- 中文、日文、韩文等 CJK 字符按每个字符计 1
582+
- ASCII 连续字母/数字按 1 个单词计数
583+
- 标点符号和空格不计入统计
584+
- 错误处理:
585+
- 输入为空或 null 时返回 0,不会抛出异常
586+
- 性能说明:
587+
- 单次调用开销较小,适合在模板中直接使用
588+
- 纯计算操作,不涉及缓存和数据库查询
589+
590+
**使用示例**
591+
592+
```html
593+
<!--/* 统计任意 HTML 内容的字数 */-->
594+
<!--/* 传入映射形式参数的写法 */-->
595+
<span th:text="${extraApiStatsFinder.getContentWordCount({htmlContent: '<p>这是一段测试文本</p>'})}"></span>
596+
<!--/* 传入 HTML 内容字符串的写法 */-->
597+
<span th:text="${extraApiStatsFinder.getContentWordCount('<p>这是一段测试文本</p>')}"></span>
598+
599+
<!--/* 统计文章内容的字数,下面这段代码可直接用于 /templates/post.html */-->
600+
<span th:text="${extraApiStatsFinder.getContentWordCount(post.content?.content)}"></span>
601+
602+
<!--/* 统计瞬间内容的字数,下面这段代码可直接用于 /templates/moment.html */-->
603+
<span th:text="${extraApiStatsFinder.getContentWordCount(moment.spec.content?.html)}"></span>
604+
605+
<!--/* 在变量中使用 */-->
606+
<div th:with="wordCount=${extraApiStatsFinder.getContentWordCount(customContent)}">
607+
<span>字数:[[${wordCount}]]</span>
608+
</div>
609+
```
610+
497611
### 渲染 API
498612

499613
**Finder 名称:** `extraApiRenderFinder`
@@ -506,16 +620,17 @@ extraApiRenderFinder.renderCodeHtml(htmlContent)
506620

507621
**参数**
508622

509-
- `htmlContent:string` – HTML 内容
623+
- `htmlContent`
624+
- 类型:`string`
625+
- 解释:包含代码块的 HTML 内容,通常是文章或页面的内容字段。
510626

511627
**返回值**
512628

513-
- `string` – 渲染后的 HTML 内容,渲染样式已内联,渲染失败时返回原始内容
629+
- 类型:`string`
630+
- 解释:渲染后的 HTML 内容,渲染样式已内联,渲染失败时返回原始内容
514631

515632
**描述**
516633

517-
- 参数说明:
518-
- `htmlContent`:包含代码块的 HTML 内容,通常是文章或页面的内容字段。
519634
- 功能特性:
520635
-[处理器文档 - 代码高亮(通过 Shiki.js 渲染)](#代码高亮通过-shikijs-渲染) 中描述的功能特性一致。也受同样的配置项影响。
521636

src/main/java/top/howiehz/halo/plugin/extra/api/finder/basic/ExtraApiStatsFinder.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,30 @@ public interface ExtraApiStatsFinder {
3535
default Mono<BigInteger> getPostWordCount() {
3636
return getPostWordCount(Collections.emptyMap());
3737
}
38+
39+
/**
40+
* Count words in the provided HTML content.
41+
* 统计提供的 HTML 内容的字数。
42+
* <p>
43+
* This method accepts raw HTML content and returns the word count.
44+
* CJK characters are counted individually, ASCII letters/digits are grouped as words.
45+
* 此方法接收原始 HTML 内容并返回字数统计。
46+
* 中日韩字符单独计数,ASCII 字母/数字按单词分组计数。
47+
* </p>
48+
*
49+
* @param params parameter map containing 'htmlContent' key / 包含 'htmlContent' 键的参数映射
50+
* @return word count as Mono (non-negative) / 返回字数(非负)的 Mono
51+
*/
52+
Mono<BigInteger> getContentWordCount(java.util.Map<String, Object> params);
53+
54+
/**
55+
* Count words in the provided HTML content.
56+
* 统计提供的 HTML 内容的字数。
57+
*
58+
* @param htmlContent the HTML content to count / 要统计的 HTML 内容
59+
* @return word count as Mono (non-negative) / 返回字数(非负)的 Mono
60+
*/
61+
default Mono<BigInteger> getContentWordCount(String htmlContent) {
62+
return getContentWordCount(Collections.singletonMap("htmlContent", htmlContent));
63+
}
3864
}

src/main/java/top/howiehz/halo/plugin/extra/api/finder/basic/impl/ExtraApiStatsFinderImpl.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import run.halo.app.theme.finders.Finder;
99
import top.howiehz.halo.plugin.extra.api.finder.basic.ExtraApiStatsFinder;
1010
import top.howiehz.halo.plugin.extra.api.service.basic.post.stats.PostWordCountService;
11+
import top.howiehz.halo.plugin.extra.api.service.basic.post.stats.utils.PostWordCountUtil;
1112

1213
/**
1314
* Implementation of ExtraApiStatsFinder.
@@ -44,4 +45,29 @@ public Mono<BigInteger> getPostWordCount(Map<String, Object> params) {
4445

4546
return postWordCountService.getPostWordCount(postName, isDraft);
4647
}
48+
49+
/**
50+
* Count words in the provided HTML content.
51+
* 统计提供的 HTML 内容的字数。
52+
*
53+
* @param params parameter map containing 'htmlContent' key / 包含 'htmlContent' 键的参数映射
54+
* @return word count as Mono (non-negative) / 返回字数(非负)的 Mono
55+
*/
56+
@Override
57+
public Mono<BigInteger> getContentWordCount(Map<String, Object> params) {
58+
Map<String, Object> map = params == null ? java.util.Collections.emptyMap() : params;
59+
Object htmlContentObj = map.get("htmlContent");
60+
61+
if (htmlContentObj == null) {
62+
return Mono.just(BigInteger.ZERO);
63+
}
64+
65+
String htmlContent = String.valueOf(htmlContentObj);
66+
if ("null".equals(htmlContent) || htmlContent.isBlank()) {
67+
return Mono.just(BigInteger.ZERO);
68+
}
69+
70+
return Mono.fromCallable(() -> PostWordCountUtil.countHTMLWords(htmlContent))
71+
.onErrorReturn(BigInteger.ZERO);
72+
}
4773
}

0 commit comments

Comments
 (0)