Passed
Pull Request — lib (#351)
by Aurélien
03:02
created

MarkdownExtra::_doImages_reference_callback()   B

Complexity

Conditions 10
Paths 74

Size

Total Lines 38

Duplication

Lines 12
Ratio 31.58 %

Code Coverage

Tests 19
CRAP Score 11.3824

Importance

Changes 0
Metric Value
dl 12
loc 38
ccs 19
cts 25
cp 0.76
rs 7.6666
c 0
b 0
f 0
cc 10
nc 74
nop 1
crap 11.3824

How to fix   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-2019 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|details|summary';
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_quote		=& $matches[6];
936 12
		$title			=& $matches[7];
937 12
		$attr  = $this->doExtraAttributes("a", $dummy =& $matches[8]);
938
939
		// if the URL was of the form <s p a c e s> it got caught by the HTML
940
		// tag parser and hashed. Need to reverse the process before using the URL.
941 12
		$unhashed = $this->unhash($url);
942 12
		if ($unhashed !== $url)
943 2
			$url = preg_replace('/^<(.*)>$/', '\1', $unhashed);
944
945 12
		$url = $this->encodeURLAttribute($url);
946
947 12
		$result = "<a href=\"$url\"";
948 12
		if (isset($title) && $title_quote) {
949 6
			$title = $this->encodeAttribute($title);
950 6
			$result .=  " title=\"$title\"";
951
		}
952 12
		$result .= $attr;
953
954 12
		$link_text = $this->runSpanGamut($link_text);
955 12
		$result .= ">$link_text</a>";
956
957 12
		return $this->hashPart($result);
958
	}
959
960
	/**
961
	 * Turn Markdown image shortcuts into <img> tags.
962
	 * @param  string $text
963
	 * @return string
964
	 */
965 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...
966
		// First, handle reference-style labeled images: ![alt text][id]
967 62
		$text = preg_replace_callback('{
968
			(				# wrap whole match in $1
969
			  !\[
970 62
				(' . $this->nested_brackets_re . ')		# alt text = $2
971
			  \]
972
973
			  [ ]?				# one optional space
974
			  (?:\n[ ]*)?		# one optional newline followed by spaces
975
976
			  \[
977
				(.*?)		# id = $3
978
			  \]
979
980
			)
981
			}xs',
982 62
			array($this, '_doImages_reference_callback'), $text);
983
984
		// Next, handle inline images:  ![alt text](url "optional title")
985
		// Don't forget: encode * and _
986 62
		$text = preg_replace_callback('{
987
			(				# wrap whole match in $1
988
			  !\[
989 62
				(' . $this->nested_brackets_re . ')		# alt text = $2
990
			  \]
991
			  \s?			# One optional whitespace character
992
			  \(			# literal paren
993
				[ \n]*
994
				(?:
995
					<(\S*)>	# src url = $3
996
				|
997 62
					(' . $this->nested_url_parenthesis_re . ')	# src url = $4
998
				)
999
				[ \n]*
1000
				(			# $5
1001
				  ([\'"])	# quote char = $6
1002
				  (.*?)		# title = $7
1003
				  \6		# matching quote
1004
				  [ \n]*
1005
				)?			# title is optional
1006
			  \)
1007 62
			  (?:[ ]? ' . $this->id_class_attr_catch_re . ' )?	 # $8 = id/class attributes
1008
			)
1009
			}xs',
1010 62
			array($this, '_doImages_inline_callback'), $text);
1011
1012 62
		return $text;
1013
	}
1014
1015
	/**
1016
	 * Callback for referenced images
1017
	 * @param  array $matches
1018
	 * @return string
1019
	 */
1020 3
	protected function _doImages_reference_callback($matches) {
1021 3
		$whole_match = $matches[1];
1022 3
		$alt_text    = $matches[2];
1023 3
		$link_id     = strtolower($matches[3]);
1024
1025 3
		if ($link_id === "") {
1026
			$link_id = strtolower($alt_text); // for shortcut links like ![this][].
1027
		}
1028
1029 3
		$alt_text = $this->encodeAttribute($alt_text);
1030 3
		if (isset($this->urls[$link_id])) {
1031 3
			$url = $this->encodeURLAttribute($this->urls[$link_id]);
1032 3
			$result = "<img src=\"$url\" alt=\"$alt_text\"";
1033 3 View Code Duplication
			if(file_exists($url))
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...
1034
			{
1035
				list($width, $height, $type, $attr) = getimagesize($url);
0 ignored issues
show
Unused Code introduced by
The assignment to $type is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $attr is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1036
				if(isset($width)) $result .= " width=\"$width\"";
1037
				if(isset($height)) $result .= " height=\"$height\"";
1038
				if(isset($width) && isset($height)) $result .= " loading=\"lazy\"";
1039
			}
1040 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...
1041 3
				$title = $this->titles[$link_id];
1042 3
				$title = $this->encodeAttribute($title);
1043 3
				$result .=  " title=\"$title\"";
1044
			}
1045 3
			if (isset($this->ref_attr[$link_id])) {
1046 3
				$result .= $this->ref_attr[$link_id];
1047
			}
1048 3
			$result .= $this->empty_element_suffix;
1049 3
			$result = $this->hashPart($result);
1050
		}
