Hrátky s Texy! na blog
Davídek nám ukázal, jak má nastavené texy na https://nette.org, takže jsem toho využil a napsal si nad Texy! vrstvičku.
Stará se o zvýrazňování kódu a taky zpracovává magické “meta makra”.
Potřeboval jsem také, aby do zvárazněného kódu generoval seznamy, tak jako to ve své dokumentaci dělá Twitter Bootstrap, takže jsem povolil ol
v elementu pre
.
Další požadavek, na kterém jsem se docela zapotil, bylo odstraňování hlavního nadpisu z výsledného kódu a kontrola, jestli obsahuje odkaz. Chci si totiž nadpis renderovat nad článkem zvlášť sám, kvůli tomu, aby se stejný HTML kód dal použít v RSS a nebyl v něm 2x nadpis. Implementace je dost naivní, ale dělám to pro sebe, tak si budu muset pamatovat, že to funguje pouze pokud odkaz obaluje celý obsah nadpisu.
Tohle fungovat bude
"**nadpis**":http://example.com
***
ale tohle už fungovat nebude
**"nadpis":http://example.com**
***
Texy! je nastaveno s vědomím, že výsledek bude na mém blogu - dovolí mi skoro vše.
use Nette\Utils\Html;
use Nette\Utils\Strings;
class Processor extends Nette\Object
{
/** @var \Texy */
private $lastTexy;
/** @var \FSHL\Highlighter */
private $highlighter;
/** @var array */
private $meta = array();
/** @var string */
private $title;
/** @var array */
private static $highlights = array(
'block/code' => TRUE,
'block/php' => 'FSHL\Lexer\Php',
'block/neon' => 'FSHL\Lexer\Neon',
'block/config' => TRUE, // @todo
'block/sh' => TRUE, // @todo
'block/texy' => TRUE, // @todo
'block/javascript' => 'FSHL\Lexer\Javascript',
'block/js' => 'FSHL\Lexer\Javascript',
'block/css' => 'FSHL\Lexer\Css',
'block/sql' => 'FSHL\Lexer\Sql',
'block/html' => 'FSHL\Lexer\Html',
'block/htmlcb' => 'FSHL\Lexer\Html',
);
public function __construct(\FSHL\Highlighter $highlighter)
{
$this->highlighter = $highlighter;
}
public function process($text)
{
$this->meta = array();
$this->title = array('link' => NULL, 'heading' => NULL, 'el' => NULL);
return $this->createTexy()->process($text);
}
public function getMeta()
{
return $this->meta;
}
public function getTitle()
{
return $this->title;
}
public function getLastTexy()
{
return $this->lastTexy;
}
protected function createTexy()
{
$texy = new \Texy();
// obecné nastavení
$texy->allowedTags = \Texy::ALL;
$texy->linkModule->root = '';
$texy->tabWidth = 4;
$texy->phraseModule->tags['phrase/strong'] = 'b';
$texy->phraseModule->tags['phrase/em'] = 'i';
$texy->phraseModule->tags['phrase/em-alt'] = 'i';
// nadpisy
$texy->headingModule->top = 1;
$texy->headingModule->generateID = TRUE;
$texy->addHandler('afterParse', array($this, 'headingHandler'));
// čísla řádků pro twitter bootstrap
$texy->dtd['pre'][1]['ol'] = 1;
// vypne generování bílých znaků ve výsledném kódu,
// aby se neroztahoval kód v elementu <pre>
$texy->htmlOutputModule->indent = FALSE;
// <code>
$texy->addHandler('block', array($this, 'blockHandler'));
// meta
$texy->registerBlockPattern(
array($this, 'metaHandler'),
'#\{\{([^:]+):([^:]+)\}\}$#m', // block patterns must be multiline and line-anchored
'metaBlockSyntax'
);
// return
return $this->lastTexy = $texy;
}
/**
* Metoda vykuchá element hlavního nadpisu z výsledného HTML
* a taky koukne, jeslti nadpis neobsahuje odkaz.
* @internal
*/
public function headingHandler(\Texy $texy, \TexyHtml $DOM, $isSingleLine)
{
list($title) = $texy->headingModule->TOC;
// zkopírovat element
$titleEl = Html::el($title['el']->getName(), $title['el']->attrs);
foreach ($title['el']->getChildren() as $child) {
$titleEl[] = $child;
}
// uklidit
$title['el']->attrs = array();
$title['el']->removeChildren();
$title['el']->setName(NULL);
// parsování odkazu
foreach ($titleEl->getChildren() as $i => $child) {
$matches = Strings::matchAll(
$texy->unProtect($child), // texy magie
'~<([\\w]+)([^>]*?)(([\\s]*\/>)|(>((([^<]*?|<\!\-\-.*?\-\->)|(?R))*)<\/\\1[\s]*>))~sm',
PREG_OFFSET_CAPTURE
);
if (!$matches) break;
list($tag) = $matches;
$titleEl[$i] = $el = Html::el($tag[1][0] . ' ' . $tag[2][0]);
$el->setHtml($tag[6][0]);
if ($el->getName() === 'a') {
$this->title['link'] = $el->attrs['href'];
}
}
// obsah nadpisu
$this->title['heading'] = $titleEl->getText();
$this->title['el'] = $titleEl;
}
/**
* Parsuje meta značky
* @internal
*/
public function metaHandler(\TexyParser $parser, array $matches, $name)
{
list(, $metaName, $metaValue) = $matches;
$this->meta[] = array(
trim(Strings::normalize($metaName)),
trim(Strings::normalize($metaValue))
);
}
/**
* Zýrazňuje kód
* @internal
*/
public function blockHandler(\TexyHandlerInvocation $invocation, $blockType, $content, $lang, $modifier)
{
if (isset(self::$highlights[$blockType])) {
list(, $lang) = explode('/', $blockType);
} else {
return $invocation->proceed($blockType, $content, $lang, $modifier);
}
$texy = $invocation->getTexy();
$content = \Texy::outdent($content);
// zvýraznění syntaxe
if (class_exists($lexerClass = self::$highlights[$blockType])) {
$content = $this->highlighter->highlight($content, new $lexerClass());
} else {
$content = htmlspecialchars($content);
}
$elPre = \TexyHtml::el('pre');
if ($modifier) $modifier->decorate($texy, $elPre);
$elPre->attrs['class'] = 'src-' . strtolower($lang) . ' prettyprint linenums';
// čísla řádků
$elOl = $elPre->create('ol', array('class' => 'linenums'));
foreach (Strings::split($content, '~[\n\r]~') as $i => $line) {
$elLi = $elOl->create('li', array('class' => 'L' . $i));
$elLi->create('span', $texy->protect($line, \Texy::CONTENT_BLOCK));
}
return $elPre;
}
}
Kvůli tomu, že každý řádek nyní obaluji prvkem <li>
, je potřeba upravit FSHL, aby se nám nekřížily tagy přes řádek.
class FshlHtmlOutput implements \FSHL\Output
{
private $lastClass = null;
public function template($part, $class)
{
$output = '';
if ($this->lastClass !== $class) {
if (null !== $this->lastClass) $output .= '</span>';
if (null !== $class) $output .= '<span class="' . $class . '">';
$this->lastClass = $class;
}
$part = htmlspecialchars($part, ENT_COMPAT, 'UTF-8');
if ($this->lastClass && strpos($part, "\n") !== FALSE) {
$endline = "</span>\n" . '<span class="' . $this->lastClass . '">';
$part = str_replace("\n", $endline, $part);
}
return $output . $part;
}
public function keyword($part, $class)
{
$output = '';
if ($this->lastClass !== $class) {
if (null !== $this->lastClass) $output .= '</span>';
if (null !== $class) $output .= '<span class="' . $class . '">';
$this->lastClass = $class;
}
return $output . htmlspecialchars($part, ENT_COMPAT, 'UTF-8');
}
}
Výsledný Processor
pak používám následovně
$processor = new Processor(new FSHL\Highlighter(new FshlHtmlOutput()));
$html = $processor->process($texy);
$meta = $processor->meta;
Byl jsem krapet v šoku, když jsem zjistil, že tento krásný blok s kódem není ve standardní distribuci Twitter Bootstrap. Kdo je líný kuchat to z jejich webu, tak CSS je zde:
.prettyprint {
padding: 8px; background-color: #f7f7f9; border: 1px solid #e1e1e8;
}
.prettyprint.linenums {
-webkit-box-shadow: inset 45px 0 0 #fbfbfc, inset 46px 0 0 #ececf0;
-moz-box-shadow: inset 45px 0 0 #fbfbfc, inset 46px 0 0 #ececf0;
box-shadow: inset 45px 0 0 #fbfbfc, inset 46px 0 0 #ececf0;
}
ol.linenums {
margin: 0 0 0 43px; /* IE indents via margin-left */
}
ol.linenums li {
padding-left: 6px; color: #bebec5; line-height: 20px; text-shadow: 0 1px 0 #fff;
}
ol.linenums li > span {
color: black;
}
Autor: Filip Procházka