Location: PHPKode > scripts > ApiGen > apigen/libs/FSHL/FSHL/Highlighter.php
<?php

/**
 * FSHL 2.1.0                                  | Fast Syntax HighLighter |
 * -----------------------------------------------------------------------
 *
 * LICENSE
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 */

namespace FSHL;

/**
 * Highlighter.
 *
 * @copyright Copyright (c) 2002-2005 Juraj 'hvge' Durech
 * @copyright Copyright (c) 2011-2012 Jaroslav Hanslík
 * @license http://fshl.kukulich.cz/#license
 */
class Highlighter
{
	/**
	 * No options.
	 *
	 * @var integer
	 */
	const OPTION_DEFAULT = 0x0000;

	/**
	 * Tab indentation option.
	 *
	 * @var integer
	 */
	const OPTION_TAB_INDENT = 0x0010;

	/**
	 * Line counter option.
	 *
	 * @var integer
	 */
	const OPTION_LINE_COUNTER = 0x0020;

	/**
	 * Output mode.
	 *
	 * @var \FSHL\Output
	 */
	private $output = null;

	/**
	 * Options.
	 *
	 * @var integer
	 */
	private $options;

	/**
	 * Tab indent width.
	 *
	 * @var integer
	 */
	private $tabIndentWidth;

	/**
	 * List of already used lexers.
	 *
	 * @var array
	 */
	private $lexers = array();

	/**
	 * Current lexer.
	 *
	 * @var \FSHL\Lexer
	 */
	private $lexer = null;

	/**
	 * Table for tab indentation.
	 *
	 * @var array
	 */
	private $tabs = array();

	/**
	 * States stack.
	 *
	 * @var array
	 */
	private $stack = array();

	/**
	 * Prepares the highlighter.
	 *
	 * @param \FSHL\Output $output
	 * @param integer $options
	 * @param integer $tabIndentWidth
	 */
	public function __construct(Output $output, $options = self::OPTION_DEFAULT, $tabIndentWidth = 4)
	{
		$this->setOutput($output)
			->setOptions($options, $tabIndentWidth);
	}

