mirror of
https://github.com/Brandon-Rozek/website.git
synced 2024-11-29 19:57:14 -05:00
324 lines
11 KiB
Markdown
324 lines
11 KiB
Markdown
|
---
|
|||
|
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
|
|||
|
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)
|
|||
|
|
|||
|
<!--more-->
|
|||
|
|
|||
|
### 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.
|
|||
|
|
|||
|
<pre><code class="language-javascript">
|
|||
|
/*
|
|||
|
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;
|
|||
|
}
|
|||
|
</code></pre>
|
|||
|
|
|||
|
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 <a href="http://indieweb.thatmustbe.us/" rel="nofollow">sites</a> (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.
|
|||
|
|
|||
|
<pre><code class="language-javascript">
|
|||
|
/*
|
|||
|
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 !== "" })
|
|||
|
}
|
|||
|
|
|||
|
</code></pre>
|
|||
|
|
|||
|
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.
|
|||
|
|
|||
|
<pre><code class="language-javascript">
|
|||
|
/*
|
|||
|
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();
|
|||
|
});
|
|||
|
</code></pre>
|
|||
|
|
|||
|
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}
|