@pipobscure/vcard 0.0.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/LICENSE +190 -0
- package/README.md +1079 -0
- package/dist/escape.d.ts +59 -0
- package/dist/escape.d.ts.map +1 -0
- package/dist/escape.js +136 -0
- package/dist/escape.js.map +1 -0
- package/dist/generator.d.ts +49 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +194 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +54 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +381 -0
- package/dist/parser.js.map +1 -0
- package/dist/property.d.ts +347 -0
- package/dist/property.d.ts.map +1 -0
- package/dist/property.js +854 -0
- package/dist/property.js.map +1 -0
- package/dist/tests/edgecases.test.d.ts +10 -0
- package/dist/tests/edgecases.test.d.ts.map +1 -0
- package/dist/tests/edgecases.test.js +950 -0
- package/dist/tests/edgecases.test.js.map +1 -0
- package/dist/tests/vcard.test.d.ts +6 -0
- package/dist/tests/vcard.test.d.ts.map +1 -0
- package/dist/tests/vcard.test.js +496 -0
- package/dist/tests/vcard.test.js.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/vcard.d.ts +177 -0
- package/dist/vcard.d.ts.map +1 -0
- package/dist/vcard.js +424 -0
- package/dist/vcard.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
# @pipobscure/vcard
|
|
2
|
+
|
|
3
|
+
A fully featured, RFC 6350-compliant vCard v4 library for Node.js and TypeScript.
|
|
4
|
+
|
|
5
|
+
Designed for use with CardDAV servers and clients. The parser is deliberately tolerant — it handles vCard v2.1, v3.0, and v4.0 input, Apple Contacts exports, QUOTED-PRINTABLE encoding, and various real-world quirks without throwing. The generator is deliberately strict — it produces RFC 6350-compliant output with CRLF line endings, UTF-8 byte-accurate line folding, and full validation.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @pipobscure/vcard
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Node.js 18 or later (uses `Buffer`, ES modules).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { VCard, FNProperty, EmailProperty, TelProperty, NProperty } from '@pipobscure/vcard';
|
|
23
|
+
|
|
24
|
+
// --- Parse ---
|
|
25
|
+
const vcards = VCard.parse(rawText);
|
|
26
|
+
const vc = vcards[0];
|
|
27
|
+
console.log(vc.displayName); // 'Alice Example'
|
|
28
|
+
console.log(vc.primaryEmail); // 'alice@example.com'
|
|
29
|
+
|
|
30
|
+
// --- Build ---
|
|
31
|
+
const vc = new VCard();
|
|
32
|
+
vc.fn.push(new FNProperty('Alice Example'));
|
|
33
|
+
vc.n = new NProperty({
|
|
34
|
+
familyNames: ['Example'],
|
|
35
|
+
givenNames: ['Alice'],
|
|
36
|
+
additionalNames: [],
|
|
37
|
+
honorificPrefixes: [],
|
|
38
|
+
honorificSuffixes: [],
|
|
39
|
+
});
|
|
40
|
+
vc.email.push(new EmailProperty('alice@example.com'));
|
|
41
|
+
|
|
42
|
+
const text = vc.toString();
|
|
43
|
+
// BEGIN:VCARD\r\n
|
|
44
|
+
// VERSION:4.0\r\n
|
|
45
|
+
// FN:Alice Example\r\n
|
|
46
|
+
// ...
|
|
47
|
+
// END:VCARD\r\n
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Table of Contents
|
|
53
|
+
|
|
54
|
+
- [Parsing](#parsing)
|
|
55
|
+
- [Generating](#generating)
|
|
56
|
+
- [The VCard class](#the-vcard-class)
|
|
57
|
+
- [Property classes](#property-classes)
|
|
58
|
+
- [Common parameter accessors](#common-parameter-accessors)
|
|
59
|
+
- [Identification properties](#identification-properties)
|
|
60
|
+
- [Delivery addressing](#delivery-addressing)
|
|
61
|
+
- [Communications](#communications)
|
|
62
|
+
- [Geographic](#geographic)
|
|
63
|
+
- [Organizational](#organizational)
|
|
64
|
+
- [Explanatory](#explanatory)
|
|
65
|
+
- [Security](#security)
|
|
66
|
+
- [Calendar](#calendar)
|
|
67
|
+
- [General](#general)
|
|
68
|
+
- [Unknown and extended properties](#unknown-and-extended-properties)
|
|
69
|
+
- [Date and time values](#date-and-time-values)
|
|
70
|
+
- [Escaping utilities](#escaping-utilities)
|
|
71
|
+
- [Validation](#validation)
|
|
72
|
+
- [RFC compliance notes](#rfc-compliance-notes)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Parsing
|
|
77
|
+
|
|
78
|
+
### `VCard.parse(text: string): VCard[]`
|
|
79
|
+
|
|
80
|
+
Parse one or more vCards from a string. Returns an array (empty if no vCards are found). Never throws — malformed input is handled tolerantly and a list of `parseWarnings` is attached to each resulting `VCard`.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { VCard } from '@pipobscure/vcard';
|
|
84
|
+
|
|
85
|
+
const vcards = VCard.parse(text);
|
|
86
|
+
for (const vc of vcards) {
|
|
87
|
+
if (vc.parseWarnings.length > 0) {
|
|
88
|
+
console.warn('Parse warnings:', vc.parseWarnings);
|
|
89
|
+
}
|
|
90
|
+
console.log(vc.displayName);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `VCard.parseOne(text: string): VCard`
|
|
95
|
+
|
|
96
|
+
Parse exactly one vCard. Throws `Error` if the input contains no vCards.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const vc = VCard.parseOne(text);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Convenience functions
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { parse, parseOne } from '@pipobscure/vcard';
|
|
106
|
+
|
|
107
|
+
const vcards = parse(text); // same as VCard.parse()
|
|
108
|
+
const vc = parseOne(text); // same as VCard.parseOne()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Parsing tolerance
|
|
112
|
+
|
|
113
|
+
The parser handles all of the following without throwing:
|
|
114
|
+
|
|
115
|
+
- vCard versions 2.1, 3.0, and 4.0
|
|
116
|
+
- LF-only line endings (in addition to RFC-required CRLF)
|
|
117
|
+
- Mixed line endings within a single file
|
|
118
|
+
- Folded content lines (CRLF + whitespace continuation)
|
|
119
|
+
- `ENCODING=QUOTED-PRINTABLE` with multi-byte UTF-8 sequences (v2.1/v3.0)
|
|
120
|
+
- `ENCODING=b` (base64) parameter flag (v3.0 syntax)
|
|
121
|
+
- Case-insensitive property names and parameter names
|
|
122
|
+
- Comma-separated `TYPE` values (`TYPE=WORK,VOICE`)
|
|
123
|
+
- Quoted parameter values with commas (`TYPE="work,voice"`)
|
|
124
|
+
- Item-grouped properties (`item1.EMAIL`, `item1.X-ABLabel`)
|
|
125
|
+
- Unknown / proprietary properties (stored verbatim as `UnknownProperty`)
|
|
126
|
+
- Multiple vCards in a single string
|
|
127
|
+
- Content before `BEGIN:VCARD` and between vCards
|
|
128
|
+
- Missing `END:VCARD` (parsed with a warning)
|
|
129
|
+
- Properties with empty values
|
|
130
|
+
|
|
131
|
+
### `ParseWarning`
|
|
132
|
+
|
|
133
|
+
Each parsed `VCard` has a `parseWarnings: ParseWarning[]` field.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
interface ParseWarning {
|
|
137
|
+
line?: number; // 1-based line number in the input, if known
|
|
138
|
+
message: string;
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Generating
|
|
145
|
+
|
|
146
|
+
### `vcard.toString(options?: GenerateOptions): string`
|
|
147
|
+
|
|
148
|
+
Serialize the vCard to RFC 6350-compliant text. Throws `VCardError` if the card fails validation.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const text = vc.toString();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### `vcard.toStringLenient(): string`
|
|
155
|
+
|
|
156
|
+
Serialize without validation. Useful for inspecting or debugging partial/draft cards.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const text = vc.toStringLenient();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `stringify(vcard: VCard | VCard[]): string`
|
|
163
|
+
|
|
164
|
+
Serialize one or multiple vCards to a single string.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { stringify } from '@pipobscure/vcard';
|
|
168
|
+
|
|
169
|
+
const text = stringify([vc1, vc2]);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `GenerateOptions`
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
interface GenerateOptions {
|
|
176
|
+
validate?: boolean; // default: true — throw VCardError on invalid cards
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Output guarantees
|
|
181
|
+
|
|
182
|
+
- Line endings are always `\r\n` (CRLF) per RFC 6350 §3.2.
|
|
183
|
+
- Lines are folded at 75 octets (UTF-8 byte count), not 75 characters. Continuation lines begin with a single space.
|
|
184
|
+
- `VERSION:4.0` is always the first property after `BEGIN:VCARD`.
|
|
185
|
+
- All text values are escaped (`\`, `,`, `;`, newline) per RFC 6350 §3.4.
|
|
186
|
+
- Parameter values that contain `:`, `;`, `,`, or `"` are automatically quoted.
|
|
187
|
+
- Properties are emitted in a consistent, human-readable order.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## The VCard class
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
class VCard {
|
|
195
|
+
// Accumulates warnings from parsing; empty for programmatically built cards
|
|
196
|
+
parseWarnings: ParseWarning[];
|
|
197
|
+
|
|
198
|
+
// Version string from input (always '4.0' in generated output)
|
|
199
|
+
parsedVersion: string;
|
|
200
|
+
|
|
201
|
+
// ── Required (RFC cardinality 1*) ──────────────────────────────────────
|
|
202
|
+
fn: FNProperty[]; // at least one required
|
|
203
|
+
|
|
204
|
+
// ── Optional singular (RFC cardinality *1) ─────────────────────────────
|
|
205
|
+
n?: NProperty;
|
|
206
|
+
bday?: BDayProperty;
|
|
207
|
+
anniversary?: AnniversaryProperty;
|
|
208
|
+
gender?: GenderProperty;
|
|
209
|
+
prodid?: ProdIDProperty;
|
|
210
|
+
rev?: RevProperty;
|
|
211
|
+
uid?: UIDProperty;
|
|
212
|
+
kind?: KindProperty;
|
|
213
|
+
|
|
214
|
+
// ── Optional multiple (RFC cardinality *) ──────────────────────────────
|
|
215
|
+
nickname: NicknameProperty[];
|
|
216
|
+
photo: PhotoProperty[];
|
|
217
|
+
adr: AdrProperty[];
|
|
218
|
+
tel: TelProperty[];
|
|
219
|
+
email: EmailProperty[];
|
|
220
|
+
impp: IMPPProperty[];
|
|
221
|
+
lang: LangProperty[];
|
|
222
|
+
tz: TZProperty[];
|
|
223
|
+
geo: GeoProperty[];
|
|
224
|
+
title: TitleProperty[];
|
|
225
|
+
role: RoleProperty[];
|
|
226
|
+
logo: LogoProperty[];
|
|
227
|
+
org: OrgProperty[];
|
|
228
|
+
member: MemberProperty[];
|
|
229
|
+
related: RelatedProperty[];
|
|
230
|
+
categories: CategoriesProperty[];
|
|
231
|
+
note: NoteProperty[];
|
|
232
|
+
sound: SoundProperty[];
|
|
233
|
+
clientpidmap: ClientPidMapProperty[];
|
|
234
|
+
url: URLProperty[];
|
|
235
|
+
key: KeyProperty[];
|
|
236
|
+
fburl: FBURLProperty[];
|
|
237
|
+
caladruri: CALADRURIProperty[];
|
|
238
|
+
caluri: CALURIProperty[];
|
|
239
|
+
source: SourceProperty[];
|
|
240
|
+
xml: XMLProperty[];
|
|
241
|
+
|
|
242
|
+
// Unknown / extended / vendor properties (X-, unrecognised IANA)
|
|
243
|
+
extended: UnknownProperty[];
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Convenience accessors
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
vc.displayName // string — value of the most-preferred FN property
|
|
251
|
+
vc.primaryEmail // string | undefined — most-preferred email address
|
|
252
|
+
vc.primaryTel // string | undefined — most-preferred telephone
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
"Most preferred" means the property with the lowest `PREF` parameter value (1 = highest preference). If no `PREF` is set, the first property in the list is used.
|
|
256
|
+
|
|
257
|
+
### `VCard.create(fn: string): VCard`
|
|
258
|
+
|
|
259
|
+
Quick-create a valid vCard with a single formatted name.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const vc = VCard.create('Bob Builder');
|
|
263
|
+
vc.email.push(new EmailProperty('bob@example.com'));
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### `vcard.addProperty(prop: Property): void`
|
|
267
|
+
|
|
268
|
+
Add any property to the correct typed field on the VCard.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
vc.addProperty(new EmailProperty('alice@example.com'));
|
|
272
|
+
// equivalent to: vc.email.push(new EmailProperty('alice@example.com'))
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### `vcard.allProperties(): Property[]`
|
|
276
|
+
|
|
277
|
+
Return all properties as a flat array in logical order. Used internally by `toString()`.
|
|
278
|
+
|
|
279
|
+
### `vcard.validate(): ValidationResult`
|
|
280
|
+
|
|
281
|
+
Validate without throwing.
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
const result = vc.validate();
|
|
285
|
+
if (!result.valid) {
|
|
286
|
+
for (const err of result.errors) {
|
|
287
|
+
console.error(`${err.property}: ${err.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### `vcard.clone(): VCard`
|
|
293
|
+
|
|
294
|
+
Deep-clone by round-tripping through serialization. Always produces a clean v4 vCard.
|
|
295
|
+
|
|
296
|
+
### `vcard.toJSON(): Record<string, unknown>`
|
|
297
|
+
|
|
298
|
+
Simplified JSON representation (not full jCard / RFC 7095).
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Property classes
|
|
303
|
+
|
|
304
|
+
Every property class extends `Property` and exposes:
|
|
305
|
+
|
|
306
|
+
- `name: string` — uppercase property name (e.g. `'FN'`)
|
|
307
|
+
- `group?: string` — optional group label (e.g. `'item1'` in Apple exports)
|
|
308
|
+
- `params: ParameterMap` — raw parameter map (`Map<string, string | string[]>`)
|
|
309
|
+
- `toContentLine(): string` — serializes the value portion (used by the generator)
|
|
310
|
+
|
|
311
|
+
### Common parameter accessors
|
|
312
|
+
|
|
313
|
+
All property classes inherit these convenience getters/setters:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
prop.type // string[] — TYPE parameter values, lowercased
|
|
317
|
+
prop.pref // number | undefined — PREF parameter (1–100, 1 = most preferred)
|
|
318
|
+
prop.language // string | undefined — LANGUAGE parameter (BCP 47 tag)
|
|
319
|
+
prop.altid // string | undefined — ALTID parameter
|
|
320
|
+
prop.pid // string | undefined — PID parameter
|
|
321
|
+
prop.valueType // string | undefined — VALUE parameter (e.g. 'uri', 'text')
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Setting a value to `undefined` removes the parameter:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
prop.type = ['work', 'voice'];
|
|
328
|
+
prop.pref = 1;
|
|
329
|
+
prop.language = 'en';
|
|
330
|
+
prop.language = undefined; // removes LANGUAGE parameter
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
### Identification properties
|
|
336
|
+
|
|
337
|
+
#### `FNProperty` — Formatted Name (RFC 6350 §6.2.1)
|
|
338
|
+
|
|
339
|
+
Cardinality: `1*` (required, one or more)
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
class FNProperty extends TextProperty {
|
|
343
|
+
value: string;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
new FNProperty('Alice Example')
|
|
347
|
+
new FNProperty('Alice Example', params, group)
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
A vCard must have at least one `FN`. Multiple `FN` properties may be given to provide alternate language versions using `ALTID` and `LANGUAGE` parameters:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
const fn1 = new FNProperty('山田太郎');
|
|
354
|
+
fn1.altid = '1';
|
|
355
|
+
fn1.language = 'ja';
|
|
356
|
+
|
|
357
|
+
const fn2 = new FNProperty('Yamada Taro');
|
|
358
|
+
fn2.altid = '1';
|
|
359
|
+
fn2.language = 'en';
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### `NProperty` — Structured Name (RFC 6350 §6.2.2)
|
|
363
|
+
|
|
364
|
+
Cardinality: `*1` (optional, at most one)
|
|
365
|
+
|
|
366
|
+
```ts
|
|
367
|
+
class NProperty extends Property {
|
|
368
|
+
value: StructuredName;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
interface StructuredName {
|
|
372
|
+
familyNames: string[]; // e.g. ['Smith']
|
|
373
|
+
givenNames: string[]; // e.g. ['John']
|
|
374
|
+
additionalNames: string[]; // e.g. ['Q.']
|
|
375
|
+
honorificPrefixes: string[]; // e.g. ['Dr.']
|
|
376
|
+
honorificSuffixes: string[]; // e.g. ['Jr.', 'PhD']
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Each component is a list to support multiple values (e.g. compound surnames). The `SORT-AS` parameter provides a sort key:
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
const n = new NProperty({
|
|
384
|
+
familyNames: ['van der Berg'],
|
|
385
|
+
givenNames: ['Jan'],
|
|
386
|
+
additionalNames: [],
|
|
387
|
+
honorificPrefixes: [],
|
|
388
|
+
honorificSuffixes: [],
|
|
389
|
+
});
|
|
390
|
+
n.params.set('SORT-AS', 'Berg,Jan');
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
#### `NicknameProperty` — Nickname (RFC 6350 §6.2.3)
|
|
394
|
+
|
|
395
|
+
Cardinality: `*`
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
class NicknameProperty extends TextListProperty {
|
|
399
|
+
values: string[];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
new NicknameProperty(['Johnny', 'The Genius'])
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### `PhotoProperty` — Photo (RFC 6350 §6.2.4)
|
|
406
|
+
|
|
407
|
+
Cardinality: `*`
|
|
408
|
+
|
|
409
|
+
The value is a URI. In v4, inline data is expressed as a `data:` URI.
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
class PhotoProperty extends UriProperty {
|
|
413
|
+
value: string;
|
|
414
|
+
mediatype?: string; // MEDIATYPE parameter
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
new PhotoProperty('https://example.com/alice.jpg')
|
|
418
|
+
new PhotoProperty('data:image/jpeg;base64,/9j/4AA...')
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### `BDayProperty` — Birthday (RFC 6350 §6.2.5)
|
|
422
|
+
|
|
423
|
+
Cardinality: `*1`
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
class BDayProperty extends Property {
|
|
427
|
+
dateValue: DateAndOrTime | null; // parsed date, or null if VALUE=text
|
|
428
|
+
textValue?: string; // present when VALUE=text
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Parsed from a date string
|
|
432
|
+
BDayProperty.fromText('19900315') // full date
|
|
433
|
+
BDayProperty.fromText('--0315') // month+day, no year
|
|
434
|
+
BDayProperty.fromText('1990') // year only
|
|
435
|
+
|
|
436
|
+
// VALUE=text for approximate dates
|
|
437
|
+
BDayProperty.fromText('circa 1800', new Map([['VALUE', 'text']]))
|
|
438
|
+
|
|
439
|
+
// From a typed value
|
|
440
|
+
new BDayProperty({ year: 1990, month: 3, day: 15, hasTime: false })
|
|
441
|
+
new BDayProperty('circa 1800') // stores as textValue
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### `AnniversaryProperty` — Anniversary (RFC 6350 §6.2.6)
|
|
445
|
+
|
|
446
|
+
Cardinality: `*1`. Identical structure to `BDayProperty`.
|
|
447
|
+
|
|
448
|
+
#### `GenderProperty` — Gender (RFC 6350 §6.2.7)
|
|
449
|
+
|
|
450
|
+
Cardinality: `*1`
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
class GenderProperty extends Property {
|
|
454
|
+
value: Gender;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
interface Gender {
|
|
458
|
+
sex: GenderSex; // 'M' | 'F' | 'O' | 'N' | 'U' | ''
|
|
459
|
+
identity?: string; // free-form identity text
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
new GenderProperty({ sex: 'M' })
|
|
463
|
+
new GenderProperty({ sex: 'O', identity: 'non-binary' })
|
|
464
|
+
new GenderProperty({ sex: '', identity: 'it/its' })
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Sex values per RFC 6350:
|
|
468
|
+
|
|
469
|
+
| Value | Meaning |
|
|
470
|
+
|-------|---------|
|
|
471
|
+
| `M` | Male |
|
|
472
|
+
| `F` | Female |
|
|
473
|
+
| `O` | Other |
|
|
474
|
+
| `N` | None or not applicable |
|
|
475
|
+
| `U` | Unknown |
|
|
476
|
+
| `''` | Not specified (use with identity text) |
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
### Delivery addressing
|
|
481
|
+
|
|
482
|
+
#### `AdrProperty` — Address (RFC 6350 §6.3.1)
|
|
483
|
+
|
|
484
|
+
Cardinality: `*`
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
class AdrProperty extends Property {
|
|
488
|
+
value: Address;
|
|
489
|
+
label?: string; // LABEL parameter — delivery label text
|
|
490
|
+
cc?: string; // CC parameter — ISO 3166-1 country code
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
interface Address {
|
|
494
|
+
postOfficeBox: string;
|
|
495
|
+
extendedAddress: string;
|
|
496
|
+
streetAddress: string;
|
|
497
|
+
locality: string; // city
|
|
498
|
+
region: string; // state/province
|
|
499
|
+
postalCode: string;
|
|
500
|
+
countryName: string;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const adr = new AdrProperty({
|
|
504
|
+
postOfficeBox: '',
|
|
505
|
+
extendedAddress: 'Suite 100',
|
|
506
|
+
streetAddress: '1 Infinite Loop',
|
|
507
|
+
locality: 'Cupertino',
|
|
508
|
+
region: 'CA',
|
|
509
|
+
postalCode: '95014',
|
|
510
|
+
countryName: 'USA',
|
|
511
|
+
});
|
|
512
|
+
adr.type = ['work'];
|
|
513
|
+
adr.label = '1 Infinite Loop\nCupertino, CA 95014\nUSA';
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
### Communications
|
|
519
|
+
|
|
520
|
+
#### `TelProperty` — Telephone (RFC 6350 §6.4.1)
|
|
521
|
+
|
|
522
|
+
Cardinality: `*`
|
|
523
|
+
|
|
524
|
+
In v4, telephone values should be URIs (using `tel:` or `sip:` schemes). Plain text values are also accepted for compatibility.
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
class TelProperty extends Property {
|
|
528
|
+
value: string;
|
|
529
|
+
isUri: boolean; // true when value is a URI
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
new TelProperty('tel:+1-555-123-4567') // URI (recommended)
|
|
533
|
+
new TelProperty('+1 555 123 4567') // text (tolerated)
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Well-known TYPE values: `voice`, `fax`, `cell`, `video`, `pager`, `textphone`, `text`, `work`, `home`.
|
|
537
|
+
|
|
538
|
+
#### `EmailProperty` — Email (RFC 6350 §6.4.2)
|
|
539
|
+
|
|
540
|
+
Cardinality: `*`
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
class EmailProperty extends TextProperty {
|
|
544
|
+
value: string;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const email = new EmailProperty('alice@example.com');
|
|
548
|
+
email.type = ['work'];
|
|
549
|
+
email.pref = 1;
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
#### `IMPPProperty` — Instant Messaging (RFC 6350 §6.4.3)
|
|
553
|
+
|
|
554
|
+
Cardinality: `*`. Value is a URI (e.g. `xmpp:alice@example.com`, `sip:alice@example.com`).
|
|
555
|
+
|
|
556
|
+
#### `LangProperty` — Language (RFC 6350 §6.4.4)
|
|
557
|
+
|
|
558
|
+
Cardinality: `*`. Value is a BCP 47 language tag.
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
const lang = new LangProperty('fr');
|
|
562
|
+
lang.pref = 1;
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
### Geographic
|
|
568
|
+
|
|
569
|
+
#### `TZProperty` — Time Zone (RFC 6350 §6.5.1)
|
|
570
|
+
|
|
571
|
+
Cardinality: `*`
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
class TZProperty extends Property {
|
|
575
|
+
value: string;
|
|
576
|
+
valueKind: 'utc-offset' | 'uri' | 'text';
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
TZProperty.fromText('-0500') // UTC offset → valueKind: 'utc-offset'
|
|
580
|
+
TZProperty.fromText('-05:00') // colon format UTC offset
|
|
581
|
+
TZProperty.fromText('America/New_York', // IANA name → valueKind: 'text'
|
|
582
|
+
new Map([['VALUE', 'text']]))
|
|
583
|
+
TZProperty.fromText('https://...', ...) // URI → valueKind: 'uri'
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
#### `GeoProperty` — Geographic Position (RFC 6350 §6.5.2)
|
|
587
|
+
|
|
588
|
+
Cardinality: `*`. Value is a `geo:` URI.
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
class GeoProperty extends Property {
|
|
592
|
+
uri: string;
|
|
593
|
+
readonly coordinates: { latitude: number; longitude: number } | undefined;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// From coordinates
|
|
597
|
+
const geo = GeoProperty.fromCoordinates(37.386013, -122.082932);
|
|
598
|
+
// geo.uri === 'geo:37.386013,-122.082932'
|
|
599
|
+
|
|
600
|
+
// From URI string
|
|
601
|
+
const geo = new GeoProperty('geo:51.5074,-0.1278');
|
|
602
|
+
console.log(geo.coordinates); // { latitude: 51.5074, longitude: -0.1278 }
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
### Organizational
|
|
608
|
+
|
|
609
|
+
#### `TitleProperty` / `RoleProperty`
|
|
610
|
+
|
|
611
|
+
Cardinality: `*`. Both are text properties.
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
new TitleProperty('Software Engineer')
|
|
615
|
+
new RoleProperty('Lead Developer')
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
#### `LogoProperty`
|
|
619
|
+
|
|
620
|
+
Cardinality: `*`. URI value (same as `PhotoProperty`).
|
|
621
|
+
|
|
622
|
+
#### `OrgProperty` — Organization (RFC 6350 §6.6.4)
|
|
623
|
+
|
|
624
|
+
Cardinality: `*`
|
|
625
|
+
|
|
626
|
+
```ts
|
|
627
|
+
class OrgProperty extends Property {
|
|
628
|
+
value: Organization;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
interface Organization {
|
|
632
|
+
name: string;
|
|
633
|
+
units: string[]; // organizational units (zero or more)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
new OrgProperty({ name: 'Acme Corp', units: [] })
|
|
637
|
+
new OrgProperty({ name: 'Acme Corp', units: ['Engineering', 'Platform'] })
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
The `SORT-AS` parameter provides a sort key for the name and units.
|
|
641
|
+
|
|
642
|
+
#### `MemberProperty` — Group Member (RFC 6350 §6.6.5)
|
|
643
|
+
|
|
644
|
+
Cardinality: `*`. URI value. Used in `KIND:group` cards to list members.
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
new MemberProperty('urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6')
|
|
648
|
+
new MemberProperty('mailto:bob@example.com')
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
#### `RelatedProperty` — Related Entity (RFC 6350 §6.6.6)
|
|
652
|
+
|
|
653
|
+
Cardinality: `*`. May be a URI or text (`VALUE=text`).
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
class RelatedProperty extends Property {
|
|
657
|
+
value: string;
|
|
658
|
+
isUri: boolean;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
RelatedProperty.fromText('urn:uuid:...', params) // URI
|
|
662
|
+
RelatedProperty.fromText('Jane Doe', // text
|
|
663
|
+
new Map([['VALUE', 'text'], ['TYPE', 'spouse']]))
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Well-known TYPE values: `contact`, `acquaintance`, `friend`, `met`, `co-worker`, `colleague`, `co-resident`, `neighbor`, `child`, `parent`, `sibling`, `spouse`, `kin`, `muse`, `crush`, `date`, `sweetheart`, `me`, `agent`, `emergency`.
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
### Explanatory
|
|
671
|
+
|
|
672
|
+
#### `CategoriesProperty` — Categories (RFC 6350 §6.7.1)
|
|
673
|
+
|
|
674
|
+
Cardinality: `*`. Value is a comma-separated list of text tags.
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
new CategoriesProperty(['friend', 'colleague', 'vip'])
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
#### `NoteProperty` — Note (RFC 6350 §6.7.2)
|
|
681
|
+
|
|
682
|
+
Cardinality: `*`. Text value; newlines are encoded as `\n` in the vCard text.
|
|
683
|
+
|
|
684
|
+
#### `ProdIDProperty` — Product Identifier (RFC 6350 §6.7.3)
|
|
685
|
+
|
|
686
|
+
Cardinality: `*1`. Should identify the software that created the vCard.
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
vc.prodid = new ProdIDProperty('-//My App//My App 1.0//EN');
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
#### `RevProperty` — Revision Timestamp (RFC 6350 §6.7.4)
|
|
693
|
+
|
|
694
|
+
Cardinality: `*1`. Stored as a JavaScript `Date`, or a raw string if parsing failed.
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
class RevProperty extends Property {
|
|
698
|
+
value: Date | string;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
vc.rev = new RevProperty(new Date());
|
|
702
|
+
// Serialized as: REV:20240615T103000Z
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
#### `SoundProperty` — Sound (RFC 6350 §6.7.5)
|
|
706
|
+
|
|
707
|
+
Cardinality: `*`. URI value.
|
|
708
|
+
|
|
709
|
+
#### `UIDProperty` — Unique Identifier (RFC 6350 §6.7.6)
|
|
710
|
+
|
|
711
|
+
Cardinality: `*1`. Typically a `urn:uuid:` URI, but may be any URI or text.
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
vc.uid = new UIDProperty('urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6');
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
When the value looks like a URI (has a scheme), it is serialized without text escaping.
|
|
718
|
+
|
|
719
|
+
#### `ClientPidMapProperty` — Client PID Map (RFC 6350 §6.7.7)
|
|
720
|
+
|
|
721
|
+
Cardinality: `*`. Used for synchronisation between CardDAV clients. The value is a semicolon-separated pair of a PID number and a URI.
|
|
722
|
+
|
|
723
|
+
```ts
|
|
724
|
+
class ClientPidMapProperty extends Property {
|
|
725
|
+
value: ClientPidMap;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
interface ClientPidMap {
|
|
729
|
+
pid: number;
|
|
730
|
+
uri: string;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
new ClientPidMapProperty({ pid: 1, uri: 'urn:uuid:...' })
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
#### `URLProperty` — URL (RFC 6350 §6.7.8)
|
|
737
|
+
|
|
738
|
+
Cardinality: `*`. URI value.
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
### Security
|
|
743
|
+
|
|
744
|
+
#### `KeyProperty` — Public Key (RFC 6350 §6.8.1)
|
|
745
|
+
|
|
746
|
+
Cardinality: `*`. May be a URI or inline base64-encoded data.
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
class KeyProperty extends Property {
|
|
750
|
+
value: string;
|
|
751
|
+
isUri: boolean;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
KeyProperty.fromText('http://example.com/key.pgp',
|
|
755
|
+
new Map([['VALUE', 'uri'], ['TYPE', 'work']]))
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
### Calendar
|
|
761
|
+
|
|
762
|
+
#### `FBURLProperty` — Free/Busy URL (RFC 6350 §6.9.1)
|
|
763
|
+
|
|
764
|
+
Cardinality: `*`. URI value.
|
|
765
|
+
|
|
766
|
+
#### `CALADRURIProperty` — Calendar User Address URI (RFC 6350 §6.9.2)
|
|
767
|
+
|
|
768
|
+
Cardinality: `*`. URI value. Used to schedule meetings with the contact.
|
|
769
|
+
|
|
770
|
+
#### `CALURIProperty` — Calendar URI (RFC 6350 §6.9.3)
|
|
771
|
+
|
|
772
|
+
Cardinality: `*`. URI value.
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
### General
|
|
777
|
+
|
|
778
|
+
#### `KindProperty` — Kind (RFC 6350 §6.1.4)
|
|
779
|
+
|
|
780
|
+
Cardinality: `*1`. Classifies the vCard object.
|
|
781
|
+
|
|
782
|
+
```ts
|
|
783
|
+
vc.kind = new KindProperty('individual'); // default
|
|
784
|
+
vc.kind = new KindProperty('group'); // distribution list
|
|
785
|
+
vc.kind = new KindProperty('org'); // organisation
|
|
786
|
+
vc.kind = new KindProperty('location'); // place
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
#### `SourceProperty` — Source (RFC 6350 §6.1.3)
|
|
790
|
+
|
|
791
|
+
Cardinality: `*`. URI indicating where the vCard data can be fetched.
|
|
792
|
+
|
|
793
|
+
#### `XMLProperty` — XML (RFC 6350 §6.1.5)
|
|
794
|
+
|
|
795
|
+
Cardinality: `*`. Extends vCard with XML data. The value is escaped text containing XML.
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
### Unknown and extended properties
|
|
800
|
+
|
|
801
|
+
Any property not listed in RFC 6350 — including `X-` vendor extensions, proprietary Apple/Google/Outlook properties, and unknown IANA properties — is stored as an `UnknownProperty` in `vcard.extended`. This ensures round-trip fidelity.
|
|
802
|
+
|
|
803
|
+
```ts
|
|
804
|
+
class UnknownProperty extends Property {
|
|
805
|
+
rawValue: string; // the raw, uninterpreted value string
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Example: Apple-specific X-ABLabel grouped with an email
|
|
809
|
+
// item1.EMAIL:john@example.com
|
|
810
|
+
// item1.X-ABLabel:Work
|
|
811
|
+
const label = vc.extended.find(p => p.name === 'X-ABLABEL' && p.group === 'item1');
|
|
812
|
+
console.log(label?.rawValue); // 'Work'
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
Unknown properties are serialized back verbatim, preserving groups and parameters.
|
|
816
|
+
|
|
817
|
+
---
|
|
818
|
+
|
|
819
|
+
## Date and time values
|
|
820
|
+
|
|
821
|
+
RFC 6350 §4.3 defines several date/time formats. The library uses the `DateAndOrTime` interface for structured representation.
|
|
822
|
+
|
|
823
|
+
```ts
|
|
824
|
+
interface DateAndOrTime {
|
|
825
|
+
year?: number;
|
|
826
|
+
month?: number;
|
|
827
|
+
day?: number;
|
|
828
|
+
hour?: number;
|
|
829
|
+
minute?: number;
|
|
830
|
+
second?: number;
|
|
831
|
+
utcOffset?: string; // 'Z', '+HH:MM', '-HH:MM', '+HHMM', etc.
|
|
832
|
+
hasTime: boolean;
|
|
833
|
+
}
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### `parseDateAndOrTime(value: string): DateAndOrTime | null`
|
|
837
|
+
|
|
838
|
+
Parse any RFC 6350 date/time string.
|
|
839
|
+
|
|
840
|
+
```ts
|
|
841
|
+
import { parseDateAndOrTime } from '@pipobscure/vcard';
|
|
842
|
+
|
|
843
|
+
parseDateAndOrTime('19900315')
|
|
844
|
+
// { year: 1990, month: 3, day: 15, hasTime: false }
|
|
845
|
+
|
|
846
|
+
parseDateAndOrTime('--0315')
|
|
847
|
+
// { year: undefined, month: 3, day: 15, hasTime: false }
|
|
848
|
+
// (birthday where year is not known)
|
|
849
|
+
|
|
850
|
+
parseDateAndOrTime('1990')
|
|
851
|
+
// { year: 1990, hasTime: false }
|
|
852
|
+
|
|
853
|
+
parseDateAndOrTime('20090808T1430-0500')
|
|
854
|
+
// { year: 2009, month: 8, day: 8, hour: 14, minute: 30, utcOffset: '-0500', hasTime: true }
|
|
855
|
+
|
|
856
|
+
parseDateAndOrTime('20240101T120000Z')
|
|
857
|
+
// { year: 2024, month: 1, day: 1, hour: 12, minute: 0, second: 0, utcOffset: 'Z', hasTime: true }
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
Returns `null` if the string is empty or completely unparseable.
|
|
861
|
+
|
|
862
|
+
### `formatDateAndOrTime(dt: DateAndOrTime): string`
|
|
863
|
+
|
|
864
|
+
Serialize a `DateAndOrTime` back to RFC 6350 text.
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
## Escaping utilities
|
|
869
|
+
|
|
870
|
+
These are exported for advanced use; the library handles them automatically during parsing and generation.
|
|
871
|
+
|
|
872
|
+
```ts
|
|
873
|
+
import {
|
|
874
|
+
escapeText,
|
|
875
|
+
escapeStructuredComponent,
|
|
876
|
+
unescapeText,
|
|
877
|
+
parseStructured,
|
|
878
|
+
parseList,
|
|
879
|
+
parseStructuredList,
|
|
880
|
+
needsParamQuoting,
|
|
881
|
+
quoteParamValue,
|
|
882
|
+
unquoteParamValue,
|
|
883
|
+
splitStructured,
|
|
884
|
+
splitList,
|
|
885
|
+
} from '@pipobscure/vcard';
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
### Text escaping (RFC 6350 §3.4)
|
|
889
|
+
|
|
890
|
+
```ts
|
|
891
|
+
escapeText('Smith, John; Jr.')
|
|
892
|
+
// 'Smith\\, John\\; Jr.'
|
|
893
|
+
|
|
894
|
+
unescapeText('Smith\\, John\\; Jr.')
|
|
895
|
+
// 'Smith, John; Jr.'
|
|
896
|
+
|
|
897
|
+
unescapeText('Line one\\nLine two')
|
|
898
|
+
// 'Line one\nLine two'
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
### Structured value splitting
|
|
902
|
+
|
|
903
|
+
Splitting respects backslash escapes, so an escaped delimiter is not treated as a component boundary.
|
|
904
|
+
|
|
905
|
+
```ts
|
|
906
|
+
// Semicolon-separated (N, ADR, ORG, GENDER)
|
|
907
|
+
parseStructured('Smith;John;Q.;Dr.;')
|
|
908
|
+
// ['Smith', 'John', 'Q.', 'Dr.', '']
|
|
909
|
+
|
|
910
|
+
// Comma-separated (NICKNAME, CATEGORIES)
|
|
911
|
+
parseList('friend,colleague,vip')
|
|
912
|
+
// ['friend', 'colleague', 'vip']
|
|
913
|
+
|
|
914
|
+
// Structured-with-lists (N honorific-suffixes: "ing. jr,M.Sc.")
|
|
915
|
+
parseStructuredList('Smith;Simon;;;ing. jr,M.Sc.')
|
|
916
|
+
// [['Smith'], ['Simon'], [], [], ['ing. jr', 'M.Sc.']]
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
### Parameter quoting
|
|
920
|
+
|
|
921
|
+
```ts
|
|
922
|
+
needsParamQuoting('work,voice') // true
|
|
923
|
+
needsParamQuoting('work') // false
|
|
924
|
+
|
|
925
|
+
quoteParamValue('work,voice') // '"work,voice"'
|
|
926
|
+
quoteParamValue('work') // 'work'
|
|
927
|
+
|
|
928
|
+
unquoteParamValue('"work,voice"') // 'work,voice'
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
---
|
|
932
|
+
|
|
933
|
+
## Validation
|
|
934
|
+
|
|
935
|
+
### `VCardError`
|
|
936
|
+
|
|
937
|
+
Thrown by `vcard.toString()` when the card fails strict validation. Has an optional `property` field naming the offending property.
|
|
938
|
+
|
|
939
|
+
```ts
|
|
940
|
+
import { VCardError } from '@pipobscure/vcard';
|
|
941
|
+
|
|
942
|
+
try {
|
|
943
|
+
const text = vc.toString();
|
|
944
|
+
} catch (err) {
|
|
945
|
+
if (err instanceof VCardError) {
|
|
946
|
+
console.error(`Validation failed on ${err.property}: ${err.message}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
### Validation rules (enforced on generation)
|
|
952
|
+
|
|
953
|
+
| Rule | Detail |
|
|
954
|
+
|------|--------|
|
|
955
|
+
| `FN` required | At least one `FN` property must be present (cardinality `1*`). |
|
|
956
|
+
| `PREF` range | PREF parameter must be an integer between 1 and 100 inclusive. |
|
|
957
|
+
| `GENDER` sex | Must be one of `M`, `F`, `O`, `N`, `U`, or empty string. |
|
|
958
|
+
| `REV` validity | If a `Date` object is stored, it must not be `NaN`. |
|
|
959
|
+
|
|
960
|
+
### `vcard.validate(): ValidationResult`
|
|
961
|
+
|
|
962
|
+
Non-throwing alternative to `toString()` for checking validity.
|
|
963
|
+
|
|
964
|
+
```ts
|
|
965
|
+
interface ValidationResult {
|
|
966
|
+
valid: boolean;
|
|
967
|
+
errors: ValidationError[];
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
interface ValidationError {
|
|
971
|
+
property: string;
|
|
972
|
+
message: string;
|
|
973
|
+
}
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
---
|
|
977
|
+
|
|
978
|
+
## RFC compliance notes
|
|
979
|
+
|
|
980
|
+
### vCard 4.0 (RFC 6350)
|
|
981
|
+
|
|
982
|
+
This library targets RFC 6350. All properties defined in §6 are implemented as typed classes with correct cardinality semantics, value type parsing, and serialization.
|
|
983
|
+
|
|
984
|
+
### Line folding (RFC 6350 §3.2)
|
|
985
|
+
|
|
986
|
+
Lines are folded at **75 octets** (UTF-8 bytes), not 75 characters. This is significant for non-ASCII content: a line of 25 three-byte characters (e.g. CJK) reaches the limit even though it is only 25 characters long.
|
|
987
|
+
|
|
988
|
+
The fold indicator (a single space on the continuation line) is stripped during unfolding. To preserve a word boundary across a fold, include the space as the last character of the preceding segment:
|
|
989
|
+
|
|
990
|
+
```
|
|
991
|
+
FN:A very long name that spans \r\n
|
|
992
|
+
multiple lines\r\n
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
After unfolding: `A very long name that spans multiple lines`.
|
|
996
|
+
|
|
997
|
+
### Text escaping (RFC 6350 §3.4)
|
|
998
|
+
|
|
999
|
+
In `TEXT` value types, the following characters are escaped on output and unescaped on input:
|
|
1000
|
+
|
|
1001
|
+
| Sequence | Meaning |
|
|
1002
|
+
|----------|---------|
|
|
1003
|
+
| `\\` | Literal backslash |
|
|
1004
|
+
| `\n` or `\N` | Newline (U+000A) |
|
|
1005
|
+
| `\,` | Literal comma |
|
|
1006
|
+
| `\;` | Literal semicolon |
|
|
1007
|
+
|
|
1008
|
+
Note that colons do **not** need escaping in property values (the parser finds the first colon to split name from value).
|
|
1009
|
+
|
|
1010
|
+
### Multi-valued parameters
|
|
1011
|
+
|
|
1012
|
+
The `TYPE` parameter may be specified in two equivalent ways, both of which are handled:
|
|
1013
|
+
|
|
1014
|
+
```
|
|
1015
|
+
TEL;TYPE=work;TYPE=voice:...
|
|
1016
|
+
TEL;TYPE="work,voice":...
|
|
1017
|
+
TEL;TYPE=WORK,VOICE:... (v3 style — tolerated)
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
All three produce `prop.type === ['work', 'voice']`.
|
|
1021
|
+
|
|
1022
|
+
### vCard 3.0 and 2.1 compatibility
|
|
1023
|
+
|
|
1024
|
+
The parser accepts v3.0 and v2.1 input:
|
|
1025
|
+
|
|
1026
|
+
- `ENCODING=QUOTED-PRINTABLE` values are decoded (with correct multi-byte UTF-8 support).
|
|
1027
|
+
- `ENCODING=b` (base64) is stripped; the value is stored as-is.
|
|
1028
|
+
- `CHARSET` parameters are accepted and ignored (the library assumes UTF-8 throughout).
|
|
1029
|
+
- The `LABEL` property (removed in v4) is stored as an `UnknownProperty`.
|
|
1030
|
+
- Bare type tokens without `=` (`TEL;WORK;VOICE:...`) are interpreted as `TYPE` values.
|
|
1031
|
+
|
|
1032
|
+
### Property groups (Apple Contacts)
|
|
1033
|
+
|
|
1034
|
+
Apple Contacts uses a grouping mechanism to associate related properties:
|
|
1035
|
+
|
|
1036
|
+
```
|
|
1037
|
+
item1.EMAIL;type=INTERNET:john@example.com
|
|
1038
|
+
item1.X-ABLabel:Work
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
The `group` field on the property is set to `'item1'`. Grouped properties are preserved on round-trip.
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
## Types reference
|
|
1046
|
+
|
|
1047
|
+
```ts
|
|
1048
|
+
// Value types
|
|
1049
|
+
type ValueType = 'text' | 'uri' | 'date' | 'time' | 'date-time' |
|
|
1050
|
+
'date-and-or-time' | 'timestamp' | 'boolean' |
|
|
1051
|
+
'integer' | 'float' | 'utc-offset' | 'language-tag';
|
|
1052
|
+
|
|
1053
|
+
type TypeValue = 'work' | 'home' | 'voice' | 'fax' | 'cell' | 'video' |
|
|
1054
|
+
'pager' | 'textphone' | 'text' | 'contact' | 'friend' |
|
|
1055
|
+
'spouse' | 'child' | 'parent' | /* ... */ | string;
|
|
1056
|
+
|
|
1057
|
+
type KindValue = 'individual' | 'group' | 'org' | 'location' | string;
|
|
1058
|
+
type GenderSex = 'M' | 'F' | 'O' | 'N' | 'U' | '';
|
|
1059
|
+
|
|
1060
|
+
// Structured values
|
|
1061
|
+
interface StructuredName { familyNames, givenNames, additionalNames,
|
|
1062
|
+
honorificPrefixes, honorificSuffixes }
|
|
1063
|
+
interface Address { postOfficeBox, extendedAddress, streetAddress,
|
|
1064
|
+
locality, region, postalCode, countryName }
|
|
1065
|
+
interface Organization { name, units }
|
|
1066
|
+
interface Gender { sex, identity? }
|
|
1067
|
+
interface ClientPidMap { pid, uri }
|
|
1068
|
+
interface DateAndOrTime { year?, month?, day?, hour?, minute?, second?,
|
|
1069
|
+
utcOffset?, hasTime }
|
|
1070
|
+
|
|
1071
|
+
// Parameter map
|
|
1072
|
+
type ParameterMap = Map<string, string | string[]>;
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
---
|
|
1076
|
+
|
|
1077
|
+
## License
|
|
1078
|
+
|
|
1079
|
+
MIT
|