Completed
Pull Request — lib (#303)
by Michael
03:19
created

MarkdownExtra::_hashHTMLBlocks_inMarkdown()   D

Complexity

Conditions 24
Paths 61

Size

Total Lines 200
Code Lines 78

Duplication

Lines 27
Ratio 13.5 %

Code Coverage

Tests 65
CRAP Score 24.0494

Importance

Changes 0
Metric Value
dl 27
loc 200
ccs 65
cts 68
cp 0.9559
rs 4.5989
c 0
b 0
f 0
cc 24
eloc 78
nc 61
nop 4
crap 24.0494

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 and backlinks.
29
	 * @var string
30
	 */
31
	public $fn_link_title     = "";
32
	public $fn_backlink_title = "";
33
34
	/**
35
	 * Optional class attribute for footnote links and backlinks.
36
	 * @var string
37
	 */
38
	public $fn_link_class     = "footnote-ref";
39
	public $fn_backlink_class = "footnote-backref";
40
41
	/**
42
	 * Content to be displayed within footnote backlinks. The default is '↩';
43
	 * the U+FE0E on the end is a Unicode variant selector used to prevent iOS
44
	 * from displaying the arrow character as an emoji.
45
	 * @var string
46
	 */
47
	public $fn_backlink_html = '&#8617;&#xFE0E;';
48
49
	/**
50
	 * Class name for table cell alignment (%% replaced left/center/right)
51
	 * For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center'
52
	 * If empty, the align attribute is used instead of a class name.
53
	 * @var string
54
	 */
55
	public $table_align_class_tmpl = '';
56
57
	/**
58
	 * Optional class prefix for fenced code block.
59
	 * @var string
60
	 */
61
	public $code_class_prefix = "";
62
63
	/**
64
	 * Class attribute for code blocks goes on the `code` tag;
65
	 * setting this to true will put attributes on the `pre` tag instead.
66
	 * @var boolean
67
	 */
68
	public $code_attr_on_pre = false;
69
70
	/**
71
	 * Predefined abbreviations.
72
	 * @var array
73
	 */
74
	public $predef_abbr = array();
75
76
	/**
77
	 * Only convert atx-style headers if there's a space between the header and #
78
	 * @var boolean
79
	 */
80
	public $hashtag_protection = false;
81
82
	/**
83
	 * Determines whether footnotes should be appended to the end of the document.
84
	 * If true, footnote html can be retrieved from $this->footnotes_assembled.
85
	 * @var boolean
86
	 */
87
	public $omit_footnotes = false;
88
89
90
	/**
91
	 * After parsing, the HTML for the list of footnotes appears here.
92
	 * This is available only if $omit_footnotes == true.
93
	 *
94
	 * Note: when placing the content of `footnotes_assembled` on the page,
95
	 * consider adding the attribute `role="doc-endnotes"` to the `div` or
96
	 * `section` that will enclose the list of footnotes so they are
97
	 * reachable to accessibility tools the same way they would be with the
98
	 * default HTML output.
99
	 * @var null|string
100
	 */
101
	public $footnotes_assembled = null;
102
103
	/**
104
	 * Parser implementation
105
	 */
106
107
	/**
108
	 * Constructor function. Initialize the parser object.
109
	 * @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...
110
	 */
111 4
	public function __construct() {
112
		// Add extra escapable characters before parent constructor
113
		// initialize the table.
114 4
		$this->escape_chars .= ':|';
115
116
		// Insert extra document, block, and span transformations.
117
		// Parent constructor will do the sorting.
118 4
		$this->document_gamut += array(
119
			"doFencedCodeBlocks" => 5,
120
			"stripFootnotes"     => 15,
121
			"stripAbbreviations" => 25,
122
			"appendFootnotes"    => 50,
123
		);
124 4
		$this->block_gamut += array(
125
			"doFencedCodeBlocks" => 5,
126
			"doTables"           => 15,
127
			"doDefLists"         => 45,
128
		);
129 4
		$this->span_gamut += array(
130
			"doFootnotes"        => 5,
131
			"doAbbreviations"    => 70,
132
		);
133
134 4
		$this->enhanced_ordered_list = true;
135 4
		parent::__construct();
136 4
	}
137
138
139
	/**
140
	 * Extra variables used during extra transformations.
141
	 * @var array
142
	 */
143
	protected $footnotes = array();
144
	protected $footnotes_ordered = array();
145
	protected $footnotes_ref_count = array();
146
	protected $footnotes_numbers = array();
147
	protected $abbr_desciptions = array();
148
	/** @var string */
149
	protected $abbr_word_re = '';
150
151
	/**
152
	 * Give the current footnote number.
153
	 * @var integer
154
	 */
155
	protected $footnote_counter = 1;
156
157
	protected $ref_attr = array();
158
159
	/**
160
	 * Setting up Extra-specific variables.
161
	 */
162 62
	protected function setup() {
163 62
		parent::setup();
164
165 62
		$this->footnotes = array();
166 62
		$this->footnotes_ordered = array();
167 62
		$this->footnotes_ref_count = array();
168 62
		$this->footnotes_numbers = array();
169 62
		$this->abbr_desciptions = array();
170 62
		$this->abbr_word_re = '';
171 62
		$this->footnote_counter = 1;
172 62
		$this->footnotes_assembled = null;
173
174 62
		foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
175 2
			if ($this->abbr_word_re)
176 1
				$this->abbr_word_re .= '|';
177 2
			$this->abbr_word_re .= preg_quote($abbr_word);
178 2
			$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
179
		}
180 62
	}
181
182
	/**
183
	 * Clearing Extra-specific variables.
184
	 */
185 62
	protected function teardown() {
186 62
		$this->footnotes = array();
187 62
		$this->footnotes_ordered = array();
188 62
		$this->footnotes_ref_count = array();
189 62
		$this->footnotes_numbers = array();
190 62
		$this->abbr_desciptions = array();
191 62
		$this->abbr_word_re = '';
192
193 62
		if ( ! $this->omit_footnotes )
194 62
			$this->footnotes_assembled = null;
195
196 62
		parent::teardown();
197 62
	}
198
199
200
	/**
201
	 * Extra attribute parser
202
	 */
203
204
	/**
205
	 * Expression to use to catch attributes (includes the braces)
206
	 * @var string
207
	 */
208
	protected $id_class_attr_catch_re = '\{((?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}';
209
210
	/**
211
	 * Expression to use when parsing in a context when no capture is desired
212
	 * @var string
213
	 */
214
	protected $id_class_attr_nocatch_re = '\{(?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}';
215
216
	/**
217
	 * Parse attributes caught by the $this->id_class_attr_catch_re expression
218
	 * and return the HTML-formatted list of attributes.
219
	 *
220
	 * Currently supported attributes are .class and #id.
221
	 *
222
	 * In addition, this method also supports supplying a default Id value,
223
	 * which will be used to populate the id attribute in case it was not
224
	 * overridden.
225
	 * @param  string $tag_name
226
	 * @param  string $attr
227
	 * @param  mixed  $defaultIdValue
228
	 * @param  array  $classes
229
	 * @return string
230
	 */
231 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...
232 29
		if (empty($attr) && !$defaultIdValue && empty($classes)) {
233 27
			return "";
234
		}
235
236
		// Split on components
237 6
		preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches);
238 6
		$elements = $matches[0];
239
240
		// Handle classes and IDs (only first ID taken into account)
241 6
		$attributes = array();
242 6
		$id = false;
243 6
		foreach ($elements as $element) {
244 4
			if ($element{0} === '.') {
245 4
				$classes[] = substr($element, 1);
246 4
			} else if ($element{0} === '#') {
247 4
				if ($id === false) $id = substr($element, 1);
248 1
			} else if (strpos($element, '=') > 0) {
249 1
				$parts = explode('=', $element, 2);
250 4
				$attributes[] = $parts[0] . '="' . $parts[1] . '"';
251
			}
252
		}
253
254 6
		if ($id === false || $id === '') {
255 5
			$id = $defaultIdValue;
256
		}
257
258
		// Compose attributes as string
259 6
		$attr_str = "";
260 6
		if (!empty($id)) {
261 4
			$attr_str .= ' id="'.$this->encodeAttribute($id) .'"';
262
		}
263 6
		if (!empty($classes)) {
264 6
			$attr_str .= ' class="'. implode(" ", $classes) . '"';
265
		}
