Skip to content

Commit df8dfa5

Browse files
committed
v3.3.2
1 parent 14218da commit df8dfa5

File tree

3 files changed

+167
-112
lines changed

3 files changed

+167
-112
lines changed

README.md

Lines changed: 84 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,134 @@
11
# Read Time Counter JS
22

3-
Read Time Counter is a ***simple and robust*** reading time counter that **accurately** counts words, characters, and images in your content.
3+
Read Time Counter is a **simple and robust** reading time counter that **accurately** counts words, characters, and images in your content.
44

5-
It calculates the estimated reading time for English, CKJ (Chinese, Korean, Japanese), and other Latin-based languages by combining text reading and image viewing times.
5+
It calculates the estimated reading time for English, CKJ (Chinese, Korean, Japanese), and other Latin-based languages by combining text reading and image viewing times.
66

7-
 
7+
## Features
88

9-
## How to use?
9+
- Accurate word counting for English and Latin-based languages.
10+
- Character counting for CJK (Chinese, Korean, Japanese) languages.
11+
- Image viewing time calculation.
12+
- Customizable reading speeds and output targets.
13+
- Lightweight and easy to integrate.
1014

11-
Ways to include the script:
15+
## Installation
1216

13-
### 1. Directly embed the script
17+
You can include the script in your project in one of the following ways:
1418

15-
Embed the script on your page, ideally before the closing `</body>` tag, to ensure all elements are loaded before the script runs.
19+
### 1. CDN (Recommended)
20+
21+
Add the following script tag to your HTML `<head>`:
1622

1723
```html
18-
<script>
19-
// Paste the code of readtime.js here
20-
</script>
24+
<script defer src="https://cdn.jsdelivr.net/gh/SPACESODA/readtimecounter@3.3.2/readtime.min.js"></script>
2125
```
2226

23-
### 2. Host and link your own .js file
27+
### 2. Local File
2428

25-
Place this in the `<head>` with the `defer` attribute so it runs after the DOM is parsed:
29+
Download `readtime.js` and include it in your project:
2630

2731
```html
28-
<script defer src="readtime.js"></script>
32+
<script defer src="path/to/readtime.js"></script>
2933
```
3034

31-
### 3. Use the CDN version
35+
### 3. Inline Script
36+
37+
Copy the contents of `readtime.js` and embed it directly:
3238

3339
```html
34-
<script defer src="https://cdn.jsdelivr.net/gh/SPACESODA/readtimecounter@3.2.8/readtime.min.js"></script>
40+
<script>
41+
// Paste readtime.js code here
42+
</script>
3543
```
3644

37-
&nbsp;
45+
## Usage
3846

39-
## Define the area
47+
### 1. Define the Content Area
4048

41-
Wrap your article content in an element with the attribute `id="readtimearea"`.
49+
Wrap your article or content in an element with the ID `readtimearea`.
4250

43-
**Note:** Only one `readtimearea` is supported per page.
51+
```html
52+
<article id="readtimearea">
53+
<!-- Your content goes here -->
54+
</article>
55+
```
4456

45-
&nbsp;
57+
> **Note:** Only one `readtimearea` is supported per page.
4658
47-
## Output elements
59+
### 2. Display the Output
4860

49-
Display the estimated reading time (in minutes) by adding an element with the `id="readtime"`. For example: `<span id="readtime"></span>`
61+
Add elements with specific IDs where you want the statistics to appear.
5062

51-
The script also outputs individual counts for:
52-
**Words:** Displayed in an element with `id="wordCount"`.
53-
**CKJ Characters:** Displayed in an element with `id="ckjCount"`.
54-
**Images:** Displayed in an element with `id="imgCount"`.
55-
**Combined Info:** For displaying a summary, use `id="hybridCount"`.
63+
| ID | Description |
64+
| :------------ | :--------------------------------------------------- |
65+
| `readtime` | Estimated reading time in minutes. |
66+
| `wordCount` | Total word count (English/Latin). |
67+
| `ckjCount` | Total CJK character count. |
68+
| `imgCount` | Total image count. |
69+
| `hybridCount` | A combined summary of words, characters, and images. |
5670

