@naturalcycles/js-lib 14.107.0 → 14.108.1
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 +1 -1
- package/dist/error/error.util.js +0 -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 +29 -0
- package/dist/string/readingTime.js +106 -0
- package/dist-esm/error/error.util.js +0 -1
- 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 +3 -3
- 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/dist/error/error.util.js
CHANGED
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-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
|
@@ -158,9 +158,9 @@ export function _isErrorObject(o: any): o is ErrorObject {
|
|
|
158
158
|
* })
|
|
159
159
|
* }
|
|
160
160
|
*/
|
|
161
|
-
export function _errorDataAppend(err:
|
|
162
|
-
|
|
163
|
-
...
|
|
161
|
+
export function _errorDataAppend(err: any, data: ErrorData): void {
|
|
162
|
+
err.data = {
|
|
163
|
+
...err.data,
|
|
164
164
|
...data,
|
|
165
165
|
}
|
|
166
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,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
|
+
}
|