	/**
	 * Highlightes a string.
	 *
	 * @param string $text
	 * @param \FSHL\Lexer $lexer
	 * @return string
	 * @throws \RuntimeException If no lexer is set.
	 */
	public function highlight($text, Lexer $lexer = null)
	{
		// Sets the lexer
		$initialLexer = $this->lexer;
		if (null !== $lexer) {
			$this->setLexer($lexer);
		}

		// No lexer
		if (null === $this->lexer) {
			throw new \RuntimeException('No lexer set');
		}

		// Prepares the text
		$text = str_replace(array("\r\n", "\r"), "\n", (string) $text);
		$textLength = strlen($text);
		$textPos = 0;

		// Parses the text
		$output = array();
		$fragment = '';
		$maxLineWidth = 0;
		$line = 1;
		$char = 0;
		if ($this->options & self::OPTION_LINE_COUNTER) {
			// Right aligment of line counter
			$maxLineWidth = strlen(substr_count($text, "\n") + 1);
			$fragment .= $this->line($line, $maxLineWidth);
		}
		$newLexerName = $lexerName = $this->lexer->language;
		$newState = $state = $this->lexer->initialState;
		$this->stack = array();

		while (true) {
			list($transitionId, $delimiter, $buffer) = $this->lexer->{'findDelimiter' . $state}($text, $textLength, $textPos);

			// Some data may be collected before getPart reaches the delimiter, we must output this before other processing
			if (false !== $buffer) {
				$bufferLength = strlen($buffer);
				$textPos += $bufferLength;
				$char += $bufferLength;
				$fragment .= $this->template($buffer, $state);
				if (isset($fragment[8192])) {
					$output[] = $fragment;
					$fragment = '';
				}
			}

			if (-1 === $transitionId) {
				// End of stream
				break;
			}

			// Processes received delimiter as string
			$prevLine = $line;
			$prevChar = $char;
			$prevTextPos = $textPos;

			$delimiterLength = strlen($delimiter);
			$textPos += $delimiterLength;
			$char += $delimiterLength;

			// Adds line counter and tab indentation
			$addLine = false;
			if ("\n" === $delimiter[$delimiterLength - 1]) {
				// Line counter
				$line++;
				$char = 0;
				if ($this->options & self::OPTION_LINE_COUNTER) {
					$addLine = true;
					$actualLine = $line;
				}
			} elseif ("\t" === $delimiter && ($this->options & self::OPTION_TAB_INDENT)) {
				// Tab indentation
				$i = $char % $this->tabIndentWidth;
				$delimiter = $this->tabs[$i][0];
				$char += $this->tabs[$i][1];
			}

			// Gets new state from the transitions table
			$newState = $this->lexer->trans[$state][$transitionId][Generator::STATE_DIAGRAM_INDEX_STATE];
			if ($newState === $this->lexer->returnState) {
				// Chooses mode of delimiter processing
				if (Generator::BACK === $this->lexer->trans[$state][$transitionId][Generator::STATE_DIAGRAM_INDEX_MODE]) {
					$line = $prevLine;
					$char = $prevChar;
					$textPos = $prevTextPos;
				} else {
					$fragment .= $this->template($delimiter, $state);
					if ($addLine) {
						$fragment .= $this->line($actualLine, $maxLineWidth);
					}
					if (isset($fragment[8192])) {
						$output[] = $fragment;
						$fragment = '';
					}
				}

				// Get state from the context stack
				if ($item = $this->popState()) {
					list($newLexerName, $state) = $item;
					// If previous context was in a different lexer, switch the lexer too
					if ($newLexerName !== $lexerName) {
						$this->setLexerByName($newLexerName);
						$lexerName = $newLexerName;
					}
				} else {
					$state = $this->lexer->initialState;
				}

				continue;
			}

			// Chooses mode of delimiter processing
			$type = $this->lexer->trans[$state][$transitionId][Generator::STATE_DIAGRAM_INDEX_MODE];
			if (Generator::BACK === $type) {
				$line = $prevLine;
				$char = $prevChar;
				$textPos = $prevTextPos;
			} else {
				$fragment .= $this->template($delimiter, Generator::NEXT === $type ? $newState : $state);
				if ($addLine) {
					$fragment .= $this->line($actualLine, $maxLineWidth);
				}
				if (isset($fragment[8192])) {
					$output[] = $fragment;
					$fragment = '';
				}
			}

			// Switches between lexers (transition to embedded language)
			if ($this->lexer->flags[$newState] & Generator::STATE_FLAG_NEWLEXER) {
				if ($newState === $this->lexer->quitState) {
					// Returns to the previous lexer
					if ($item = $this->popState()) {
						list($newLexerName, $state) = $item;
						if ($newLexerName !== $lexerName) {
							$this->setLexerByName($newLexerName);
							$lexerName = $newLexerName;
						}
					} else {
						$state = $this->lexer->initialState;
					}
				} else {
					// Switches to the embedded language
					$newLexerName = $this->lexer->data[$newState];
					$this->pushState($lexerName, $this->lexer->trans[$newState] ? $newState : $state);
					$this->setLexerByName($newLexerName);
					$lexerName = $newLexerName;
					$state = $this->lexer->initialState;
				}

				continue;
			}

			// If newState is marked with recursion flag (alias call), push current state to the context stack
			if (($this->lexer->flags[$newState] & Generator::STATE_FLAG_RECURSION) && $state !== $newState) {
				$this->pushState($lexerName, $state);
			}

			// Change the state
			$state = $newState;
		}

		// Adds template end
		$fragment .= $this->output->template('', null);
		$output[] = $fragment;

		// Restore lexer
		$this->lexer = $initialLexer;

		return implode('', $output);
	}

	/**
	 * Sets the output mode.
	 *
	 * @param \FSHL\Output $output
	 * @return \FSHL\Highlighter
	 */
	public function setOutput(Output $output)
	{
		$this->output = $output;

		return $this;
	}

	/**
	 * Sets options.
	 *
	 * @param integer $options
	 * @param integer $tabIndentWidth
	 * @return \FSHL\Highlighter
	 */
	public function setOptions($options = self::OPTION_DEFAULT, $tabIndentWidth = 4)
	{
		$this->options = $options;

		if (($this->options & self::OPTION_TAB_INDENT) && $tabIndentWidth > 0) {
			// Precalculate a table for tab indentation
			$t = ' ';
			$ti = 0;
			for ($i = $tabIndentWidth; $i; $i--) {
				$this->tabs[$i % $tabIndentWidth] = array($t, $ti++);
				$t .= ' ';
			}
			$this->tabIndentWidth = $tabIndentWidth;
		} else {
			$this->options &= ~self::OPTION_TAB_INDENT;
		}

		return $this;
	}

