Passed
Push — lib ( 93fa68...4db3e9 )
by Michel
01:23
created

MarkdownExtra::parseFootnotePlaceholders()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
crap 1
1
<?php
2
/**
3
 * Markdown Extra - A text-to-HTML conversion tool for web writers
4
 *
5
 * @package   php-markdown
6
 * @author    Michel Fortin <[email protected]>
7
 * @copyright 2004-2018 Michel Fortin <https://michelf.com/projects/php-markdown/>
8
 * @copyright (Original Markdown) 2004-2006 John Gruber <https://daringfireball.net/projects/markdown/>
9
 */
10
11
namespace Michelf;
12
13
/**
14
 * Markdown Extra Parser Class
15
 */
16
class MarkdownExtra extends \Michelf\Markdown {
17
	/**
18
	 * Configuration variables
19
	 */
20
21
	/**
22
	 * Prefix for footnote ids.
23
	 * @var string
24
	 */
25
	public $fn_id_prefix = "";
26
27
	/**
28
	 * Optional title attribute for footnote links.
29
	 * @var string
30
	 */
31
	public $fn_link_title = "";
32
33
	/**
34
	 * Optional class attribute for footnote links and backlinks.
35
	 * @var string
36
	 */
37
	public $fn_link_class     = "footnote-ref";
38
	public $fn_backlink_class = "footnote-backref";
39
40
	/**
41
	 * Content to be displayed within footnote backlinks. The default is '↩';
42
	 * the U+FE0E on the end is a Unicode variant selector used to prevent iOS
43
	 * from displaying the arrow character as an emoji.
44
	 * Optionally use '^^' and '%%' to refer to the footnote number and
45
	 * reference number respectively. {@see parseFootnotePlaceholders()}
46
	 * @var string
47
	 */
48
	public $fn_backlink_html = '&#8617;&#xFE0E;';
49
50
	/**
51
	 * Optional title and aria-label attributes for footnote backlinks for
52
	 * added accessibility (to ensure backlink uniqueness).
53
	 * Use '^^' and '%%' to refer to the footnote number and reference number
54
	 * respectively. {@see parseFootnotePlaceholders()}
55
	 * @var string
56
	 */
57
	public $fn_backlink_title = "";
58
	public $fn_backlink_label = "";
59
60
	/**
61
	 * Class name for table cell alignment (%% replaced left/center/right)
62
	 * For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center'
63
	 * If empty, the align attribute is used instead of a class name.
64
	 * @var string
65
	 */
66
	public $table_align_class_tmpl = '';
67
68
	/**
69
	 * Optional class prefix for fenced code block.
70
	 * @var string
71
	 */
72
	public $code_class_prefix = "";
73
74
	/**
75
	 * Class attribute for code blocks goes on the `code` tag;
76
	 * setting this to true will put attributes on the `pre` tag instead.
77
	 * @var boolean
78
	 */
79
	public $code_attr_on_pre = false;
80
81
	/**
82
	 * Predefined abbreviations.
83
	 * @var array
84
	 */
85
	public $predef_abbr = array();
86
87
	/**
88
	 * Only convert atx-style headers if there's a space between the header and #
89
	 * @var boolean
90
	 */
91
	public $hashtag_protection = false;
92
93
	/**
94
	 * Determines whether footnotes should be appended to the end of the document.
95
	 * If true, footnote html can be retrieved from $this->footnotes_assembled.
96
	 * @var boolean
97
	 */
98
	public $omit_footnotes = false;
99
100
101
	/**
102
	 * After parsing, the HTML for the list of footnotes appears here.
103
	 * This is available only if $omit_footnotes == true.
104
	 *
105
	 * Note: when placing the content of `footnotes_assembled` on the page,
106
	 * consider adding the attribute `role="doc-endnotes"` to the `div` or
107
	 * `section` that will enclose the list of footnotes so they are
108
	 * reachable to accessibility tools the same way they would be with the
109
	 * default HTML output.
110
	 * @var null|string
111
	 */
112
	public $footnotes_assembled = null;
113
114
	/**
115
	 * Parser implementation
116
	 */
117
118
	/**
119
	 * Constructor function. Initialize the parser object.
120
	 * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
121
	 */
122 4
	public function __construct() {
123
		// Add extra escapable characters before parent constructor
124
		// initialize the table.
125 4
		$this->escape_chars .= ':|';
126
127
		// Insert extra document, block, and span transformations.
128
		// Parent constructor will do the sorting.
129 4
		$this->document_gamut += array(
130
			"doFencedCodeBlocks" => 5,
131
			"stripFootnotes"     => 15,
132
			"stripAbbreviations" => 25,
133
			"appendFootnotes"    => 50,
134
		);
135 4
		$this->block_gamut += array(
136
			"doFencedCodeBlocks" => 5,
137
			"doTables"           => 15,
138
			"doDefLists"         => 45,
139
		);
140 4
		$this->span_gamut += array(
141
			"doFootnotes"        => 5,
142
			"doAbbreviations"    => 70,
143
		);
144
145 4
		$this->enhanced_ordered_list = true;
146 4
		parent::__construct();
147 4
	}
148
149
150
	/**
151
	 * Extra variables used during extra transformations.
152
	 * @var array
153
	 */
154
	protected $footnotes = array();
155
	protected $footnotes_ordered = array();
156
	protected $footnotes_ref_count = array();
157
	protected $footnotes_numbers = array();
158
	protected $abbr_desciptions = array();
159
	/** @var string */
160
	protected $abbr_word_re = '';
161
162
	/**
163
	 * Give the current footnote number.
164
	 * @var integer
165
	 */
166
	protected $footnote_counter = 1;
167
168
    /**
169
     * Ref attribute for links
170
     * @var array
171
     */
172
	protected $ref_attr = array();
173
174
	/**
175
	 * Setting up Extra-specific variables.
176
	 */
177 62
	protected function setup() {
178 62
		parent::setup();
179
180 62
		$this->footnotes = array();
181 62
		$this->footnotes_ordered = array();
182 62
		$this->footnotes_ref_count = array();
183 62
		$this->footnotes_numbers = array();
184 62
		$this->abbr_desciptions = array();
185 62
		$this->abbr_word_re = '';
186 62
		$this->footnote_counter = 1;
187 62
		$this->footnotes_assembled = null;
188
189 62
		foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
190 2
			if ($this->abbr_word_re)
191 1
				$this->abbr_word_re .= '|';
192 2
			$this->abbr_word_re .= preg_quote($abbr_word);
193 2
			$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
194
		}
195 62
	}
196
197
	/**
198
	 * Clearing Extra-specific variables.
199
	 */
200 62
	protected function teardown() {
201 62
		$this->footnotes = array();
202 62
		$this->footnotes_ordered = array();
203 62
		$this->footnotes_ref_count = array();
204 62
		$this->footnotes_numbers = array();
205 62
		$this->abbr_desciptions = array();
206 62
		$this->abbr_word_re = '';
207
208 62
		if ( ! $this->omit_footnotes )
209 62
			$this->footnotes_assembled = null;
210
211 62
		parent::teardown();
212 62
	}
213
214
215
	/**
216
	 * Extra attribute parser
217
	 */
218
219
	/**
220
	 * Expression to use to catch attributes (includes the braces)
221
	 * @var string
222
	 */
223
	protected $id_class_attr_catch_re = '\{((?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}';
224
225
	/**
226
	 * Expression to use when parsing in a context when no capture is desired
227
	 * @var string
228
	 */
229
	protected $id_class_attr_nocatch_re = '\{(?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}';
230
231
	/**
232
	 * Parse attributes caught by the $this->id_class_attr_catch_re expression
233
	 * and return the HTML-formatted list of attributes.
234
	 *
235
	 * Currently supported attributes are .class and #id.
236
	 *
237
	 * In addition, this method also supports supplying a default Id value,
238
	 * which will be used to populate the id attribute in case it was not
239
	 * overridden.
240
	 * @param  string $tag_name
241
	 * @param  string $attr
242
	 * @param  mixed  $defaultIdValue
243
	 * @param  array  $classes
244
	 * @return string
245
	 */
246 29
	protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = array()) {
0 ignored issues
show
Unused Code introduced by
The parameter $tag_name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
247 29
		if (empty($attr) && !$defaultIdValue && empty($classes)) {
248 27
			return "";
249
		}
250
251
		// Split on components
252 6
		preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches);
253 6
		$elements = $matches[0];
254
255
		// Handle classes and IDs (only first ID taken into account)
256 6
		$attributes = array();
257 6
		$id = false;
258 6
		foreach ($elements as $element) {
259 4
			if ($element{0} === '.') {
260 4
				$classes[] = substr($element, 1);
261 4
			} else if ($element{0} === '#') {
262 4
				if ($id === false) $id = substr($element, 1);
263 1
			} else if (strpos($element, '=') > 0) {
264 1
				$parts = explode('=', $element, 2);
265 4
				$attributes[] = $parts[0] . '="' . $parts[1] . '"';
266
			}
267
		}