1051
		else {
1052
			// If there's no such link ID, leave intact:
1053
			$result = $whole_match;
1054
		}
1055
1056 3
		return $result;
1057
	}
1058
1059
	/**
1060
	 * Callback for inline images
1061
	 * @param  array $matches
1062
	 * @return string
1063
	 */
1064 3
	protected function _doImages_inline_callback($matches) {
1065 3
		$alt_text		= $matches[2];
1066 3
		$url			= $matches[3] === '' ? $matches[4] : $matches[3];
1067 3
		$title_quote		=& $matches[6];
1068 3
		$title			=& $matches[7];
1069 3
		$attr  = $this->doExtraAttributes("img", $dummy =& $matches[8]);
1070
1071 3
		$alt_text = $this->encodeAttribute($alt_text);
1072 3
		$url = $this->encodeURLAttribute($url);
1073 3
		$result = "<img src=\"$url\" alt=\"$alt_text\"";
1074 3 View Code Duplication
		if(file_exists($url))
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...
1075
		{
1076
			list($width, $height, $type, $attr) = getimagesize($url);
0 ignored issues
show
Unused Code introduced by
The assignment to $type is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1077
			if(isset($width)) $result .= " width=\"$width\"";
1078
			if(isset($height)) $result .= " height=\"$height\"";
1079
			if(isset($width) && isset($height)) $result .= " loading=\"lazy\"";
1080
		}
1081 3
		if (isset($title) && $title_quote) {
1082 2
			$title = $this->encodeAttribute($title);
1083 2
			$result .=  " title=\"$title\""; // $title already quoted
1084
		}
1085 3
		$result .= $attr;
1086 3
		$result .= $this->empty_element_suffix;
1087
1088 3
		return $this->hashPart($result);
1089
	}
1090
1091
	/**
1092
	 * Process markdown headers. Redefined to add ID and class attribute support.
1093
	 * @param  string $text
1094
	 * @return string
1095
	 */
1096 62
	protected function doHeaders($text) {
1097
		// Setext-style headers:
1098
		//  Header 1  {#header1}
1099
		//	  ========
1100
		//
1101
		//	  Header 2  {#header2 .class1 .class2}
1102
		//	  --------
1103
		//
1104 62
		$text = preg_replace_callback(
1105
			'{
1106
				(^.+?)								# $1: Header text
1107 62
				(?:[ ]+ ' . $this->id_class_attr_catch_re . ' )?	 # $3 = id/class attributes
1108
				[ ]*\n(=+|-+)[ ]*\n+				# $3: Header footer
1109
			}mx',
1110 62
			array($this, '_doHeaders_callback_setext'), $text);
1111
1112
		// atx-style headers:
1113
		//	# Header 1        {#header1}
1114
		//	## Header 2       {#header2}
1115
		//	## Header 2 with closing hashes ##  {#header3.class1.class2}
1116
		//	...
1117
		//	###### Header 6   {.class2}
1118
		//
1119 62
		$text = preg_replace_callback('{
1120
				^(\#{1,6})	# $1 = string of #\'s
1121 62
				[ ]'.($this->hashtag_protection ? '+' : '*').'
1122
				(.+?)		# $2 = Header text
1123
				[ ]*
1124
				\#*			# optional closing #\'s (not counted)
1125 62
				(?:[ ]+ ' . $this->id_class_attr_catch_re . ' )?	 # $3 = id/class attributes
1126
				[ ]*
1127
				\n+
1128
			}xm',
1129 62
			array($this, '_doHeaders_callback_atx'), $text);
1130
1131 62
		return $text;
1132
	}
1133
1134
	/**
1135
	 * Callback for setext headers
1136
	 * @param  array $matches
1137
	 * @return string
1138
	 */
1139 6
	protected function _doHeaders_callback_setext($matches) {
1140 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...
1141 1
			return $matches[0];
1142
		}
1143
1144 5
		$level = $matches[3][0] === '=' ? 1 : 2;
1145
1146 5
		$defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null;
1147
1148 5
		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId);
1149 5
		$block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>";
1150 5
		return "\n" . $this->hashBlock($block) . "\n\n";
1151
	}
1152
1153
	/**
1154
	 * Callback for atx headers
1155
	 * @param  array $matches
1156
	 * @return string
1157
	 */
1158 12
	protected function _doHeaders_callback_atx($matches) {
1159 12
		$level = strlen($matches[1]);
1160
1161 12
		$defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null;
1162 12
		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId);
