Repository: adamgibbons/ics
Pull request #200 (2021-11-05 15:50:05)

Support for html content
Using `X-ALT-DESC;FMTTYPE=text/html` attribute in ICS i added support for adding html content to the ICS file. It was discussed here: https://github.com/adamgibbons/ics/issues/119 


Patch:
diff --git a/README.md b/README.md
index 58f74d9..70f1440 100644
--- a/README.md
+++ b/README.md
@@ -233,9 +233,11 @@ The following properties are accepted:
 | recurrenceRule        | A recurrence rule, commonly referred to as an RRULE, defines the repeat pattern or rule for to-dos, journal entries and events. If specified, RRULE can be used to compute the recurrence set (the complete set of recurrence instances in a calendar component). You can use a generator like this [one](https://www.textmagic.com/free-tools/rrule-generator) | `FREQ=DAILY`
 | sequence      | For sending an update for an event (with the same uid), defines the revision sequence number. | `2`
 | busyStatus    | Used to specify busy status for Microsoft applications, like Outlook. See [Microsoft spec](https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/cd68eae7-ed65-4dd3-8ea7-ad585c76c736). | `'BUSY'` OR `'FREE'` OR `'TENTATIVE`' OR `'OOF'`
+| classification    | This property defines the access classification for a calendar component. See [iCalender spec](https://icalendar.org/iCalendar-RFC-5545/3-8-1-3-classification.html). | `'PUBLIC'` OR `'PRIVATE'` OR `'CONFIDENTIAL`' OR any non-standard string
 | created | Date-time representing event's creation date. Provide a date-time in UTC | [2000, 1, 5, 10, 0] (January 5, 2000 GMT +00:00)
 | lastModified | Date-time representing date when event was last modified. Provide a date-time in UTC | [2000, 1, 5, 10, 0] (January 5, 2000 GMT +00:00)
 | calName       |  Specifies the _calendar_ (not event) name. Used by Apple iCal and Microsoft Outlook; see [Open Specification](https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/1da58449-b97e-46bd-b018-a1ce576f3e6d) | `'Example Calendar'` |
+| htmlContent       | Used to include HTML markup in an event's description. Standard DESCRIPTION tag should contain non-HTML version. | `<!DOCTYPE html><html><body><p>This is<br>test<br>html code.</p></body></html>` |
 
 To create an **all-day** event, pass only three values (`year`, `month`, and `date`) to the `start` and `end` properties.
 The date of the `end` property should be the day *after* your all-day event.
diff --git a/index.d.ts b/index.d.ts
index f899751..4897f07 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -48,6 +48,11 @@ export type Attendee = Person & {
 
 export type ActionType = 'audio' | 'display' | 'email' | 'procedure';
 
+/**
+ * This property defines the access classification for a calendar component.
+ */
+export type classificationType = 'PUBLIC' | 'PRIVATE' | 'CONFIDENTIAL' | string;
+
 export type Alarm = {
   action?: ActionType;
   trigger?: DurationObject; // @todo DateArray | DurationObject;
@@ -73,21 +78,23 @@ export type EventAttributes = {
   url?: string;
   status?: EventStatus;
   busyStatus?: 'FREE' | 'BUSY' | 'TENTATIVE' | 'OOF';
-
+  
   organizer?: Person;
   attendees?: Attendee[];
-
+  
   categories?: string[];
   alarms?: Alarm[];
-
+  
   productId?: string;
   uid?: string;
   method?: string;
   recurrenceRule?: string;
   sequence?: number;
   calName?: string;
+  classification?: classificationType;
   created?: DateArray;
   lastModified?: DateArray;
+  htmlContent?: string;
 } & ({ end: DateArray } | { duration: DurationObject });
 
 export type ReturnObject = { error?: Error; value?: string };
diff --git a/notes.md b/notes.md
index 70c226f..297c5fe 100644
--- a/notes.md
+++ b/notes.md
@@ -46,7 +46,7 @@ TZID=America/New_York:19980119T020000
 - [x] dtstart
 
 ### OPTIONAL but MUST NOT occur more than once
-- [ ] class
+- [x] class
 - [ ] created
 - [x] description
 - [x] geo
diff --git a/package.json b/package.json
index 3a411f8..5453e6b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ics",
-  "version": "2.31.0",
+  "version": "2.32.0",
   "description": "iCal (ics) file generator",
   "main": "index.js",
   "types": "index.d.ts",
diff --git a/src/pipeline/build.js b/src/pipeline/build.js
index 812cf6c..ca59264 100644
--- a/src/pipeline/build.js
+++ b/src/pipeline/build.js
@@ -23,7 +23,8 @@ export default function buildEvent(attributes = {}) {
     recurrenceRule,
     created,
     lastModified,
-    calName
+    calName,
+    htmlContent
   } = attributes;
 
   // fill in default values where necessary
diff --git a/src/pipeline/format.js b/src/pipeline/format.js
index 247127c..192b08c 100644
--- a/src/pipeline/format.js
+++ b/src/pipeline/format.js
@@ -38,9 +38,11 @@ export default function formatEvent(attributes = {}) {
     alarms,
     recurrenceRule,
     busyStatus,
+    classification,
     created,
     lastModified,
-    calName
+    calName,
+    htmlContent
   } = attributes
 
   let icsFormat = ''
@@ -75,8 +77,10 @@ export default function formatEvent(attributes = {}) {
   icsFormat += categories ? (foldLine(`CATEGORIES:${categories}`) + '\r\n') : ''
   icsFormat += organizer ? (foldLine(`ORGANIZER;${setOrganizer(organizer)}`) + '\r\n') : ''
   icsFormat += busyStatus ? (foldLine(`X-MICROSOFT-CDO-BUSYSTATUS:${busyStatus}`) + '\r\n') : ''
+  icsFormat += classification ? (foldLine(`CLASS:${classification}`) + '\r\n') : ''
   icsFormat += created ? ('CREATED:' + formatDate(created) + '\r\n') : ''
   icsFormat += lastModified ? ('LAST-MODIFIED:' + formatDate(lastModified) + '\r\n') : ''
+  icsFormat += htmlContent ? (foldLine(`X-ALT-DESC;FMTTYPE=text/html:${htmlContent}`) + '\r\n') : ''
   if (attendees) {
     attendees.map(function (attendee) {
       icsFormat += foldLine(`ATTENDEE;${setContact(attendee)}`) + '\r\n'
diff --git a/src/schema/index.js b/src/schema/index.js
index 3a99951..19a4f65 100644
--- a/src/schema/index.js
+++ b/src/schema/index.js
@@ -83,9 +83,11 @@ const schema = yup.object().shape({
   alarms: yup.array().of(alarmSchema),
   recurrenceRule: yup.string(),
   busyStatus: yup.string().matches(/TENTATIVE|FREE|BUSY|OOF/i),
+  classification: yup.string(),
   created: dateTimeSchema,
   lastModified: dateTimeSchema,
-  calName: yup.string()
+  calName: yup.string(),
+  htmlContent: yup.string()
 }).test('xor', `object should have end or duration`, val => {
   const hasEnd = !!val.end
   const hasDuration = !!val.duration


Test patch:
diff --git a/test/pipeline/build.spec.js b/test/pipeline/build.spec.js
index d82d444..3299f8c 100644
--- a/test/pipeline/build.spec.js
+++ b/test/pipeline/build.spec.js
@@ -82,6 +82,12 @@ describe('pipeline.build properties', () => {
       expect(event.calName).to.equal('John\'s Calendar')
     })
   })
+  describe('htmlContent', () => {
+    it('sets a html content', () => {
+      const event = buildEvent({ htmlContent: '<!DOCTYPE html><html><body><p>This is<br>test<br>html code.</p></body></html>' })
+      expect(event.htmlContent).to.equal('<!DOCTYPE html><html><body><p>This is<br>test<br>html code.</p></body></html>')
+    })
+  })
   describe('description', () => {
     it('removes a falsey value', () => {
       const event = buildEvent()
diff --git a/test/pipeline/format.spec.js b/test/pipeline/format.spec.js
index dd1e830..d1ca25f 100644
--- a/test/pipeline/format.spec.js
+++ b/test/pipeline/format.spec.js
@@ -104,6 +104,11 @@ describe('pipeline.formatEvent', () => {
     const formattedEvent = formatEvent(event)
     expect(formattedEvent).to.contain('X-WR-CALNAME:John\'s Calendar')
   })
+  it('writes a html content and folds correctly', () => {
+    const event = buildEvent({ htmlContent: '<!DOCTYPE html><html><body><p>This is<br>test<br>html code.</p></body></html>' })
+    const formattedEvent = formatEvent(event)
+    expect(formattedEvent).to.contain(`X-ALT-DESC;FMTTYPE=text/html:<!DOCTYPE html><html><body><p>This is<br>test<\r\n\tbr>html code.</p></body></html>`)
+  })
   it('writes a sequence', () => {
     const event = buildEvent({ sequence: 8 })
     const formattedEvent = formatEvent(event)
@@ -186,6 +191,20 @@ describe('pipeline.formatEvent', () => {
     expect(formattedEventTent).to.contain('X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE')
     expect(formattedEventOOF).to.contain('X-MICROSOFT-CDO-BUSYSTATUS:OOF')
   })
+  it('writes a access classification', () => {
+    const eventPublic = buildEvent({ classification: "PUBLIC" })
+    const eventPrivate = buildEvent({ classification: "PRIVATE"})
+    const eventConfidential = buildEvent({ classification: "CONFIDENTIAL"})
+    const eventAnyClass = buildEvent({ classification: "non-standard-property" })
+    const formattedEventPublic = formatEvent(eventPublic)
+    const formattedEventPrivate = formatEvent(eventPrivate)
+    const formattedEventConfidential = formatEvent(eventConfidential)
+    const formattedEventAnyClass = formatEvent(eventAnyClass)
+    expect(formattedEventPublic).to.contain('CLASS:PUBLIC')
+    expect(formattedEventPrivate).to.contain('CLASS:PRIVATE')
+    expect(formattedEventConfidential).to.contain('CLASS:CONFIDENTIAL')
+    expect(formattedEventAnyClass).to.contain('CLASS:non-standard-property')
+  })
   it('writes an organizer', () => {
     const event = formatEvent({ organizer: {
       name: 'Adam Gibbons',
diff --git a/test/schema/index.spec.js b/test/schema/index.spec.js
index c9ee9d6..e5e79d0 100644
--- a/test/schema/index.spec.js
+++ b/test/schema/index.spec.js
@@ -237,6 +237,15 @@ describe('.validateEvent', () => {
         start: [2018, 12, 1, 10, 30],
       }).value.calName).to.exist
     })
+
+    it('htmlContent', () => {
+      expect(validateEvent({
+        title: 'foo',
+        uid: 'foo',
+        htmlContent: '<!DOCTYPE html><html><body><p>This is<br>test<br>html code.</p></body></html>',
+        start: [2018, 12, 1, 10, 30],
+      }).value.htmlContent).to.exist
+    })
   })
 
   describe('may have one or more occurrences of', () => {