268
269 6
		if ($id === false || $id === '') {
270 5
			$id = $defaultIdValue;
271
		}
272
273
		// Compose attributes as string
274 6
		$attr_str = "";
275 6
		if (!empty($id)) {
276 4
			$attr_str .= ' id="'.$this->encodeAttribute($id) .'"';
277
		}
278 6
		if (!empty($classes)) {
279 6
			$attr_str .= ' class="'. implode(" ", $classes) . '"';
280
		}
281 6
		if (!$this->no_markup && !empty($attributes)) {
282 1
			$attr_str .= ' '.implode(" ", $attributes);
283
		}
284 6
		return $attr_str;
285
	}
286
287
	/**
288
	 * Strips link definitions from text, stores the URLs and titles in
289
	 * hash references.
290
	 * @param  string $text
291
	 * @return string
292
	 */
293 62 View Code Duplication
	protected function stripLinkDefinitions($text) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
294 62
		$less_than_tab = $this->tab_width - 1;
295
296
		// Link defs are in the form: ^[id]: url "optional title"
297 62
		$text = preg_replace_callback('{
298 62
							^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?:	# id = $1
299
							  [ ]*
300
							  \n?				# maybe *one* newline
301
							  [ ]*
302
							(?:
303
							  <(.+?)>			# url = $2
304
							|
305
							  (\S+?)			# url = $3
306
							)
307
							  [ ]*
308
							  \n?				# maybe one newline
309
							  [ ]*
310
							(?:
311
								(?<=\s)			# lookbehind for whitespace
312
								["(]
313
								(.*?)			# title = $4
314
								[")]
315
								[ ]*
316
							)?	# title is optional
317 62
					(?:[ ]* '.$this->id_class_attr_catch_re.' )?  # $5 = extra id & class attr
318
							(?:\n+|\Z)
319
			}xm',
320 62
			array($this, '_stripLinkDefinitions_callback'),
321 62
			$text);
322 62
		return $text;
323
	}
324
325
	/**
326
	 * Strip link definition callback
327
	 * @param  array $matches
328
	 * @return string
329
	 */
330 11
	protected function _stripLinkDefinitions_callback($matches) {
331 11
		$link_id = strtolower($matches[1]);
332 11
		$url = $matches[2] == '' ? $matches[3] : $matches[2];
333 11
		$this->urls[$link_id] = $url;
334 11
		$this->titles[$link_id] =& $matches[4];
335 11
		$this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]);
336 11
		return ''; // String that will replace the block
337
	}
338
339
340
	/**
341
	 * HTML block parser
342
	 */
343
344
	/**
345
	 * Tags that are always treated as block tags
346
	 * @var string
347
	 */
348
	protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure';
349
350
	/**
351
	 * Tags treated as block tags only if the opening tag is alone on its line
352
	 * @var string
353
	 */
354
	protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
355
356
	/**
357
	 * Tags where markdown="1" default to span mode:
358
	 * @var string
359
	 */
360
	protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
361
362
	/**
363
	 * Tags which must not have their contents modified, no matter where
364
	 * they appear
365
	 * @var string
366
	 */
367
	protected $clean_tags_re = 'script|style|math|svg';
368
369
	/**
370
	 * Tags that do not need to be closed.
371
	 * @var string
372
	 */
373
	protected $auto_close_tags_re = 'hr|img|param|source|track';
374
375
	/**
376
	 * Hashify HTML Blocks and "clean tags".
377
	 *
378
	 * We only want to do this for block-level HTML tags, such as headers,
379
	 * lists, and tables. That's because we still want to wrap <p>s around
380
	 * "paragraphs" that are wrapped in non-block-level tags, such as anchors,
381
	 * phrase emphasis, and spans. The list of tags we're looking for is
382
	 * hard-coded.
383
	 *
384
	 * This works by calling _HashHTMLBlocks_InMarkdown, which then calls
385
	 * _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
386
	 * attribute is found within a tag, _HashHTMLBlocks_InHTML calls back
387
	 *  _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
388
	 * These two functions are calling each other. It's recursive!
389
	 * @param  string $text
390
	 * @return string
391
	 */
392 62
	protected function hashHTMLBlocks($text) {
393 62
		if ($this->no_markup) {
394 1
			return $text;
395
		}
396
397
		// Call the HTML-in-Markdown hasher.
398 61
		list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
399
400 61
		return $text;
401
	}
402
403
	/**
404
	 * Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
405
	 *
406
	 * *   $indent is the number of space to be ignored when checking for code
407
	 *     blocks. This is important because if we don't take the indent into
408
	 *     account, something like this (which looks right) won't work as expected:
409
	 *
410
	 *     <div>
411
	 *         <div markdown="1">
412
	 *         Hello World.  <-- Is this a Markdown code block or text?
413
	 *         </div>  <-- Is this a Markdown code block or a real tag?
414
	 *     <div>
415
	 *
416
	 *     If you don't like this, just don't indent the tag on which
417
	 *     you apply the markdown="1" attribute.
418
	 *
419
	 * *   If $enclosing_tag_re is not empty, stops at the first unmatched closing
420
	 *     tag with that name. Nested tags supported.
421
	 *
422
	 * *   If $span is true, text inside must treated as span. So any double
423
	 *     newline will be replaced by a single newline so that it does not create
424
	 *     paragraphs.
425
	 *
426
	 * Returns an array of that form: ( processed text , remaining text )
427
	 *
428
	 * @param  string  $text
429
	 * @param  integer $indent
430
	 * @param  string  $enclosing_tag_re
431
	 * @param  boolean $span
432
	 * @return array
433
	 */
434 61
	protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
435
										$enclosing_tag_re = '', $span = false)
436
	{
437
438 61
		if ($text === '') return array('', '');
439
440
		// Regex to check for the presense of newlines around a block tag.
441 61
		$newline_before_re = '/(?:^\n?|\n\n)*$/';
442
		$newline_after_re =
443 61
			'{
444
				^						# Start of text following the tag.
445
				(?>[ ]*<!--.*?-->)?		# Optional comment.
446
				[ ]*\n					# Must be followed by newline.
447
			}xs';
448
449
		// Regex to match any tag.
450
		$block_tag_re =
451
			'{
