PageRenderTime 34ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/src/PEAR2/Text/Markdown/Wiki/Link.php

https://github.com/pear2/Text_Markdown
PHP | 527 lines | 169 code | 46 blank | 312 comment | 11 complexity | 63c5a3cb251100a4f7294543d0719cfd MD5 | raw file
  1. <?php
  2. /**
  3. *
  4. * Replaces wiki and interwiki links in source text with XHTML anchors.
  5. *
  6. * Wiki links are in this format ...
  7. *
  8. * [[wiki page]]
  9. * [[wiki page #anchor]]
  10. * [[wiki page]]s
  11. * [[wiki page | display this instead]]
  12. * [[wiki page #anchor | ]]
  13. *
  14. * The "wiki page" name is normalized to "Wiki_page". The last
  15. * example, the one with the blank display text, will not display
  16. * the anchor fragment.
  17. *
  18. * Page links are replaced with encoded placeholders. At cleanup()
  19. * time, the placeholders are transformed into XHTML anchors.
  20. *
  21. * This plugin also supports Interwiki links, in this format ...
  22. *
  23. * [[site::page]]
  24. * [[site::page #anchor]]
  25. * [[site::page]]s
  26. * [[site::page | display this instead]]
  27. * [[site::page #anchor | ]]
  28. *
  29. * Site prefixes and page names are **not** normalize. The last
  30. * example, the one with the blank display text, will not display
  31. * the site prefix or the anchor fragment.
  32. *
  33. * Interwiki links are replaced with HTML immediately and are not
  34. * checked for existence.
  35. *
  36. * @category Solar
  37. *
  38. * @package Markdown_Wiki
  39. *
  40. * @author Paul M. Jones <pmjones@solarphp.com>
  41. *
  42. * @license http://opensource.org/licenses/bsd-license.php BSD
  43. *
  44. * @version $Id: Link.php 3988 2009-09-04 13:51:51Z pmjones $
  45. *
  46. */
  47. namespace PEAR2\Text;
  48. class Markdown_Wiki_Link extends Markdown_Plugin
  49. {
  50. /**
  51. *
  52. * This is a span plugin.
  53. *
  54. * @var bool
  55. *
  56. */
  57. protected $_is_span = true;
  58. /**
  59. *
  60. * Runs during the cleanup() phase.
  61. *
  62. * @var bool
  63. *
  64. */
  65. protected $_is_cleanup = true;
  66. /**
  67. *
  68. * Array of which pages exist and which don't.
  69. *
  70. * Format is page name => true/false.
  71. *
  72. * @var array
  73. *
  74. */
  75. protected $_pages;
  76. /**
  77. *
  78. * Array of information for each link found in the source text.
  79. *
  80. * Each element is an array with these keys ...
  81. *
  82. * `norm`
  83. * : The normalized form of the page name.
  84. *
  85. * `page`
  86. * : The page name as entered in the source text.
  87. *
  88. * `frag`
  89. * : A fragment anchor for the target page (for example, "#example").
  90. *
  91. * `text`
  92. * : The text to display in place of the page name.
  93. *
  94. * `atch`
  95. * : Attached suffix text to go on the end of the displayed text.
  96. *
  97. * @var array
  98. *
  99. */
  100. protected $_links;
  101. /**
  102. *
  103. * Running count of $this->_links, so we don't have to call count()
  104. * on it all the time.
  105. *
  106. * @var int
  107. *
  108. */
  109. protected $_count = 0;
  110. /**
  111. *
  112. * The name of this class, for identifying encoded keys in the
  113. * source text.
  114. *
  115. * @var string
  116. *
  117. */
  118. protected $_class;
  119. /**
  120. *
  121. * Attribs for 'read' and 'add' style links.
  122. *
  123. * Note that 'href' is special, in that it is an sprintf() format
  124. * string.
  125. *
  126. * @var array
  127. *
  128. */
  129. protected $_attribs = array(
  130. 'read' => array(
  131. 'href' => '/wiki/read/%s'
  132. ),
  133. 'add' => array(
  134. 'href' => '/wiki/add/%s'
  135. ),
  136. );
  137. /**
  138. *
  139. * Array of interwiki site names to base hrefs.
  140. *
  141. * Interwiki href values are actually sprintf() strings, where %s
  142. * will be replaced with the page requested at the interwiki site.
  143. * For example, this key-value pair ...
  144. *
  145. * 'php' => 'http://php.net/%s'
  146. *
  147. * ... means that ``[[php::print()]]`` will become a link to
  148. * ``http://php.net/print()``.
  149. *
  150. * @var array
  151. *
  152. */
  153. protected $_interwiki = array(
  154. 'amazon' => 'http://amazon.com/s?keywords=%s',
  155. 'ask' => 'http://www.ask.com/web?q=%s',
  156. 'google' => 'http://www.google.com/search?q=%s',
  157. 'imdb' => 'http://imdb.com/find?s=all&q=%s',
  158. 'php' => 'http://php.net/%s',
  159. );
  160. /**
  161. *
  162. * Callback to check if pages linked from the source text exist or
  163. * not.
  164. *
  165. * @var callback
  166. *
  167. */
  168. protected $_check_pages = false;
  169. /**
  170. *
  171. * Post-construction tasks to complete object construction.
  172. *
  173. * @return void
  174. *
  175. */
  176. function __construct()
  177. {
  178. $this->_class = get_class($this);
  179. }
  180. /**
  181. *
  182. * Sets the callback to check if pages exist.
  183. *
  184. * The callback has to take exactly one parameter, an array keyed
  185. * on page names, with the value being true or false. It should
  186. * return a similar array, saying whether or not each page in the
  187. * array exists.
  188. *
  189. * If left empty, the plugin will assume all links exist.
  190. *
  191. * @param callback $callback The callback to check if pages exist.
  192. *
  193. * @return array An array of which pages exist and which don't.
  194. *
  195. */
  196. public function setCheckPagesCallback($callback)
  197. {
  198. $this->_check_pages = $callback;
  199. }
  200. /**
  201. *
  202. * Sets one anchor attribute.
  203. *
  204. * @param string $type The anchor type, generally 'read' or 'add'.
  205. *
  206. * @param string $key The attribute key, for example 'href' or 'class'.
  207. *
  208. * @param string $val The attribute value.
  209. *
  210. * @return void
  211. *
  212. */
  213. public function setAttrib($type, $key, $val)
  214. {
  215. $this->_attribs[$type][$key] = $val;
  216. }
  217. /**
  218. *
  219. * Sets one or more interwiki name and href mapping.
  220. *
  221. * Interwiki href values are actually sprintf() strings, where %s
  222. * will be replaced with the page requested at the interwiki site.
  223. *
  224. * @param string|array $spec If a string, the interwiki site name;
  225. * if an array, an array of name => href mappings to merge with
  226. * current interwiki list.
  227. *
  228. * @param string $val If $spec is a string, this is the sprintf()
  229. * format string for the href to the interwiki.
  230. *
  231. * @return void
  232. *
  233. */
  234. public function setInterwiki($spec, $val = null)
  235. {
  236. if (is_array($spec)) {
  237. $this->_interwiki = array_merge($spec, $this->_interwiki);
  238. } else {
  239. $this->_interwiki[$spec] = $val;
  240. }
  241. }
  242. /**
  243. *
  244. * Gets the list of interwiki mappings.
  245. *
  246. * @return array
  247. *
  248. */
  249. public function getInterwiki()
  250. {
  251. return $this->_interwiki;
  252. }
  253. /**
  254. *
  255. * Sets all attributes for one anchor type.
  256. *
  257. * @param string $type The anchor type, generally 'read' or 'add'.
  258. *
  259. * @param array $list The attributes to set in key => value format.
  260. *
  261. * @return void
  262. *
  263. */
  264. public function setAttribs($type, $list)
  265. {
  266. $this->_attribs[$type] = $list;
  267. }
  268. /**
  269. *
  270. * Gets the list of pages found in the source text.
  271. *
  272. * @return array
  273. *
  274. */
  275. public function getPages()
  276. {
  277. return array_keys($this->_pages);
  278. }
  279. /**
  280. *
  281. * Resets this plugin for a new transformation.
  282. *
  283. * @return void
  284. *
  285. */
  286. public function reset()
  287. {
  288. parent::reset();
  289. $this->_links = array();
  290. $this->_pages = array();
  291. $this->_count = 0;
  292. }
  293. /**
  294. *
  295. * Parses the source text for wiki page and interwiki links.
  296. *
  297. * @param string $text The source text.
  298. *
  299. * @return string The parsed text.
  300. *
  301. */
  302. public function parse($text)
  303. {
  304. $regex = '/\[\[(.*?)(\#.*?)?(\|.*?)?\]\](\w*)?/';
  305. return preg_replace_callback(
  306. $regex,
  307. array($this, '_parse'),
  308. $text
  309. );
  310. }
  311. /**
  312. *
  313. * Support callback for parsing wiki links.
  314. *
  315. * @param array $matches Matches from preg_replace_callback().
  316. *
  317. * @return string The replacement text.
  318. *
  319. */
  320. protected function _parse($matches)
  321. {
  322. $page = $matches[1];
  323. $frag = empty($matches[2]) ? null : '#' . trim($matches[2], "# \t");
  324. $text = empty($matches[3]) ? $page . $frag : trim($matches[3], "| \t");
  325. $atch = empty($matches[4]) ? null : trim($matches[4]);
  326. // is this an interwiki page?
  327. $pos = strpos($page, '::');
  328. if ($pos !== false) {
  329. return $this->_interwiki($matches);
  330. }
  331. // normalize the page name
  332. $norm = $this->_normalize($page);
  333. // assume the page exists
  334. $this->_pages[$norm] = true;
  335. // save the link
  336. $this->_links[$this->_count] = array(
  337. 'norm' => $norm,
  338. 'page' => $page,
  339. 'frag' => $frag,
  340. 'text' => $text,
  341. 'atch' => $atch,
  342. );
  343. // generate an escaped WikiLink token to be replaced at
  344. // cleanup() time with real HTML.
  345. $key = $this->_class . ':' . $this->_count ++;
  346. return "\x1B$key\x1B";
  347. }
  348. /**
  349. *
  350. * Normalizes a wiki page name.
  351. *
  352. * @param string $page The page name from the source text.
  353. *
  354. * @return string The normalized page name.
  355. *
  356. */
  357. protected function _normalize($page)
  358. {
  359. // trim, force only the first letter to upper-case (leaving all
  360. // other characters alone), and then replace all whitespace
  361. // runs with a single underscore.
  362. return preg_replace('/\s+/', '_', ucfirst(trim($page)));
  363. }
  364. /**
  365. *
  366. * Support callback for parsing interwiki links.
  367. *
  368. * @param array $matches Matches from preg_replace_callback().
  369. *
  370. * @return string The replacement text.
  371. *
  372. */
  373. protected function _interwiki($matches)
  374. {
  375. $pos = strpos($matches[1], '::');
  376. $site = trim(substr($matches[1], 0, $pos));
  377. $page = trim(substr($matches[1], $pos + 2));
  378. // does the requested interwiki site exist?
  379. if (empty($this->_interwiki[$site])) {
  380. return $matches[0];
  381. }
  382. $frag = empty($matches[2]) ? null : '#' . trim($matches[2], "# \t");
  383. $text = empty($matches[3]) ? $site . '::' . $page . $frag : trim($matches[3], "| \t");
  384. $atch = empty($matches[4]) ? null : trim($matches[4]);
  385. if (empty($text)) {
  386. // support for [[php::function() #anchor | ]] to get "function()"
  387. $text = $page;
  388. }
  389. // allow indiviual access to url page and url fragment
  390. $href = sprintf($this->_interwiki[$site], $page . $frag, $page, $frag);
  391. $html = '<a href="' . $this->_escape($href) . '">'
  392. . $this->_escape($text . $atch)
  393. . '</a>';
  394. return $this->_toHtmlToken($html);
  395. }
  396. /**
  397. *
  398. * Cleans up text to replace encoded placeholders with anchors.
  399. *
  400. * @param string $text The source text with placeholders.
  401. *
  402. * @return string The text with anchors instead of placeholders.
  403. *
  404. */
  405. public function cleanup($text)
  406. {
  407. // first, update $this->_pages against the data store to see
  408. // which pages exist and which do not.
  409. if ($this->_check_pages) {
  410. $this->_pages = call_user_func($this->_check_pages, $this->_pages);
  411. }
  412. // now go through and replace tokens
  413. $regex = "/\x1B{$this->_class}:(.*?)\x1B/";
  414. return preg_replace_callback(
  415. $regex,
  416. array($this, '_cleanup'),
  417. $text
  418. );
  419. }
  420. /**
  421. *
  422. * Support callback for replacing placeholder with anchors.
  423. *
  424. * @param array $matches Matches from preg_replace_callback().
  425. *
  426. * @return string The replacement text.
  427. *
  428. */
  429. protected function _cleanup($matches)
  430. {
  431. $key = $matches[1];
  432. $tmp = $this->_links[$key];
  433. // normalized page name
  434. $norm = $tmp['norm'];
  435. // page name as entered
  436. $page = $tmp['page'];
  437. // anchor "#fragment"
  438. $frag = $tmp['frag'];
  439. // optional display text
  440. $text = $tmp['text'];
  441. if (empty($text)) {
  442. // support for [][page name#anchor | ]] to get "page name"?
  443. $text = $page;
  444. }
  445. // optional attached text outside the link
  446. $atch = $tmp['atch'];
  447. // make sure the page is listed; the check-pages callback
  448. // may not have populated it back.
  449. if (empty($this->_pages[$norm])) {
  450. $this->_pages[$norm] = false;
  451. }
  452. // use "read" or "add" attribs?
  453. if ($this->_pages[$norm]) {
  454. // page exists
  455. $attribs = $this->_attribs['read'];
  456. } else {
  457. // page does not exist
  458. $attribs = $this->_attribs['add'];
  459. }
  460. // make sure we have an href attrib
  461. if (empty($attribs['href'])) {
  462. $attribs['href'] = '%s';
  463. }
  464. // build the opening <a href="" portion of the tag.
  465. $html = '<a href="'
  466. . $this->_escape(sprintf($attribs['href'], $norm . $frag, $norm, $frag))
  467. . '"';
  468. // add attributes and close the opening tag
  469. unset($attribs['href']);
  470. foreach ($attribs as $key => $val) {
  471. $key = $this->_escape($key);
  472. $val = $this->_escape($val);
  473. $html .= " $key=\"$val\"";
  474. }
  475. $html .= ">";
  476. // add the escaped the display text and close the tag
  477. $html .= $this->_escape($text . $atch) . "</a>";
  478. // done!
  479. return $html;
  480. }
  481. }