PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 261001
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v261001
261001 260927 260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 All 191 releases
s2member / src / includes / externals / markdown / nc-markdown.inc.php

nc-markdown.inc.php in s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions 261001, at src/includes/externals/markdown/nc-markdown.inc.php

1,481 lines 39.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // @codingStandardsIgnoreFile
3 if(!defined('WPINC')) //260928.0402 This parser is only loaded through WordPress/s2Member; reject direct web requests.
4 exit('Do not access this file directly.');
5
6 /**
7 * PHP Markdown class.
8 *
9 * Copyright {@link http://www.michelf.com/projects/php-markdown/ Michel Fortin}.
10 * Original Markdown. Copyright {@link http://daringfireball.net/projects/markdown/ John Gruber}.
11 *
12 * Modified by {@link http://websharks-inc.com/ WebSharks, Inc.}.
13 * Excludes WordPress and all other interfaces.
14 * Uses a custom class name and interface.
15 *
16 * This file is included with all WordPress themes/plugins by WebSharks, Inc.
17 *
18 * @package WebSharks\Xtnls\Markdown
19 * @since x.xx
20 */
21 /**
22 * PHP Markdown interface.
23 *
24 * @package WebSharks\Xtnls\Markdown
25 * @since x.xx
26 *
27 * @param string $text Text to be parsed by the Markdown class.
28 * @return string HTML output; after having been parsed by the Markdown class.
29 */
30 function NC_Markdown($text) {
31
32 static $parser;
33 if (!isset($parser)) {
34 $parser = new NC_Markdown_Parser;
35 }
36
37 return $parser->transform($text);
38 }
39 /**
40 * PHP Markdown class.
41 * @package Xtnls\Markdown
42 * @since x.xx
43 */
44 class NC_Markdown_Parser {
45
46 # Regex to match balanced [brackets].
47 # Needed to insert a maximum bracked depth while converting to PHP.
48 var $nested_brackets_depth = 6;
49 var $nested_brackets_re;
50
51 var $nested_url_parenthesis_depth = 4;
52 var $nested_url_parenthesis_re;
53
54 # Table of hash values for escaped characters:
55 var $escape_chars = '\`*_{}[]()>#+-.!';
56 var $escape_chars_re;
57
58 # Change to ">" for HTML output.
59 var $empty_element_suffix = " />";
60 var $tab_width = 4;
61
62 # Change to `true` to disallow markup or entities.
63 var $no_markup = false;
64 var $no_entities = false;
65
66 # Predefined urls and titles for reference links and images.
67 var $predef_urls = array();
68 var $predef_titles = array();
69
70
71 function __construct() {
72 #
73 # Constructor function. Initialize appropriate member variables.
74 #
75 $this->_initDetab();
76 $this->prepareItalicsAndBold();
77
78 $this->nested_brackets_re =
79 str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth).
80 str_repeat('\])*', $this->nested_brackets_depth);
81
82 $this->nested_url_parenthesis_re =
83 str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth).
84 str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth);
85
86 $this->escape_chars_re = '['.preg_quote($this->escape_chars).']';
87
88 # Sort document, block, and span gamut in ascendent priority order.
89 asort($this->document_gamut);
90 asort($this->block_gamut);
91 asort($this->span_gamut);
92 }
93
94
95 # Internal hashes used during transformation.
96 var $urls = array();
97 var $titles = array();
98 var $html_hashes = array();
99
100 # Status flag to avoid invalid nesting.
101 var $in_anchor = false;
102
103
104 function setup() {
105 #
106 # Called before the transformation process starts to setup parser
107 # states.
108 #
109 # Clear global hashes.
110 $this->urls = $this->predef_urls;
111 $this->titles = $this->predef_titles;
112 $this->html_hashes = array();
113
114 $in_anchor = false;
115 }
116
117 function teardown() {
118 #
119 # Called after the transformation process to clear any variable
120 # which may be taking up memory unnecessarly.
121 #
122 $this->urls = array();
123 $this->titles = array();
124 $this->html_hashes = array();
125 }
126
127
128 function transform($text) {
129 #
130 # Main function. Performs some preprocessing on the input text
131 # and pass it through the document gamut.
132 #
133 $this->setup();
134
135 # Remove UTF-8 BOM and marker character in input, if present.
136 $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text);
137
138 # Standardize line endings:
139 # DOS to Unix and Mac to Unix
140 $text = preg_replace('{\r\n?}', "\n", $text);
141
142 # Make sure $text ends with a couple of newlines:
143 $text .= "\n\n";
144
145 # Convert all tabs to spaces.
146 $text = $this->detab($text);
147
148 # Turn block-level HTML blocks into hash entries
149 $text = $this->hashHTMLBlocks($text);
150
151 # Strip any lines consisting only of spaces and tabs.
152 # This makes subsequent regexen easier to write, because we can
153 # match consecutive blank lines with /\n+/ instead of something
154 # contorted like /[ ]*\n+/ .
155 $text = preg_replace('/^[ ]+$/m', '', $text);
156
157 # Run document gamut methods.
158 foreach ($this->document_gamut as $method => $priority) {
159 $text = $this->$method($text);
160 }
161
162 $this->teardown();
163
164 return $text . "\n";
165 }
166
167 var $document_gamut = array(
168 # Strip link definitions, store in hashes.
169 "stripLinkDefinitions" => 20,
170
171 "runBasicBlockGamut" => 30,
172 );
173
174
175 function stripLinkDefinitions($text) {
176 #
177 # Strips link definitions from text, stores the URLs and titles in
178 # hash references.
179 #
180 $less_than_tab = $this->tab_width - 1;
181
182 # Link defs are in the form: ^[id]: url "optional title"
183 $text = preg_replace_callback('{
184 ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1
185 [ ]*
186 \n? # maybe *one* newline
187 [ ]*
188 <?(\S+?)>? # url = $2
189 [ ]*
190 \n? # maybe one newline
191 [ ]*
192 (?:
193 (?<=\s) # lookbehind for whitespace
194 ["(]
195 (.*?) # title = $3
196 [")]
197 [ ]*
198 )? # title is optional
199 (?:\n+|\Z)
200 }xm',
201 array(&$this, '_stripLinkDefinitions_callback'),
202 $text);
203 return $text;
204 }
205 function _stripLinkDefinitions_callback($matches) {
206 $link_id = strtolower($matches[1]);
207 $this->urls[$link_id] = $matches[2];
208 $this->titles[$link_id] =& $matches[3];
209 return ''; # String that will replace the block
210 }
211
212
213 function hashHTMLBlocks($text) {
214 if ($this->no_markup) return $text;
215
216 $less_than_tab = $this->tab_width - 1;
217
218 # Hashify HTML blocks:
219 # We only want to do this for block-level HTML tags, such as headers,
220 # lists, and tables. That's because we still want to wrap <p>s around
221 # "paragraphs" that are wrapped in non-block-level tags, such as anchors,
222 # phrase emphasis, and spans. The list of tags we're looking for is
223 # hard-coded:
224 #
225 # * List "a" is made of tags which can be both inline or block-level.
226 # These will be treated block-level when the start tag is alone on
227 # its line, otherwise they're not matched here and will be taken as
228 # inline later.
229 # * List "b" is made of tags which are always block-level;
230 #
231 $block_tags_a_re = 'ins|del';
232 $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'.
233 'script|noscript|form|fieldset|iframe|math';
234
235 # Regular expression for the content of a block tag.
236 $nested_tags_level = 4;
237 $attr = '
238 (?> # optional tag attributes
239 \s # starts with whitespace
240 (?>
241 [^>"/]+ # text outside quotes
242 |
243 /+(?!>) # slash not followed by ">"
244 |
245 "[^"]*" # text inside double quotes (tolerate ">")
246 |
247 \'[^\']*\' # text inside single quotes (tolerate ">")
248 )*
249 )?
250 ';
251 $content =
252 str_repeat('
253 (?>
254 [^<]+ # content without tag
255 |
256 <\2 # nested opening tag
257 '.$attr.' # attributes
258 (?>
259 />
260 |
261 >', $nested_tags_level). # end of opening tag
262 '.*?'. # last level nested tag content
263 str_repeat('
264 </\2\s*> # closing nested tag
265 )
266 |
267 <(?!/\2\s*> # other tags with a different name
268 )
269 )*',
270 $nested_tags_level);
271 $content2 = str_replace('\2', '\3', $content);
272
273 # First, look for nested blocks, e.g.:
274 # <div>
275 # <div>
276 # tags for inner block must be indented.
277 # </div>
278 # </div>
279 #
280 # The outermost tags must start at the left margin for this to match, and
281 # the inner nested divs must be indented.
282 # We need to do this before the next, more liberal match, because the next
283 # match will start at the first `<div>` and stop at the first `</div>`.
284 $text = preg_replace_callback('{(?>
285 (?>
286 (?<=\n\n) # Starting after a blank line
287 | # or
288 \A\n? # the beginning of the doc
289 )
290 ( # save in $1
291
292 # Match from `\n<tag>` to `</tag>\n`, handling nested tags
293 # in between.
294
295 [ ]{0,'.$less_than_tab.'}
296 <('.$block_tags_b_re.')# start tag = $2
297 '.$attr.'> # attributes followed by > and \n
298 '.$content.' # content, support nesting
299 </\2> # the matching end tag
300 [ ]* # trailing spaces/tabs
301 (?=\n+|\Z) # followed by a newline or end of document
302
303 | # Special version for tags of group a.
304
305 [ ]{0,'.$less_than_tab.'}
306 <('.$block_tags_a_re.')# start tag = $3
307 '.$attr.'>[ ]*\n # attributes followed by >
308 '.$content2.' # content, support nesting
309 </\3> # the matching end tag
310 [ ]* # trailing spaces/tabs
311 (?=\n+|\Z) # followed by a newline or end of document
312
313 | # Special case just for <hr />. It was easier to make a special
314 # case than to make the other regex more complicated.
315
316 [ ]{0,'.$less_than_tab.'}
317 <(hr) # start tag = $2
318 '.$attr.' # attributes
319 /?> # the matching end tag
320 [ ]*
321 (?=\n{2,}|\Z) # followed by a blank line or end of document
322
323 | # Special case for standalone HTML comments:
324
325 [ ]{0,'.$less_than_tab.'}
326 (?s:
327 <!-- .*? -->
328 )
329 [ ]*
330 (?=\n{2,}|\Z) # followed by a blank line or end of document
331
332 | # PHP and ASP-style processor instructions (<? and <%)
333
334 [ ]{0,'.$less_than_tab.'}
335 (?s:
336 <([?%]) # $2
337 .*?
338 \2>
339 )
340 [ ]*
341 (?=\n{2,}|\Z) # followed by a blank line or end of document
342
343 )
344 )}Sxmi',
345 array(&$this, '_hashHTMLBlocks_callback'),
346 $text);
347
348 return $text;
349 }
350 function _hashHTMLBlocks_callback($matches) {
351 $text = $matches[1];
352 $key = $this->hashBlock($text);
353 return "\n\n$key\n\n";
354 }
355
356
357 function hashPart($text, $boundary = 'X') {
358 #
359 # Called whenever a tag must be hashed when a function insert an atomic
360 # element in the text stream. Passing $text to through this function gives
361 # a unique text-token which will be reverted back when calling unhash.
362 #
363 # The $boundary argument specify what character should be used to surround
364 # the token. By convension, "B" is used for block elements that needs not
365 # to be wrapped into paragraph tags at the end, ":" is used for elements
366 # that are word separators and "X" is used in the general case.
367 #
368 # Swap back any tag hash found in $text so we do not have to `unhash`
369 # multiple times at the end.
370 $text = $this->unhash($text);
371
372 # Then hash the block.
373 static $i = 0;
374 $key = "$boundary\x1A" . ++$i . $boundary;
375 $this->html_hashes[$key] = $text;
376 return $key; # String that will replace the tag.
377 }
378
379
380 function hashBlock($text) {
381 #
382 # Shortcut function for hashPart with block-level boundaries.
383 #
384 return $this->hashPart($text, 'B');
385 }
386
387
388 var $block_gamut = array(
389 #
390 # These are all the transformations that form block-level
391 # tags like paragraphs, headers, and list items.
392 #
393 "doHeaders" => 10,
394 "doHorizontalRules" => 20,
395
396 "doLists" => 40,
397 "doCodeBlocks" => 50,
398 "doBlockQuotes" => 60,
399 );
400
401 function runBlockGamut($text) {
402 #
403 # Run block gamut tranformations.
404 #
405 # We need to escape raw HTML in Markdown source before doing anything
406 # else. This need to be done for each block, and not only at the
407 # begining in the Markdown function since hashed blocks can be part of
408 # list items and could have been indented. Indented blocks would have
409 # been seen as a code block in a previous pass of hashHTMLBlocks.
410 $text = $this->hashHTMLBlocks($text);
411
412 return $this->runBasicBlockGamut($text);
413 }
414
415 function runBasicBlockGamut($text) {
416 #
417 # Run block gamut tranformations, without hashing HTML blocks. This is
418 # useful when HTML blocks are known to be already hashed, like in the first
419 # whole-document pass.
420 #
421 foreach ($this->block_gamut as $method => $priority) {
422 $text = $this->$method($text);
423 }
424
425 # Finally form paragraph and restore hashed blocks.
426 $text = $this->formParagraphs($text);
427
428 return $text;
429 }
430
431
432 function doHorizontalRules($text) {
433 # Do Horizontal Rules:
434 return preg_replace(
435 '{
436 ^[ ]{0,3} # Leading space
437 ([-*_]) # $1: First marker
438 (?> # Repeated marker group
439 [ ]{0,2} # Zero, one, or two spaces.
440 \1 # Marker character
441 ){2,} # Group repeated at least twice
442 [ ]* # Tailing spaces
443 $ # End of line.
444 }mx',
445 "\n".$this->hashBlock("<hr$this->empty_element_suffix")."\n",
446 $text);
447 }
448
449
450 var $span_gamut = array(
451 #
452 # These are all the transformations that occur *within* block-level
453 # tags like paragraphs, headers, and list items.
454 #
455 # Process character escapes, code spans, and inline HTML
456 # in one shot.
457 "parseSpan" => -30,
458
459 # Process anchor and image tags. Images must come first,
460 # because ![foo][f] looks like an anchor.
461 "doImages" => 10,
462 "doAnchors" => 20,
463
464 # Make links out of things like `<http://example.com/>`
465 # Must come after doAnchors, because you can use < and >
466 # delimiters in inline links like [this](<url>).
467 "doAutoLinks" => 30,
468 "encodeAmpsAndAngles" => 40,
469
470 "doItalicsAndBold" => 50,
471 "doHardBreaks" => 60,
472 );
473
474 function runSpanGamut($text) {
475 #
476 # Run span gamut tranformations.
477 #
478 foreach ($this->span_gamut as $method => $priority) {
479 $text = $this->$method($text);
480 }
481
482 return $text;
483 }
484
485
486 function doHardBreaks($text) {
487 # Do hard breaks:
488 return preg_replace_callback('/ {2,}\n/',
489 array(&$this, '_doHardBreaks_callback'), $text);
490 }
491 function _doHardBreaks_callback($matches) {
492 return $this->hashPart("<br$this->empty_element_suffix\n");
493 }
494
495
496 function doAnchors($text) {
497 #
498 # Turn Markdown link shortcuts into XHTML <a> tags.
499 #
500 if ($this->in_anchor) return $text;
501 $this->in_anchor = true;
502
503 #
504 # First, handle reference-style links: [link text] [id]
505 #
506 $text = preg_replace_callback('{
507 ( # wrap whole match in $1
508 \[
509 ('.$this->nested_brackets_re.') # link text = $2
510 \]
511
512 [ ]? # one optional space
513 (?:\n[ ]*)? # one optional newline followed by spaces
514
515 \[
516 (.*?) # id = $3
517 \]
518 )
519 }xs',
520 array(&$this, '_doAnchors_reference_callback'), $text);
521
522 #
523 # Next, inline-style links: [link text](url "optional title")
524 #
525 $text = preg_replace_callback('{
526 ( # wrap whole match in $1
527 \[
528 ('.$this->nested_brackets_re.') # link text = $2
529 \]
530 \( # literal paren
531 [ ]*
532 (?:
533 <(\S*)> # href = $3
534 |
535 ('.$this->nested_url_parenthesis_re.') # href = $4
536 )
537 [ ]*
538 ( # $5
539 ([\'"]) # quote char = $6
540 (.*?) # Title = $7
541 \6 # matching quote
542 [ ]* # ignore any spaces/tabs between closing quote and )
543 )? # title is optional
544 \)
545 )
546 }xs',
547 array(&$this, '_DoAnchors_inline_callback'), $text);
548
549 #
550 # Last, handle reference-style shortcuts: [link text]
551 # These must come last in case you've also got [link test][1]
552 # or [link test](/foo)
553 #
554 // $text = preg_replace_callback('{
555 // ( # wrap whole match in $1
556 // \[
557 // ([^\[\]]+) # link text = $2; can\'t contain [ or ]
558 // \]
559 // )
560 // }xs',
561 // array(&$this, '_doAnchors_reference_callback'), $text);
562
563 $this->in_anchor = false;
564 return $text;
565 }
566 function _doAnchors_reference_callback($matches) {
567 $whole_match = $matches[1];
568 $link_text = $matches[2];
569 $link_id =& $matches[3];
570
571 if ($link_id == "") {
572 # for shortcut links like [this][] or [this].
573 $link_id = $link_text;
574 }
575
576 # lower-case and turn embedded newlines into spaces
577 $link_id = strtolower($link_id);
578 $link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
579
580 if (isset($this->urls[$link_id])) {
581 $url = $this->urls[$link_id];
582 $url = $this->encodeAttribute($url);
583
584 $result = "<a href=\"$url\"";
585 if ( isset( $this->titles[$link_id] ) ) {
586 $title = $this->titles[$link_id];
587 $title = $this->encodeAttribute($title);
588 $result .= " title=\"$title\"";
589 }
590
591 $link_text = $this->runSpanGamut($link_text);
592 $result .= ">$link_text</a>";
593 $result = $this->hashPart($result);
594 }
595 else {
596 $result = $whole_match;
597 }
598 return $result;
599 }
600 function _doAnchors_inline_callback($matches) {
601 $whole_match = $matches[1];
602 $link_text = $this->runSpanGamut($matches[2]);
603 $url = $matches[3] == '' ? $matches[4] : $matches[3];
604 $title =& $matches[7];
605
606 $url = $this->encodeAttribute($url);
607
608 $result = "<a href=\"$url\"";
609 if (isset($title)) {
610 $title = $this->encodeAttribute($title);
611 $result .= " title=\"$title\"";
612 }
613
614 $link_text = $this->runSpanGamut($link_text);
615 $result .= ">$link_text</a>";
616
617 return $this->hashPart($result);
618 }
619
620
621 function doImages($text) {
622 #
623 # Turn Markdown image shortcuts into <img> tags.
624 #
625 #
626 # First, handle reference-style labeled images: ![alt text][id]
627 #
628 $text = preg_replace_callback('{
629 ( # wrap whole match in $1
630 !\[
631 ('.$this->nested_brackets_re.') # alt text = $2
632 \]
633
634 [ ]? # one optional space
635 (?:\n[ ]*)? # one optional newline followed by spaces
636
637 \[
638 (.*?) # id = $3
639 \]
640
641 )
642 }xs',
643 array(&$this, '_doImages_reference_callback'), $text);
644
645 #
646 # Next, handle inline images: ![alt text](url "optional title")
647 # Don't forget: encode * and _
648 #
649 $text = preg_replace_callback('{
650 ( # wrap whole match in $1
651 !\[
652 ('.$this->nested_brackets_re.') # alt text = $2
653 \]
654 \s? # One optional whitespace character
655 \( # literal paren
656 [ ]*
657 (?:
658 <(\S*)> # src url = $3
659 |
660 ('.$this->nested_url_parenthesis_re.') # src url = $4
661 )
662 [ ]*
663 ( # $5
664 ([\'"]) # quote char = $6
665 (.*?) # title = $7
666 \6 # matching quote
667 [ ]*
668 )? # title is optional
669 \)
670 )
671 }xs',
672 array(&$this, '_doImages_inline_callback'), $text);
673
674 return $text;
675 }
676 function _doImages_reference_callback($matches) {
677 $whole_match = $matches[1];
678 $alt_text = $matches[2];
679 $link_id = strtolower($matches[3]);
680
681 if ($link_id == "") {
682 $link_id = strtolower($alt_text); # for shortcut links like ![this][].
683 }
684
685 $alt_text = $this->encodeAttribute($alt_text);
686 if (isset($this->urls[$link_id])) {
687 $url = $this->encodeAttribute($this->urls[$link_id]);
688 $result = "<img src=\"$url\" alt=\"$alt_text\"";
689 if (isset($this->titles[$link_id])) {
690 $title = $this->titles[$link_id];
691 $title = $this->encodeAttribute($title);
692 $result .= " title=\"$title\"";
693 }
694 $result .= $this->empty_element_suffix;
695 $result = $this->hashPart($result);
696 }
697 else {
698 # If there's no such link ID, leave intact:
699 $result = $whole_match;
700 }
701
702 return $result;
703 }
704 function _doImages_inline_callback($matches) {
705 $whole_match = $matches[1];
706 $alt_text = $matches[2];
707 $url = $matches[3] == '' ? $matches[4] : $matches[3];
708 $title =& $matches[7];
709
710 $alt_text = $this->encodeAttribute($alt_text);
711 $url = $this->encodeAttribute($url);
712 $result = "<img src=\"$url\" alt=\"$alt_text\"";
713 if (isset($title)) {
714 $title = $this->encodeAttribute($title);
715 $result .= " title=\"$title\""; # $title already quoted
716 }
717 $result .= $this->empty_element_suffix;
718
719 return $this->hashPart($result);
720 }
721
722
723 function doHeaders($text) {
724 # Setext-style headers:
725 # Header 1
726 # ========
727 #
728 # Header 2
729 # --------
730 #
731 $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx',
732 array(&$this, '_doHeaders_callback_setext'), $text);
733
734 # atx-style headers:
735 # # Header 1
736 # ## Header 2
737 # ## Header 2 with closing hashes ##
738 # ...
739 # ###### Header 6
740 #
741 $text = preg_replace_callback('{
742 ^(\#{1,6}) # $1 = string of #\'s
743 [ ]*
744 (.+?) # $2 = Header text
745 [ ]*
746 \#* # optional closing #\'s (not counted)
747 \n+
748 }xm',
749 array(&$this, '_doHeaders_callback_atx'), $text);
750
751 return $text;
752 }
753 function _doHeaders_callback_setext($matches) {
754 # Terrible hack to check we haven't found an empty list item.
755 if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1]))
756 return $matches[0];
757
758 $level = $matches[2][0] == '=' ? 1 : 2;
759 $block = "<h$level>".$this->runSpanGamut($matches[1])."</h$level>";
760 return "\n" . $this->hashBlock($block) . "\n\n";
761 }
762 function _doHeaders_callback_atx($matches) {
763 $level = strlen($matches[1]);
764 $block = "<h$level>".$this->runSpanGamut($matches[2])."</h$level>";
765 return "\n" . $this->hashBlock($block) . "\n\n";
766 }
767
768
769 function doLists($text) {
770 #
771 # Form HTML ordered (numbered) and unordered (bulleted) lists.
772 #
773 $less_than_tab = $this->tab_width - 1;
774
775 # Re-usable patterns to match list item bullets and number markers:
776 $marker_ul_re = '[*+-]';
777 $marker_ol_re = '\d+[.]';
778 $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
779
780 $markers_relist = array($marker_ul_re, $marker_ol_re);
781
782 foreach ($markers_relist as $marker_re) {
783 # Re-usable pattern to match any entirel ul or ol list:
784 $whole_list_re = '
785 ( # $1 = whole list
786 ( # $2
787 [ ]{0,'.$less_than_tab.'}
788 ('.$marker_re.') # $3 = first list item marker
789 [ ]+
790 )
791 (?s:.+?)
792 ( # $4
793 \z
794 |
795 \n{2,}
796 (?=\S)
797 (?! # Negative lookahead for another list item marker
798 [ ]*
799 '.$marker_re.'[ ]+
800 )
801 )
802 )
803 '; // mx
804
805 # We use a different prefix before nested lists than top-level lists.
806 # See extended comment in _ProcessListItems().
807
808 if ($this->list_level) {
809 $text = preg_replace_callback('{
810 ^
811 '.$whole_list_re.'
812 }mx',
813 array(&$this, '_doLists_callback'), $text);
814 }
815 else {
816 $text = preg_replace_callback('{
817 (?:(?<=\n)\n|\A\n?) # Must eat the newline
818 '.$whole_list_re.'
819 }mx',
820 array(&$this, '_doLists_callback'), $text);
821 }
822 }
823
824 return $text;
825 }
826 function _doLists_callback($matches) {
827 # Re-usable patterns to match list item bullets and number markers:
828 $marker_ul_re = '[*+-]';
829 $marker_ol_re = '\d+[.]';
830 $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)";
831
832 $list = $matches[1];
833 $list_type = preg_match("/$marker_ul_re/", $matches[3]) ? "ul" : "ol";
834
835 $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re );
836
837 $list .= "\n";
838 $result = $this->processListItems($list, $marker_any_re);
839
840 $result = $this->hashBlock("<$list_type>\n" . $result . "</$list_type>");
841 return "\n". $result ."\n\n";
842 }
843
844 var $list_level = 0;
845
846 function processListItems($list_str, $marker_any_re) {
847 #
848 # Process the contents of a single ordered or unordered list, splitting it
849 # into individual list items.
850 #
851 # The $this->list_level global keeps track of when we're inside a list.
852 # Each time we enter a list, we increment it; when we leave a list,
853 # we decrement. If it's zero, we're not in a list anymore.
854 #
855 # We do this because when we're not inside a list, we want to treat
856 # something like this:
857 #
858 # I recommend upgrading to version
859 # 8. Oops, now this line is treated
860 # as a sub-list.
861 #
862 # As a single paragraph, despite the fact that the second line starts
863 # with a digit-period-space sequence.
864 #
865 # Whereas when we're inside a list (or sub-list), that line will be
866 # treated as the start of a sub-list. What a kludge, huh? This is
867 # an aspect of Markdown's syntax that's hard to parse perfectly
868 # without resorting to mind-reading. Perhaps the solution is to
869 # change the syntax rules such that sub-lists must start with a
870 # starting cardinal number; e.g., "1." or "a.".
871
872 $this->list_level++;
873
874 # trim trailing blank lines:
875 $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
876
877 $list_str = preg_replace_callback('{
878 (\n)? # leading line = $1
879 (^[ ]*) # leading whitespace = $2
880 ('.$marker_any_re.' # list marker and space = $3
881 (?:[ ]+|(?=\n)) # space only required if item is not empty
882 )
883 ((?s:.*?)) # list item text = $4
884 (?:(\n+(?=\n))|\n) # tailing blank line = $5
885 (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n))))
886 }xm',
887 array(&$this, '_processListItems_callback'), $list_str);
888
889 $this->list_level--;
890 return $list_str;
891 }
892 function _processListItems_callback($matches) {
893 $item = $matches[4];
894 $leading_line =& $matches[1];
895 $leading_space =& $matches[2];
896 $marker_space = $matches[3];
897 $tailing_blank_line =& $matches[5];
898
899 if ($leading_line || $tailing_blank_line ||
900 preg_match('/\n{2,}/', $item))
901 {
902 # Replace marker with the appropriate whitespace indentation
903 $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item;
904 $item = $this->runBlockGamut($this->outdent($item)."\n");
905 }
906 else {
907 # Recursion for sub-lists:
908 $item = $this->doLists($this->outdent($item));
909 $item = preg_replace('/\n+$/', '', $item);
910 $item = $this->runSpanGamut($item);
911 }
912
913 return "<li>" . $item . "</li>\n";
914 }
915
916
917 function doCodeBlocks($text) {
918 #
919 # Process Markdown `<pre><code>` blocks.
920 #
921 $text = preg_replace_callback('{
922 (?:\n\n|\A\n?)
923 ( # $1 = the code block -- one or more lines, starting with a space/tab
924 (?>
925 [ ]{'.$this->tab_width.'} # Lines must start with a tab or a tab-width of spaces
926 .*\n+
927 )+
928 )
929 ((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
930 }xm',
931 array(&$this, '_doCodeBlocks_callback'), $text);
932
933 return $text;
934 }
935 function _doCodeBlocks_callback($matches) {
936 $codeblock = $matches[1];
937
938 $codeblock = $this->outdent($codeblock);
939 $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
940
941 # trim leading newlines and trailing newlines
942 $codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
943
944 $codeblock = "<pre><code>$codeblock\n</code></pre>";
945 return "\n\n".$this->hashBlock($codeblock)."\n\n";
946 }
947
948
949 function makeCodeSpan($code) {
950 #
951 # Create a code span markup for $code. Called from handleSpanToken.
952 #
953 $code = htmlspecialchars(trim($code), ENT_NOQUOTES);
954 return $this->hashPart("<code>$code</code>");
955 }
956
957
958 var $em_relist = array(
959 '' => '(?:(?<!\*)\*(?!\*)|(?<!_)_(?!_))(?=\S)(?![.,:;]\s)',
960 '*' => '(?<=\S)(?<!\*)\*(?!\*)',
961 '_' => '(?<=\S)(?<!_)_(?!_)',
962 );
963 var $strong_relist = array(
964 '' => '(?:(?<!\*)\*\*(?!\*)|(?<!_)__(?!_))(?=\S)(?![.,:;]\s)',
965 '**' => '(?<=\S)(?<!\*)\*\*(?!\*)',
966 '__' => '(?<=\S)(?<!_)__(?!_)',
967 );
968 var $em_strong_relist = array(
969 '' => '(?:(?<!\*)\*\*\*(?!\*)|(?<!_)___(?!_))(?=\S)(?![.,:;]\s)',
970 '***' => '(?<=\S)(?<!\*)\*\*\*(?!\*)',
971 '___' => '(?<=\S)(?<!_)___(?!_)',
972 );
973 var $em_strong_prepared_relist;
974
975 function prepareItalicsAndBold() {
976 #
977 # Prepare regular expressions for seraching emphasis tokens in any
978 # context.
979 #
980 foreach ($this->em_relist as $em => $em_re) {
981 foreach ($this->strong_relist as $strong => $strong_re) {
982 # Construct list of allowed token expressions.
983 $token_relist = array();
984 if (isset($this->em_strong_relist["$em$strong"])) {
985 $token_relist[] = $this->em_strong_relist["$em$strong"];
986 }
987 $token_relist[] = $em_re;
988 $token_relist[] = $strong_re;
989
990 # Construct master expression from list.
991 $token_re = '{('. implode('|', $token_relist) .')}';
992 $this->em_strong_prepared_relist["$em$strong"] = $token_re;
993 }
994 }
995 }
996
997 function doItalicsAndBold($text) {
998 $token_stack = array('');
999 $text_stack = array('');
1000 $em = '';
1001 $strong = '';
1002 $tree_char_em = false;
1003
1004 while (1) {
1005 #
1006 # Get prepared regular expression for seraching emphasis tokens
1007 # in current context.
1008 #
1009 $token_re = $this->em_strong_prepared_relist["$em$strong"];
1010
1011 #
1012 # Each loop iteration seach for the next emphasis token.
1013 # Each token is then passed to handleSpanToken.
1014 #
1015 $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
1016 $text_stack[0] .= $parts[0];
1017 $token =& $parts[1];
1018 $text =& $parts[2];
1019
1020 if (empty($token)) {
1021 # Reached end of text span: empty stack without emitting.
1022 # any more emphasis.
1023 while ($token_stack[0]) {
1024 $text_stack[1] .= array_shift($token_stack);
1025 $text_stack[0] .= array_shift($text_stack);
1026 }
1027 break;
1028 }
1029
1030 $token_len = strlen($token);
1031 if ($tree_char_em) {
1032 # Reached closing marker while inside a three-char emphasis.
1033 if ($token_len == 3) {
1034 # Three-char closing marker, close em and strong.
1035 array_shift($token_stack);
1036 $span = array_shift($text_stack);
1037 $span = $this->runSpanGamut($span);
1038 $span = "<strong><em>$span</em></strong>";
1039 $text_stack[0] .= $this->hashPart($span);
1040 $em = '';
1041 $strong = '';
1042 } else {
1043 # Other closing marker: close one em or strong and
1044 # change current token state to match the other
1045 $token_stack[0] = str_repeat($token[0], 3-$token_len);
1046 $tag = $token_len == 2 ? "strong" : "em";
1047 $span = $text_stack[0];
1048 $span = $this->runSpanGamut($span);
1049 $span = "<$tag>$span</$tag>";
1050 $text_stack[0] = $this->hashPart($span);
1051 $$tag = ''; # $$tag stands for $em or $strong
1052 }
1053 $tree_char_em = false;
1054 } else if ($token_len == 3) {
1055 if ($em) {
1056 # Reached closing marker for both em and strong.
1057 # Closing strong marker:
1058 for ($i = 0; $i < 2; ++$i) {
1059 $shifted_token = array_shift($token_stack);
1060 $tag = strlen($shifted_token) == 2 ? "strong" : "em";
1061 $span = array_shift($text_stack);
1062 $span = $this->runSpanGamut($span);
1063 $span = "<$tag>$span</$tag>";
1064 $text_stack[0] .= $this->hashPart($span);
1065 $$tag = ''; # $$tag stands for $em or $strong
1066 }
1067 } else {
1068 # Reached opening three-char emphasis marker. Push on token
1069 # stack; will be handled by the special condition above.
1070 $em = $token[0];
1071 $strong = "$em$em";
1072 array_unshift($token_stack, $token);
1073 array_unshift($text_stack, '');
1074 $tree_char_em = true;
1075 }
1076 } else if ($token_len == 2) {
1077 if ($strong) {
1078 # Unwind any dangling emphasis marker:
1079 if (strlen($token_stack[0]) == 1) {
1080 $text_stack[1] .= array_shift($token_stack);
1081 $text_stack[0] .= array_shift($text_stack);
1082 }
1083 # Closing strong marker:
1084 array_shift($token_stack);
1085 $span = array_shift($text_stack);
1086 $span = $this->runSpanGamut($span);
1087 $span = "<strong>$span</strong>";
1088 $text_stack[0] .= $this->hashPart($span);
1089 $strong = '';
1090 } else {
1091 array_unshift($token_stack, $token);
1092 array_unshift($text_stack, '');
1093 $strong = $token;
1094 }
1095 } else {
1096 # Here $token_len == 1
1097 if ($em) {
1098 if (strlen($token_stack[0]) == 1) {
1099 # Closing emphasis marker:
1100 array_shift($token_stack);
1101 $span = array_shift($text_stack);
1102 $span = $this->runSpanGamut($span);
1103 $span = "<em>$span</em>";
1104 $text_stack[0] .= $this->hashPart($span);
1105 $em = '';
1106 } else {
1107 $text_stack[0] .= $token;
1108 }
1109 } else {
1110 array_unshift($token_stack, $token);
1111 array_unshift($text_stack, '');
1112 $em = $token;
1113 }
1114 }
1115 }
1116 return $text_stack[0];
1117 }
1118
1119
1120 function doBlockQuotes($text) {
1121 $text = preg_replace_callback('/
1122 ( # Wrap whole match in $1
1123 (?>
1124 ^[ ]*>[ ]? # ">" at the start of a line
1125 .+\n # rest of the first line
1126 (.+\n)* # subsequent consecutive lines
1127 \n* # blanks
1128 )+
1129 )
1130 /xm',
1131 array(&$this, '_doBlockQuotes_callback'), $text);
1132
1133 return $text;
1134 }
1135 function _doBlockQuotes_callback($matches) {
1136 $bq = $matches[1];
1137 # trim one level of quoting - trim whitespace-only lines
1138 $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq);
1139 $bq = $this->runBlockGamut($bq); # recurse
1140
1141 $bq = preg_replace('/^/m', " ", $bq);
1142 # These leading spaces cause problem with <pre> content,
1143 # so we need to fix that:
1144 $bq = preg_replace_callback('{(\s*<pre>.+?</pre>)}sx',
1145 array(&$this, '_DoBlockQuotes_callback2'), $bq);
1146
1147 return "\n". $this->hashBlock("<blockquote>\n$bq\n</blockquote>")."\n\n";
1148 }
1149 function _doBlockQuotes_callback2($matches) {
1150 $pre = $matches[1];
1151 $pre = preg_replace('/^ /m', '', $pre);
1152 return $pre;
1153 }
1154
1155
1156 function formParagraphs($text) {
1157 #
1158 # Params:
1159 # $text - string to process with html <p> tags
1160 #
1161 # Strip leading and trailing lines:
1162 $text = preg_replace('/\A\n+|\n+\z/', '', $text);
1163
1164 $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
1165
1166 #
1167 # Wrap <p> tags and unhashify HTML blocks
1168 #
1169 foreach ($grafs as $key => $value) {
1170 if (!preg_match('/^B\x1A[0-9]+B$/', $value)) {
1171 # Is a paragraph.
1172 $value = $this->runSpanGamut($value);
1173 $value = preg_replace('/^([ ]*)/', "<p>", $value);
1174 $value .= "</p>";
1175 $grafs[$key] = $this->unhash($value);
1176 }
1177 else {
1178 # Is a block.
1179 # Modify elements of @grafs in-place...
1180 $graf = $value;
1181 $block = $this->html_hashes[$graf];
1182 $graf = $block;
1183 // if (preg_match('{
1184 // \A
1185 // ( # $1 = <div> tag
1186 // <div \s+
1187 // [^>]*
1188 // \b
1189 // markdown\s*=\s* ([\'"]) # $2 = attr quote char
1190 // 1
1191 // \2
1192 // [^>]*
1193 // >
1194 // )
1195 // ( # $3 = contents
1196 // .*
1197 // )
1198 // (</div>) # $4 = closing tag
1199 // \z
1200 // }xs', $block, $matches))
1201 // {
1202 // list(, $div_open, , $div_content, $div_close) = $matches;
1203 //
1204 // # We can't call Markdown(), because that resets the hash;
1205 // # that initialization code should be pulled into its own sub, though.
1206 // $div_content = $this->hashHTMLBlocks($div_content);
1207 //
1208 // # Run document gamut methods on the content.
1209 // foreach ($this->document_gamut as $method => $priority) {
1210 // $div_content = $this->$method($div_content);
1211 // }
1212 //
1213 // $div_open = preg_replace(
1214 // '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open);
1215 //
1216 // $graf = $div_open . "\n" . $div_content . "\n" . $div_close;
1217 // }
1218 $grafs[$key] = $graf;
1219 }
1220 }
1221
1222 return implode("\n\n", $grafs);
1223 }
1224
1225
1226 function encodeAttribute($text) {
1227 #
1228 # Encode text for a double-quoted HTML attribute. This function
1229 # is *not* suitable for attributes enclosed in single quotes.
1230 #
1231 $text = $this->encodeAmpsAndAngles($text);
1232 $text = str_replace('"', '&quot;', $text);
1233 return $text;
1234 }
1235
1236
1237 function encodeAmpsAndAngles($text) {
1238 #
1239 # Smart processing for ampersands and angle brackets that need to
1240 # be encoded. Valid character entities are left alone unless the
1241 # no-entities mode is set.
1242 #
1243 if ($this->no_entities) {
1244 $text = str_replace('&', '&amp;', $text);
1245 } else {
1246 # Ampersand-encoding based entirely on Nat Irons's Amputator
1247 # MT plugin: <http://bumppo.net/projects/amputator/>
1248 $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/',
1249 '&amp;', $text);;
1250 }
1251 # Encode remaining <'s
1252 $text = str_replace('<', '&lt;', $text);
1253
1254 return $text;
1255 }
1256
1257
1258 function doAutoLinks($text) {
1259 $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i',
1260 array(&$this, '_doAutoLinks_url_callback'), $text);
1261
1262 # Email addresses: <[email protected]>
1263 $text = preg_replace_callback('{
1264 <
1265 (?:mailto:)?
1266 (
1267 [-.\w\x80-\xFF]+
1268 \@
1269 [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+
1270 )
1271 >
1272 }xi',
1273 array(&$this, '_doAutoLinks_email_callback'), $text);
1274
1275 return $text;
1276 }
1277 function _doAutoLinks_url_callback($matches) {
1278 $url = $this->encodeAttribute($matches[1]);
1279 $link = "<a href=\"$url\">$url</a>";
1280 return $this->hashPart($link);
1281 }
1282 function _doAutoLinks_email_callback($matches) {
1283 $address = $matches[1];
1284 $link = $this->encodeEmailAddress($address);
1285 return $this->hashPart($link);
1286 }
1287
1288
1289 function encodeEmailAddress($addr) {
1290 #
1291 # Input: an email address, e.g., "[email protected]"
1292 #
1293 # Output: the email address as a mailto link, with each character
1294 # of the address encoded as either a decimal or hex entity, in
1295 # the hopes of foiling most address harvesting spam bots. E.g.:
1296 #
1297 # <p><a href="&#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111;
1298 # &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111;
1299 # &#x6d;">&#x66;o&#111;&#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;
1300 # &#101;&#46;&#x63;&#111;&#x6d;</a></p>
1301 #
1302 # Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
1303 # With some optimizations by Milian Wolff.
1304 #
1305 $addr = "mailto:" . $addr;
1306 $chars = preg_split('/(?<!^)(?!$)/', $addr);
1307 $seed = (int)abs(crc32($addr) / strlen($addr)); # Deterministic seed.
1308
1309 foreach ($chars as $key => $char) {
1310 $ord = ord($char);
1311 # Ignore non-ascii chars.
1312 if ($ord < 128) {
1313 $r = ($seed * (1 + $key)) % 100; # Pseudo-random function.
1314 # roughly 10% raw, 45% hex, 45% dec
1315 # '@' *must* be encoded. I insist.
1316 if ($r > 90 && $char != '@') /* do nothing */;
1317 else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';';
1318 else $chars[$key] = '&#'.$ord.';';
1319 }
1320 }
1321
1322 $addr = implode('', $chars);
1323 $text = implode('', array_slice($chars, 7)); # text without `mailto:`
1324 $addr = "<a href=\"$addr\">$text</a>";
1325
1326 return $addr;
1327 }
1328
1329
1330 function parseSpan($str) {
1331 #
1332 # Take the string $str and parse it into tokens, hashing embeded HTML,
1333 # escaped characters and handling code spans.
1334 #
1335 $output = '';
1336
1337 $span_re = '{
1338 (
1339 \\\\'.$this->escape_chars_re.'
1340 |
1341 (?<![`\\\\])
1342 `+ # code span marker
1343 '.( $this->no_markup ? '' : '
1344 |
1345 <!-- .*? --> # comment
1346 |
1347 <\?.*?\?> | <%.*?%> # processing instruction
1348 |
1349 <[/!$]?[-a-zA-Z0-9:]+ # regular tags
1350 (?>
1351 \s
1352 (?>[^"\'>]+|"[^"]*"|\'[^\']*\')*
1353 )?
1354 >
1355 ').'
1356 )
1357 }xs';
1358
1359 while (1) {
1360 #
1361 # Each loop iteration seach for either the next tag, the next
1362 # openning code span marker, or the next escaped character.
1363 # Each token is then passed to handleSpanToken.
1364 #
1365 $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE);
1366
1367 # Create token from text preceding tag.
1368 if ($parts[0] != "") {
1369 $output .= $parts[0];
1370 }
1371
1372 # Check if we reach the end.
1373 if (isset($parts[1])) {
1374 $output .= $this->handleSpanToken($parts[1], $parts[2]);
1375 $str = $parts[2];
1376 }
1377 else {
1378 break;
1379 }
1380 }
1381
1382 return $output;
1383 }
1384
1385
1386 function handleSpanToken($token, &$str) {
1387 #
1388 # Handle $token provided by parseSpan by determining its nature and
1389 # returning the corresponding value that should replace it.
1390 #
1391 switch ($token[0]) {
1392 case "\\":
1393 return $this->hashPart("&#". ord($token[1]). ";");
1394 case "`":
1395 # Search for end marker in remaining text.
1396 if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm',
1397 $str, $matches))
1398 {
1399 $str = $matches[2];
1400 $codespan = $this->makeCodeSpan($matches[1]);
1401 return $this->hashPart($codespan);
1402 }
1403 return $token; // return as text since no ending marker found.
1404 default:
1405 return $this->hashPart($token);
1406 }
1407 }
1408
1409
1410 function outdent($text) {
1411 #
1412 # Remove one level of line-leading tabs or spaces
1413 #
1414 return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text);
1415 }
1416
1417
1418 # String length function for detab. `_initDetab` will create a function to
1419 # hanlde UTF-8 if the default function does not exist.
1420 var $utf8_strlen = 'mb_strlen';
1421
1422 function detab($text) {
1423 #
1424 # Replace tabs with the appropriate amount of space.
1425 #
1426 # For each line we separate the line in blocks delemited by
1427 # tab characters. Then we reconstruct every line by adding the
1428 # appropriate number of space between each blocks.
1429
1430 $text = preg_replace_callback('/^.*\t.*$/m',
1431 array(&$this, '_detab_callback'), $text);
1432
1433 return $text;
1434 }
1435 function _detab_callback($matches) {
1436 $line = $matches[0];
1437 $strlen = $this->utf8_strlen; # strlen function for UTF-8.
1438
1439 # Split in blocks.
1440 $blocks = explode("\t", $line);
1441 # Add each blocks to the line.
1442 $line = $blocks[0];
1443 unset($blocks[0]); # Do not add first block twice.
1444 foreach ($blocks as $block) {
1445 # Calculate amount of space, insert spaces, insert block.
1446 $amount = $this->tab_width -
1447 $strlen($line, 'UTF-8') % $this->tab_width;
1448 $line .= str_repeat(" ", $amount) . $block;
1449 }
1450 return $line;
1451 }
1452 function _initDetab() {
1453 #
1454 # Check for the availability of the function in the `utf8_strlen` property
1455 # (initially `mb_strlen`). If the function is not available, create a
1456 # function that will loosely count the number of UTF-8 characters with a
1457 # regular expression.
1458 #
1459 if (function_exists($this->utf8_strlen)) return;
1460 //260816 create_function() was removed in PHP 8; use a PHP 5.3+ closure for this fallback.
1461 $this->utf8_strlen = function($text) {
1462 return preg_match_all(
1463 "/[\\x00-\\xBF]|[\\xC0-\\xFF][\\x80-\\xBF]*/",
1464 $text, $m);
1465 };
1466 }
1467
1468
1469 function unhash($text) {
1470 #
1471 # Swap back in all the tags hashed by _HashHTMLBlocks.
1472 #
1473 return preg_replace_callback('/(.)\x1A[0-9]+\1/',
1474 array(&$this, '_unhash_callback'), $text);
1475 }
1476 function _unhash_callback($matches) {
1477 return $this->html_hashes[$matches[0]];
1478 }
1479
1480 }
1481