handler.php 36 KB

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