266 6
		if (!$this->no_markup && !empty($attributes)) {
267 1
			$attr_str .= ' '.implode(" ", $attributes);
268
		}
269 6
		return $attr_str;
270
	}
271
272
	/**
273
	 * Strips link definitions from text, stores the URLs and titles in
274
	 * hash references.
275
	 * @param  string $text
276
	 * @return string
277
	 */
278 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...
279 62
		$less_than_tab = $this->tab_width - 1;
280
281
		// Link defs are in the form: ^[id]: url "optional title"
282 62
		$text = preg_replace_callback('{
283 62
							^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?:	# id = $1
284
							  [ ]*
285
							  \n?				# maybe *one* newline
286
							  [ ]*
287
							(?:
288
							  <(.+?)>			# url = $2
289
							|
290
							  (\S+?)			# url = $3
291
							)
292
							  [ ]*
293
							  \n?				# maybe one newline
294
							  [ ]*
295
							(?:
296
								(?<=\s)			# lookbehind for whitespace
297
								["(]
298
								(.*?)			# title = $4
299
								[")]
300
								[ ]*
301
							)?	# title is optional
302 62
					(?:[ ]* '.$this->id_class_attr_catch_re.' )?  # $5 = extra id & class attr
303
							(?:\n+|\Z)
304
			}xm',
305 62
			array($this, '_stripLinkDefinitions_callback'),
306 62
			$text);
307 62
		return $text;
308
	}
309
310
	/**
311
	 * Strip link definition callback
312
	 * @param  array $matches
313
	 * @return string
314
	 */
315 11
	protected function _stripLinkDefinitions_callback($matches) {
316 11
		$link_id = strtolower($matches[1]);
317 11
		$url = $matches[2] == '' ? $matches[3] : $matches[2];
318 11
		$this->urls[$link_id] = $url;
319 11
		$this->titles[$link_id] =& $matches[4];
320 11
		$this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]);
321 11
		return ''; // String that will replace the block
322
	}
323
324
325
	/**
326
	 * HTML block parser
327
	 */
328
329
	/**
330
	 * Tags that are always treated as block tags
331
	 * @var string
332
	 */
333
	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';
334
335
	/**
336
	 * Tags treated as block tags only if the opening tag is alone on its line
337
	 * @var string
338
	 */
339
	protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
340
341
	/**
342
	 * Tags where markdown="1" default to span mode:
343
	 * @var string
344
	 */
345
	protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
346
347
	/**
348
	 * Tags which must not have their contents modified, no matter where
349
	 * they appear
350
	 * @var string
351
	 */
352
	protected $clean_tags_re = 'script|style|math|svg';
353
354
	/**
355
	 * Tags that do not need to be closed.
356
	 * @var string
357
	 */
358
	protected $auto_close_tags_re = 'hr|img|param|source|track';
359
360
	/**
361
	 * Hashify HTML Blocks and "clean tags".
362
	 *
363
	 * We only want to do this for block-level HTML tags, such as headers,
364
	 * lists, and tables. That's because we still want to wrap <p>s around
365
	 * "paragraphs" that are wrapped in non-block-level tags, such as anchors,
366
	 * phrase emphasis, and spans. The list of tags we're looking for is
367
	 * hard-coded.
368
	 *
369
	 * This works by calling _HashHTMLBlocks_InMarkdown, which then calls
370
	 * _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
371
	 * attribute is found within a tag, _HashHTMLBlocks_InHTML calls back
372
	 *  _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
373
	 * These two functions are calling each other. It's recursive!
374
	 * @param  string $text
375
	 * @return string
376
	 */
377 62
	protected function hashHTMLBlocks($text) {
378 62
		if ($this->no_markup) {
379 1
			return $text;
380
		}
381
382
		// Call the HTML-in-Markdown hasher.
383 61
		list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
384
385 61
		return $text;
386
	}
387
388
	/**
389
	 * Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
390
	 *
391
	 * *   $indent is the number of space to be ignored when checking for code
392
	 *     blocks. This is important because if we don't take the indent into
393
	 *     account, something like this (which looks right) won't work as expected:
394
	 *
395
	 *     <div>
396
	 *         <div markdown="1">
397
	 *         Hello World.  <-- Is this a Markdown code block or text?
398
	 *         </div>  <-- Is this a Markdown code block or a real tag?
399
	 *     <div>
400
	 *
401
	 *     If you don't like this, just don't indent the tag on which
402
	 *     you apply the markdown="1" attribute.
403
	 *
404
	 * *   If $enclosing_tag_re is not empty, stops at the first unmatched closing
405
	 *     tag with that name. Nested tags supported.
406
	 *
407
	 * *   If $span is true, text inside must treated as span. So any double
408
	 *     newline will be replaced by a single newline so that it does not create
409
	 *     paragraphs.
410
	 *
411
	 * Returns an array of that form: ( processed text , remaining text )
412
	 *
413
	 * @param  string  $text
414
	 * @param  integer $indent
415
	 * @param  string  $enclosing_tag_re
416
	 * @param  boolean $span
417
	 * @return array
418
	 */
419 61
	protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
420
										$enclosing_tag_re = '', $span = false)
421
	{
422
423 61
		if ($text === '') return array('', '');
424
425
		// Regex to check for the presense of newlines around a block tag.
426 61
		$newline_before_re = '/(?:^\n?|\n\n)*$/';
427
		$newline_after_re =
428 61
			'{
429
				^						# Start of text following the tag.
430
				(?>[ ]*<!--.*?-->)?		# Optional comment.
431
				[ ]*\n					# Must be followed by newline.
432
			}xs';
433
434
		// Regex to match any tag.
435
		$block_tag_re =
436
			'{
437
				(					# $2: Capture whole tag.
438
					</?					# Any opening or closing tag.
439
						(?>				# Tag name.
440 61
							' . $this->block_tags_re . '			|
441 61
							' . $this->context_block_tags_re . '	|
442 61
							' . $this->clean_tags_re . '        	|
443 61
							(?!\s)'.$enclosing_tag_re . '
444
						)
445
						(?:
446
							(?=[\s"\'/a-zA-Z0-9])	# Allowed characters after tag name.
447
							(?>
448
								".*?"		|	# Double quotes (can contain `>`)
449
								\'.*?\'   	|	# Single quotes (can contain `>`)
450
								.+?				# Anything but quotes and `>`.
451
							)*?
452
						)?
453
					>					# End of tag.
454
				|
455
					<!--    .*?     -->	# HTML Comment
456
				|
457
					<\?.*?\?> | <%.*?%>	# Processing instruction
458
				|
459
					<!\[CDATA\[.*?\]\]>	# CData Block
460 61
				' . ( !$span ? ' # If not in span.
461
				|
462
					# Indented code block
463
					(?: ^[ ]*\n | ^ | \n[ ]*\n )
464 61
					[ ]{' . ($indent + 4) . '}[^\n]* \n
465
					(?>
466 61
						(?: [ ]{' . ($indent + 4) . '}[^\n]* | [ ]* ) \n
467
					)*
468
				|
469
					# Fenced code block marker
470
					(?<= ^ | \n )
471 61
					[ ]{0,' . ($indent + 3) . '}(?:~{3,}|`{3,})
472
					[ ]*
473
					(?: \.?[-_:a-zA-Z0-9]+ )? # standalone class name
474
					[ ]*
475 61
					(?: ' . $this->id_class_attr_nocatch_re . ' )? # extra attributes
476
					[ ]*
477
					(?= \n )
478 61
				' : '' ) . ' # End (if not is span).
479
				|
480
					# Code span marker
481
					# Note, this regex needs to go after backtick fenced
482
					# code blocks but it should also be kept outside of the
483
					# "if not in span" condition adding backticks to the parser
484
					`+
485
				)
486
			}xs';
487
488
489 61
		$depth = 0;		// Current depth inside the tag tree.
490 61
		$parsed = "";	// Parsed text that will be returned.
491
492
		// Loop through every tag until we find the closing tag of the parent
493
		// or loop until reaching the end of text if no parent tag specified.
494
		do {
495
			// Split the text using the first $tag_match pattern found.
496
			// Text before  pattern will be first in the array, text after
497
			// pattern will be at the end, and between will be any catches made
498
			// by the pattern.
499 61
			$parts = preg_split($block_tag_re, $text, 2,
500 61
								PREG_SPLIT_DELIM_CAPTURE);
501
502
			// If in Markdown span mode, add a empty-string span-level hash
503
			// after each newline to prevent triggering any block element.
504 61
			if ($span) {
505 1
				$void = $this->hashPart("", ':');
506 1
				$newline = "\n$void";
507 1
				$parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
508
			}
509
510 61
			$parsed .= $parts[0]; // Text before current tag.
511
512
			// If end of $text has been reached. Stop loop.
513 61
			if (count($parts) < 3) {
514 61
				$text = "";
515 61
				break;
516
			}
517
518 39
			$tag  = $parts[1]; // Tag to handle.
519 39
			$text = $parts[2]; // Remaining text after current tag.
520
521
			// Check for: Fenced code block marker.
522
			// Note: need to recheck the whole tag to disambiguate backtick
523
			// fences from code spans
524 39
			if (preg_match('{^\n?([ ]{0,' . ($indent + 3) . '})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+)?[ ]*(?:' . $this->id_class_attr_nocatch_re . ')?[ ]*\n?$}', $tag, $capture)) {
525
				// Fenced code block marker: find matching end marker.
526 4
				$fence_indent = strlen($capture[1]); // use captured indent in re
527 4
				$fence_re = $capture[2]; // use captured fence in re
528 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...
529 4
					$matches))
530
				{
531
					// End marker found: pass text unchanged until marker.
532 4
					$parsed .= $tag . $matches[0];
533 4
					$text = substr($text, strlen($matches[0]));
534
				}
535
				else {
536
					// No end marker: just skip it.
537 4
					$parsed .= $tag;
538
				}
539
			}
540
			// Check for: Indented code block.
541 39
			else if ($tag{0} === "\n" || $tag{0} === " ") {
542
				// Indented code block: pass it unchanged, will be handled
543
				// later.
544 23
				$parsed .= $tag;
545
			}
546
			// Check for: Code span marker
547
			// Note: need to check this after backtick fenced code blocks
548 27
			else if ($tag{0} === "`") {
549
				// Find corresponding end marker.
550 13
				$tag_re = preg_quote($tag);
551 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...
552 13
					$text, $matches))