452
				(					# $2: Capture whole tag.
453
					</?					# Any opening or closing tag.
454
						(?>				# Tag name.
455 61
							' . $this->block_tags_re . '			|
456 61
							' . $this->context_block_tags_re . '	|
457 61
							' . $this->clean_tags_re . '        	|
458 61
							(?!\s)'.$enclosing_tag_re . '
459
						)
460
						(?:
461
							(?=[\s"\'/a-zA-Z0-9])	# Allowed characters after tag name.
462
							(?>
463
								".*?"		|	# Double quotes (can contain `>`)
464
								\'.*?\'   	|	# Single quotes (can contain `>`)
465
								.+?				# Anything but quotes and `>`.
466
							)*?
467
						)?
468
					>					# End of tag.
469
				|
470
					<!--    .*?     -->	# HTML Comment
471
				|
472
					<\?.*?\?> | <%.*?%>	# Processing instruction
473
				|
474
					<!\[CDATA\[.*?\]\]>	# CData Block
475 61
				' . ( !$span ? ' # If not in span.
476
				|
477
					# Indented code block
478
					(?: ^[ ]*\n | ^ | \n[ ]*\n )
479 61
					[ ]{' . ($indent + 4) . '}[^\n]* \n
480
					(?>
481 61
						(?: [ ]{' . ($indent + 4) . '}[^\n]* | [ ]* ) \n
482
					)*
483
				|
484
					# Fenced code block marker
485
					(?<= ^ | \n )
486 61
					[ ]{0,' . ($indent + 3) . '}(?:~{3,}|`{3,})
487
					[ ]*
488
					(?: \.?[-_:a-zA-Z0-9]+ )? # standalone class name
489
					[ ]*
490 61
					(?: ' . $this->id_class_attr_nocatch_re . ' )? # extra attributes
491
					[ ]*
492
					(?= \n )
493 61
				' : '' ) . ' # End (if not is span).
494
				|
495
					# Code span marker
496
					# Note, this regex needs to go after backtick fenced
497
					# code blocks but it should also be kept outside of the
498
					# "if not in span" condition adding backticks to the parser
499
					`+
500
				)
501
			}xs';
502
503
504 61
		$depth = 0;		// Current depth inside the tag tree.
505 61
		$parsed = "";	// Parsed text that will be returned.
506
507
		// Loop through every tag until we find the closing tag of the parent
508
		// or loop until reaching the end of text if no parent tag specified.
509
		do {
510
			// Split the text using the first $tag_match pattern found.
511
			// Text before  pattern will be first in the array, text after
512
			// pattern will be at the end, and between will be any catches made
513
			// by the pattern.
514 61
			$parts = preg_split($block_tag_re, $text, 2,
515 61
								PREG_SPLIT_DELIM_CAPTURE);
516
517
			// If in Markdown span mode, add a empty-string span-level hash
518
			// after each newline to prevent triggering any block element.
519 61
			if ($span) {
520 1
				$void = $this->hashPart("", ':');
521 1
				$newline = "\n$void";
522 1
				$parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
523
			}
524
525 61
			$parsed .= $parts[0]; // Text before current tag.
526
527
			// If end of $text has been reached. Stop loop.
528 61
			if (count($parts) < 3) {
529 61
				$text = "";
530 61
				break;
531
			}
532
533 39
			$tag  = $parts[1]; // Tag to handle.
534 39
			$text = $parts[2]; // Remaining text after current tag.
535
536
			// Check for: Fenced code block marker.
537
			// Note: need to recheck the whole tag to disambiguate backtick
538
			// fences from code spans
539 39
			if (preg_match('{^\n?([ ]{0,' . ($indent + 3) . '})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+)?[ ]*(?:' . $this->id_class_attr_nocatch_re . ')?[ ]*\n?$}', $tag, $capture)) {
540
				// Fenced code block marker: find matching end marker.
541 4
				$fence_indent = strlen($capture[1]); // use captured indent in re
542 4
				$fence_re = $capture[2]; // use captured fence in re
543 4 View Code Duplication
				if (preg_match('{^(?>.*\n)*?[ ]{' . ($fence_indent) . '}' . $fence_re . '[ ]*(?:\n|$)}', $text,
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
544 4
					$matches))
545
				{
546
					// End marker found: pass text unchanged until marker.
547 4
					$parsed .= $tag . $matches[0];
548 4
					$text = substr($text, strlen($matches[0]));
549
				}
550
				else {
551
					// No end marker: just skip it.
552 4
					$parsed .= $tag;
553
				}
554
			}
555
			// Check for: Indented code block.
556 39
			else if ($tag{0} === "\n" || $tag{0} === " ") {
557
				// Indented code block: pass it unchanged, will be handled
558
				// later.
559 23
				$parsed .= $tag;
560
			}
561
			// Check for: Code span marker
562
			// Note: need to check this after backtick fenced code blocks
563 27
			else if ($tag{0} === "`") {
564
				// Find corresponding end marker.
565 13
				$tag_re = preg_quote($tag);
566 13 View Code Duplication
				if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)' . $tag_re . '(?!`)}',
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
567 13
					$text, $matches))
568
				{
569
					// End marker found: pass text unchanged until marker.
570 12
					$parsed .= $tag . $matches[0];
571 12
					$text = substr($text, strlen($matches[0]));
572
				}
573
				else {
574
					// Unmatched marker: just skip it.
575 13
					$parsed .= $tag;
576
				}
577
			}
578
			// Check for: Opening Block level tag or
579
			//            Opening Context Block tag (like ins and del)
580
			//               used as a block tag (tag is alone on it's line).
581 24
			else if (preg_match('{^<(?:' . $this->block_tags_re . ')\b}', $tag) ||
582 19
				(	preg_match('{^<(?:' . $this->context_block_tags_re . ')\b}', $tag) &&
583 19
					preg_match($newline_before_re, $parsed) &&
584 24
					preg_match($newline_after_re, $text)	)
585
				)
586
			{
587
				// Need to parse tag and following text using the HTML parser.
588
				list($block_text, $text) =
589 10
					$this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
590
591
				// Make sure it stays outside of any paragraph by adding newlines.
592 10
				$parsed .= "\n\n$block_text\n\n";
593
			}
594
			// Check for: Clean tag (like script, math)
595
			//            HTML Comments, processing instructions.
596 19
			else if (preg_match('{^<(?:' . $this->clean_tags_re . ')\b}', $tag) ||
597 19
				$tag{1} === '!' || $tag{1} === '?')
598
			{
599
				// Need to parse tag and following text using the HTML parser.
600
				// (don't check for markdown attribute)
601
				list($block_text, $text) =
602 3
					$this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
603
604 3
				$parsed .= $block_text;
605
			}
606
			// Check for: Tag with same name as enclosing tag.
607 16
			else if ($enclosing_tag_re !== '' &&
608
				// Same name as enclosing tag.
609 16
				preg_match('{^</?(?:' . $enclosing_tag_re . ')\b}', $tag))
610
			{
611
				// Increase/decrease nested tag count.
612 3 View Code Duplication
				if ($tag{1} === '/') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
613 3
					$depth--;
614
				} else if ($tag{strlen($tag)-2} !== '/') {
615
					$depth++;
616
				}
617
618 3
				if ($depth < 0) {
619
					// Going out of parent element. Clean up and break so we
620
					// return to the calling function.
621 3
					$text = $tag . $text;
622 3
					break;
623
				}
624
625
				$parsed .= $tag;
626
			}
627
			else {
628 13
				$parsed .= $tag;
629
			}
630 39
		} while ($depth >= 0);
631
632 61
		return array($parsed, $text);
633
	}
634
635
	/**
636
	 * Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
637
	 *
638
	 * *   Calls $hash_method to convert any blocks.
639
	 * *   Stops when the first opening tag closes.
640
	 * *   $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
641
	 *     (it is not inside clean tags)
642
	 *
643
	 * Returns an array of that form: ( processed text , remaining text )
644
	 * @param  string $text
645
	 * @param  string $hash_method
646
	 * @param  bool $md_attr Handle `markdown="1"` attribute
647
	 * @return array
648
	 */
649 12
	protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
650 12
		if ($text === '') return array('', '');
651
652
		// Regex to match `markdown` attribute inside of a tag.
653 12
		$markdown_attr_re = '
654
			{
655
				\s*			# Eat whitespace before the `markdown` attribute
656
				markdown
657
				\s*=\s*
658
				(?>
659
					(["\'])		# $1: quote delimiter
660
					(.*?)		# $2: attribute value
661
					\1			# matching delimiter
662
				|
663
					([^\s>]*)	# $3: unquoted attribute value
664
				)
665
				()				# $4: make $3 always defined (avoid warnings)
666
			}xs';
667
668
		// Regex to match any tag.
669 12
		$tag_re = '{
670
				(					# $2: Capture whole tag.
671
					</?					# Any opening or closing tag.
672
						[\w:$]+			# Tag name.
673
						(?:
674
							(?=[\s"\'/a-zA-Z0-9])	# Allowed characters after tag name.
675
							(?>
676
								".*?"		|	# Double quotes (can contain `>`)
677
								\'.*?\'   	|	# Single quotes (can contain `>`)
678
								.+?				# Anything but quotes and `>`.
679
							)*?
680
						)?
681
					>					# End of tag.
682
				|
683
					<!--    .*?     -->	# HTML Comment
684
				|
685
					<\?.*?\?> | <%.*?%>	# Processing instruction
686
				|
687
					<!\[CDATA\[.*?\]\]>	# CData Block
688
				)
689
			}xs';
690
691 12
		$original_text = $text;		// Save original text in case of faliure.
692
693 12
		$depth		= 0;	// Current depth inside the tag tree.
694 12
		$block_text	= "";	// Temporary text holder for current text.
695 12
		$parsed		= "";	// Parsed text that will be returned.
696 12
		$base_tag_name_re = '';
697
698
		// Get the name of the starting tag.
699
		// (This pattern makes $base_tag_name_re safe without quoting.)
700 12
		if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
701 10
			$base_tag_name_re = $matches[1];
702
703
		// Loop through every tag until we find the corresponding closing tag.
704
		do {
705
			// Split the text using the first $tag_match pattern found.
706
			// Text before  pattern will be first in the array, text after
707
			// pattern will be at the end, and between will be any catches made
708
			// by the pattern.
709 12
			$parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
710
711 12
			if (count($parts) < 3) {
712
				// End of $text reached with unbalenced tag(s).
713
				// In that case, we return original text unchanged and pass the
714
				// first character as filtered to prevent an infinite loop in the
715
				// parent function.
716
				return array($original_text{0}, substr($original_text, 1));
717
			}
718
719 12
			$block_text .= $parts[0]; // Text before current tag.
720 12
			$tag         = $parts[1]; // Tag to handle.
721 12
			$text        = $parts[2]; // Remaining text after current tag.
722
723
			// Check for: Auto-close tag (like <hr/>)
724
			//			 Comments and Processing Instructions.
725 12
			if (preg_match('{^</?(?:' . $this->auto_close_tags_re . ')\b}', $tag) ||
726 12
				$tag{1} === '!' || $tag{1} === '?')
727
			{
728
				// Just add the tag to the block as if it was text.
729 4
				$block_text .= $tag;
730
			}
731
			else {
732
				// Increase/decrease nested tag count. Only do so if
733
				// the tag's name match base tag's.
734 10
				if (preg_match('{^</?' . $base_tag_name_re . '\b}', $tag)) {
735 10 View Code Duplication
					if ($tag{1} === '/') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
736 10
						$depth--;
737 10
					} else if ($tag{strlen($tag)-2} !== '/') {
738 10
						$depth++;
739
					}
740
				}
741
742
				// Check for `markdown="1"` attribute and handle it.
743 10
				if ($md_attr &&
744 10
					preg_match($markdown_attr_re, $tag, $attr_m) &&
745 10
					preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
746
				{
747
					// Remove `markdown` attribute from opening tag.
748 3
					$tag = preg_replace($markdown_attr_re, '', $tag);
749
750
					// Check if text inside this tag must be parsed in span mode.
751 3
					$mode = $attr_m[2] . $attr_m[3];
752 3
					$span_mode = $mode === 'span' || ($mode !== 'block' &&
753 3
						preg_match('{^<(?:' . $this->contain_span_tags_re . ')\b}', $tag));
754
755
					// Calculate indent before tag.
756 3
					if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
757 3
						$strlen = $this->utf8_strlen;
758 3
						$indent = $strlen($matches[1], 'UTF-8');
759
					} else {
760
						$indent = 0;
761
					}
762
763
					// End preceding block with this tag.
764 3
					$block_text .= $tag;
765 3
					$parsed .= $this->$hash_method($block_text);
766
767
					// Get enclosing tag name for the ParseMarkdown function.
768
					// (This pattern makes $tag_name_re safe without quoting.)
769 3
					preg_match('/^<([\w:$]*)\b/', $tag, $matches);
770 3
					$tag_name_re = $matches[1];
771
772
					// Parse the content using the HTML-in-Markdown parser.
773
					list ($block_text, $text)
774 3
						= $this->_hashHTMLBlocks_inMarkdown($text, $indent,
775 3
							$tag_name_re, $span_mode);
776
777
					// Outdent markdown text.
778 3
					if ($indent > 0) {
779 1
						$block_text = preg_replace("/^[ ]{1,$indent}/m", "",
780 1
													$block_text);
781
					}
782
783
					// Append tag content to parsed text.
784 3
					if (!$span_mode) {
785 3
						$parsed .= "\n\n$block_text\n\n";
786
					} else {
787 1
						$parsed .= (string) $block_text;
788
					}
789
790
					// Start over with a new block.
791 3
					$block_text = "";
792
				}
793 10
				else $block_text .= $tag;
794
			}
795
796 12
		} while ($depth > 0);
797
798
		// Hash last block text that wasn't processed inside the loop.
799 12
		$parsed .= $this->$hash_method($block_text);
800
801 12
		return array($parsed, $text);
802
	}
803
804
	/**
805
	 * Called whenever a tag must be hashed when a function inserts a "clean" tag
806
	 * in $text, it passes through this function and is automaticaly escaped,
807
	 * blocking invalid nested overlap.
808
	 * @param  string $text
809
	 * @return string
810
	 */
811 3
	protected function hashClean($text) {
812 3
		return $this->hashPart($text, 'C');
813
	}
814
815
	/**
816
	 * Turn Markdown link shortcuts into XHTML <a> tags.
817
	 * @param  string $text
818
	 * @return string
819
	 */
820 62 View Code Duplication
	protected function doAnchors($text) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
821 62
		if ($this->in_anchor) {
822 15
			return $text;
823
		}
824 62
		$this->in_anchor = true;
825
826
		// First, handle reference-style links: [link text] [id]
827 62
		$text = preg_replace_callback('{
828
			(					# wrap whole match in $1
829
			  \[
830 62
				(' . $this->nested_brackets_re . ')	# link text = $2
831
			  \]
832
833
			  [ ]?				# one optional space
834
			  (?:\n[ ]*)?		# one optional newline followed by spaces
835
836
			  \[
837
				(.*?)		# id = $3
838
			  \]
839
			)
840
			}xs',
841 62
			array($this, '_doAnchors_reference_callback'), $text);
842
843
		// Next, inline-style links: [link text](url "optional title")
844 62
		$text = preg_replace_callback('{
845
			(				# wrap whole match in $1
846
			  \[
847 62
				(' . $this->nested_brackets_re . ')	# link text = $2
848
			  \]
849
			  \(			# literal paren
850
				[ \n]*
851
				(?:
852
					<(.+?)>	# href = $3
853
				|
854 62
					(' . $this->nested_url_parenthesis_re . ')	# href = $4
855
				)
856
				[ \n]*
857
				(			# $5
858
				  ([\'"])	# quote char = $6
859
				  (.*?)		# Title = $7
860
				  \6		# matching quote
861
				  [ \n]*	# ignore any spaces/tabs between closing quote and )
862
				)?			# title is optional
863
			  \)
864 62
			  (?:[ ]? ' . $this->id_class_attr_catch_re . ' )?	 # $8 = id/class attributes
865
			)
866
			}xs',
867 62
			array($this, '_doAnchors_inline_callback'), $text);
868
869
		// Last, handle reference-style shortcuts: [link text]
870
		// These must come last in case you've also got [link text][1]
871
		// or [link text](/foo)
872 62
		$text = preg_replace_callback('{
873
			(					# wrap whole match in $1
874
			  \[
875
				([^\[\]]+)		# link text = $2; can\'t contain [ or ]
876
			  \]
877
			)
878
			}xs',
879 62
			array($this, '_doAnchors_reference_callback'), $text);
880
881 62
		$this->in_anchor = false;
882 62
		return $text;
883
	}
884
885
	/**
886
	 * Callback for reference anchors
887
	 * @param  array $matches
888
	 * @return string
889
	 */
890 11
	protected function _doAnchors_reference_callback($matches) {
891 11
		$whole_match =  $matches[1];
892 11
		$link_text   =  $matches[2];
893 11
		$link_id     =& $matches[3];
894
895 11
		if ($link_id == "") {
896
			// for shortcut links like [this][] or [this].
897 7
			$link_id = $link_text;
898
		}
899
900
		// lower-case and turn embedded newlines into spaces
901 11
		$link_id = strtolower($link_id);
902 11
		$link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
903
904 11
		if (isset($this->urls[$link_id])) {
905 10
			$url = $this->urls[$link_id];
906 10
			$url = $this->encodeURLAttribute($url);
907
908 10
			$result = "<a href=\"$url\"";
909 10 View Code Duplication
			if ( isset( $this->titles[$link_id] ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
910 7
				$title = $this->titles[$link_id];
911 7
				$title = $this->encodeAttribute($title);
912 7
				$result .=  " title=\"$title\"";
913
			}
914 10
			if (isset($this->ref_attr[$link_id]))
915 10
				$result .= $this->ref_attr[$link_id];
916
917 10
			$link_text = $this->runSpanGamut($link_text);
918 10
			$result .= ">$link_text</a>";
919 10
			$result = $this->hashPart($result);
920
		}
921
		else {
922 3
			$result = $whole_match;
923
		}
924 11
		return $result;
925
	}
926
927
	/**
928
	 * Callback for inline anchors
929
	 * @param  array $matches
930
	 * @return string
931
	 */
932 12
	protected function _doAnchors_inline_callback($matches) {
933 12
		$link_text		=  $this->runSpanGamut($matches[2]);
934 12
		$url			=  $matches[3] === '' ? $matches[4] : $matches[3];
935 12
		$title			=& $matches[7];
936 12
		$attr  = $this->doExtraAttributes("a", $dummy =& $matches[8]);
937
938
		// if the URL was of the form <s p a c e s> it got caught by the HTML
939
		// tag parser and hashed. Need to reverse the process before using the URL.
940 12
		$unhashed = $this->unhash($url);
941 12
		if ($unhashed !== $url)
942 2
			$url = preg_replace('/^<(.*)>$/', '\1', $unhashed);
943
944 12
		$url = $this->encodeURLAttribute($url);
945
946 12
		$result = "<a href=\"$url\"";
947 12
		if (isset($title)) {
948 6
			$title = $this->encodeAttribute($title);
949 6
			$result .=  " title=\"$title\"";
950
		}
951 12
		$result .= $attr;
952
953 12
		$link_text = $this->runSpanGamut($link_text);
954 12
		$result .= ">$link_text</a>";
955
956 12
		return $this->hashPart($result);
957
	}
958
959
	/**
960
	 * Turn Markdown image shortcuts into <img> tags.
961
	 * @param  string $text
962
	 * @return string
963
	 */
964 62 View Code Duplication
	protected function doImages($text) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
965
		// First, handle reference-style labeled images: ![alt text][id]
966 62
		$text = preg_replace_callback('{
967
			(				# wrap whole match in $1
968
			  !\[
969 62
				(' . $this->nested_brackets_re . ')		# alt text = $2
970
			  \]
971
972
			  [ ]?				# one optional space
973
			  (?:\n[ ]*)?		# one optional newline followed by spaces
974
975
			  \[
976
				(.*?)		# id = $3
977
			  \]
978
979
			)
980
			}xs',
981 62
			array($this, '_doImages_reference_callback'), $text);
982
983
		// Next, handle inline images:  ![alt text](url "optional title")
984
		// Don't forget: encode * and _
985 62
		$text = preg_replace_callback('{
986
			(				# wrap whole match in $1
987
			  !\[
988 62
				(' . $this->nested_brackets_re . ')		# alt text = $2
989
			  \]
990
			  \s?			# One optional whitespace character
991
			  \(			# literal paren
992
				[ \n]*
993
				(?:
994
					<(\S*)>	# src url = $3
995
				|
996 62
					(' . $this->nested_url_parenthesis_re . ')	# src url = $4
997
				)
998
				[ \n]*
999
				(			# $5
1000
				  ([\'"])	# quote char = $6
1001
				  (.*?)		# title = $7
1002
				  \6		# matching quote
1003
				  [ \n]*
1004
				)?			# title is optional
1005
			  \)
1006 62
			  (?:[ ]? ' . $this->id_class_attr_catch_re . ' )?	 # $8 = id/class attributes
1007
			)
1008
			}xs',
1009 62
			array($this, '_doImages_inline_callback'), $text);
1010
1011 62
		return $text;
1012
	}
1013
1014
	/**
1015
	 * Callback for referenced images
1016
	 * @param  array $matches
1017
	 * @return string
1018
	 */
1019 3
	protected function _doImages_reference_callback($matches) {
1020 3
		$whole_match = $matches[1];
1021 3
		$alt_text    = $matches[2];
1022 3
		$link_id     = strtolower($matches[3]);
1023
1024 3
		if ($link_id === "") {
1025
			$link_id = strtolower($alt_text); // for shortcut links like ![this][].
1026
		}
1027
1028 3
		$alt_text = $this->encodeAttribute($alt_text);
1029 3
		if (isset($this->urls[$link_id])) {
1030 3
			$url = $this->encodeURLAttribute($this->urls[$link_id]);
1031 3
			$result = "<img src=\"$url\" alt=\"$alt_text\"";
1032 3 View Code Duplication
			if (isset($this->titles[$link_id])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1033 3
				$title = $this->titles[$link_id];
1034 3
				$title = $this->encodeAttribute($title);
1035 3
				$result .=  " title=\"$title\"";
1036
			}
1037 3
			if (isset($this->ref_attr[$link_id])) {
1038 3
				$result .= $this->ref_attr[$link_id];
1039
			}
1040 3
			$result .= $this->empty_element_suffix;
1041 3
			$result = $this->hashPart($result);
1042
		}
1043
		else {
1044
			// If there's no such link ID, leave intact:
1045
			$result = $whole_match;
1046
		}
1047
1048 3
		return $result;
1049
	}
1050
1051
	/**
1052
	 * Callback for inline images
1053
	 * @param  array $matches
1054
	 * @return string
1055
	 */
1056 3
	protected function _doImages_inline_callback($matches) {
1057 3
		$alt_text		= $matches[2];
1058 3
		$url			= $matches[3] === '' ? $matches[4] : $matches[3];
1059 3
		$title			=& $matches[7];
1060 3
		$attr  = $this->doExtraAttributes("img", $dummy =& $matches[8]);
1061
1062 3
		$alt_text = $this->encodeAttribute($alt_text);
1063 3
		$url = $this->encodeURLAttribute($url);
1064 3
		$result = "<img src=\"$url\" alt=\"$alt_text\"";
1065 3
		if (isset($title)) {
1066 2
			$title = $this->encodeAttribute($title);
1067 2
			$result .=  " title=\"$title\""; // $title already quoted
1068
		}
1069 3
		$result .= $attr;
1070 3
		$result .= $this->empty_element_suffix;
1071
1072 3
		return $this->hashPart($result);
1073
	}
1074
1075
	/**
1076
	 * Process markdown headers. Redefined to add ID and class attribute support.
1077
	 * @param  string $text
1078
	 * @return string
1079
	 */
1080 62
	protected function doHeaders($text) {
1081
		// Setext-style headers:
1082
		//  Header 1  {#header1}
1083
		//	  ========
1084
		//
1085
		//	  Header 2  {#header2 .class1 .class2}
1086
		//	  --------
1087
		//
1088 62
		$text = preg_replace_callback(
1089
			'{
1090
				(^.+?)								# $1: Header text
1091 62
				(?:[ ]+ ' . $this->id_class_attr_catch_re . ' )?	 # $3 = id/class attributes
1092
				[ ]*\n(=+|-+)[ ]*\n+				# $3: Header footer
1093
			}mx',
1094 62
			array($this, '_doHeaders_callback_setext'), $text);
1095
1096
		// atx-style headers:
1097
		//	# Header 1        {#header1}
1098
		//	## Header 2       {#header2}
1099
		//	## Header 2 with closing hashes ##  {#header3.class1.class2}
1100
		//	...
1101
		//	###### Header 6   {.class2}
1102
		//
1103 62
		$text = preg_replace_callback('{
1104
				^(\#{1,6})	# $1 = string of #\'s
1105 62
				[ ]'.($this->hashtag_protection ? '+' : '*').'
1106
				(.+?)		# $2 = Header text
1107
				[ ]*
1108
				\#*			# optional closing #\'s (not counted)
1109 62
				(?:[ ]+ ' . $this->id_class_attr_catch_re . ' )?	 # $3 = id/class attributes
1110
				[ ]*
1111
				\n+
1112
			}xm',
1113 62
			array($this, '_doHeaders_callback_atx'), $text);
1114
1115 62
		return $text;
1116
	}
1117
1118
	/**
1119
	 * Callback for setext headers
1120
	 * @param  array $matches
1121
	 * @return string
1122
	 */
1123 6
	protected function _doHeaders_callback_setext($matches) {
1124 6 View Code Duplication
		if ($matches[3] === '-' && preg_match('{^- }', $matches[1])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1125 1
			return $matches[0];
1126
		}
1127
1128 5
		$level = $matches[3]{0} === '=' ? 1 : 2;
1129
1130 5
		$defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null;
1131
1132 5
		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId);
1133 5
		$block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>";
1134 5
		return "\n" . $this->hashBlock($block) . "\n\n";
1135
	}
1136
1137
	/**
1138
	 * Callback for atx headers
1139
	 * @param  array $matches
1140
	 * @return string
1141
	 */
1142 12
	protected function _doHeaders_callback_atx($matches) {
1143 12
		$level = strlen($matches[1]);
1144
1145 12
		$defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null;
1146 12
		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId);
1147 12
		$block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>";
1148 12
		return "\n" . $this->hashBlock($block) . "\n\n";
1149
	}
1150
1151
	/**
1152
	 * Form HTML tables.
1153
	 * @param  string $text
1154
	 * @return string
1155
	 */
1156 62
	protected function doTables($text) {
1157 62
		$less_than_tab = $this->tab_width - 1;
1158
		// Find tables with leading pipe.
1159
		//
1160
		//	| Header 1 | Header 2
1161
		//	| -------- | --------
1162
		//	| Cell 1   | Cell 2
1163
		//	| Cell 3   | Cell 4
1164 62
		$text = preg_replace_callback('
1165
			{
1166
				^							# Start of a line
1167 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1168
				[|]							# Optional leading pipe (present)
1169
				(.+) \n						# $1: Header row (at least one pipe)
1170
1171 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1172
				[|] ([ ]*[-:]+[-| :]*) \n	# $2: Header underline
1173
1174
				(							# $3: Cells
1175
					(?>
1176
						[ ]*				# Allowed whitespace.
1177
						[|] .* \n			# Row content.
1178
					)*
1179
				)
1180
				(?=\n|\Z)					# Stop at final double newline.
1181
			}xm',
1182 62
			array($this, '_doTable_leadingPipe_callback'), $text);
1183
1184
		// Find tables without leading pipe.
1185
		//
1186
		//	Header 1 | Header 2
1187
		//	-------- | --------
1188
		//	Cell 1   | Cell 2
1189
		//	Cell 3   | Cell 4
1190 62
		$text = preg_replace_callback('
1191
			{
1192
				^							# Start of a line
1193 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1194
				(\S.*[|].*) \n				# $1: Header row (at least one pipe)
1195
1196 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1197
				([-:]+[ ]*[|][-| :]*) \n	# $2: Header underline
1198
1199
				(							# $3: Cells
1200
					(?>
1201
						.* [|] .* \n		# Row content
1202
					)*
1203
				)
1204
				(?=\n|\Z)					# Stop at final double newline.
1205
			}xm',
1206 62
			array($this, '_DoTable_callback'), $text);
1207
1208 62
		return $text;
1209
	}
1210
1211
	/**
1212
	 * Callback for removing the leading pipe for each row
1213
	 * @param  array $matches
1214
	 * @return string
1215
	 */
1216 1
	protected function _doTable_leadingPipe_callback($matches) {
1217 1
		$head		= $matches[1];
1218 1
		$underline	= $matches[2];
1219 1
		$content	= $matches[3];
1220
1221 1
		$content	= preg_replace('/^ *[|]/m', '', $content);
1222
1223 1
		return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
1224
	}
1225
1226
	/**
1227
	 * Make the align attribute in a table
1228
	 * @param  string $alignname
1229
	 * @return string
1230
	 */
1231 1
	protected function _doTable_makeAlignAttr($alignname) {
1232 1
		if (empty($this->table_align_class_tmpl)) {
1233 1
			return " align=\"$alignname\"";
1234
		}
1235
1236
		$classname = str_replace('%%', $alignname, $this->table_align_class_tmpl);
1237
		return " class=\"$classname\"";
1238
	}
1239
1240
	/**
1241
	 * Calback for processing tables
1242
	 * @param  array $matches
1243
	 * @return string
1244
	 */
1245 1
	protected function _doTable_callback($matches) {
1246 1
		$head		= $matches[1];
1247 1
		$underline	= $matches[2];
1248 1
		$content	= $matches[3];
1249
1250
		// Remove any tailing pipes for each line.
1251 1
		$head		= preg_replace('/[|] *$/m', '', $head);
1252 1
		$underline	= preg_replace('/[|] *$/m', '', $underline);
1253 1
		$content	= preg_replace('/[|] *$/m', '', $content);
1254
1255
		// Reading alignement from header underline.
1256 1
		$separators	= preg_split('/ *[|] */', $underline);
1257 1
		foreach ($separators as $n => $s) {
1258 1
			if (preg_match('/^ *-+: *$/', $s))
1259 1
				$attr[$n] = $this->_doTable_makeAlignAttr('right');
0 ignored issues
show
Coding Style Comprehensibility introduced by
$attr was never initialized. Although not strictly required by PHP, it is generally a good practice to add $attr = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1260 1
			else if (preg_match('/^ *:-+: *$/', $s))
1261 1
				$attr[$n] = $this->_doTable_makeAlignAttr('center');
0 ignored issues
show
Bug introduced by
The variable $attr does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1262 1
			else if (preg_match('/^ *:-+ *$/', $s))
1263 1
				$attr[$n] = $this->_doTable_makeAlignAttr('left');
1264
			else
1265 1
				$attr[$n] = '';
1266
		}
1267
1268
		// Parsing span elements, including code spans, character escapes,
1269
		// and inline HTML tags, so that pipes inside those gets ignored.
1270 1
		$head		= $this->parseSpan($head);
1271 1
		$headers	= preg_split('/ *[|] */', $head);
1272 1
		$col_count	= count($headers);
1273 1
		$attr       = array_pad($attr, $col_count, '');
1274
1275
		// Write column headers.
1276 1
		$text = "<table>\n";
1277 1
		$text .= "<thead>\n";
1278 1
		$text .= "<tr>\n";
1279 1
		foreach ($headers as $n => $header) {
1280 1
			$text .= "  <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n";
1281
		}
1282 1
		$text .= "</tr>\n";
1283 1
		$text .= "</thead>\n";
1284
1285
		// Split content by row.
1286 1
		$rows = explode("\n", trim($content, "\n"));
1287
1288 1
		$text .= "<tbody>\n";
1289 1
		foreach ($rows as $row) {
1290
			// Parsing span elements, including code spans, character escapes,
1291
			// and inline HTML tags, so that pipes inside those gets ignored.
1292 1
			$row = $this->parseSpan($row);
1293
1294
			// Split row by cell.
1295 1
			$row_cells = preg_split('/ *[|] */', $row, $col_count);
1296 1
			$row_cells = array_pad($row_cells, $col_count, '');
1297
1298 1
			$text .= "<tr>\n";
1299 1
			foreach ($row_cells as $n => $cell) {
1300 1
				$text .= "  <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n";
1301
			}
1302 1
			$text .= "</tr>\n";
1303
		}
1304 1
		$text .= "</tbody>\n";
1305 1
		$text .= "</table>";
1306
1307 1
		return $this->hashBlock($text) . "\n";
1308
	}
1309
1310
	/**
1311
	 * Form HTML definition lists.
1312
	 * @param  string $text
1313
	 * @return string
1314
	 */
1315 62
	protected function doDefLists($text) {
1316 62
		$less_than_tab = $this->tab_width - 1;
1317
1318
		// Re-usable pattern to match any entire dl list:
1319
		$whole_list_re = '(?>
1320
			(								# $1 = whole list
1321
			  (								# $2
1322 62
				[ ]{0,' . $less_than_tab . '}
1323
				((?>.*\S.*\n)+)				# $3 = defined term
1324
				\n?
1325 62
				[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1326
			  )
1327
			  (?s:.+?)
1328
			  (								# $4
1329
				  \z
1330
				|
1331
				  \n{2,}
1332
				  (?=\S)
1333
				  (?!						# Negative lookahead for another term
1334 62
					[ ]{0,' . $less_than_tab . '}
1335
					(?: \S.*\n )+?			# defined term
1336
					\n?
1337 62
					[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1338
				  )
1339
				  (?!						# Negative lookahead for another definition
1340 62
					[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1341
				  )
1342
			  )
1343
			)
1344
		)'; // mx
1345
1346 62
		$text = preg_replace_callback('{
1347
				(?>\A\n?|(?<=\n\n))
1348 62
				' . $whole_list_re . '
1349
			}mx',
1350 62
			array($this, '_doDefLists_callback'), $text);
1351
1352 62
		return $text;
1353
	}
1354
1355
	/**
1356
	 * Callback for processing definition lists
1357
	 * @param  array $matches
1358
	 * @return string
1359
	 */
1360 1
	protected function _doDefLists_callback($matches) {
1361
		// Re-usable patterns to match list item bullets and number markers:
1362 1
		$list = $matches[1];
1363
1364
		// Turn double returns into triple returns, so that we can make a
1365
		// paragraph for the last item in a list, if necessary:
1366 1
		$result = trim($this->processDefListItems($list));
1367 1
		$result = "<dl>\n" . $result . "\n</dl>";
1368 1
		return $this->hashBlock($result) . "\n\n";
1369
	}
1370
1371
	/**
1372
	 * Process the contents of a single definition list, splitting it
1373
	 * into individual term and definition list items.
1374
	 * @param  string $list_str
1375
	 * @return string
1376
	 */
1377 1
	protected function processDefListItems($list_str) {
1378
1379 1
		$less_than_tab = $this->tab_width - 1;
1380
1381
		// Trim trailing blank lines:
1382 1
		$list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
1383
1384
		// Process definition terms.
1385 1
		$list_str = preg_replace_callback('{
1386
			(?>\A\n?|\n\n+)						# leading line
1387
			(									# definition terms = $1
1388 1
				[ ]{0,' . $less_than_tab . '}	# leading whitespace
1389
				(?!\:[ ]|[ ])					# negative lookahead for a definition
1390
												#   mark (colon) or more whitespace.
1391
				(?> \S.* \n)+?					# actual term (not whitespace).
1392
			)
1393
			(?=\n?[ ]{0,3}:[ ])					# lookahead for following line feed
1394
												#   with a definition mark.
1395
			}xm',
1396 1
			array($this, '_processDefListItems_callback_dt'), $list_str);
1397
1398
		// Process actual definitions.
1399 1
		$list_str = preg_replace_callback('{
1400
			\n(\n+)?							# leading line = $1
1401
			(									# marker space = $2
1402 1
				[ ]{0,' . $less_than_tab . '}	# whitespace before colon
1403
				\:[ ]+							# definition mark (colon)
1404
			)
1405
			((?s:.+?))							# definition text = $3
1406
			(?= \n+ 							# stop at next definition mark,
1407
				(?:								# next term or end of text
1408 1
					[ ]{0,' . $less_than_tab . '} \:[ ]	|
1409
					<dt> | \z
1410
				)
1411
			)
1412
			}xm',
1413 1
			array($this, '_processDefListItems_callback_dd'), $list_str);
1414
1415 1
		return $list_str;
1416
	}
1417
1418
	/**
1419
	 * Callback for <dt> elements in definition lists
1420
	 * @param  array $matches
1421
	 * @return string
1422
	 */
1423 1
	protected function _processDefListItems_callback_dt($matches) {
1424 1
		$terms = explode("\n", trim($matches[1]));
1425 1
		$text = '';
1426 1
		foreach ($terms as $term) {
1427 1
			$term = $this->runSpanGamut(trim($term));
1428 1
			$text .= "\n<dt>" . $term . "</dt>";
1429
		}
1430 1
		return $text . "\n";
1431
	}
1432
1433
	/**
1434
	 * Callback for <dd> elements in definition lists
1435
	 * @param  array $matches
1436
	 * @return string
1437
	 */
1438 1
	protected function _processDefListItems_callback_dd($matches) {
1439 1
		$leading_line	= $matches[1];
1440 1
		$marker_space	= $matches[2];
1441 1
		$def			= $matches[3];
1442
1443 1
		if ($leading_line || preg_match('/\n{2,}/', $def)) {
1444
			// Replace marker with the appropriate whitespace indentation
1445 1
			$def = str_repeat(' ', strlen($marker_space)) . $def;
1446 1
			$def = $this->runBlockGamut($this->outdent($def . "\n\n"));
1447 1
			$def = "\n". $def ."\n";
1448
		}
1449
		else {
1450 1
			$def = rtrim($def);
1451 1
			$def = $this->runSpanGamut($this->outdent($def));
1452
		}
1453
1454 1
		return "\n<dd>" . $def . "</dd>\n";
1455
	}
1456
1457
	/**
1458
	 * Adding the fenced code block syntax to regular Markdown:
1459
	 *
1460
	 * ~~~
1461
	 * Code block
1462
	 * ~~~
1463
	 *
1464
	 * @param  string $text
1465
	 * @return string
1466
	 */
1467 62
	protected function doFencedCodeBlocks($text) {
1468
1469 62
		$text = preg_replace_callback('{
1470
				(?:\n|\A)
1471
				# 1: Opening marker
1472
				(
1473
					(?:~{3,}|`{3,}) # 3 or more tildes/backticks.
1474
				)
1475
				[ ]*
1476
				(?:
1477
					\.?([-_:a-zA-Z0-9]+) # 2: standalone class name
1478
				)?
1479
				[ ]*
1480
				(?:
1481 62
					' . $this->id_class_attr_catch_re . ' # 3: Extra attributes
1482
				)?
1483
				[ ]* \n # Whitespace and newline following marker.
1484
1485
				# 4: Content
1486
				(
1487
					(?>
1488
						(?!\1 [ ]* \n)	# Not a closing marker.
1489
						.*\n+
1490
					)+
1491
				)
1492
1493
				# Closing marker.
1494
				\1 [ ]* (?= \n )
1495
			}xm',
1496 62
			array($this, '_doFencedCodeBlocks_callback'), $text);
