@hebcal/icalendar 7.0.2 → 7.0.4

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/foldLine.js CHANGED
@@ -1,6 +1,28 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  let foldSegmenter;
3
- const char74re = /.{1,74}/g;
3
+ /**
4
+ * Number of UTF-8 octets used to encode the code point starting at `i`.
5
+ * Returns 4 for a surrogate pair (which occupies two UTF-16 units).
6
+ */
7
+ function octetsAt(line, i) {
8
+ const c = line.charCodeAt(i);
9
+ if (c < 0x80) {
10
+ return 1;
11
+ }
12
+ if (c < 0x800) {
13
+ return 2;
14
+ }
15
+ // A high surrogate starts a 4-octet code point; an unpaired surrogate
16
+ // still encodes as 3 octets, but WHATWG replaces it with U+FFFD, which
17
+ // is also 3 octets, so the count stays correct either way.
18
+ if (c >= 0xd800 && c <= 0xdbff && i + 1 < line.length) {
19
+ const lo = line.charCodeAt(i + 1);
20
+ if (lo >= 0xdc00 && lo <= 0xdfff) {
21
+ return 4;
22
+ }
23
+ }
24
+ return 3;
25
+ }
4
26
  export function foldLine(line) {
5
27
  const lineBytes = Buffer.byteLength(line);
6
28
  if (lineBytes <= 74) {
@@ -9,38 +31,58 @@ export function foldLine(line) {
9
31
  if (lineBytes === line.length) {
10
32
  // All-ASCII fast path: every UTF-16 unit encodes to one UTF-8 byte
11
33
  // iff the string is pure ASCII, so we can split by JS chars.
12
- return line.match(char74re).join('\r\n ');
34
+ // A /.{1,74}/g regex looks tempting here but is wrong: `.` does not
35
+ // match \r or \n, so it silently drops those characters and returns
36
+ // null outright for a line built only from them.
37
+ const parts = [];
38
+ for (let i = 0; i < line.length; i += 74) {
39
+ parts.push(line.slice(i, i + 74));
40
+ }
41
+ return parts.join('\r\n ');
13
42
  }
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.
43
+ // Fold on code-point boundaries so a multi-octet UTF-8 sequence is never
44
+ // split, then pull each break back to the start of its grapheme cluster
45
+ // so combining marks, ZWJ sequences and regional-indicator pairs stay
46
+ // with their base character.
47
+ //
48
+ // Only the break positions need the Segmenter, and there are at most
49
+ // lineBytes/74 of them, so we ask it about those offsets via
50
+ // `containing()` rather than iterating every cluster in the line --
51
+ // the same fold points for a fraction of the work.
18
52
  if (!foldSegmenter) {
19
53
  foldSegmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
20
54
  }
55
+ const segments = foldSegmenter.segment(line);
21
56
  const parts = [];
57
+ const n = line.length;
22
58
  let chunkStart = 0;
23
59
  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
- }
60
+ let i = 0;
61
+ while (i < n) {
62
+ const octets = octetsAt(line, i);
63
+ const units = octets === 4 ? 2 : 1;
36
64
  if (len + octets >= 75) {
37
- parts.push(line.slice(chunkStart, index));
38
- chunkStart = index;
39
- len = octets;
65
+ let brk = i;
66
+ if (i > chunkStart) {
67
+ const cluster = segments.containing(i);
68
+ // Back up to the cluster start, unless that would consume the whole
69
+ // chunk -- a single cluster wider than the limit has to be split.
70
+ if (cluster && cluster.index > chunkStart && cluster.index < i) {
71
+ brk = cluster.index;
72
+ }
73
+ }
74
+ parts.push(line.slice(chunkStart, brk));
75
+ chunkStart = brk;
76
+ // Anything we backed up over carries into the next chunk.
77
+ len = 0;
78
+ for (let k = brk; k < i + units; k += octetsAt(line, k) === 4 ? 2 : 1) {
79
+ len += octetsAt(line, k);
80
+ }
40
81
  }
41
82
  else {
42
83
  len += octets;
43
84
  }
85
+ i += units;
44
86
  }
45
87
  parts.push(line.slice(chunkStart));
46
88
  return parts.join('\r\n ');
package/dist/icalendar.js CHANGED
@@ -59,6 +59,37 @@ const DAILY_LEARNING = flags.DAILY_LEARNING |
59
59
  flags.MISHNA_YOMI |
60
60
  flags.YERUSHALMI_YOMI |
61
61
  flags.NACH_YOMI;
62
+ /** Formats a date-time string as `YYYYMMDDTHHMMSS` */
63
+ function rfc5545DateTime(dt, hour, minute) {
64
+ return IcalEvent.formatYYYYMMDD(dt) + 'T' + pad2(hour) + pad2(minute) + '00';
65
+ }
66
+ /**
67
+ * Computes the DTSTART/DTEND date-time strings (in `YYYYMMDDTHHMMSS`
68
+ * format) for a timed event, adding `duration` minutes (if any) to the
69
+ * wall-clock start time. Adding to the wall clock (rather than to the
70
+ * underlying instant) keeps the result in the same timezone as DTSTART
71
+ * regardless of the host machine's own local timezone.
72
+ * @private
73
+ */
74
+ function formatTimedStartEnd(ev) {
75
+ const [hourStr, minuteStr] = ev.eventTimeStr.split(':');
76
+ const hour = +hourStr;
77
+ const minute = +minuteStr;
78
+ const dt = ev.greg();
79
+ const startDate = rfc5545DateTime(dt, hour, minute);
80
+ if (!ev.duration) {
81
+ return { startDate, endDate: startDate };
82
+ }
83
+ const totalMinutes = hour * 60 + minute + ev.duration;
84
+ const endHour = Math.floor(totalMinutes / 60) % 24;
85
+ const endMinute = totalMinutes % 60;
86
+ const dayOffset = Math.floor(totalMinutes / 1440);
87
+ if (dayOffset) {
88
+ dt.setDate(dt.getDate() + dayOffset);
89
+ }
90
+ const endDate = rfc5545DateTime(dt, endHour, endMinute);
91
+ return { startDate, endDate };
92
+ }
62
93
  /**
63
94
  * Represents an RFC 2445 iCalendar VEVENT
64
95
  */
@@ -117,11 +148,9 @@ export class IcalEvent {
117
148
  this.transp = 'TRANSPARENT';
118
149
  this.busyStatus = 'FREE';
119
150
  if (timed) {
120
- let [hour, minute] = ev0.eventTimeStr.split(':');
121
- hour = +hour;
122
- minute = +minute;
123
- this.startDate += 'T' + pad2(hour) + pad2(minute) + '00';
124
- this.endDate = this.startDate;
151
+ const { startDate, endDate } = formatTimedStartEnd(ev);
152
+ this.startDate = startDate;
153
+ this.endDate = endDate;
125
154
  if (location?.getTzid()) {
126
155
  this.dtargs = `;TZID=${location.getTzid()}`;
127
156
  }
@@ -1 +1 @@
1
- export declare const version = "7.0.2";
1
+ export declare const version = "7.0.4";
@@ -1,2 +1,2 @@
1
1
  // DO NOT EDIT THIS AUTO-GENERATED FILE!
2
- export const version = '7.0.2';
2
+ export const version = '7.0.4';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hebcal/icalendar",
3
- "version": "7.0.2",
3
+ "version": "7.0.4",
4
4
  "author": "Michael J. Radwin (https://github.com/mjradwin)",
5
5
  "keywords": [
6
6
  "ical",
@@ -30,10 +30,10 @@
30
30
  },
31
31
  "homepage": "https://hebcal.github.io/api/icalendar/",
32
32
  "dependencies": {
33
- "@hebcal/core": "^6.9.1",
34
- "@hebcal/hdate": "^0.22.7",
33
+ "@hebcal/core": "^6.9.3",
34
+ "@hebcal/hdate": "^0.22.8",
35
35
  "@hebcal/murmurhash3": "^1.0.0",
36
- "@hebcal/rest-api": "^7.1.1"
36
+ "@hebcal/rest-api": "^7.1.3"
37
37
  },
38
38
  "scripts": {
39
39
  "build:version": "node ./version.js package.json src/pkgVersion.ts",
@@ -48,11 +48,11 @@
48
48
  },
49
49
  "license": "BSD-2-Clause",
50
50
  "devDependencies": {
51
- "@hebcal/learning": "^6.9.5",
52
- "@types/node": "26.1.2",
53
- "oxlint": "^1.76.0",
54
- "prettier": "^3.9.6",
51
+ "@hebcal/learning": "^6.11.3",
52
+ "@types/node": "26.6.2",
53
+ "oxlint": "^1.83.0",
54
+ "prettier": "^3.9.8",
55
55
  "typescript": "^7.0.2",
56
- "vitest": "^4.1.10"
56
+ "vitest": "^5.0.1"
57
57
  }
58
58
  }