553
				{
554
					// End marker found: pass text unchanged until marker.
555 12
					$parsed .= $tag . $matches[0];
556 12
					$text = substr($text, strlen($matches[0]));
557
				}
558
				else {
559
					// Unmatched marker: just skip it.
560 13
					$parsed .= $tag;
561
				}
562
			}
563
			// Check for: Opening Block level tag or
564
			//            Opening Context Block tag (like ins and del)
565
			//               used as a block tag (tag is alone on it's line).
566 24
			else if (preg_match('{^<(?:' . $this->block_tags_re . ')\b}', $tag) ||
567 19
				(	preg_match('{^<(?:' . $this->context_block_tags_re . ')\b}', $tag) &&
568 19
					preg_match($newline_before_re, $parsed) &&
569 24
					preg_match($newline_after_re, $text)	)
570
				)
571
			{
572
				// Need to parse tag and following text using the HTML parser.
573
				list($block_text, $text) =
574 10
					$this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
575
576
				// Make sure it stays outside of any paragraph by adding newlines.
577 10
				$parsed .= "\n\n$block_text\n\n";
578
			}
579
			// Check for: Clean tag (like script, math)
580
			//            HTML Comments, processing instructions.
581 19
			else if (preg_match('{^<(?:' . $this->clean_tags_re . ')\b}', $tag) ||
582 19
				$tag{1} === '!' || $tag{1} === '?')
583
			{
584
				// Need to parse tag and following text using the HTML parser.
585
				// (don't check for markdown attribute)
586
				list($block_text, $text) =
587 3
					$this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
588
589 3
				$parsed .= $block_text;
590
			}
591
			// Check for: Tag with same name as enclosing tag.
592 16
			else if ($enclosing_tag_re !== '' &&
593
				// Same name as enclosing tag.
594 16
				preg_match('{^</?(?:' . $enclosing_tag_re . ')\b}', $tag))
595
			{
596
				// Increase/decrease nested tag count.
597 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...
598 3
					$depth--;
599
				} else if ($tag{strlen($tag)-2} !== '/') {
600
					$depth++;
601
				}
602
603 3
				if ($depth < 0) {
604
					// Going out of parent element. Clean up and break so we
605
					// return to the calling function.
606 3
					$text = $tag . $text;
607 3
					break;
608
				}
609
610
				$parsed .= $tag;
611
			}
612
			else {
613 13
				$parsed .= $tag;
614
			}
615 39
		} while ($depth >= 0);
616
617 61
		return array($parsed, $text);
618
	}
619
620
	/**
621
	 * Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
622
	 *
623
	 * *   Calls $hash_method to convert any blocks.
624
	 * *   Stops when the first opening tag closes.
625
	 * *   $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
626
	 *     (it is not inside clean tags)
627
	 *
628
	 * Returns an array of that form: ( processed text , remaining text )
629
	 * @param  string $text
630
	 * @param  string $hash_method
631
	 * @param  bool $md_attr Handle `markdown="1"` attribute
632
	 * @return array
633
	 */
634 12
	protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
635 12
		if ($text === '') return array('', '');
636
637
		// Regex to match `markdown` attribute inside of a tag.
638 12
		$markdown_attr_re = '