1497
1498 62
		return $text;
1499
	}
1500
1501
	/**
1502
	 * Callback to process fenced code blocks
1503
	 * @param  array $matches
1504
	 * @return string
1505
	 */
1506 4
	protected function _doFencedCodeBlocks_callback($matches) {
1507 4
		$classname =& $matches[2];
1508 4
		$attrs     =& $matches[3];
1509 4
		$codeblock = $matches[4];
1510
1511 4
		if ($this->code_block_content_func) {
1512
			$codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname);
1513
		} else {
1514 4
			$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
1515
		}
1516
1517 4
		$codeblock = preg_replace_callback('/^\n+/',
1518 4
			array($this, '_doFencedCodeBlocks_newlines'), $codeblock);
1519
1520 4
		$classes = array();
1521 4
		if ($classname !== "") {
1522 4
			if ($classname{0} === '.') {
1523
				$classname = substr($classname, 1);
1524
			}
1525 4
			$classes[] = $this->code_class_prefix . $classname;
1526
		}
1527 4
		$attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes);
1528 4
		$pre_attr_str  = $this->code_attr_on_pre ? $attr_str : '';
1529 4
		$code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
1530 4
		$codeblock  = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
1531
1532 4
		return "\n\n".$this->hashBlock($codeblock)."\n\n";