1163 12
		$block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>";
1164 12
		return "\n" . $this->hashBlock($block) . "\n\n";
1165
	}
1166
1167
	/**
1168
	 * Form HTML tables.
1169
	 * @param  string $text
1170
	 * @return string
1171
	 */
1172 62
	protected function doTables($text) {
1173 62
		$less_than_tab = $this->tab_width - 1;
1174
		// Find tables with leading pipe.
1175
		//
1176
		//	| Header 1 | Header 2
1177
		//	| -------- | --------
1178
		//	| Cell 1   | Cell 2
1179
		//	| Cell 3   | Cell 4
1180 62
		$text = preg_replace_callback('
1181
			{
1182
				^							# Start of a line
1183 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1184
				[|]							# Optional leading pipe (present)
1185
				(.+) \n						# $1: Header row (at least one pipe)
1186
1187 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1188
				[|] ([ ]*[-:]+[-| :]*) \n	# $2: Header underline
1189
1190
				(							# $3: Cells
1191
					(?>
1192
						[ ]*				# Allowed whitespace.
1193
						[|] .* \n			# Row content.
1194
					)*
1195
				)
1196
				(?=\n|\Z)					# Stop at final double newline.
1197
			}xm',
1198 62
			array($this, '_doTable_leadingPipe_callback'), $text);
1199
1200
		// Find tables without leading pipe.
1201
		//
1202
		//	Header 1 | Header 2
1203
		//	-------- | --------
1204
		//	Cell 1   | Cell 2
1205
		//	Cell 3   | Cell 4
1206 62
		$text = preg_replace_callback('
1207
			{
1208
				^							# Start of a line
1209 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1210
				(\S.*[|].*) \n				# $1: Header row (at least one pipe)
1211
1212 62
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1213
				([-:]+[ ]*[|][-| :]*) \n	# $2: Header underline
1214
1215
				(							# $3: Cells
1216
					(?>
1217
						.* [|] .* \n		# Row content
1218
					)*
1219
				)
1220
				(?=\n|\Z)					# Stop at final double newline.
1221
			}xm',
1222 62
			array($this, '_DoTable_callback'), $text);
1223
1224 62
		return $text;
1225
	}
1226
1227
	/**
1228
	 * Callback for removing the leading pipe for each row
1229
	 * @param  array $matches
1230
	 * @return string
1231
	 */
1232 1
	protected function _doTable_leadingPipe_callback($matches) {
1233 1
		$head		= $matches[1];
1234 1
		$underline	= $matches[2];
1235 1
		$content	= $matches[3];
1236
1237 1
		$content	= preg_replace('/^ *[|]/m', '', $content);
1238
1239 1
		return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
1240
	}
1241
1242
	/**
1243
	 * Make the align attribute in a table
1244
	 * @param  string $alignname
1245
	 * @return string
1246
	 */
1247 1
	protected function _doTable_makeAlignAttr($alignname) {
1248 1
		if (empty($this->table_align_class_tmpl)) {
1249 1
			return " align=\"$alignname\"";
1250
		}
1251
1252
		$classname = str_replace('%%', $alignname, $this->table_align_class_tmpl);
1253
		return " class=\"$classname\"";
1254
	}
1255
1256
	/**
1257
	 * Calback for processing tables
1258
	 * @param  array $matches
1259
	 * @return string
1260
	 */
