Skip to content

Commit 04a5b47

Browse files
committed
fix: count reading units and share Vite compiler
1 parent d3e03b4 commit 04a5b47

5 files changed

Lines changed: 97 additions & 6 deletions

File tree

native/src/document.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub struct PreparedMdx {
2929
pub code_blocks: Vec<CodeBlock>,
3030
pub dependencies: Vec<PathBuf>,
3131
pub diagnostics: Vec<Diagnostic>,
32+
pub reading_words: usize,
3233
pub(crate) tree: mdxjs::hast::Node,
3334
}
3435

@@ -94,6 +95,7 @@ pub fn prepare_mdx(
9495
let options = mdx_options(file, config);
9596
let mdast = mdxjs::mdast_util_from_mdx(body, &options)
9697
.map_err(|error| vec![mdx_diagnostic(file, error)])?;
98+
let reading_words = reading_units(&mdast);
9799
let mut code_blocks = Vec::new();
98100
if highlight {
99101
collect_code_blocks(&mdast, document_id, &mut code_blocks);
@@ -108,10 +110,47 @@ pub fn prepare_mdx(
108110
code_blocks,
109111
dependencies: media.dependencies,
110112
diagnostics: media.diagnostics,
113+
reading_words,
111114
tree,
112115
})
113116
}
114117

118+
fn reading_units(node: &markdown::mdast::Node) -> usize {
119+
match node {
120+
markdown::mdast::Node::Code(_) | markdown::mdast::Node::InlineCode(_) => 0,
121+
markdown::mdast::Node::Text(text) => count_reading_units(&text.value),
122+
_ => node
123+
.children()
124+
.map(|children| children.iter().map(reading_units).sum())
125+
.unwrap_or_default(),
126+
}
127+
}
128+
129+
fn count_reading_units(text: &str) -> usize {
130+
let mut count = 0;
131+
let mut in_english_word = false;
132+
for character in text.chars() {
133+
if matches!(
134+
character,
135+
'\u{4e00}'..='\u{9fff}'
136+
| '\u{3040}'..='\u{309f}'
137+
| '\u{30a0}'..='\u{30ff}'
138+
| '\u{ac00}'..='\u{d7af}'
139+
) {
140+
count += 1;
141+
in_english_word = false;
142+
} else if character.is_ascii_alphabetic() {
143+
if !in_english_word {
144+
count += 1;
145+
in_english_word = true;
146+
}
147+
} else {
148+
in_english_word = false;
149+
}
150+
}
151+
count
152+
}
153+
115154
pub fn finish_mdx(
116155
prepared: PreparedMdx,
117156
file: &str,

native/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,12 +348,12 @@ pub fn prepare_batch(config_json: String, inputs_json: String) -> Result<Prepare
348348
.map_err(diagnostic_error)?;
349349
let mut derived = serde_json::Map::new();
350350
if config.derived.reading_time {
351-
let words = parsed.body.split_whitespace().count();
351+
let words = prepared.reading_words;
352352
derived.insert(
353353
"readingTime".into(),
354354
serde_json::json!({
355355
"words": words,
356-
"minutes": words.div_ceil(200).max(1),
356+
"minutes": words.div_ceil(300).max(1),
357357
}),
358358
);
359359
}

src/__tests__/native.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,35 @@ test('maps schema failures to structured diagnostics', () => {
7272
)
7373
})
7474

75+
test('counts CJK text but excludes code from reading time', () => {
76+
const readingConfig = normalizeConfig({
77+
cache: false,
78+
highlight: false,
79+
root: '/project',
80+
collections: {
81+
posts: {
82+
directory: 'content/posts',
83+
schema: {
84+
type: 'object',
85+
properties: { title: { type: 'string' } },
86+
required: ['title'],
87+
},
88+
},
89+
},
90+
derived: { readingTime: true },
91+
})
92+
const batch = prepareNativeBatch(readingConfig, [
93+
{
94+
collection: 'posts',
95+
file: '/project/content/posts/cjk.mdx',
96+
key: 'cjk',
97+
source: `---\ntitle: CJK\n---\n${'字'.repeat(201)}\n\n\`\`\`text\n${'字'.repeat(500)}\n\`\`\`\n`,
98+
},
99+
])
100+
101+
assert.deepEqual(batch.finish([])[0]?.derived.readingTime, { minutes: 1, words: 201 })
102+
})
103+
75104
test('injects real Shiki HAST into the compiled module', async () => {
76105
const batch = prepareNativeBatch(config, [
77106
{

src/__tests__/vite.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module'
33
import { readFile, stat, unlink, writeFile } from 'node:fs/promises'
44
import path from 'node:path'
55

6-
import { build, createServer } from 'vite'
6+
import { build, createBuilder, createServer } from 'vite'
77
import { test } from 'vitest'
88

99
import { amamoMdx } from '../vite.js'
@@ -45,6 +45,32 @@ test('Vite builds MDX without rewriting unchanged generated outputs', async () =
4545
}
4646
})
4747

48+
test('Vite reuses one compiler across client and SSR environments', async () => {
49+
const fixture = await createCompilerFixture()
50+
const entry = path.join(fixture.root, 'main.js')
51+
await writeFile(entry, "import Post from './content/posts/hello.mdx'; console.log(Post)\n")
52+
53+
try {
54+
const builder = await createBuilder({
55+
root: fixture.root,
56+
builder: { sharedPlugins: true },
57+
environments: {
58+
client: { build: { outDir: 'dist/client', rollupOptions: { input: entry } } },
59+
ssr: { build: { outDir: 'dist/server', rollupOptions: { input: entry }, ssr: true } },
60+
},
61+
logLevel: 'silent',
62+
plugins: [amamoMdx(fixture.config)],
63+
resolve: { alias: { 'react/jsx-runtime': reactJsxRuntime } },
64+
})
65+
66+
await builder.buildApp()
67+
assert.ok((await stat(path.join(fixture.root, 'dist/client'))).isDirectory())
68+
assert.ok((await stat(path.join(fixture.root, 'dist/server'))).isDirectory())
69+
} finally {
70+
await fixture.cleanup()
71+
}
72+
})
73+
4874
test('Vite dev watcher updates generated output for create, update, and delete', async () => {
4975
const fixture = await createCompilerFixture()
5076
const secondPost = path.join(path.dirname(fixture.post), 'second.mdx')

src/vite.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@ export function amamoMdx(config: IAmamoMdxConfig): Plugin {
7373
}
7474
})
7575
},
76-
async closeBundle() {
77-
await dispose()
78-
},
7976
}
8077
}
8178

0 commit comments

Comments
 (0)