1533
	}
1534
1535
	/**
1536
	 * Replace new lines in fenced code blocks
1537
	 * @param  array $matches
1538
	 * @return string
1539
	 */
1540 2
	protected function _doFencedCodeBlocks_newlines($matches) {
1541 2
		return str_repeat("<br$this->empty_element_suffix",
1542 2
			strlen($matches[0]));
1543
	}
1544
1545
	/**
1546
	 * Redefining emphasis markers so that emphasis by underscore does not
1547
	 * work in the middle of a word.
1548
	 * @var array
1549
	 */
1550
	protected $em_relist = array(
1551
		''  => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)',
1552
		'*' => '(?<![\s*])\*(?!\*)',
1553
		'_' => '(?<![\s_])_(?![a-zA-Z0-9_])',
1554
	);
1555
	protected $strong_relist = array(
1556
		''   => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)',
1557
		'**' => '(?<![\s*])\*\*(?!\*)',
1558
		'__' => '(?<![\s_])__(?![a-zA-Z0-9_])',
1559
	);
1560
	protected $em_strong_relist = array(
1561
		''    => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)',
1562
		'***' => '(?<![\s*])\*\*\*(?!\*)',
1563
		'___' => '(?<![\s_])___(?![a-zA-Z0-9_])',
1564
	);