1261 1
	protected function _doTable_callback($matches) {
1262 1
		$head		= $matches[1];
1263 1
		$underline	= $matches[2];
1264 1
		$content	= $matches[3];
1265
1266
		// Remove any tailing pipes for each line.
1267 1
		$head		= preg_replace('/[|] *$/m', '', $head);
1268 1
		$underline	= preg_replace('/[|] *$/m', '', $underline);
1269 1
		$content	= preg_replace('/[|] *$/m', '', $content);
1270
1271
		// Reading alignement from header underline.
1272 1
		$separators	= preg_split('/ *[|] */', $underline);
1273 1
		foreach ($separators as $n => $s) {
1274 1
			if (preg_match('/^ *-+: *$/', $s))
1275 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...
1276 1
			else if (preg_match('/^ *:-+: *$/', $s))
1277 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...
1278 1
			else if (preg_match('/^ *:-+ *$/', $s))
1279 1
				$attr[$n] = $this->_doTable_makeAlignAttr('left');
1280
			else
1281 1
				$attr[$n] = '';
1282
		}
1283
1284
		// Parsing span elements, including code spans, character escapes,
1285
		// and inline HTML tags, so that pipes inside those gets ignored.
1286 1
		$head		= $this->parseSpan($head);
1287 1
		$headers	= preg_split('/ *[|] */', $head);
1288 1
		$col_count	= count($headers);
1289 1
		$attr       = array_pad($attr, $col_count, '');
1290
1291
		// Write column headers.
1292 1
		$text = "<table>\n";
1293 1
		$text .= "<thead>\n";
1294 1
		$text .= "<tr>\n";
1295 1
		foreach ($headers as $n => $header) {
1296 1
			$text .= "  <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n";
1297
		}
1298 1
		$text .= "</tr>\n";
1299 1
		$text .= "</thead>\n";
1300
1301
		// Split content by row.
1302 1
		$rows = explode("\n", trim($content, "\n"));
1303
1304 1
		$text .= "<tbody>\n";
1305 1
		foreach ($rows as $row) {
1306
			// Parsing span elements, including code spans, character escapes,
1307
			// and inline HTML tags, so that pipes inside those gets ignored.
1308 1
			$row = $this->parseSpan($row);
1309
1310
			// Split row by cell.
1311 1
			$row_cells = preg_split('/ *[|] */', $row, $col_count);
1312 1
			$row_cells = array_pad($row_cells, $col_count, '');
1313
1314 1
			$text .= "<tr>\n";
1315 1
			foreach ($row_cells as $n => $cell) {
1316 1
				$text .= "  <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n";
1317
			}
1318 1
			$text .= "</tr>\n";
1319
		}
1320 1
		$text .= "</tbody>\n";
1321 1
		$text .= "</table>";
1322
1323 1
		return $this->hashBlock($text) . "\n";
1324
	}
1325
1326
	/**
1327
	 * Form HTML definition lists.
1328
	 * @param  string $text
1329
	 * @return string
1330
	 */
1331 62
	protected function doDefLists($text) {
1332 62
		$less_than_tab = $this->tab_width - 1;
1333
1334
		// Re-usable pattern to match any entire dl list:
1335
		$whole_list_re = '(?>
1336
			(								# $1 = whole list
1337
			  (								# $2
1338 62
				[ ]{0,' . $less_than_tab . '}
1339
				((?>.*\S.*\n)+)				# $3 = defined term
1340
				\n?
1341 62
				[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1342
			  )
1343
			  (?s:.+?)
1344
			  (								# $4
1345
				  \z
1346
				|
1347
				  \n{2,}
1348
				  (?=\S)
1349
				  (?!						# Negative lookahead for another term
1350 62
					[ ]{0,' . $less_than_tab . '}
1351
					(?: \S.*\n )+?			# defined term
1352
					\n?
1353 62
					[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1354
				  )
1355
				  (?!						# Negative lookahead for another definition
1356 62
					[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1357
				  )
1358
			  )
1359
			)
1360
		)'; // mx
1361
1362 62
		$text = preg_replace_callback('{
1363
				(?>\A\n?|(?<=\n\n))
1364 62
				' . $whole_list_re . '
1365
			}mx',
1366 62
			array($this, '_doDefLists_callback'), $text);
1367
1368 62
		return $text;
1369
	}
1370
1371
	/**
1372
	 * Callback for processing definition lists
1373
	 * @param  array $matches
1374
	 * @return string
1375
	 */
1376 1
	protected function _doDefLists_callback($matches) {
1377
		// Re-usable patterns to match list item bullets and number markers:
1378 1
		$list = $matches[1];
1379
1380
		// Turn double returns into triple returns, so that we can make a
1381
		// paragraph for the last item in a list, if necessary:
1382 1
		$result = trim($this->processDefListItems($list));
1383 1
		$result = "<dl>\n" . $result . "\n</dl>";
1384 1
		return $this->hashBlock($result) . "\n\n";
1385
	}
1386
1387
	/**
1388
	 * Process the contents of a single definition list, splitting it
1389
	 * into individual term and definition list items.
1390
	 * @param  string $list_str
1391
	 * @return string
1392
	 */
1393 1
	protected function processDefListItems($list_str) {
1394
1395 1
		$less_than_tab = $this->tab_width - 1;
1396
1397
		// Trim trailing blank lines:
1398 1
		$list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
1399
1400
		// Process definition terms.
1401 1
		$list_str = preg_replace_callback('{
1402
			(?>\A\n?|\n\n+)						# leading line
1403
			(									# definition terms = $1
1404 1
				[ ]{0,' . $less_than_tab . '}	# leading whitespace
1405
				(?!\:[ ]|[ ])					# negative lookahead for a definition
1406
												#   mark (colon) or more whitespace.
1407
				(?> \S.* \n)+?					# actual term (not whitespace).
1408
			)
1409
			(?=\n?[ ]{0,3}:[ ])					# lookahead for following line feed
1410
												#   with a definition mark.
1411
			}xm',
1412 1
			array($this, '_processDefListItems_callback_dt'), $list_str);
1413
1414
		// Process actual definitions.
1415 1
		$list_str = preg_replace_callback('{
1416
			\n(\n+)?							# leading line = $1
1417
			(									# marker space = $2
1418 1
				[ ]{0,' . $less_than_tab . '}	# whitespace before colon
1419
				\:[ ]+							# definition mark (colon)
1420
			)
1421
			((?s:.+?))							# definition text = $3
1422
			(?= \n+ 							# stop at next definition mark,
1423
				(?:								# next term or end of text
1424 1
					[ ]{0,' . $less_than_tab . '} \:[ ]	|
1425
					<dt> | \z
1426
				)
1427
			)
1428
			}xm',
1429 1
			array($this, '_processDefListItems_callback_dd'), $list_str);
