Software zum Installieren eines Smart-Mirror Frameworks , zum Nutzen von hochschulrelevanten Informationen, auf einem Raspberry-Pi.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ical.js 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. /* eslint-disable max-depth, max-params, no-warning-comments, complexity */
  2. const {v4: uuid} = require('uuid');
  3. const moment = require('moment-timezone');
  4. const rrule = require('rrule').RRule;
  5. /** **************
  6. * A tolerant, minimal icalendar parser
  7. * (http://tools.ietf.org/html/rfc5545)
  8. *
  9. * <peterbraden@peterbraden.co.uk>
  10. * ************* */
  11. // Unescape Text re RFC 4.3.11
  12. const text = function (t = '') {
  13. return t
  14. .replace(/\\,/g, ',')
  15. .replace(/\\;/g, ';')
  16. .replace(/\\[nN]/g, '\n')
  17. .replace(/\\\\/g, '\\');
  18. };
  19. const parseValue = function (value) {
  20. if (value === 'TRUE') {
  21. return true;
  22. }
  23. if (value === 'FALSE') {
  24. return false;
  25. }
  26. const number = Number(value);
  27. if (!Number.isNaN(number)) {
  28. return number;
  29. }
  30. return value;
  31. };
  32. const parseParameters = function (p) {
  33. const out = {};
  34. for (const element of p) {
  35. if (element.includes('=')) {
  36. const segs = element.split('=');
  37. out[segs[0]] = parseValue(segs.slice(1).join('='));
  38. }
  39. }
  40. // Sp is not defined in this scope, typo?
  41. // original code from peterbraden
  42. // return out || sp;
  43. return out;
  44. };
  45. const storeValueParameter = function (name) {
  46. return function (value, curr) {
  47. const current = curr[name];
  48. if (Array.isArray(current)) {
  49. current.push(value);
  50. return curr;
  51. }
  52. if (typeof current === 'undefined') {
  53. curr[name] = value;
  54. } else {
  55. curr[name] = [current, value];
  56. }
  57. return curr;
  58. };
  59. };
  60. const storeParameter = function (name) {
  61. return function (value, parameters, curr) {
  62. const data = parameters && parameters.length > 0 && !(parameters.length === 1 && parameters[0] === 'CHARSET=utf-8') ? {params: parseParameters(parameters), val: text(value)} : text(value);
  63. return storeValueParameter(name)(data, curr);
  64. };
  65. };
  66. const addTZ = function (dt, parameters) {
  67. const p = parseParameters(parameters);
  68. if (dt.tz) {
  69. // Date already has a timezone property
  70. return dt;
  71. }
  72. if (parameters && p && dt) {
  73. dt.tz = p.TZID;
  74. if (dt.tz !== undefined) {
  75. // Remove surrouding quotes if found at the begining and at the end of the string
  76. // (Occurs when parsing Microsoft Exchange events containing TZID with Windows standard format instead IANA)
  77. dt.tz = dt.tz.replace(/^"(.*)"$/, '$1');
  78. }
  79. }
  80. return dt;
  81. };
  82. let zoneTable = null;
  83. function getIanaTZFromMS(msTZName) {
  84. if (!zoneTable) {
  85. const p = require('path');
  86. zoneTable = require(p.join(__dirname, 'windowsZones.json'));
  87. }
  88. // Get hash entry
  89. const he = zoneTable[msTZName];
  90. // If found return iana name, else null
  91. return he ? he.iana[0] : null;
  92. }
  93. function isDateOnly(value, parameters) {
  94. const dateOnly = ((parameters && parameters.includes('VALUE=DATE') && !parameters.includes('VALUE=DATE-TIME')) || /^\d{8}$/.test(value) === true);
  95. return dateOnly;
  96. }
  97. const typeParameter = function (name) {
  98. // Typename is not used in this function?
  99. return function (value, parameters, curr) {
  100. const returnValue = isDateOnly(value, parameters) ? 'date' : 'date-time';
  101. return storeValueParameter(name)(returnValue, curr);
  102. };
  103. };
  104. const dateParameter = function (name) {
  105. return function (value, parameters, curr) {
  106. // The regex from main gets confued by extra :
  107. const pi = parameters.indexOf('TZID=tzone');
  108. if (pi >= 0) {
  109. // Correct the parameters with the part on the value
  110. parameters[pi] = parameters[pi] + ':' + value.split(':')[0];
  111. // Get the date from the field, other code uses the value parameter
  112. value = value.split(':')[1];
  113. }
  114. let newDate = text(value);
  115. // Process 'VALUE=DATE' and EXDATE
  116. if (isDateOnly(value, parameters)) {
  117. // Just Date
  118. const comps = /^(\d{4})(\d{2})(\d{2}).*$/.exec(value);
  119. if (comps !== null) {
  120. // No TZ info - assume same timezone as this computer
  121. newDate = new Date(comps[1], Number.parseInt(comps[2], 10) - 1, comps[3]);
  122. newDate.dateOnly = true;
  123. // Store as string - worst case scenario
  124. return storeValueParameter(name)(newDate, curr);
  125. }
  126. }
  127. // Typical RFC date-time format
  128. const comps = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/.exec(value);
  129. if (comps !== null) {
  130. if (comps[7] === 'Z') {
  131. // GMT
  132. newDate = new Date(
  133. Date.UTC(
  134. Number.parseInt(comps[1], 10),
  135. Number.parseInt(comps[2], 10) - 1,
  136. Number.parseInt(comps[3], 10),
  137. Number.parseInt(comps[4], 10),
  138. Number.parseInt(comps[5], 10),
  139. Number.parseInt(comps[6], 10)
  140. )
  141. );
  142. newDate.tz = 'Etc/UTC';
  143. } else if (parameters && parameters[0] && parameters[0].includes('TZID=') && parameters[0].split('=')[1]) {
  144. // Get the timeozone from trhe parameters TZID value
  145. let tz = parameters[0].split('=')[1];
  146. let found = '';
  147. let offset = '';
  148. // If this is the custom timezone from MS Outlook
  149. if (tz === 'tzone://Microsoft/Custom') {
  150. // Set it to the local timezone, cause we can't tell
  151. tz = moment.tz.guess();
  152. parameters[0] = 'TZID=' + tz;
  153. }
  154. // Remove quotes if found
  155. tz = tz.replace(/^"(.*)"$/, '$1');
  156. // Watch out for windows timezones
  157. if (tz && tz.includes(' ')) {
  158. const tz1 = getIanaTZFromMS(tz);
  159. if (tz1) {
  160. tz = tz1;
  161. // We have a confirmed timezone, dont use offset, may confuse DST/STD time
  162. offset = '';
  163. }
  164. }
  165. // Watch out for offset timezones
  166. // If the conversion above didn't find any matching IANA tz
  167. // And oiffset is still present
  168. if (tz && tz.startsWith('(')) {
  169. // Extract just the offset
  170. const regex = /[+|-]\d*:\d*/;
  171. offset = tz.match(regex);
  172. tz = null;
  173. found = offset;
  174. }
  175. // Timezone not confirmed yet
  176. if (found === '') {
  177. // Lookup tz
  178. found = moment.tz.names().find(zone => {
  179. return zone === tz;
  180. });
  181. }
  182. // Timezone confirmed or forced to offset
  183. newDate = found ? moment.tz(value, 'YYYYMMDDTHHmmss' + offset, tz).toDate() : new Date(
  184. Number.parseInt(comps[1], 10),
  185. Number.parseInt(comps[2], 10) - 1,
  186. Number.parseInt(comps[3], 10),
  187. Number.parseInt(comps[4], 10),
  188. Number.parseInt(comps[5], 10),
  189. Number.parseInt(comps[6], 10)
  190. );
  191. newDate = addTZ(newDate, parameters);
  192. } else {
  193. newDate = new Date(
  194. Number.parseInt(comps[1], 10),
  195. Number.parseInt(comps[2], 10) - 1,
  196. Number.parseInt(comps[3], 10),
  197. Number.parseInt(comps[4], 10),
  198. Number.parseInt(comps[5], 10),
  199. Number.parseInt(comps[6], 10)
  200. );
  201. }
  202. }
  203. // Store as string - worst case scenario
  204. return storeValueParameter(name)(newDate, curr);
  205. };
  206. };
  207. const geoParameter = function (name) {
  208. return function (value, parameters, curr) {
  209. storeParameter(value, parameters, curr);
  210. const parts = value.split(';');
  211. curr[name] = {lat: Number(parts[0]), lon: Number(parts[1])};
  212. return curr;
  213. };
  214. };
  215. const categoriesParameter = function (name) {
  216. const separatorPattern = /\s*,\s*/g;
  217. return function (value, parameters, curr) {
  218. storeParameter(value, parameters, curr);
  219. if (curr[name] === undefined) {
  220. curr[name] = value ? value.split(separatorPattern) : [];
  221. } else if (value) {
  222. curr[name] = curr[name].concat(value.split(separatorPattern));
  223. }
  224. return curr;
  225. };
  226. };
  227. // EXDATE is an entry that represents exceptions to a recurrence rule (ex: "repeat every day except on 7/4").
  228. // The EXDATE entry itself can also contain a comma-separated list, so we make sure to parse each date out separately.
  229. // There can also be more than one EXDATE entries in a calendar record.
  230. // Since there can be multiple dates, we create an array of them. The index into the array is the ISO string of the date itself, for ease of use.
  231. // i.e. You can check if ((curr.exdate != undefined) && (curr.exdate[date iso string] != undefined)) to see if a date is an exception.
  232. // NOTE: This specifically uses date only, and not time. This is to avoid a few problems:
  233. // 1. The ISO string with time wouldn't work for "floating dates" (dates without timezones).
  234. // ex: "20171225T060000" - this is supposed to mean 6 AM in whatever timezone you're currently in
  235. // 2. Daylight savings time potentially affects the time you would need to look up
  236. // 3. Some EXDATE entries in the wild seem to have times different from the recurrence rule, but are still excluded by calendar programs. Not sure how or why.
  237. // These would fail any sort of sane time lookup, because the time literally doesn't match the event. So we'll ignore time and just use date.
  238. // ex: DTSTART:20170814T140000Z
  239. // RRULE:FREQ=WEEKLY;WKST=SU;INTERVAL=2;BYDAY=MO,TU
  240. // EXDATE:20171219T060000
  241. // Even though "T060000" doesn't match or overlap "T1400000Z", it's still supposed to be excluded? Odd. :(
  242. // TODO: See if this causes any problems with events that recur multiple times a day.
  243. const exdateParameter = function (name) {
  244. return function (value, parameters, curr) {
  245. const separatorPattern = /\s*,\s*/g;
  246. curr[name] = curr[name] || [];
  247. const dates = value ? value.split(separatorPattern) : [];
  248. for (const entry of dates) {
  249. const exdate = [];
  250. dateParameter(name)(entry, parameters, exdate);
  251. if (exdate[name]) {
  252. if (typeof exdate[name].toISOString === 'function') {
  253. curr[name][exdate[name].toISOString().slice(0, 10)] = exdate[name];
  254. } else {
  255. throw new TypeError('No toISOString function in exdate[name]', exdate[name]);
  256. }
  257. }
  258. }
  259. return curr;
  260. };
  261. };
  262. // RECURRENCE-ID is the ID of a specific recurrence within a recurrence rule.
  263. // TODO: It's also possible for it to have a range, like "THISANDPRIOR", "THISANDFUTURE". This isn't currently handled.
  264. const recurrenceParameter = function (name) {
  265. return dateParameter(name);
  266. };
  267. const addFBType = function (fb, parameters) {
  268. const p = parseParameters(parameters);
  269. if (parameters && p) {
  270. fb.type = p.FBTYPE || 'BUSY';
  271. }
  272. return fb;
  273. };
  274. const freebusyParameter = function (name) {
  275. return function (value, parameters, curr) {
  276. const fb = addFBType({}, parameters);
  277. curr[name] = curr[name] || [];
  278. curr[name].push(fb);
  279. storeParameter(value, parameters, fb);
  280. const parts = value.split('/');
  281. for (const [index, name] of ['start', 'end'].entries()) {
  282. dateParameter(name)(parts[index], parameters, fb);
  283. }
  284. return curr;
  285. };
  286. };
  287. module.exports = {
  288. objectHandlers: {
  289. BEGIN(component, parameters, curr, stack) {
  290. stack.push(curr);
  291. return {type: component, params: parameters};
  292. },
  293. END(value, parameters, curr, stack) {
  294. // Original end function
  295. const originalEnd = function (component, parameters_, curr, stack) {
  296. // Prevents the need to search the root of the tree for the VCALENDAR object
  297. if (component === 'VCALENDAR') {
  298. // Scan all high level object in curr and drop all strings
  299. let key;
  300. let object;
  301. for (key in curr) {
  302. if (!{}.hasOwnProperty.call(curr, key)) {
  303. continue;
  304. }
  305. object = curr[key];
  306. if (typeof object === 'string') {
  307. delete curr[key];
  308. }
  309. }
  310. return curr;
  311. }
  312. const par = stack.pop();
  313. if (!curr.end) { // RFC5545, 3.6.1
  314. if (curr.datetype === 'date-time') {
  315. curr.end = curr.start;
  316. // If the duration is not set
  317. } else if (curr.duration === undefined) {
  318. // Set the end to the start plus one day RFC5545, 3.6.1
  319. curr.end = moment.utc(curr.start).add(1, 'days').toDate(); // New Date(moment(curr.start).add(1, 'days'));
  320. } else {
  321. const durationUnits =
  322. {
  323. // Y: 'years',
  324. // M: 'months',
  325. W: 'weeks',
  326. D: 'days',
  327. H: 'hours',
  328. M: 'minutes',
  329. S: 'seconds'
  330. };
  331. // Get the list of duration elements
  332. const r = curr.duration.match(/-?\d+[YMWDHS]/g);
  333. let newend = moment.utc(curr.start);
  334. // Is the 1st character a negative sign?
  335. const indicator = curr.duration.startsWith('-') ? -1 : 1;
  336. // Process each element
  337. for (const d of r) {
  338. newend = newend.add(Number.parseInt(d, 10) * indicator, durationUnits[d.slice(-1)]);
  339. }
  340. curr.end = newend.toDate();
  341. }
  342. }
  343. if (curr.uid) {
  344. // If this is the first time we run into this UID, just save it.
  345. if (par[curr.uid] === undefined) {
  346. par[curr.uid] = curr;
  347. } else if (curr.recurrenceid === undefined) {
  348. // If we have multiple ical entries with the same UID, it's either going to be a
  349. // modification to a recurrence (RECURRENCE-ID), and/or a significant modification
  350. // to the entry (SEQUENCE).
  351. // TODO: Look into proper sequence logic.
  352. // If we have the same UID as an existing record, and it *isn't* a specific recurrence ID,
  353. // not quite sure what the correct behaviour should be. For now, just take the new information
  354. // and merge it with the old record by overwriting only the fields that appear in the new record.
  355. let key;
  356. for (key in curr) {
  357. if (key !== null) {
  358. par[curr.uid][key] = curr[key];
  359. }
  360. }
  361. }
  362. // If we have recurrence-id entries, list them as an array of recurrences keyed off of recurrence-id.
  363. // To use - as you're running through the dates of an rrule, you can try looking it up in the recurrences
  364. // array. If it exists, then use the data from the calendar object in the recurrence instead of the parent
  365. // for that day.
  366. // NOTE: Sometimes the RECURRENCE-ID record will show up *before* the record with the RRULE entry. In that
  367. // case, what happens is that the RECURRENCE-ID record ends up becoming both the parent record and an entry
  368. // in the recurrences array, and then when we process the RRULE entry later it overwrites the appropriate
  369. // fields in the parent record.
  370. if (typeof curr.recurrenceid !== 'undefined') {
  371. // TODO: Is there ever a case where we have to worry about overwriting an existing entry here?
  372. // Create a copy of the current object to save in our recurrences array. (We *could* just do par = curr,
  373. // except for the case that we get the RECURRENCE-ID record before the RRULE record. In that case, we
  374. // would end up with a shared reference that would cause us to overwrite *both* records at the point
  375. // that we try and fix up the parent record.)
  376. const recurrenceObject = {};
  377. let key;
  378. for (key in curr) {
  379. if (key !== null) {
  380. recurrenceObject[key] = curr[key];
  381. }
  382. }
  383. if (typeof recurrenceObject.recurrences !== 'undefined') {
  384. delete recurrenceObject.recurrences;
  385. }
  386. // If we don't have an array to store recurrences in yet, create it.
  387. if (par[curr.uid].recurrences === undefined) {
  388. par[curr.uid].recurrences = {};
  389. }
  390. // Save off our cloned recurrence object into the array, keyed by date but not time.
  391. // We key by date only to avoid timezone and "floating time" problems (where the time isn't associated with a timezone).
  392. // TODO: See if this causes a problem with events that have multiple recurrences per day.
  393. if (typeof curr.recurrenceid.toISOString === 'function') {
  394. par[curr.uid].recurrences[curr.recurrenceid.toISOString().slice(0, 10)] = recurrenceObject;
  395. } else { // Removed issue 56
  396. throw new TypeError('No toISOString function in curr.recurrenceid', curr.recurrenceid);
  397. }
  398. }
  399. // One more specific fix - in the case that an RRULE entry shows up after a RECURRENCE-ID entry,
  400. // let's make sure to clear the recurrenceid off the parent field.
  401. if (typeof par[curr.uid].rrule !== 'undefined' && typeof par[curr.uid].recurrenceid !== 'undefined') {
  402. delete par[curr.uid].recurrenceid;
  403. }
  404. } else {
  405. par[uuid()] = curr;
  406. }
  407. return par;
  408. };
  409. // Recurrence rules are only valid for VEVENT, VTODO, and VJOURNAL.
  410. // More specifically, we need to filter the VCALENDAR type because we might end up with a defined rrule
  411. // due to the subtypes.
  412. if ((value === 'VEVENT' || value === 'VTODO' || value === 'VJOURNAL') && curr.rrule) {
  413. let rule = curr.rrule.replace('RRULE:', '');
  414. // Make sure the rrule starts with FREQ=
  415. rule = rule.slice(rule.lastIndexOf('FREQ='));
  416. // If no rule start date
  417. if (rule.includes('DTSTART') === false) {
  418. // Get date/time into a specific format for comapare
  419. let x = moment(curr.start).format('MMMM/Do/YYYY, h:mm:ss a');
  420. // If the local time value is midnight
  421. // This a whole day event
  422. if (x.slice(-11) === '12:00:00 am') {
  423. // Get the timezone offset
  424. // The internal date is stored in UTC format
  425. const offset = curr.start.getTimezoneOffset();
  426. // Only east of gmt is a problem
  427. if (offset < 0) {
  428. // Calculate the new startdate with the offset applied, bypass RRULE/Luxon confusion
  429. // Make the internally stored DATE the actual date (not UTC offseted)
  430. // Luxon expects local time, not utc, so gets start date wrong if not adjusted
  431. curr.start = new Date(curr.start.getTime() + (Math.abs(offset) * 60000));
  432. } else {
  433. // Get rid of any time (shouldn't be any, but be sure)
  434. x = moment(curr.start).format('MMMM/Do/YYYY');
  435. const comps = /^(\d{2})\/(\d{2})\/(\d{4})/.exec(x);
  436. if (comps) {
  437. curr.start = new Date(comps[3], comps[1] - 1, comps[2]);
  438. }
  439. }
  440. }
  441. // If the date has an toISOString function
  442. if (curr.start && typeof curr.start.toISOString === 'function') {
  443. try {
  444. rule += `;DTSTART=${curr.start.toISOString().replace(/[-:]/g, '')}`;
  445. rule = rule.replace(/\.\d{3}/, '');
  446. } catch (error) { // This should not happen, issue 56
  447. throw new Error('ERROR when trying to convert to ISOString', error);
  448. }
  449. } else {
  450. throw new Error('No toISOString function in curr.start', curr.start);
  451. }
  452. }
  453. // Make sure to catch error from rrule.fromString()
  454. try {
  455. curr.rrule = rrule.fromString(rule);
  456. } catch (error) {
  457. throw error;
  458. }
  459. }
  460. return originalEnd.call(this, value, parameters, curr, stack);
  461. },
  462. SUMMARY: storeParameter('summary'),
  463. DESCRIPTION: storeParameter('description'),
  464. URL: storeParameter('url'),
  465. UID: storeParameter('uid'),
  466. LOCATION: storeParameter('location'),
  467. DTSTART(value, parameters, curr) {
  468. curr = dateParameter('start')(value, parameters, curr);
  469. return typeParameter('datetype')(value, parameters, curr);
  470. },
  471. DTEND: dateParameter('end'),
  472. EXDATE: exdateParameter('exdate'),
  473. ' CLASS': storeParameter('class'), // Should there be a space in this property?
  474. TRANSP: storeParameter('transparency'),
  475. GEO: geoParameter('geo'),
  476. 'PERCENT-COMPLETE': storeParameter('completion'),
  477. COMPLETED: dateParameter('completed'),
  478. CATEGORIES: categoriesParameter('categories'),
  479. FREEBUSY: freebusyParameter('freebusy'),
  480. DTSTAMP: dateParameter('dtstamp'),
  481. CREATED: dateParameter('created'),
  482. 'LAST-MODIFIED': dateParameter('lastmodified'),
  483. 'RECURRENCE-ID': recurrenceParameter('recurrenceid'),
  484. RRULE(value, parameters, curr, stack, line) {
  485. curr.rrule = line;
  486. return curr;
  487. }
  488. },
  489. handleObject(name, value, parameters, ctx, stack, line) {
  490. if (this.objectHandlers[name]) {
  491. return this.objectHandlers[name](value, parameters, ctx, stack, line);
  492. }
  493. // Handling custom properties
  494. if (/X-[\w-]+/.test(name) && stack.length > 0) {
  495. // Trimming the leading and perform storeParam
  496. name = name.slice(2);
  497. return storeParameter(name)(value, parameters, ctx, stack, line);
  498. }
  499. return storeParameter(name.toLowerCase())(value, parameters, ctx);
  500. },
  501. parseLines(lines, limit, ctx, stack, lastIndex, cb) {
  502. if (!cb && typeof ctx === 'function') {
  503. cb = ctx;
  504. ctx = undefined;
  505. }
  506. ctx = ctx || {};
  507. stack = stack || [];
  508. let limitCounter = 0;
  509. let i = lastIndex || 0;
  510. for (let ii = lines.length; i < ii; i++) {
  511. let l = lines[i];
  512. // Unfold : RFC#3.1
  513. while (lines[i + 1] && /[ \t]/.test(lines[i + 1][0])) {
  514. l += lines[i + 1].slice(1);
  515. i++;
  516. }
  517. // Remove any double quotes in any tzid statement// except around (utc+hh:mm
  518. if (l.indexOf('TZID=') && !l.includes('"(')) {
  519. l = l.replace(/"/g, '');
  520. }
  521. const exp = /([^":;]+)((?:;[^":;]+=(?:(?:"[^"]*")|[^":;]+))*):(.*)/;
  522. let kv = l.match(exp);
  523. if (kv === null) {
  524. // Invalid line - must have k&v
  525. continue;
  526. }
  527. kv = kv.slice(1);
  528. const value = kv[kv.length - 1];
  529. const name = kv[0];
  530. const parameters = kv[1] ? kv[1].split(';').slice(1) : [];
  531. ctx = this.handleObject(name, value, parameters, ctx, stack, l) || {};
  532. if (++limitCounter > limit) {
  533. break;
  534. }
  535. }
  536. if (i >= lines.length) {
  537. // Type and params are added to the list of items, get rid of them.
  538. delete ctx.type;
  539. delete ctx.params;
  540. }
  541. if (cb) {
  542. if (i < lines.length) {
  543. setImmediate(() => {
  544. this.parseLines(lines, limit, ctx, stack, i + 1, cb);
  545. });
  546. } else {
  547. setImmediate(() => {
  548. cb(null, ctx);
  549. });
  550. }
  551. } else {
  552. return ctx;
  553. }
  554. },
  555. getLineBreakChar(string) {
  556. const indexOfLF = string.indexOf('\n', 1); // No need to check first-character
  557. if (indexOfLF === -1) {
  558. if (string.includes('\r')) {
  559. return '\r';
  560. }
  561. return '\n';
  562. }
  563. if (string[indexOfLF - 1] === '\r') {
  564. return '\r?\n';
  565. }
  566. return '\n';
  567. },
  568. parseICS(string, cb) {
  569. const lineEndType = this.getLineBreakChar(string);
  570. const lines = string.split(lineEndType === '\n' ? /\n/ : /\r?\n/);
  571. let ctx;
  572. if (cb) {
  573. // Asynchronous execution
  574. this.parseLines(lines, 2000, cb);
  575. } else {
  576. // Synchronous execution
  577. ctx = this.parseLines(lines, lines.length);
  578. return ctx;
  579. }
  580. }
  581. };