You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1158 lines
35 KiB

  1. <?php
  2. use dokuwiki\Extension\Event;
  3. use dokuwiki\Extension\SyntaxPlugin;
  4. use dokuwiki\Parsing\Handler\Block;
  5. use dokuwiki\Parsing\Handler\CallWriter;
  6. use dokuwiki\Parsing\Handler\CallWriterInterface;
  7. use dokuwiki\Parsing\Handler\Lists;
  8. use dokuwiki\Parsing\Handler\Nest;
  9. use dokuwiki\Parsing\Handler\Preformatted;
  10. use dokuwiki\Parsing\Handler\Quote;
  11. use dokuwiki\Parsing\Handler\Table;
  12. /**
  13. * Class Doku_Handler
  14. */
  15. class Doku_Handler
  16. {
  17. /** @var CallWriterInterface */
  18. protected $callWriter;
  19. /** @var array The current CallWriter will write directly to this list of calls, Parser reads it */
  20. public $calls = [];
  21. /** @var array internal status holders for some modes */
  22. protected $status = [
  23. 'section' => false,
  24. 'doublequote' => 0
  25. ];
  26. /** @var bool should blocks be rewritten? FIXME seems to always be true */
  27. protected $rewriteBlocks = true;
  28. /**
  29. * @var bool are we in a footnote already?
  30. */
  31. protected $footnote;
  32. /**
  33. * Doku_Handler constructor.
  34. */
  35. public function __construct()
  36. {
  37. $this->callWriter = new CallWriter($this);
  38. }
  39. /**
  40. * Add a new call by passing it to the current CallWriter
  41. *
  42. * @param string $handler handler method name (see mode handlers below)
  43. * @param mixed $args arguments for this call
  44. * @param int $pos byte position in the original source file
  45. */
  46. public function addCall($handler, $args, $pos)
  47. {
  48. $call = [$handler, $args, $pos];
  49. $this->callWriter->writeCall($call);
  50. }
  51. /**
  52. * Accessor for the current CallWriter
  53. *
  54. * @return CallWriterInterface
  55. */
  56. public function getCallWriter()
  57. {
  58. return $this->callWriter;
  59. }
  60. /**
  61. * Set a new CallWriter
  62. *
  63. * @param CallWriterInterface $callWriter
  64. */
  65. public function setCallWriter($callWriter)
  66. {
  67. $this->callWriter = $callWriter;
  68. }
  69. /**
  70. * Return the current internal status of the given name
  71. *
  72. * @param string $status
  73. * @return mixed|null
  74. */
  75. public function getStatus($status)
  76. {
  77. if (!isset($this->status[$status])) return null;
  78. return $this->status[$status];
  79. }
  80. /**
  81. * Set a new internal status
  82. *
  83. * @param string $status
  84. * @param mixed $value
  85. */
  86. public function setStatus($status, $value)
  87. {
  88. $this->status[$status] = $value;
  89. }
  90. /** @deprecated 2019-10-31 use addCall() instead */
  91. public function _addCall($handler, $args, $pos)
  92. {
  93. dbg_deprecated('addCall');
  94. $this->addCall($handler, $args, $pos);
  95. }
  96. /**
  97. * Similar to addCall, but adds a plugin call
  98. *
  99. * @param string $plugin name of the plugin
  100. * @param mixed $args arguments for this call
  101. * @param int $state a LEXER_STATE_* constant
  102. * @param int $pos byte position in the original source file
  103. * @param string $match matched syntax
  104. */
  105. public function addPluginCall($plugin, $args, $state, $pos, $match)
  106. {
  107. $call = ['plugin', [$plugin, $args, $state, $match], $pos];
  108. $this->callWriter->writeCall($call);
  109. }
  110. /**
  111. * Finishes handling
  112. *
  113. * Called from the parser. Calls finalise() on the call writer, closes open
  114. * sections, rewrites blocks and adds document_start and document_end calls.
  115. *
  116. * @triggers PARSER_HANDLER_DONE
  117. */
  118. public function finalize()
  119. {
  120. $this->callWriter->finalise();
  121. if ($this->status['section']) {
  122. $last_call = end($this->calls);
  123. $this->calls[] = ['section_close', [], $last_call[2]];
  124. }
  125. if ($this->rewriteBlocks) {
  126. $B = new Block();
  127. $this->calls = $B->process($this->calls);
  128. }
  129. Event::createAndTrigger('PARSER_HANDLER_DONE', $this);
  130. array_unshift($this->calls, ['document_start', [], 0]);
  131. $last_call = end($this->calls);
  132. $this->calls[] = ['document_end', [], $last_call[2]];
  133. }
  134. /**
  135. * fetch the current call and advance the pointer to the next one
  136. *
  137. * @fixme seems to be unused?
  138. * @return bool|mixed
  139. */
  140. public function fetch()
  141. {
  142. $call = current($this->calls);
  143. if ($call !== false) {
  144. next($this->calls); //advance the pointer
  145. return $call;
  146. }
  147. return false;
  148. }
  149. /**
  150. * Internal function for parsing highlight options.
  151. * $options is parsed for key value pairs separated by commas.
  152. * A value might also be missing in which case the value will simple
  153. * be set to true. Commas in strings are ignored, e.g. option="4,56"
  154. * will work as expected and will only create one entry.
  155. *
  156. * @param string $options space separated list of key-value pairs,
  157. * e.g. option1=123, option2="456"
  158. * @return array|null Array of key-value pairs $array['key'] = 'value';
  159. * or null if no entries found
  160. */
  161. protected function parse_highlight_options($options)
  162. {
  163. $result = [];
  164. preg_match_all('/(\w+(?:="[^"]*"))|(\w+(?:=[^\s]*))|(\w+[^=\s\]])(?:\s*)/', $options, $matches, PREG_SET_ORDER);
  165. foreach ($matches as $match) {
  166. $equal_sign = strpos($match [0], '=');
  167. if ($equal_sign === false) {
  168. $key = trim($match[0]);
  169. $result [$key] = 1;
  170. } else {
  171. $key = substr($match[0], 0, $equal_sign);
  172. $value = substr($match[0], $equal_sign + 1);
  173. $value = trim($value, '"');
  174. if (strlen($value) > 0) {
  175. $result [$key] = $value;
  176. } else {
  177. $result [$key] = 1;
  178. }
  179. }
  180. }
  181. // Check for supported options
  182. $result = array_intersect_key(
  183. $result,
  184. array_flip([
  185. 'enable_line_numbers',
  186. 'start_line_numbers_at',
  187. 'highlight_lines_extra',
  188. 'enable_keyword_links'
  189. ])
  190. );
  191. // Sanitize values
  192. if (isset($result['enable_line_numbers'])) {
  193. if ($result['enable_line_numbers'] === 'false') {
  194. $result['enable_line_numbers'] = false;
  195. }
  196. $result['enable_line_numbers'] = (bool)$result['enable_line_numbers'];
  197. }
  198. if (isset($result['highlight_lines_extra'])) {
  199. $result['highlight_lines_extra'] = array_map('intval', explode(',', $result['highlight_lines_extra']));
  200. $result['highlight_lines_extra'] = array_filter($result['highlight_lines_extra']);
  201. $result['highlight_lines_extra'] = array_unique($result['highlight_lines_extra']);
  202. }
  203. if (isset($result['start_line_numbers_at'])) {
  204. $result['start_line_numbers_at'] = (int)$result['start_line_numbers_at'];
  205. }
  206. if (isset($result['enable_keyword_links'])) {
  207. if ($result['enable_keyword_links'] === 'false') {
  208. $result['enable_keyword_links'] = false;
  209. }
  210. $result['enable_keyword_links'] = (bool)$result['enable_keyword_links'];
  211. }
  212. if (count($result) == 0) {
  213. return null;
  214. }
  215. return $result;
  216. }
  217. /**
  218. * Simplifies handling for the formatting tags which all behave the same
  219. *
  220. * @param string $match matched syntax
  221. * @param int $state a LEXER_STATE_* constant
  222. * @param int $pos byte position in the original source file
  223. * @param string $name actual mode name
  224. */
  225. protected function nestingTag($match, $state, $pos, $name)
  226. {
  227. switch ($state) {
  228. case DOKU_LEXER_ENTER:
  229. $this->addCall($name . '_open', [], $pos);
  230. break;
  231. case DOKU_LEXER_EXIT:
  232. $this->addCall($name . '_close', [], $pos);
  233. break;
  234. case DOKU_LEXER_UNMATCHED:
  235. $this->addCall('cdata', [$match], $pos);
  236. break;
  237. }
  238. }
  239. /**
  240. * The following methods define the handlers for the different Syntax modes
  241. *
  242. * The handlers are called from dokuwiki\Parsing\Lexer\Lexer\invokeParser()
  243. *
  244. * @todo it might make sense to move these into their own class or merge them with the
  245. * ParserMode classes some time.
  246. */
  247. // region mode handlers
  248. /**
  249. * Special plugin handler
  250. *
  251. * This handler is called for all modes starting with 'plugin_'.
  252. * An additional parameter with the plugin name is passed. The plugin's handle()
  253. * method is called here
  254. *
  255. * @param string $match matched syntax
  256. * @param int $state a LEXER_STATE_* constant
  257. * @param int $pos byte position in the original source file
  258. * @param string $pluginname name of the plugin
  259. * @return bool mode handled?
  260. * @author Andreas Gohr <andi@splitbrain.org>
  261. *
  262. */
  263. public function plugin($match, $state, $pos, $pluginname)
  264. {
  265. $data = [$match];
  266. /** @var SyntaxPlugin $plugin */
  267. $plugin = plugin_load('syntax', $pluginname);
  268. if ($plugin != null) {
  269. $data = $plugin->handle($match, $state, $pos, $this);
  270. }
  271. if ($data !== false) {
  272. $this->addPluginCall($pluginname, $data, $state, $pos, $match);
  273. }
  274. return true;
  275. }
  276. /**
  277. * @param string $match matched syntax
  278. * @param int $state a LEXER_STATE_* constant
  279. * @param int $pos byte position in the original source file
  280. * @return bool mode handled?
  281. */
  282. public function base($match, $state, $pos)
  283. {
  284. if ($state === DOKU_LEXER_UNMATCHED) {
  285. $this->addCall('cdata', [$match], $pos);
  286. return true;
  287. }
  288. return false;
  289. }
  290. /**
  291. * @param string $match matched syntax
  292. * @param int $state a LEXER_STATE_* constant
  293. * @param int $pos byte position in the original source file
  294. * @return bool mode handled?
  295. */
  296. public function header($match, $state, $pos)
  297. {
  298. // get level and title
  299. $title = trim($match);
  300. $level = 7 - strspn($title, '=');
  301. if ($level < 1) $level = 1;
  302. $title = trim($title, '=');
  303. $title = trim($title);
  304. if ($this->status['section']) $this->addCall('section_close', [], $pos);
  305. $this->addCall('header', [$title, $level, $pos], $pos);
  306. $this->addCall('section_open', [$level], $pos);
  307. $this->status['section'] = true;
  308. return true;
  309. }
  310. /**
  311. * @param string $match matched syntax
  312. * @param int $state a LEXER_STATE_* constant
  313. * @param int $pos byte position in the original source file
  314. * @return bool mode handled?
  315. */
  316. public function notoc($match, $state, $pos)
  317. {
  318. $this->addCall('notoc', [], $pos);
  319. return true;
  320. }
  321. /**
  322. * @param string $match matched syntax
  323. * @param int $state a LEXER_STATE_* constant
  324. * @param int $pos byte position in the original source file
  325. * @return bool mode handled?
  326. */
  327. public function nocache($match, $state, $pos)
  328. {
  329. $this->addCall('nocache', [], $pos);
  330. return true;
  331. }
  332. /**
  333. * @param string $match matched syntax
  334. * @param int $state a LEXER_STATE_* constant
  335. * @param int $pos byte position in the original source file
  336. * @return bool mode handled?
  337. */
  338. public function linebreak($match, $state, $pos)
  339. {
  340. $this->addCall('linebreak', [], $pos);
  341. return true;
  342. }
  343. /**
  344. * @param string $match matched syntax
  345. * @param int $state a LEXER_STATE_* constant
  346. * @param int $pos byte position in the original source file
  347. * @return bool mode handled?
  348. */
  349. public function eol($match, $state, $pos)
  350. {
  351. $this->addCall('eol', [], $pos);
  352. return true;
  353. }
  354. /**
  355. * @param string $match matched syntax
  356. * @param int $state a LEXER_STATE_* constant
  357. * @param int $pos byte position in the original source file
  358. * @return bool mode handled?
  359. */
  360. public function hr($match, $state, $pos)
  361. {
  362. $this->addCall('hr', [], $pos);
  363. return true;
  364. }
  365. /**
  366. * @param string $match matched syntax
  367. * @param int $state a LEXER_STATE_* constant
  368. * @param int $pos byte position in the original source file
  369. * @return bool mode handled?
  370. */
  371. public function strong($match, $state, $pos)
  372. {
  373. $this->nestingTag($match, $state, $pos, 'strong');
  374. return true;
  375. }
  376. /**
  377. * @param string $match matched syntax
  378. * @param int $state a LEXER_STATE_* constant
  379. * @param int $pos byte position in the original source file
  380. * @return bool mode handled?
  381. */
  382. public function emphasis($match, $state, $pos)
  383. {
  384. $this->nestingTag($match, $state, $pos, 'emphasis');
  385. return true;
  386. }
  387. /**
  388. * @param string $match matched syntax
  389. * @param int $state a LEXER_STATE_* constant
  390. * @param int $pos byte position in the original source file
  391. * @return bool mode handled?
  392. */
  393. public function underline($match, $state, $pos)
  394. {
  395. $this->nestingTag($match, $state, $pos, 'underline');
  396. return true;
  397. }
  398. /**
  399. * @param string $match matched syntax
  400. * @param int $state a LEXER_STATE_* constant
  401. * @param int $pos byte position in the original source file
  402. * @return bool mode handled?
  403. */
  404. public function monospace($match, $state, $pos)
  405. {
  406. $this->nestingTag($match, $state, $pos, 'monospace');
  407. return true;
  408. }
  409. /**
  410. * @param string $match matched syntax
  411. * @param int $state a LEXER_STATE_* constant
  412. * @param int $pos byte position in the original source file
  413. * @return bool mode handled?
  414. */
  415. public function subscript($match, $state, $pos)
  416. {
  417. $this->nestingTag($match, $state, $pos, 'subscript');
  418. return true;
  419. }
  420. /**
  421. * @param string $match matched syntax
  422. * @param int $state a LEXER_STATE_* constant
  423. * @param int $pos byte position in the original source file
  424. * @return bool mode handled?
  425. */
  426. public function superscript($match, $state, $pos)
  427. {
  428. $this->nestingTag($match, $state, $pos, 'superscript');
  429. return true;
  430. }
  431. /**
  432. * @param string $match matched syntax
  433. * @param int $state a LEXER_STATE_* constant
  434. * @param int $pos byte position in the original source file
  435. * @return bool mode handled?
  436. */
  437. public function deleted($match, $state, $pos)
  438. {
  439. $this->nestingTag($match, $state, $pos, 'deleted');
  440. return true;
  441. }
  442. /**
  443. * @param string $match matched syntax
  444. * @param int $state a LEXER_STATE_* constant
  445. * @param int $pos byte position in the original source file
  446. * @return bool mode handled?
  447. */
  448. public function footnote($match, $state, $pos)
  449. {
  450. if (!isset($this->footnote)) $this->footnote = false;
  451. switch ($state) {
  452. case DOKU_LEXER_ENTER:
  453. // footnotes can not be nested - however due to limitations in lexer it can't be prevented
  454. // we will still enter a new footnote mode, we just do nothing
  455. if ($this->footnote) {
  456. $this->addCall('cdata', [$match], $pos);
  457. break;
  458. }
  459. $this->footnote = true;
  460. $this->callWriter = new Nest($this->callWriter, 'footnote_close');
  461. $this->addCall('footnote_open', [], $pos);
  462. break;
  463. case DOKU_LEXER_EXIT:
  464. // check whether we have already exitted the footnote mode, can happen if the modes were nested
  465. if (!$this->footnote) {
  466. $this->addCall('cdata', [$match], $pos);
  467. break;
  468. }
  469. $this->footnote = false;
  470. $this->addCall('footnote_close', [], $pos);
  471. /** @var Nest $reWriter */
  472. $reWriter = $this->callWriter;
  473. $this->callWriter = $reWriter->process();
  474. break;
  475. case DOKU_LEXER_UNMATCHED:
  476. $this->addCall('cdata', [$match], $pos);
  477. break;
  478. }
  479. return true;
  480. }
  481. /**
  482. * @param string $match matched syntax
  483. * @param int $state a LEXER_STATE_* constant
  484. * @param int $pos byte position in the original source file
  485. * @return bool mode handled?
  486. */
  487. public function listblock($match, $state, $pos)
  488. {
  489. switch ($state) {
  490. case DOKU_LEXER_ENTER:
  491. $this->callWriter = new Lists($this->callWriter);
  492. $this->addCall('list_open', [$match], $pos);
  493. break;
  494. case DOKU_LEXER_EXIT:
  495. $this->addCall('list_close', [], $pos);
  496. /** @var Lists $reWriter */
  497. $reWriter = $this->callWriter;
  498. $this->callWriter = $reWriter->process();
  499. break;
  500. case DOKU_LEXER_MATCHED:
  501. $this->addCall('list_item', [$match], $pos);
  502. break;
  503. case DOKU_LEXER_UNMATCHED:
  504. $this->addCall('cdata', [$match], $pos);
  505. break;
  506. }
  507. return true;
  508. }
  509. /**
  510. * @param string $match matched syntax
  511. * @param int $state a LEXER_STATE_* constant
  512. * @param int $pos byte position in the original source file
  513. * @return bool mode handled?
  514. */
  515. public function unformatted($match, $state, $pos)
  516. {
  517. if ($state == DOKU_LEXER_UNMATCHED) {
  518. $this->addCall('unformatted', [$match], $pos);
  519. }
  520. return true;
  521. }
  522. /**
  523. * @param string $match matched syntax
  524. * @param int $state a LEXER_STATE_* constant
  525. * @param int $pos byte position in the original source file
  526. * @return bool mode handled?
  527. */
  528. public function preformatted($match, $state, $pos)
  529. {
  530. switch ($state) {
  531. case DOKU_LEXER_ENTER:
  532. $this->callWriter = new Preformatted($this->callWriter);
  533. $this->addCall('preformatted_start', [], $pos);
  534. break;
  535. case DOKU_LEXER_EXIT:
  536. $this->addCall('preformatted_end', [], $pos);
  537. /** @var Preformatted $reWriter */
  538. $reWriter = $this->callWriter;
  539. $this->callWriter = $reWriter->process();
  540. break;
  541. case DOKU_LEXER_MATCHED:
  542. $this->addCall('preformatted_newline', [], $pos);
  543. break;
  544. case DOKU_LEXER_UNMATCHED:
  545. $this->addCall('preformatted_content', [$match], $pos);
  546. break;
  547. }
  548. return true;
  549. }
  550. /**
  551. * @param string $match matched syntax
  552. * @param int $state a LEXER_STATE_* constant
  553. * @param int $pos byte position in the original source file
  554. * @return bool mode handled?
  555. */
  556. public function quote($match, $state, $pos)
  557. {
  558. switch ($state) {
  559. case DOKU_LEXER_ENTER:
  560. $this->callWriter = new Quote($this->callWriter);
  561. $this->addCall('quote_start', [$match], $pos);
  562. break;
  563. case DOKU_LEXER_EXIT:
  564. $this->addCall('quote_end', [], $pos);
  565. /** @var Lists $reWriter */
  566. $reWriter = $this->callWriter;
  567. $this->callWriter = $reWriter->process();
  568. break;
  569. case DOKU_LEXER_MATCHED:
  570. $this->addCall('quote_newline', [$match], $pos);
  571. break;
  572. case DOKU_LEXER_UNMATCHED:
  573. $this->addCall('cdata', [$match], $pos);
  574. break;
  575. }
  576. return true;
  577. }
  578. /**
  579. * @param string $match matched syntax
  580. * @param int $state a LEXER_STATE_* constant
  581. * @param int $pos byte position in the original source file
  582. * @return bool mode handled?
  583. */
  584. public function file($match, $state, $pos)
  585. {
  586. return $this->code($match, $state, $pos, 'file');
  587. }
  588. /**
  589. * @param string $match matched syntax
  590. * @param int $state a LEXER_STATE_* constant
  591. * @param int $pos byte position in the original source file
  592. * @param string $type either 'code' or 'file'
  593. * @return bool mode handled?
  594. */
  595. public function code($match, $state, $pos, $type = 'code')
  596. {
  597. if ($state == DOKU_LEXER_UNMATCHED) {
  598. $matches = sexplode('>', $match, 2, '');
  599. // Cut out variable options enclosed in []
  600. preg_match('/\[.*\]/', $matches[0], $options);
  601. if (!empty($options[0])) {
  602. $matches[0] = str_replace($options[0], '', $matches[0]);
  603. }
  604. $param = preg_split('/\s+/', $matches[0], 2, PREG_SPLIT_NO_EMPTY);
  605. while (count($param) < 2) $param[] = null;
  606. // We shortcut html here.
  607. if ($param[0] == 'html') $param[0] = 'html4strict';
  608. if ($param[0] == '-') $param[0] = null;
  609. array_unshift($param, $matches[1]);
  610. if (!empty($options[0])) {
  611. $param [] = $this->parse_highlight_options($options[0]);
  612. }
  613. $this->addCall($type, $param, $pos);
  614. }
  615. return true;
  616. }
  617. /**
  618. * @param string $match matched syntax
  619. * @param int $state a LEXER_STATE_* constant
  620. * @param int $pos byte position in the original source file
  621. * @return bool mode handled?
  622. */
  623. public function acronym($match, $state, $pos)
  624. {
  625. $this->addCall('acronym', [$match], $pos);
  626. return true;
  627. }
  628. /**
  629. * @param string $match matched syntax
  630. * @param int $state a LEXER_STATE_* constant
  631. * @param int $pos byte position in the original source file
  632. * @return bool mode handled?
  633. */
  634. public function smiley($match, $state, $pos)
  635. {
  636. $this->addCall('smiley', [$match], $pos);
  637. return true;
  638. }
  639. /**
  640. * @param string $match matched syntax
  641. * @param int $state a LEXER_STATE_* constant
  642. * @param int $pos byte position in the original source file
  643. * @return bool mode handled?
  644. */
  645. public function wordblock($match, $state, $pos)
  646. {
  647. $this->addCall('wordblock', [$match], $pos);
  648. return true;
  649. }
  650. /**
  651. * @param string $match matched syntax
  652. * @param int $state a LEXER_STATE_* constant
  653. * @param int $pos byte position in the original source file
  654. * @return bool mode handled?
  655. */
  656. public function entity($match, $state, $pos)
  657. {
  658. $this->addCall('entity', [$match], $pos);
  659. return true;
  660. }
  661. /**
  662. * @param string $match matched syntax
  663. * @param int $state a LEXER_STATE_* constant
  664. * @param int $pos byte position in the original source file
  665. * @return bool mode handled?
  666. */
  667. public function multiplyentity($match, $state, $pos)
  668. {
  669. preg_match_all('/\d+/', $match, $matches);
  670. $this->addCall('multiplyentity', [$matches[0][0], $matches[0][1]], $pos);
  671. return true;
  672. }
  673. /**
  674. * @param string $match matched syntax
  675. * @param int $state a LEXER_STATE_* constant
  676. * @param int $pos byte position in the original source file
  677. * @return bool mode handled?
  678. */
  679. public function singlequoteopening($match, $state, $pos)
  680. {
  681. $this->addCall('singlequoteopening', [], $pos);
  682. return true;
  683. }
  684. /**
  685. * @param string $match matched syntax
  686. * @param int $state a LEXER_STATE_* constant
  687. * @param int $pos byte position in the original source file
  688. * @return bool mode handled?
  689. */
  690. public function singlequoteclosing($match, $state, $pos)
  691. {
  692. $this->addCall('singlequoteclosing', [], $pos);
  693. return true;
  694. }
  695. /**
  696. * @param string $match matched syntax
  697. * @param int $state a LEXER_STATE_* constant
  698. * @param int $pos byte position in the original source file
  699. * @return bool mode handled?
  700. */
  701. public function apostrophe($match, $state, $pos)
  702. {
  703. $this->addCall('apostrophe', [], $pos);
  704. return true;
  705. }
  706. /**
  707. * @param string $match matched syntax
  708. * @param int $state a LEXER_STATE_* constant
  709. * @param int $pos byte position in the original source file
  710. * @return bool mode handled?
  711. */
  712. public function doublequoteopening($match, $state, $pos)
  713. {
  714. $this->addCall('doublequoteopening', [], $pos);
  715. $this->status['doublequote']++;
  716. return true;
  717. }
  718. /**
  719. * @param string $match matched syntax
  720. * @param int $state a LEXER_STATE_* constant
  721. * @param int $pos byte position in the original source file
  722. * @return bool mode handled?
  723. */
  724. public function doublequoteclosing($match, $state, $pos)
  725. {
  726. if ($this->status['doublequote'] <= 0) {
  727. $this->doublequoteopening($match, $state, $pos);
  728. } else {
  729. $this->addCall('doublequoteclosing', [], $pos);
  730. $this->status['doublequote'] = max(0, --$this->status['doublequote']);
  731. }
  732. return true;
  733. }
  734. /**
  735. * @param string $match matched syntax
  736. * @param int $state a LEXER_STATE_* constant
  737. * @param int $pos byte position in the original source file
  738. * @return bool mode handled?
  739. */
  740. public function camelcaselink($match, $state, $pos)
  741. {
  742. $this->addCall('camelcaselink', [$match], $pos);
  743. return true;
  744. }
  745. /**
  746. * @param string $match matched syntax
  747. * @param int $state a LEXER_STATE_* constant
  748. * @param int $pos byte position in the original source file
  749. * @return bool mode handled?
  750. */
  751. public function internallink($match, $state, $pos)
  752. {
  753. // Strip the opening and closing markup
  754. $link = preg_replace(['/^\[\[/', '/\]\]$/u'], '', $match);
  755. // Split title from URL
  756. $link = sexplode('|', $link, 2);
  757. if ($link[1] === null) {
  758. $link[1] = null;
  759. } elseif (preg_match('/^\{\{[^\}]+\}\}$/', $link[1])) {
  760. // If the title is an image, convert it to an array containing the image details
  761. $link[1] = Doku_Handler_Parse_Media($link[1]);
  762. }
  763. $link[0] = trim($link[0]);
  764. //decide which kind of link it is
  765. if (link_isinterwiki($link[0])) {
  766. // Interwiki
  767. $interwiki = sexplode('>', $link[0], 2, '');
  768. $this->addCall(
  769. 'interwikilink',
  770. [$link[0], $link[1], strtolower($interwiki[0]), $interwiki[1]],
  771. $pos
  772. );
  773. } elseif (preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u', $link[0])) {
  774. // Windows Share
  775. $this->addCall(
  776. 'windowssharelink',
  777. [$link[0], $link[1]],
  778. $pos
  779. );
  780. } elseif (preg_match('#^([a-z0-9\-\.+]+?)://#i', $link[0])) {
  781. // external link (accepts all protocols)
  782. $this->addCall(
  783. 'externallink',
  784. [$link[0], $link[1]],
  785. $pos
  786. );
  787. } elseif (preg_match('<' . PREG_PATTERN_VALID_EMAIL . '>', $link[0])) {
  788. // E-Mail (pattern above is defined in inc/mail.php)
  789. $this->addCall(
  790. 'emaillink',
  791. [$link[0], $link[1]],
  792. $pos
  793. );
  794. } elseif (preg_match('!^#.+!', $link[0])) {
  795. // local link
  796. $this->addCall(
  797. 'locallink',
  798. [substr($link[0], 1), $link[1]],
  799. $pos
  800. );
  801. } else {
  802. // internal link
  803. $this->addCall(
  804. 'internallink',
  805. [$link[0], $link[1]],
  806. $pos
  807. );
  808. }
  809. return true;
  810. }
  811. /**
  812. * @param string $match matched syntax
  813. * @param int $state a LEXER_STATE_* constant
  814. * @param int $pos byte position in the original source file
  815. * @return bool mode handled?
  816. */
  817. public function filelink($match, $state, $pos)
  818. {
  819. $this->addCall('filelink', [$match, null], $pos);
  820. return true;
  821. }
  822. /**
  823. * @param string $match matched syntax
  824. * @param int $state a LEXER_STATE_* constant
  825. * @param int $pos byte position in the original source file
  826. * @return bool mode handled?
  827. */
  828. public function windowssharelink($match, $state, $pos)
  829. {
  830. $this->addCall('windowssharelink', [$match, null], $pos);
  831. return true;
  832. }
  833. /**
  834. * @param string $match matched syntax
  835. * @param int $state a LEXER_STATE_* constant
  836. * @param int $pos byte position in the original source file
  837. * @return bool mode handled?
  838. */
  839. public function media($match, $state, $pos)
  840. {
  841. $p = Doku_Handler_Parse_Media($match);
  842. $this->addCall(
  843. $p['type'],
  844. [$p['src'], $p['title'], $p['align'], $p['width'], $p['height'], $p['cache'], $p['linking']],
  845. $pos
  846. );
  847. return true;
  848. }
  849. /**
  850. * @param string $match matched syntax
  851. * @param int $state a LEXER_STATE_* constant
  852. * @param int $pos byte position in the original source file
  853. * @return bool mode handled?
  854. */
  855. public function rss($match, $state, $pos)
  856. {
  857. $link = preg_replace(['/^\{\{rss>/', '/\}\}$/'], '', $match);
  858. // get params
  859. [$link, $params] = sexplode(' ', $link, 2, '');
  860. $p = [];
  861. if (preg_match('/\b(\d+)\b/', $params, $match)) {
  862. $p['max'] = $match[1];
  863. } else {
  864. $p['max'] = 8;
  865. }
  866. $p['reverse'] = (preg_match('/rev/', $params));
  867. $p['author'] = (preg_match('/\b(by|author)/', $params));
  868. $p['date'] = (preg_match('/\b(date)/', $params));
  869. $p['details'] = (preg_match('/\b(desc|detail)/', $params));
  870. $p['nosort'] = (preg_match('/\b(nosort)\b/', $params));
  871. if (preg_match('/\b(\d+)([dhm])\b/', $params, $match)) {
  872. $period = ['d' => 86400, 'h' => 3600, 'm' => 60];
  873. $p['refresh'] = max(600, $match[1] * $period[$match[2]]); // n * period in seconds, minimum 10 minutes
  874. } else {
  875. $p['refresh'] = 14400; // default to 4 hours
  876. }
  877. $this->addCall('rss', [$link, $p], $pos);
  878. return true;
  879. }
  880. /**
  881. * @param string $match matched syntax
  882. * @param int $state a LEXER_STATE_* constant
  883. * @param int $pos byte position in the original source file
  884. * @return bool mode handled?
  885. */
  886. public function externallink($match, $state, $pos)
  887. {
  888. $url = $match;
  889. $title = null;
  890. // add protocol on simple short URLs
  891. if (str_starts_with($url, 'ftp') && !str_starts_with($url, 'ftp://')) {
  892. $title = $url;
  893. $url = 'ftp://' . $url;
  894. }
  895. if (str_starts_with($url, 'www')) {
  896. $title = $url;
  897. $url = 'http://' . $url;
  898. }
  899. $this->addCall('externallink', [$url, $title], $pos);
  900. return true;
  901. }
  902. /**
  903. * @param string $match matched syntax
  904. * @param int $state a LEXER_STATE_* constant
  905. * @param int $pos byte position in the original source file
  906. * @return bool mode handled?
  907. */
  908. public function emaillink($match, $state, $pos)
  909. {
  910. $email = preg_replace(['/^</', '/>$/'], '', $match);
  911. $this->addCall('emaillink', [$email, null], $pos);
  912. return true;
  913. }
  914. /**
  915. * @param string $match matched syntax
  916. * @param int $state a LEXER_STATE_* constant
  917. * @param int $pos byte position in the original source file
  918. * @return bool mode handled?
  919. */
  920. public function table($match, $state, $pos)
  921. {
  922. switch ($state) {
  923. case DOKU_LEXER_ENTER:
  924. $this->callWriter = new Table($this->callWriter);
  925. $this->addCall('table_start', [$pos + 1], $pos);
  926. if (trim($match) == '^') {
  927. $this->addCall('tableheader', [], $pos);
  928. } else {
  929. $this->addCall('tablecell', [], $pos);
  930. }
  931. break;
  932. case DOKU_LEXER_EXIT:
  933. $this->addCall('table_end', [$pos], $pos);
  934. /** @var Table $reWriter */
  935. $reWriter = $this->callWriter;
  936. $this->callWriter = $reWriter->process();
  937. break;
  938. case DOKU_LEXER_UNMATCHED:
  939. if (trim($match) != '') {
  940. $this->addCall('cdata', [$match], $pos);
  941. }
  942. break;
  943. case DOKU_LEXER_MATCHED:
  944. if ($match == ' ') {
  945. $this->addCall('cdata', [$match], $pos);
  946. } elseif (preg_match('/:::/', $match)) {
  947. $this->addCall('rowspan', [$match], $pos);
  948. } elseif (preg_match('/\t+/', $match)) {
  949. $this->addCall('table_align', [$match], $pos);
  950. } elseif (preg_match('/ {2,}/', $match)) {
  951. $this->addCall('table_align', [$match], $pos);
  952. } elseif ($match == "\n|") {
  953. $this->addCall('table_row', [], $pos);
  954. $this->addCall('tablecell', [], $pos);
  955. } elseif ($match == "\n^") {
  956. $this->addCall('table_row', [], $pos);
  957. $this->addCall('tableheader', [], $pos);
  958. } elseif ($match == '|') {
  959. $this->addCall('tablecell', [], $pos);
  960. } elseif ($match == '^') {
  961. $this->addCall('tableheader', [], $pos);
  962. }
  963. break;
  964. }
  965. return true;
  966. }
  967. // endregion modes
  968. }
  969. //------------------------------------------------------------------------
  970. function Doku_Handler_Parse_Media($match)
  971. {
  972. // Strip the opening and closing markup
  973. $link = preg_replace(['/^\{\{/', '/\}\}$/u'], '', $match);
  974. // Split title from URL
  975. $link = sexplode('|', $link, 2);
  976. // Check alignment
  977. $ralign = (bool)preg_match('/^ /', $link[0]);
  978. $lalign = (bool)preg_match('/ $/', $link[0]);
  979. // Logic = what's that ;)...
  980. if ($lalign & $ralign) {
  981. $align = 'center';
  982. } elseif ($ralign) {
  983. $align = 'right';
  984. } elseif ($lalign) {
  985. $align = 'left';
  986. } else {
  987. $align = null;
  988. }
  989. // The title...
  990. if (!isset($link[1])) {
  991. $link[1] = null;
  992. }
  993. //remove aligning spaces
  994. $link[0] = trim($link[0]);
  995. //split into src and parameters (using the very last questionmark)
  996. $pos = strrpos($link[0], '?');
  997. if ($pos !== false) {
  998. $src = substr($link[0], 0, $pos);
  999. $param = substr($link[0], $pos + 1);
  1000. } else {
  1001. $src = $link[0];
  1002. $param = '';
  1003. }
  1004. //parse width and height
  1005. if (preg_match('#(\d+)(x(\d+))?#i', $param, $size)) {
  1006. $w = empty($size[1]) ? null : $size[1];
  1007. $h = empty($size[3]) ? null : $size[3];
  1008. } else {
  1009. $w = null;
  1010. $h = null;
  1011. }
  1012. //get linking command
  1013. if (preg_match('/nolink/i', $param)) {
  1014. $linking = 'nolink';
  1015. } elseif (preg_match('/direct/i', $param)) {
  1016. $linking = 'direct';
  1017. } elseif (preg_match('/linkonly/i', $param)) {
  1018. $linking = 'linkonly';
  1019. } else {
  1020. $linking = 'details';
  1021. }
  1022. //get caching command
  1023. if (preg_match('/(nocache|recache)/i', $param, $cachemode)) {
  1024. $cache = $cachemode[1];
  1025. } else {
  1026. $cache = 'cache';
  1027. }
  1028. // Check whether this is a local or remote image or interwiki
  1029. if (media_isexternal($src) || link_isinterwiki($src)) {
  1030. $call = 'externalmedia';
  1031. } else {
  1032. $call = 'internalmedia';
  1033. }
  1034. $params = [
  1035. 'type' => $call,
  1036. 'src' => $src,
  1037. 'title' => $link[1],
  1038. 'align' => $align,
  1039. 'width' => $w,
  1040. 'height' => $h,
  1041. 'cache' => $cache,
  1042. 'linking' => $linking
  1043. ];
  1044. return $params;
  1045. }