@hebcal/icalendar 6.3.7 → 7.0.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/README.md +4 -1
- package/dist/foldLine.d.ts +1 -0
- package/dist/foldLine.js +47 -0
- package/dist/icalendar.d.ts +21 -14
- package/dist/icalendar.js +23 -142
- package/dist/pkgVersion.d.ts +1 -1
- package/dist/pkgVersion.js +1 -4
- package/package.json +20 -21
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# @hebcal/icalendar
|
|
2
|
+
|
|
2
3
|
Jewish holidays and Hebrew calendar as iCalendar RFC 2445
|
|
3
4
|
|
|
4
5
|
## Installation
|
|
6
|
+
|
|
5
7
|
```bash
|
|
6
8
|
$ npm install @hebcal/icalendar
|
|
7
9
|
```
|
|
8
10
|
|
|
9
11
|
## Synopsis
|
|
12
|
+
|
|
10
13
|
```javascript
|
|
11
14
|
import {HebrewCalendar, Location} from '@hebcal/core';
|
|
12
15
|
import {eventsToIcalendar} from '@hebcal/icalendar';
|
|
@@ -24,7 +27,7 @@ const events = HebrewCalendar.calendar(options);
|
|
|
24
27
|
const str = await eventsToIcalendar(events, {
|
|
25
28
|
locale: 'he',
|
|
26
29
|
...options,
|
|
27
|
-
})
|
|
30
|
+
});
|
|
28
31
|
|
|
29
32
|
const icalStream = fs.createWriteStream('feed.ics');
|
|
30
33
|
icalStream.write(str);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function foldLine(line: string): string;
|
package/dist/foldLine.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
let foldSegmenter;
|
|
3
|
+
const char74re = /.{1,74}/g;
|
|
4
|
+
export function foldLine(line) {
|
|
5
|
+
const lineBytes = Buffer.byteLength(line);
|
|
6
|
+
if (lineBytes <= 74) {
|
|
7
|
+
return line;
|
|
8
|
+
}
|
|
9
|
+
if (lineBytes === line.length) {
|
|
10
|
+
// All-ASCII fast path: every UTF-16 unit encodes to one UTF-8 byte
|
|
11
|
+
// iff the string is pure ASCII, so we can split by JS chars.
|
|
12
|
+
return line.match(char74re).join('\r\n ');
|
|
13
|
+
}
|
|
14
|
+
// Walk grapheme clusters so we never split a multi-octet UTF-8
|
|
15
|
+
// sequence (or a combining/ZWJ/regional-indicator cluster) across
|
|
16
|
+
// a fold boundary. Reuse a single Segmenter to avoid per-call
|
|
17
|
+
// construction cost.
|
|
18
|
+
if (!foldSegmenter) {
|
|
19
|
+
foldSegmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
20
|
+
}
|
|
21
|
+
const parts = [];
|
|
22
|
+
let chunkStart = 0;
|
|
23
|
+
let len = 0;
|
|
24
|
+
for (const { segment, index } of foldSegmenter.segment(line)) {
|
|
25
|
+
const cp = segment.codePointAt(0);
|
|
26
|
+
// Most segments are a single code point; compute UTF-8 length
|
|
27
|
+
// arithmetically and skip the Buffer.byteLength call.
|
|
28
|
+
const cpUnits = cp >= 0x10000 ? 2 : 1;
|
|
29
|
+
let octets;
|
|
30
|
+
if (segment.length === cpUnits) {
|
|
31
|
+
octets = cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
octets = Buffer.byteLength(segment);
|
|
35
|
+
}
|
|
36
|
+
if (len + octets >= 75) {
|
|
37
|
+
parts.push(line.slice(chunkStart, index));
|
|
38
|
+
chunkStart = index;
|
|
39
|
+
len = octets;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
len += octets;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
parts.push(line.slice(chunkStart));
|
|
46
|
+
return parts.join('\r\n ');
|
|
47
|
+
}
|
package/dist/icalendar.d.ts
CHANGED
|
@@ -3,6 +3,13 @@ import { CalOptions } from '@hebcal/core/dist/esm/CalOptions';
|
|
|
3
3
|
export type ICalEventOptions = {
|
|
4
4
|
dtstamp?: string;
|
|
5
5
|
sequence?: number;
|
|
6
|
+
/**
|
|
7
|
+
* Text for the `DESCRIPTION` property, overriding `ev.memo`. This is
|
|
8
|
+
* per-event, so pass a fresh options object to each `IcalEvent` (for
|
|
9
|
+
* example `new IcalEvent(ev, {...opts, memo})`) rather than setting it on
|
|
10
|
+
* an options object shared by a whole calendar.
|
|
11
|
+
*/
|
|
12
|
+
memo?: string;
|
|
6
13
|
emoji?: boolean;
|
|
7
14
|
utmSource?: string;
|
|
8
15
|
utmMedium?: string;
|
|
@@ -22,20 +29,20 @@ export type ICalOptions = CalOptions & ICalEventOptions;
|
|
|
22
29
|
* Represents an RFC 2445 iCalendar VEVENT
|
|
23
30
|
*/
|
|
24
31
|
export declare class IcalEvent {
|
|
25
|
-
ev: Event;
|
|
26
|
-
options: ICalOptions;
|
|
27
|
-
dtstamp: string;
|
|
28
|
-
sequence?: number;
|
|
29
|
-
timed: boolean;
|
|
30
|
-
locationName: string | null | undefined;
|
|
31
|
-
startDate: string;
|
|
32
|
-
isoDateOnly: string;
|
|
33
|
-
dtargs: string;
|
|
34
|
-
transp: string;
|
|
35
|
-
busyStatus: string;
|
|
36
|
-
endDate: string;
|
|
37
|
-
subj: string;
|
|
38
|
-
category: string | null;
|
|
32
|
+
readonly ev: Event;
|
|
33
|
+
readonly options: ICalOptions;
|
|
34
|
+
readonly dtstamp: string;
|
|
35
|
+
readonly sequence?: number;
|
|
36
|
+
readonly timed: boolean;
|
|
37
|
+
readonly locationName: string | null | undefined;
|
|
38
|
+
readonly startDate: string;
|
|
39
|
+
readonly isoDateOnly: string;
|
|
40
|
+
readonly dtargs: string;
|
|
41
|
+
readonly transp: string;
|
|
42
|
+
readonly busyStatus: string;
|
|
43
|
+
readonly endDate: string;
|
|
44
|
+
readonly subj: string;
|
|
45
|
+
readonly category: string | null;
|
|
39
46
|
lines?: string[];
|
|
40
47
|
/**
|
|
41
48
|
* Builds an IcalEvent object from a Hebcal Event
|
package/dist/icalendar.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
/*! @hebcal/icalendar v6.3.7 */
|
|
2
|
-
import { Buffer } from 'node:buffer';
|
|
3
1
|
import { flags } from '@hebcal/core/dist/esm/event';
|
|
4
2
|
import { Locale } from '@hebcal/core/dist/esm/locale';
|
|
5
3
|
import { murmur32HexSync } from 'murmurhash3';
|
|
6
|
-
import { pad2,
|
|
7
|
-
import {
|
|
8
|
-
import { getHolidayDescription } from '@hebcal/rest-api/dist/esm/holiday';
|
|
9
|
-
import { makeTorahMemoText } from '@hebcal/rest-api/dist/esm/memo';
|
|
4
|
+
import { pad2, pad4, isDate } from '@hebcal/hdate';
|
|
5
|
+
import { getCalendarTitle, getEventCategories, shouldRenderBrief, } from '@hebcal/rest-api/dist/esm/common';
|
|
10
6
|
import { appendIsraelAndTracking } from '@hebcal/rest-api/dist/esm/url';
|
|
11
7
|
import { makeAnchor } from '@hebcal/rest-api/dist/esm/makeAnchor';
|
|
12
|
-
import { promises } from 'node:fs';
|
|
8
|
+
import { promises as fs } from 'node:fs';
|
|
13
9
|
import { version } from './pkgVersion.js';
|
|
14
|
-
|
|
10
|
+
import { foldLine } from './foldLine.js';
|
|
11
|
+
const ESC_NEWLINE = String.raw `\n`;
|
|
15
12
|
const vtimezoneCache = new Map();
|
|
16
13
|
const CATEGORY = {
|
|
17
14
|
candles: 'Holiday',
|
|
@@ -57,7 +54,6 @@ function appendTrackingToUrl(url, options) {
|
|
|
57
54
|
const utmCampaign = options.utmCampaign;
|
|
58
55
|
return appendIsraelAndTracking(url, options.il, utmSource, utmMedium, utmCampaign);
|
|
59
56
|
}
|
|
60
|
-
const char74re = /(.{1,74})/g;
|
|
61
57
|
const DAILY_LEARNING = flags.DAILY_LEARNING |
|
|
62
58
|
flags.DAF_YOMI |
|
|
63
59
|
flags.MISHNA_YOMI |
|
|
@@ -66,7 +62,7 @@ const DAILY_LEARNING = flags.DAILY_LEARNING |
|
|
|
66
62
|
/**
|
|
67
63
|
* Represents an RFC 2445 iCalendar VEVENT
|
|
68
64
|
*/
|
|
69
|
-
class IcalEvent {
|
|
65
|
+
export class IcalEvent {
|
|
70
66
|
ev;
|
|
71
67
|
options;
|
|
72
68
|
dtstamp;
|
|
@@ -161,7 +157,9 @@ class IcalEvent {
|
|
|
161
157
|
}
|
|
162
158
|
}
|
|
163
159
|
this.subj = subj;
|
|
164
|
-
this.category =
|
|
160
|
+
this.category =
|
|
161
|
+
ev0.category ||
|
|
162
|
+
(CATEGORY[getEventCategories(ev)?.[0]] ?? null);
|
|
165
163
|
}
|
|
166
164
|
getAlarm() {
|
|
167
165
|
const ev = this.ev;
|
|
@@ -236,9 +234,13 @@ class IcalEvent {
|
|
|
236
234
|
arr.push('CLASS:PUBLIC');
|
|
237
235
|
}
|
|
238
236
|
const options = this.options;
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
237
|
+
// The caller is responsible for building the memo (holiday description,
|
|
238
|
+
// Torah reading, URL, etc), either as `options.memo` or on the event
|
|
239
|
+
// itself; we only escape newlines for RFC 5545
|
|
240
|
+
const memo = options.memo ?? ev.memo;
|
|
241
|
+
if (memo) {
|
|
242
|
+
addOptional(arr, 'DESCRIPTION', memo.replaceAll('\n', ESC_NEWLINE));
|
|
243
|
+
}
|
|
242
244
|
addOptional(arr, 'LOCATION', this.locationName);
|
|
243
245
|
const loc = options.location;
|
|
244
246
|
if (this.timed && loc) {
|
|
@@ -266,50 +268,13 @@ class IcalEvent {
|
|
|
266
268
|
* fold lines to 75 characters
|
|
267
269
|
*/
|
|
268
270
|
getLines() {
|
|
269
|
-
return this.getLongLines().map(
|
|
271
|
+
return this.getLongLines().map(foldLine);
|
|
270
272
|
}
|
|
271
273
|
/**
|
|
272
274
|
* fold line to 75 characters
|
|
273
275
|
*/
|
|
274
276
|
static fold(line) {
|
|
275
|
-
|
|
276
|
-
return line;
|
|
277
|
-
}
|
|
278
|
-
let isASCII = true;
|
|
279
|
-
for (let i = 0; i < line.length; i++) {
|
|
280
|
-
if (line.codePointAt(i) > 255) {
|
|
281
|
-
isASCII = false;
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
if (isASCII) {
|
|
286
|
-
// It's longer than 74 octets, and all characters are ASCII, so we
|
|
287
|
-
// use a simple regex to split it into chunks of 74 characters
|
|
288
|
-
const matches = line.match(char74re);
|
|
289
|
-
return matches.join('\r\n ');
|
|
290
|
-
}
|
|
291
|
-
// iterate unicode character by character, making sure
|
|
292
|
-
// that adding a new character would keep the line <= 75 octets
|
|
293
|
-
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
|
|
294
|
-
const segments = segmenter.segment(line);
|
|
295
|
-
const chars = Array.from(segments, s => s.segment);
|
|
296
|
-
let result = '';
|
|
297
|
-
let current = '';
|
|
298
|
-
let len = 0;
|
|
299
|
-
for (const ch of chars) {
|
|
300
|
-
const octets = ch.codePointAt(0) < 256 ? 1 : Buffer.byteLength(ch);
|
|
301
|
-
const newlen = len + octets;
|
|
302
|
-
if (newlen < 75) {
|
|
303
|
-
current += ch;
|
|
304
|
-
len = newlen;
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
result += current + '\r\n ';
|
|
308
|
-
current = ch;
|
|
309
|
-
len = octets;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return result + current;
|
|
277
|
+
return foldLine(line);
|
|
313
278
|
}
|
|
314
279
|
static escape(str) {
|
|
315
280
|
if (str.includes(',')) {
|
|
@@ -343,96 +308,14 @@ class IcalEvent {
|
|
|
343
308
|
* Transforms a single Event into a VEVENT string
|
|
344
309
|
* @returns multi-line result, delimited by \r\n
|
|
345
310
|
*/
|
|
346
|
-
function eventToIcal(ev, options) {
|
|
311
|
+
export function eventToIcal(ev, options) {
|
|
347
312
|
const ical = new IcalEvent(ev, options);
|
|
348
313
|
return ical.toString();
|
|
349
314
|
}
|
|
350
|
-
const torahMemoCache = new Map();
|
|
351
|
-
const HOLIDAY_IGNORE_MASK = DAILY_LEARNING |
|
|
352
|
-
flags.OMER_COUNT |
|
|
353
|
-
flags.SHABBAT_MEVARCHIM |
|
|
354
|
-
flags.MOLAD |
|
|
355
|
-
flags.USER_EVENT |
|
|
356
|
-
flags.HEBREW_DATE;
|
|
357
|
-
const ESC_NEWLINE = String.raw `\n`;
|
|
358
|
-
const DBL_NEWLINE = ESC_NEWLINE + ESC_NEWLINE;
|
|
359
|
-
/**
|
|
360
|
-
* @private
|
|
361
|
-
*/
|
|
362
|
-
function makeTorahMemo(ev, il) {
|
|
363
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
364
|
-
if (ev.getFlags() & HOLIDAY_IGNORE_MASK || ev.eventTime) {
|
|
365
|
-
return '';
|
|
366
|
-
}
|
|
367
|
-
const hd = ev.getDate();
|
|
368
|
-
const yy = hd.getFullYear();
|
|
369
|
-
const mm = hd.getMonth();
|
|
370
|
-
const dd = hd.getDate();
|
|
371
|
-
const key = [yy, mm, dd, il ? '1' : '0', ev.getDesc()].join('-');
|
|
372
|
-
let memo = torahMemoCache.get(key);
|
|
373
|
-
if (typeof memo === 'string') {
|
|
374
|
-
return memo;
|
|
375
|
-
}
|
|
376
|
-
memo = makeTorahMemoText(ev, il).replaceAll('\n', ESC_NEWLINE);
|
|
377
|
-
torahMemoCache.set(key, memo);
|
|
378
|
-
return memo;
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* @private
|
|
382
|
-
*/
|
|
383
|
-
function createMemo(ev, options) {
|
|
384
|
-
let memo = ev.memo || '';
|
|
385
|
-
if (memo.length && memo.includes('\n')) {
|
|
386
|
-
memo = memo.replaceAll('\n', ESC_NEWLINE);
|
|
387
|
-
}
|
|
388
|
-
const desc = ev.getDesc();
|
|
389
|
-
if (desc === 'Havdalah' || desc === 'Candle lighting') {
|
|
390
|
-
return memo;
|
|
391
|
-
}
|
|
392
|
-
const mask = ev.getFlags();
|
|
393
|
-
if (mask & flags.OMER_COUNT) {
|
|
394
|
-
const omerEv = ev;
|
|
395
|
-
const sefira = [
|
|
396
|
-
omerEv.sefira('en'),
|
|
397
|
-
omerEv.sefira('he'),
|
|
398
|
-
omerEv.sefira('translit'),
|
|
399
|
-
].join(ESC_NEWLINE);
|
|
400
|
-
return (omerEv.getTodayIs('en') +
|
|
401
|
-
DBL_NEWLINE +
|
|
402
|
-
omerEv.getTodayIs('he') +
|
|
403
|
-
DBL_NEWLINE +
|
|
404
|
-
sefira);
|
|
405
|
-
}
|
|
406
|
-
if (!memo) {
|
|
407
|
-
memo = getHolidayDescription(ev);
|
|
408
|
-
}
|
|
409
|
-
if (!memo) {
|
|
410
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
411
|
-
const linkEv = ev.linkedEvent;
|
|
412
|
-
if (linkEv && linkEv.getDesc() !== ev.getDesc()) {
|
|
413
|
-
memo = linkEv.render(options.locale);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
const torahMemo = makeTorahMemo(ev, options.il);
|
|
417
|
-
if (torahMemo) {
|
|
418
|
-
if (memo.length) {
|
|
419
|
-
memo += DBL_NEWLINE;
|
|
420
|
-
}
|
|
421
|
-
memo += torahMemo;
|
|
422
|
-
}
|
|
423
|
-
const url = appendTrackingToUrl(ev.url(), options);
|
|
424
|
-
if (url) {
|
|
425
|
-
if (memo.length) {
|
|
426
|
-
memo += DBL_NEWLINE;
|
|
427
|
-
}
|
|
428
|
-
memo += url;
|
|
429
|
-
}
|
|
430
|
-
return memo;
|
|
431
|
-
}
|
|
432
315
|
/**
|
|
433
316
|
* Generates an RFC 2445 iCalendar string from an array of events
|
|
434
317
|
*/
|
|
435
|
-
async function eventsToIcalendar(events, options) {
|
|
318
|
+
export async function eventsToIcalendar(events, options) {
|
|
436
319
|
if (!events.length)
|
|
437
320
|
throw new RangeError('Events can not be empty');
|
|
438
321
|
if (!options)
|
|
@@ -461,7 +344,7 @@ async function getVtimezone(tzid) {
|
|
|
461
344
|
}
|
|
462
345
|
const filename = `./zoneinfo/${tzid}.ics`;
|
|
463
346
|
try {
|
|
464
|
-
const contents = await
|
|
347
|
+
const contents = await fs.readFile(filename, 'utf-8');
|
|
465
348
|
const lines = contents.split('\r\n');
|
|
466
349
|
// ignore first 3 and last 1 lines
|
|
467
350
|
const str = lines.slice(3, lines.length - 2).join('\r\n');
|
|
@@ -510,14 +393,14 @@ function makeIcalPreamble(opts) {
|
|
|
510
393
|
/**
|
|
511
394
|
* Generates an RFC 2445 iCalendar string from an array of IcalEvents
|
|
512
395
|
*/
|
|
513
|
-
async function icalEventsToString(icals, options) {
|
|
396
|
+
export async function icalEventsToString(icals, options) {
|
|
514
397
|
if (!icals.length)
|
|
515
398
|
throw new RangeError('Events can not be empty');
|
|
516
399
|
if (!options)
|
|
517
400
|
throw new TypeError('Invalid options object');
|
|
518
401
|
const stream = [];
|
|
519
402
|
const preamble = makeIcalPreamble(options);
|
|
520
|
-
for (const line of preamble.map(
|
|
403
|
+
for (const line of preamble.map(foldLine)) {
|
|
521
404
|
stream.push(line);
|
|
522
405
|
stream.push('\r\n');
|
|
523
406
|
}
|
|
@@ -541,5 +424,3 @@ async function icalEventsToString(icals, options) {
|
|
|
541
424
|
stream.push('END:VCALENDAR\r\n');
|
|
542
425
|
return stream.join('');
|
|
543
426
|
}
|
|
544
|
-
|
|
545
|
-
export { IcalEvent, eventToIcal, eventsToIcalendar, icalEventsToString };
|
package/dist/pkgVersion.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "
|
|
1
|
+
export declare const version = "7.0.0";
|
package/dist/pkgVersion.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hebcal/icalendar",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"author": "Michael J. Radwin (https://github.com/mjradwin)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ical",
|
|
@@ -30,33 +30,32 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://hebcal.github.io/api/icalendar/",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@hebcal/core": "^6.
|
|
34
|
-
"@hebcal/hdate": "^0.22.
|
|
35
|
-
"@hebcal/rest-api": "^
|
|
36
|
-
"murmurhash3": "^0.5.0"
|
|
37
|
-
"tslib": "^2.8.1"
|
|
33
|
+
"@hebcal/core": "^6.8.2",
|
|
34
|
+
"@hebcal/hdate": "^0.22.5",
|
|
35
|
+
"@hebcal/rest-api": "^7.0.0",
|
|
36
|
+
"murmurhash3": "^0.5.0"
|
|
38
37
|
},
|
|
39
38
|
"scripts": {
|
|
40
|
-
"build:
|
|
41
|
-
"build
|
|
42
|
-
"build": "npm run build:version && npm run build:rollup",
|
|
39
|
+
"build:version": "node ./version.js package.json src/pkgVersion.ts",
|
|
40
|
+
"build": "npm run build:version && tsc",
|
|
43
41
|
"pretest": "npm run build",
|
|
44
|
-
"docs": "typedoc",
|
|
42
|
+
"docs": "npx -y typedoc",
|
|
45
43
|
"test": "vitest",
|
|
46
|
-
"lint": "
|
|
47
|
-
"clean": "
|
|
44
|
+
"lint": "oxlint && prettier --check .",
|
|
45
|
+
"clean": "rm -rf dist",
|
|
48
46
|
"compile": "tsc",
|
|
49
|
-
"fix": "
|
|
47
|
+
"fix": "oxlint --fix && prettier --write ."
|
|
50
48
|
},
|
|
51
49
|
"license": "BSD-2-Clause",
|
|
52
50
|
"devDependencies": {
|
|
53
|
-
"@hebcal/learning": "^6.
|
|
54
|
-
"@
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
"@hebcal/learning": "^6.9.5",
|
|
52
|
+
"@types/node": "26.1.1",
|
|
53
|
+
"oxlint": "^1.76.0",
|
|
54
|
+
"prettier": "^3.9.6",
|
|
55
|
+
"typescript": "^7.0.2",
|
|
56
|
+
"vitest": "^4.1.10"
|
|
57
|
+
},
|
|
58
|
+
"allowScripts": {
|
|
59
|
+
"murmurhash3": true
|
|
61
60
|
}
|
|
62
61
|
}
|