	/**
	 * Sets the default lexer.
	 *
	 * @param \FSHL\Lexer $lexer
	 * @return \FSHL\Highlighter
	 */
	public function setLexer(Lexer $lexer)
	{
		// Generates the lexer cache on fly, if the lexer cache doesn't exist
		if (!$this->findCache($lexer->getLanguage())) {
			$this->generateCache($lexer);
		}

		return $this;
	}

	/**
	 * Sets the current lexer by name.
	 *
	 * @param string $lexerName
	 * @return \FSHL\Highlighter
	 * @throws \InvalidArgumentException If the class for given lexer doesn't exist.
	 */
	private function setLexerByName($lexerName)
	{
		// Generates the lexer cache on fly, if the lexer cache doesn't exist
		if (!$this->findCache($lexerName)) {
			// Finds the lexer
			$lexerClass = '\\FSHL\\Lexer\\' . $lexerName;
			if (!class_exists($lexerClass)) {
				throw new \InvalidArgumentException(sprintf('The class for "%s" lexer doesn\'t exist', $lexerName));
			}
			$lexer = new $lexerClass();

			// Generates the lexer cache on fly
			$this->generateCache($lexer);
		}

		return $this;
	}

	/**
	 * Tries to find the lexer cache.
	 *
	 * @param string $lexerName
	 * @return boolean
	 */
	private function findCache($lexerName)
	{
		// Lexer has been used before
		if (isset($this->lexers[$lexerName])) {
			$this->lexer = $this->lexers[$lexerName];
			return true;
		}

		// Loads lexer cache
		$lexerCacheClass = '\\FSHL\\Lexer\\Cache\\' . $lexerName;
		if (class_exists($lexerCacheClass)) {
			$this->lexers[$lexerName] = new $lexerCacheClass();
			$this->lexer = $this->lexers[$lexerName];
			return true;
		}

		return false;
	}

	/**
	 * Generates the lexer cache on fly.
	 *
	 * @param \FSHL\Lexer $lexer
	 * @return \FSHL\Highlighter
	 */
	private function generateCache(Lexer $lexer)
	{
		$generator = new Generator($lexer);
		try {
			$generator->saveToCache();
		} catch (\RuntimeException $e) {
			$file = tempnam(sys_get_temp_dir(), 'fshl');
			file_put_contents($file, $generator->getSource());
			require_once $file;
			unlink($file);
		}

		$lexerName = $lexer->getLanguage();
		$lexerCacheClass = '\\FSHL\\Lexer\\Cache\\' . $lexerName;
		$this->lexers[$lexerName] = new $lexerCacheClass();
		$this->lexer = $this->lexers[$lexerName];

		return $this;
	}

	/**
	 * Outputs a word.
	 *
	 * @param string $part
	 * @param string $state
	 * @return string
	 */
	private function template($part, $state)
	{
		if ($this->lexer->flags[$state] & Generator::STATE_FLAG_KEYWORD) {
			$normalized = Generator::CASE_SENSITIVE === $this->lexer->keywords[Generator::KEYWORD_INDEX_CASE_SENSITIVE] ? $part : strtolower($part);

			if (isset($this->lexer->keywords[Generator::KEYWORD_INDEX_LIST][$normalized])) {
				return $this->output->keyword($part, $this->lexer->keywords[Generator::KEYWORD_INDEX_CLASS] . $this->lexer->keywords[Generator::KEYWORD_INDEX_LIST][$normalized]);
			}
		}

		return $this->output->template($part, $this->lexer->classes[$state]);
	}

	/**
	 * Outputs a line.
	 *
	 * @param integer $line
	 * @param integer $maxLineWidth
	 * @return string
	 */
	private function line($line, $maxLineWidth)
	{
		return $this->output->template(str_pad($line, $maxLineWidth, ' ', STR_PAD_LEFT) . ': ', 'line');
	}

	/**
	 * Pushes a state to the context stack.
	 *
	 * @param string $lexerName
	 * @param string $state
	 * @return \FSHL\Highlighter
	 */
	private function pushState($lexerName, $state)
	{
		array_unshift($this->stack, array($lexerName, $state));
		return $this;
	}

	/**
	 * Pops a state from the context stack.
	 *
	 * @return array|null
	 */
	private function popState()
	{
		return array_shift($this->stack);
	}
}
Return current item: ApiGen