1<?php 2/** 3 * vCard Plugin: creates a link to download a vCard file 4 * uses the vCard class by Kai Blankenhorn <kaib@bitfolge.de> 5 * 6 * Can also output hCard microformat: 7 * @link http://microformats.org/wiki/hcard 8 * 9 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 10 * @author Esther Brunner <esther@kaffeehaus.ch> 11 * @author Jürgen A.Lamers <jaloma.ac@googlemail.de> 12 * @author Elan Ruusamäe <glen@delfi.ee> 13 */ 14 15// must be run within Dokuwiki 16if(!defined('DOKU_INC')) die(); 17 18/** 19 * All DokuWiki plugins to extend the parser/rendering mechanism 20 * need to inherit from this class 21 */ 22class syntax_plugin_vcard extends DokuWiki_Syntax_Plugin { 23 /** 24 * plugin folded is present and enabled 25 * @var bool $have_folded 26 */ 27 private $have_folded = false; 28 29 public function __construct() { 30 $this->have_folded = !plugin_isdisabled('folded'); 31 } 32 33 function getType(){ return 'substition'; } 34 function getSort(){ return 314; } 35 function connectTo($mode) { $this->Lexer->addSpecialPattern("{{vcard>.*?}}", $mode, 'plugin_vcard'); } 36 37 /** 38 * Handle the match 39 */ 40 function handle($match, $state, $pos, Doku_Handler $handler) { 41 // strip markup 42 $match = substr($match, 8, -2); 43 44 // split address from rest and divide it into parts 45 list($match, $address) = explode('|', $match, 2); 46 if ($address) { 47 list($street, $place, $country) = explode(',', $address, 3); 48 list($zip, $city) = explode(' ', trim($place), 2); 49 } 50 51 // split phone(s) from rest and create an array 52 list($match, $phones) = explode('#', $match, 2); 53 $phones = explode('&', $phones); 54 foreach ($phones as $phone) { 55 $phone = trim($phone); 56 } 57 58 // get birthday 59 if (preg_match('/\d{4}\-\d{2}\-\d{2}/', $match, $m)) { 60 $birthday = $m[0]; 61 } 62 63 // get website 64 $punc = '.:?\-;,'; 65 $any = '\w/~:.?+=&%@!\-'; 66 if (preg_match('#http://['.$any.']+?(?=['.$punc.']*[^'.$any.'])#i',$match, $m)) { 67 $website = $m[0]; 68 } 69 70 // get e-mail address 71 if (preg_match('/<[\w0-9\-_.]+?@[\w\-]+\.[\w\-\.]+\.*[\w]+>/i', $match, $email, PREG_OFFSET_CAPTURE)) { 72 $match = substr($match,0,$email[0][1]); 73 $email = substr($email[0][0],1,-1); 74 } 75 76 // get company name 77 if (preg_match('/\[(.*)\]/', $match, $m)) { 78 $match = str_replace($m[0], '', $match); 79 $company = $m[1]; 80 } 81 82 // the rest is the name 83 $match = trim($match); 84 $pos = strrpos($match, ' '); 85 if ($pos !== false) { 86 list($first,$middle) = explode(' ', substr($match, 0, $pos), 2); 87 $last = substr($match, $pos + 1); 88 } else { 89 $first = $match; 90 $middle = null; 91 $last = null; 92 } 93 94 return array( 95 'given-name' => $first, 96 'additional-name' => $middle, 97 'family-name' => $last, 98 'email' => $email, 99 'website' => $website, 100 'bday' => $birthday, 101 'work' => trim($phones[0]), 102 'cell' => trim($phones[1]), 103 'home' => trim($phones[2]), 104 'fax' => trim($phones[3]), 105 'street-address' => trim($street), 106 'postal-code' => $zip, 107 'locality' => $city, 108 'country-name' => trim($country), 109 'org' => $company, 110 ); 111 } 112 113 /** 114 * Create output 115 */ 116 function render($mode, Doku_Renderer $renderer, $data) { 117 if ($mode != 'xhtml') { 118 return false; 119 } 120 121 $html = ''; 122 $hcard = $this->getConf('do_hcard'); 123 124 if ($this->getConf('email_shortcut')) { 125 $html .= ' '.$this->_emaillink($renderer, $data['email']).' '; 126 } 127 128 if ($hcard) { 129 $name = $this->_tagclass('given-name', $data['given-name']); 130 131 if ($data['additional-name']) { 132 $name .= ' '.$this->_tagclass('additional-name', $data['additional-name']); 133 } 134 135 if ($data['family-name']) { 136 $name .= ' '.$this->_tagclass('family-name', $data['family-name']); 137 } 138 } else { 139 $name = $renderer->_xmlEntities($data['given-name']. ($data['family-name'] ? ' '.$data['family-name'] : '')); 140 } 141 142 $url = DOKU_URL.'lib/plugins/vcard/vcard.php?'.buildURLparams($data); 143 $html .= $this->_weblink($renderer, $url, $name, 'iw_vcard url fn n', $data['email']); 144 145 if ($this->have_folded) { 146 global $plugin_folded_count; 147 $plugin_folded_count++; 148 149 // folded plugin is installed: enables additional feature 150 $html .= '<a href="#folded_'.$plugin_folded_count.'" class="folder"></a>'; 151 $html .= '<span class="folded hidden" id="folded_'.$plugin_folded_count.'">'; 152 153 if ($hcard) { 154 $html .= $this->_tag('folded', $this->_folded_hcard($renderer, $data)); 155 } else { 156 $html .= $this->_folded_vcard($renderer, $data); 157 } 158 159 $html .= '</span>'; 160 } 161 162 if ($hcard) { 163 $renderer->doc .= $this->_tagclass('vcard', $html); 164 } else { 165 $renderer->doc .= $html; 166 } 167 168 return true; 169 } 170 171 /** 172 * Build hCard formatted folded body 173 */ 174 private function _folded_hcard(&$renderer, $data) { 175 $folded = ''; 176 177 if ($data['org']) { 178 $folded .= $this->_tag('org', '<b class="org">'.$data['org'].'</b>'); 179 } 180 181 if ($data['email']) { 182 $folded .= ' <b>'.$this->getLang('email').'</b> '; 183 $folded .= $this->_emaillink($renderer, $data['email'], $data['email']); 184 } 185 186 if ($data['work']) { 187 $folded .= $this->_tel($renderer, 'work', $data['work']); 188 } 189 190 if ($data['cell']) { 191 $folded .= $this->_tel($renderer, 'cell', $data['cell']); 192 } 193 194 if ($data['home']) { 195 $folded .= $this->_tel($renderer, 'home', $data['home']); 196 } 197 198 if ($data['fax']) { 199 $folded .= $this->_tel($renderer, 'fax', $data['fax']); 200 } 201 202 if ($data['website']) { 203 $html = '<b>'.$this->getLang('website').'</b> '; 204 $html .= $this->_weblink($renderer, $data['website'], $renderer->_xmlEntities($data['website']), 'url'); 205 $folded .= $this->_tag('url', $html); 206 } 207 208 if ($data['bday']) { 209 $html = '<b>'.$this->getLang('bday').'</b> '. $renderer->_xmlEntities($data['bday']); 210 $folded .= $this->_tagclass('bday', $html); 211 } 212 213 $addr = array(); 214 if ($data['street-address']) { 215 $html = $renderer->_xmlEntities($data['street-address']); 216 $addr[] = $this->_tagclass('street-address', $html); 217 } 218 219 // postal code and locality (city) separated by space 220 if ($data['postal-code'] || $data['locality']) { 221 $loc = array(); 222 if ($data['postal-code']) { 223 $html = $renderer->_xmlEntities($data['postal-code']); 224 $loc[] = $this->_tagclass('postal-code', $html); 225 } 226 227 if ($data['locality']) { 228 $html = $renderer->_xmlEntities($data['locality']); 229 $loc[] = $this->_tagclass('locality', $html); 230 } 231 $addr[] = join(' ', $loc); 232 } 233 234 if ($data['country-name']) { 235 $html = $renderer->_xmlEntities($data['country-name']); 236 $addr[] = $this->_tagclass('country-name', $html); 237 } 238 239 if (!empty($addr)) { 240 $folded .= ' <b>'.$this->getLang('address').'</b> '. join(', ', $addr); 241 } 242 243 return $folded; 244 } 245 246 /** 247 * Build plain formatting for vCard 248 * @deprecated, to be dropped if hCard output is complete, because this 249 * portion is incomplete and hCard provides similar (but complete) output 250 */ 251 private function _folded_vcard(&$renderer, $data) { 252 $folded = ''; 253 254 if ($data['org']) { 255 $folded .= '<b>'.$data['org'].'</b>'; 256 } 257 258 if ($data['email']) { 259 $folded .= ' '.$this->_emaillink($renderer, $data['email'], $data['email']); 260 } 261 262 if ($data['work']) { 263 $html = $renderer->_xmlEntities($data['work']); 264 $folded .= ' <b>'.$this->getLang('work').':</b> '. $html; 265 } 266 267 if ($data['cell']) { 268 $html = $renderer->_xmlEntities($data['cell']); 269 $folded .= ' <b>'.$this->getLang('cell').':</b> '. $html; 270 } 271 272 if ($data['home']) { 273 $html = $renderer->_xmlEntities($data['home']); 274 $folded .= ' <b>'.$this->getLang('home').'</b> '. $html; 275 } 276 277 if ($data['website']) { 278 $html = $renderer->_xmlEntities($data['website']); 279 $folded .= $this->_weblink($renderer, $data['website'], $html, 'url'); 280 } 281 282 if ($data['street-address']) { 283 $folded .= ' '.$renderer->_xmlEntities($data['street-address']).','; 284 } 285 286 if ($data['postal-code']) { 287 $folded .= ' '.$renderer->_xmlEntities($data['postal-code']); 288 } 289 290 if ($data['locality']) { 291 $folded .= ' '.$renderer->_xmlEntities($data['locality']); 292 } 293 294 return $folded; 295 } 296 297 /** 298 * create $tag with $class 299 */ 300 private function _tag($tag_, $text, $class = '') { 301 $tag = $this->getConf("tag_$tag_"); 302 if (!$tag) { 303 $tag = 'span'; 304 } 305 $html = ''; 306 $html .= '<'.$tag; 307 if ($class) { 308 $html .= ' class="'. $class. '"'; 309 } 310 311 // if tag is 'abbr', use translation and value in title 312 if ($tag == 'abbr') { 313 $html .= ' title="'. $text. '"'; 314 $html .= '>'.$this->getLang($text); 315 } else { 316 $html .= '>'.$text; 317 } 318 $html .= '</'.$tag.'>'; 319 return $html; 320 } 321 322 /** 323 * create tag with class as $tag 324 */ 325 private function _tagclass($tag, $text) { 326 return $this->_tag($tag, $text, $tag); 327 } 328 329 /** 330 * normalize telephone number to include all non-numeric outside class=value 331 * see http://microformats.org/wiki/value-class-pattern 332 */ 333 private function _tel_normalize($value) { 334 $res = array(); 335 336 // match all but +, digits and space 337 if (!preg_match_all('/[^+\d\s]+/', $value, $matches, PREG_OFFSET_CAPTURE)) { 338 $res[] = array('+', $value); 339 return $res; 340 } 341 342 $offset = 0; 343 foreach ($matches[0] as $match) { 344 $v = substr($value, $offset, $match[1] - $offset); 345 if ($v) { 346 $res[] = array('+', $v); 347 } 348 $res[] = array('-', $match[0]); 349 $offset = $match[1] + strlen($match[0]); 350 } 351 $v = substr($value, $offset); 352 if ($v) { 353 $res[] = array('+', $v); 354 } 355 return $res; 356 } 357 358 /** 359 * format hcard telephone numbers 360 */ 361 private function _tel(&$renderer, $type, $value) { 362 363 $type = '<b>'.$this->_tag('tel_type_'.$type, $type, 'type').'</b> '; 364 365 $values = ''; 366 foreach ($this->_tel_normalize($value) as $res) { 367 list($t, $value) = $res; 368 if ($t === '+') { 369 $values .= $this->_tag('tel_value', $renderer->_xmlEntities($value), 'value'); 370 } else { 371 $values .= $value; 372 } 373 } 374 375 return $this->_tagclass('tel', $type.$values); 376 } 377 378 private function _emaillink(&$renderer, $mail, $name = '') { 379 return $renderer->_formatLink(array( 380 'url' => 'mailto:'.$mail, 381 'name' => $name, 382 'class'=> 'email', 383 )); 384 } 385 386 private function _weblink(&$renderer, $url, $name = '', $class = '', $title ='') { 387 global $conf; 388 return $renderer->_formatLink(array( 389 'url' => $url, 390 'name' => $name, 391 'title' => $title, 392 'rel' => 'nofollow', 393 'target' => $conf['target']['extern'], 394 'class'=> trim('urlextern '. $class), 395 )); 396 } 397} 398 399//Setup VIM: ex: noet ts=4 sw=4 enc=utf-8 : 400