texy = $texy; $texy->allowed['link/definition'] = TRUE; $texy->addHandler('newReference', array($this, 'solveNewReference')); $texy->addHandler('linkReference', array($this, 'solve')); $texy->addHandler('linkEmail', array($this, 'solveUrlEmail')); $texy->addHandler('linkURL', array($this, 'solveUrlEmail')); $texy->addHandler('beforeParse', array($this, 'beforeParse')); // [reference] $texy->registerLinePattern( array($this, 'patternReference'), '#(\[[^\[\]\*\n'.TEXY_MARK.']+\])#U', 'link/reference' ); // direct url and email $texy->registerLinePattern( array($this, 'patternUrlEmail'), '#(?<=^|[\s([<:\x17])(?:https?://|www\.|ftp://)[0-9.'.TEXY_CHAR.'-][/\d'.TEXY_CHAR.'+\.~%&?@=_:;\#,\x{ad}-]+[/\d'.TEXY_CHAR.'+~%?@=_\#]#u', 'link/url', '#(?:https?://|www\.|ftp://)#u' ); $texy->registerLinePattern( array($this, 'patternUrlEmail'), '#(?<=^|[\s([<:\x17])'.TEXY_EMAIL.'#u', 'link/email', '#'.TEXY_EMAIL.'#u' ); } /** * Text pre-processing. * @param Texy * @param string * @return void */ public function beforeParse($texy, & $text) { self::$livelock = array(); // [la trine]: http://www.latrine.cz/ text odkazu .(title)[class]{style} if (!empty($texy->allowed['link/definition'])) { $text = preg_replace_callback( '#^\[([^\[\]\#\?\*\n]+)\]: +(\S+)(\ .+)?'.TEXY_MODIFIER.'?\s*()$#mUu', array($this, 'patternReferenceDef'), $text ); } } /** * Callback for: [la trine]: http://www.latrine.cz/ text odkazu .(title)[class]{style}. * * @param array regexp matches * @return string */ private function patternReferenceDef($matches) { list(, $mRef, $mLink, $mLabel, $mMod) = $matches; // [1] => [ (reference) ] // [2] => link // [3] => ... // [4] => .(title)[class]{style} $link = new TexyLink($mLink); $link->label = trim($mLabel); $link->modifier->setProperties($mMod); $this->checkLink($link); $this->addReference($mRef, $link); return ''; } /** * Callback for: [ref]. * * @param TexyLineParser * @param array regexp matches * @param string pattern name * @return TexyHtml|string|FALSE */ public function patternReference($parser, $matches) { list(, $mRef) = $matches; // [1] => [ref] $tx = $this->texy; $name = substr($mRef, 1, -1); $link = $this->getReference($name); if (!$link) { return $tx->invokeAroundHandlers('newReference', $parser, array($name)); } $link->type = TexyLink::BRACKET; if ($link->label != '') { // NULL or '' // prevent circular references if (isset(self::$livelock[$link->name])) { $content = $link->label; } else { self::$livelock[$link->name] = TRUE; $el = TexyHtml::el(); $lineParser = new TexyLineParser($tx, $el); $lineParser->parse($link->label); $content = $el->toString($tx); unset(self::$livelock[$link->name]); } } else { $content = $this->textualUrl($link); $content = $this->texy->protect($content, Texy::CONTENT_TEXTUAL); } return $tx->invokeAroundHandlers('linkReference', $parser, array($link, $content)); } /** * Callback for: http://davidgrudl.com david@grudl.com. * * @param TexyLineParser * @param array regexp matches * @param string pattern name * @return TexyHtml|string|FALSE */ public function patternUrlEmail($parser, $matches, $name) { list($mURL) = $matches; // [0] => URL $link = new TexyLink($mURL); $this->checkLink($link); return $this->texy->invokeAroundHandlers( $name === 'link/email' ? 'linkEmail' : 'linkURL', $parser, array($link) ); } /** * Adds new named reference. * * @param string reference name * @param TexyLink * @return void */ public function addReference($name, TexyLink $link) { $link->name = TexyUtf::strtolower($name); $this->references[$link->name] = $link; } /** * Returns named reference. * * @param string reference name * @return TexyLink reference descriptor (or FALSE) */ public function getReference($name) { $name = TexyUtf::strtolower($name); if (isset($this->references[$name])) { return clone $this->references[$name]; } else { $pos = strpos($name, '?'); if ($pos === FALSE) $pos = strpos($name, '#'); if ($pos !== FALSE) { // try to extract ?... #... part $name2 = substr($name, 0, $pos); if (isset($this->references[$name2])) { $link = clone $this->references[$name2]; $link->URL .= substr($name, $pos); return $link; } } } return FALSE; } /** * @param string * @param string * @param string * @return TexyLink */ public function factoryLink($dest, $mMod, $label) { $tx = $this->texy; $type = TexyLink::COMMON; // [ref] if (strlen($dest)>1 && $dest{0} === '[' && $dest{1} !== '*') { $type = TexyLink::BRACKET; $dest = substr($dest, 1, -1); $link = $this->getReference($dest); // [* image *] } elseif (strlen($dest)>1 && $dest{0} === '[' && $dest{1} === '*') { $type = TexyLink::IMAGE; $dest = trim(substr($dest, 2, -2)); $image = $tx->imageModule->getReference($dest); if ($image) { $link = new TexyLink($image->linkedURL === NULL ? $image->URL : $image->linkedURL); $link->modifier = $image->modifier; } } if (empty($link)) { $link = new TexyLink(trim($dest)); $this->checkLink($link); } if (strpos($link->URL, '%s') !== FALSE) { $link->URL = str_replace('%s', urlencode($tx->stringToText($label)), $link->URL); } $link->modifier->setProperties($mMod); $link->type = $type; return $link; } /** * Finish invocation. * * @param TexyHandlerInvocation handler invocation * @param TexyLink * @param TexyHtml|string * @return TexyHtml|string */ public function solve($invocation, $link, $content = NULL) { if ($link->URL == NULL) return $content; $tx = $this->texy; $el = TexyHtml::el('a'); if (empty($link->modifier)) { $nofollow = $popup = FALSE; } else { $nofollow = isset($link->modifier->classes['nofollow']); $popup = isset($link->modifier->classes['popup']); unset($link->modifier->classes['nofollow'], $link->modifier->classes['popup']); $el->attrs['href'] = NULL; // trick - move to front $link->modifier->decorate($tx, $el); } if ($link->type === TexyLink::IMAGE) { // image $el->attrs['href'] = Texy::prependRoot($link->URL, $tx->imageModule->linkedRoot); $el->attrs['onclick'] = $this->imageOnClick; } else { $el->attrs['href'] = Texy::prependRoot($link->URL, $this->root); // rel="nofollow" if ($nofollow || ($this->forceNoFollow && strpos($el->attrs['href'], '//') !== FALSE)) $el->attrs['rel'] = 'nofollow'; } // popup on click if ($popup) $el->attrs['onclick'] = $this->popupOnClick; if ($content !== NULL) $el->add($content); $tx->summary['links'][] = $el->attrs['href']; return $el; } /** * Finish invocation. * * @param TexyHandlerInvocation handler invocation * @param TexyLink * @return TexyHtml|string */ public function solveUrlEmail($invocation, $link) { $content = $this->textualUrl($link); $content = $this->texy->protect($content, Texy::CONTENT_TEXTUAL); return $this->solve(NULL, $link, $content); } /** * Finish invocation. * * @param TexyHandlerInvocation handler invocation * @param string * @return FALSE */ public function solveNewReference($invocation, $name) { // no change return FALSE; } /** * Checks and corrects $URL. * @param TexyLink * @return void */ private function checkLink($link) { // remove soft hyphens; if not removed by Texy::process() $link->URL = str_replace("\xC2\xAD", '', $link->URL); if (strncasecmp($link->URL, 'www.', 4) === 0) { // special supported case $link->URL = 'http://' . $link->URL; } elseif (preg_match('#'.TEXY_EMAIL.'$#Au', $link->URL)) { // email $link->URL = 'mailto:' . $link->URL; } elseif (!$this->texy->checkURL($link->URL, Texy::FILTER_ANCHOR)) { $link->URL = NULL; } else { $link->URL = str_replace('&', '&', $link->URL); // replace unwanted & } } /** * Returns textual representation of URL. * @param TexyLink * @return string */ private function textualUrl($link) { if ($this->texy->obfuscateEmail && preg_match('#^'.TEXY_EMAIL.'$#u', $link->raw)) { // email return str_replace('@', "@", $link->raw); } if ($this->shorten && preg_match('#^(https?://|ftp://|www\.|/)#i', $link->raw)) { $raw = strncasecmp($link->raw, 'www.', 4) === 0 ? 'none://' . $link->raw : $link->raw; // parse_url() in PHP damages UTF-8 - use regular expression if (!preg_match('~^(?:(?P[a-z]+):)?(?://(?P[^/?#]+))?(?P(?:/|^)(?!/)[^?#]*)?(?:\?(?P[^#]*))?(?:#(?P.*))?()$~', $raw, $parts)) { return $link->raw; } $res = ''; if ($parts['scheme'] !== '' && $parts['scheme'] !== 'none') $res .= $parts['scheme'] . '://'; if ($parts['host'] !== '') $res .= $parts['host']; if ($parts['path'] !== '') $res .= (iconv_strlen($parts['path'], 'UTF-8') > 16 ? ("/\xe2\x80\xa6" . iconv_substr($parts['path'], -12, 12, 'UTF-8')) : $parts['path']); if ($parts['query'] !== '') { $res .= iconv_strlen($parts['query'], 'UTF-8') > 4 ? "?\xe2\x80\xa6" : ('?' . $parts['query']); } elseif ($parts['fragment'] !== '') { $res .= iconv_strlen($parts['fragment'], 'UTF-8') > 4 ? "#\xe2\x80\xa6" : ('#' . $parts['fragment']); } return $res; } return $link->raw; } } /** * @package Texy */ final class TexyLink extends TexyObject { /** @see $type */ const COMMON = 1, BRACKET = 2, IMAGE = 3; /** @var string URL in resolved form */ public $URL; /** @var string URL as written in text */ public $raw; /** @var TexyModifier */ public $modifier; /** @var int how was link created? */ public $type = TexyLink::COMMON; /** @var string optional label, used by references */ public $label; /** @var string reference name (if is stored as reference) */ public $name; public function __construct($URL) { $this->URL = $URL; $this->raw = $URL; $this->modifier = new TexyModifier; } public function __clone() { if ($this->modifier) { $this->modifier = clone $this->modifier; } } }