--- id: 599 title: Creating vCards from h-cards date: 2015-12-27T15:17:12+00:00 author: Brandon Rozek layout: post guid: https://brandonrozek.com/?p=599 aliases: - /2015/12/creating-vcards-from-h-cards/ permalink: /2015/12/creating-vcards-from-h-cards/ medium_post: - 'O:11:"Medium_Post":11:{s:16:"author_image_url";N;s:10:"author_url";N;s:11:"byline_name";N;s:12:"byline_email";N;s:10:"cross_link";N;s:2:"id";N;s:21:"follower_notification";N;s:7:"license";N;s:14:"publication_id";N;s:6:"status";N;s:3:"url";N;}' - 'O:11:"Medium_Post":11:{s:16:"author_image_url";s:74:"https://cdn-images-1.medium.com/fit/c/200/200/1*dmbNkD5D-u45r44go_cf0g.png";s:10:"author_url";s:32:"https://medium.com/@brandonrozek";s:11:"byline_name";N;s:12:"byline_email";N;s:10:"cross_link";s:2:"no";s:2:"id";s:12:"9eab6bd8e0e4";s:21:"follower_notification";s:3:"yes";s:7:"license";s:19:"all-rights-reserved";s:14:"publication_id";s:2:"-1";s:6:"status";s:6:"public";s:3:"url";s:74:"https://medium.com/@brandonrozek/creating-vcards-from-h-cards-9eab6bd8e0e4";}' mf2_cite: - 'a:1:{s:6:"author";a:0:{}}' tumblr_post_id: - "136059699334" kind: - article --- Microformats is semantic HTML used to convey metadata. Using an userscript, I can generate a vCard from the representative h-card of the page. The code for this is on this gist [here.](https://gist.github.com/brandonrozek/e0153b2733e947fa9c87) ### Terminology [Microformats](http://microformats.org/wiki/Main_Page) allows search engines, browsers, websites, or people like me to consume content on a site. [H-card](http://microformats.org/wiki/h-card) is a type of microformat that serves as a contact card for people and organizations. [vCard](https://en.wikipedia.org/wiki/VCard) is the standard for electronic business cards. They’re most likely used in your phone to store contacts. Userscript is essentially JavaScript that runs in the [Greasemonkey](http://www.greasespot.net/) extension. ### What I’ll need * Microformat parser * Way to find the representative h-card * Way to generate a vCard from an h-card * The userscript itself ### Implementation To keep everything in small [reusable components](https://en.wikipedia.org/wiki/Modular_programming), I created four different sections. Thankfully, [Glenn Jones](http://glennjones.net/) already wrote a JavaScript microformats parser called [microformat-shiv.](https://github.com/glennjones/microformat-shiv) It’s licensed with [MIT](https://tldrlegal.com/license/mit-license), so we can use it, yay! Next, I need to find the representative h-card of the page. Following the [instructions](http://microformats.org/wiki/representative-h-card-parsing) on the microformats wiki, I wrote the following code.
/*
representative-h-card - v0.1.0
Copyright (c) 2015 Brandon Rozek
Licensed MIT
*/
/**
Finds the representative h-card of the page
[http://microformats.org/wiki/representative-h-card-parsing]
@returns representative h-card if found, null otherwise
**/
var representativeHCard = function(hCards, url) {
if (hCards.items.length == 0) {
return null;
} else if (hCards.items.length == 1 && urlsMatchURL(hCards.items[0], url)) {
hCard = hCards;
hCard.items = [hCards.items[0]];
return hCard
} else {
for (var i = 0; i < hCards.items.length; i++) {
if (urlsMatchURL(hCards.items[i], url) && (uidsMatchURL(hCards.items[i], url) || relMeMatchURL(hCards, url))) {
hCard = hCards;
hCard.items = [hCards.items[i]];
return hCard
}
}
}
return null;
}
var urlsMatchURL = function(hCard, url) {
var urls = hCard.properties.url;
if (typeof(urls) == "object") {
for (var i = 0; i < urls.length; i++) {
if (new URL(urls[i]).toString() == new URL(url).toString()) {
return true;
}
}
}
return false;
}
var uidsMatchURL = function(hCard, url) {
var uids = hCard.properties.uid;
if (typeof(uids) == "object") {
for (var i = 0; i < uids.length; i++) {
if (new URL(uids[i]).toString() == new URL(url).toString()) {
return true;
}
}
}
return false;
};
var relMeMatchURL = function(microformats, url) {
var me = microformats.rels.me;
if (typeof(me) == "object") {
for (var i = 0; i < me.length; i++) {
if (new URL(me[i]).toString() == new URL(url).toString()) {
return true;
}
}
}
return false;
}
Next up, is making the vCard. For this, I had to look at the [vCard 4.0 specification](https://tools.ietf.org/html/rfc6350) to figure out what the property names and values are. Then I browsed around different sites (takes you to a random [Indieweb](https://indiewebcamp.com/) site) to figure out which properties are the most common.
The properties I ended up adding to the vCard.
* name
* photo
* telephone numbers
* URLs
* [IMPPs](https://en.wikipedia.org/wiki/Instant_Messaging_and_Presence_Protocol) (Instant Messaging and Presence Protocol)
* emails
* roles
* categories
* notes
As I was browsing around, I noticed that a few people would have empty values for certain properties on their h-card. To avoid having this show up on the vCard, I added a filter that takes out empty strings.
/*
vCard-from-h-card - v0.1.0
Copyright (c) 2015 Brandon Rozek
Licensed MIT
*/
var makeVCard = function(hCard) {
var vCard = "BEGIN:VCARDnVERSION:4.0n";
//Add full name
var name = hCard.items[0].properties.name;
if (typeof(name) == "object") {
name.removeEmptyStrings();
for (var i = 0; i < name.length; i++) {
vCard += "FN: " + name[i] + "n";
}
}
//Add photo
var photo = hCard.items[0].properties.photo;
if (typeof(photo) == "object") {
photo.removeEmptyStrings();
for (var i = 0; i < photo.length; i++) {
vCard += "PHOTO: " + photo[i] + "n";
}
}
//Add phone number
var tel = hCard.items[0].properties.tel;
if (typeof(tel) == "object") {
tel.removeEmptyStrings();
for (var i = 0; i < tel.length; i++) {
try {
if (new URL(tel[i]).schema == "sms:") {
vCard += "TEL;TYPE=text;VALUE=text: " + new URL(tel[i]).pathname + "n";
} else {
vCard += "TEL;TYPE=voice;VALUE=text: " + new URL(tel[i]).pathname + "n";
}
} catch(e) {
vCard += "TEL;TYPE=voice;VALUE=text: " + tel[i] + "n";
}
}
}
//Add URLs
var url = hCard.items[0].properties.url;
if (typeof(url) == "object") {
url.removeEmptyStrings();
for (var i = 0; i < url.length; i++) {
vCard += "URL: " + url[i] + "n";
}
}
var impp = hCard.items[0].properties.impp;
//Add IMPP (Instant Messaging and Presence Protocol)
if (typeof(impp) == "object") {
impp.removeEmptyStrings();
for (var i = 0; i < impp.length; i++) {
vCard += "IMPP;PREF=" + (i + 1) + ": " + impp[i] + "n";
}
}
//Add emails
var email = hCard.items[0].properties.email;
if (typeof(email) == "object") {
email.removeEmptyStrings();
for (var i = 0; i < email.length; i++) {
try {
vCard += "EMAIL: " + new URL(email[i]).pathname + "n";
} catch (e) {
vCard += "EMAIL: " + email[i] + "n"
}
}
}
//Add roles
var role = hCard.items[0].properties.role;
if (typeof(role) == "object") {
role.removeEmptyStrings();
for (var i = 0; i < role.length; i++) {
vCard += "ROLE: " + role[i] + "n";
}
}
//Add Organizations
var org = hCard.items[0].properties.org;
if (typeof(org) == "object") {
org.removeEmptyStrings();
for (var i = 0; i < org.length; i++) {
vCard += "ORG: " + org[i] + "n";
}
}
//Add Categories
var category = hCard.items[0].properties.category;
if (typeof(category) == "object") {
vCard += "CATEGORIES: " + category.removeEmptyStrings().join(",") + "n";
}
//Add notes
var note = hCard.items[0].properties.note;
if (typeof(note) == "object") {
note.removeEmptyStrings();
for (var i = 0; i < note.length; i++) {
vCard += "NOTE: " + note[i] + "n";
}
}
return vCard + "END:VCARD";
}
Array.prototype.removeEmptyStrings = function() {
return this.filter(function(i) { return i !== "" })
}
Now for the final part, making the userscript. Inspired by [Ryan Barret](https://snarfed.org/) and his userscript [Let’s Talk,](https://github.com/snarfed/misc/blob/master/userscripts/lets_talk.user.js) this userscript brings all of the above modules together. First it grabs the microformats from the page using microformat-shiv.
For some reason, when I tried filtering it by ‘h-card’ it froze my computer. So I wrote my own little filter instead.
After I grab the representative h-card from the page using the little module I wrote, I generated a vCard. With the vCard generated, I set up a little HTML and CSS to display the link in the top left corner of the screen.
The link is actually a [data uri](https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs) that has all of the information of the vCard encoded in it. Depending on your browser, once you click the link you might have to hit CTRL-S to save.
/*
show-vCard - v0.1.0
Copyright (c) 2015 Brandon Rozek
Licensed MIT
*/
var filterMicroformats = function(items, filter) {
var newItems = [];
for (var i = 0; i < items.items.length; i++) {
for (var k = 0; k < items.items[i].type.length; k++) {
if (filter.indexOf(items.items[i].type[k]) != -1) {
newItems.push(items.items[i]);
}
}
}
items.items = newItems;
return items;
}
var render = function() {
var hCards = filterMicroformats(Microformats.get(), ['h-card']);
var person = representativeHCard(hCards, location.origin);
if (person == null) {
return;
}
var node = document.createElement("div");
node.setAttribute("class", "lt");
var link = "<a href="text/vcf;base64," + btoa(makeVCard(person))+ "" target="_blank">vCard</a>";
var style = "
.lt {
position: absolute;
left: 24px;
top: 0;
color: #DDD;
background-color: #FFD700;
z-index: 9999;
border-width: medium 1px 1px;
border-style: none solid solid;
border-color: #DDD #C7A900 #9E8600;
box-shadow: 0px 1px rgba(0, 0, 0, 0.1), 0px 1px 2px rgba(0, 0, 0, 0.1), 0px 1px rgba(255, 255, 255, 0.34) inset;
border-radius: 0px 0px 4px 4px;
}
.lt a {
padding: .5rem;
color: #8f6900;
text-shadow: 0px 1px #FFE770;
border: medium none;
}
";
node.innerHTML = link + style;
document.body.appendChild(node);
}
document.addEventListener("DOMContentLoaded", function() {
render();
});
Sadly, I have no way of controlling the file name when you save it so you’ll have to manually rename it to something more meaningful than a random string of characters. Also remember to add the extension ‘.vcf’ for it to be recognized by some devices.
### Conclusion
Fire up your favorite userscript handling tool and add the [script](https://gist.github.com/brandonrozek/e0153b2733e947fa9c87) in! Of course, since it’s pure JavaScript, you can also add it to your own site to serve the same purpose.
I ran into a small problem loading a contact onto my Android 5.0.2 phone. Apparently, they don’t support vCard 4.0 yet so I had to go into the file and change the line that says “VERSION 4.0” to “VERSION 3.0” which then allowed me to import the file into my contacts.
As with all the code I write, feel free to comment/criticize. I love hearing feedback so if you spot anything, [contact me](mailto:hello@brandonrozek.com) 🙂
Also posted on [IndieNews](http://news.indiewebcamp.com/en){.u-syndication}