639
			{
640
				\s*			# Eat whitespace before the `markdown` attribute
641
				markdown
642
				\s*=\s*
643
				(?>
644
					(["\'])		# $1: quote delimiter
645
					(.*?)		# $2: attribute value
646
					\1			# matching delimiter
647
				|
648
					([^\s>]*)	# $3: unquoted attribute value
649
				)
650
				()				# $4: make $3 always defined (avoid warnings)
651
			}xs';
652
653
		// Regex to match any tag.
654 12
		$tag_re = '{
655
				(					# $2: Capture whole tag.
656
					</?					# Any opening or closing tag.
657
						[\w:$]+			# Tag name.
658
						(?:
659
							(?=[\s"\'/a-zA-Z0-9])	# Allowed characters after tag name.
660
							(?>
661
								".*?"		|	# Double quotes (can contain `>`)
662
								\'.*?\'   	|	# Single quotes (can contain `>`)
663
								.+?				# Anything but quotes and `>`.
664
							)*?
665
						)?
666
					>					# End of tag.
667
				|
668
					<!--    .*?     -->	# HTML Comment
669
				|
670
					<\?.*?\?> | <%.*?%>	# Processing instruction
671
				|
672
					<!\[CDATA\[.*?\]\]>	# CData Block
673
				)
674
			}xs';
675
676 12
		$original_text = $text;		// Save original text in case of faliure.
677
678 12
		$depth		= 0;	// Current depth inside the tag tree.
679 12
		$block_text	= "";	// Temporary text holder for current text.
680 12
		$parsed		= "";	// Parsed text that will be returned.
681 12
		$base_tag_name_re = '';
682
683
		// Get the name of the starting tag.
684
		// (This pattern makes $base_tag_name_re safe without quoting.)
685 12
		if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
686 10
			$base_tag_name_re = $matches[1];
687
688
		// Loop through every tag until we find the corresponding closing tag.
689
		do {
690
			// Split the text using the first $tag_match pattern found.
691
			// Text before  pattern will be first in the array, text after
692
			// pattern will be at the end, and between will be any catches made
693
			// by the pattern.
694 12
			$parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
695
696 12
			if (count($parts) < 3) {
697
				// End of $text reached with unbalenced tag(s).
698
				// In that case, we return original text unchanged and pass the
699
				// first character as filtered to prevent an infinite loop in the
700
				// parent function.
701
				return array($original_text{0}, substr($original_text, 1));
702
			}
703
704 12
			$block_text .= $parts[0]; // Text before current tag.
705 12
			$tag         = $parts[1]; // Tag to handle.
706 12
			$text        = $parts[2]; // Remaining text after current tag.
707
708
			// Check for: Auto-close tag (like <hr/>)
709
			//			 Comments and Processing Instructions.
710 12
			if (preg_match('{^</?(?:' . $this->auto_close_tags_re . ')\b}', $tag) ||
711 12
				$tag{1} === '!' || $tag{1} === '?')
712
			{
713
				// Just add the tag to the block as if it was text.
714 4
				$block_text .= $tag;
715
			}
716
			else {
717
				// Increase/decrease nested tag count. Only do so if
718
				// the tag's name match base tag's.
719 10
				if (preg_match('{^</?' . $base_tag_name_re . '\b}', $tag)) {
720 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...
721 10
						$depth--;
722 10
					} else if ($tag{strlen($tag)-2} !== '/') {
723 10
						$depth++;
724
					}
725
				}
726
727
				// Check for `markdown="1"` attribute and handle it.
728 10
				if ($md_attr &&
729 10
					preg_match($markdown_attr_re, $tag, $attr_m) &&
730 10
					preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
731
				{
732
					// Remove `markdown` attribute from opening tag.
733 3
					$tag = preg_replace($markdown_attr_re, '', $tag);
734
735
					// Check if text inside this tag must be parsed in span mode.
736 3
					$this->mode = $attr_m[2] . $attr_m[3];
0 ignored issues
show
Bug introduced by
The property mode does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
737 3
					$span_mode = $this->mode === 'span' || ($this->mode !== 'block' &&
738 3
						preg_match('{^<(?:' . $this->contain_span_tags_re . ')\b}', $tag));
739
740
					// Calculate indent before tag.
741 3
					if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
742 3
						$strlen = $this->utf8_strlen;
743 3
						$indent = $strlen($matches[1], 'UTF-8');
744
					} else {
745
						$indent = 0;
746
					}
747
748
					// End preceding block with this tag.
749 3
					$block_text .= $tag;
750 3
					$parsed .= $this->$hash_method($block_text);
751
752
					// Get enclosing tag name for the ParseMarkdown function.
753
					// (This pattern makes $tag_name_re safe without quoting.)
754 3
					preg_match('/^<([\w:$]*)\b/', $tag, $matches);
755 3
					$tag_name_re = $matches[1];
756
757
					// Parse the content using the HTML-in-Markdown parser.
758
					list ($block_text, $text)
759 3
						= $this->_hashHTMLBlocks_inMarkdown($text, $indent,
760 3
							$tag_name_re, $span_mode);
761
762
					// Outdent markdown text.
763 3
					if ($indent > 0) {
764 1
						$block_text = preg_replace("/^[ ]{1,$indent}/m", "",
765 1
													$block_text);
766
					}
767
768
					// Append tag content to parsed text.
769 3
					if (!$span_mode) {
770 3
						$parsed .= "\n\n$block_text\n\n";
771
					} else {
772 1
						$parsed .= (string) $block_text;
773
					}
774
775
					// Start over with a new block.
776 3
					$block_text = "";
777
				}
778 10
				else $block_text .= $tag;
779
			}
780
781 12
		} while ($depth > 0);
782
783
		// Hash last block text that wasn't processed inside the loop.
784 12
		$parsed .= $this->$hash_method($block_text);
785
786 12
		return array($parsed, $text);
787
	}
788
789
	/**
790
	 * Called whenever a tag must be hashed when a function inserts a "clean" tag
791
	 * in $text, it passes through this function and is automaticaly escaped,
792
	 * blocking invalid nested overlap.
793
	 * @param  string $text
794
	 * @return string
795
	 */
796 3
	protected function hashClean($text) {
797 3
		return $this->hashPart($text, 'C');
798
	}
799
800
	/**
801
	 * Turn Markdown link shortcuts into XHTML <a> tags.
802
	 * @param  string $text
803
	 * @return string
804
	 */
805 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...
806 62
		if ($this->in_anchor) {
807 15
			return $text;
808
		}
809 62
		$this->in_anchor = true;
810
811
		// First, handle reference-style links: [link text] [id]
812 62
		$text = preg_replace_callback('{
813
			(					# wrap whole match in $1
814
			  \[
815 62
				(' . $this->nested_brackets_re . ')	# link text = $2
816
			  \]
817
818
			  [ ]?				# one optional space
819
			  (?:\n[ ]*)?		# one optional newline followed by spaces
820
821
			  \[
822
				(.*?)		# id = $3
823
			  \]
824
			)
825
			}xs',
826 62
			array($this, '_doAnchors_reference_callback'), $text);
827
828
		// Next, inline-style links: [link text](url "optional title")
829 62
		$text = preg_replace_callback('{
830
			(				# wrap whole match in $1
831
			  \[
832 62
				(' . $this->nested_brackets_re . ')	# link text = $2
833
			  \]
834
			  \(			# literal paren
835
				[ \n]*
836
				(?:
837
					<(.+?)>	# href = $3
838
				|
839 62
					(' . $this->nested_url_parenthesis_re . ')	# href = $4
840
				)
841
				[ \n]*
842
				(			# $5
843
				  ([\'"])	# quote char = $6
844
				  (.*?)		# Title = $7
845
				  \6		# matching quote
846
				  [ \n]*	# ignore any spaces/tabs between closing quote and )
847
				)?			# title is optional
848
			  \)
849 62
			  (?:[ ]? ' . $this->id_class_attr_catch_re . ' )?	 # $8 = id/class attributes
850
			)
851
			}xs',
852 62
			array($this, '_doAnchors_inline_callback'), $text);
853
854
		// Last, handle reference-style shortcuts: [link text]
855
		// These must come last in case you've also got [link text][1]
856
		// or [link text](/foo)
857 62
		$text = preg_replace_callback('{
858
			(					# wrap whole match in $1
859
			  \[
860
				([^\[\]]+)		# link text = $2; can\'t contain [ or ]
861
			  \]
862
			)
863
			}xs',
864 62
			array($this, '_doAnchors_reference_callback'), $text);
865
866 62
		$this->in_anchor = false;
867 62
		return $text;
868
	}
869
870
	/**
871
	 * Callback for reference anchors
872
	 * @param  array $matches
873
	 * @return string
874
	 */
875 11
	protected function _doAnchors_reference_callback($matches) {
876 11
		$whole_match =  $matches[1];
877 11
		$link_text   =  $matches[2];
878 11
		$link_id     =& $matches[3];
879
880 11
		if ($link_id == "") {
881
			// for shortcut links like [this][] or [this].
882 7
			$link_id = $link_text;
883
		}
884
885
		// lower-case and turn embedded newlines into spaces
886 11
		$link_id = strtolower($link_id);
887 11
		$link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
888
889 11
		if (isset($this->urls[$link_id])) {
890 10
			$url = $this->urls[$link_id];
891 10
			$url = $this->encodeURLAttribute($url);
892
893 10
			$result = "<a href=\"$url\"";
894 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...
895 7
				$title = $this->titles[$link_id];
896 7
				$title = $this->encodeAttribute($title);
897 7
				$result .=  " title=\"$title\"";
898
			}
899 10
			if (isset($this->ref_attr[$link_id]))
900 10
				$result .= $this->ref_attr[$link_id];
901
902 10
			$link_text = $this->runSpanGamut($link_text);
903 10
			$result .= ">$link_text</a>";
904 10
			$result = $this->hashPart($result);
905
		}
906
		else {
907 3
			$result = $whole_match;
908
		}
909 11
		return $result;
910
	}
911
912
	/**
913
	 * Callback for inline anchors
914
	 * @param  array $matches
915
	 * @return string
916
	 */
917 12
	protected function _doAnchors_inline_callback($matches) {
918 12
		$link_text		=  $this->runSpanGamut($matches[2]);
919 12
		$url			=  $matches[3] === '' ? $matches[4] : $matches[3];
920 12
		$title			=& $matches[7];
921 12
		$attr  = $this->doExtraAttributes("a", $dummy =& $matches[8]);
922
923
		// if the URL was of the form <s p a c e s> it got caught by the HTML
924
		// tag parser and hashed. Need to reverse the process before using the URL.
925 12
		$unhashed = $this->unhash($url);
926 12
		if ($unhashed !== $url)
927 2
			$url = preg_replace('/^<(.*)>$/', '\1', $unhashed);
928
929 12
		$url = $this->encodeURLAttribute($url);
930
931 12
		$result = "<a href=\"$url\"";
932 12
		if (isset($title)) {
933 6
			$title = $this->encodeAttribute($title);
934 6
			$result .=  " title=\"$title\"";
935
		}
936 12
		$result .= $attr;
937
938 12
		$link_text = $this->runSpanGamut($link_text);
939 12
		$result .= ">$link_text</a>";
940
941 12
		return $this->hashPart($result);
942
	}
943
944
	/**
945
	 * Turn Markdown image shortcuts into <img> tags.
946
	 * @param  string $text
947
	 * @return string
948
	 */
949 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...
950
		// First, handle reference-style labeled images: ![alt text][id]
