Test Failed
Pull Request — lib (#351)
by Aurélien
03:33
created

MarkdownExtra::_doImages_inline_callback()   B

Complexity

Conditions 8
Paths 32

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 14.8028

Importance

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