1565
1566
	/**
1567
	 * Parse text into paragraphs
1568
	 * @param  string $text String to process in paragraphs
1569
	 * @param  boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags
1570
	 * @return string       HTML output
1571
	 */
1572 62
	protected function formParagraphs($text, $wrap_in_p = true) {
1573
		// Strip leading and trailing lines:
1574 62
		$text = preg_replace('/\A\n+|\n+\z/', '', $text);
1575
1576 62
		$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
1577
1578
		// Wrap <p> tags and unhashify HTML blocks
1579 62
		foreach ($grafs as $key => $value) {
1580 62
			$value = trim($this->runSpanGamut($value));
1581
1582
			// Check if this should be enclosed in a paragraph.
1583
			// Clean tag hashes & block tag hashes are left alone.
1584 62
			$is_p = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
1585
1586 62
			if ($is_p) {
1587 58
				$value = "<p>$value</p>";
1588
			}
1589 62
			$grafs[$key] = $value;
1590
		}
1591
1592
		// Join grafs in one text, then unhash HTML tags.
1593 62
		$text = implode("\n\n", $grafs);
1594
1595
		// Finish by removing any tag hashes still present in $text.
1596 62
		$text = $this->unhash($text);
1597
1598 62
		return $text;
1599
	}
1600
1601
1602
	/**
1603
	 * Footnotes - Strips link definitions from text, stores the URLs and
1604
	 * titles in hash references.
1605
	 * @param  string $text
1606
	 * @return string
1607
	 */
