├── .gitignore ├── .travis.yml ├── Builder.php ├── Directive.php ├── Directives ├── CodeBlock.php ├── Document.php ├── Dummy.php ├── Raw.php ├── RedirectionTitle.php ├── Replace.php └── Toctree.php ├── Document.php ├── Environment.php ├── ErrorManager.php ├── HTML ├── Directives │ ├── Div.php │ ├── Figure.php │ ├── Image.php │ ├── Meta.php │ ├── Stylesheet.php │ ├── Title.php │ ├── Url.php │ └── Wrap.php ├── Document.php ├── Environment.php ├── Kernel.php ├── Nodes │ ├── AnchorNode.php │ ├── CodeNode.php │ ├── FigureNode.php │ ├── ImageNode.php │ ├── ListNode.php │ ├── MetaNode.php │ ├── ParagraphNode.php │ ├── QuoteNode.php │ ├── SeparatorNode.php │ ├── TableNode.php │ ├── TitleNode.php │ └── TocNode.php └── Span.php ├── Kernel.php ├── LICENSE ├── LaTeX ├── Directives │ ├── Image.php │ ├── LaTeXMain.php │ ├── Meta.php │ ├── Stylesheet.php │ ├── Title.php │ ├── Url.php │ └── Wrap.php ├── Document.php ├── Environment.php ├── Kernel.php ├── Nodes │ ├── AnchorNode.php │ ├── CodeNode.php │ ├── ImageNode.php │ ├── LaTeXMainNode.php │ ├── ListNode.php │ ├── MetaNode.php │ ├── ParagraphNode.php │ ├── QuoteNode.php │ ├── SeparatorNode.php │ ├── TableNode.php │ ├── TitleNode.php │ └── TocNode.php └── Span.php ├── Metas.php ├── Nodes ├── AnchorNode.php ├── BlockNode.php ├── CodeNode.php ├── DocumentNode.php ├── DummyNode.php ├── FigureNode.php ├── ImageNode.php ├── ListNode.php ├── MetaNode.php ├── Node.php ├── ParagraphNode.php ├── QuoteNode.php ├── RawNode.php ├── SeparatorNode.php ├── TableNode.php ├── TitleNode.php ├── TocNode.php └── WrapperNode.php ├── Parser.php ├── README.md ├── Reference.php ├── References └── Doc.php ├── Span.php ├── SubDirective.php ├── autoload.php ├── composer.json ├── phpunit.xml ├── test ├── document │ ├── demo.php │ ├── document.html │ ├── document.rst │ ├── include.rst │ ├── latex.php │ ├── parse.php │ ├── rst.png │ └── style.css └── tree │ ├── .gitignore │ ├── input │ ├── css │ │ └── style.css │ ├── include.rst │ ├── index.rst │ ├── otherpage.rst │ ├── special.rst │ └── subdir │ │ ├── testinclude.rst │ │ └── testing_sub.rst │ ├── latex.php │ └── parse.php └── tests ├── BuilderTests.php ├── EnvironmentTests.php ├── HTMLTests.php ├── ParserTests.php ├── bootstrap.php ├── builder ├── .gitignore └── input │ ├── another.rst │ ├── file.txt │ ├── index.rst │ ├── introduction.rst │ ├── subdir │ └── index.rst │ └── subdirective.rst ├── files ├── code-block-lastline.rst ├── code.rst ├── comment.rst ├── directive.rst ├── empty.rst ├── include.rst ├── inclusion-newline-include.rst ├── inclusion-newline.rst ├── inclusion-policy.rst ├── inclusion-scope-include.rst ├── inclusion-scope.rst ├── inclusion.rst ├── indented-list.rst ├── list-empty.rst ├── list.rst ├── mixed-titles-1.rst ├── mixed-titles-2.rst ├── multi-comment.rst ├── paragraph.rst ├── paragraphs.rst ├── pretty-table.rst ├── quote.rst ├── replace.rst ├── subdir │ └── test.rst ├── table.rst ├── title.rst ├── title2.rst └── titles.rst └── html ├── anonymous.rst ├── bom.rst ├── code-block.rst ├── code-java.rst ├── code-list.rst ├── code.rst ├── comment-3.rst ├── comments.rst ├── crlf.rst ├── css.rst ├── directive-title.rst ├── div.rst ├── empty-p.rst ├── escape.rst ├── figure.rst ├── image-follow.rst ├── image-inline.rst ├── image.rst ├── include-comments.rst ├── include-external.rst ├── indented-list.rst ├── italic.rst ├── link-span.rst ├── links.rst ├── list-dash.rst ├── list-empty.rst ├── list.rst ├── literal.rst ├── nbsp.rst ├── no-eager-literals.rst ├── ordered.rst ├── ordered2.rst ├── pretty-table-multibyte.rst ├── pretty-table.rst ├── quote-title.rst ├── quote.rst ├── quote2.rst ├── quote3.rst ├── raw.rst ├── reference-directive.rst ├── separator.rst ├── strong.rst ├── table-multibyte.rst ├── table.rst ├── table2.rst ├── titles-auto.rst ├── titles.rst ├── unknown-directive.rst ├── variable-wrap.rst └── wrap.rst /.gitignore: -------------------------------------------------------------------------------- 1 | **.swp 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 7.2 7 | 8 | script: 9 | - composer install 10 | - vendor/bin/phpunit 11 | -------------------------------------------------------------------------------- /Builder.php: -------------------------------------------------------------------------------- 1 | errorManager = new ErrorManager; 58 | 59 | if ($kernel) { 60 | $this->kernel = $kernel; 61 | } else { 62 | $this->kernel = new HTML\Kernel; 63 | } 64 | 65 | $this->kernel->initBuilder($this); 66 | } 67 | 68 | public function getErrorManager() 69 | { 70 | return $this->errorManager; 71 | } 72 | 73 | /** 74 | * Adds an hook which will be called on each document after parsing 75 | */ 76 | public function addHook($function) 77 | { 78 | $this->hooks[] = $function; 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Adds an hook which will be called on each environment during building 85 | */ 86 | public function addBeforeHook($function) 87 | { 88 | $this->beforeHooks[] = $function; 89 | 90 | return $this; 91 | } 92 | 93 | protected function display($text) 94 | { 95 | if ($this->verbose) { 96 | echo $text."\n"; 97 | } 98 | } 99 | 100 | public function build($directory, $targetDirectory = 'output', $verbose = true) 101 | { 102 | $this->verbose = $verbose; 103 | $this->directory = $directory; 104 | $this->targetDirectory = $targetDirectory; 105 | 106 | // Creating output directory if doesn't exists 107 | if (!is_dir($targetDirectory)) { 108 | mkdir($targetDirectory, 0755, true); 109 | } 110 | 111 | // Try to load metas, if it does not exists, create it 112 | $this->display('* Loading metas'); 113 | $this->metas = new Metas($this->loadMetas()); 114 | 115 | // Scan all the metas and the index 116 | $this->display('* Pre-scanning files'); 117 | $this->scan($this->getIndexName()); 118 | $this->scanMetas(); 119 | 120 | // Parses all the documents 121 | $this->parseAll(); 122 | 123 | // Renders all the documents 124 | $this->render(); 125 | 126 | // Saving the meta 127 | $this->display('* Writing metas'); 128 | $this->saveMetas(); 129 | 130 | // Copy the files 131 | $this->display('* Running the copies'); 132 | $this->doMkdir(); 133 | $this->doCopy(); 134 | } 135 | 136 | /** 137 | * Renders all the pending documents 138 | */ 139 | protected function render() 140 | { 141 | $this->display('* Rendering documents'); 142 | foreach ($this->documents as $file => &$document) { 143 | $this->display(' -> Rendering '.$file.'...'); 144 | $target = $this->getTargetOf($file); 145 | 146 | $directory = dirname($target); 147 | if (!is_dir($directory)) { 148 | mkdir($directory, 0755, true); 149 | } 150 | 151 | file_put_contents($target, $document->renderDocument()); 152 | } 153 | } 154 | 155 | /** 156 | * Adding a file to the parse queue 157 | */ 158 | protected function addToParseQueue($file) 159 | { 160 | $this->states[$file] = self::PARSE; 161 | 162 | if (!isset($this->documents[$file])) { 163 | $this->parseQueue[$file] = $file; 164 | } 165 | } 166 | 167 | /** 168 | * Returns the next file to parse 169 | */ 170 | protected function getFileToParse() 171 | { 172 | if ($this->parseQueue) { 173 | return array_shift($this->parseQueue); 174 | } else { 175 | return null; 176 | } 177 | } 178 | 179 | /** 180 | * Parses all the document that need to be parsed 181 | */ 182 | protected function parseAll() 183 | { 184 | $this->display('* Parsing files'); 185 | while ($file = $this->getFileToParse()) { 186 | $this->display(' -> Parsing '.$file.'...'); 187 | // Process the file 188 | $rst = $this->getRST($file); 189 | $parser = new Parser(null, $this->kernel); 190 | 191 | $environment = $parser->getEnvironment(); 192 | $environment->setMetas($this->metas); 193 | $environment->setCurrentFilename($file); 194 | $environment->setCurrentDirectory($this->directory); 195 | $environment->setTargetDirectory($this->targetDirectory); 196 | $environment->setErrorManager($this->errorManager); 197 | $environment->setUseRelativeUrls($this->relativeUrls); 198 | 199 | foreach ($this->beforeHooks as $hook) { 200 | $hook($parser); 201 | } 202 | 203 | if (!file_exists($rst)) { 204 | $this->errorManager->error('Can\'t parse the file '.$rst); 205 | continue; 206 | } 207 | 208 | $document = $this->documents[$file] = $parser->parseFile($rst); 209 | 210 | // Calling all the post-process hooks 211 | foreach ($this->hooks as $hook) { 212 | $hook($document); 213 | } 214 | 215 | // Calling the kernel document tweaking 216 | $this->kernel->postParse($document); 217 | 218 | $dependencies = $document->getEnvironment()->getDependencies(); 219 | 220 | if ($dependencies) { 221 | $this->display(' -> Scanning dependencies of '.$file.'...'); 222 | // Scan the dependencies for this document 223 | foreach ($dependencies as $dependency) { 224 | $this->scan($dependency); 225 | } 226 | } 227 | 228 | // Append the meta for this document 229 | $this->metas->set( 230 | $file, 231 | $this->getUrl($document), 232 | $document->getTitle(), 233 | $document->getTitles(), 234 | $document->getTocs(), 235 | filectime($rst), 236 | $dependencies 237 | ); 238 | } 239 | } 240 | 241 | /** 242 | * Scans a file, this will check the status of the file and tell if it 243 | * needs to be parsed or not 244 | */ 245 | public function scan($file) 246 | { 247 | // If no decision is already made about this file 248 | if (!isset($this->states[$file])) { 249 | $this->display(' -> Scanning '.$file.'...'); 250 | $this->states[$file] = self::NO_PARSE; 251 | $entry = $this->metas->get($file); 252 | $rst = $this->getRST($file); 253 | 254 | if (!$entry || !file_exists($rst) || $entry['ctime'] < filectime($rst)) { 255 | // File was never seen or changed and thus need to be parsed 256 | $this->addToParseQueue($file); 257 | } else { 258 | // Have a look to the file dependencies to knoww if you need to parse 259 | // it or not 260 | $depends = $entry['depends']; 261 | 262 | if (isset($entry['parent'])) { 263 | $depends[] = $entry['parent']; 264 | } 265 | 266 | foreach ($depends as $dependency) { 267 | $this->scan($dependency); 268 | 269 | // If any dependency needs to be parsed, this file needs also to be 270 | // parsed 271 | if ($this->states[$dependency] == self::PARSE) { 272 | $this->addToParseQueue($file); 273 | } 274 | } 275 | } 276 | } 277 | } 278 | 279 | /** 280 | * Scans all the metas 281 | */ 282 | public function scanMetas() 283 | { 284 | $entries = $this->metas->getAll(); 285 | 286 | foreach ($entries as $file => $infos) { 287 | $this->scan($file); 288 | } 289 | } 290 | 291 | /** 292 | * Get the meta file name 293 | */ 294 | protected function getMetaFile() 295 | { 296 | return $this->getTargetFile('meta.php'); 297 | } 298 | 299 | 300 | /** 301 | * Try to inport the metas from the meta files 302 | */ 303 | protected function loadMetas() 304 | { 305 | $metaFile = $this->getMetaFile(); 306 | 307 | if (file_exists($metaFile)) { 308 | return @include($metaFile); 309 | } 310 | 311 | return null; 312 | } 313 | 314 | /** 315 | * Saving the meta files 316 | */ 317 | protected function saveMetas() 318 | { 319 | $metas = 'metas->getAll(), true).';'; 320 | file_put_contents($this->getMetaFile(), $metas); 321 | } 322 | 323 | /** 324 | * Gets the .rst of a source file 325 | */ 326 | public function getRST($file) 327 | { 328 | return $this->getSourceFile($file . '.rst'); 329 | } 330 | 331 | /** 332 | * Gets the name of a target for a file, for instance /introduction/part1 could 333 | * be resolved into /path/to/introduction/part1.html 334 | */ 335 | public function getTargetOf($file) 336 | { 337 | $meta = $this->metas->get($file); 338 | 339 | return $this->getTargetFile($meta['url']); 340 | } 341 | 342 | /** 343 | * Gets the URL of a target file 344 | */ 345 | public function getUrl($document) 346 | { 347 | $environment = $document->getEnvironment(); 348 | 349 | return $environment->getUrl() . '.' . $this->kernel->getFileExtension(); 350 | } 351 | 352 | /** 353 | * Gets the name of a target file 354 | */ 355 | public function getTargetFile($filename) 356 | { 357 | return $this->targetDirectory . '/' . $filename; 358 | } 359 | 360 | /** 361 | * Gets the name of a source file 362 | */ 363 | public function getSourceFile($filename) 364 | { 365 | return $this->directory . '/' . $filename; 366 | } 367 | 368 | /** 369 | * Is the given path absolute ? 370 | */ 371 | protected function isAbsolute(string $path) 372 | { 373 | return $path[0] === DIRECTORY_SEPARATOR || preg_match('~\A[A-Z]:(?![^/\\\\])~i', $path) > 0; 374 | } 375 | 376 | /** 377 | * Run the copy 378 | */ 379 | public function doCopy() 380 | { 381 | foreach ($this->toCopy as $copy) { 382 | list($source, $destination) = $copy; 383 | if (!$this->isAbsolute($source)) { 384 | $source = $this->getSourceFile($source); 385 | } 386 | $destination = $this->getTargetFile($destination); 387 | 388 | if (is_dir($source) && is_dir($destination)) { 389 | $destination = dirname($destination); 390 | } 391 | 392 | shell_exec('cp -R '.$source.' '.$destination); 393 | } 394 | } 395 | 396 | /** 397 | * Add a file to copy 398 | */ 399 | public function copy($source, $destination = null) 400 | { 401 | if ($destination === null) { 402 | $destination = basename($source); 403 | } 404 | 405 | $this->toCopy[] = array($source, $destination); 406 | 407 | return $this; 408 | } 409 | 410 | /** 411 | * Run the directories creation 412 | */ 413 | public function doMkdir() 414 | { 415 | foreach ($this->toMkdir as $mkdir) { 416 | $dir = $this->getTargetFile($mkdir); 417 | 418 | if (!is_dir($dir)) { 419 | mkdir($dir, 0755, true); 420 | } 421 | } 422 | } 423 | 424 | /** 425 | * Creates a directory in the target 426 | * 427 | * @param $directory the directory name to create 428 | */ 429 | public function mkdir($directory) 430 | { 431 | $this->toMkdir[] = $directory; 432 | 433 | return $this; 434 | } 435 | 436 | public function setIndexName($name) 437 | { 438 | $this->indexName = $name; 439 | 440 | return $this; 441 | } 442 | 443 | public function getIndexName() 444 | { 445 | return $this->indexName; 446 | } 447 | 448 | /** 449 | * Use relative URLs for links 450 | */ 451 | public function setUseRelativeUrls($enable) 452 | { 453 | $this->relativeUrls = $enable; 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /Directive.php: -------------------------------------------------------------------------------- 1 | getDocument(); 40 | 41 | $processNode = $this->processNode($parser, $variable, $data, $options); 42 | 43 | if ($processNode) { 44 | if ($variable) { 45 | $environment = $parser->getEnvironment(); 46 | $environment->setVariable($variable, $processNode); 47 | } else { 48 | $document->addNode($processNode); 49 | } 50 | } 51 | 52 | if ($node) { 53 | $document->addNode($node); 54 | } 55 | } 56 | 57 | /** 58 | * This can be overloaded to write a directive that just create one node for the 59 | * document, which is common 60 | * 61 | * The arguments are the same that process 62 | */ 63 | public function processNode(Parser $parser, $variable, $data, array $options) 64 | { 65 | $this->processAction($parser, $variable, $data, $options); 66 | 67 | return null; 68 | } 69 | 70 | /** 71 | * This can be overloaded to write a directive that just do an action without changing 72 | * the nodes of the document 73 | * 74 | * The arguments are the same that process 75 | */ 76 | public function processAction(Parser $parser, $variabe, $data, array $options) 77 | { 78 | } 79 | 80 | /** 81 | * Called at the end of the parsing to finalize the document (add something or tweak nodes) 82 | */ 83 | public function finalize(Document &$document) 84 | { 85 | } 86 | 87 | /** 88 | * Should the following block be passed as a CodeNode? 89 | */ 90 | public function wantCode() 91 | { 92 | return false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Directives/CodeBlock.php: -------------------------------------------------------------------------------- 1 | getKernel(); 31 | 32 | if ($node instanceof CodeNode) { 33 | $node->setLanguage(trim($data)); 34 | } 35 | 36 | if ($variable) { 37 | $environment = $parser->getEnvironment(); 38 | $environment->setVariable($variable, $node); 39 | } else { 40 | $document = $parser->getDocument(); 41 | $document->addNode($node); 42 | } 43 | } 44 | } 45 | 46 | public function wantCode() 47 | { 48 | return true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Directives/Document.php: -------------------------------------------------------------------------------- 1 | $data, 'options' => $options)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Directives/Raw.php: -------------------------------------------------------------------------------- 1 | Undelined! 17 | */ 18 | class Raw extends Directive 19 | { 20 | public function getName() 21 | { 22 | return 'raw'; 23 | } 24 | 25 | public function process(Parser $parser, $node, $variable, $data, array $options) 26 | { 27 | if ($node) { 28 | $kernel = $parser->getKernel(); 29 | 30 | if ($node instanceof CodeNode) { 31 | $node->setRaw(true); 32 | } 33 | 34 | if ($variable) { 35 | $environment = $parser->getEnvironment(); 36 | $environment->setVariable($variable, $node); 37 | } else { 38 | $document = $parser->getDocument(); 39 | $document->addNode($node); 40 | } 41 | } 42 | } 43 | 44 | public function wantCode() 45 | { 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Directives/RedirectionTitle.php: -------------------------------------------------------------------------------- 1 | getDocument(); 24 | 25 | if ($node) { 26 | if ($node instanceof TitleNode) { 27 | $node->setTarget($data); 28 | } 29 | $document->addNode($node); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Directives/Replace.php: -------------------------------------------------------------------------------- 1 | createSpan($data); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Directives/Toctree.php: -------------------------------------------------------------------------------- 1 | getEnvironment(); 18 | $kernel = $parser->getKernel(); 19 | $files = array(); 20 | 21 | foreach (explode("\n", $node->getValue()) as $file) { 22 | $file = trim($file); 23 | if ($file) { 24 | $environment->addDependency($file); 25 | $files[] = $file; 26 | } 27 | } 28 | 29 | $document = $parser->getDocument(); 30 | $document->addNode($kernel->build('Nodes\TocNode', $files, $environment, $options)); 31 | } 32 | 33 | public function wantCode() 34 | { 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Document.php: -------------------------------------------------------------------------------- 1 | environment = $environment; 19 | } 20 | 21 | public function getEnvironment() 22 | { 23 | return $this->environment; 24 | } 25 | 26 | public function renderDocument() 27 | { 28 | return $this->render(); 29 | } 30 | 31 | /** 32 | * Getting all nodes of the document that satisfies the given 33 | * function. If the function is null, all the nodes are returned. 34 | */ 35 | public function getNodes($function = null) 36 | { 37 | $nodes = array(); 38 | 39 | if ($function == null) { 40 | return $this->nodes; 41 | } 42 | 43 | foreach ($this->nodes as $node) { 44 | if ($function($node)) { 45 | $nodes[] = $node; 46 | } 47 | } 48 | 49 | return $nodes; 50 | } 51 | 52 | /** 53 | * Gets the main title of the document 54 | */ 55 | public function getTitle() 56 | { 57 | foreach ($this->nodes as $node) { 58 | if ($node instanceof TitleNode && $node->getLevel() == 1) { 59 | return $node->getValue().''; 60 | } 61 | } 62 | 63 | return null; 64 | } 65 | 66 | /** 67 | * Get the table of contents of the document 68 | */ 69 | public function getTocs() 70 | { 71 | $tocs = array(); 72 | 73 | $nodes = $this->getNodes(function($node) { 74 | return $node instanceof TocNode; 75 | }); 76 | 77 | foreach ($nodes as $toc) { 78 | $files = $toc->getFiles(); 79 | 80 | foreach ($files as &$file) { 81 | $file = $this->environment->canonicalUrl($file); 82 | } 83 | 84 | $tocs[] = $files; 85 | } 86 | 87 | return $tocs; 88 | } 89 | 90 | /** 91 | * Gets the titles hierarchy in arrays, for instance : 92 | * 93 | * array( 94 | * array('Main title', array( 95 | * array('Sub title', array()), 96 | * array('Sub title 2', array(), 97 | * array(array('Redirection', 'target'), array(), 98 | * ) 99 | * ) 100 | */ 101 | public function getTitles() 102 | { 103 | $titles = array(); 104 | $levels = array(&$titles); 105 | 106 | foreach ($this->nodes as $node) { 107 | if ($node instanceof TitleNode) { 108 | $level = $node->getLevel(); 109 | $text = (string)$node->getValue(); 110 | $redirection = $node->getTarget(); 111 | $value = $redirection ? array($text, $redirection) : $text; 112 | 113 | if (isset($levels[$level-1])) { 114 | $parent = &$levels[$level-1]; 115 | $element = array($value, array()); 116 | $parent[] = $element; 117 | $levels[$level] = &$parent[count($parent)-1][1]; 118 | } 119 | } 120 | } 121 | 122 | return $titles; 123 | } 124 | 125 | public function addNode($node) 126 | { 127 | if (is_string($node)) { 128 | $node = new RawNode($node); 129 | } 130 | 131 | if (!$node instanceof Node) { 132 | $this->getEnvironment()->getErrorManager('addNode($node): $node should be a string or a Node'); 133 | } 134 | 135 | $this->nodes[] = $node; 136 | } 137 | 138 | public function prependNode(Node $node) 139 | { 140 | array_unshift($this->nodes, $node); 141 | } 142 | 143 | public function addHeaderNode(Node $node) 144 | { 145 | $this->headerNodes[] = $node; 146 | } 147 | 148 | public function __toString() 149 | { 150 | return $this->render(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Environment.php: -------------------------------------------------------------------------------- 1 | errorManager = new ErrorManager; 59 | 60 | $this->reset(); 61 | } 62 | 63 | /** 64 | * Puts the environment in a clean state for a new parse, like title level order. 65 | */ 66 | public function reset() 67 | { 68 | $this->titleLetters = array(); 69 | $this->currentTitleLevel = 0; 70 | $this->levels = array(); 71 | $this->counters = array(); 72 | 73 | for ($level=0; $level<16; $level++) { 74 | $this->levels[$level] = 1; 75 | $this->counters[$level] = 0; 76 | } 77 | } 78 | 79 | public function getErrorManager() 80 | { 81 | return $this->errorManager; 82 | } 83 | 84 | public function setErrorManager(ErrorManager $errorManager) 85 | { 86 | $this->errorManager = $errorManager; 87 | } 88 | 89 | public function setMetas($metas) 90 | { 91 | $this->metas = $metas; 92 | } 93 | 94 | /** 95 | * Get my parent metas 96 | */ 97 | public function getParent() 98 | { 99 | if (!$this->currentFileName || !$this->metas) { 100 | return null; 101 | } 102 | 103 | $meta = $this->metas->get($this->currentFileName); 104 | 105 | if (!$meta || !isset($meta['parent'])) { 106 | return null; 107 | } 108 | 109 | $parent = $this->metas->get($meta['parent']); 110 | 111 | if (!$parent) { 112 | return null; 113 | } 114 | 115 | return $parent; 116 | } 117 | 118 | /** 119 | * Get the docs involving this document 120 | */ 121 | public function getMyToc() 122 | { 123 | $parent = $this->getParent(); 124 | 125 | if ($parent) { 126 | foreach ($parent['tocs'] as $toc) { 127 | if (in_array($this->currentFileName, $toc)) { 128 | $before = array(); 129 | $after = $toc; 130 | 131 | while ($after) { 132 | $file = array_shift($after); 133 | if ($file == $this->currentFileName) { 134 | return array($before, $after); 135 | } 136 | $before[] = $file; 137 | } 138 | } 139 | } 140 | } 141 | 142 | return null; 143 | } 144 | 145 | /** 146 | * Registers a new reference 147 | */ 148 | public function registerReference(Reference $reference) 149 | { 150 | $name = $reference->getName(); 151 | $this->references[$name] = $reference; 152 | } 153 | 154 | /** 155 | * Resolves a reference 156 | */ 157 | public function resolve($section, $data) 158 | { 159 | if (isset($this->references[$section])) { 160 | $reference = $this->references[$section]; 161 | 162 | return $reference->resolve($this, $data); 163 | } 164 | 165 | $this->errorManager->error('Unknown reference section '.$section); 166 | } 167 | 168 | public function found($section, $data) 169 | { 170 | if (isset($this->references[$section])) { 171 | $reference = $this->references[$section]; 172 | 173 | return $reference->found($this, $data); 174 | } 175 | 176 | $this->errorManager->error('Unknown reference section '.$section); 177 | } 178 | 179 | /** 180 | * Sets the giving variable to a value 181 | * 182 | * @param $variable the variable name 183 | * @param $value the variable value 184 | */ 185 | public function setVariable($variable, $value) 186 | { 187 | $this->variables[$variable] = $value; 188 | } 189 | 190 | /** 191 | * Title level 192 | */ 193 | public function createTitle($level) 194 | { 195 | for ($currentLevel=0; $currentLevel<16; $currentLevel++) { 196 | if ($currentLevel > $level) { 197 | $this->levels[$currentLevel] = 1; 198 | $this->counters[$currentLevel] = 0; 199 | } 200 | } 201 | 202 | $this->levels[$level] = 1; 203 | $this->counters[$level]++; 204 | $token = array('title'); 205 | 206 | for ($i=1; $i<=$level; $i++) { 207 | $token[] = $this->counters[$i]; 208 | } 209 | 210 | return implode('.', $token); 211 | } 212 | 213 | /** 214 | * Get a level number 215 | */ 216 | public function getNumber($level) 217 | { 218 | return $this->levels[$level]++; 219 | } 220 | 221 | /** 222 | * Gets the variable value 223 | * 224 | * @param $name the variable name 225 | */ 226 | public function getVariable($variable, $default = null) 227 | { 228 | if (isset($this->variables[$variable])) { 229 | return $this->variables[$variable]; 230 | } 231 | 232 | return $default; 233 | } 234 | 235 | /** 236 | * Set the link url 237 | */ 238 | public function setLink($name, $url) 239 | { 240 | $name = trim(strtolower($name)); 241 | 242 | if ($name == '_') { 243 | $name = array_shift($this->anonymous); 244 | } 245 | 246 | $this->links[$name] = trim($url); 247 | } 248 | 249 | /** 250 | * Resets the anonymous stack 251 | */ 252 | public function resetAnonymousStack() 253 | { 254 | $this->anonymous = array(); 255 | } 256 | 257 | /** 258 | * Set the current anonymous links name 259 | */ 260 | public function pushAnonymous($name) 261 | { 262 | $this->anonymous[] = trim(strtolower($name)); 263 | } 264 | 265 | /** 266 | * Get a link value 267 | */ 268 | public function getLink($name, $relative = true) 269 | { 270 | $name = trim(strtolower($name)); 271 | if (isset($this->links[$name])) { 272 | $link = $this->links[$name]; 273 | 274 | if ($relative) { 275 | return $this->relativeUrl($link); 276 | } 277 | 278 | return $link; 279 | } 280 | 281 | return null; 282 | } 283 | 284 | /** 285 | * Adds a dependency to the document 286 | */ 287 | public function addDependency($dependency) 288 | { 289 | $dependency = $this->canonicalUrl($dependency); 290 | $this->dependencies[] = $dependency; 291 | } 292 | 293 | /** 294 | * Getting all the dependencies for this environment 295 | */ 296 | public function getDependencies() 297 | { 298 | return $this->dependencies; 299 | } 300 | 301 | /** 302 | * Resolves a relative URL using directories, for instance, if the 303 | * current directory is "path/to/something", and you want to get the 304 | * relative URL to "path/to/something/else.html", the result will 305 | * be else.html. Else, "../" will be added to go to the upper directory 306 | */ 307 | public function relativeUrl($url) 308 | { 309 | // If string contains ://, it is considered as absolute 310 | if (preg_match('/:\\/\\//mUsi', $url)) { 311 | return $url; 312 | } 313 | 314 | // If string begins with "/", the "/" is removed to resolve the 315 | // relative path 316 | if (strlen($url) && $url[0] == '/') { 317 | $url = substr($url, 1); 318 | if ($this->samePrefix($url)) { 319 | // If the prefix is the same, simply returns the file name 320 | $relative = basename($url); 321 | } else { 322 | // Else, returns enough ../ to get upper 323 | $relative = ''; 324 | 325 | for ($k=0; $k<$this->getDepth(); $k++) { 326 | $relative .= '../'; 327 | } 328 | 329 | $relative .= $url; 330 | } 331 | } else { 332 | $relative = $url; 333 | } 334 | 335 | return $relative; 336 | } 337 | 338 | /** 339 | * Use relative URLs for links 340 | */ 341 | public function useRelativeUrls() 342 | { 343 | return $this->relativeUrls; 344 | } 345 | 346 | /** 347 | * Use relative URLs for links 348 | */ 349 | public function setUseRelativeUrls($enable) 350 | { 351 | $this->relativeUrls = $enable; 352 | } 353 | 354 | /** 355 | * Get the depth of the current file name (the number of parent 356 | * directories) 357 | */ 358 | public function getDepth() 359 | { 360 | return count(explode('/', $this->currentFileName))-1; 361 | } 362 | 363 | /** 364 | * Returns true if the given url have the same prefix as the 365 | * current document 366 | */ 367 | protected function samePrefix($url) 368 | { 369 | $partsA = explode('/', $url); 370 | $partsB = explode('/', $this->currentFileName); 371 | 372 | $n = count($partsA); 373 | if ($n != count($partsB)) { 374 | return false; 375 | } 376 | 377 | unset($partsA[$n-1]); 378 | unset($partsB[$n-1]); 379 | 380 | return $partsA == $partsB; 381 | } 382 | 383 | /** 384 | * Returns the directory name 385 | */ 386 | public function getDirName() 387 | { 388 | $dirname = dirname($this->currentFileName); 389 | 390 | if ($dirname == '.') { 391 | return ''; 392 | } 393 | 394 | return $dirname; 395 | } 396 | 397 | /** 398 | * Canonicalize a path, a/b/c/../d/e will become 399 | * a/b/d/e 400 | */ 401 | protected function canonicalize($url) 402 | { 403 | $parts = explode('/', $url); 404 | $stack = array(); 405 | 406 | foreach ($parts as $part) { 407 | if ($part == '..') { 408 | array_pop($stack); 409 | } else { 410 | $stack[] = $part; 411 | } 412 | } 413 | 414 | return implode('/', $stack); 415 | } 416 | 417 | /** 418 | * Gets a canonical URL from the given one 419 | */ 420 | public function canonicalUrl($url) 421 | { 422 | if (strlen($url)) { 423 | if ($url[0] == '/') { 424 | // If the URL begins with a "/", the following is the 425 | // canonical URL 426 | return substr($url, 1); 427 | } else { 428 | // Else, the canonical name is under the current dir 429 | if ($this->getDirName()) { 430 | return $this->canonicalize($this->getDirName() . '/' .$url); 431 | } else { 432 | return $this->canonicalize($url); 433 | } 434 | } 435 | } 436 | 437 | return null; 438 | } 439 | 440 | /** 441 | * Sets the current file name 442 | */ 443 | public function setCurrentFileName($filename) 444 | { 445 | $this->currentFileName = $filename; 446 | } 447 | 448 | /** 449 | * Sets the directory of the current parsing 450 | */ 451 | public function setCurrentDirectory($directory) 452 | { 453 | $this->currentDirectory = $directory; 454 | } 455 | 456 | /** 457 | * Returns an absolute path for a relative given URL 458 | */ 459 | public function absoluteRelativePath($url) 460 | { 461 | return $this->currentDirectory . '/' . $this->getDirName() . '/' . $this->relativeUrl($url); 462 | } 463 | 464 | public function setTargetDirectory($directory) 465 | { 466 | $this->targetDirectory = $directory; 467 | } 468 | 469 | public function getTargetDirectory() 470 | { 471 | return $this->targetDirectory; 472 | } 473 | 474 | public function getUrl() 475 | { 476 | if ($this->url) { 477 | return $this->url; 478 | } else { 479 | return $this->currentFileName; 480 | } 481 | } 482 | 483 | public function setUrl($url) 484 | { 485 | if ($this->getDirName()) { 486 | $url = $this->getDirName() . '/' . $url; 487 | } 488 | 489 | $this->url = $url; 490 | } 491 | 492 | public function getMetas() 493 | { 494 | return $this->metas; 495 | } 496 | 497 | public function getLevel($letter) 498 | { 499 | foreach ($this->titleLetters as $level => $titleLetter) { 500 | if ($letter == $titleLetter) { 501 | return $level; 502 | } 503 | } 504 | 505 | $this->currentTitleLevel++; 506 | $this->titleLetters[$this->currentTitleLevel] = $letter; 507 | return $this->currentTitleLevel; 508 | } 509 | 510 | public function getTitleLetters() 511 | { 512 | return $this->titleLetters; 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /ErrorManager.php: -------------------------------------------------------------------------------- 1 | abort = $abort; 12 | } 13 | 14 | public function error($message) 15 | { 16 | if ($this->abort) { 17 | throw new \Exception($message); 18 | } else { 19 | echo '/!\\ '.$message."\n"; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /HTML/Directives/Div.php: -------------------------------------------------------------------------------- 1 | ', ''); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /HTML/Directives/Figure.php: -------------------------------------------------------------------------------- 1 | getEnvironment(); 31 | $url = $environment->relativeUrl($data); 32 | 33 | return new FigureNode(new ImageNode($url, $options), $document); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /HTML/Directives/Image.php: -------------------------------------------------------------------------------- 1 | getEnvironment(); 28 | $url = $environment->relativeUrl($data); 29 | 30 | return new ImageNode($url, $options); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /HTML/Directives/Meta.php: -------------------------------------------------------------------------------- 1 | getDocument(); 26 | 27 | foreach ($options as $key => $value) { 28 | $meta = new MetaNode($key, $value); 29 | $document->addHeaderNode($meta); 30 | } 31 | 32 | if ($node) { 33 | $document->addNode($node); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /HTML/Directives/Stylesheet.php: -------------------------------------------------------------------------------- 1 | getDocument(); 25 | 26 | $document->addCss($data); 27 | 28 | if ($node) { 29 | $document->addNode($node); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /HTML/Directives/Title.php: -------------------------------------------------------------------------------- 1 | getDocument(); 25 | 26 | $document->addHeaderNode(new RawNode('
language."\">".htmlspecialchars($this->value)."
";
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/HTML/Nodes/FigureNode.php:
--------------------------------------------------------------------------------
1 | ';
12 | $html .= $this->image->render();
13 | if ($this->document) {
14 | $caption = trim($this->document->render());
15 | if ($caption) {
16 | $html .= ''.$text.'
'; 15 | } else { 16 | return ''; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /HTML/Nodes/QuoteNode.php: -------------------------------------------------------------------------------- 1 | ".$this->value.""; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /HTML/Nodes/SeparatorNode.php: -------------------------------------------------------------------------------- 1 | '; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /HTML/Nodes/TableNode.php: -------------------------------------------------------------------------------- 1 | '; 12 | foreach ($this->data as $k=>&$row) { 13 | if (!$row) { 14 | continue; 15 | } 16 | 17 | $html .= ''.$text.'
';
32 | }
33 |
34 | public function link($url, $title)
35 | {
36 | return ''.$title.'';
37 | }
38 |
39 | public function escape($span)
40 | {
41 | return htmlspecialchars($span);
42 | }
43 |
44 | public function reference($reference, $value)
45 | {
46 | if ($reference) {
47 | $text = $value['text'] ?: (isset($reference['title']) ? $reference['title'] : '');
48 | $url = $reference['url'];
49 | if ($value['anchor']) {
50 | $url .= '#' . $value['anchor'];
51 | }
52 | $link = $this->link($url, trim($text));
53 | } else {
54 | $link = $this->link('#', '(unresolved reference)');
55 | }
56 |
57 | return $link;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Kernel.php:
--------------------------------------------------------------------------------
1 | getName().'\\'.$name;
21 | }
22 |
23 | /**
24 | * Create an instance of some class
25 | */
26 | public function build($name, $arg1 = null, $arg2 = null, $arg3 = null, $arg4 = null)
27 | {
28 | $class = $this->getClass($name);
29 |
30 | if ($class) {
31 | return new $class($arg1, $arg2, $arg3, $arg4);
32 | }
33 |
34 | return null;
35 | }
36 |
37 | /**
38 | * Gets the available directives
39 | */
40 | public function getDirectives()
41 | {
42 | return array(
43 | new Directives\Dummy,
44 | new Directives\CodeBlock,
45 | new Directives\Raw,
46 | new Directives\Replace,
47 | new Directives\Toctree,
48 | new Directives\Document,
49 | new Directives\RedirectionTitle,
50 | );
51 | }
52 |
53 | /**
54 | * Document references
55 | */
56 | public function getReferences()
57 | {
58 | return array(
59 | new References\Doc,
60 | new References\Doc('ref'),
61 | );
62 | }
63 |
64 | /**
65 | * Allowing the kernel to tweak document after the build
66 | */
67 | public function postParse(Document $document)
68 | {
69 | }
70 |
71 | /**
72 | * Allowing the kernel to tweak the builder
73 | */
74 | public function initBuilder(Builder $builder)
75 | {
76 | }
77 |
78 | /**
79 | * Get the output files extension
80 | */
81 | public function getFileExtension()
82 | {
83 | return 'txt';
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) <2013> Grégoire Passault
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/LaTeX/Directives/Image.php:
--------------------------------------------------------------------------------
1 | getEnvironment();
28 | $url = $environment->relativeUrl($data);
29 |
30 | return new ImageNode($url, $options);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LaTeX/Directives/LaTeXMain.php:
--------------------------------------------------------------------------------
1 | getDocument();
26 |
27 | foreach ($options as $key => $value) {
28 | $meta = new MetaNode($key, $value);
29 | $document->addHeaderNode($meta);
30 | }
31 |
32 | if ($node) {
33 | $document->addNode($node);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LaTeX/Directives/Stylesheet.php:
--------------------------------------------------------------------------------
1 | getDocument();
25 |
26 | $document->addHeaderNode(new RawNode('\title{'.$data.'}'));
27 |
28 | if ($node) {
29 | $document->addNode($node);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LaTeX/Directives/Url.php:
--------------------------------------------------------------------------------
1 | getEnvironment();
21 | $environment->setUrl(trim($data));
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/LaTeX/Directives/Wrap.php:
--------------------------------------------------------------------------------
1 | class = $class;
19 | }
20 |
21 | public function getName()
22 | {
23 | return $this->class;
24 | }
25 |
26 | public function processSub(Parser $parser, $document, $variable, $data, array $options)
27 | {
28 | return $document;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LaTeX/Document.php:
--------------------------------------------------------------------------------
1 | nodes as $node) {
16 | $document .= $node->render() . "\n";
17 | }
18 |
19 | return $document;
20 | }
21 |
22 | public function renderDocument()
23 | {
24 | $isMain = count($this->getNodes(function($node) {
25 | return $node instanceof LaTeXMainNode;
26 | })) != 0;
27 |
28 | $document = '';
29 |
30 | if ($isMain) {
31 | $document .= "\\documentclass[11pt]{report}\n";
32 | $document .= "\\usepackage[utf8]{inputenc}\n";
33 | $document .= "\\usepackage[T1]{fontenc}\n";
34 | $document .= "\\usepackage[french]{babel}\n";
35 | $document .= "\\usepackage{cite}\n";
36 | $document .= "\\usepackage{amssymb}\n";
37 | $document .= "\\usepackage{amsmath}\n";
38 | $document .= "\\usepackage{mathrsfs}\n";
39 | $document .= "\\usepackage{graphicx}\n";
40 | $document .= "\\usepackage{hyperref}\n";
41 | $document .= "\\usepackage{listings}\n";
42 |
43 | foreach ($this->headerNodes as $node) {
44 | $document .= $node->render()."\n";
45 | }
46 | $document .= "\\begin{document}\n";
47 | }
48 |
49 | $document .= "\label{".$this->environment->getUrl()."}\n";
50 | $document .= $this->render();
51 |
52 | if ($isMain) {
53 | $document .= "\\end{document}\n";
54 | }
55 |
56 | return $document;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/LaTeX/Environment.php:
--------------------------------------------------------------------------------
1 | value.'}';
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/LaTeX/Nodes/CodeNode.php:
--------------------------------------------------------------------------------
1 | language."}\n";
12 | $tex .= "\\begin{lstlisting}\n";
13 | $tex .= $this->value . "\n";
14 | $tex .= "\\end{lstlisting}\n";
15 |
16 | return $tex;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/LaTeX/Nodes/ImageNode.php:
--------------------------------------------------------------------------------
1 | options as $key => $value) {
13 | $attributes[] = $key . '='.$value;
14 | }
15 |
16 | return '\includegraphics{'.$this->url.'}';
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/LaTeX/Nodes/LaTeXMainNode.php:
--------------------------------------------------------------------------------
1 | value;
12 |
13 | if (trim($text) !== '') {
14 | return $text."\n";
15 | } else {
16 | return '';
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/LaTeX/Nodes/QuoteNode.php:
--------------------------------------------------------------------------------
1 | value."\n\\end{quotation}\n";
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/LaTeX/Nodes/SeparatorNode.php:
--------------------------------------------------------------------------------
1 | data as &$row) {
15 | if (!$row) {
16 | continue;
17 | }
18 |
19 | $rowTex = '';
20 | $cols = max($cols, count($row));
21 |
22 | foreach ($row as $n => &$col) {
23 | $rowTex .= $col->render();
24 |
25 | if ($n+1 < count($row)) {
26 | $rowTex .= ' & ';
27 | }
28 | }
29 | $rowTex .= ' \\\\' . "\n";
30 | $rows[] = $rowTex;
31 | }
32 | $aligns = array();
33 | for ($i=0; $i<$cols; $i++) {
34 | $aligns[] = 'l';
35 | }
36 |
37 | $aligns = '|'.implode('|', $aligns).'|';
38 | $rows = "\\hline\n".implode("\\hline\n", $rows)."\\hline\n";
39 |
40 | $tex = "\\begin{tabular}{".$aligns."}\n".$rows."\n\\end{tabular}\n";
41 |
42 | return $tex;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/LaTeX/Nodes/TitleNode.php:
--------------------------------------------------------------------------------
1 | level > 1) {
14 | $type = 'section';
15 |
16 | for ($i=2; $i<$this->level; $i++) {
17 | $type = 'sub'.$type;
18 | }
19 | }
20 |
21 | return '\\'.$type.'{'.$this->value.'}';
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/LaTeX/Nodes/TocNode.php:
--------------------------------------------------------------------------------
1 | files as $file) {
14 | $reference = $this->environment->resolve('doc', $file);
15 | $reference['url'] = $this->environment->relativeUrl($reference['url']);
16 | $tex .= "\\input{".$reference['url']."}\n";
17 | }
18 |
19 | return $tex;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/LaTeX/Span.php:
--------------------------------------------------------------------------------
1 | environment->getUrl();
39 | }
40 | $url = substr($url, 1);
41 | $url = $url ? '#'.$url : '';
42 | return '\ref{'.$refDoc.$url.'}';
43 | } else {
44 | return '\href{'.$url.'}{'.$title.'}';
45 | }
46 | }
47 |
48 | public function escape($span)
49 | {
50 | return $span;
51 | }
52 |
53 | public function reference($reference, $value)
54 | {
55 | if ($reference) {
56 | $file = $reference['file'];
57 | $text = $value['text'] ?: (isset($reference['title']) ? $reference['title'] : '');
58 | $refDoc = $file;
59 | $url = '#';
60 | if ($value['anchor']) {
61 | $url .= $value['anchor'];
62 | }
63 | $link = $this->link($url, trim($text), $refDoc);
64 | } else {
65 | $link = $this->link('#', '(unresolved reference)');
66 | }
67 |
68 | return $link;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Metas.php:
--------------------------------------------------------------------------------
1 | entries = $entries;
14 | }
15 | }
16 |
17 | public function getAll()
18 | {
19 | return $this->entries;
20 | }
21 |
22 | /**
23 | * Sets the meta for url, giving the title, the modification time and
24 | * the dependencies list
25 | */
26 | public function set($file, $url, $title, $titles, $tocs, $ctime, array $depends)
27 | {
28 | foreach ($tocs as $toc) {
29 | foreach ($toc as $child) {
30 | $this->parents[$child] = $file;
31 | if (isset($this->entries[$child])) {
32 | $this->entries[$child]['parent'] = $file;
33 | }
34 | }
35 | }
36 |
37 | $this->entries[$file] = array(
38 | 'file' => $file,
39 | 'url' => $url,
40 | 'title' => $title,
41 | 'titles' => $titles,
42 | 'tocs' => $tocs,
43 | 'ctime' => $ctime,
44 | 'depends' => $depends
45 | );
46 |
47 | if (isset($this->parents[$file])) {
48 | $this->entries[$file]['parent'] = $this->parents[$file];
49 | }
50 | }
51 |
52 | /**
53 | * Gets the meta for a given document reference url
54 | */
55 | public function get($url)
56 | {
57 | if (isset($this->entries[$url])) {
58 | return $this->entries[$url];
59 | } else {
60 | return null;
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Nodes/AnchorNode.php:
--------------------------------------------------------------------------------
1 | value = implode("\n", $lines);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Nodes/CodeNode.php:
--------------------------------------------------------------------------------
1 | language = $language;
13 | }
14 |
15 | public function getLanguage()
16 | {
17 | return $this->language;
18 | }
19 |
20 | public function setRaw($raw)
21 | {
22 | $this->raw = $raw;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Nodes/DocumentNode.php:
--------------------------------------------------------------------------------
1 | data = $data;
12 | }
13 |
14 | public function render()
15 | {
16 | return '';
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Nodes/FigureNode.php:
--------------------------------------------------------------------------------
1 | image = $image;
13 | $this->document = $document;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Nodes/ImageNode.php:
--------------------------------------------------------------------------------
1 | url = $url;
13 | $this->options = $options;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Nodes/ListNode.php:
--------------------------------------------------------------------------------
1 | lines[] = $line;
20 | }
21 |
22 | public function render()
23 | {
24 | $depth = -1;
25 | $value = '';
26 | $stack = array();
27 |
28 | foreach ($this->lines as $line) {
29 | $prefix = $line['prefix'];
30 | $text = $line['text'];
31 | $ordered = $line['ordered'];
32 | $newDepth = $line['depth'];
33 |
34 | if ($depth < $newDepth) {
35 | $tags = $this->createList($ordered);
36 | $value .= $tags[0];
37 | $stack[] = array($newDepth, $tags[1]."\n");
38 | $depth = $newDepth;
39 | }
40 |
41 | while ($depth > $newDepth) {
42 | $top = $stack[count($stack)-1];
43 |
44 | if ($top[0] > $newDepth) {
45 | $value .= $top[1];
46 | array_pop($stack);
47 | $top = $stack[count($stack)-1];
48 | $depth = $top[0];
49 | }
50 | }
51 |
52 | $value .= $this->createElement($text, $prefix)."\n";
53 | }
54 |
55 | while ($stack) {
56 | list($d, $closing) = array_pop($stack);
57 | $value .= $closing;
58 | }
59 |
60 | return $value;
61 | }
62 |
63 | abstract protected function createElement($text, $prefix);
64 | abstract protected function createList($ordered);
65 | }
66 |
--------------------------------------------------------------------------------
/Nodes/MetaNode.php:
--------------------------------------------------------------------------------
1 | key = $key;
13 | $this->value = $value;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Nodes/Node.php:
--------------------------------------------------------------------------------
1 | value = $value;
12 | }
13 |
14 | public function getValue()
15 | {
16 | return $this->value;
17 | }
18 |
19 | public function setValue($value)
20 | {
21 | $this->value = $value;
22 | }
23 |
24 | abstract public function render();
25 |
26 | public function __toString()
27 | {
28 | return $this->render();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Nodes/ParagraphNode.php:
--------------------------------------------------------------------------------
1 | value;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Nodes/SeparatorNode.php:
--------------------------------------------------------------------------------
1 | level = $level;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Nodes/TableNode.php:
--------------------------------------------------------------------------------
1 | parts = $parts;
16 | $this->data[] = array();
17 | }
18 |
19 | /**
20 | * Gets the columns count of the table
21 | */
22 | public function getCols()
23 | {
24 | return count($this->parts[2]);
25 | }
26 |
27 | /**
28 | * Gets the rows count of the table
29 | */
30 | public function getRows()
31 | {
32 | return count($this->data)-1;
33 | }
34 |
35 | public function push($parts, $line)
36 | {
37 | if ($parts) {
38 | // New line in the tab
39 | if ($parts[2] != $this->parts[2]) {
40 | return false;
41 | }
42 |
43 | if ($parts[0]) {
44 | $this->headers[count($this->data)-1] = true;
45 | }
46 | $this->data[] = array();
47 | } else {
48 | // Pushing data in the cells
49 | list($header, $pretty, $parts) = $this->parts;
50 | $row = &$this->data[count($this->data)-1];
51 |
52 | $partsCount = count($parts);
53 | for ($k = 1; $k <= $partsCount; $k++) {
54 | if ($k === $partsCount) {
55 | $data = mb_substr($line, $parts[$k-1]);
56 | } else {
57 | $data = mb_substr($line, $parts[$k-1], $parts[$k]-$parts[$k-1]);
58 | }
59 |
60 | if ($pretty) {
61 | $data = mb_substr($data, 0, -1);
62 | }
63 |
64 | $data = trim($data);
65 |
66 | if (isset($row[$k-1])) {
67 | $row[$k-1] = trim($row[$k-1].' '.$data);
68 | } else {
69 | $row[$k-1] = $data;
70 | }
71 | }
72 | }
73 |
74 | return true;
75 | }
76 |
77 | public function finalize(Parser $parser)
78 | {
79 | foreach ($this->data as &$row) {
80 | if ($row) {
81 | foreach ($row as &$col) {
82 | $col = $parser->createSpan($col);
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Nodes/TitleNode.php:
--------------------------------------------------------------------------------
1 | level = $level;
15 | $this->token = $token;
16 | }
17 |
18 | public function getLevel()
19 | {
20 | return $this->level;
21 | }
22 |
23 | public function setTarget($target)
24 | {
25 | $this->target = $target;
26 | }
27 |
28 | public function getTarget()
29 | {
30 | return $this->target;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Nodes/TocNode.php:
--------------------------------------------------------------------------------
1 | files = $files;
16 | $this->environment = $environment;
17 | $this->options = $options;
18 | }
19 |
20 | public function getFiles()
21 | {
22 | return $this->files;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Nodes/WrapperNode.php:
--------------------------------------------------------------------------------
1 | node = $node;
14 | $this->before = $before;
15 | $this->after = $after;
16 | }
17 |
18 | public function render()
19 | {
20 | $contents = $this->node ? $this->node->render() : '';
21 |
22 | return $this->before . $contents . $this->after;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RST
2 |
3 | [](https://travis-ci.org/Gregwar/RST)
4 | [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YUXRLWHQSWS6L)
5 |
6 | PHP library to parse reStructuredText document
7 |
8 | ## Usage
9 |
10 | The parser can be used this way:
11 |
12 | ```php
13 | `_
29 | ';
30 |
31 | // Parse it
32 | $document = $parser->parse($rst);
33 |
34 | // Render it
35 | echo $document;
36 | /* Will output, in HTML mode:
37 | This is a RST document!
40 |You can get it on the GitHub page
42 | */ 43 | ``` 44 | 45 | For more information, you can have a look at `test/document/document.rst` and its result 46 | `test/document/document.html` 47 | 48 | ## Using the builder 49 | 50 | The builder is another tool that will parses a whole tree of documents and generates 51 | an output directory containing files. 52 | 53 | You can simply use it with: 54 | 55 | ```php 56 | build('input', 'output'); 60 | ``` 61 | 62 | It will parses all the files in the `input` directory, starting with `index.rst` and 63 | scanning for dependencies references and generates you target files in the `output` 64 | directory. Default format is HTML. 65 | 66 | You can use those methods on it to customize the build: 67 | 68 | * `copy($source, $destination)`: copy the `$source` file or directory to the `$destination` 69 | file or directory of the build 70 | * `mkdir($directory)`: create the `$directory` in build directory 71 | * `addHook($function)`: adds an hook that will be called after each document is parsed, this 72 | hook will be called with the `$document` as parameter and can then tweak it as you want 73 | * `addBeforeHook($function)`: adds an hook that will be called before parsing the 74 | document, the parser will be passed as a parameter 75 | 76 | ## Abort on error 77 | 78 | In some situation you want the build to continue even if there is some errors, 79 | like missing references: 80 | 81 | ```php 82 | getEnvironment()->getErrorManager()->abortOnError(false); 86 | 87 | // Using builder 88 | $builder->getErrorManager()->abortOnError(false); 89 | ``` 90 | 91 | ## Writing directives 92 | 93 | ### Step 1: Extends the Directive class 94 | 95 | Write your own class that extends the `Gregwar\RST\Directive` class, and define the 96 | method `getName()` that return the directive name. 97 | 98 | You can then redefine one of the following method: 99 | 100 | * `processAction()` if your directive simply tweak the document without modifying the nodes 101 | * `processNode()` if your directive is adding a node 102 | * `process()` if your directive is tweaking the node that just follows it 103 | 104 | See `Directive.php` for more information 105 | 106 | ### Step 2: Register your directive 107 | 108 | You can register your directive by directly calling `registerDirective()` on your 109 | `Parser` object. 110 | 111 | Else, you will have to also create your own kernel by extending the `Kernel` class 112 | and adding your own logic to define extra directives, see `Kernel.php` for more information. 113 | Then, pass the kernel when constructing the `Parser` or the `Builder` 114 | 115 | ## License 116 | 117 | This library is under MIT license 118 | -------------------------------------------------------------------------------- /Reference.php: -------------------------------------------------------------------------------- 1 | name = $name; 15 | } 16 | 17 | public function getName() 18 | { 19 | return $this->name; 20 | } 21 | 22 | public function resolve(Environment $environment, $data) 23 | { 24 | $metas = $environment->getMetas(); 25 | $file = $environment->canonicalUrl($data); 26 | 27 | if ($metas && $metas->get($file)) { 28 | $entry = $metas->get($file); 29 | $entry['url'] = $environment->relativeUrl('/'.$entry['url']); 30 | } else { 31 | $entry = array( 32 | 'title' => '(unresolved)', 33 | 'url' => '#' 34 | ); 35 | } 36 | 37 | return $entry; 38 | } 39 | 40 | public function found(Environment $environment, $data) 41 | { 42 | $environment->addDependency($data); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Span.php: -------------------------------------------------------------------------------- 1 | tokenPrefix) { 19 | $this->tokenPrefix = mt_rand().'|'.time(); 20 | } 21 | $this->tokenId++; 22 | return sha1($this->tokenPrefix.'|'.$this->tokenId); 23 | } 24 | 25 | public function __construct(Parser $parser, $span, $tokens = array()) 26 | { 27 | if (is_array($span)) { 28 | $span = implode("\n", $span); 29 | } 30 | 31 | // Replacing literal with tokens 32 | $span = preg_replace_callback('/``(.+)``(?!`)/mUsi', function($match) use (&$tokens) { 33 | $id = $this->generateToken(); 34 | $tokens[$id] = array( 35 | 'type' => 'literal', 36 | 'text' => htmlspecialchars($match[1]) 37 | ); 38 | 39 | return $id; 40 | }, $span); 41 | 42 | $environment = $parser->getEnvironment(); 43 | $this->environment = $environment; 44 | 45 | // Replacing numbering 46 | foreach ($environment->getTitleLetters() as $level => $letter) { 47 | $span = preg_replace_callback('/\#\\'.$letter.'/mUsi', function($match) use ($environment, $level) { 48 | return $environment->getNumber($level); 49 | }, $span); 50 | } 51 | 52 | // Signaling anonymous names 53 | $environment->resetAnonymousStack(); 54 | if (preg_match_all('/(([a-z0-9]+)|(`(.+)`))__/mUsi', $span, $matches)) { 55 | foreach ($matches[2] as $k => $y) { 56 | $name = $matches[2][$k] ?: $matches[4][$k]; 57 | $environment->pushAnonymous($name); 58 | } 59 | } 60 | 61 | // Looking for references to other documents 62 | $span = preg_replace_callback('/:([a-z0-9]+):`(.+)`/mUsi', function($match) use (&$environment, &$tokens) { 63 | $section = $match[1]; 64 | $url = $match[2]; 65 | $id = $this->generateToken(); 66 | $anchor = null; 67 | 68 | $text = null; 69 | if (preg_match('/^(.+)<(.+)>$/mUsi', $url, $match)) { 70 | $text = $match[1]; 71 | $url = $match[2]; 72 | } 73 | 74 | if (preg_match('/^(.+)#(.+)$/mUsi', $url, $match)) { 75 | $url = $match[1]; 76 | $anchor = $match[2]; 77 | } 78 | 79 | $tokens[$id] = array( 80 | 'type' => 'reference', 81 | 'section' => $section, 82 | 'url' => $url, 83 | 'text' => $text, 84 | 'anchor' => $anchor 85 | ); 86 | 87 | $environment->found($section, $url); 88 | 89 | return $id; 90 | }, $span); 91 | 92 | // Link callback 93 | $linkCallback = function($match) use ($environment, &$tokens) { 94 | $link = $match[2] ?: $match[4]; 95 | $id = $this->generateToken(); 96 | $next = $match[5]; 97 | $url = null; 98 | 99 | if (preg_match('/^(.+) <(.+)>$/mUsi', $link, $match)) { 100 | $link = $match[1]; 101 | $environment->setLink($link, $match[2]); 102 | $url = $match[2]; 103 | } 104 | 105 | $tokens[$id] = array( 106 | 'type' => 'link', 107 | 'link' => $link, 108 | 'url' => $url 109 | ); 110 | 111 | return $id.$next; 112 | }; 113 | 114 | // Replacing anonymous links 115 | $span = preg_replace_callback('/(([a-z0-9]+)|(`(.+)`))__([^a-z0-9]{1}|$)/mUsi', $linkCallback, $span); 116 | 117 | // Replacing links 118 | $span = preg_replace_callback('/(([a-z0-9]+)|(`(.+)`))_([^a-z0-9]{1}|$)/mUsi', $linkCallback, $span); 119 | 120 | $this->tokens = $tokens; 121 | $this->parser = $parser; 122 | $this->span = $span; 123 | } 124 | 125 | /** 126 | * Processes some data in the context of the span, this will process the 127 | * **emphasis**, the nbsp, replace variables and end-of-line brs 128 | */ 129 | public function process($data) 130 | { 131 | $self = $this; 132 | $environment = $this->parser->getEnvironment(); 133 | 134 | $span = $this->escape($data); 135 | 136 | // Emphasis 137 | $span = preg_replace_callback('/\*\*(.+)\*\*/mUsi', function ($matches) use ($self) { 138 | return $self->strongEmphasis($matches[1]); 139 | }, $span); 140 | $span = preg_replace_callback('/\*(.+)\*/mUsi', function ($matches) use ($self) { 141 | return $self->emphasis($matches[1]); 142 | }, $span); 143 | 144 | // Nbsp 145 | $span = preg_replace('/~/', $this->nbsp(), $span); 146 | 147 | // Replacing variables 148 | $span = preg_replace_callback('/\|(.+)\|/mUsi', function($match) use ($environment) { 149 | return $environment->getVariable($match[1]); 150 | }, $span); 151 | 152 | // Adding brs when a space is at the end of a line 153 | $span = preg_replace('/ \n/', $this->br(), $span); 154 | 155 | return $span; 156 | } 157 | 158 | /** 159 | * Renders the span 160 | */ 161 | public function render() 162 | { 163 | $environment = $this->parser->getEnvironment(); 164 | $span = $this->process($this->span); 165 | 166 | // Replacing tokens 167 | if ($this->tokens) { 168 | foreach ($this->tokens as $id => $value) { 169 | switch ($value['type']) { 170 | case 'raw': 171 | $span = str_replace($id, $value['text'], $span); 172 | break; 173 | case 'literal': 174 | $span = str_replace($id, $this->literal($value['text']), $span); 175 | break; 176 | case 'reference': 177 | $reference = $environment->resolve($value['section'], $value['url']); 178 | $link = $this->reference($reference, $value); 179 | 180 | $span = str_replace($id, $link, $span); 181 | break; 182 | case 'link': 183 | if ($value['url']) { 184 | if ($environment->useRelativeUrls()) { 185 | $url = $environment->relativeUrl($value['url']); 186 | } else { 187 | $url = $value['url']; 188 | } 189 | } else { 190 | $url = $environment->getLink($value['link']); 191 | } 192 | $link = $this->link($url, $this->process($value['link'])); 193 | $span = str_replace($id, $link, $span); 194 | break; 195 | } 196 | } 197 | } 198 | 199 | return $span; 200 | } 201 | 202 | public function emphasis($text) 203 | { 204 | return $text; 205 | } 206 | 207 | public function strongEmphasis($text) 208 | { 209 | return $text; 210 | } 211 | 212 | public function nbsp() 213 | { 214 | return ' '; 215 | } 216 | 217 | public function br() 218 | { 219 | return "\n"; 220 | } 221 | 222 | public function literal($text) 223 | { 224 | return $text; 225 | } 226 | 227 | public function link($url, $title) 228 | { 229 | return $title.' ('.$url.')'; 230 | } 231 | 232 | public function escape($span) 233 | { 234 | return $span; 235 | } 236 | 237 | public function reference($reference, $value) 238 | { 239 | if ($reference) { 240 | $text = $value['text'] ?: (isset($reference['title']) ? $reference['title'] : ''); 241 | $link = $this->link($url, trim($text)); 242 | } else { 243 | $link = $this->link('#', '(unresolved reference)'); 244 | } 245 | 246 | return $link; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /SubDirective.php: -------------------------------------------------------------------------------- 1 | getSubParser(); 25 | 26 | if ($node instanceof CodeNode) { 27 | $document = $subParser->parseLocal($node->getValue()); 28 | } else { 29 | $document = $node; 30 | } 31 | 32 | $newNode = $this->processSub($parser, $document, $variable, $data, $options); 33 | 34 | if ($newNode) { 35 | if ($variable) { 36 | $parser->getEnvironment()->setVariable($variable, $newNode); 37 | } else { 38 | $parser->getDocument()->addNode($newNode); 39 | } 40 | } 41 | } 42 | 43 | /** 44 | * Process a sub directive 45 | */ 46 | public function processSub(Parser $parser, $document, $variable, $data, array $options) 47 | { 48 | return null; 49 | } 50 | 51 | public function wantCode() 52 | { 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | =5.3.0", 16 | "symfony/polyfill-mbstring": "^1.12" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^6.4" 20 | }, 21 | "target-dir": "Gregwar/RST", 22 | "autoload": { 23 | "psr-0": { 24 | "Gregwar\\RST": "" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 |This is a RST document!
27 |You can get it on the GitHub page
29 | */ 30 | -------------------------------------------------------------------------------- /test/document/document.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |Reference to HoHo
13 |Note:
This is a testing sandbox, if you want to understand how it works, have a
14 | look to the rst original file
Titles can be wrote using the underlining, the default order is:
19 |=======
-------
~~~~~~~
*******
This is not a standard of RST but is really useful, you can use the special syntax
27 | #
followed by the letter of the title you are in (it resets the counter when used).
You can for instance use Question #*
if you are under an *******
, the number
29 | displayed will be auto-incremented:
Question #*
31 |The first question
32 |Question #*
33 |The second question
34 |This can of course also be used to number parts, chapter etc.
35 |A separator is like a title underline but without any text above:
37 |-----
38 |
39 | This will result in a text separation:
40 |*italic*
renders as italic**strong**
renders as strongYou can force a line break by adding an extra space at the end of a line
47 |Tables can be created using the line separator ====
:
================ ================
50 | **First column** **Other column**
51 | ================ ================
52 | Second row with Second row, of the
53 | some contents text other column
54 | ============ ================
55 |
56 | Will result in:
57 |First column | Other column |
Second row with some contents text | Second row, of the other column |
Another example:
59 |Col A | Col B | Col C |
Col X | Col Y | Col Z |
Col U | Col J | Col K |
Lists can be ordered or unordered, and nested, for instance this:
62 |* Element A
63 | * Sub A, this a
64 | multiline sentence in the source
65 | 1. Sub ordered as "1"
66 | 2. Sub ordered as "2"
67 | * Sub hello two
68 | * Element B
69 |
70 | While result in:
71 |You can quote a block by indenting it:
85 |This is a normal pagagraph
86 |
87 | This is a quote
88 |
89 | Will result in:
90 |This is a normal paragraph
91 |93 |This is a quote
92 |
You can quote code the same way as quote, but using the ::
at the end
95 | of the previous paragraph:
Here is a piece of code:
97 |
98 | .. code-block:: php
99 |
100 | <?php
101 |
102 | echo "I love RST";
103 |
104 | Will result in:
105 |Here is a piece of code:
106 |<?php
107 |
108 | echo "I love RST";
109 |
110 | Links can be defined once for all using the trailing _
, like this:
PHP_ is a great language
114 |
115 | .. _PHP: http://php.net/
116 |
117 | Will result in:
118 |PHP is a great language
119 |Anonymous links can also be used to avoid copying the name just after the 121 | block that uses it, for instance:
122 |I love GitHub__
123 |
124 | .. __: http://www.github.com/
125 |
126 | Will result in:
127 |I love GitHub
128 |You can use the following shortcut:
129 |I love GitHub__
130 |
131 | __ http://www.github.com/
132 |
133 | You can also define the link target inside the link:
135 |Do you know `Google <http://www.google.com>`_ ?
136 |
137 | Will result in:
138 |Do you know Google ?
139 |An anchor can be used like this:
141 |.. _anchor:
142 |
143 | Some anchor section, you can link to it like `this <#anchor>`_
144 |
145 | Will result in:
146 | 147 |Some anchor section, you can link to it like this
148 |You can include a file with the include pseudo-directive:
151 |.. include:: file.rst
152 |
153 | You can use the replace directive like this:
155 |.. |name| replace:: bob
156 |
157 | Hello |name| !
158 |
159 | Will result in:
160 |Hello bob !
161 |The image
directive can be used to display images, this way:
.. image:: rst.png
164 | :width: 250px
165 | :title: RST logo
166 |
167 | Will result in:
168 |This is a simple note!
', $contents); 95 | $this->assertContains('