1430
1431 1
		return $list_str;
1432
	}
1433
1434
	/**
1435
	 * Callback for <dt> elements in definition lists
1436
	 * @param  array $matches
1437
	 * @return string
1438
	 */
1439 1
	protected function _processDefListItems_callback_dt($matches) {
1440 1
		$terms = explode("\n", trim($matches[1]));
1441 1
		$text = '';
1442 1
		foreach ($terms as $term) {
1443 1
			$term = $this->runSpanGamut(trim($term));
1444 1
			$text .= "\n<dt>" . $term . "</dt>";
1445
		}
1446 1
		return $text . "\n";
1447
	}
1448
1449
	/**
1450
	 * Callback for <dd> elements in definition lists
1451
	 * @param  array $matches
1452
	 * @return string
1453
	 */
1454 1
	protected function _processDefListItems_callback_dd($matches) {
1455 1
		$leading_line	= $matches[1];
1456 1
		$marker_space	= $matches[2];
1457 1
		$def			= $matches[3];
1458
1459 1
		if ($leading_line || preg_match('/\n{2,}/', $def)) {
1460
			// Replace marker with the appropriate whitespace indentation
1461 1
			$def = str_repeat(' ', strlen($marker_space)) . $def;
1462 1
			$def = $this->runBlockGamut($this->outdent($def . "\n\n"));
1463 1
			$def = "\n". $def ."\n";
1464
		}
1465
		else {
1466 1
			$def = rtrim($def);
1467 1
			$def = $this->runSpanGamut($this->outdent($def));
1468
		}
1469
1470 1
		return "\n<dd>" . $def . "</dd>\n";
1471
	}
1472
1473
	/**
1474
	 * Adding the fenced code block syntax to regular Markdown:
1475
	 *
1476
	 * ~~~
1477
	 * Code block
1478
	 * ~~~
1479
	 *
1480
	 * @param  string $text
1481
	 * @return string
1482
	 */