57-
Example:
71+
**Example:**
5872

5973
```html
60-
<p>This article takes around <span id="readtime"></span> minutes to read.</p>
61-
<p class="note">
62-
There are
63-
<span id="wordCount"></span> words,
64-
<span id="ckjCount"></span> CJK characters, and
65-
<span id="imgCount"></span> images in this article.
66-
</p>
67-
<p class="note">Summary: <span id="hybridCount"></span></p>
68-
```
74+
<p>Estimated reading time: <span id="readtime"></span> minutes</p>
75+
76+
<div class="stats">
77+
<span id="wordCount"></span> words |
78+
<span id="ckjCount"></span> characters |
79+
<span id="imgCount"></span> images
80+
</div>
6981

70-
&nbsp;
82+
<!-- Or use the summary -->
83+
<p><span id="hybridCount"></span></p>
84+
```
7185

72-
## Configurations
86+
## Configuration
7387

74-
If you’re using the CDN version, you can override the default reading speed and time format like this:
88+
You can customize the reading speed and other settings by defining `window.readingTimeSettings` **before** the script loads (or simply ensure the script runs after this configuration).
7589

7690
```html
77-
<script defer src="https://cdn.jsdelivr.net/gh/SPACESODA/readtimecounter@3.2.8/readtime.min.js"></script>
7891
<script>
7992
window.readingTimeSettings = {
80-
engSpeed: 225, // words per minute for English
81-
charSpeed: 300, // characters per minute for CKJ
82-
imgSpeed: 10, // seconds per image
83-
timeFormat: "integer" // "decimal" or "integer"
93+
engSpeed: 230, // English words per minute
94+
charSpeed: 285, // CJK characters per minute
95+
imgSpeed: 8, // Seconds per image
96+
timeFormat: "decimal", // "decimal" or "integer"
97+
// Custom Selectors (optional)
98+
readTimeTarget: "readtime",
99+
wordCountTarget: "wordCount",
100+
ckjCountTarget: "ckjCount",
101+
imgCountTarget: "imgCount",
102+
hybridCountTarget: "hybridCount",
103+
readTimeArea: "readtimearea"
84104
};
85105
</script>
106+
<!-- Include the script after settings -->
107+
<script defer src="https://cdn.jsdelivr.net/gh/SPACESODA/readtimecounter@3.3.2/readtime.min.js"></script>
86108
```
87109

88-
&nbsp;
110+
### Default Settings
89111

90-
## Live Example on CodePen
112+
| Option | Type | Default | Description |
113+
| :----------- | :----- | :---------- | :----------------------------------------------- |
114+
| `engSpeed` | Number | `230` | Reading speed for English words (WPM). |
115+
| `charSpeed` | Number | `285` | Reading speed for CJK characters (CPM). |
116+
| `imgSpeed` | Number | `8` | Time allocated for viewing each image (seconds). |
117+
| `timeFormat` | String | `"decimal"` | Output format: `"decimal"` or `"integer"`. |
91118

92-
Visit: https://codepen.io/pen/EaxyZzG
119+
## Live Demo
93120

94-
📣 **Please feel free to comment, contribute and make the code better!**
121+
Check out the live example on CodePen:
122+
[https://codepen.io/pen/EaxyZzG](https://codepen.io/pen/EaxyZzG)
95123

96-
&nbsp;
124+
## Contributing
97125

98-
---
126+
Please feel free to comment, contribute, and make the code better!
99127

100-
References:
101-
102-
https://en.wikipedia.org/wiki/English_alphabet
103-
104-
https://en.wikipedia.org/wiki/CJK_characters
128+
---
105129

106-
https://en.wikipedia.org/wiki/Universal_Character_Set_characters
130+
### References
107131

108-
&nbsp;
132+
- [English Alphabet](https://en.wikipedia.org/wiki/English_alphabet)
133+
- [CJK Characters](https://en.wikipedia.org/wiki/CJK_characters)
134+
- [Universal Character Set](https://en.wikipedia.org/wiki/Universal_Character_Set_characters)

readtime.js

Lines changed: 82 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Read Time Counter v3.3.1
2+
* Read Time Counter v3.3.2
33
* https://github.com/SPACESODA/readtimecounter
44
*/
55

@@ -25,6 +25,12 @@
2525
window.readingTimeSettings || {}
2626
);
2727

28+
// Cache Intl.Segmenter instance if available
29+
let segmenter;
30+
if (typeof Intl !== "undefined" && Intl.Segmenter) {
31+
segmenter = new Intl.Segmenter("ja", { granularity: "word" });
32+
}
33+
2834
// Helper: Debounce function
2935
function debounce(func, wait) {
3036
let timeout;
@@ -35,89 +41,112 @@
3541
};
3642
}
3743

38-
// Helper: Count words using Intl.Segmenter if available
39-
function countWords(text, isCKJ = false) {
40-
if (typeof Intl !== "undefined" && Intl.Segmenter) {
41-
const segmenter = new Intl.Segmenter(isCKJ ? "ja" : "en", {
42-
granularity: "word",
43-
});
44+
// Helper: Check if a string contains CJK characters
45+
function isCJK(str) {
46+
return /[\p{Script=Han}\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(str);
47+
}
48+
49+
// Helper: Count words and characters
50+
function analyzeText(text) {
51+
let engCount = 0;
52+
let ckjCount = 0;
53+
54+
if (segmenter) {
4455
const segments = segmenter.segment(text);
45-
let count = 0;
4656
for (const { segment, isWordLike } of segments) {
4757
if (isWordLike) {
48-
if (isCKJ) {
49-
// Check if the segment actually contains CKJ characters
50-
if (/[\p{Script=Han}\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(segment)) {
51-
count += [...segment].length;
52-
}
58+
if (isCJK(segment)) {
59+
ckjCount += [...segment].length;
5360
} else {
54-
// For English/non-CKJ, we count if it's word-like and NOT CKJ
55-
if (!/[\p{Script=Han}\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(segment)) {
56-
count++;
57-
}
61+
engCount++;
5862
}
5963
}
6064
}
61-
return count;
62-
}
63-
64-
// Fallback for older browsers
65-
if (isCKJ) {
66-
let matches = text.match(
65+
} else {
66+
// Fallback for older browsers
67+
// Count CJK characters
68+
const ckjMatches = text.match(
6769
/[\p{Script=Han}\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}]+/gu
6870
);
69-
return matches ? matches.join("").length : 0;
70-
} else {
71-
let cleanedText = text
71+
if (ckjMatches) {
72+
ckjCount = ckjMatches.join("").length;
73+
}
74+
75+
// Count English words (excluding CJK)
76+
// Remove CJK characters to avoid counting them as words
77+
const nonCkjText = text.replace(
78+
/[\p{Script=Han}\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}]+/gu,
79+
" "
80+
);
81+
82+
const cleanedText = nonCkjText
7283
.replace(/[^\w\s'-]/g, " ") // Keep apostrophes and hyphens
73-
.replace(/\s+/g, " ") // Normalize spaces and line breaks
84+
.replace(/\s+/g, " ") // Normalize spaces
7485
.trim();
75-
return cleanedText.split(" ").filter((word) => word.length > 0).length;
86+
87+
if (cleanedText.length > 0) {
88+
engCount = cleanedText.split(" ").length;
89+
}
7690
}
91+
92+
return { engCount, ckjCount };
7793
}
7894

7995
function countImages(element) {
8096
return element.querySelectorAll("img[alt], svg[alt]").length;
8197
}
8298

8399
function getReadableText(element) {
84-
function walkNodes(node) {
85-
if (node.nodeType === Node.ELEMENT_NODE) {
86-
if (["SCRIPT", "STYLE"].includes(node.nodeName)) {
87-
return "";
88-
}
89-
return Array.from(node.childNodes).map(walkNodes).join(" ");
90-
} else if (node.nodeType === Node.TEXT_NODE) {
91-
return node.textContent || "";
100+
let text = "";
101+
const walker = document.createTreeWalker(
102+
element,
103+
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
104+
{
105+
acceptNode: function (node) {
106+
if (node.nodeType === Node.ELEMENT_NODE) {
107+
if (["SCRIPT", "STYLE", "NOSCRIPT"].includes(node.tagName)) {
108+
return NodeFilter.FILTER_REJECT;
109+
}
110+
return NodeFilter.FILTER_SKIP;
111+
}
112+
return NodeFilter.FILTER_ACCEPT;
113+
},
92114
}
93-
return "";
115+
);
116+
117+
while (walker.nextNode()) {
118+
text += walker.currentNode.textContent + " ";
94119
}
95-
let rawText = walkNodes(element);
120+
96121
// Normalize whitespace and trim
97-
return rawText.replace(/\s+/g, " ").trim();
122+
return text.replace(/\s+/g, " ").trim();
98123
}
99124

100125
function updateDisplay(element) {
101-
let text = getReadableText(element);
102-
let engCount = countWords(text, false);
103-
let ckjCount = countWords(text, true);
104-
let imgCount = countImages(element);
105-
let engTime = engCount / settings.engSpeed;
106-
let ckjTime = ckjCount / settings.charSpeed;
107-
let imgTime = (imgCount * settings.imgSpeed) / 60;
108-
let totalReadingTime = engTime + ckjTime + imgTime;
126+
const text = getReadableText(element);
127+
const { engCount, ckjCount } = analyzeText(text);
128+
const imgCount = countImages(element);
129+
130+
const engTime = engCount / settings.engSpeed;
131+
const ckjTime = ckjCount / settings.charSpeed;
132+
const imgTime = (imgCount * settings.imgSpeed) / 60;
133+
const totalReadingTime = engTime + ckjTime + imgTime;
134+
109135
// Format time based on settings.timeFormat
110-
let roundedTime = Math.round(totalReadingTime * 10) / 10;
111136
let displayTime;
112137
if (settings.timeFormat === "integer") {
113-
displayTime = Math.round(totalReadingTime); // Round to nearest integer
138+
displayTime = Math.round(totalReadingTime);
114139
} else {
115-
displayTime = roundedTime; // Keep 1 decimal place
140+
displayTime = (Math.round(totalReadingTime * 10) / 10).toFixed(1);
141+
if (displayTime.endsWith(".0")) {
142+
displayTime = displayTime.slice(0, -2);
143+
}
116144
}
117-
displayTime = displayTime === 0 ? "0" : displayTime; // 0 for empty content
145+
146+
displayTime = parseFloat(displayTime) === 0 ? "0" : displayTime;
118147

119148
const readTimeElement = document.getElementById(settings.readTimeTarget);
120-
if (readTimeElement) readTimeElement.textContent = `${displayTime}`;
149+
if (readTimeElement) readTimeElement.textContent = displayTime;
121150

122151
const wordCountElement = document.getElementById(settings.wordCountTarget);
123152
if (wordCountElement) wordCountElement.textContent = engCount;
@@ -161,7 +190,7 @@
161190
attributes: true,
162191
childList: true,
163192
subtree: true,
164-
characterData: true
193+
characterData: true,
165194
});
166195
}
167196
// Initial call

test.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ <h1>The Universe</h1>
303303
</main>
304304
</div>
305305

306-
<script src="readtime.js"></script>
306+
<script defer src="readtime.js"></script>
307307
</body>
308308

309309
</html>

0 commit comments

Comments
 (0)