@xingwangzhe/stalux 1.25.6 → 1.25.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.25.6",
3
+ "version": "1.25.8",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -78,6 +78,7 @@
78
78
  "@astrojs/markdown-satteri": "0.3.7",
79
79
  "@astrojs/rss": "^4.0.19",
80
80
  "@astrojs/sitemap": "^3.7.3",
81
+ "@echogarden/icu-segmentation-wasm": "^0.4.0",
81
82
  "@echogarden/text-segmentation": "^0.7.0",
82
83
  "@expressive-code/plugin-line-numbers": "^0.44.1",
83
84
  "@lucide/astro": "^1.33.0",
@@ -6,10 +6,9 @@ import jsTokens from "js-tokens";
6
6
  * 只从 Sätteri 的 MDAST/HAST 节点读取信息,并通过 ctx 注入最终结果。
7
7
  *
8
8
  * 字数统计策略:
9
- * - 只数 heading 和 paragraph 的内容(用 ctx.textContent 获取纯文本)。
10
- * - 独立的 code 块、math/displayMath 公式不参与计数
11
- * (代码是"看"不是"读",符号表达式不按词计)。
12
- * - 行内 code 和行内 math 会被 textContent(paragraph) 自然包含。
9
+ * - 统计 paragraph、heading 和 tableCell,覆盖列表、引用、表格等正文。
10
+ * - 独立的 code 块、math/displayMath 公式不参与正文词数统计。
11
+ * - 行内 code 参与正文统计,inlineMath 单独计入公式阅读成本。
13
12
  *
14
13
  * 特性标记:
15
14
  * - HAST 阶段 <img> 检测 → hasImage
@@ -20,6 +19,9 @@ import { defineMdastPlugin, defineHastPlugin } from "satteri";
20
19
 
21
20
  const WORDS_PER_MINUTE = 400;
22
21
  const CODE_SECONDS_PER_NON_EMPTY_LINE = 2;
22
+ const INLINE_MATH_BASE_SECONDS = 1;
23
+ const DISPLAY_MATH_BASE_SECONDS = 4;
24
+ const MATH_SECONDS_PER_NON_WHITESPACE_CHARACTER = 0.03;
23
25
 
24
26
  type FeatureFlagsState = {
25
27
  proseText: string;
@@ -27,6 +29,10 @@ type FeatureFlagsState = {
27
29
  codeCharacters: number;
28
30
  codeNonEmptyLines: number;
29
31
  codeBlocks: number;
32
+ inlineMathBlocks: number;
33
+ displayMathBlocks: number;
34
+ mathCharacters: number;
35
+ mathNonWhitespaceCharacters: number;
30
36
  hasImage: boolean;
31
37
  };
32
38
 
@@ -38,6 +44,10 @@ function getState(ctx: any): FeatureFlagsState {
38
44
  codeCharacters: 0,
39
45
  codeNonEmptyLines: 0,
40
46
  codeBlocks: 0,
47
+ inlineMathBlocks: 0,
48
+ displayMathBlocks: 0,
49
+ mathCharacters: 0,
50
+ mathNonWhitespaceCharacters: 0,
41
51
  hasImage: false,
42
52
  }) as FeatureFlagsState;
43
53
  }
@@ -50,6 +60,10 @@ function injectState(ctx: any) {
50
60
  injected.codeCharacters = state.codeCharacters;
51
61
  injected.codeNonEmptyLines = state.codeNonEmptyLines;
52
62
  injected.codeBlocks = state.codeBlocks;
63
+ injected.inlineMathBlocks = state.inlineMathBlocks;
64
+ injected.displayMathBlocks = state.displayMathBlocks;
65
+ injected.mathCharacters = state.mathCharacters;
66
+ injected.mathNonWhitespaceCharacters = state.mathNonWhitespaceCharacters;
53
67
  injected.hasImage = state.hasImage;
54
68
  }
55
69
 
@@ -69,6 +83,26 @@ function countCodeTokens(value: string, lang?: string): number {
69
83
  return value.match(/\p{L}[\p{L}\p{N}_]*|\p{N}+(?:\.\p{N}+)?|[^\s]/gu)?.length ?? 0;
70
84
  }