951 62
		$text = preg_replace_callback('{
952
			(				# wrap whole match in $1
953
			  !\[
954 62
				(' . $this->nested_brackets_re . ')		# alt text = $2
955
			  \]
956
957
			  [ ]?				# one optional space
958
			  (?:\n[ ]*)?		# one optional newline followed by spaces
959
960
			  \[
961
				(.*?)		# id = $3
962
			  \]
963
964
			)
965
			}xs',
966 62
			array($this, '_doImages_reference_callback'), $text);
967
968
		// Next, handle inline images:  ![alt text](url "optional title")
969
		// Don't forget: encode * and _
970 62
		$text = preg_replace_callback('{
971
			(				# wrap whole match in $1
972
			  !\[
973 62
				(' . $this->nested_brackets_re . ')		# alt text = $2
974
			  \]
975
			  \s?			# One optional whitespace character
976
			  \(			# literal paren
977
				[ \n]*
978
				(?:
979
					<(\S*)>	# src url = $3
980
				|
981 62
					(' . $this->nested_url_parenthesis_re . ')	# src url = $4
982
				)
983
				[ \n]*
984
				(			# $5
985
				  ([\'"])	# quote char = $6
986
				  (.*?)		# title = $7
987
				  \6		# matching quote
988
				  [ \n]*
989
				)?			# title is optional
990
			  \)
991 62
			  (?:[ ]? ' . $this->id_class_attr_catch_re . ' )?	 # $8 = id/class attributes
992
			)
993
			}xs',
994 62
			array($this, '_doImages_inline_callback'), $text);
995
996 62
		return $text;
997
	}
998
999
	/**
1000
	 * Callback for referenced images
1001
	 * @param  array $matches
1002
	 * @return string
1003
	 */
1004 3
	protected function _doImages_reference_callback($matches) {
1005 3
		$whole_match = $matches[1];
1006 3
		$alt_text    = $matches[2];
1007 3
		$link_id     = strtolower($matches[3]);
1008
1009 3
		if ($link_id === "") {
1010
			$link_id = strtolower($alt_text); // for shortcut links like ![this][].
1011
		}
1012
1013 3
		$alt_text = $this->encodeAttribute($alt_text);
1014 3
		if (isset($this->urls[$link_id])) {
1015 3
			$url = $this->encodeURLAttribute($this->urls[$link_id]);
1016 3
			$result = "<img src=\"$url\" alt=\"$alt_text\"";
1017 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...
1018 3
				$title = $this->titles[$link_id];
1019 3
				$title = $this->encodeAttribute($title);
1020 3
				$result .=  " title=\"$title\"";
1021
			}
1022 3
			if (isset($this->ref_attr[$link_id])) {
1023 3
				$result .= $this->ref_attr[$link_id];
1024
			}
1025 3
			$result .= $this->empty_element_suffix;
1026 3
			$result = $this->hashPart($result);
1027
		}
1028
		else {
1029
			// If there's no such link ID, leave intact:
1030
			$result = $whole_match;
1031
		}
1032
1033 3
		return $result;
1034
	}
1035
1036
	/**
1037
	 * Callback for inline images
1038
	 * @param  array $matches
1039
	 * @return string
1040
	 */
1041 3
	protected function _doImages_inline_callback($matches) {
1042 3
		$alt_text		= $matches[2];
1043 3
		$url			= $matches[3] === '' ? $matches[4] : $matches[3];
1044 3
		$title			=& $matches[7];
1045 3
		$attr  = $this->doExtraAttributes("img", $dummy =& $matches[8]);
1046
1047 3
		$alt_text = $this->encodeAttribute($alt_text);
1048 3
		$url = $this->encodeURLAttribute($url);
1049 3
		$result = "<img src=\"$url\" alt=\"$alt_text\"";
1050 3
		if (isset($title)) {
1051 2
			$title = $this->encodeAttribute($title);
1052 2
			$result .=  " title=\"$title\""; // $title already quoted
1053
		}
1054 3
		$result .= $attr;
1055 3
		$result .= $this->empty_element_suffix;
1056
1057 3
		return $this->hashPart($result);
1058
	}
1059
1060
	/**
1061
	 * Process markdown headers. Redefined to add ID and class attribute support.
1062
	 * @param  string $text
1063
	 * @return string
1064
	 */
1065 62
	protected function doHeaders($text) {
1066
		// Setext-style headers:
1067
		//  Header 1  {#header1}
1068
		//	  ========
1069
		//
1070
		//	  Header 2  {#header2 .class1 .class2}
1071
		//	  --------
1072
		//
1073 62
		$text = preg_replace_callback(
1074
			'{
1075
				(^.+?)								# $1: Header text
1076 62
				(?:[ ]+ ' . $this->id_class_attr_catch_re . ' )?	 # $3 = id/class attributes
1077
				[ ]*\n(=+|-+)[ ]*\n+				# $3: Header footer
1078
			}mx',
1079 62
			array($this, '_doHeaders_callback_setext'), $text);
1080
1081
		// atx-style headers:
1082
		//	# Header 1        {#header1}
1083
		//	## Header 2       {#header2}
1084
		//	## Header 2 with closing hashes ##  {#header3.class1.class2}
1085
		//	...
1086
		//	###### Header 6   {.class2}
1087
		//
1088 62
		$text = preg_replace_callback('{
1089
				^(\#{1,6})	# $1 = string of #\'s
1090 62
				[ ]'.($this->hashtag_protection ? '+' : '*').'
1091
				(.+?)		# $2 = Header text
1092
				[ ]*
1093
				\#*			# optional closing #\'s (not counted)
1094 62
				(?:[ ]+ ' . $this->id_class_attr_catch_re . ' )?	 # $3 = id/class attributes
1095
				[ ]*
1096
				\n+
1097
			}xm',
1098 62
			array($this, '_doHeaders_callback_atx'), $text);
1099
1100 62
		return $text;
1101
	}
1102
1103
	/**
1104
	 * Callback for setext headers
1105
	 * @param  array $matches
1106
	 * @return string
1107
	 */
1108 6
	protected function _doHeaders_callback_setext($matches) {
1109 6
		if ($matches[3] === '-' && preg_match('{^- }', $matches[1])) {
1110 1
			return $matches[0];
1111
		}
1112
1113 5
		$level = $matches[3]{0} === '=' ? 1 : 2;
1114
1115 5
		$defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null;
1116
1117 5
		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId);
1118 5
		$block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>";
1119 5
		return "\n" . $this->hashBlock($block) . "\n\n";
1120
	}
1121
1122
	/**
1123
	 * Callback for atx headers
1124
	 * @param  array $matches
1125
	 * @return string
1126
	 */
1127 12
	protected function _doHeaders_callback_atx($matches) {
1128 12
		$level = strlen($matches[1]);
1129
1130 12
		$defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null;
1131 12
		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId);
1132 12
		$block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>";
1133 12
		return "\n" . $this->hashBlock($block) . "\n\n";
1134
	}
1135
1136
	/**
1137
	 * Form HTML tables.
1138
	 * @param  string $text
1139
	 * @return string
1140
	 */
1141 62
	protected function doTables($text) {
1142 62
		$less_than_tab = $this->tab_width - 1;
1143
		// Find tables with leading pipe.
1144
		//
1145
		//	| Header 1 | Header 2
1146
		//	| -------- | --------
1147
		//	| Cell 1   | Cell 2
1148
		//	| Cell 3   | Cell 4
1149 62
		$text = preg_replace_callback('
1150
			{
1151
				^							# Start of a line
1152 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1153
				[|]							# Optional leading pipe (present)
1154
				(.+) \n						# $1: Header row (at least one pipe)
1155
1156 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1157
				[|] ([ ]*[-:]+[-| :]*) \n	# $2: Header underline
1158
1159
				(							# $3: Cells
1160
					(?>
1161
						[ ]*				# Allowed whitespace.
1162
						[|] .* \n			# Row content.
1163
					)*
1164
				)
1165
				(?=\n|\Z)					# Stop at final double newline.
1166
			}xm',
1167 62
			array($this, '_doTable_leadingPipe_callback'), $text);
1168
1169
		// Find tables without leading pipe.
1170
		//
1171
		//	Header 1 | Header 2
1172
		//	-------- | --------
1173
		//	Cell 1   | Cell 2
1174
		//	Cell 3   | Cell 4
1175 62
		$text = preg_replace_callback('
1176
			{
1177
				^							# Start of a line
1178 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1179
				(\S.*[|].*) \n				# $1: Header row (at least one pipe)
1180
1181 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1182
				([-:]+[ ]*[|][-| :]*) \n	# $2: Header underline
1183
1184
				(							# $3: Cells
1185
					(?>
1186
						.* [|] .* \n		# Row content
1187
					)*
1188
				)
1189
				(?=\n|\Z)					# Stop at final double newline.
1190
			}xm',
1191 62
			array($this, '_DoTable_callback'), $text);
1192
1193 62
		return $text;
1194
	}