1608 62 View Code Duplication
	protected function stripFootnotes($text) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1609 62
		$less_than_tab = $this->tab_width - 1;
1610
1611
		// Link defs are in the form: [^id]: url "optional title"
1612 62
		$text = preg_replace_callback('{
1613 62
			^[ ]{0,' . $less_than_tab . '}\[\^(.+?)\][ ]?:	# note_id = $1
1614
			  [ ]*
1615
			  \n?					# maybe *one* newline
1616
			(						# text = $2 (no blank lines allowed)
1617
				(?:
1618
					.+				# actual text
1619
				|
1620
					\n				# newlines but
1621
					(?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker.
1622
					(?!\n+[ ]{0,3}\S)# ensure line is not blank and followed
1623
									# by non-indented content
1624
				)*
1625
			)
1626
			}xm',
1627 62
			array($this, '_stripFootnotes_callback'),
1628 62
			$text);
1629 62
		return $text;
1630
	}
1631
1632
	/**
1633
	 * Callback for stripping footnotes
1634
	 * @param  array $matches
1635
	 * @return string
1636
	 */
1637 1
	protected function _stripFootnotes_callback($matches) {
1638 1
		$note_id = $this->fn_id_prefix . $matches[1];
1639 1
		$this->footnotes[$note_id] = $this->outdent($matches[2]);
1640 1
		return ''; // String that will replace the block
1641
	}
1642
1643
	/**
1644
	 * Replace footnote references in $text [^id] with a special text-token
1645
	 * which will be replaced by the actual footnote marker in appendFootnotes.
1646
	 * @param  string $text
1647
	 * @return string
1648
	 */
1649 62
	protected function doFootnotes($text) {
1650 62
		if (!$this->in_anchor) {
1651 62
			$text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
1652
		}
1653 62
		return $text;
1654
	}
1655
1656
	/**
1657
	 * Append footnote list to text
1658
	 * @param  string $text
1659
	 * @return string
1660
	 */
1661 62
	protected function appendFootnotes($text) {
1662 62
		$text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1663 62
			array($this, '_appendFootnotes_callback'), $text);
1664
1665 62
		if ( ! empty( $this->footnotes_ordered ) ) {
1666 1
			$this->_doFootnotes();
1667 1
			if ( ! $this->omit_footnotes ) {
1668 1
				$text .= "\n\n";
1669 1
				$text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n";
1670 1
				$text .= "<hr" . $this->empty_element_suffix . "\n";
1671 1
				$text .= $this->footnotes_assembled;
1672 1
				$text .= "</div>";
1673
			}
1674
		}
1675 62
		return $text;
1676
	}
