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

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

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