1195
1196
	/**
1197
	 * Callback for removing the leading pipe for each row
1198
	 * @param  array $matches
1199
	 * @return string
1200
	 */
1201 1
	protected function _doTable_leadingPipe_callback($matches) {
1202 1
		$head		= $matches[1];
1203 1
		$underline	= $matches[2];
1204 1
		$content	= $matches[3];
1205
1206 1
		$content	= preg_replace('/^ *[|]/m', '', $content);
1207
1208 1
		return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
1209
	}
1210
1211
	/**
1212
	 * Make the align attribute in a table
1213
	 * @param  string $alignname
1214
	 * @return string
1215
	 */
1216 1
	protected function _doTable_makeAlignAttr($alignname)
1217
	{
1218 1
		if (empty($this->table_align_class_tmpl)) {
1219 1
			return " align=\"$alignname\"";
1220
		}
1221
1222
		$classname = str_replace('%%', $alignname, $this->table_align_class_tmpl);
1223
		return " class=\"$classname\"";
1224
	}
1225
1226
	/**
1227
	 * Calback for processing tables
1228
	 * @param  array $matches
1229
	 * @return string
1230
	 */
1231 1
	protected function _doTable_callback($matches) {
1232 1
		$head		= $matches[1];
1233 1
		$underline	= $matches[2];
1234 1
		$content	= $matches[3];
1235
1236
		// Remove any tailing pipes for each line.
1237 1
		$head		= preg_replace('/[|] *$/m', '', $head);
1238 1
		$underline	= preg_replace('/[|] *$/m', '', $underline);
1239 1
		$content	= preg_replace('/[|] *$/m', '', $content);
1240
1241
		// Reading alignement from header underline.
1242 1
		$separators	= preg_split('/ *[|] */', $underline);
1243 1
		foreach ($separators as $n => $s) {
1244 1
			if (preg_match('/^ *-+: *$/', $s))
1245 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...
1246 1
			else if (preg_match('/^ *:-+: *$/', $s))
1247 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...
1248 1
			else if (preg_match('/^ *:-+ *$/', $s))
1249 1
				$attr[$n] = $this->_doTable_makeAlignAttr('left');
1250
			else
1251 1
				$attr[$n] = '';
1252
		}
1253
1254
		// Parsing span elements, including code spans, character escapes,
1255
		// and inline HTML tags, so that pipes inside those gets ignored.
1256 1
		$head		= $this->parseSpan($head);
1257 1
		$headers	= preg_split('/ *[|] */', $head);
1258 1
		$col_count	= count($headers);
1259 1
		$attr       = array_pad($attr, $col_count, '');
1260
1261
		// Write column headers.
1262 1
		$text = "<table>\n";
1263 1
		$text .= "<thead>\n";
1264 1
		$text .= "<tr>\n";
1265 1
		foreach ($headers as $n => $header) {
1266 1
			$text .= "  <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n";
1267
		}
1268 1
		$text .= "</tr>\n";
1269 1
		$text .= "</thead>\n";
1270
1271
		// Split content by row.
1272 1
		$rows = explode("\n", trim($content, "\n"));
1273
1274 1
		$text .= "<tbody>\n";
1275 1
		foreach ($rows as $row) {
1276
			// Parsing span elements, including code spans, character escapes,
1277
			// and inline HTML tags, so that pipes inside those gets ignored.
1278 1
			$row = $this->parseSpan($row);
1279
1280
			// Split row by cell.
1281 1
			$row_cells = preg_split('/ *[|] */', $row, $col_count);
1282 1
			$row_cells = array_pad($row_cells, $col_count, '');
1283
1284 1
			$text .= "<tr>\n";
1285 1
			foreach ($row_cells as $n => $cell) {
1286 1
				$text .= "  <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n";
1287
			}
1288 1
			$text .= "</tr>\n";
1289
		}
1290 1
		$text .= "</tbody>\n";
1291 1
		$text .= "</table>";
1292
1293 1
		return $this->hashBlock($text) . "\n";
1294
	}
1295
1296
	/**
1297
	 * Form HTML definition lists.
1298
	 * @param  string $text
1299
	 * @return string
1300
	 */
1301 62
	protected function doDefLists($text) {
1302 62
		$less_than_tab = $this->tab_width - 1;
1303
1304
		// Re-usable pattern to match any entire dl list:
1305
		$whole_list_re = '(?>
1306
			(								# $1 = whole list
1307
			  (								# $2
1308 62
				[ ]{0,' . $less_than_tab . '}
1309
				((?>.*\S.*\n)+)				# $3 = defined term
1310
				\n?
1311 62
				[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1312
			  )
1313
			  (?s:.+?)
1314
			  (								# $4
1315
				  \z
1316
				|
1317
				  \n{2,}
1318
				  (?=\S)
1319
				  (?!						# Negative lookahead for another term
1320 62
					[ ]{0,' . $less_than_tab . '}
1321
					(?: \S.*\n )+?			# defined term
1322
					\n?
1323 62
					[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1324
				  )
1325
				  (?!						# Negative lookahead for another definition
1326 62
					[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1327
				  )
1328
			  )
1329
			)
1330
		)'; // mx
1331
1332 62
		$text = preg_replace_callback('{
1333
				(?>\A\n?|(?<=\n\n))
1334 62
				' . $whole_list_re . '
1335
			}mx',
1336 62
			array($this, '_doDefLists_callback'), $text);
1337
1338 62
		return $text;
1339
	}
1340
1341
	/**
1342
	 * Callback for processing definition lists
1343
	 * @param  array $matches
1344
	 * @return string
1345
	 */
1346 1
	protected function _doDefLists_callback($matches) {
1347
		// Re-usable patterns to match list item bullets and number markers:
1348 1
		$list = $matches[1];
1349
1350
		// Turn double returns into triple returns, so that we can make a
1351
		// paragraph for the last item in a list, if necessary:
1352 1
		$result = trim($this->processDefListItems($list));
1353 1
		$result = "<dl>\n" . $result . "\n</dl>";
1354 1
		return $this->hashBlock($result) . "\n\n";
1355
	}
1356
1357
	/**
1358
	 * Process the contents of a single definition list, splitting it
1359
	 * into individual term and definition list items.
1360
	 * @param  string $list_str
1361
	 * @return string
1362
	 */
1363 1
	protected function processDefListItems($list_str) {
1364
1365 1
		$less_than_tab = $this->tab_width - 1;
1366
1367
		// Trim trailing blank lines:
1368 1
		$list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
1369
1370
		// Process definition terms.
1371 1
		$list_str = preg_replace_callback('{
1372
			(?>\A\n?|\n\n+)						# leading line
1373
			(									# definition terms = $1
1374 1
				[ ]{0,' . $less_than_tab . '}	# leading whitespace
1375
				(?!\:[ ]|[ ])					# negative lookahead for a definition
1376
												#   mark (colon) or more whitespace.
1377
				(?> \S.* \n)+?					# actual term (not whitespace).
1378
			)
1379
			(?=\n?[ ]{0,3}:[ ])					# lookahead for following line feed
1380
												#   with a definition mark.
1381
			}xm',
1382 1
			array($this, '_processDefListItems_callback_dt'), $list_str);
1383
1384
		// Process actual definitions.
1385 1
		$list_str = preg_replace_callback('{
1386
			\n(\n+)?							# leading line = $1
1387
			(									# marker space = $2
1388 1
				[ ]{0,' . $less_than_tab . '}	# whitespace before colon
1389
				\:[ ]+							# definition mark (colon)
1390
			)
1391
			((?s:.+?))							# definition text = $3
1392
			(?= \n+ 							# stop at next definition mark,
1393
				(?:								# next term or end of text
1394 1
					[ ]{0,' . $less_than_tab . '} \:[ ]	|
1395
					<dt> | \z
1396
				)
1397
			)
1398
			}xm',
1399 1
			array($this, '_processDefListItems_callback_dd'), $list_str);
1400
1401 1
		return $list_str;
1402
	}
1403
1404
	/**
1405
	 * Callback for <dt> elements in definition lists
1406
	 * @param  array $matches
1407
	 * @return string
1408
	 */
