-
-
Notifications
You must be signed in to change notification settings - Fork 9k
refactor(docusaurus-plugin-content-blog): Replace reading-time
npm with Intl.Segmenter
API
#11091
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
import readingTime from 'reading-time'; | ||
import {defaultReadingTime} from '../blogUtils'; | ||
import {calculateReadingTime} from '../readingTime'; | ||
|
||
describe('readingTime implementation', () => { | ||
it('calculates reading time for simple text', () => { | ||
const content = 'This is a simple test with 7 words.'; | ||
const result = readingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: expect.any(Number), | ||
}); | ||
}); | ||
|
||
it('calculates reading time for empty content', () => { | ||
const content = ''; | ||
const result = readingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: 0, | ||
time: 0, | ||
words: 0, | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with emojis', () => { | ||
const content = 'Hello 😊 World 🌍'; | ||
const result = readingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: expect.any(Number), | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with special characters', () => { | ||
const content = 'Hello! How are you? This is a test...'; | ||
const result = readingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: expect.any(Number), | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with multiple languages', () => { | ||
const content = 'Hello 你好 Bonjour'; | ||
const result = readingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: expect.any(Number), | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with HTML tags', () => { | ||
const content = '<p>This is a <strong>test</strong> with HTML</p>'; | ||
const result = readingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: expect.any(Number), | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with code blocks', () => { | ||
const content = '```js\nconst x = 1;\n```\nThis is a test'; | ||
const result = readingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: expect.any(Number), | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with frontmatter', () => { | ||
const content = '---\ntitle: Test\n---\nThis is a test'; | ||
const result = readingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: expect.any(Number), | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with custom options', () => { | ||
const content = 'This is a test'; | ||
const result = readingTime(content, {wordsPerMinute: 100}); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: expect.any(Number), | ||
}); | ||
}); | ||
|
||
it('calculates reading time using defaultReadingTime', () => { | ||
const content = 'This is a test'; | ||
const result = defaultReadingTime({content, options: {}}); | ||
expect(result).toBeGreaterThan(0); | ||
}); | ||
|
||
describe('Intl.Segmenter implementation', () => { | ||
it('calculates reading time for simple text', () => { | ||
const content = 'This is a simple test with 7 words.'; | ||
const result = calculateReadingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: 7, | ||
}); | ||
}); | ||
|
||
it('calculates reading time for empty content', () => { | ||
const content = ''; | ||
const result = calculateReadingTime(content); | ||
expect(result).toMatchObject({ | ||
text: '0 min read', | ||
minutes: 0, | ||
time: 0, | ||
words: 0, | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with emojis', () => { | ||
const content = 'Hello 😊 World 🌍'; | ||
const result = calculateReadingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: 2, | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with special characters', () => { | ||
const content = 'Hello! How are you? This is a test...'; | ||
const result = calculateReadingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: 7, | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with multiple languages', () => { | ||
const content = 'Hello 你好 Bonjour'; | ||
const result = calculateReadingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: 3, | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with HTML tags', () => { | ||
const content = '<p>This is a <strong>test</strong> with HTML</p>'; | ||
const result = calculateReadingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: 6, | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with code blocks', () => { | ||
const content = '```js\nconst x = 1;\n```\nThis is a test'; | ||
const result = calculateReadingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: 4, | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with frontmatter', () => { | ||
const content = '---\ntitle: Test\n---\nThis is a test'; | ||
const result = calculateReadingTime(content); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: 3, | ||
}); | ||
}); | ||
|
||
it('calculates reading time for content with custom options', () => { | ||
const content = 'This is a test'; | ||
const result = calculateReadingTime(content, {wordsPerMinute: 100}); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: 4, | ||
}); | ||
}); | ||
|
||
it('calculates reading time with different locale', () => { | ||
const content = 'Hello 你好 Bonjour'; | ||
const result = calculateReadingTime(content, {locale: 'zh'}); | ||
expect(result).toMatchObject({ | ||
text: expect.any(String), | ||
minutes: expect.any(Number), | ||
time: expect.any(Number), | ||
words: 3, | ||
}); | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
interface ReadingTimeOptions { | ||
wordsPerMinute?: number; | ||
locale?: string; | ||
} | ||
|
||
interface ReadingTimeResult { | ||
text: string; | ||
minutes: number; | ||
time: number; | ||
words: number; | ||
} | ||
Comment on lines
+12
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We only need the number of minutes as an output |
||
|
||
const DEFAULT_WORDS_PER_MINUTE = 200; | ||
const DEFAULT_LOCALE = 'en'; | ||
|
||
export function calculateReadingTime( | ||
content: string, | ||
options: ReadingTimeOptions = {}, | ||
): ReadingTimeResult { | ||
const wordsPerMinute = options.wordsPerMinute ?? DEFAULT_WORDS_PER_MINUTE; | ||
const locale = options.locale ?? DEFAULT_LOCALE; | ||
const contentWithoutFrontmatter = content.replace(/^---[\s\S]*?---\n/, ''); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We didn't have that before so I'd prefer to not do that. The called should be responsible from providing text content, and this function shouldn't assume it's called in a markdown/mdx context |
||
|
||
const segmenter = new Intl.Segmenter(locale, {granularity: 'word'}); | ||
const segments = segmenter.segment(contentWithoutFrontmatter); | ||
|
||
let wordCount = 0; | ||
for (const segment of segments) { | ||
if (segment.isWordLike) { | ||
wordCount += 1; | ||
} | ||
} | ||
Comment on lines
+30
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you extract this as a "countWords" function that we can unit test independently? |
||
|
||
const minutes = wordCount / wordsPerMinute; | ||
const time = Math.round(minutes * 60 * 1000); | ||
const displayed = Math.ceil(minutes); | ||
|
||
return { | ||
text: `${displayed} min read`, | ||
minutes, | ||
time, | ||
words: wordCount, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The locale should always be provided, a Docusaurus site always has one