1483 62
	protected function doFencedCodeBlocks($text) {
1484
1485 62
		$text = preg_replace_callback('{
1486
				(?:\n|\A)
1487
				# 1: Opening marker
1488
				(
1489
					(?:~{3,}|`{3,}) # 3 or more tildes/backticks.
1490
				)
1491
				[ ]*
1492
				(?:
1493
					\.?([-_:a-zA-Z0-9]+) # 2: standalone class name
1494
				)?
1495
				[ ]*
1496
				(?:
1497 62
					' . $this->id_class_attr_catch_re . ' # 3: Extra attributes
1498
				)?
1499
				[ ]* \n # Whitespace and newline following marker.
1500
1501
				# 4: Content
1502
				(
1503
					(?>
1504
						(?!\1 [ ]* \n)	# Not a closing marker.
1505
						.*\n+
1506
					)+
1507
				)
1508
1509
				# Closing marker.
1510
				\1 [ ]* (?= \n )
1511
			}xm',
1512 62
			array($this, '_doFencedCodeBlocks_callback'), $text);
1513
1514 62
		return $text;
1515
	}
1516
1517
	/**
1518
	 * Callback to process fenced code blocks
1519
	 * @param  array $matches
1520
	 * @return string
1521
	 */
1522 4
	protected function _doFencedCodeBlocks_callback($matches) {
1523 4
		$classname =& $matches[2];
1524 4
		$attrs     =& $matches[3];
1525 4
		$codeblock = $matches[4];
1526
1527 4
		if ($this->code_block_content_func) {
1528
			$codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname);
1529
		} else {
1530 4
			$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
1531
		}
1532
1533 4
		$codeblock = preg_replace_callback('/^\n+/',
1534 4
			array($this, '_doFencedCodeBlocks_newlines'), $codeblock);
1535
1536 4
		$classes = array();
1537 4
		if ($classname !== "") {
1538 4
			if ($classname[0] === '.') {
1539
				$classname = substr($classname, 1);
1540
			}
1541 4
			$classes[] = $this->code_class_prefix . $classname;
1542
		}
1543 4
		$attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes);
1544 4
		$pre_attr_str  = $this->code_attr_on_pre ? $attr_str : '';
1545 4
		$code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
1546 4
		$codeblock  = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
1547
1548 4
		return "\n\n".$this->hashBlock($codeblock)."\n\n";
1549
	}
1550
1551
	/**
1552
	 * Replace new lines in fenced code blocks
1553
	 * @param  array $matches
1554
	 * @return string
1555
	 */
1556 2
	protected function _doFencedCodeBlocks_newlines($matches) {
1557 2
		return str_repeat("<br$this->empty_element_suffix",
1558 2
			strlen($matches[0]));
1559
	}
1560
1561
	/**
1562
	 * Redefining emphasis markers so that emphasis by underscore does not
1563
	 * work in the middle of a word.
1564
	 * @var array
1565
	 */
1566
	protected $em_relist = array(
1567
		''  => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)',
1568
		'*' => '(?<![\s*])\*(?!\*)',
1569
		'_' => '(?<![\s_])_(?![a-zA-Z0-9_])',
1570
	);
1571
	protected $strong_relist = array(
1572
		''   => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)',
1573
		'**' => '(?<![\s*])\*\*(?!\*)',
1574
		'__' => '(?<![\s_])__(?![a-zA-Z0-9_])',
1575
	);
1576
	protected $em_strong_relist = array(
1577
		''    => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)',
1578
		'***' => '(?<![\s*])\*\*\*(?!\*)',
1579
		'___' => '(?<![\s_])___(?![a-zA-Z0-9_])',
1580
	);
1581
1582
	/**
1583
	 * Parse text into paragraphs
1584
	 * @param  string $text String to process in paragraphs
1585
	 * @param  boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags
1586
	 * @return string       HTML output
1587
	 */
1588 62
	protected function formParagraphs($text, $wrap_in_p = true) {
1589
		// Strip leading and trailing lines:
1590 62
		$text = preg_replace('/\A\n+|\n+\z/', '', $text);
1591
1592 62
		$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
1593
1594
		// Wrap <p> tags and unhashify HTML blocks
1595 62
		foreach ($grafs as $key => $value) {
1596 62
			$value = trim($this->runSpanGamut($value));
1597
1598
			// Check if this should be enclosed in a paragraph.
1599
			// Clean tag hashes & block tag hashes are left alone.
1600 62
			$is_p = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
1601
1602 62
			if ($is_p) {
1603 58
				$value = "<p>$value</p>";
1604
			}
1605 62
			$grafs[$key] = $value;
1606
		}
1607
1608
		// Join grafs in one text, then unhash HTML tags.
1609 62
		$text = implode("\n\n", $grafs);
1610
1611
		// Finish by removing any tag hashes still present in $text.
1612 62
		$text = $this->unhash($text);
1613
1614 62
		return $text;
1615
	}
1616
1617
1618
	/**
1619
	 * Footnotes - Strips link definitions from text, stores the URLs and
1620
	 * titles in hash references.
1621
	 * @param  string $text
1622
	 * @return string
1623
	 */
1624 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...
1625 62
		$less_than_tab = $this->tab_width - 1;
1626
1627
		// Link defs are in the form: [^id]: url "optional title"
1628 62
		$text = preg_replace_callback('{
1629 62
			^[ ]{0,' . $less_than_tab . '}\[\^(.+?)\][ ]?:	# note_id = $1
1630
			  [ ]*
1631
			  \n?					# maybe *one* newline
1632
			(						# text = $2 (no blank lines allowed)
1633
				(?:
1634
					.+				# actual text
1635
				|
1636
					\n				# newlines but
1637
					(?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker.
1638
					(?!\n+[ ]{0,3}\S)# ensure line is not blank and followed
1639
									# by non-indented content
1640
				)*
1641
			)
1642
			}xm',
1643 62
			array($this, '_stripFootnotes_callback'),
1644 62
			$text);
1645 62
		return $text;
1646
	}
1647
1648
	/**
1649
	 * Callback for stripping footnotes
1650
	 * @param  array $matches
1651
	 * @return string
1652
	 */
1653 1
	protected function _stripFootnotes_callback($matches) {
1654 1
		$note_id = $this->fn_id_prefix . $matches[1];
1655 1
		$this->footnotes[$note_id] = $this->outdent($matches[2]);
1656 1
		return ''; // String that will replace the block
1657
	}
1658
1659
	/**
1660
	 * Replace footnote references in $text [^id] with a special text-token
1661
	 * which will be replaced by the actual footnote marker in appendFootnotes.
1662
	 * @param  string $text
1663
	 * @return string
1664
	 */
1665 62
	protected function doFootnotes($text) {
1666 62
		if (!$this->in_anchor) {
1667 62
			$text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
1668
		}
1669 62
		return $text;
1670
	}
1671
1672
	/**
1673
	 * Append footnote list to text
1674
	 * @param  string $text
1675
	 * @return string
1676
	 */
1677 62
	protected function appendFootnotes($text) {
1678 62
		$text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1679 62
			array($this, '_appendFootnotes_callback'), $text);
1680
1681 62
		if ( ! empty( $this->footnotes_ordered ) ) {
1682 1
			$this->_doFootnotes();
1683 1
			if ( ! $this->omit_footnotes ) {
1684 1
				$text .= "\n\n";
1685 1
				$text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n";
1686 1
				$text .= "<hr" . $this->empty_element_suffix . "\n";
1687 1
				$text .= $this->footnotes_assembled;
1688 1
				$text .= "</div>";
1689
			}
1690
		}
1691 62
		return $text;
1692
	}
1693
1694
1695
	/**
1696
	 * Generates the HTML for footnotes.  Called by appendFootnotes, even if
1697
	 * footnotes are not being appended.
1698
	 * @return void
1699
	 */
1700 1
	protected function _doFootnotes() {
1701 1
		$attr = array();
1702 1
		if ($this->fn_backlink_class !== "") {
1703 1
			$class = $this->fn_backlink_class;
1704 1
			$class = $this->encodeAttribute($class);
1705 1
			$attr['class'] = " class=\"$class\"";
1706
		}
1707 1
		$attr['role'] = " role=\"doc-backlink\"";
1708 1
		$num = 0;
1709
1710 1
		$text = "<ol>\n\n";
1711 1
		while (!empty($this->footnotes_ordered)) {
1712 1
			$footnote = reset($this->footnotes_ordered);
1713 1
			$note_id = key($this->footnotes_ordered);
1714 1
			unset($this->footnotes_ordered[$note_id]);
1715 1
			$ref_count = $this->footnotes_ref_count[$note_id];
1716 1
			unset($this->footnotes_ref_count[$note_id]);
1717 1
			unset($this->footnotes[$note_id]);
1718
1719 1
			$footnote .= "\n"; // Need to append newline before parsing.
1720 1
			$footnote = $this->runBlockGamut("$footnote\n");
1721 1
			$footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1722 1
				array($this, '_appendFootnotes_callback'), $footnote);
1723
1724 1
			$num++;
1725 1
			$note_id = $this->encodeAttribute($note_id);
1726
1727
			// Prepare backlink, multiple backlinks if multiple references
1728
			// Do not create empty backlinks if the html is blank
1729 1
			$backlink = "";
1730 1
			if (!empty($this->fn_backlink_html)) {
1731 1
				for ($ref_num = 1; $ref_num <= $ref_count; ++$ref_num) {
1732 1
					if (!empty($this->fn_backlink_title)) {
1733
						$attr['title'] = ' title="' . $this->encodeAttribute($this->fn_backlink_title) . '"';
1734
					}
1735 1
					if (!empty($this->fn_backlink_label)) {
1736
						$attr['label'] = ' aria-label="' . $this->encodeAttribute($this->fn_backlink_label) . '"';
1737
					}
1738 1
					$parsed_attr = $this->parseFootnotePlaceholders(
1739 1
						implode('', $attr),
1740 1
						$num,
1741 1
						$ref_num
1742
					);
1743 1
					$backlink_text = $this->parseFootnotePlaceholders(
1744 1
						$this->fn_backlink_html,
1745 1
						$num,
1746 1
						$ref_num
1747
					);
1748 1
					$ref_count_mark = $ref_num > 1 ? $ref_num : '';
1749 1
					$backlink .= " <a href=\"#fnref$ref_count_mark:$note_id\"$parsed_attr>$backlink_text</a>";
1750
				}
1751 1
				$backlink = trim($backlink);
1752
			}
1753
1754
			// Add backlink to last paragraph; create new paragraph if needed.
1755 1
			if (!empty($backlink)) {
1756 1
				if (preg_match('{</p>$}', $footnote)) {
1757 1
					$footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
1758
				} else {
1759 1
					$footnote .= "\n\n<p>$backlink</p>";
1760
				}
1761
			}
1762
1763 1
			$text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n";
1764 1
			$text .= $footnote . "\n";
1765 1
			$text .= "</li>\n\n";
1766
		}
1767 1
		$text .= "</ol>\n";
1768
1769 1
		$this->footnotes_assembled = $text;
1770 1
	}
1771
1772
	/**
1773
	 * Callback for appending footnotes
1774
	 * @param  array $matches
1775
	 * @return string
1776
	 */
1777 1
	protected function _appendFootnotes_callback($matches) {
1778 1
		$node_id = $this->fn_id_prefix . $matches[1];
1779
1780
		// Create footnote marker only if it has a corresponding footnote *and*
1781
		// the footnote hasn't been used by another marker.
1782 1
		if (isset($this->footnotes[$node_id])) {
1783 1
			$num =& $this->footnotes_numbers[$node_id];
1784 1
			if (!isset($num)) {
1785
				// Transfer footnote content to the ordered list and give it its
1786
				// number
1787 1
				$this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
1788 1
				$this->footnotes_ref_count[$node_id] = 1;
1789 1
				$num = $this->footnote_counter++;
1790 1
				$ref_count_mark = '';
1791
			} else {
1792 1
				$ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
1793
			}
1794
1795 1
			$attr = "";
1796 1
			if ($this->fn_link_class !== "") {
1797 1
				$class = $this->fn_link_class;
1798 1
				$class = $this->encodeAttribute($class);
1799 1
				$attr .= " class=\"$class\"";
1800
			}
1801 1
			if ($this->fn_link_title !== "") {
1802
				$title = $this->fn_link_title;
1803
				$title = $this->encodeAttribute($title);
1804
				$attr .= " title=\"$title\"";
1805
			}
1806 1
			$attr .= " role=\"doc-noteref\"";
1807
1808 1
			$attr = str_replace("%%", $num, $attr);
1809 1
			$node_id = $this->encodeAttribute($node_id);
1810
1811
			return
1812 1
				"<sup id=\"fnref$ref_count_mark:$node_id\">".
1813 1
				"<a href=\"#fn:$node_id\"$attr>$num</a>".
1814 1
				"</sup>";
1815
		}
1816
1817 1
		return "[^" . $matches[1] . "]";
1818
	}
1819
1820
	/**
1821
	 * Build footnote label by evaluating any placeholders.
1822
	 * - ^^  footnote number
1823
	 * - %%  footnote reference number (Nth reference to footnote number)
1824
	 * @param  string $label
1825
	 * @param  int    $footnote_number
1826
	 * @param  int    $reference_number
1827
	 * @return string
1828
	 */
1829 1
	protected function parseFootnotePlaceholders($label, $footnote_number, $reference_number) {
1830 1
		return str_replace(
1831 1
			array('^^', '%%'),
1832 1
			array($footnote_number, $reference_number),
1833 1
			$label
1834
		);
1835
	}
1836
1837
1838
	/**
1839
	 * Abbreviations - strips abbreviations from text, stores titles in hash
1840
	 * references.
1841
	 * @param  string $text
1842
	 * @return string
1843
	 */
1844 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...
1845 62
		$less_than_tab = $this->tab_width - 1;
1846
1847
		// Link defs are in the form: [id]*: url "optional title"
1848 62
		$text = preg_replace_callback('{
1849 62
			^[ ]{0,' . $less_than_tab . '}\*\[(.+?)\][ ]?:	# abbr_id = $1
1850
			(.*)					# text = $2 (no blank lines allowed)
1851
			}xm',
1852 62
			array($this, '_stripAbbreviations_callback'),
1853 62
			$text);
1854 62
		return $text;
1855
	}
1856
1857
	/**
1858
	 * Callback for stripping abbreviations
1859
	 * @param  array $matches
1860
	 * @return string
1861
	 */
1862 1
	protected function _stripAbbreviations_callback($matches) {
1863 1
		$abbr_word = $matches[1];
1864 1
		$abbr_desc = $matches[2];
1865 1
		if ($this->abbr_word_re) {
1866 1
			$this->abbr_word_re .= '|';
1867
		}
1868 1
		$this->abbr_word_re .= preg_quote($abbr_word);
1869 1
		$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
1870 1
		return ''; // String that will replace the block
1871
	}
1872
1873
	/**
1874
	 * Find defined abbreviations in text and wrap them in <abbr> elements.
1875
	 * @param  string $text
1876
	 * @return string
1877
	 */
1878 62
	protected function doAbbreviations($text) {
1879 62
		if ($this->abbr_word_re) {
1880
			// cannot use the /x modifier because abbr_word_re may
1881
			// contain significant spaces:
1882 3
			$text = preg_replace_callback('{' .
1883
				'(?<![\w\x1A])' .
1884 3
				'(?:' . $this->abbr_word_re . ')' .
1885 3
				'(?![\w\x1A])' .
1886 3
				'}',
1887 3
				array($this, '_doAbbreviations_callback'), $text);
1888
		}
1889 62
		return $text;
1890
	}
1891
1892
	/**
1893
	 * Callback for processing abbreviations
1894
	 * @param  array $matches
1895
	 * @return string
1896
	 */
1897 3
	protected function _doAbbreviations_callback($matches) {
1898 3
		$abbr = $matches[0];
1899 3
		if (isset($this->abbr_desciptions[$abbr])) {
1900 3
			$desc = $this->abbr_desciptions[$abbr];
1901 3
			if (empty($desc)) {
1902
				return $this->hashPart("<abbr>$abbr</abbr>");
1903
			}
1904 3
			$desc = $this->encodeAttribute($desc);
1905 3
			return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
1906
		}
1907
		return $matches[0];
1908
	}
1909
}
1910