1409 1
	protected function _processDefListItems_callback_dt($matches) {
1410 1
		$terms = explode("\n", trim($matches[1]));
1411 1
		$text = '';
1412 1
		foreach ($terms as $term) {
1413 1
			$term = $this->runSpanGamut(trim($term));
1414 1
			$text .= "\n<dt>" . $term . "</dt>";
1415
		}
1416 1
		return $text . "\n";
1417
	}
1418
1419
	/**
1420
	 * Callback for <dd> elements in definition lists
1421
	 * @param  array $matches
1422
	 * @return string
1423
	 */
1424 1
	protected function _processDefListItems_callback_dd($matches) {
1425 1
		$leading_line	= $matches[1];
1426 1
		$marker_space	= $matches[2];
1427 1
		$def			= $matches[3];
1428
1429 1
		if ($leading_line || preg_match('/\n{2,}/', $def)) {
1430
			// Replace marker with the appropriate whitespace indentation
1431 1
			$def = str_repeat(' ', strlen($marker_space)) . $def;
1432 1
			$def = $this->runBlockGamut($this->outdent($def . "\n\n"));
1433 1
			$def = "\n". $def ."\n";
1434
		}
1435
		else {
1436 1
			$def = rtrim($def);
1437 1
			$def = $this->runSpanGamut($this->outdent($def));
1438
		}
1439
1440 1
		return "\n<dd>" . $def . "</dd>\n";
1441
	}
1442
1443
	/**
1444
	 * Adding the fenced code block syntax to regular Markdown:
1445
	 *
1446
	 * ~~~
1447
	 * Code block
1448
	 * ~~~
1449
	 *
1450
	 * @param  string $text
1451
	 * @return string
1452
	 */
