MakeWellFormed.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. <?php
  2. /**
  3. * Takes tokens makes them well-formed (balance end tags, etc.)
  4. *
  5. * Specification of the armor attributes this strategy uses:
  6. *
  7. * - MakeWellFormed_TagClosedError: This armor field is used to
  8. * suppress tag closed errors for certain tokens [TagClosedSuppress],
  9. * in particular, if a tag was generated automatically by HTML
  10. * Purifier, we may rely on our infrastructure to close it for us
  11. * and shouldn't report an error to the user [TagClosedAuto].
  12. */
  13. class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
  14. {
  15. /**
  16. * Array stream of tokens being processed.
  17. * @type HTMLPurifier_Token[]
  18. */
  19. protected $tokens;
  20. /**
  21. * Current token.
  22. * @type HTMLPurifier_Token
  23. */
  24. protected $token;
  25. /**
  26. * Zipper managing the true state.
  27. * @type HTMLPurifier_Zipper
  28. */
  29. protected $zipper;
  30. /**
  31. * Current nesting of elements.
  32. * @type array
  33. */
  34. protected $stack;
  35. /**
  36. * Injectors active in this stream processing.
  37. * @type HTMLPurifier_Injector[]
  38. */
  39. protected $injectors;
  40. /**
  41. * Current instance of HTMLPurifier_Config.
  42. * @type HTMLPurifier_Config
  43. */
  44. protected $config;
  45. /**
  46. * Current instance of HTMLPurifier_Context.
  47. * @type HTMLPurifier_Context
  48. */
  49. protected $context;
  50. /**
  51. * @param HTMLPurifier_Token[] $tokens
  52. * @param HTMLPurifier_Config $config
  53. * @param HTMLPurifier_Context $context
  54. * @return HTMLPurifier_Token[]
  55. * @throws HTMLPurifier_Exception
  56. */
  57. public function execute($tokens, $config, $context)
  58. {
  59. $definition = $config->getHTMLDefinition();
  60. // local variables
  61. $generator = new HTMLPurifier_Generator($config, $context);
  62. $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
  63. // used for autoclose early abortion
  64. $global_parent_allowed_elements = $definition->info_parent_def->child->getAllowedElements($config);
  65. $e = $context->get('ErrorCollector', true);
  66. $i = false; // injector index
  67. list($zipper, $token) = HTMLPurifier_Zipper::fromArray($tokens);
  68. if ($token === NULL) {
  69. return array();
  70. }
  71. $reprocess = false; // whether or not to reprocess the same token
  72. $stack = array();
  73. // member variables
  74. $this->stack =& $stack;
  75. $this->tokens =& $tokens;
  76. $this->token =& $token;
  77. $this->zipper =& $zipper;
  78. $this->config = $config;
  79. $this->context = $context;
  80. // context variables
  81. $context->register('CurrentNesting', $stack);
  82. $context->register('InputZipper', $zipper);
  83. $context->register('CurrentToken', $token);
  84. // -- begin INJECTOR --
  85. $this->injectors = array();
  86. $injectors = $config->getBatch('AutoFormat');
  87. $def_injectors = $definition->info_injector;
  88. $custom_injectors = $injectors['Custom'];
  89. unset($injectors['Custom']); // special case
  90. foreach ($injectors as $injector => $b) {
  91. // XXX: Fix with a legitimate lookup table of enabled filters
  92. if (strpos($injector, '.') !== false) {
  93. continue;
  94. }
  95. $injector = "HTMLPurifier_Injector_$injector";
  96. if (!$b) {
  97. continue;
  98. }
  99. $this->injectors[] = new $injector;
  100. }
  101. foreach ($def_injectors as $injector) {
  102. // assumed to be objects
  103. $this->injectors[] = $injector;
  104. }
  105. foreach ($custom_injectors as $injector) {
  106. if (!$injector) {
  107. continue;
  108. }
  109. if (is_string($injector)) {
  110. $injector = "HTMLPurifier_Injector_$injector";
  111. $injector = new $injector;
  112. }
  113. $this->injectors[] = $injector;
  114. }
  115. // give the injectors references to the definition and context
  116. // variables for performance reasons
  117. foreach ($this->injectors as $ix => $injector) {
  118. $error = $injector->prepare($config, $context);
  119. if (!$error) {
  120. continue;
  121. }
  122. array_splice($this->injectors, $ix, 1); // rm the injector
  123. trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
  124. }
  125. // -- end INJECTOR --
  126. // a note on reprocessing:
  127. // In order to reduce code duplication, whenever some code needs
  128. // to make HTML changes in order to make things "correct", the
  129. // new HTML gets sent through the purifier, regardless of its
  130. // status. This means that if we add a start token, because it
  131. // was totally necessary, we don't have to update nesting; we just
  132. // punt ($reprocess = true; continue;) and it does that for us.
  133. // isset is in loop because $tokens size changes during loop exec
  134. for (;;
  135. // only increment if we don't need to reprocess
  136. $reprocess ? $reprocess = false : $token = $zipper->next($token)) {
  137. // check for a rewind
  138. if (is_int($i)) {
  139. // possibility: disable rewinding if the current token has a
  140. // rewind set on it already. This would offer protection from
  141. // infinite loop, but might hinder some advanced rewinding.
  142. $rewind_offset = $this->injectors[$i]->getRewindOffset();
  143. if (is_int($rewind_offset)) {
  144. for ($j = 0; $j < $rewind_offset; $j++) {
  145. if (empty($zipper->front)) break;
  146. $token = $zipper->prev($token);
  147. // indicate that other injectors should not process this token,
  148. // but we need to reprocess it. See Note [Injector skips]
  149. unset($token->skip[$i]);
  150. $token->rewind = $i;
  151. if ($token instanceof HTMLPurifier_Token_Start) {
  152. array_pop($this->stack);
  153. } elseif ($token instanceof HTMLPurifier_Token_End) {
  154. $this->stack[] = $token->start;
  155. }
  156. }
  157. }
  158. $i = false;
  159. }
  160. // handle case of document end
  161. if ($token === NULL) {
  162. // kill processing if stack is empty
  163. if (empty($this->stack)) {
  164. break;
  165. }
  166. // peek
  167. $top_nesting = array_pop($this->stack);
  168. $this->stack[] = $top_nesting;
  169. // send error [TagClosedSuppress]
  170. if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) {
  171. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting);
  172. }
  173. // append, don't splice, since this is the end
  174. $token = new HTMLPurifier_Token_End($top_nesting->name);
  175. // punt!
  176. $reprocess = true;
  177. continue;
  178. }
  179. //echo '<br>'; printZipper($zipper, $token);//printTokens($this->stack);
  180. //flush();
  181. // quick-check: if it's not a tag, no need to process
  182. if (empty($token->is_tag)) {
  183. if ($token instanceof HTMLPurifier_Token_Text) {
  184. foreach ($this->injectors as $i => $injector) {
  185. if (isset($token->skip[$i])) {
  186. // See Note [Injector skips]
  187. continue;
  188. }
  189. if ($token->rewind !== null && $token->rewind !== $i) {
  190. continue;
  191. }
  192. // XXX fuckup
  193. $r = $token;
  194. $injector->handleText($r);
  195. $token = $this->processToken($r, $i);
  196. $reprocess = true;
  197. break;
  198. }
  199. }
  200. // another possibility is a comment
  201. continue;
  202. }
  203. if (isset($definition->info[$token->name])) {
  204. $type = $definition->info[$token->name]->child->type;
  205. } else {
  206. $type = false; // Type is unknown, treat accordingly
  207. }
  208. // quick tag checks: anything that's *not* an end tag
  209. $ok = false;
  210. if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
  211. // claims to be a start tag but is empty
  212. $token = new HTMLPurifier_Token_Empty(
  213. $token->name,
  214. $token->attr,
  215. $token->line,
  216. $token->col,
  217. $token->armor
  218. );
  219. $ok = true;
  220. } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
  221. // claims to be empty but really is a start tag
  222. // NB: this assignment is required
  223. $old_token = $token;
  224. $token = new HTMLPurifier_Token_End($token->name);
  225. $token = $this->insertBefore(
  226. new HTMLPurifier_Token_Start($old_token->name, $old_token->attr, $old_token->line, $old_token->col, $old_token->armor)
  227. );
  228. // punt (since we had to modify the input stream in a non-trivial way)
  229. $reprocess = true;
  230. continue;
  231. } elseif ($token instanceof HTMLPurifier_Token_Empty) {
  232. // real empty token
  233. $ok = true;
  234. } elseif ($token instanceof HTMLPurifier_Token_Start) {
  235. // start tag
  236. // ...unless they also have to close their parent
  237. if (!empty($this->stack)) {
  238. // Performance note: you might think that it's rather
  239. // inefficient, recalculating the autoclose information
  240. // for every tag that a token closes (since when we
  241. // do an autoclose, we push a new token into the
  242. // stream and then /process/ that, before
  243. // re-processing this token.) But this is
  244. // necessary, because an injector can make an
  245. // arbitrary transformations to the autoclosing
  246. // tokens we introduce, so things may have changed
  247. // in the meantime. Also, doing the inefficient thing is
  248. // "easy" to reason about (for certain perverse definitions
  249. // of "easy")
  250. $parent = array_pop($this->stack);
  251. $this->stack[] = $parent;
  252. $parent_def = null;
  253. $parent_elements = null;
  254. $autoclose = false;
  255. if (isset($definition->info[$parent->name])) {
  256. $parent_def = $definition->info[$parent->name];
  257. $parent_elements = $parent_def->child->getAllowedElements($config);
  258. $autoclose = !isset($parent_elements[$token->name]);
  259. }
  260. if ($autoclose && $definition->info[$token->name]->wrap) {
  261. // Check if an element can be wrapped by another
  262. // element to make it valid in a context (for
  263. // example, <ul><ul> needs a <li> in between)
  264. $wrapname = $definition->info[$token->name]->wrap;
  265. $wrapdef = $definition->info[$wrapname];
  266. $elements = $wrapdef->child->getAllowedElements($config);
  267. if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) {
  268. $newtoken = new HTMLPurifier_Token_Start($wrapname);
  269. $token = $this->insertBefore($newtoken);
  270. $reprocess = true;
  271. continue;
  272. }
  273. }
  274. $carryover = false;
  275. if ($autoclose && $parent_def->formatting) {
  276. $carryover = true;
  277. }
  278. if ($autoclose) {
  279. // check if this autoclose is doomed to fail
  280. // (this rechecks $parent, which his harmless)
  281. $autoclose_ok = isset($global_parent_allowed_elements[$token->name]);
  282. if (!$autoclose_ok) {
  283. foreach ($this->stack as $ancestor) {
  284. $elements = $definition->info[$ancestor->name]->child->getAllowedElements($config);
  285. if (isset($elements[$token->name])) {
  286. $autoclose_ok = true;
  287. break;
  288. }
  289. if ($definition->info[$token->name]->wrap) {
  290. $wrapname = $definition->info[$token->name]->wrap;
  291. $wrapdef = $definition->info[$wrapname];
  292. $wrap_elements = $wrapdef->child->getAllowedElements($config);
  293. if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) {
  294. $autoclose_ok = true;
  295. break;
  296. }
  297. }
  298. }
  299. }
  300. if ($autoclose_ok) {
  301. // errors need to be updated
  302. $new_token = new HTMLPurifier_Token_End($parent->name);
  303. $new_token->start = $parent;
  304. // [TagClosedSuppress]
  305. if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) {
  306. if (!$carryover) {
  307. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
  308. } else {
  309. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent);
  310. }
  311. }
  312. if ($carryover) {
  313. $element = clone $parent;
  314. // [TagClosedAuto]
  315. $element->armor['MakeWellFormed_TagClosedError'] = true;
  316. $element->carryover = true;
  317. $token = $this->processToken(array($new_token, $token, $element));
  318. } else {
  319. $token = $this->insertBefore($new_token);
  320. }
  321. } else {
  322. $token = $this->remove();
  323. }
  324. $reprocess = true;
  325. continue;
  326. }
  327. }
  328. $ok = true;
  329. }
  330. if ($ok) {
  331. foreach ($this->injectors as $i => $injector) {
  332. if (isset($token->skip[$i])) {
  333. // See Note [Injector skips]
  334. continue;
  335. }
  336. if ($token->rewind !== null && $token->rewind !== $i) {
  337. continue;
  338. }
  339. $r = $token;
  340. $injector->handleElement($r);
  341. $token = $this->processToken($r, $i);
  342. $reprocess = true;
  343. break;
  344. }
  345. if (!$reprocess) {
  346. // ah, nothing interesting happened; do normal processing
  347. if ($token instanceof HTMLPurifier_Token_Start) {
  348. $this->stack[] = $token;
  349. } elseif ($token instanceof HTMLPurifier_Token_End) {
  350. throw new HTMLPurifier_Exception(
  351. 'Improper handling of end tag in start code; possible error in MakeWellFormed'
  352. );
  353. }
  354. }
  355. continue;
  356. }
  357. // sanity check: we should be dealing with a closing tag
  358. if (!$token instanceof HTMLPurifier_Token_End) {
  359. throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier');
  360. }
  361. // make sure that we have something open
  362. if (empty($this->stack)) {
  363. if ($escape_invalid_tags) {
  364. if ($e) {
  365. $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
  366. }
  367. $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token));
  368. } else {
  369. if ($e) {
  370. $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
  371. }
  372. $token = $this->remove();
  373. }
  374. $reprocess = true;
  375. continue;
  376. }
  377. // first, check for the simplest case: everything closes neatly.
  378. // Eventually, everything passes through here; if there are problems
  379. // we modify the input stream accordingly and then punt, so that
  380. // the tokens get processed again.
  381. $current_parent = array_pop($this->stack);
  382. if ($current_parent->name == $token->name) {
  383. $token->start = $current_parent;
  384. foreach ($this->injectors as $i => $injector) {
  385. if (isset($token->skip[$i])) {
  386. // See Note [Injector skips]
  387. continue;
  388. }
  389. if ($token->rewind !== null && $token->rewind !== $i) {
  390. continue;
  391. }
  392. $r = $token;
  393. $injector->handleEnd($r);
  394. $token = $this->processToken($r, $i);
  395. $this->stack[] = $current_parent;
  396. $reprocess = true;
  397. break;
  398. }
  399. continue;
  400. }
  401. // okay, so we're trying to close the wrong tag
  402. // undo the pop previous pop
  403. $this->stack[] = $current_parent;
  404. // scroll back the entire nest, trying to find our tag.
  405. // (feature could be to specify how far you'd like to go)
  406. $size = count($this->stack);
  407. // -2 because -1 is the last element, but we already checked that
  408. $skipped_tags = false;
  409. for ($j = $size - 2; $j >= 0; $j--) {
  410. if ($this->stack[$j]->name == $token->name) {
  411. $skipped_tags = array_slice($this->stack, $j);
  412. break;
  413. }
  414. }
  415. // we didn't find the tag, so remove
  416. if ($skipped_tags === false) {
  417. if ($escape_invalid_tags) {
  418. if ($e) {
  419. $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
  420. }
  421. $token = new HTMLPurifier_Token_Text($generator->generateFromToken($token));
  422. } else {
  423. if ($e) {
  424. $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
  425. }
  426. $token = $this->remove();
  427. }
  428. $reprocess = true;
  429. continue;
  430. }
  431. // do errors, in REVERSE $j order: a,b,c with </a></b></c>
  432. $c = count($skipped_tags);
  433. if ($e) {
  434. for ($j = $c - 1; $j > 0; $j--) {
  435. // notice we exclude $j == 0, i.e. the current ending tag, from
  436. // the errors... [TagClosedSuppress]
  437. if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) {
  438. $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]);
  439. }
  440. }
  441. }
  442. // insert tags, in FORWARD $j order: c,b,a with </a></b></c>
  443. $replace = array($token);
  444. for ($j = 1; $j < $c; $j++) {
  445. // ...as well as from the insertions
  446. $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name);
  447. $new_token->start = $skipped_tags[$j];
  448. array_unshift($replace, $new_token);
  449. if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) {
  450. // [TagClosedAuto]
  451. $element = clone $skipped_tags[$j];
  452. $element->carryover = true;
  453. $element->armor['MakeWellFormed_TagClosedError'] = true;
  454. $replace[] = $element;
  455. }
  456. }
  457. $token = $this->processToken($replace);
  458. $reprocess = true;
  459. continue;
  460. }
  461. $context->destroy('CurrentToken');
  462. $context->destroy('CurrentNesting');
  463. $context->destroy('InputZipper');
  464. unset($this->injectors, $this->stack, $this->tokens);
  465. return $zipper->toArray($token);
  466. }
  467. /**
  468. * Processes arbitrary token values for complicated substitution patterns.
  469. * In general:
  470. *
  471. * If $token is an array, it is a list of tokens to substitute for the
  472. * current token. These tokens then get individually processed. If there
  473. * is a leading integer in the list, that integer determines how many
  474. * tokens from the stream should be removed.
  475. *
  476. * If $token is a regular token, it is swapped with the current token.
  477. *
  478. * If $token is false, the current token is deleted.
  479. *
  480. * If $token is an integer, that number of tokens (with the first token
  481. * being the current one) will be deleted.
  482. *
  483. * @param HTMLPurifier_Token|array|int|bool $token Token substitution value
  484. * @param HTMLPurifier_Injector|int $injector Injector that performed the substitution; default is if
  485. * this is not an injector related operation.
  486. * @throws HTMLPurifier_Exception
  487. */
  488. protected function processToken($token, $injector = -1)
  489. {
  490. // Zend OpCache miscompiles $token = array($token), so
  491. // avoid this pattern. See: https://github.com/ezyang/htmlpurifier/issues/108
  492. // normalize forms of token
  493. if (is_object($token)) {
  494. $tmp = $token;
  495. $token = array(1, $tmp);
  496. }
  497. if (is_int($token)) {
  498. $tmp = $token;
  499. $token = array($tmp);
  500. }
  501. if ($token === false) {
  502. $token = array(1);
  503. }
  504. if (!is_array($token)) {
  505. throw new HTMLPurifier_Exception('Invalid token type from injector');
  506. }
  507. if (!is_int($token[0])) {
  508. array_unshift($token, 1);
  509. }
  510. if ($token[0] === 0) {
  511. throw new HTMLPurifier_Exception('Deleting zero tokens is not valid');
  512. }
  513. // $token is now an array with the following form:
  514. // array(number nodes to delete, new node 1, new node 2, ...)
  515. $delete = array_shift($token);
  516. list($old, $r) = $this->zipper->splice($this->token, $delete, $token);
  517. if ($injector > -1) {
  518. // See Note [Injector skips]
  519. // Determine appropriate skips. Here's what the code does:
  520. // *If* we deleted one or more tokens, copy the skips
  521. // of those tokens into the skips of the new tokens (in $token).
  522. // Also, mark the newly inserted tokens as having come from
  523. // $injector.
  524. $oldskip = isset($old[0]) ? $old[0]->skip : array();
  525. foreach ($token as $object) {
  526. $object->skip = $oldskip;
  527. $object->skip[$injector] = true;
  528. }
  529. }
  530. return $r;
  531. }
  532. /**
  533. * Inserts a token before the current token. Cursor now points to
  534. * this token. You must reprocess after this.
  535. * @param HTMLPurifier_Token $token
  536. */
  537. private function insertBefore($token)
  538. {
  539. // NB not $this->zipper->insertBefore(), due to positioning
  540. // differences
  541. $splice = $this->zipper->splice($this->token, 0, array($token));
  542. return $splice[1];
  543. }
  544. /**
  545. * Removes current token. Cursor now points to new token occupying previously
  546. * occupied space. You must reprocess after this.
  547. */
  548. private function remove()
  549. {
  550. return $this->zipper->delete();
  551. }
  552. }
  553. // Note [Injector skips]
  554. // ~~~~~~~~~~~~~~~~~~~~~
  555. // When I originally designed this class, the idea behind the 'skip'
  556. // property of HTMLPurifier_Token was to help avoid infinite loops
  557. // in injector processing. For example, suppose you wrote an injector
  558. // that bolded swear words. Naively, you might write it so that
  559. // whenever you saw ****, you replaced it with <strong>****</strong>.
  560. //
  561. // When this happens, we will reprocess all of the tokens with the
  562. // other injectors. Now there is an opportunity for infinite loop:
  563. // if we rerun the swear-word injector on these tokens, we might
  564. // see **** and then reprocess again to get
  565. // <strong><strong>****</strong></strong> ad infinitum.
  566. //
  567. // Thus, the idea of a skip is that once we process a token with
  568. // an injector, we mark all of those tokens as having "come from"
  569. // the injector, and we never run the injector again on these
  570. // tokens.
  571. //
  572. // There were two more complications, however:
  573. //
  574. // - With HTMLPurifier_Injector_RemoveEmpty, we noticed that if
  575. // you had <b><i></i></b>, after you removed the <i></i>, you
  576. // really would like this injector to go back and reprocess
  577. // the <b> tag, discovering that it is now empty and can be
  578. // removed. So we reintroduced the possibility of infinite looping
  579. // by adding a "rewind" function, which let you go back to an
  580. // earlier point in the token stream and reprocess it with injectors.
  581. // Needless to say, we need to UN-skip the token so it gets
  582. // reprocessed.
  583. //
  584. // - Suppose that you successfuly process a token, replace it with
  585. // one with your skip mark, but now another injector wants to
  586. // process the skipped token with another token. Should you continue
  587. // to skip that new token, or reprocess it? If you reprocess,
  588. // you can end up with an infinite loop where one injector converts
  589. // <a> to <b>, and then another injector converts it back. So
  590. // we inherit the skips, but for some reason, I thought that we
  591. // should inherit the skip from the first token of the token
  592. // that we deleted. Why? Well, it seems to work OK.
  593. //
  594. // If I were to redesign this functionality, I would absolutely not
  595. // go about doing it this way: the semantics are just not very well
  596. // defined, and in any case you probably wanted to operate on trees,
  597. // not token streams.
  598. // vim: et sw=4 sts=4