@naturalcycles/js-lib 14.107.1 → 14.109.0
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/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/string/escape.d.ts +2 -0
- package/dist/string/escape.js +54 -0
- package/dist/string/pupa.d.ts +22 -0
- package/dist/string/pupa.js +60 -0
- package/dist/string/readingTime.d.ts +29 -0
- package/dist/string/readingTime.js +106 -0
- package/dist/types.d.ts +3 -5
- package/dist/types.js +3 -14
- package/dist-esm/index.js +3 -0
- package/dist-esm/string/escape.js +49 -0
- package/dist-esm/string/pupa.js +55 -0
- package/dist-esm/string/readingTime.js +99 -0
- package/dist-esm/types.js +3 -11
- package/package.json +2 -4
- package/src/index.ts +3 -0
- package/src/string/escape.ts +57 -0
- package/src/string/pupa.ts +86 -0
- package/src/string/readingTime.ts +150 -0
- package/src/types.ts +3 -11
package/dist/index.d.ts
CHANGED
|
@@ -49,6 +49,9 @@ import { pTimeout, pTimeoutFn, PTimeoutOptions } from './promise/pTimeout';
|
|
|
49
49
|
export * from './string/case';
|
|
50
50
|
export * from './string/json.util';
|
|
51
51
|
export * from './string/string.util';
|
|
52
|
+
export * from './string/readingTime';
|
|
53
|
+
export * from './string/escape';
|
|
54
|
+
export * from './string/pupa';
|
|
52
55
|
import { JsonStringifyFunction, StringifyAnyOptions, _stringifyAny } from './string/stringifyAny';
|
|
53
56
|
export * from './time/time.util';
|
|
54
57
|
export * from './is.util';
|
package/dist/index.js
CHANGED
|
@@ -63,6 +63,9 @@ Object.defineProperty(exports, "pTimeoutFn", { enumerable: true, get: function (
|
|
|
63
63
|
tslib_1.__exportStar(require("./string/case"), exports);
|
|
64
64
|
tslib_1.__exportStar(require("./string/json.util"), exports);
|
|
65
65
|
tslib_1.__exportStar(require("./string/string.util"), exports);
|
|
66
|
+
tslib_1.__exportStar(require("./string/readingTime"), exports);
|
|
67
|
+
tslib_1.__exportStar(require("./string/escape"), exports);
|
|
68
|
+
tslib_1.__exportStar(require("./string/pupa"), exports);
|
|
66
69
|
const stringifyAny_1 = require("./string/stringifyAny");
|
|
67
70
|
Object.defineProperty(exports, "_stringifyAny", { enumerable: true, get: function () { return stringifyAny_1._stringifyAny; } });
|
|
68
71
|
tslib_1.__exportStar(require("./time/time.util"), exports);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
|
|
4
|
+
Vendored:
|
|
5
|
+
https://github.com/sindresorhus/escape-goat
|
|
6
|
+
|
|
7
|
+
(c) Sindre Sorhus
|
|
8
|
+
|
|
9
|
+
Reasons:
|
|
10
|
+
1. Stable enough to be included in "core" js-lib
|
|
11
|
+
2. ESM-only
|
|
12
|
+
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.htmlUnescape = exports.htmlEscape = void 0;
|
|
16
|
+
// Multiple `.replace()` calls are actually faster than using replacer functions
|
|
17
|
+
function _htmlEscape(s) {
|
|
18
|
+
return s
|
|
19
|
+
.replace(/&/g, '&') // Must happen first or else it will escape other just-escaped characters.
|
|
20
|
+
.replace(/"/g, '"')
|
|
21
|
+
.replace(/'/g, ''')
|
|
22
|
+
.replace(/</g, '<')
|
|
23
|
+
.replace(/>/g, '>');
|
|
24
|
+
}
|
|
25
|
+
function _htmlUnescape(html) {
|
|
26
|
+
return html
|
|
27
|
+
.replace(/>/g, '>')
|
|
28
|
+
.replace(/</g, '<')
|
|
29
|
+
.replace(/�?39;/g, "'")
|
|
30
|
+
.replace(/"/g, '"')
|
|
31
|
+
.replace(/&/g, '&'); // Must happen last or else it will unescape other characters in the wrong order.
|
|
32
|
+
}
|
|
33
|
+
function htmlEscape(strings, ...values) {
|
|
34
|
+
if (typeof strings === 'string') {
|
|
35
|
+
return _htmlEscape(strings);
|
|
36
|
+
}
|
|
37
|
+
let output = strings[0];
|
|
38
|
+
for (const [index, value] of values.entries()) {
|
|
39
|
+
output = output + _htmlEscape(String(value)) + strings[index + 1];
|
|
40
|
+
}
|
|
41
|
+
return output;
|
|
42
|
+
}
|
|
43
|
+
exports.htmlEscape = htmlEscape;
|
|
44
|
+
function htmlUnescape(strings, ...values) {
|
|
45
|
+
if (typeof strings === 'string') {
|
|
46
|
+
return _htmlUnescape(strings);
|
|
47
|
+
}
|
|
48
|
+
let output = strings[0];
|
|
49
|
+
for (const [index, value] of values.entries()) {
|
|
50
|
+
output = output + _htmlUnescape(String(value)) + strings[index + 1];
|
|
51
|
+
}
|
|
52
|
+
return output;
|
|
53
|
+
}
|
|
54
|
+
exports.htmlUnescape = htmlUnescape;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AnyObject } from '../types';
|
|
2
|
+
export declare class MissingValueError extends Error {
|
|
3
|
+
key: any;
|
|
4
|
+
constructor(key: any);
|
|
5
|
+
}
|
|
6
|
+
export interface PupaOptions {
|
|
7
|
+
/**
|
|
8
|
+
* By default, Pupa throws a `MissingValueError` when a placeholder resolves to `undefined`. With this option set to `true`, it simply ignores it and leaves the placeholder as is.
|
|
9
|
+
*/
|
|
10
|
+
ignoreMissing?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Performs arbitrary operation for each interpolation. If the returned value was `undefined`, it behaves differently depending on the `ignoreMissing` option. Otherwise, the returned value will be interpolated into a string (and escaped when double-braced) and embedded into the template.
|
|
13
|
+
*/
|
|
14
|
+
transform?: (data: {
|
|
15
|
+
value: any;
|
|
16
|
+
key: string;
|
|
17
|
+
}) => unknown;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* API: https://github.com/sindresorhus/pupa
|
|
21
|
+
*/
|
|
22
|
+
export declare function pupa(template: string, data: any[] | AnyObject, opt?: PupaOptions): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
|
|
4
|
+
Vendored:
|
|
5
|
+
https://github.com/sindresorhus/pupa
|
|
6
|
+
|
|
7
|
+
(c) Sindre Sorhus
|
|
8
|
+
|
|
9
|
+
Reasons:
|
|
10
|
+
1. Stable enough to be included in "core" js-lib
|
|
11
|
+
2. ESM-only
|
|
12
|
+
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.pupa = exports.MissingValueError = void 0;
|
|
16
|
+
const escape_1 = require("./escape");
|
|
17
|
+
class MissingValueError extends Error {
|
|
18
|
+
constructor(key) {
|
|
19
|
+
super(`Missing a value for ${key ? `the placeholder: ${key}` : 'a placeholder'}`);
|
|
20
|
+
this.key = key;
|
|
21
|
+
this.name = 'MissingValueError';
|
|
22
|
+
this.key = key;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.MissingValueError = MissingValueError;
|
|
26
|
+
/**
|
|
27
|
+
* API: https://github.com/sindresorhus/pupa
|
|
28
|
+
*/
|
|
29
|
+
function pupa(template, data, opt = {}) {
|
|
30
|
+
if (typeof template !== 'string') {
|
|
31
|
+
throw new TypeError(`Expected a \`string\` in the first argument, got \`${typeof template}\``);
|
|
32
|
+
}
|
|
33
|
+
if (typeof data !== 'object') {
|
|
34
|
+
throw new TypeError(`Expected an \`object\` or \`Array\` in the second argument, got \`${typeof data}\``);
|
|
35
|
+
}
|
|
36
|
+
const { ignoreMissing = false, transform = ({ value }) => value } = opt;
|
|
37
|
+
const replace = (placeholder, key) => {
|
|
38
|
+
let value = data;
|
|
39
|
+
for (const property of key.split('.')) {
|
|
40
|
+
value = value ? value[property] : undefined;
|
|
41
|
+
}
|
|
42
|
+
const transformedValue = transform({ value, key });
|
|
43
|
+
if (transformedValue === undefined) {
|
|
44
|
+
if (ignoreMissing) {
|
|
45
|
+
return placeholder;
|
|
46
|
+
}
|
|
47
|
+
throw new MissingValueError(key);
|
|
48
|
+
}
|
|
49
|
+
return String(transformedValue);
|
|
50
|
+
};
|
|
51
|
+
const composeHtmlEscape = (replacer) => (...args) => (0, escape_1.htmlEscape)(replacer(...args));
|
|
52
|
+
// The regex tries to match either a number inside `{{ }}` or a valid JS identifier or key path.
|
|
53
|
+
const doubleBraceRegex = /{{(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}}/gi;
|
|
54
|
+
if (doubleBraceRegex.test(template)) {
|
|
55
|
+
template = template.replace(doubleBraceRegex, composeHtmlEscape(replace));
|
|
56
|
+
}
|
|
57
|
+
const braceRegex = /{(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}/gi;
|
|
58
|
+
return template.replace(braceRegex, replace);
|
|
59
|
+
}
|
|
60
|
+
exports.pupa = pupa;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Integer } from '../types';
|
|
2
|
+
export interface ReadingTimeOptions {
|
|
3
|
+
/**
|
|
4
|
+
* A function that returns a boolean value depending on if a character is considered as a word bound.
|
|
5
|
+
* Default: spaces, new lines and tabs
|
|
6
|
+
*/
|
|
7
|
+
wordBound?: (char: string) => boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Default 200
|
|
10
|
+
*/
|
|
11
|
+
wordsPerMinute?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ReadingTimeStats {
|
|
14
|
+
/**
|
|
15
|
+
* Number of milliseconds.
|
|
16
|
+
*/
|
|
17
|
+
time: Integer;
|
|
18
|
+
minutes: Integer;
|
|
19
|
+
}
|
|
20
|
+
export interface WordCountStats {
|
|
21
|
+
total: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ReadingTimeResult extends ReadingTimeStats {
|
|
24
|
+
words: WordCountStats;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* API: https://github.com/ngryman/reading-time
|
|
28
|
+
*/
|
|
29
|
+
export declare function readingTime(text: string, options?: ReadingTimeOptions): ReadingTimeResult;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
|
|
4
|
+
Vendored from https://github.com/ngryman/reading-time
|
|
5
|
+
(c) Nicolas Gryman <ngryman@gmail.com>
|
|
6
|
+
|
|
7
|
+
Reasons:
|
|
8
|
+
1. latest (1.5.0) version had a typescript import bug.
|
|
9
|
+
2. Streaming mode is not needed (but brings confusion when used in the Browser).
|
|
10
|
+
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.readingTime = void 0;
|
|
14
|
+
function codeIsInRanges(num, arrayOfRanges) {
|
|
15
|
+
return arrayOfRanges.some(([lowerBound, upperBound]) => lowerBound <= num && num <= upperBound);
|
|
16
|
+
}
|
|
17
|
+
const isCJK = c => {
|
|
18
|
+
const charCode = c.codePointAt(0);
|
|
19
|
+
// Help wanted!
|
|
20
|
+
// This should be good for most cases, but if you find it unsatisfactory
|
|
21
|
+
// (e.g. some other language where each character should be standalone words),
|
|
22
|
+
// contributions welcome!
|
|
23
|
+
return codeIsInRanges(charCode, [
|
|
24
|
+
// Hiragana (Katakana not included on purpose,
|
|
25
|
+
// context: https://github.com/ngryman/reading-time/pull/35#issuecomment-853364526)
|
|
26
|
+
// If you think Katakana should be included and have solid reasons, improvement is welcomed
|
|
27
|
+
[0x3040, 0x309f],
|
|
28
|
+
// CJK Unified ideographs
|
|
29
|
+
[0x4e00, 0x9fff],
|
|
30
|
+
// Hangul
|
|
31
|
+
[0xac00, 0xd7a3],
|
|
32
|
+
// CJK extensions
|
|
33
|
+
[0x20000, 0x2ebe0],
|
|
34
|
+
]);
|
|
35
|
+
};
|
|
36
|
+
const isAnsiWordBound = c => {
|
|
37
|
+
return ' \n\r\t'.includes(c);
|
|
38
|
+
};
|
|
39
|
+
const isPunctuation = c => {
|
|
40
|
+
const charCode = c.codePointAt(0);
|
|
41
|
+
return codeIsInRanges(charCode, [
|
|
42
|
+
[0x21, 0x2f],
|
|
43
|
+
[0x3a, 0x40],
|
|
44
|
+
[0x5b, 0x60],
|
|
45
|
+
[0x7b, 0x7e],
|
|
46
|
+
// CJK Symbols and Punctuation
|
|
47
|
+
[0x3000, 0x303f],
|
|
48
|
+
// Full-width ASCII punctuation variants
|
|
49
|
+
[0xff00, 0xffef],
|
|
50
|
+
]);
|
|
51
|
+
};
|
|
52
|
+
function countWords(text, options = {}) {
|
|
53
|
+
let words = 0;
|
|
54
|
+
let start = 0;
|
|
55
|
+
let end = text.length - 1;
|
|
56
|
+
const isWordBound = options.wordBound || isAnsiWordBound;
|
|
57
|
+
// fetch bounds
|
|
58
|
+
while (isWordBound(text[start]))
|
|
59
|
+
start++;
|
|
60
|
+
while (isWordBound(text[end]))
|
|
61
|
+
end--;
|
|
62
|
+
// Add a trailing word bound to make handling edges more convenient
|
|
63
|
+
const normalizedText = `${text}\n`;
|
|
64
|
+
// calculate the number of words
|
|
65
|
+
for (let i = start; i <= end; i++) {
|
|
66
|
+
// A CJK character is a always word;
|
|
67
|
+
// A non-word bound followed by a word bound / CJK is the end of a word.
|
|
68
|
+
if (isCJK(normalizedText[i]) ||
|
|
69
|
+
(!isWordBound(normalizedText[i]) &&
|
|
70
|
+
(isWordBound(normalizedText[i + 1]) || isCJK(normalizedText[i + 1])))) {
|
|
71
|
+
words++;
|
|
72
|
+
}
|
|
73
|
+
// In case of CJK followed by punctuations, those characters have to be eaten as well
|
|
74
|
+
if (isCJK(normalizedText[i])) {
|
|
75
|
+
while (i <= end &&
|
|
76
|
+
(isPunctuation(normalizedText[i + 1]) || isWordBound(normalizedText[i + 1]))) {
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { total: words };
|
|
82
|
+
}
|
|
83
|
+
function readingTimeWithCount(words, options = {}) {
|
|
84
|
+
const { wordsPerMinute = 200 } = options;
|
|
85
|
+
// reading time stats
|
|
86
|
+
const minutes = words.total / wordsPerMinute;
|
|
87
|
+
// Math.round used to resolve floating point funkiness
|
|
88
|
+
// http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
|
89
|
+
const time = Math.round(minutes * 60 * 1000);
|
|
90
|
+
const displayed = Math.ceil(parseFloat(minutes.toFixed(2)));
|
|
91
|
+
return {
|
|
92
|
+
minutes: displayed,
|
|
93
|
+
time,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* API: https://github.com/ngryman/reading-time
|
|
98
|
+
*/
|
|
99
|
+
function readingTime(text, options = {}) {
|
|
100
|
+
const words = countWords(text, options);
|
|
101
|
+
return {
|
|
102
|
+
...readingTimeWithCount(words, options),
|
|
103
|
+
words,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
exports.readingTime = readingTime;
|
package/dist/types.d.ts
CHANGED
|
@@ -179,19 +179,17 @@ export declare type Reviver = (this: any, key: string, value: any) => any;
|
|
|
179
179
|
* Needed due to https://github.com/microsoft/TypeScript/issues/13778
|
|
180
180
|
* Only affects typings, no runtime effect.
|
|
181
181
|
*/
|
|
182
|
-
export declare
|
|
182
|
+
export declare const _stringMapValues: <T>(m: StringMap<T>) => T[];
|
|
183
183
|
/**
|
|
184
184
|
* Needed due to https://github.com/microsoft/TypeScript/issues/13778
|
|
185
185
|
* Only affects typings, no runtime effect.
|
|
186
186
|
*/
|
|
187
|
-
export declare
|
|
187
|
+
export declare const _stringMapEntries: <T>(m: StringMap<T>) => [k: string, v: T][];
|
|
188
188
|
/**
|
|
189
189
|
* Like `Object.keys`, but returns keys typed as `keyof T`, not as just `string`.
|
|
190
190
|
* This is how TypeScript should work, actually.
|
|
191
|
-
*
|
|
192
|
-
* @experimental
|
|
193
191
|
*/
|
|
194
|
-
export declare
|
|
192
|
+
export declare const _objectKeys: <T extends AnyObject>(obj: T) => (keyof T)[];
|
|
195
193
|
export declare type NullishValue = null | undefined;
|
|
196
194
|
export declare type FalsyValue = false | '' | 0 | null | undefined;
|
|
197
195
|
/**
|
package/dist/types.js
CHANGED
|
@@ -26,28 +26,17 @@ exports._passNothingPredicate = _passNothingPredicate;
|
|
|
26
26
|
* Needed due to https://github.com/microsoft/TypeScript/issues/13778
|
|
27
27
|
* Only affects typings, no runtime effect.
|
|
28
28
|
*/
|
|
29
|
-
|
|
30
|
-
return Object.values(m);
|
|
31
|
-
}
|
|
32
|
-
exports._stringMapValues = _stringMapValues;
|
|
29
|
+
exports._stringMapValues = Object.values;
|
|
33
30
|
/**
|
|
34
31
|
* Needed due to https://github.com/microsoft/TypeScript/issues/13778
|
|
35
32
|
* Only affects typings, no runtime effect.
|
|
36
33
|
*/
|
|
37
|
-
|
|
38
|
-
return Object.entries(m);
|
|
39
|
-
}
|
|
40
|
-
exports._stringMapEntries = _stringMapEntries;
|
|
34
|
+
exports._stringMapEntries = Object.entries;
|
|
41
35
|
/**
|
|
42
36
|
* Like `Object.keys`, but returns keys typed as `keyof T`, not as just `string`.
|
|
43
37
|
* This is how TypeScript should work, actually.
|
|
44
|
-
*
|
|
45
|
-
* @experimental
|
|
46
38
|
*/
|
|
47
|
-
|
|
48
|
-
return Object.keys(obj);
|
|
49
|
-
}
|
|
50
|
-
exports._objectKeys = _objectKeys;
|
|
39
|
+
exports._objectKeys = Object.keys;
|
|
51
40
|
/**
|
|
52
41
|
* Utility function that helps to cast *existing variable* to needed type T.
|
|
53
42
|
*
|
package/dist-esm/index.js
CHANGED
|
@@ -46,6 +46,9 @@ import { pTimeout, pTimeoutFn } from './promise/pTimeout';
|
|
|
46
46
|
export * from './string/case';
|
|
47
47
|
export * from './string/json.util';
|
|
48
48
|
export * from './string/string.util';
|
|
49
|
+
export * from './string/readingTime';
|
|
50
|
+
export * from './string/escape';
|
|
51
|
+
export * from './string/pupa';
|
|
49
52
|
import { _stringifyAny } from './string/stringifyAny';
|
|
50
53
|
export * from './time/time.util';
|
|
51
54
|
export * from './is.util';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Vendored:
|
|
4
|
+
https://github.com/sindresorhus/escape-goat
|
|
5
|
+
|
|
6
|
+
(c) Sindre Sorhus
|
|
7
|
+
|
|
8
|
+
Reasons:
|
|
9
|
+
1. Stable enough to be included in "core" js-lib
|
|
10
|
+
2. ESM-only
|
|
11
|
+
|
|
12
|
+
*/
|
|
13
|
+
// Multiple `.replace()` calls are actually faster than using replacer functions
|
|
14
|
+
function _htmlEscape(s) {
|
|
15
|
+
return s
|
|
16
|
+
.replace(/&/g, '&') // Must happen first or else it will escape other just-escaped characters.
|
|
17
|
+
.replace(/"/g, '"')
|
|
18
|
+
.replace(/'/g, ''')
|
|
19
|
+
.replace(/</g, '<')
|
|
20
|
+
.replace(/>/g, '>');
|
|
21
|
+
}
|
|
22
|
+
function _htmlUnescape(html) {
|
|
23
|
+
return html
|
|
24
|
+
.replace(/>/g, '>')
|
|
25
|
+
.replace(/</g, '<')
|
|
26
|
+
.replace(/�?39;/g, "'")
|
|
27
|
+
.replace(/"/g, '"')
|
|
28
|
+
.replace(/&/g, '&'); // Must happen last or else it will unescape other characters in the wrong order.
|
|
29
|
+
}
|
|
30
|
+
export function htmlEscape(strings, ...values) {
|
|
31
|
+
if (typeof strings === 'string') {
|
|
32
|
+
return _htmlEscape(strings);
|
|
33
|
+
}
|
|
34
|
+
let output = strings[0];
|
|
35
|
+
for (const [index, value] of values.entries()) {
|
|
36
|
+
output = output + _htmlEscape(String(value)) + strings[index + 1];
|
|
37
|
+
}
|
|
38
|
+
return output;
|
|
39
|
+
}
|
|
40
|
+
export function htmlUnescape(strings, ...values) {
|
|
41
|
+
if (typeof strings === 'string') {
|
|
42
|
+
return _htmlUnescape(strings);
|
|
43
|
+
}
|
|
44
|
+
let output = strings[0];
|
|
45
|
+
for (const [index, value] of values.entries()) {
|
|
46
|
+
output = output + _htmlUnescape(String(value)) + strings[index + 1];
|
|
47
|
+
}
|
|
48
|
+
return output;
|
|
49
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Vendored:
|
|
4
|
+
https://github.com/sindresorhus/pupa
|
|
5
|
+
|
|
6
|
+
(c) Sindre Sorhus
|
|
7
|
+
|
|
8
|
+
Reasons:
|
|
9
|
+
1. Stable enough to be included in "core" js-lib
|
|
10
|
+
2. ESM-only
|
|
11
|
+
|
|
12
|
+
*/
|
|
13
|
+
import { htmlEscape } from './escape';
|
|
14
|
+
export class MissingValueError extends Error {
|
|
15
|
+
constructor(key) {
|
|
16
|
+
super(`Missing a value for ${key ? `the placeholder: ${key}` : 'a placeholder'}`);
|
|
17
|
+
this.key = key;
|
|
18
|
+
this.name = 'MissingValueError';
|
|
19
|
+
this.key = key;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* API: https://github.com/sindresorhus/pupa
|
|
24
|
+
*/
|
|
25
|
+
export function pupa(template, data, opt = {}) {
|
|
26
|
+
if (typeof template !== 'string') {
|
|
27
|
+
throw new TypeError(`Expected a \`string\` in the first argument, got \`${typeof template}\``);
|
|
28
|
+
}
|
|
29
|
+
if (typeof data !== 'object') {
|
|
30
|
+
throw new TypeError(`Expected an \`object\` or \`Array\` in the second argument, got \`${typeof data}\``);
|
|
31
|
+
}
|
|
32
|
+
const { ignoreMissing = false, transform = ({ value }) => value } = opt;
|
|
33
|
+
const replace = (placeholder, key) => {
|
|
34
|
+
let value = data;
|
|
35
|
+
for (const property of key.split('.')) {
|
|
36
|
+
value = value ? value[property] : undefined;
|
|
37
|
+
}
|
|
38
|
+
const transformedValue = transform({ value, key });
|
|
39
|
+
if (transformedValue === undefined) {
|
|
40
|
+
if (ignoreMissing) {
|
|
41
|
+
return placeholder;
|
|
42
|
+
}
|
|
43
|
+
throw new MissingValueError(key);
|
|
44
|
+
}
|
|
45
|
+
return String(transformedValue);
|
|
46
|
+
};
|
|
47
|
+
const composeHtmlEscape = (replacer) => (...args) => htmlEscape(replacer(...args));
|
|
48
|
+
// The regex tries to match either a number inside `{{ }}` or a valid JS identifier or key path.
|
|
49
|
+
const doubleBraceRegex = /{{(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}}/gi;
|
|
50
|
+
if (doubleBraceRegex.test(template)) {
|
|
51
|
+
template = template.replace(doubleBraceRegex, composeHtmlEscape(replace));
|
|
52
|
+
}
|
|
53
|
+
const braceRegex = /{(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}/gi;
|
|
54
|
+
return template.replace(braceRegex, replace);
|
|
55
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Vendored from https://github.com/ngryman/reading-time
|
|
4
|
+
(c) Nicolas Gryman <ngryman@gmail.com>
|
|
5
|
+
|
|
6
|
+
Reasons:
|
|
7
|
+
1. latest (1.5.0) version had a typescript import bug.
|
|
8
|
+
2. Streaming mode is not needed (but brings confusion when used in the Browser).
|
|
9
|
+
|
|
10
|
+
*/
|
|
11
|
+
function codeIsInRanges(num, arrayOfRanges) {
|
|
12
|
+
return arrayOfRanges.some(([lowerBound, upperBound]) => lowerBound <= num && num <= upperBound);
|
|
13
|
+
}
|
|
14
|
+
const isCJK = c => {
|
|
15
|
+
const charCode = c.codePointAt(0);
|
|
16
|
+
// Help wanted!
|
|
17
|
+
// This should be good for most cases, but if you find it unsatisfactory
|
|
18
|
+
// (e.g. some other language where each character should be standalone words),
|
|
19
|
+
// contributions welcome!
|
|
20
|
+
return codeIsInRanges(charCode, [
|
|
21
|
+
// Hiragana (Katakana not included on purpose,
|
|
22
|
+
// context: https://github.com/ngryman/reading-time/pull/35#issuecomment-853364526)
|
|
23
|
+
// If you think Katakana should be included and have solid reasons, improvement is welcomed
|
|
24
|
+
[0x3040, 0x309f],
|
|
25
|
+
// CJK Unified ideographs
|
|
26
|
+
[0x4e00, 0x9fff],
|
|
27
|
+
// Hangul
|
|
28
|
+
[0xac00, 0xd7a3],
|
|
29
|
+
// CJK extensions
|
|
30
|
+
[0x20000, 0x2ebe0],
|
|
31
|
+
]);
|
|
32
|
+
};
|
|
33
|
+
const isAnsiWordBound = c => {
|
|
34
|
+
return ' \n\r\t'.includes(c);
|
|
35
|
+
};
|
|
36
|
+
const isPunctuation = c => {
|
|
37
|
+
const charCode = c.codePointAt(0);
|
|
38
|
+
return codeIsInRanges(charCode, [
|
|
39
|
+
[0x21, 0x2f],
|
|
40
|
+
[0x3a, 0x40],
|
|
41
|
+
[0x5b, 0x60],
|
|
42
|
+
[0x7b, 0x7e],
|
|
43
|
+
// CJK Symbols and Punctuation
|
|
44
|
+
[0x3000, 0x303f],
|
|
45
|
+
// Full-width ASCII punctuation variants
|
|
46
|
+
[0xff00, 0xffef],
|
|
47
|
+
]);
|
|
48
|
+
};
|
|
49
|
+
function countWords(text, options = {}) {
|
|
50
|
+
let words = 0;
|
|
51
|
+
let start = 0;
|
|
52
|
+
let end = text.length - 1;
|
|
53
|
+
const isWordBound = options.wordBound || isAnsiWordBound;
|
|
54
|
+
// fetch bounds
|
|
55
|
+
while (isWordBound(text[start]))
|
|
56
|
+
start++;
|
|
57
|
+
while (isWordBound(text[end]))
|
|
58
|
+
end--;
|
|
59
|
+
// Add a trailing word bound to make handling edges more convenient
|
|
60
|
+
const normalizedText = `${text}\n`;
|
|
61
|
+
// calculate the number of words
|
|
62
|
+
for (let i = start; i <= end; i++) {
|
|
63
|
+
// A CJK character is a always word;
|
|
64
|
+
// A non-word bound followed by a word bound / CJK is the end of a word.
|
|
65
|
+
if (isCJK(normalizedText[i]) ||
|
|
66
|
+
(!isWordBound(normalizedText[i]) &&
|
|
67
|
+
(isWordBound(normalizedText[i + 1]) || isCJK(normalizedText[i + 1])))) {
|
|
68
|
+
words++;
|
|
69
|
+
}
|
|
70
|
+
// In case of CJK followed by punctuations, those characters have to be eaten as well
|
|
71
|
+
if (isCJK(normalizedText[i])) {
|
|
72
|
+
while (i <= end &&
|
|
73
|
+
(isPunctuation(normalizedText[i + 1]) || isWordBound(normalizedText[i + 1]))) {
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { total: words };
|
|
79
|
+
}
|
|
80
|
+
function readingTimeWithCount(words, options = {}) {
|
|
81
|
+
const { wordsPerMinute = 200 } = options;
|
|
82
|
+
// reading time stats
|
|
83
|
+
const minutes = words.total / wordsPerMinute;
|
|
84
|
+
// Math.round used to resolve floating point funkiness
|
|
85
|
+
// http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
|
86
|
+
const time = Math.round(minutes * 60 * 1000);
|
|
87
|
+
const displayed = Math.ceil(parseFloat(minutes.toFixed(2)));
|
|
88
|
+
return {
|
|
89
|
+
minutes: displayed,
|
|
90
|
+
time,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* API: https://github.com/ngryman/reading-time
|
|
95
|
+
*/
|
|
96
|
+
export function readingTime(text, options = {}) {
|
|
97
|
+
const words = countWords(text, options);
|
|
98
|
+
return Object.assign(Object.assign({}, readingTimeWithCount(words, options)), { words });
|
|
99
|
+
}
|
package/dist-esm/types.js
CHANGED
|
@@ -18,25 +18,17 @@ export const _passNothingPredicate = () => false;
|
|
|
18
18
|
* Needed due to https://github.com/microsoft/TypeScript/issues/13778
|
|
19
19
|
* Only affects typings, no runtime effect.
|
|
20
20
|
*/
|
|
21
|
-
export
|
|
22
|
-
return Object.values(m);
|
|
23
|
-
}
|
|
21
|
+
export const _stringMapValues = Object.values;
|
|
24
22
|
/**
|
|
25
23
|
* Needed due to https://github.com/microsoft/TypeScript/issues/13778
|
|
26
24
|
* Only affects typings, no runtime effect.
|
|
27
25
|
*/
|
|
28
|
-
export
|
|
29
|
-
return Object.entries(m);
|
|
30
|
-
}
|
|
26
|
+
export const _stringMapEntries = Object.entries;
|
|
31
27
|
/**
|
|
32
28
|
* Like `Object.keys`, but returns keys typed as `keyof T`, not as just `string`.
|
|
33
29
|
* This is how TypeScript should work, actually.
|
|
34
|
-
*
|
|
35
|
-
* @experimental
|
|
36
30
|
*/
|
|
37
|
-
export
|
|
38
|
-
return Object.keys(obj);
|
|
39
|
-
}
|
|
31
|
+
export const _objectKeys = Object.keys;
|
|
40
32
|
/**
|
|
41
33
|
* Utility function that helps to cast *existing variable* to needed type T.
|
|
42
34
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/js-lib",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.109.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"prepare": "husky install",
|
|
6
6
|
"build-prod": "build-prod-esm-cjs",
|
|
@@ -18,12 +18,10 @@
|
|
|
18
18
|
"@types/node": "^18.0.0",
|
|
19
19
|
"expect-type": "^0.13.0",
|
|
20
20
|
"jest": "^28.0.3",
|
|
21
|
-
"patch-package": "^6.2.1",
|
|
22
21
|
"prettier": "^2.1.2",
|
|
23
22
|
"rxjs": "^7.0.1",
|
|
24
23
|
"vuepress": "^1.7.1",
|
|
25
|
-
"vuepress-plugin-typescript": "^0.3.1"
|
|
26
|
-
"weak-napi": "^2.0.2"
|
|
24
|
+
"vuepress-plugin-typescript": "^0.3.1"
|
|
27
25
|
},
|
|
28
26
|
"files": [
|
|
29
27
|
"dist",
|
package/src/index.ts
CHANGED
|
@@ -82,6 +82,9 @@ import { pTimeout, pTimeoutFn, PTimeoutOptions } from './promise/pTimeout'
|
|
|
82
82
|
export * from './string/case'
|
|
83
83
|
export * from './string/json.util'
|
|
84
84
|
export * from './string/string.util'
|
|
85
|
+
export * from './string/readingTime'
|
|
86
|
+
export * from './string/escape'
|
|
87
|
+
export * from './string/pupa'
|
|
85
88
|
import { JsonStringifyFunction, StringifyAnyOptions, _stringifyAny } from './string/stringifyAny'
|
|
86
89
|
export * from './time/time.util'
|
|
87
90
|
export * from './is.util'
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Vendored:
|
|
4
|
+
https://github.com/sindresorhus/escape-goat
|
|
5
|
+
|
|
6
|
+
(c) Sindre Sorhus
|
|
7
|
+
|
|
8
|
+
Reasons:
|
|
9
|
+
1. Stable enough to be included in "core" js-lib
|
|
10
|
+
2. ESM-only
|
|
11
|
+
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Multiple `.replace()` calls are actually faster than using replacer functions
|
|
15
|
+
function _htmlEscape(s: string): string {
|
|
16
|
+
return s
|
|
17
|
+
.replace(/&/g, '&') // Must happen first or else it will escape other just-escaped characters.
|
|
18
|
+
.replace(/"/g, '"')
|
|
19
|
+
.replace(/'/g, ''')
|
|
20
|
+
.replace(/</g, '<')
|
|
21
|
+
.replace(/>/g, '>')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _htmlUnescape(html: string): string {
|
|
25
|
+
return html
|
|
26
|
+
.replace(/>/g, '>')
|
|
27
|
+
.replace(/</g, '<')
|
|
28
|
+
.replace(/�?39;/g, "'")
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/&/g, '&') // Must happen last or else it will unescape other characters in the wrong order.
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function htmlEscape(strings: string | TemplateStringsArray, ...values: any[]): string {
|
|
34
|
+
if (typeof strings === 'string') {
|
|
35
|
+
return _htmlEscape(strings)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let output = strings[0]!
|
|
39
|
+
for (const [index, value] of values.entries()) {
|
|
40
|
+
output = output + _htmlEscape(String(value)) + strings[index + 1]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return output
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function htmlUnescape(strings: string | TemplateStringsArray, ...values: any[]): string {
|
|
47
|
+
if (typeof strings === 'string') {
|
|
48
|
+
return _htmlUnescape(strings)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let output = strings[0]!
|
|
52
|
+
for (const [index, value] of values.entries()) {
|
|
53
|
+
output = output + _htmlUnescape(String(value)) + strings[index + 1]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return output
|
|
57
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Vendored:
|
|
4
|
+
https://github.com/sindresorhus/pupa
|
|
5
|
+
|
|
6
|
+
(c) Sindre Sorhus
|
|
7
|
+
|
|
8
|
+
Reasons:
|
|
9
|
+
1. Stable enough to be included in "core" js-lib
|
|
10
|
+
2. ESM-only
|
|
11
|
+
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AnyObject } from '../types'
|
|
15
|
+
import { htmlEscape } from './escape'
|
|
16
|
+
|
|
17
|
+
export class MissingValueError extends Error {
|
|
18
|
+
constructor(public key: any) {
|
|
19
|
+
super(`Missing a value for ${key ? `the placeholder: ${key}` : 'a placeholder'}`)
|
|
20
|
+
this.name = 'MissingValueError'
|
|
21
|
+
this.key = key
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PupaOptions {
|
|
26
|
+
/**
|
|
27
|
+
* By default, Pupa throws a `MissingValueError` when a placeholder resolves to `undefined`. With this option set to `true`, it simply ignores it and leaves the placeholder as is.
|
|
28
|
+
*/
|
|
29
|
+
ignoreMissing?: boolean
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Performs arbitrary operation for each interpolation. If the returned value was `undefined`, it behaves differently depending on the `ignoreMissing` option. Otherwise, the returned value will be interpolated into a string (and escaped when double-braced) and embedded into the template.
|
|
33
|
+
*/
|
|
34
|
+
transform?: (data: { value: any; key: string }) => unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* API: https://github.com/sindresorhus/pupa
|
|
39
|
+
*/
|
|
40
|
+
export function pupa(template: string, data: any[] | AnyObject, opt: PupaOptions = {}): string {
|
|
41
|
+
if (typeof template !== 'string') {
|
|
42
|
+
throw new TypeError(`Expected a \`string\` in the first argument, got \`${typeof template}\``)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeof data !== 'object') {
|
|
46
|
+
throw new TypeError(
|
|
47
|
+
`Expected an \`object\` or \`Array\` in the second argument, got \`${typeof data}\``,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { ignoreMissing = false, transform = ({ value }) => value } = opt
|
|
52
|
+
|
|
53
|
+
const replace = (placeholder: string, key: string): string => {
|
|
54
|
+
let value = data
|
|
55
|
+
for (const property of key.split('.')) {
|
|
56
|
+
value = value ? (value as AnyObject)[property] : undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const transformedValue = transform({ value, key })
|
|
60
|
+
if (transformedValue === undefined) {
|
|
61
|
+
if (ignoreMissing) {
|
|
62
|
+
return placeholder
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new MissingValueError(key)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return String(transformedValue)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const composeHtmlEscape =
|
|
72
|
+
(replacer: any) =>
|
|
73
|
+
(...args: any[]) =>
|
|
74
|
+
htmlEscape(replacer(...args))
|
|
75
|
+
|
|
76
|
+
// The regex tries to match either a number inside `{{ }}` or a valid JS identifier or key path.
|
|
77
|
+
const doubleBraceRegex = /{{(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}}/gi
|
|
78
|
+
|
|
79
|
+
if (doubleBraceRegex.test(template)) {
|
|
80
|
+
template = template.replace(doubleBraceRegex, composeHtmlEscape(replace))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const braceRegex = /{(\d+|[a-z$_][\w\-$]*?(?:\.[\w\-$]*?)*?)}/gi
|
|
84
|
+
|
|
85
|
+
return template.replace(braceRegex, replace)
|
|
86
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Vendored from https://github.com/ngryman/reading-time
|
|
4
|
+
(c) Nicolas Gryman <ngryman@gmail.com>
|
|
5
|
+
|
|
6
|
+
Reasons:
|
|
7
|
+
1. latest (1.5.0) version had a typescript import bug.
|
|
8
|
+
2. Streaming mode is not needed (but brings confusion when used in the Browser).
|
|
9
|
+
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Integer } from '../types'
|
|
13
|
+
|
|
14
|
+
export interface ReadingTimeOptions {
|
|
15
|
+
/**
|
|
16
|
+
* A function that returns a boolean value depending on if a character is considered as a word bound.
|
|
17
|
+
* Default: spaces, new lines and tabs
|
|
18
|
+
*/
|
|
19
|
+
wordBound?: (char: string) => boolean
|
|
20
|
+
/**
|
|
21
|
+
* Default 200
|
|
22
|
+
*/
|
|
23
|
+
wordsPerMinute?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReadingTimeStats {
|
|
27
|
+
/**
|
|
28
|
+
* Number of milliseconds.
|
|
29
|
+
*/
|
|
30
|
+
time: Integer
|
|
31
|
+
minutes: Integer
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WordCountStats {
|
|
35
|
+
total: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ReadingTimeResult extends ReadingTimeStats {
|
|
39
|
+
words: WordCountStats
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type WordBoundFunction = (char: string) => boolean
|
|
43
|
+
|
|
44
|
+
function codeIsInRanges(num: number, arrayOfRanges: number[][]) {
|
|
45
|
+
return arrayOfRanges.some(([lowerBound, upperBound]) => lowerBound! <= num && num <= upperBound!)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isCJK: WordBoundFunction = c => {
|
|
49
|
+
const charCode = c.codePointAt(0)!
|
|
50
|
+
// Help wanted!
|
|
51
|
+
// This should be good for most cases, but if you find it unsatisfactory
|
|
52
|
+
// (e.g. some other language where each character should be standalone words),
|
|
53
|
+
// contributions welcome!
|
|
54
|
+
return codeIsInRanges(charCode, [
|
|
55
|
+
// Hiragana (Katakana not included on purpose,
|
|
56
|
+
// context: https://github.com/ngryman/reading-time/pull/35#issuecomment-853364526)
|
|
57
|
+
// If you think Katakana should be included and have solid reasons, improvement is welcomed
|
|
58
|
+
[0x3040, 0x309f],
|
|
59
|
+
// CJK Unified ideographs
|
|
60
|
+
[0x4e00, 0x9fff],
|
|
61
|
+
// Hangul
|
|
62
|
+
[0xac00, 0xd7a3],
|
|
63
|
+
// CJK extensions
|
|
64
|
+
[0x20000, 0x2ebe0],
|
|
65
|
+
])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isAnsiWordBound: WordBoundFunction = c => {
|
|
69
|
+
return ' \n\r\t'.includes(c)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const isPunctuation: WordBoundFunction = c => {
|
|
73
|
+
const charCode = c.codePointAt(0)!
|
|
74
|
+
return codeIsInRanges(charCode, [
|
|
75
|
+
[0x21, 0x2f],
|
|
76
|
+
[0x3a, 0x40],
|
|
77
|
+
[0x5b, 0x60],
|
|
78
|
+
[0x7b, 0x7e],
|
|
79
|
+
// CJK Symbols and Punctuation
|
|
80
|
+
[0x3000, 0x303f],
|
|
81
|
+
// Full-width ASCII punctuation variants
|
|
82
|
+
[0xff00, 0xffef],
|
|
83
|
+
])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function countWords(text: string, options: ReadingTimeOptions = {}): WordCountStats {
|
|
87
|
+
let words = 0
|
|
88
|
+
let start = 0
|
|
89
|
+
let end = text.length - 1
|
|
90
|
+
const isWordBound = options.wordBound || isAnsiWordBound
|
|
91
|
+
|
|
92
|
+
// fetch bounds
|
|
93
|
+
while (isWordBound(text[start]!)) start++
|
|
94
|
+
while (isWordBound(text[end]!)) end--
|
|
95
|
+
|
|
96
|
+
// Add a trailing word bound to make handling edges more convenient
|
|
97
|
+
const normalizedText = `${text}\n`
|
|
98
|
+
|
|
99
|
+
// calculate the number of words
|
|
100
|
+
for (let i = start; i <= end; i++) {
|
|
101
|
+
// A CJK character is a always word;
|
|
102
|
+
// A non-word bound followed by a word bound / CJK is the end of a word.
|
|
103
|
+
if (
|
|
104
|
+
isCJK(normalizedText[i]!) ||
|
|
105
|
+
(!isWordBound(normalizedText[i]!) &&
|
|
106
|
+
(isWordBound(normalizedText[i + 1]!) || isCJK(normalizedText[i + 1]!)))
|
|
107
|
+
) {
|
|
108
|
+
words++
|
|
109
|
+
}
|
|
110
|
+
// In case of CJK followed by punctuations, those characters have to be eaten as well
|
|
111
|
+
if (isCJK(normalizedText[i]!)) {
|
|
112
|
+
while (
|
|
113
|
+
i <= end &&
|
|
114
|
+
(isPunctuation(normalizedText[i + 1]!) || isWordBound(normalizedText[i + 1]!))
|
|
115
|
+
) {
|
|
116
|
+
i++
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { total: words }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readingTimeWithCount(
|
|
124
|
+
words: WordCountStats,
|
|
125
|
+
options: ReadingTimeOptions = {},
|
|
126
|
+
): ReadingTimeStats {
|
|
127
|
+
const { wordsPerMinute = 200 } = options
|
|
128
|
+
// reading time stats
|
|
129
|
+
const minutes = words.total / wordsPerMinute
|
|
130
|
+
// Math.round used to resolve floating point funkiness
|
|
131
|
+
// http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
|
132
|
+
const time = Math.round(minutes * 60 * 1000)
|
|
133
|
+
const displayed = Math.ceil(parseFloat(minutes.toFixed(2)))
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
minutes: displayed,
|
|
137
|
+
time,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* API: https://github.com/ngryman/reading-time
|
|
143
|
+
*/
|
|
144
|
+
export function readingTime(text: string, options: ReadingTimeOptions = {}): ReadingTimeResult {
|
|
145
|
+
const words = countWords(text, options)
|
|
146
|
+
return {
|
|
147
|
+
...readingTimeWithCount(words, options),
|
|
148
|
+
words,
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -243,27 +243,19 @@ export type Reviver = (this: any, key: string, value: any) => any
|
|
|
243
243
|
* Needed due to https://github.com/microsoft/TypeScript/issues/13778
|
|
244
244
|
* Only affects typings, no runtime effect.
|
|
245
245
|
*/
|
|
246
|
-
export
|
|
247
|
-
return Object.values(m) as T[]
|
|
248
|
-
}
|
|
246
|
+
export const _stringMapValues = Object.values as <T>(m: StringMap<T>) => T[]
|
|
249
247
|
|
|
250
248
|
/**
|
|
251
249
|
* Needed due to https://github.com/microsoft/TypeScript/issues/13778
|
|
252
250
|
* Only affects typings, no runtime effect.
|
|
253
251
|
*/
|
|
254
|
-
export
|
|
255
|
-
return Object.entries(m) as [string, T][]
|
|
256
|
-
}
|
|
252
|
+
export const _stringMapEntries = Object.entries as <T>(m: StringMap<T>) => [k: string, v: T][]
|
|
257
253
|
|
|
258
254
|
/**
|
|
259
255
|
* Like `Object.keys`, but returns keys typed as `keyof T`, not as just `string`.
|
|
260
256
|
* This is how TypeScript should work, actually.
|
|
261
|
-
*
|
|
262
|
-
* @experimental
|
|
263
257
|
*/
|
|
264
|
-
export
|
|
265
|
-
return Object.keys(obj)
|
|
266
|
-
}
|
|
258
|
+
export const _objectKeys = Object.keys as <T extends AnyObject>(obj: T) => (keyof T)[]
|
|
267
259
|
|
|
268
260
|
export type NullishValue = null | undefined
|
|
269
261
|
export type FalsyValue = false | '' | 0 | null | undefined
|