@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
103 lines (99 loc) • 3.45 kB
JavaScript
/*
Vendored from https://github.com/ngryman/reading-time
(c) Nicolas Gryman <ngryman@gmail.com>
Reasons:
1. latest (1.5.0) version had a typescript import bug.
2. Streaming mode is not needed (but brings confusion when used in the Browser).
*/
function codeIsInRanges(num, arrayOfRanges) {
return arrayOfRanges.some(([lowerBound, upperBound]) => lowerBound <= num && num <= upperBound);
}
const isCJK = c => {
const charCode = c.codePointAt(0);
// Help wanted!
// This should be good for most cases, but if you find it unsatisfactory
// (e.g. some other language where each character should be standalone words),
// contributions welcome!
return codeIsInRanges(charCode, [
// Hiragana (Katakana not included on purpose,
// context: https://github.com/ngryman/reading-time/pull/35#issuecomment-853364526)
// If you think Katakana should be included and have solid reasons, improvement is welcomed
[0x3040, 0x309f],
// CJK Unified ideographs
[0x4e00, 0x9fff],
// Hangul
[0xac00, 0xd7a3],
// CJK extensions
[0x20000, 0x2ebe0],
]);
};
const isAnsiWordBound = c => {
return ' \n\r\t'.includes(c);
};
const isPunctuation = c => {
const charCode = c.codePointAt(0);
return codeIsInRanges(charCode, [
[0x21, 0x2f],
[0x3a, 0x40],
[0x5b, 0x60],
[0x7b, 0x7e],
// CJK Symbols and Punctuation
[0x3000, 0x303f],
// Full-width ASCII punctuation variants
[0xff00, 0xffef],
]);
};
function countWords(text, options = {}) {
let words = 0;
let start = 0;
let end = text.length - 1;
const isWordBound = options.wordBound || isAnsiWordBound;
// fetch bounds
while (isWordBound(text[start]))
start++;
while (isWordBound(text[end]))
end--;
// Add a trailing word bound to make handling edges more convenient
const normalizedText = `${text}\n`;
// calculate the number of words
for (let i = start; i <= end; i++) {
// A CJK character is a always word;
// A non-word bound followed by a word bound / CJK is the end of a word.
if (isCJK(normalizedText[i]) ||
(!isWordBound(normalizedText[i]) &&
(isWordBound(normalizedText[i + 1]) || isCJK(normalizedText[i + 1])))) {
words++;
}
// In case of CJK followed by punctuations, those characters have to be eaten as well
if (isCJK(normalizedText[i])) {
while (i <= end &&
(isPunctuation(normalizedText[i + 1]) || isWordBound(normalizedText[i + 1]))) {
i++;
}
}
}
return { total: words };
}
function readingTimeWithCount(words, options = {}) {
const { wordsPerMinute = 200 } = options;
// reading time stats
const minutes = words.total / wordsPerMinute;
// Math.round used to resolve floating point funkiness
// http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
const time = Math.round(minutes * 60 * 1000);
const displayed = Math.ceil(parseFloat(minutes.toFixed(2)));
return {
minutes: displayed,
time,
};
}
/**
* API: https://github.com/ngryman/reading-time
*/
export function readingTime(text, options = {}) {
const words = countWords(text, options);
return {
...readingTimeWithCount(words, options),
words,
};
}