1677
1678
1679
	/**
1680
	 * Generates the HTML for footnotes.  Called by appendFootnotes, even if
1681
	 * footnotes are not being appended.
1682
	 * @return void
1683
	 */
1684 1
	protected function _doFootnotes() {
1685 1
		$attr = array();
1686 1
		if ($this->fn_backlink_class !== "") {
1687 1
			$class = $this->fn_backlink_class;
1688 1
			$class = $this->encodeAttribute($class);
1689 1
			$attr['class'] = " class=\"$class\"";
1690
		}
1691 1
		$attr['role'] = " role=\"doc-backlink\"";
1692 1
		$num = 0;
1693
1694 1
		$text = "<ol>\n\n";
1695 1
		while (!empty($this->footnotes_ordered)) {
1696 1
			$footnote = reset($this->footnotes_ordered);
1697 1
			$note_id = key($this->footnotes_ordered);
1698 1
			unset($this->footnotes_ordered[$note_id]);
1699 1
			$ref_count = $this->footnotes_ref_count[$note_id];
1700 1
			unset($this->footnotes_ref_count[$note_id]);
1701 1
			unset($this->footnotes[$note_id]);
1702
1703 1
			$footnote .= "\n"; // Need to append newline before parsing.
1704 1
			$footnote = $this->runBlockGamut("$footnote\n");
1705 1
			$footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1706 1
				array($this, '_appendFootnotes_callback'), $footnote);
1707
1708 1
			$num++;
1709 1
			$note_id = $this->encodeAttribute($note_id);
1710
1711
			// Prepare backlink, multiple backlinks if multiple references
1712
			// Do not create empty backlinks if the html is blank
1713 1
			$backlink = "";
1714 1
			if (!empty($this->fn_backlink_html)) {
1715 1
				for ($ref_num = 1; $ref_num <= $ref_count; ++$ref_num) {
1716 1
					if (!empty($this->fn_backlink_title)) {
1717
						$attr['title'] = ' title="' . $this->encodeAttribute($this->fn_backlink_title) . '"';
1718
					}
1719 1
					if (!empty($this->fn_backlink_label)) {
1720
						$attr['label'] = ' aria-label="' . $this->encodeAttribute($this->fn_backlink_label) . '"';
1721
					}
1722 1
					$parsed_attr = $this->parseFootnotePlaceholders(
1723 1
						implode('', $attr),
1724 1
						$num,
1725 1
						$ref_num
1726
					);
1727 1
					$backlink_text = $this->parseFootnotePlaceholders(
1728 1
						$this->fn_backlink_html,
1729 1
						$num,
1730 1
						$ref_num
1731
					);
1732 1
					$ref_count_mark = $ref_num > 1 ? $ref_num : '';
1733 1
					$backlink .= " <a href=\"#fnref$ref_count_mark:$note_id\"$parsed_attr>$backlink_text</a>";
1734
				}
1735 1
				$backlink = trim($backlink);
1736
			}
1737
1738
			// Add backlink to last paragraph; create new paragraph if needed.
1739 1
			if (!empty($backlink)) {
1740 1
				if (preg_match('{</p>$}', $footnote)) {
1741 1
					$footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
1742
				} else {
1743 1
					$footnote .= "\n\n<p>$backlink</p>";
1744
				}
1745
			}
1746
1747 1
			$text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n";
1748 1
			$text .= $footnote . "\n";
1749 1
			$text .= "</li>\n\n";
1750
		}
1751 1
		$text .= "</ol>\n";
1752
1753 1
		$this->footnotes_assembled = $text;
1754 1
	}
1755
1756
	/**
1757
	 * Callback for appending footnotes
1758
	 * @param  array $matches
1759
	 * @return string
1760
	 */
1761 1
	protected function _appendFootnotes_callback($matches) {
1762 1
		$node_id = $this->fn_id_prefix . $matches[1];
1763
1764
		// Create footnote marker only if it has a corresponding footnote *and*
1765
		// the footnote hasn't been used by another marker.
1766 1
		if (isset($this->footnotes[$node_id])) {
1767 1
			$num =& $this->footnotes_numbers[$node_id];
1768 1
			if (!isset($num)) {
1769
				// Transfer footnote content to the ordered list and give it its
1770
				// number
1771 1
				$this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
1772 1
				$this->footnotes_ref_count[$node_id] = 1;
1773 1
				$num = $this->footnote_counter++;
1774 1
				$ref_count_mark = '';
1775
			} else {
1776 1
				$ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
1777
			}
1778
1779 1
			$attr = "";
1780 1
			if ($this->fn_link_class !== "") {
1781 1
				$class = $this->fn_link_class;
1782 1
				$class = $this->encodeAttribute($class);
1783 1
				$attr .= " class=\"$class\"";
1784
			}
1785 1
			if ($this->fn_link_title !== "") {
1786
				$title = $this->fn_link_title;
1787
				$title = $this->encodeAttribute($title);
1788
				$attr .= " title=\"$title\"";
1789
			}
1790 1
			$attr .= " role=\"doc-noteref\"";
1791
1792 1
			$attr = str_replace("%%", $num, $attr);
1793 1
			$node_id = $this->encodeAttribute($node_id);
1794
1795
			return
1796 1
				"<sup id=\"fnref$ref_count_mark:$node_id\">".
1797 1
				"<a href=\"#fn:$node_id\"$attr>$num</a>".
1798 1
				"</sup>";
1799
		}
1800
1801 1
		return "[^" . $matches[1] . "]";
1802
	}
1803
1804
	/**
1805
	 * Build footnote label by evaluating any placeholders.
1806
	 * - ^^  footnote number
1807
	 * - %%  footnote reference number (Nth reference to footnote number)
1808
	 * @param  string $label
1809
	 * @param  int    $footnote_number
1810
	 * @param  int    $reference_number
1811
	 * @return string
1812
	 */
1813 1
	protected function parseFootnotePlaceholders($label, $footnote_number, $reference_number) {
1814 1
		return str_replace(
1815 1
			array('^^', '%%'),
1816 1
			array($footnote_number, $reference_number),
1817 1
			$label
1818
		);
1819
	}
1820
1821
1822
	/**
1823
	 * Abbreviations - strips abbreviations from text, stores titles in hash
1824
	 * references.
1825
	 * @param  string $text
1826
	 * @return string
1827
	 */
1828 62 View Code Duplication
	protected function stripAbbreviations($text) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1829 62
		$less_than_tab = $this->tab_width - 1;
1830
1831
		// Link defs are in the form: [id]*: url "optional title"
1832 62
		$text = preg_replace_callback('{
1833 62
			^[ ]{0,' . $less_than_tab . '}\*\[(.+?)\][ ]?:	# abbr_id = $1
1834
			(.*)					# text = $2 (no blank lines allowed)
1835
			}xm',
1836 62
			array($this, '_stripAbbreviations_callback'),
1837 62
			$text);
1838 62
		return $text;
1839
	}
1840
1841
	/**
1842
	 * Callback for stripping abbreviations
1843
	 * @param  array $matches
1844
	 * @return string
1845
	 */
1846 1
	protected function _stripAbbreviations_callback($matches) {
1847 1
		$abbr_word = $matches[1];
1848 1
		$abbr_desc = $matches[2];
1849 1
		if ($this->abbr_word_re) {
1850 1
			$this->abbr_word_re .= '|';
1851
		}
1852 1
		$this->abbr_word_re .= preg_quote($abbr_word);
1853 1
		$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
1854 1
		return ''; // String that will replace the block
1855
	}
1856
1857
	/**
1858
	 * Find defined abbreviations in text and wrap them in <abbr> elements.
1859
	 * @param  string $text
1860
	 * @return string
1861
	 */
1862 62
	protected function doAbbreviations($text) {
1863 62
		if ($this->abbr_word_re) {
1864
			// cannot use the /x modifier because abbr_word_re may
1865
			// contain significant spaces:
1866 3
			$text = preg_replace_callback('{' .
1867
				'(?<![\w\x1A])' .
1868 3
				'(?:' . $this->abbr_word_re . ')' .
1869 3
				'(?![\w\x1A])' .
1870 3
				'}',
1871 3
				array($this, '_doAbbreviations_callback'), $text);
1872
		}
1873 62
		return $text;
1874
	}
1875
1876
	/**
1877
	 * Callback for processing abbreviations
1878
	 * @param  array $matches
1879
	 * @return string
1880
	 */
1881 3
	protected function _doAbbreviations_callback($matches) {
1882 3
		$abbr = $matches[0];
1883 3
		if (isset($this->abbr_desciptions[$abbr])) {
1884 3
			$desc = $this->abbr_desciptions[$abbr];
1885 3
			if (empty($desc)) {
1886
				return $this->hashPart("<abbr>$abbr</abbr>");
1887
			}
1888 3
			$desc = $this->encodeAttribute($desc);
1889 3
			return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
1890
		}
1891
		return $matches[0];
1892
	}
1893
}
1894