FineDiff.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. <?php
  2. namespace GorHill\FineDiff;
  3. /**
  4. * FINE granularity DIFF
  5. *
  6. * Computes a set of instructions to convert the content of
  7. * one string into another.
  8. *
  9. * Copyright (c) 2011 Raymond Hill (http://raymondhill.net/blog/?p=441)
  10. *
  11. * Licensed under The MIT License
  12. *
  13. * Permission is hereby granted, free of charge, to any person obtaining a copy
  14. * of this software and associated documentation files (the "Software"), to deal
  15. * in the Software without restriction, including without limitation the rights
  16. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  17. * copies of the Software, and to permit persons to whom the Software is
  18. * furnished to do so, subject to the following conditions:
  19. *
  20. * The above copyright notice and this permission notice shall be included in
  21. * all copies or substantial portions of the Software.
  22. *
  23. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  24. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  25. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  26. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  27. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  28. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  29. * THE SOFTWARE.
  30. *
  31. * @copyright Copyright 2011 (c) Raymond Hill (http://raymondhill.net/blog/?p=441)
  32. * @link http://www.raymondhill.net/finediff/
  33. * @version 0.6
  34. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  35. */
  36. /**
  37. * Usage (simplest):
  38. *
  39. * include 'finediff.php';
  40. *
  41. * // for the stock stack, granularity values are:
  42. * // FineDiff::$paragraphGranularity = paragraph/line level
  43. * // FineDiff::$sentenceGranularity = sentence level
  44. * // FineDiff::$wordGranularity = word level
  45. * // FineDiff::$characterGranularity = character level [default]
  46. *
  47. * $opcodes = FineDiff::getDiffOpcodes($from_text, $to_text [, $granularityStack = null] );
  48. * // store opcodes for later use...
  49. *
  50. * ...
  51. *
  52. * // restore $to_text from $from_text + $opcodes
  53. * include 'finediff.php';
  54. * $to_text = FineDiff::renderToTextFromOpcodes($from_text, $opcodes);
  55. *
  56. * ...
  57. */
  58. /**
  59. * Persisted opcodes (string) are a sequence of atomic opcode.
  60. * A single opcode can be one of the following:
  61. * c | c{n} | d | d{n} | i:{c} | i{length}:{s}
  62. * 'c' = copy one character from source
  63. * 'c{n}' = copy n characters from source
  64. * 'd' = skip one character from source
  65. * 'd{n}' = skip n characters from source
  66. * 'i:{c} = insert character 'c'
  67. * 'i{n}:{s}' = insert string s, which is of length n
  68. *
  69. * Do not exist as of now, under consideration:
  70. * 'm{n}:{o} = move n characters from source o characters ahead.
  71. * It would be essentially a shortcut for a delete->copy->insert
  72. * command (swap) for when the inserted segment is exactly the same
  73. * as the deleted one, and with only a copy operation in between.
  74. * TODO: How often this case occurs? Is it worth it? Can only
  75. * be done as a postprocessing method (->optimize()?)
  76. */
  77. /**
  78. * FineDiff class
  79. *
  80. * TODO: Document
  81. *
  82. */
  83. class FineDiff {
  84. /**------------------------------------------------------------------------
  85. *
  86. * Public section
  87. *
  88. */
  89. /**
  90. * Constructor
  91. * ...
  92. * The $granularityStack allows FineDiff to be configurable so that
  93. * a particular stack tailored to the specific content of a document can
  94. * be passed.
  95. */
  96. public function __construct($from_text = '', $to_text = '', $granularityStack = null) {
  97. // setup stack for generic text documents by default
  98. $this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity;
  99. $this->edits = array();
  100. $this->from_text = $from_text;
  101. $this->doDiff($from_text, $to_text);
  102. }
  103. public function getOps() {
  104. return $this->edits;
  105. }
  106. public function getOpcodes() {
  107. $opcodes = array();
  108. foreach ( $this->edits as $edit ) {
  109. $opcodes[] = $edit->getOpcode();
  110. }
  111. return implode('', $opcodes);
  112. }
  113. public function renderDiffToHTML() {
  114. $in_offset = 0;
  115. ob_start();
  116. foreach ( $this->edits as $edit ) {
  117. $n = $edit->getFromLen();
  118. if ( $edit instanceof FineDiffCopyOp ) {
  119. FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
  120. }
  121. else if ( $edit instanceof FineDiffDeleteOp ) {
  122. FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
  123. }
  124. else if ( $edit instanceof FineDiffInsertOp ) {
  125. FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
  126. }
  127. else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
  128. FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
  129. FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
  130. }
  131. $in_offset += $n;
  132. }
  133. return ob_get_clean();
  134. }
  135. /**------------------------------------------------------------------------
  136. * Return an opcodes string describing the diff between a "From" and a
  137. * "To" string
  138. */
  139. public static function getDiffOpcodes($from, $to, $granularities = null) {
  140. $diff = new FineDiff($from, $to, $granularities);
  141. return $diff->getOpcodes();
  142. }
  143. /**------------------------------------------------------------------------
  144. * Return an iterable collection of diff ops from an opcodes string
  145. */
  146. public static function getDiffOpsFromOpcodes($opcodes) {
  147. $diffops = new FineDiffOps();
  148. FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode'));
  149. return $diffops->edits;
  150. }
  151. /**------------------------------------------------------------------------
  152. * Re-create the "To" string from the "From" string and an "Opcodes" string
  153. */
  154. public static function renderToTextFromOpcodes($from, $opcodes) {
  155. ob_start();
  156. FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
  157. return ob_get_clean();
  158. }
  159. /**------------------------------------------------------------------------
  160. * Render the diff to an HTML string
  161. */
  162. public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
  163. ob_start();
  164. FineDiff::renderFromOpcodes($from, $opcodes, array(__NAMESPACE__ . '\FineDiff','renderDiffToHTMLFromOpcode'));
  165. return ob_get_clean();
  166. }
  167. /**------------------------------------------------------------------------
  168. * Generic opcodes parser, user must supply callback for handling
  169. * single opcode
  170. */
  171. public static function renderFromOpcodes($from, $opcodes, $callback) {
  172. if ( !is_callable($callback) ) {
  173. return;
  174. }
  175. $opcodes_len = strlen($opcodes);
  176. $from_offset = $opcodes_offset = 0;
  177. while ( $opcodes_offset < $opcodes_len ) {
  178. $opcode = substr($opcodes, $opcodes_offset, 1);
  179. $opcodes_offset++;
  180. $n = intval(substr($opcodes, $opcodes_offset));
  181. if ( $n ) {
  182. $opcodes_offset += strlen(strval($n));
  183. }
  184. else {
  185. $n = 1;
  186. }
  187. if ( $opcode === 'c' ) { // copy n characters from source
  188. call_user_func($callback, 'c', $from, $from_offset, $n, '');
  189. $from_offset += $n;
  190. }
  191. else if ( $opcode === 'd' ) { // delete n characters from source
  192. call_user_func($callback, 'd', $from, $from_offset, $n, '');
  193. $from_offset += $n;
  194. }
  195. else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
  196. call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
  197. $opcodes_offset += 1 + $n;
  198. }
  199. }
  200. }
  201. /**
  202. * Stock granularity stacks and delimiters
  203. */
  204. const paragraphDelimiters = "\n\r";
  205. public static $paragraphGranularity = array(
  206. FineDiff::paragraphDelimiters
  207. );
  208. const sentenceDelimiters = ".\n\r";
  209. public static $sentenceGranularity = array(
  210. FineDiff::paragraphDelimiters,
  211. FineDiff::sentenceDelimiters
  212. );
  213. const wordDelimiters = " \t.\n\r";
  214. public static $wordGranularity = array(
  215. FineDiff::paragraphDelimiters,
  216. FineDiff::sentenceDelimiters,
  217. FineDiff::wordDelimiters
  218. );
  219. const characterDelimiters = "";
  220. public static $characterGranularity = array(
  221. FineDiff::paragraphDelimiters,
  222. FineDiff::sentenceDelimiters,
  223. FineDiff::wordDelimiters,
  224. FineDiff::characterDelimiters
  225. );
  226. public static $textStack = array(
  227. ".",
  228. " \t.\n\r",
  229. ""
  230. );
  231. /**------------------------------------------------------------------------
  232. *
  233. * Private section
  234. *
  235. */
  236. /**
  237. * Entry point to compute the diff.
  238. */
  239. private function doDiff($from_text, $to_text) {
  240. $this->last_edit = false;
  241. $this->stackpointer = 0;
  242. $this->from_text = $from_text;
  243. $this->from_offset = 0;
  244. // can't diff without at least one granularity specifier
  245. if ( empty($this->granularityStack) ) {
  246. return;
  247. }
  248. $this->_processGranularity($from_text, $to_text);
  249. }
  250. /**
  251. * This is the recursive function which is responsible for
  252. * handling/increasing granularity.
  253. *
  254. * Incrementally increasing the granularity is key to compute the
  255. * overall diff in a very efficient way.
  256. */
  257. private function _processGranularity($from_segment, $to_segment) {
  258. $delimiters = $this->granularityStack[$this->stackpointer++];
  259. $has_next_stage = $this->stackpointer < count($this->granularityStack);
  260. foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) {
  261. // increase granularity
  262. if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) {
  263. $this->_processGranularity(
  264. substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()),
  265. $fragment_edit->getText()
  266. );
  267. }
  268. // fuse copy ops whenever possible
  269. else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) {
  270. $this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen());
  271. $this->from_offset += $fragment_edit->getFromLen();
  272. }
  273. else {
  274. /* $fragment_edit instanceof FineDiffCopyOp */
  275. /* $fragment_edit instanceof FineDiffDeleteOp */
  276. /* $fragment_edit instanceof FineDiffInsertOp */
  277. $this->edits[] = $this->last_edit = $fragment_edit;
  278. $this->from_offset += $fragment_edit->getFromLen();
  279. }
  280. }
  281. $this->stackpointer--;
  282. }
  283. /**
  284. * This is the core algorithm which actually perform the diff itself,
  285. * fragmenting the strings as per specified delimiters.
  286. *
  287. * This function is naturally recursive, however for performance purpose
  288. * a local job queue is used instead of outright recursivity.
  289. */
  290. private static function doFragmentDiff($from_text, $to_text, $delimiters) {
  291. // Empty delimiter means character-level diffing.
  292. // In such case, use code path optimized for character-level
  293. // diffing.
  294. if ( empty($delimiters) ) {
  295. return FineDiff::doCharDiff($from_text, $to_text);
  296. }
  297. $result = array();
  298. // fragment-level diffing
  299. $from_text_len = strlen($from_text);
  300. $to_text_len = strlen($to_text);
  301. $from_fragments = FineDiff::extractFragments($from_text, $delimiters);
  302. $to_fragments = FineDiff::extractFragments($to_text, $delimiters);
  303. $jobs = array(array(0, $from_text_len, 0, $to_text_len));
  304. $cached_array_keys = array();
  305. while ( $job = array_pop($jobs) ) {
  306. // get the segments which must be diff'ed
  307. list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
  308. // catch easy cases first
  309. $from_segment_length = $from_segment_end - $from_segment_start;
  310. $to_segment_length = $to_segment_end - $to_segment_start;
  311. if ( !$from_segment_length || !$to_segment_length ) {
  312. if ( $from_segment_length ) {
  313. $result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length);
  314. }
  315. else if ( $to_segment_length ) {
  316. $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length));
  317. }
  318. continue;
  319. }
  320. // find longest copy operation for the current segments
  321. $best_copy_length = 0;
  322. $from_base_fragment_index = $from_segment_start;
  323. $cached_array_keys_for_current_segment = array();
  324. while ( $from_base_fragment_index < $from_segment_end ) {
  325. $from_base_fragment = $from_fragments[$from_base_fragment_index];
  326. $from_base_fragment_length = strlen($from_base_fragment);
  327. // performance boost: cache array keys
  328. if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) {
  329. if ( !isset($cached_array_keys[$from_base_fragment]) ) {
  330. $to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true);
  331. }
  332. else {
  333. $to_all_fragment_indices = $cached_array_keys[$from_base_fragment];
  334. }
  335. // get only indices which falls within current segment
  336. if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) {
  337. $to_fragment_indices = array();
  338. foreach ( $to_all_fragment_indices as $to_fragment_index ) {
  339. if ( $to_fragment_index < $to_segment_start ) { continue; }
  340. if ( $to_fragment_index >= $to_segment_end ) { break; }
  341. $to_fragment_indices[] = $to_fragment_index;
  342. }
  343. $cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices;
  344. }
  345. else {
  346. $to_fragment_indices = $to_all_fragment_indices;
  347. }
  348. }
  349. else {
  350. $to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment];
  351. }
  352. // iterate through collected indices
  353. foreach ( $to_fragment_indices as $to_base_fragment_index ) {
  354. $fragment_index_offset = $from_base_fragment_length;
  355. // iterate until no more match
  356. for (;;) {
  357. $fragment_from_index = $from_base_fragment_index + $fragment_index_offset;
  358. if ( $fragment_from_index >= $from_segment_end ) {
  359. break;
  360. }
  361. $fragment_to_index = $to_base_fragment_index + $fragment_index_offset;
  362. if ( $fragment_to_index >= $to_segment_end ) {
  363. break;
  364. }
  365. if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) {
  366. break;
  367. }
  368. $fragment_length = strlen($from_fragments[$fragment_from_index]);
  369. $fragment_index_offset += $fragment_length;
  370. }
  371. if ( $fragment_index_offset > $best_copy_length ) {
  372. $best_copy_length = $fragment_index_offset;
  373. $best_from_start = $from_base_fragment_index;
  374. $best_to_start = $to_base_fragment_index;
  375. }
  376. }
  377. $from_base_fragment_index += strlen($from_base_fragment);
  378. // If match is larger than half segment size, no point trying to find better
  379. // TODO: Really?
  380. if ( $best_copy_length >= $from_segment_length / 2) {
  381. break;
  382. }
  383. // no point to keep looking if what is left is less than
  384. // current best match
  385. if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) {
  386. break;
  387. }
  388. }
  389. if ( $best_copy_length ) {
  390. $jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start);
  391. $result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length);
  392. $jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end);
  393. }
  394. else {
  395. $result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length));
  396. }
  397. }
  398. ksort($result, SORT_NUMERIC);
  399. return array_values($result);
  400. }
  401. /**
  402. * Perform a character-level diff.
  403. *
  404. * The algorithm is quite similar to doFragmentDiff(), except that
  405. * the code path is optimized for character-level diff -- strpos() is
  406. * used to find out the longest common subequence of characters.
  407. *
  408. * We try to find a match using the longest possible subsequence, which
  409. * is at most the length of the shortest of the two strings, then incrementally
  410. * reduce the size until a match is found.
  411. *
  412. * I still need to study more the performance of this function. It
  413. * appears that for long strings, the generic doFragmentDiff() is more
  414. * performant. For word-sized strings, doCharDiff() is somewhat more
  415. * performant.
  416. */
  417. private static function doCharDiff($from_text, $to_text) {
  418. $result = array();
  419. $jobs = array(array(0, strlen($from_text), 0, strlen($to_text)));
  420. while ( $job = array_pop($jobs) ) {
  421. // get the segments which must be diff'ed
  422. list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
  423. $from_segment_len = $from_segment_end - $from_segment_start;
  424. $to_segment_len = $to_segment_end - $to_segment_start;
  425. // catch easy cases first
  426. if ( !$from_segment_len || !$to_segment_len ) {
  427. if ( $from_segment_len ) {
  428. $result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len);
  429. }
  430. else if ( $to_segment_len ) {
  431. $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len));
  432. }
  433. continue;
  434. }
  435. if ( $from_segment_len >= $to_segment_len ) {
  436. $copy_len = $to_segment_len;
  437. while ( $copy_len ) {
  438. $to_copy_start = $to_segment_start;
  439. $to_copy_start_max = $to_segment_end - $copy_len;
  440. while ( $to_copy_start <= $to_copy_start_max ) {
  441. $from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len));
  442. if ( $from_copy_start !== false ) {
  443. $from_copy_start += $from_segment_start;
  444. break 2;
  445. }
  446. $to_copy_start++;
  447. }
  448. $copy_len--;
  449. }
  450. }
  451. else {
  452. $copy_len = $from_segment_len;
  453. while ( $copy_len ) {
  454. $from_copy_start = $from_segment_start;
  455. $from_copy_start_max = $from_segment_end - $copy_len;
  456. while ( $from_copy_start <= $from_copy_start_max ) {
  457. $to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len));
  458. if ( $to_copy_start !== false ) {
  459. $to_copy_start += $to_segment_start;
  460. break 2;
  461. }
  462. $from_copy_start++;
  463. }
  464. $copy_len--;
  465. }
  466. }
  467. // match found
  468. if ( $copy_len ) {
  469. $jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start);
  470. $result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len);
  471. $jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end);
  472. }
  473. // no match, so delete all, insert all
  474. else {
  475. $result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len));
  476. }
  477. }
  478. ksort($result, SORT_NUMERIC);
  479. return array_values($result);
  480. }
  481. /**
  482. * Efficiently fragment the text into an array according to
  483. * specified delimiters.
  484. * No delimiters means fragment into single character.
  485. * The array indices are the offset of the fragments into
  486. * the input string.
  487. * A sentinel empty fragment is always added at the end.
  488. * Careful: No check is performed as to the validity of the
  489. * delimiters.
  490. */
  491. private static function extractFragments($text, $delimiters) {
  492. // special case: split into characters
  493. if ( empty($delimiters) ) {
  494. $chars = str_split($text, 1);
  495. $chars[strlen($text)] = '';
  496. return $chars;
  497. }
  498. $fragments = array();
  499. $start = $end = 0;
  500. for (;;) {
  501. $end += strcspn($text, $delimiters, $end);
  502. $end += strspn($text, $delimiters, $end);
  503. if ( $end === $start ) {
  504. break;
  505. }
  506. $fragments[$start] = substr($text, $start, $end - $start);
  507. $start = $end;
  508. }
  509. $fragments[$start] = '';
  510. return $fragments;
  511. }
  512. /**
  513. * Stock opcode renderers
  514. */
  515. private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
  516. if ( $opcode === 'c' || $opcode === 'i' ) {
  517. echo substr($from, $from_offset, $from_len);
  518. }
  519. }
  520. private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
  521. if ( $opcode === 'c' ) {
  522. echo htmlentities(substr($from, $from_offset, $from_len));
  523. }
  524. else if ( $opcode === 'd' ) {
  525. $deletion = substr($from, $from_offset, $from_len);
  526. if ( strcspn($deletion, " \n\r") === 0 ) {
  527. $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
  528. }
  529. echo '<del>', htmlentities($deletion), '</del>';
  530. }
  531. else /* if ( $opcode === 'i' ) */ {
  532. echo '<ins>', htmlentities(substr($from, $from_offset, $from_len)), '</ins>';
  533. }
  534. }
  535. }