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