71
85
 
86
+ function countMath(node: any, ctx: any, display: boolean) {
87
+ const state = getState(ctx);
88
+ const value = String(node.value ?? "");
89
+ if (display) state.displayMathBlocks++;
90
+ else state.inlineMathBlocks++;
91
+ state.mathCharacters += [...value].length;
92
+ state.mathNonWhitespaceCharacters += (value.match(/\S/gu) ?? []).length;
93
+ injectState(ctx);
94
+ }
95
+
96
+ function collectProseNodeText(node: any): string {
97
+ if (node.type === "inlineMath" || node.type === "math" || node.type === "code") {
98
+ return "";
99
+ }
100
+ if (node.type === "text" || node.type === "inlineCode") {
101
+ return node.value ?? "";
102
+ }
103
+ return Array.isArray(node.children) ? node.children.map(collectProseNodeText).join(" ") : "";
104
+ }
105
+
72
106
  export const featureFlagsMdast = defineMdastPlugin({
73
107
  name: "feature-flags-mdast",
74
108
 
@@ -83,13 +117,26 @@ export const featureFlagsMdast = defineMdastPlugin({
83
117
  injectState(ctx);
84
118
  },
85
119
 
120
+ math(node, ctx) {
121
+ countMath(node, ctx, true);
122
+ },
123
+
124
+ inlineMath(node, ctx) {
125
+ countMath(node, ctx, false);
126
+ },
127
+
86
128
  heading(node, ctx) {
87
- getState(ctx).proseText += ` ${ctx.textContent(node)}`;
129
+ getState(ctx).proseText += ` ${collectProseNodeText(node)}`;
88
130
  injectState(ctx);
89
131
  },
90
132
 
91
133
  paragraph(node, ctx) {
92
- getState(ctx).proseText += ` ${ctx.textContent(node)}`;
134
+ getState(ctx).proseText += ` ${collectProseNodeText(node)}`;
135
+ injectState(ctx);
136
+ },
137
+
138
+ tableCell(node, ctx) {
139
+ getState(ctx).proseText += ` ${collectProseNodeText(node)}`;
93
140
  injectState(ctx);
94
141
  },
95
142
  });
@@ -135,6 +182,7 @@ export async function analyzeFeatureFlags(body: string | undefined): Promise<{
135
182
  const processor = await createSatteriMarkdownProcessor({
136
183
  mdastPlugins: [featureFlagsMdast],
137
184
  hastPlugins: [featureFlagsHast],
185
+ features: { math: true },
138
186
  });
139
187
  const result = await processor.render(body ?? "");
140
188
  const metadata = result.metadata.frontmatter as Record<string, unknown>;
@@ -142,11 +190,18 @@ export async function analyzeFeatureFlags(body: string | undefined): Promise<{
142
190
  await splitToWords(String(metadata.proseText ?? ""))
143
191
  ).nonPunctuationEntries.filter((entry) => /\S/u.test(entry.text)).length;
144
192
  const codeNonEmptyLines = Number(metadata.codeNonEmptyLines ?? 0);
193
+ const inlineMathBlocks = Number(metadata.inlineMathBlocks ?? 0);
194
+ const displayMathBlocks = Number(metadata.displayMathBlocks ?? 0);
195
+ const mathNonWhitespaceCharacters = Number(metadata.mathNonWhitespaceCharacters ?? 0);
145
196
  return {
146
197
  hasImage: metadata.hasImage === true,
147
198
  readingMinutes: Math.ceil(
148
199
  proseWords / WORDS_PER_MINUTE +
149
- (codeNonEmptyLines * CODE_SECONDS_PER_NON_EMPTY_LINE) / 60,
200
+ (codeNonEmptyLines * CODE_SECONDS_PER_NON_EMPTY_LINE +
201
+ inlineMathBlocks * INLINE_MATH_BASE_SECONDS +
202
+ displayMathBlocks * DISPLAY_MATH_BASE_SECONDS +
203
+ mathNonWhitespaceCharacters * MATH_SECONDS_PER_NON_WHITESPACE_CHARACTER) /
204
+ 60,
150
205
  ),
151
206
  wordCount: proseWords,
152
207
  };