1453 62
	protected function doFencedCodeBlocks($text) {
1454
1455 62
		$text = preg_replace_callback('{
1456
				(?:\n|\A)
1457
				# 1: Opening marker
1458
				(
1459
					(?:~{3,}|`{3,}) # 3 or more tildes/backticks.
1460
				)
1461
				[ ]*
1462
				(?:
1463
					\.?([-_:a-zA-Z0-9]+) # 2: standalone class name
1464
				)?
1465
				[ ]*
1466
				(?:
1467 62
					' . $this->id_class_attr_catch_re . ' # 3: Extra attributes
1468
				)?
1469
				[ ]* \n # Whitespace and newline following marker.
1470
1471
				# 4: Content
1472
				(
1473
					(?>
1474
						(?!\1 [ ]* \n)	# Not a closing marker.
1475
						.*\n+
1476
					)+
1477
				)
1478
1479
				# Closing marker.
1480
				\1 [ ]* (?= \n )
1481
			}xm',
1482 62
			array($this, '_doFencedCodeBlocks_callback'), $text);
1483
1484 62
		return $text;
1485
	}
1486
1487
	/**
1488
	 * Callback to process fenced code blocks
1489
	 * @param  array $matches
1490
	 * @return string
1491
	 */
1492 4
	protected function _doFencedCodeBlocks_callback($matches) {
1493 4
		$classname =& $matches[2];
1494 4
		$attrs     =& $matches[3];
1495 4
		$codeblock = $matches[4];
1496
1497 4 View Code Duplication
		if ($this->code_block_content_func) {
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...
1498
			$codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname);
1499
		} else {
1500 4
			$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
1501
		}
1502
1503 4
		$codeblock = preg_replace_callback('/^\n+/',
1504 4
			array($this, '_doFencedCodeBlocks_newlines'), $codeblock);
1505
1506 4
		$classes = array();
1507 4
		if ($classname !== "") {
1508 4
			if ($classname{0} === '.') {
1509
				$classname = substr($classname, 1);
1510
			}
1511 4
			$classes[] = $this->code_class_prefix . $classname;
1512
		}
1513 4
		$attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes);
1514 4
		$pre_attr_str  = $this->code_attr_on_pre ? $attr_str : '';
1515 4
		$code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
1516 4
		$codeblock  = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
1517
1518 4
		return "\n\n".$this->hashBlock($codeblock)."\n\n";
1519
	}
1520
1521
	/**
1522
	 * Replace new lines in fenced code blocks
1523
	 * @param  array $matches
1524
	 * @return string
1525
	 */
1526 2
	protected function _doFencedCodeBlocks_newlines($matches) {
1527 2
		return str_repeat("<br$this->empty_element_suffix",
1528 2
			strlen($matches[0]));
1529
	}
1530
1531
	/**
1532
	 * Redefining emphasis markers so that emphasis by underscore does not
1533
	 * work in the middle of a word.
1534
	 * @var array
1535
	 */
1536
	protected $em_relist = array(
1537
		''  => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)',
1538
		'*' => '(?<![\s*])\*(?!\*)',
1539
		'_' => '(?<![\s_])_(?![a-zA-Z0-9_])',
1540
	);
1541
	protected $strong_relist = array(
1542
		''   => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)',
1543
		'**' => '(?<![\s*])\*\*(?!\*)',
1544
		'__' => '(?<![\s_])__(?![a-zA-Z0-9_])',
1545
	);
1546
	protected $em_strong_relist = array(
1547
		''    => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)',
1548
		'***' => '(?<![\s*])\*\*\*(?!\*)',
1549
		'___' => '(?<![\s_])___(?![a-zA-Z0-9_])',
1550
	);
1551
1552
	/**
1553
	 * Parse text into paragraphs
1554
	 * @param  string $text String to process in paragraphs
1555
	 * @param  boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags
1556
	 * @return string       HTML output
1557
	 */
1558 62
	protected function formParagraphs($text, $wrap_in_p = true) {
1559
		// Strip leading and trailing lines:
1560 62
		$text = preg_replace('/\A\n+|\n+\z/', '', $text);
1561
1562 62
		$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
1563
1564
		// Wrap <p> tags and unhashify HTML blocks
1565 62
		foreach ($grafs as $key => $value) {
1566 62
			$value = trim($this->runSpanGamut($value));
1567
1568
			// Check if this should be enclosed in a paragraph.
1569
			// Clean tag hashes & block tag hashes are left alone.
1570 62
			$is_p = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
1571
1572 62
			if ($is_p) {
1573 58
				$value = "<p>$value</p>";
1574
			}
1575 62
			$grafs[$key] = $value;
1576
		}
1577
1578
		// Join grafs in one text, then unhash HTML tags.
1579 62
		$text = implode("\n\n", $grafs);
1580
1581
		// Finish by removing any tag hashes still present in $text.
1582 62
		$text = $this->unhash($text);
1583
1584 62
		return $text;
1585
	}
1586
1587
1588
	/**
1589
	 * Footnotes - Strips link definitions from text, stores the URLs and
1590
	 * titles in hash references.
1591
	 * @param  string $text
1592
	 * @return string
1593
	 */
1594 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...
1595 62
		$less_than_tab = $this->tab_width - 1;
1596
1597
		// Link defs are in the form: [^id]: url "optional title"
1598 62
		$text = preg_replace_callback('{
1599 62
			^[ ]{0,' . $less_than_tab . '}\[\^(.+?)\][ ]?:	# note_id = $1
1600
			  [ ]*
1601
			  \n?					# maybe *one* newline
1602
			(						# text = $2 (no blank lines allowed)
1603
				(?:
1604
					.+				# actual text
1605
				|
1606
					\n				# newlines but
1607
					(?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker.
1608
					(?!\n+[ ]{0,3}\S)# ensure line is not blank and followed
1609
									# by non-indented content
1610
				)*
1611
			)
1612
			}xm',
1613 62
			array($this, '_stripFootnotes_callback'),
1614 62
			$text);
1615 62
		return $text;
1616
	}
1617
1618
	/**
1619
	 * Callback for stripping footnotes
1620
	 * @param  array $matches
1621
	 * @return string
1622
	 */
1623 1
	protected function _stripFootnotes_callback($matches) {
1624 1
		$note_id = $this->fn_id_prefix . $matches[1];
1625 1
		$this->footnotes[$note_id] = $this->outdent($matches[2]);
1626 1
		return ''; // String that will replace the block
1627
	}
1628
1629
	/**
1630
	 * Replace footnote references in $text [^id] with a special text-token
1631
	 * which will be replaced by the actual footnote marker in appendFootnotes.
1632
	 * @param  string $text
1633
	 * @return string
1634
	 */
1635 62
	protected function doFootnotes($text) {
1636 62
		if (!$this->in_anchor) {
1637 62
			$text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
1638
		}
1639 62
		return $text;
1640
	}
1641
1642
	/**
1643
	 * Append footnote list to text
1644
	 * @param  string $text
1645
	 * @return string
1646
	 */
1647 62
	protected function appendFootnotes($text) {
1648 62
		$text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1649 62
			array($this, '_appendFootnotes_callback'), $text);
1650
1651 62
		if ( ! empty( $this->footnotes_ordered ) ) {
1652 1
			$this->_doFootnotes();
1653 1
			if ( ! $this->omit_footnotes ) {
1654 1
				$text .= "\n\n";
1655 1
				$text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n";
1656 1
				$text .= "<hr" . $this->empty_element_suffix . "\n";
1657 1
				$text .= $this->footnotes_assembled;
1658 1
				$text .= "</div>";
1659
			}
1660
		}
1661 62
		return $text;
1662
	}
1663
1664
1665
	/**
1666
	 * Generates the HTML for footnotes.  Called by appendFootnotes, even if footnotes are not being appended.
1667
	 * @return void
1668
	 */
1669 1
	protected function _doFootnotes() {
1670 1
		$attr = "";
1671 1
		if ($this->fn_backlink_class !== "") {
1672 1
			$class = $this->fn_backlink_class;
1673 1
			$class = $this->encodeAttribute($class);
1674 1
			$attr .= " class=\"$class\"";
1675
		}
1676 1
		if ($this->fn_backlink_title !== "") {
1677
			$title = $this->fn_backlink_title;
1678
			$title = $this->encodeAttribute($title);
1679
			$attr .= " title=\"$title\"";
1680
			$attr .= " aria-label=\"$title\"";
1681
		}
1682 1
		$attr .= " role=\"doc-backlink\"";
1683 1
		$backlink_text = $this->fn_backlink_html;
1684 1
		$num = 0;
1685
1686 1
		$text = "<ol>\n\n";
1687 1
		while (!empty($this->footnotes_ordered)) {
1688 1
			$footnote = reset($this->footnotes_ordered);
1689 1
			$note_id = key($this->footnotes_ordered);
1690 1
			unset($this->footnotes_ordered[$note_id]);
1691 1
			$ref_count = $this->footnotes_ref_count[$note_id];
1692 1
			unset($this->footnotes_ref_count[$note_id]);
1693 1
			unset($this->footnotes[$note_id]);
1694
1695 1
			$footnote .= "\n"; // Need to append newline before parsing.
1696 1
			$footnote = $this->runBlockGamut("$footnote\n");
1697 1
			$footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1698 1
				array($this, '_appendFootnotes_callback'), $footnote);
1699
1700 1
			$attr = str_replace("%%", ++$num, $attr);
1701 1
			$note_id = $this->encodeAttribute($note_id);
1702
1703
			// Prepare backlink, multiple backlinks if multiple references
1704 1
			$backlink = "<a href=\"#fnref:$note_id\"$attr>$backlink_text</a>";
1705 1
			for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) {
1706 1
				$backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>$backlink_text</a>";
1707
			}
1708
			// Add backlink to last paragraph; create new paragraph if needed.
1709 1
			if (preg_match('{</p>$}', $footnote)) {
1710 1
				$footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
1711
			} else {
1712 1
				$footnote .= "\n\n<p>$backlink</p>";
1713
			}
1714
1715 1
			$text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n";
1716 1
			$text .= $footnote . "\n";
1717 1
			$text .= "</li>\n\n";
1718
		}
1719 1
		$text .= "</ol>\n";
1720
1721 1
		$this->footnotes_assembled = $text;
1722 1
	}
1723
1724
	/**
1725
	 * Callback for appending footnotes
1726
	 * @param  array $matches
1727
	 * @return string
1728
	 */
1729 1
	protected function _appendFootnotes_callback($matches) {
1730 1
		$node_id = $this->fn_id_prefix . $matches[1];
1731
1732
		// Create footnote marker only if it has a corresponding footnote *and*
1733
		// the footnote hasn't been used by another marker.
1734 1
		if (isset($this->footnotes[$node_id])) {
1735 1
			$num =& $this->footnotes_numbers[$node_id];
1736 1
			if (!isset($num)) {
1737
				// Transfer footnote content to the ordered list and give it its
1738
				// number
1739 1
				$this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
1740 1
				$this->footnotes_ref_count[$node_id] = 1;
1741 1
				$num = $this->footnote_counter++;
1742 1
				$ref_count_mark = '';
1743
			} else {
1744 1
				$ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
1745
			}
1746
1747 1
			$attr = "";
1748 1
			if ($this->fn_link_class !== "") {
1749 1
				$class = $this->fn_link_class;
1750 1
				$class = $this->encodeAttribute($class);
1751 1
				$attr .= " class=\"$class\"";
1752
			}
1753 1
			if ($this->fn_link_title !== "") {
1754
				$title = $this->fn_link_title;
1755
				$title = $this->encodeAttribute($title);
1756
				$attr .= " title=\"$title\"";
1757
			}
1758 1
			$attr .= " role=\"doc-noteref\"";
1759
1760 1
			$attr = str_replace("%%", $num, $attr);
1761 1
			$node_id = $this->encodeAttribute($node_id);
1762
1763
			return
1764 1
				"<sup id=\"fnref$ref_count_mark:$node_id\">".
1765 1
				"<a href=\"#fn:$node_id\"$attr>$num</a>".
1766 1
				"</sup>";
1767
		}
1768
1769 1
		return "[^" . $matches[1] . "]";
1770
	}
1771
1772
1773
	/**
1774
	 * Abbreviations - strips abbreviations from text, stores titles in hash
1775
	 * references.
1776
	 * @param  string $text
1777
	 * @return string
1778
	 */
1779 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...
1780 62
		$less_than_tab = $this->tab_width - 1;
1781
1782
		// Link defs are in the form: [id]*: url "optional title"
1783 62
		$text = preg_replace_callback('{
1784 62
			^[ ]{0,' . $less_than_tab . '}\*\[(.+?)\][ ]?:	# abbr_id = $1
1785
			(.*)					# text = $2 (no blank lines allowed)
1786
			}xm',
1787 62
			array($this, '_stripAbbreviations_callback'),
1788 62
			$text);
1789 62
		return $text;
1790
	}
1791
1792
	/**
1793
	 * Callback for stripping abbreviations
1794
	 * @param  array $matches
1795
	 * @return string
1796
	 */
1797 1
	protected function _stripAbbreviations_callback($matches) {
1798 1
		$abbr_word = $matches[1];
1799 1
		$abbr_desc = $matches[2];
1800 1
		if ($this->abbr_word_re) {
1801 1
			$this->abbr_word_re .= '|';
1802
		}
1803 1
		$this->abbr_word_re .= preg_quote($abbr_word);
1804 1
		$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
1805 1
		return ''; // String that will replace the block
1806
	}
1807
1808
	/**
1809
	 * Find defined abbreviations in text and wrap them in <abbr> elements.
1810
	 * @param  string $text
1811
	 * @return string
1812
	 */
1813 62
	protected function doAbbreviations($text) {
1814 62
		if ($this->abbr_word_re) {
1815
			// cannot use the /x modifier because abbr_word_re may
1816
			// contain significant spaces:
1817 3
			$text = preg_replace_callback('{' .
1818
				'(?<![\w\x1A])' .
1819 3
				'(?:' . $this->abbr_word_re . ')' .
1820 3
				'(?![\w\x1A])' .
1821 3
				'}',
1822 3
				array($this, '_doAbbreviations_callback'), $text);
1823
		}
1824 62
		return $text;
1825
	}
1826
1827
	/**
1828
	 * Callback for processing abbreviations
1829
	 * @param  array $matches
1830
	 * @return string
1831
	 */
1832 3
	protected function _doAbbreviations_callback($matches) {
1833 3
		$abbr = $matches[0];
1834 3
		if (isset($this->abbr_desciptions[$abbr])) {
1835 3
			$desc = $this->abbr_desciptions[$abbr];
1836 3
			if (empty($desc)) {
1837
				return $this->hashPart("<abbr>$abbr</abbr>");
1838
			}
1839 3
			$desc = $this->encodeAttribute($desc);
1840 3
			return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
1841
		}
1842
		return $matches[0];
1843
	}
1844
}
1845