@naturalcycles/js-lib 14.106.0 → 14.108.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/error/error.util.d.ts +13 -0
- package/dist/error/error.util.js +20 -1
- 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 +18 -0
- package/dist/string/readingTime.js +106 -0
- package/dist-esm/error/error.util.js +15 -0
- 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/package.json +1 -1
- package/src/error/error.util.ts +19 -0
- 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 +138 -0
|
@@ -24,3 +24,16 @@ export declare function _isHttpErrorObject(o: any): o is ErrorObject<HttpErrorDa
|
|
|
24
24
|
* Note: any instance of AppError is also automatically an ErrorObject
|
|
25
25
|
*/
|
|
26
26
|
export declare function _isErrorObject(o: any): o is ErrorObject;
|
|
27
|
+
/**
|
|
28
|
+
* Convenience function to safely add properties to Error's `data` object
|
|
29
|
+
* (even if it wasn't previously existing)
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
*
|
|
33
|
+
* try {} catch (err) {
|
|
34
|
+
* _errorDataAppend(err, {
|
|
35
|
+
* httpStatusCode: 401,
|
|
36
|
+
* })
|
|
37
|
+
* }
|
|
38
|
+
*/
|
|
39
|
+
export declare function _errorDataAppend(err: any, data: ErrorData): void;
|
package/dist/error/error.util.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports._isErrorObject = exports._isHttpErrorObject = exports._isHttpErrorResponse = exports._errorObjectToError = exports._errorObjectToAppError = exports._errorToErrorObject = exports._anyToErrorObject = exports._anyToError = void 0;
|
|
3
|
+
exports._errorDataAppend = exports._isErrorObject = exports._isHttpErrorObject = exports._isHttpErrorResponse = exports._errorObjectToError = exports._errorObjectToAppError = exports._errorToErrorObject = exports._anyToErrorObject = exports._anyToError = void 0;
|
|
4
4
|
const __1 = require("..");
|
|
5
5
|
/**
|
|
6
6
|
* Useful to ensure that error in `catch (err) { ... }`
|
|
@@ -108,3 +108,22 @@ function _isErrorObject(o) {
|
|
|
108
108
|
return (!!o && typeof o.name === 'string' && typeof o.message === 'string' && typeof o.data === 'object');
|
|
109
109
|
}
|
|
110
110
|
exports._isErrorObject = _isErrorObject;
|
|
111
|
+
/**
|
|
112
|
+
* Convenience function to safely add properties to Error's `data` object
|
|
113
|
+
* (even if it wasn't previously existing)
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
*
|
|
117
|
+
* try {} catch (err) {
|
|
118
|
+
* _errorDataAppend(err, {
|
|
119
|
+
* httpStatusCode: 401,
|
|
120
|
+
* })
|
|
121
|
+
* }
|
|
122
|
+
*/
|
|
123
|
+
function _errorDataAppend(err, data) {
|
|
124
|
+
err.data = {
|
|
125
|
+
...err.data,
|
|
126
|
+
...data,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
exports._errorDataAppend = _errorDataAppend;
|
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,18 @@
|
|
|
1
|
+
export interface ReadingTimeOptions {
|
|
2
|
+
wordBound?: (char: string) => boolean;
|
|
3
|
+
wordsPerMinute?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ReadingTimeStats {
|
|
6
|
+
time: number;
|
|
7
|
+
minutes: number;
|
|
8
|
+
}
|
|
9
|
+
export interface WordCountStats {
|
|
10
|
+
total: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ReadingTimeResult extends ReadingTimeStats {
|
|
13
|
+
words: WordCountStats;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* API: https://github.com/ngryman/reading-time
|
|
17
|
+
*/
|
|
18
|
+
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;
|
|
@@ -96,3 +96,18 @@ export function _isHttpErrorObject(o) {
|
|
|
96
96
|
export function _isErrorObject(o) {
|
|
97
97
|
return (!!o && typeof o.name === 'string' && typeof o.message === 'string' && typeof o.data === 'object');
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Convenience function to safely add properties to Error's `data` object
|
|
101
|
+
* (even if it wasn't previously existing)
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
*
|
|
105
|
+
* try {} catch (err) {
|
|
106
|
+
* _errorDataAppend(err, {
|
|
107
|
+
* httpStatusCode: 401,
|
|
108
|
+
* })
|
|
109
|
+
* }
|
|
110
|
+
*/
|
|
111
|
+
export function _errorDataAppend(err, data) {
|
|
112
|
+
err.data = Object.assign(Object.assign({}, err.data), data);
|
|
113
|
+
}
|
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/package.json
CHANGED
package/src/error/error.util.ts
CHANGED
|
@@ -145,3 +145,22 @@ export function _isErrorObject(o: any): o is ErrorObject {
|
|
|
145
145
|
!!o && typeof o.name === 'string' && typeof o.message === 'string' && typeof o.data === 'object'
|
|
146
146
|
)
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Convenience function to safely add properties to Error's `data` object
|
|
151
|
+
* (even if it wasn't previously existing)
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
*
|
|
155
|
+
* try {} catch (err) {
|
|
156
|
+
* _errorDataAppend(err, {
|
|
157
|
+
* httpStatusCode: 401,
|
|
158
|
+
* })
|
|
159
|
+
* }
|
|
160
|
+
*/
|
|
161
|
+
export function _errorDataAppend(err: any, data: ErrorData): void {
|
|
162
|
+
err.data = {
|
|
163
|
+
...err.data,
|
|
164
|
+
...data,
|
|
165
|
+
}
|
|
166
|
+
}
|
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,138 @@
|
|
|
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
|
+
export interface ReadingTimeOptions {
|
|
13
|
+
wordBound?: (char: string) => boolean
|
|
14
|
+
wordsPerMinute?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ReadingTimeStats {
|
|
18
|
+
time: number
|
|
19
|
+
minutes: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WordCountStats {
|
|
23
|
+
total: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReadingTimeResult extends ReadingTimeStats {
|
|
27
|
+
words: WordCountStats
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type WordBoundFunction = (char: string) => boolean
|
|
31
|
+
|
|
32
|
+
function codeIsInRanges(num: number, arrayOfRanges: number[][]) {
|
|
33
|
+
return arrayOfRanges.some(([lowerBound, upperBound]) => lowerBound! <= num && num <= upperBound!)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isCJK: WordBoundFunction = c => {
|
|
37
|
+
const charCode = c.codePointAt(0)!
|
|
38
|
+
// Help wanted!
|
|
39
|
+
// This should be good for most cases, but if you find it unsatisfactory
|
|
40
|
+
// (e.g. some other language where each character should be standalone words),
|
|
41
|
+
// contributions welcome!
|
|
42
|
+
return codeIsInRanges(charCode, [
|
|
43
|
+
// Hiragana (Katakana not included on purpose,
|
|
44
|
+
// context: https://github.com/ngryman/reading-time/pull/35#issuecomment-853364526)
|
|
45
|
+
// If you think Katakana should be included and have solid reasons, improvement is welcomed
|
|
46
|
+
[0x3040, 0x309f],
|
|
47
|
+
// CJK Unified ideographs
|
|
48
|
+
[0x4e00, 0x9fff],
|
|
49
|
+
// Hangul
|
|
50
|
+
[0xac00, 0xd7a3],
|
|
51
|
+
// CJK extensions
|
|
52
|
+
[0x20000, 0x2ebe0],
|
|
53
|
+
])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isAnsiWordBound: WordBoundFunction = c => {
|
|
57
|
+
return ' \n\r\t'.includes(c)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isPunctuation: WordBoundFunction = c => {
|
|
61
|
+
const charCode = c.codePointAt(0)!
|
|
62
|
+
return codeIsInRanges(charCode, [
|
|
63
|
+
[0x21, 0x2f],
|
|
64
|
+
[0x3a, 0x40],
|
|
65
|
+
[0x5b, 0x60],
|
|
66
|
+
[0x7b, 0x7e],
|
|
67
|
+
// CJK Symbols and Punctuation
|
|
68
|
+
[0x3000, 0x303f],
|
|
69
|
+
// Full-width ASCII punctuation variants
|
|
70
|
+
[0xff00, 0xffef],
|
|
71
|
+
])
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function countWords(text: string, options: ReadingTimeOptions = {}): WordCountStats {
|
|
75
|
+
let words = 0
|
|
76
|
+
let start = 0
|
|
77
|
+
let end = text.length - 1
|
|
78
|
+
const isWordBound = options.wordBound || isAnsiWordBound
|
|
79
|
+
|
|
80
|
+
// fetch bounds
|
|
81
|
+
while (isWordBound(text[start]!)) start++
|
|
82
|
+
while (isWordBound(text[end]!)) end--
|
|
83
|
+
|
|
84
|
+
// Add a trailing word bound to make handling edges more convenient
|
|
85
|
+
const normalizedText = `${text}\n`
|
|
86
|
+
|
|
87
|
+
// calculate the number of words
|
|
88
|
+
for (let i = start; i <= end; i++) {
|
|
89
|
+
// A CJK character is a always word;
|
|
90
|
+
// A non-word bound followed by a word bound / CJK is the end of a word.
|
|
91
|
+
if (
|
|
92
|
+
isCJK(normalizedText[i]!) ||
|
|
93
|
+
(!isWordBound(normalizedText[i]!) &&
|
|
94
|
+
(isWordBound(normalizedText[i + 1]!) || isCJK(normalizedText[i + 1]!)))
|
|
95
|
+
) {
|
|
96
|
+
words++
|
|
97
|
+
}
|
|
98
|
+
// In case of CJK followed by punctuations, those characters have to be eaten as well
|
|
99
|
+
if (isCJK(normalizedText[i]!)) {
|
|
100
|
+
while (
|
|
101
|
+
i <= end &&
|
|
102
|
+
(isPunctuation(normalizedText[i + 1]!) || isWordBound(normalizedText[i + 1]!))
|
|
103
|
+
) {
|
|
104
|
+
i++
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { total: words }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readingTimeWithCount(
|
|
112
|
+
words: WordCountStats,
|
|
113
|
+
options: ReadingTimeOptions = {},
|
|
114
|
+
): ReadingTimeStats {
|
|
115
|
+
const { wordsPerMinute = 200 } = options
|
|
116
|
+
// reading time stats
|
|
117
|
+
const minutes = words.total / wordsPerMinute
|
|
118
|
+
// Math.round used to resolve floating point funkiness
|
|
119
|
+
// http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
|
|
120
|
+
const time = Math.round(minutes * 60 * 1000)
|
|
121
|
+
const displayed = Math.ceil(parseFloat(minutes.toFixed(2)))
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
minutes: displayed,
|
|
125
|
+
time,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* API: https://github.com/ngryman/reading-time
|
|
131
|
+
*/
|
|
132
|
+
export function readingTime(text: string, options: ReadingTimeOptions = {}): ReadingTimeResult {
|
|
133
|
+
const words = countWords(text, options)
|
|
134
|
+
return {
|
|
135
|
+
...readingTimeWithCount(words, options),
|
|
136
|
+
words,
|
|
137
|
+
}
|
|
138
|
+
}
|