@xingwangzhe/stalux 1.25.7 → 1.25.9
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 +1 -1
- package/src/plugins/feature-flags.ts +85 -15
package/package.json
CHANGED
|
@@ -6,10 +6,9 @@ import jsTokens from "js-tokens";
|
|
|
6
6
|
* 只从 Sätteri 的 MDAST/HAST 节点读取信息,并通过 ctx 注入最终结果。
|
|
7
7
|
*
|
|
8
8
|
* 字数统计策略:
|
|
9
|
-
* -
|
|
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,18 +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 += ` ${
|
|
129
|
+
getState(ctx).proseText += ` ${collectProseNodeText(node)}`;
|
|
88
130
|
injectState(ctx);
|
|
89
131
|
},
|
|
90
132
|
|
|
91
133
|
paragraph(node, ctx) {
|
|
92
|
-
getState(ctx).proseText += ` ${
|
|
134
|
+
getState(ctx).proseText += ` ${collectProseNodeText(node)}`;
|
|
93
135
|
injectState(ctx);
|
|
94
136
|
},
|
|
95
137
|
|
|
96
138
|
tableCell(node, ctx) {
|
|
97
|
-
getState(ctx).proseText += ` ${
|
|
139
|
+
getState(ctx).proseText += ` ${collectProseNodeText(node)}`;
|
|
98
140
|
injectState(ctx);
|
|
99
141
|
},
|
|
100
142
|
});
|
|
@@ -128,30 +170,58 @@ export const featureFlagsHast = defineHastPlugin({
|
|
|
128
170
|
},
|
|
129
171
|
});
|
|
130
172
|
|
|
131
|
-
|
|
132
|
-
* 直接用 Sätteri AST 分析一篇文章,供文章页、全局统计和 API 复用。
|
|
133
|
-
* 这里不复用内容集合的 data,也不读取 Astro 的渲染 metadata。
|
|
134
|
-
*/
|
|
135
|
-
export async function analyzeFeatureFlags(body: string | undefined): Promise<{
|
|
173
|
+
type FeatureFlagsResult = {
|
|
136
174
|
hasImage: boolean;
|
|
137
175
|
readingMinutes: number;
|
|
138
176
|
wordCount: number;
|
|
139
|
-
}
|
|
140
|
-
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let processorPromise: ReturnType<typeof createSatteriMarkdownProcessor> | undefined;
|
|
180
|
+
const analysisCache = new Map<string, Promise<FeatureFlagsResult>>();
|
|
181
|
+
|
|
182
|
+
function getFeatureFlagsProcessor() {
|
|
183
|
+
processorPromise ??= createSatteriMarkdownProcessor({
|
|
141
184
|
mdastPlugins: [featureFlagsMdast],
|
|
142
185
|
hastPlugins: [featureFlagsHast],
|
|
186
|
+
features: { math: true },
|
|
143
187
|
});
|
|
188
|
+
return processorPromise;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 直接用 Sätteri AST 分析一篇文章,供文章页、全局统计和 API 复用。
|
|
193
|
+
* 这里不复用内容集合的 data,也不读取 Astro 的渲染 metadata。
|
|
194
|
+
*/
|
|
195
|
+
export function analyzeFeatureFlags(body: string | undefined): Promise<FeatureFlagsResult> {
|
|
196
|
+
const content = body ?? "";
|
|
197
|
+
const cached = analysisCache.get(content);
|
|
198
|
+
if (cached) return cached;
|
|
199
|
+
|
|
200
|
+
const resultPromise = analyzeFeatureFlagsUncached(content);
|
|
201
|
+
analysisCache.set(content, resultPromise);
|
|
202
|
+
return resultPromise;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function analyzeFeatureFlagsUncached(body: string): Promise<FeatureFlagsResult> {
|
|
206
|
+
const processor = await getFeatureFlagsProcessor();
|
|
144
207
|
const result = await processor.render(body ?? "");
|
|
145
208
|
const metadata = result.metadata.frontmatter as Record<string, unknown>;
|
|
146
209
|
const proseWords = (
|
|
147
210
|
await splitToWords(String(metadata.proseText ?? ""))
|
|
148
211
|
).nonPunctuationEntries.filter((entry) => /\S/u.test(entry.text)).length;
|
|
149
212
|
const codeNonEmptyLines = Number(metadata.codeNonEmptyLines ?? 0);
|
|
213
|
+
const inlineMathBlocks = Number(metadata.inlineMathBlocks ?? 0);
|
|
214
|
+
const displayMathBlocks = Number(metadata.displayMathBlocks ?? 0);
|
|
215
|
+
const mathNonWhitespaceCharacters = Number(metadata.mathNonWhitespaceCharacters ?? 0);
|
|
150
216
|
return {
|
|
151
217
|
hasImage: metadata.hasImage === true,
|
|
152
218
|
readingMinutes: Math.ceil(
|
|
153
219
|
proseWords / WORDS_PER_MINUTE +
|
|
154
|
-
(codeNonEmptyLines * CODE_SECONDS_PER_NON_EMPTY_LINE
|
|
220
|
+
(codeNonEmptyLines * CODE_SECONDS_PER_NON_EMPTY_LINE +
|
|
221
|
+
inlineMathBlocks * INLINE_MATH_BASE_SECONDS +
|
|
222
|
+
displayMathBlocks * DISPLAY_MATH_BASE_SECONDS +
|
|
223
|
+
mathNonWhitespaceCharacters * MATH_SECONDS_PER_NON_WHITESPACE_CHARACTER) /
|
|
224
|
+
60,
|
|
155
225
|
),
|
|
156
226
|
wordCount: proseWords,
|
|
157
227
|
};
|