Completed
Push — lib ( 9da5a6...89a5d6 )
by Michel
03:33
created

MarkdownExtra::_doTable_callback()   C

Complexity

Conditions 8
Paths 30

Size

Total Lines 62
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 8

Importance

Changes 0
Metric Value
dl 0
loc 62
ccs 40
cts 40
cp 1
rs 6.943
c 0
b 0
f 0
cc 8
eloc 41
nc 30
nop 1
crap 8

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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

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

Please refer to the PHP core documentation on constructors.

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

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
251
252
		// Compose attributes as string
253 6
		$attr_str = "";
254 6
		if (!empty($id)) {
255 4
			$attr_str .= ' id="'.$this->encodeAttribute($id) .'"';
256
		}
257 6
		if (!empty($classes)) {
258 6
			$attr_str .= ' class="'. implode(" ", $classes) . '"';
259
		}
260 6
		if (!$this->no_markup && !empty($attributes)) {
261 1
			$attr_str .= ' '.implode(" ", $attributes);
262
		}
263 6
		return $attr_str;
264
	}
265
266
	/**
267
	 * Strips link definitions from text, stores the URLs and titles in
268
	 * hash references.
269
	 * @param  string $text
270
	 * @return string
271
	 */
272 59 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...
273 59
		$less_than_tab = $this->tab_width - 1;
274
275
		// Link defs are in the form: ^[id]: url "optional title"
276 59
		$text = preg_replace_callback('{
277 59
							^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?:	# id = $1
278
							  [ ]*
279
							  \n?				# maybe *one* newline
280
							  [ ]*
281
							(?:
282
							  <(.+?)>			# url = $2
283
							|
284
							  (\S+?)			# url = $3
285
							)
286
							  [ ]*
287
							  \n?				# maybe one newline
288
							  [ ]*
289
							(?:
290
								(?<=\s)			# lookbehind for whitespace
291
								["(]
292
								(.*?)			# title = $4
293
								[")]
294
								[ ]*
295
							)?	# title is optional
296 59
					(?:[ ]* '.$this->id_class_attr_catch_re.' )?  # $5 = extra id & class attr
297
							(?:\n+|\Z)
298
			}xm',
299 59
			array($this, '_stripLinkDefinitions_callback'),
300 59
			$text);
301 59
		return $text;
302
	}
303
304
	/**
305
	 * Strip link definition callback
306
	 * @param  array $matches
307
	 * @return string
308
	 */
309 11
	protected function _stripLinkDefinitions_callback($matches) {
310 11
		$link_id = strtolower($matches[1]);
311 11
		$url = $matches[2] == '' ? $matches[3] : $matches[2];
312 11
		$this->urls[$link_id] = $url;
313 11
		$this->titles[$link_id] =& $matches[4];
314 11
		$this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]);
0 ignored issues
show
Bug introduced by
The property ref_attr does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

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

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
515
516
			// Check for: Fenced code block marker.
517
			// Note: need to recheck the whole tag to disambiguate backtick
518
			// fences from code spans
519 39
			if (preg_match('{^\n?([ ]{0,' . ($indent + 3) . '})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+)?[ ]*(?:' . $this->id_class_attr_nocatch_re . ')?[ ]*\n?$}', $tag, $capture)) {
520
				// Fenced code block marker: find matching end marker.
521 4
				$fence_indent = strlen($capture[1]); // use captured indent in re
522 4
				$fence_re = $capture[2]; // use captured fence in re
523 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...
524 4
					$matches))
525
				{
526
					// End marker found: pass text unchanged until marker.
527 4
					$parsed .= $tag . $matches[0];
528 4
					$text = substr($text, strlen($matches[0]));
529
				}
530
				else {
531
					// No end marker: just skip it.
532 4
					$parsed .= $tag;
533
				}
534
			}
535
			// Check for: Indented code block.
536 39
			else if ($tag{0} == "\n" || $tag{0} == " ") {
537
				// Indented code block: pass it unchanged, will be handled
538
				// later.
539 23
				$parsed .= $tag;
540
			}
541
			// Check for: Code span marker
542
			// Note: need to check this after backtick fenced code blocks
543 27
			else if ($tag{0} == "`") {
544
				// Find corresponding end marker.
545 13
				$tag_re = preg_quote($tag);
546 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...
547 13
					$text, $matches))
548
				{
549
					// End marker found: pass text unchanged until marker.
550 12
					$parsed .= $tag . $matches[0];
551 12
					$text = substr($text, strlen($matches[0]));
552
				}
553
				else {
554
					// Unmatched marker: just skip it.
555 13
					$parsed .= $tag;
556
				}
557
			}
558
			// Check for: Opening Block level tag or
559
			//            Opening Context Block tag (like ins and del)
560
			//               used as a block tag (tag is alone on it's line).
561 24
			else if (preg_match('{^<(?:' . $this->block_tags_re . ')\b}', $tag) ||
562 19
				(	preg_match('{^<(?:' . $this->context_block_tags_re . ')\b}', $tag) &&
563 19
					preg_match($newline_before_re, $parsed) &&
564 24
					preg_match($newline_after_re, $text)	)
565
				)
566
			{
567
				// Need to parse tag and following text using the HTML parser.
568
				list($block_text, $text) =
569 10
					$this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
570
571
				// Make sure it stays outside of any paragraph by adding newlines.
572 10
				$parsed .= "\n\n$block_text\n\n";
573
			}
574
			// Check for: Clean tag (like script, math)
575
			//            HTML Comments, processing instructions.
576 19
			else if (preg_match('{^<(?:' . $this->clean_tags_re . ')\b}', $tag) ||
577 19
				$tag{1} == '!' || $tag{1} == '?')
578
			{
579
				// Need to parse tag and following text using the HTML parser.
580
				// (don't check for markdown attribute)
581
				list($block_text, $text) =
582 3
					$this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
725 3
					$span_mode = $this->mode == 'span' || $this->mode != 'block' &&
726 3
						preg_match('{^<(?:' . $this->contain_span_tags_re . ')\b}', $tag);
727
728
					// Calculate indent before tag.
729 3
					if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
730 3
						$strlen = $this->utf8_strlen;
731 3
						$indent = $strlen($matches[1], 'UTF-8');
732
					} else {
733
						$indent = 0;
734
					}
735
736
					// End preceding block with this tag.
737 3
					$block_text .= $tag;
738 3
					$parsed .= $this->$hash_method($block_text);
739
740
					// Get enclosing tag name for the ParseMarkdown function.
741
					// (This pattern makes $tag_name_re safe without quoting.)
742 3
					preg_match('/^<([\w:$]*)\b/', $tag, $matches);
743 3
					$tag_name_re = $matches[1];
744
745
					// Parse the content using the HTML-in-Markdown parser.
746
					list ($block_text, $text)
747 3
						= $this->_hashHTMLBlocks_inMarkdown($text, $indent,
748 3
							$tag_name_re, $span_mode);
749
750
					// Outdent markdown text.
751 3
					if ($indent > 0) {
752 1
						$block_text = preg_replace("/^[ ]{1,$indent}/m", "",
753 1
													$block_text);
754
					}
755
756
					// Append tag content to parsed text.
757 3
					if (!$span_mode)	$parsed .= "\n\n$block_text\n\n";
758 1
					else				$parsed .= "$block_text";
759
760
					// Start over with a new block.
761 3
					$block_text = "";
762
				}
763 10
				else $block_text .= $tag;
764
			}
765
766 12
		} while ($depth > 0);
767
768
		// Hash last block text that wasn't processed inside the loop.
769 12
		$parsed .= $this->$hash_method($block_text);
770
771 12
		return array($parsed, $text);
772
	}
773
774
	/**
775
	 * Called whenever a tag must be hashed when a function inserts a "clean" tag
776
	 * in $text, it passes through this function and is automaticaly escaped,
777
	 * blocking invalid nested overlap.
778
	 * @param  string $text
779
	 * @return string
780
	 */
781 3
	protected function hashClean($text) {
782 3
		return $this->hashPart($text, 'C');
783
	}
784
785
	/**
786
	 * Turn Markdown link shortcuts into XHTML <a> tags.
787
	 * @param  string $text
788
	 * @return string
789
	 */
790 59 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...
791 59
		if ($this->in_anchor) {
792 15
			return $text;
793
		}
794 59
		$this->in_anchor = true;
795
796
		// First, handle reference-style links: [link text] [id]
797 59
		$text = preg_replace_callback('{
798
			(					# wrap whole match in $1
799
			  \[
800 59
				(' . $this->nested_brackets_re . ')	# link text = $2
801
			  \]
802
803
			  [ ]?				# one optional space
804
			  (?:\n[ ]*)?		# one optional newline followed by spaces
805
806
			  \[
807
				(.*?)		# id = $3
808
			  \]
809
			)
810
			}xs',
811 59
			array($this, '_doAnchors_reference_callback'), $text);
812
813
		// Next, inline-style links: [link text](url "optional title")
814 59
		$text = preg_replace_callback('{
815
			(				# wrap whole match in $1
816
			  \[
817 59
				(' . $this->nested_brackets_re . ')	# link text = $2
818
			  \]
819
			  \(			# literal paren
820
				[ \n]*
821
				(?:
822
					<(.+?)>	# href = $3
823
				|
824 59
					(' . $this->nested_url_parenthesis_re . ')	# href = $4
825
				)
826
				[ \n]*
827
				(			# $5
828
				  ([\'"])	# quote char = $6
829
				  (.*?)		# Title = $7
830
				  \6		# matching quote
831
				  [ \n]*	# ignore any spaces/tabs between closing quote and )
832
				)?			# title is optional
833
			  \)
834 59
			  (?:[ ]? ' . $this->id_class_attr_catch_re . ' )?	 # $8 = id/class attributes
835
			)
836
			}xs',
837 59
			array($this, '_doAnchors_inline_callback'), $text);
838
839
		// Last, handle reference-style shortcuts: [link text]
840
		// These must come last in case you've also got [link text][1]
841
		// or [link text](/foo)
842 59
		$text = preg_replace_callback('{
843
			(					# wrap whole match in $1
844
			  \[
845
				([^\[\]]+)		# link text = $2; can\'t contain [ or ]
846
			  \]
847
			)
848
			}xs',
849 59
			array($this, '_doAnchors_reference_callback'), $text);
850
851 59
		$this->in_anchor = false;
852 59
		return $text;
853
	}
854
855
	/**
856
	 * Callback for reference anchors
857
	 * @param  array $matches
858
	 * @return string
859
	 */
860 11
	protected function _doAnchors_reference_callback($matches) {
861 11
		$whole_match =  $matches[1];
862 11
		$link_text   =  $matches[2];
863 11
		$link_id     =& $matches[3];
864
865 11
		if ($link_id == "") {
866
			// for shortcut links like [this][] or [this].
867 7
			$link_id = $link_text;
868
		}
869
870
		// lower-case and turn embedded newlines into spaces
871 11
		$link_id = strtolower($link_id);
872 11
		$link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
873
874 11
		if (isset($this->urls[$link_id])) {
875 10
			$url = $this->urls[$link_id];
876 10
			$url = $this->encodeURLAttribute($url);
877
878 10
			$result = "<a href=\"$url\"";
879 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...
880 7
				$title = $this->titles[$link_id];
881 7
				$title = $this->encodeAttribute($title);
882 7
				$result .=  " title=\"$title\"";
883
			}
884 10
			if (isset($this->ref_attr[$link_id]))
885 10
				$result .= $this->ref_attr[$link_id];
886
887 10
			$link_text = $this->runSpanGamut($link_text);
888 10
			$result .= ">$link_text</a>";
889 10
			$result = $this->hashPart($result);
890
		}
891
		else {
892 3
			$result = $whole_match;
893
		}
894 11
		return $result;
895
	}
896
897
	/**
898
	 * Callback for inline anchors
899
	 * @param  array $matches
900
	 * @return string
901
	 */
902 12
	protected function _doAnchors_inline_callback($matches) {
903 12
		$whole_match	=  $matches[1];
0 ignored issues
show
Unused Code introduced by
$whole_match is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
904 12
		$link_text		=  $this->runSpanGamut($matches[2]);
905 12
		$url			=  $matches[3] == '' ? $matches[4] : $matches[3];
906 12
		$title			=& $matches[7];
907 12
		$attr  = $this->doExtraAttributes("a", $dummy =& $matches[8]);
908
909
		// if the URL was of the form <s p a c e s> it got caught by the HTML
910
		// tag parser and hashed. Need to reverse the process before using the URL.
911 12
		$unhashed = $this->unhash($url);
912 12
		if ($unhashed != $url)
913 2
			$url = preg_replace('/^<(.*)>$/', '\1', $unhashed);
914
915 12
		$url = $this->encodeURLAttribute($url);
916
917 12
		$result = "<a href=\"$url\"";
918 12
		if (isset($title)) {
919 6
			$title = $this->encodeAttribute($title);
920 6
			$result .=  " title=\"$title\"";
921
		}
922 12
		$result .= $attr;
923
924 12
		$link_text = $this->runSpanGamut($link_text);
925 12
		$result .= ">$link_text</a>";
926
927 12
		return $this->hashPart($result);
928
	}
929
930
	/**
931
	 * Turn Markdown image shortcuts into <img> tags.
932
	 * @param  string $text
933
	 * @return string
934
	 */
935 59 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...
936
		// First, handle reference-style labeled images: ![alt text][id]
937 59
		$text = preg_replace_callback('{
938
			(				# wrap whole match in $1
939
			  !\[
940 59
				(' . $this->nested_brackets_re . ')		# alt text = $2
941
			  \]
942
943
			  [ ]?				# one optional space
944
			  (?:\n[ ]*)?		# one optional newline followed by spaces
945
946
			  \[
947
				(.*?)		# id = $3
948
			  \]
949
950
			)
951
			}xs',
952 59
			array($this, '_doImages_reference_callback'), $text);
953
954
		// Next, handle inline images:  ![alt text](url "optional title")
955
		// Don't forget: encode * and _
956 59
		$text = preg_replace_callback('{
957
			(				# wrap whole match in $1
958
			  !\[
959 59
				(' . $this->nested_brackets_re . ')		# alt text = $2
960
			  \]
961
			  \s?			# One optional whitespace character
962
			  \(			# literal paren
963
				[ \n]*
964
				(?:
965
					<(\S*)>	# src url = $3
966
				|
967 59
					(' . $this->nested_url_parenthesis_re . ')	# src url = $4
968
				)
969
				[ \n]*
970
				(			# $5
971
				  ([\'"])	# quote char = $6
972
				  (.*?)		# title = $7
973
				  \6		# matching quote
974
				  [ \n]*
975
				)?			# title is optional
976
			  \)
977 59
			  (?:[ ]? ' . $this->id_class_attr_catch_re . ' )?	 # $8 = id/class attributes
978
			)
979
			}xs',
980 59
			array($this, '_doImages_inline_callback'), $text);
981
982 59
		return $text;
983
	}
984
985
	/**
986
	 * Callback for referenced images
987
	 * @param  array $matches
988
	 * @return string
989
	 */
990 3
	protected function _doImages_reference_callback($matches) {
991 3
		$whole_match = $matches[1];
992 3
		$alt_text    = $matches[2];
993 3
		$link_id     = strtolower($matches[3]);
994
995 3
		if ($link_id == "") {
996
			$link_id = strtolower($alt_text); // for shortcut links like ![this][].
997
		}
998
999 3
		$alt_text = $this->encodeAttribute($alt_text);
1000 3
		if (isset($this->urls[$link_id])) {
1001 3
			$url = $this->encodeURLAttribute($this->urls[$link_id]);
1002 3
			$result = "<img src=\"$url\" alt=\"$alt_text\"";
1003 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...
1004 3
				$title = $this->titles[$link_id];
1005 3
				$title = $this->encodeAttribute($title);
1006 3
				$result .=  " title=\"$title\"";
1007
			}
1008 3
			if (isset($this->ref_attr[$link_id]))
1009 3
				$result .= $this->ref_attr[$link_id];
1010 3
			$result .= $this->empty_element_suffix;
1011 3
			$result = $this->hashPart($result);
1012
		}
1013
		else {
1014
			// If there's no such link ID, leave intact:
1015
			$result = $whole_match;
1016
		}
1017
1018 3
		return $result;
1019
	}
1020
1021
	/**
1022
	 * Callback for inline images
1023
	 * @param  array $matches
1024
	 * @return string
1025
	 */
1026 3
	protected function _doImages_inline_callback($matches) {
1027 3
		$whole_match	= $matches[1];
0 ignored issues
show
Unused Code introduced by
$whole_match is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1028 3
		$alt_text		= $matches[2];
1029 3
		$url			= $matches[3] == '' ? $matches[4] : $matches[3];
1030 3
		$title			=& $matches[7];
1031 3
		$attr  = $this->doExtraAttributes("img", $dummy =& $matches[8]);
1032
1033 3
		$alt_text = $this->encodeAttribute($alt_text);
1034 3
		$url = $this->encodeURLAttribute($url);
1035 3
		$result = "<img src=\"$url\" alt=\"$alt_text\"";
1036 3
		if (isset($title)) {
1037 2
			$title = $this->encodeAttribute($title);
1038 2
			$result .=  " title=\"$title\""; // $title already quoted
1039
		}
1040 3
		$result .= $attr;
1041 3
		$result .= $this->empty_element_suffix;
1042
1043 3
		return $this->hashPart($result);
1044
	}
1045
1046
	/**
1047
	 * Process markdown headers. Redefined to add ID and class attribute support.
1048
	 * @param  string $text
1049
	 * @return string
1050
	 */
1051 59
	protected function doHeaders($text) {
1052
		// Setext-style headers:
1053
		//  Header 1  {#header1}
1054
		//	  ========
1055
		//
1056
		//	  Header 2  {#header2 .class1 .class2}
1057
		//	  --------
1058
		//
1059 59
		$text = preg_replace_callback(
1060
			'{
1061
				(^.+?)								# $1: Header text
1062 59
				(?:[ ]+ ' . $this->id_class_attr_catch_re . ' )?	 # $3 = id/class attributes
1063
				[ ]*\n(=+|-+)[ ]*\n+				# $3: Header footer
1064
			}mx',
1065 59
			array($this, '_doHeaders_callback_setext'), $text);
1066
1067
		// atx-style headers:
1068
		//	# Header 1        {#header1}
1069
		//	## Header 2       {#header2}
1070
		//	## Header 2 with closing hashes ##  {#header3.class1.class2}
1071
		//	...
1072
		//	###### Header 6   {.class2}
1073
		//
1074 59
		$text = preg_replace_callback('{
1075
				^(\#{1,6})	# $1 = string of #\'s
1076 59
				[ ]'.($this->hashtag_protection ? '+' : '*').'
1077
				(.+?)		# $2 = Header text
1078
				[ ]*
1079
				\#*			# optional closing #\'s (not counted)
1080 59
				(?:[ ]+ ' . $this->id_class_attr_catch_re . ' )?	 # $3 = id/class attributes
1081
				[ ]*
1082
				\n+
1083
			}xm',
1084 59
			array($this, '_doHeaders_callback_atx'), $text);
1085
1086 59
		return $text;
1087
	}
1088
1089
	/**
1090
	 * Callback for setext headers
1091
	 * @param  array $matches
1092
	 * @return string
1093
	 */
1094 6
	protected function _doHeaders_callback_setext($matches) {
1095 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...
1096 1
			return $matches[0];
1097
		}
1098
1099 5
		$level = $matches[3]{0} == '=' ? 1 : 2;
1100
1101 5
		$defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null;
1102
1103 5
		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId);
1104 5
		$block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>";
1105 5
		return "\n" . $this->hashBlock($block) . "\n\n";
1106
	}
1107
1108
	/**
1109
	 * Callback for atx headers
1110
	 * @param  array $matches
1111
	 * @return string
1112
	 */
1113 12
	protected function _doHeaders_callback_atx($matches) {
1114 12
		$level = strlen($matches[1]);
1115
1116 12
		$defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null;
1117 12
		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId);
1118 12
		$block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>";
1119 12
		return "\n" . $this->hashBlock($block) . "\n\n";
1120
	}
1121
1122
	/**
1123
	 * Form HTML tables.
1124
	 * @param  string $text
1125
	 * @return string
1126
	 */
1127 59
	protected function doTables($text) {
1128 59
		$less_than_tab = $this->tab_width - 1;
1129
		// Find tables with leading pipe.
1130
		//
1131
		//	| Header 1 | Header 2
1132
		//	| -------- | --------
1133
		//	| Cell 1   | Cell 2
1134
		//	| Cell 3   | Cell 4
1135 59
		$text = preg_replace_callback('
1136
			{
1137
				^							# Start of a line
1138 59
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1139
				[|]							# Optional leading pipe (present)
1140
				(.+) \n						# $1: Header row (at least one pipe)
1141
1142 59
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1143
				[|] ([ ]*[-:]+[-| :]*) \n	# $2: Header underline
1144
1145
				(							# $3: Cells
1146
					(?>
1147
						[ ]*				# Allowed whitespace.
1148
						[|] .* \n			# Row content.
1149
					)*
1150
				)
1151
				(?=\n|\Z)					# Stop at final double newline.
1152
			}xm',
1153 59
			array($this, '_doTable_leadingPipe_callback'), $text);
1154
1155
		// Find tables without leading pipe.
1156
		//
1157
		//	Header 1 | Header 2
1158
		//	-------- | --------
1159
		//	Cell 1   | Cell 2
1160
		//	Cell 3   | Cell 4
1161 59
		$text = preg_replace_callback('
1162
			{
1163
				^							# Start of a line
1164 59
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1165
				(\S.*[|].*) \n				# $1: Header row (at least one pipe)
1166
1167 59
				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
1168
				([-:]+[ ]*[|][-| :]*) \n	# $2: Header underline
1169
1170
				(							# $3: Cells
1171
					(?>
1172
						.* [|] .* \n		# Row content
1173
					)*
1174
				)
1175
				(?=\n|\Z)					# Stop at final double newline.
1176
			}xm',
1177 59
			array($this, '_DoTable_callback'), $text);
1178
1179 59
		return $text;
1180
	}
1181
1182
	/**
1183
	 * Callback for removing the leading pipe for each row
1184
	 * @param  array $matches
1185
	 * @return string
1186
	 */
1187 1
	protected function _doTable_leadingPipe_callback($matches) {
1188 1
		$head		= $matches[1];
1189 1
		$underline	= $matches[2];
1190 1
		$content	= $matches[3];
1191
1192 1
		$content	= preg_replace('/^ *[|]/m', '', $content);
1193
1194 1
		return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
1195
	}
1196
1197
	/**
1198
	 * Make the align attribute in a table
1199
	 * @param  string $alignname
1200
	 * @return string
1201
	 */
1202 1
	protected function _doTable_makeAlignAttr($alignname)
1203
	{
1204 1
		if (empty($this->table_align_class_tmpl)) {
1205 1
			return " align=\"$alignname\"";
1206
		}
1207
1208
		$classname = str_replace('%%', $alignname, $this->table_align_class_tmpl);
1209
		return " class=\"$classname\"";
1210
	}
1211
1212
	/**
1213
	 * Calback for processing tables
1214
	 * @param  array $matches
1215
	 * @return string
1216
	 */
1217 1
	protected function _doTable_callback($matches) {
1218 1
		$head		= $matches[1];
1219 1
		$underline	= $matches[2];
1220 1
		$content	= $matches[3];
1221
1222
		// Remove any tailing pipes for each line.
1223 1
		$head		= preg_replace('/[|] *$/m', '', $head);
1224 1
		$underline	= preg_replace('/[|] *$/m', '', $underline);
1225 1
		$content	= preg_replace('/[|] *$/m', '', $content);
1226
1227
		// Reading alignement from header underline.
1228 1
		$separators	= preg_split('/ *[|] */', $underline);
1229 1
		foreach ($separators as $n => $s) {
1230 1
			if (preg_match('/^ *-+: *$/', $s))
1231 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...
1232 1
			else if (preg_match('/^ *:-+: *$/', $s))
1233 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...
1234 1
			else if (preg_match('/^ *:-+ *$/', $s))
1235 1
				$attr[$n] = $this->_doTable_makeAlignAttr('left');
1236
			else
1237 1
				$attr[$n] = '';
1238
		}
1239
1240
		// Parsing span elements, including code spans, character escapes,
1241
		// and inline HTML tags, so that pipes inside those gets ignored.
1242 1
		$head		= $this->parseSpan($head);
1243 1
		$headers	= preg_split('/ *[|] */', $head);
1244 1
		$col_count	= count($headers);
1245 1
		$attr       = array_pad($attr, $col_count, '');
1246
1247
		// Write column headers.
1248 1
		$text = "<table>\n";
1249 1
		$text .= "<thead>\n";
1250 1
		$text .= "<tr>\n";
1251 1
		foreach ($headers as $n => $header)
1252 1
			$text .= "  <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n";
1253 1
		$text .= "</tr>\n";
1254 1
		$text .= "</thead>\n";
1255
1256
		// Split content by row.
1257 1
		$rows = explode("\n", trim($content, "\n"));
1258
1259 1
		$text .= "<tbody>\n";
1260 1
		foreach ($rows as $row) {
1261
			// Parsing span elements, including code spans, character escapes,
1262
			// and inline HTML tags, so that pipes inside those gets ignored.
1263 1
			$row = $this->parseSpan($row);
1264
1265
			// Split row by cell.
1266 1
			$row_cells = preg_split('/ *[|] */', $row, $col_count);
1267 1
			$row_cells = array_pad($row_cells, $col_count, '');
1268
1269 1
			$text .= "<tr>\n";
1270 1
			foreach ($row_cells as $n => $cell)
1271 1
				$text .= "  <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n";
1272 1
			$text .= "</tr>\n";
1273
		}
1274 1
		$text .= "</tbody>\n";
1275 1
		$text .= "</table>";
1276
1277 1
		return $this->hashBlock($text) . "\n";
1278
	}
1279
1280
	/**
1281
	 * Form HTML definition lists.
1282
	 * @param  string $text
1283
	 * @return string
1284
	 */
1285 59
	protected function doDefLists($text) {
1286 59
		$less_than_tab = $this->tab_width - 1;
1287
1288
		// Re-usable pattern to match any entire dl list:
1289
		$whole_list_re = '(?>
1290
			(								# $1 = whole list
1291
			  (								# $2
1292 59
				[ ]{0,' . $less_than_tab . '}
1293
				((?>.*\S.*\n)+)				# $3 = defined term
1294
				\n?
1295 59
				[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1296
			  )
1297
			  (?s:.+?)
1298
			  (								# $4
1299
				  \z
1300
				|
1301
				  \n{2,}
1302
				  (?=\S)
1303
				  (?!						# Negative lookahead for another term
1304 59
					[ ]{0,' . $less_than_tab . '}
1305
					(?: \S.*\n )+?			# defined term
1306
					\n?
1307 59
					[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1308
				  )
1309
				  (?!						# Negative lookahead for another definition
1310 59
					[ ]{0,' . $less_than_tab . '}:[ ]+ # colon starting definition
1311
				  )
1312
			  )
1313
			)
1314
		)'; // mx
1315
1316 59
		$text = preg_replace_callback('{
1317
				(?>\A\n?|(?<=\n\n))
1318 59
				' . $whole_list_re . '
1319
			}mx',
1320 59
			array($this, '_doDefLists_callback'), $text);
1321
1322 59
		return $text;
1323
	}
1324
1325
	/**
1326
	 * Callback for processing definition lists
1327
	 * @param  array $matches
1328
	 * @return string
1329
	 */
1330 1
	protected function _doDefLists_callback($matches) {
1331
		// Re-usable patterns to match list item bullets and number markers:
1332 1
		$list = $matches[1];
1333
1334
		// Turn double returns into triple returns, so that we can make a
1335
		// paragraph for the last item in a list, if necessary:
1336 1
		$result = trim($this->processDefListItems($list));
1337 1
		$result = "<dl>\n" . $result . "\n</dl>";
1338 1
		return $this->hashBlock($result) . "\n\n";
1339
	}
1340
1341
	/**
1342
	 * Process the contents of a single definition list, splitting it
1343
	 * into individual term and definition list items.
1344
	 * @param  string $list_str
1345
	 * @return string
1346
	 */
1347 1
	protected function processDefListItems($list_str) {
1348
1349 1
		$less_than_tab = $this->tab_width - 1;
1350
1351
		// Trim trailing blank lines:
1352 1
		$list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
1353
1354
		// Process definition terms.
1355 1
		$list_str = preg_replace_callback('{
1356
			(?>\A\n?|\n\n+)						# leading line
1357
			(									# definition terms = $1
1358 1
				[ ]{0,' . $less_than_tab . '}	# leading whitespace
1359
				(?!\:[ ]|[ ])					# negative lookahead for a definition
1360
												#   mark (colon) or more whitespace.
1361
				(?> \S.* \n)+?					# actual term (not whitespace).
1362
			)
1363
			(?=\n?[ ]{0,3}:[ ])					# lookahead for following line feed
1364
												#   with a definition mark.
1365
			}xm',
1366 1
			array($this, '_processDefListItems_callback_dt'), $list_str);
1367
1368
		// Process actual definitions.
1369 1
		$list_str = preg_replace_callback('{
1370
			\n(\n+)?							# leading line = $1
1371
			(									# marker space = $2
1372 1
				[ ]{0,' . $less_than_tab . '}	# whitespace before colon
1373
				\:[ ]+							# definition mark (colon)
1374
			)
1375
			((?s:.+?))							# definition text = $3
1376
			(?= \n+ 							# stop at next definition mark,
1377
				(?:								# next term or end of text
1378 1
					[ ]{0,' . $less_than_tab . '} \:[ ]	|
1379
					<dt> | \z
1380
				)
1381
			)
1382
			}xm',
1383 1
			array($this, '_processDefListItems_callback_dd'), $list_str);
1384
1385 1
		return $list_str;
1386
	}
1387
1388
	/**
1389
	 * Callback for <dt> elements in definition lists
1390
	 * @param  array $matches
1391
	 * @return string
1392
	 */
1393 1
	protected function _processDefListItems_callback_dt($matches) {
1394 1
		$terms = explode("\n", trim($matches[1]));
1395 1
		$text = '';
1396 1
		foreach ($terms as $term) {
1397 1
			$term = $this->runSpanGamut(trim($term));
1398 1
			$text .= "\n<dt>" . $term . "</dt>";
1399
		}
1400 1
		return $text . "\n";
1401
	}
1402
1403
	/**
1404
	 * Callback for <dd> elements in definition lists
1405
	 * @param  array $matches
1406
	 * @return string
1407
	 */
1408 1
	protected function _processDefListItems_callback_dd($matches) {
1409 1
		$leading_line	= $matches[1];
1410 1
		$marker_space	= $matches[2];
1411 1
		$def			= $matches[3];
1412
1413 1
		if ($leading_line || preg_match('/\n{2,}/', $def)) {
1414
			// Replace marker with the appropriate whitespace indentation
1415 1
			$def = str_repeat(' ', strlen($marker_space)) . $def;
1416 1
			$def = $this->runBlockGamut($this->outdent($def . "\n\n"));
1417 1
			$def = "\n". $def ."\n";
1418
		}
1419
		else {
1420 1
			$def = rtrim($def);
1421 1
			$def = $this->runSpanGamut($this->outdent($def));
1422
		}
1423
1424 1
		return "\n<dd>" . $def . "</dd>\n";
1425
	}
1426
1427
	/**
1428
	 * Adding the fenced code block syntax to regular Markdown:
1429
	 *
1430
	 * ~~~
1431
	 * Code block
1432
	 * ~~~
1433
	 *
1434
	 * @param  string $text
1435
	 * @return string
1436
	 */
1437 59 View Code Duplication
	protected function doFencedCodeBlocks($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...
1438
1439 59
		$less_than_tab = $this->tab_width;
0 ignored issues
show
Unused Code introduced by
$less_than_tab is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1440
1441 59
		$text = preg_replace_callback('{
1442
				(?:\n|\A)
1443
				# 1: Opening marker
1444
				(
1445
					(?:~{3,}|`{3,}) # 3 or more tildes/backticks.
1446
				)
1447
				[ ]*
1448
				(?:
1449
					\.?([-_:a-zA-Z0-9]+) # 2: standalone class name
1450
				)?
1451
				[ ]*
1452
				(?:
1453 59
					' . $this->id_class_attr_catch_re . ' # 3: Extra attributes
1454
				)?
1455
				[ ]* \n # Whitespace and newline following marker.
1456
1457
				# 4: Content
1458
				(
1459
					(?>
1460
						(?!\1 [ ]* \n)	# Not a closing marker.
1461
						.*\n+
1462
					)+
1463
				)
1464
1465
				# Closing marker.
1466
				\1 [ ]* (?= \n )
1467
			}xm',
1468 59
			array($this, '_doFencedCodeBlocks_callback'), $text);
1469
1470 59
		return $text;
1471
	}
1472
1473
	/**
1474
	 * Callback to process fenced code blocks
1475
	 * @param  array $matches
1476
	 * @return string
1477
	 */
1478 4
	protected function _doFencedCodeBlocks_callback($matches) {
1479 4
		$classname =& $matches[2];
1480 4
		$attrs     =& $matches[3];
1481 4
		$codeblock = $matches[4];
1482
1483 4 View Code Duplication
		if ($this->code_block_content_func) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1484
			$codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname);
1485
		} else {
1486 4
			$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
1487
		}
1488
1489 4
		$codeblock = preg_replace_callback('/^\n+/',
1490 4
			array($this, '_doFencedCodeBlocks_newlines'), $codeblock);
1491
1492 4
		$classes = array();
1493 4
		if ($classname != "") {
1494 4
			if ($classname{0} == '.')
1495
				$classname = substr($classname, 1);
1496 4
			$classes[] = $this->code_class_prefix . $classname;
1497
		}
1498 4
		$attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes);
1499 4
		$pre_attr_str  = $this->code_attr_on_pre ? $attr_str : '';
1500 4
		$code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
1501 4
		$codeblock  = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
1502
1503 4
		return "\n\n".$this->hashBlock($codeblock)."\n\n";
1504
	}
1505
1506
	/**
1507
	 * Replace new lines in fenced code blocks
1508
	 * @param  array $matches
1509
	 * @return string
1510
	 */
1511 2
	protected function _doFencedCodeBlocks_newlines($matches) {
1512 2
		return str_repeat("<br$this->empty_element_suffix",
1513 2
			strlen($matches[0]));
1514
	}
1515
1516
	/**
1517
	 * Redefining emphasis markers so that emphasis by underscore does not
1518
	 * work in the middle of a word.
1519
	 * @var array
1520
	 */
1521
	protected $em_relist = array(
1522
		''  => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)',
1523
		'*' => '(?<![\s*])\*(?!\*)',
1524
		'_' => '(?<![\s_])_(?![a-zA-Z0-9_])',
1525
	);
1526
	protected $strong_relist = array(
1527
		''   => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)',
1528
		'**' => '(?<![\s*])\*\*(?!\*)',
1529
		'__' => '(?<![\s_])__(?![a-zA-Z0-9_])',
1530
	);
1531
	protected $em_strong_relist = array(
1532
		''    => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)',
1533
		'***' => '(?<![\s*])\*\*\*(?!\*)',
1534
		'___' => '(?<![\s_])___(?![a-zA-Z0-9_])',
1535
	);
1536
1537
	/**
1538
	 * Parse text into paragraphs
1539
	 * @param  string $text String to process in paragraphs
1540
	 * @param  boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags
1541
	 * @return string       HTML output
1542
	 */
1543 59
	protected function formParagraphs($text, $wrap_in_p = true) {
1544
		// Strip leading and trailing lines:
1545 59
		$text = preg_replace('/\A\n+|\n+\z/', '', $text);
1546
1547 59
		$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
1548
1549
		// Wrap <p> tags and unhashify HTML blocks
1550 59
		foreach ($grafs as $key => $value) {
1551 59
			$value = trim($this->runSpanGamut($value));
1552
1553
			// Check if this should be enclosed in a paragraph.
1554
			// Clean tag hashes & block tag hashes are left alone.
1555 59
			$is_p = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
1556
1557 59
			if ($is_p) {
1558 55
				$value = "<p>$value</p>";
1559
			}
1560 59
			$grafs[$key] = $value;
1561
		}
1562
1563
		// Join grafs in one text, then unhash HTML tags.
1564 59
		$text = implode("\n\n", $grafs);
1565
1566
		// Finish by removing any tag hashes still present in $text.
1567 59
		$text = $this->unhash($text);
1568
1569 59
		return $text;
1570
	}
1571
1572
1573
	/**
1574
	 * Footnotes - Strips link definitions from text, stores the URLs and
1575
	 * titles in hash references.
1576
	 * @param  string $text
1577
	 * @return string
1578
	 */
1579 59 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...
1580 59
		$less_than_tab = $this->tab_width - 1;
1581
1582
		// Link defs are in the form: [^id]: url "optional title"
1583 59
		$text = preg_replace_callback('{
1584 59
			^[ ]{0,' . $less_than_tab . '}\[\^(.+?)\][ ]?:	# note_id = $1
1585
			  [ ]*
1586
			  \n?					# maybe *one* newline
1587
			(						# text = $2 (no blank lines allowed)
1588
				(?:
1589
					.+				# actual text
1590
				|
1591
					\n				# newlines but
1592
					(?!\[.+?\][ ]?:\s)# negative lookahead for footnote or link definition marker.
1593
					(?!\n+[ ]{0,3}\S)# ensure line is not blank and followed
1594
									# by non-indented content
1595
				)*
1596
			)
1597
			}xm',
1598 59
			array($this, '_stripFootnotes_callback'),
1599 59
			$text);
1600 59
		return $text;
1601
	}
1602
1603
	/**
1604
	 * Callback for stripping footnotes
1605
	 * @param  array $matches
1606
	 * @return string
1607
	 */
1608 1
	protected function _stripFootnotes_callback($matches) {
1609 1
		$note_id = $this->fn_id_prefix . $matches[1];
1610 1
		$this->footnotes[$note_id] = $this->outdent($matches[2]);
1611 1
		return ''; // String that will replace the block
1612
	}
1613
1614
	/**
1615
	 * Replace footnote references in $text [^id] with a special text-token
1616
	 * which will be replaced by the actual footnote marker in appendFootnotes.
1617
	 * @param  string $text
1618
	 * @return string
1619
	 */
1620 59
	protected function doFootnotes($text) {
1621 59
		if (!$this->in_anchor) {
1622 59
			$text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
1623
		}
1624 59
		return $text;
1625
	}
1626
1627
	/**
1628
	 * Append footnote list to text
1629
	 * @param  string $text
1630
	 * @return string
1631
	 */
1632 59
	protected function appendFootnotes($text) {
1633 59
		$text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1634 59
			array($this, '_appendFootnotes_callback'), $text);
1635
1636 59
		if ( ! empty( $this->footnotes_ordered ) ) {
1637 1
			$this->_doFootnotes();
1638 1
			if ( ! $this->omit_footnotes ) {
1639 1
				$text .= "\n\n";
1640 1
				$text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n";
1641 1
				$text .= "<hr" . $this->empty_element_suffix . "\n";
1642 1
				$text .= $this->footnotes_assembled;
1643 1
				$text .= "</div>";
1644
			}
1645
		}
1646 59
		return $text;
1647
	}
1648
1649
1650
	/**
1651
	 * Generates the HTML for footnotes.  Called by appendFootnotes, even if footnotes are not being appended.
1652
	 * @return void
1653
	 */
1654 1
	protected function _doFootnotes() {
1655 1
		$attr = "";
1656 1
		if ($this->fn_backlink_class != "") {
1657 1
			$class = $this->fn_backlink_class;
1658 1
			$class = $this->encodeAttribute($class);
1659 1
			$attr .= " class=\"$class\"";
1660
		}
1661 1
		if ($this->fn_backlink_title != "") {
1662
			$title = $this->fn_backlink_title;
1663
			$title = $this->encodeAttribute($title);
1664
			$attr .= " title=\"$title\"";
1665
			$attr .= " aria-label=\"$title\"";
1666
		}
1667 1
		$attr .= " role=\"doc-backlink\"";
1668 1
		$backlink_text = $this->fn_backlink_html;
1669 1
		$num = 0;
1670
1671 1
		$text = "<ol>\n\n";
1672 1
		while (!empty($this->footnotes_ordered)) {
1673 1
			$footnote = reset($this->footnotes_ordered);
1674 1
			$note_id = key($this->footnotes_ordered);
1675 1
			unset($this->footnotes_ordered[$note_id]);
1676 1
			$ref_count = $this->footnotes_ref_count[$note_id];
1677 1
			unset($this->footnotes_ref_count[$note_id]);
1678 1
			unset($this->footnotes[$note_id]);
1679
1680 1
			$footnote .= "\n"; // Need to append newline before parsing.
1681 1
			$footnote = $this->runBlockGamut("$footnote\n");
1682 1
			$footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1683 1
				array($this, '_appendFootnotes_callback'), $footnote);
1684
1685 1
			$attr = str_replace("%%", ++$num, $attr);
1686 1
			$note_id = $this->encodeAttribute($note_id);
1687
1688
			// Prepare backlink, multiple backlinks if multiple references
1689 1
			$backlink = "<a href=\"#fnref:$note_id\"$attr>$backlink_text</a>";
1690 1
			for ($ref_num = 2; $ref_num <= $ref_count; ++$ref_num) {
1691 1
				$backlink .= " <a href=\"#fnref$ref_num:$note_id\"$attr>$backlink_text</a>";
1692
			}
1693
			// Add backlink to last paragraph; create new paragraph if needed.
1694 1
			if (preg_match('{</p>$}', $footnote)) {
1695 1
				$footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
1696
			} else {
1697 1
				$footnote .= "\n\n<p>$backlink</p>";
1698
			}
1699
1700 1
			$text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n";
1701 1
			$text .= $footnote . "\n";
1702 1
			$text .= "</li>\n\n";
1703
		}
1704 1
		$text .= "</ol>\n";
1705
1706 1
		$this->footnotes_assembled = $text;
1707 1
	}
1708
1709
	/**
1710
	 * Callback for appending footnotes
1711
	 * @param  array $matches
1712
	 * @return string
1713
	 */
1714 1
	protected function _appendFootnotes_callback($matches) {
1715 1
		$node_id = $this->fn_id_prefix . $matches[1];
1716
1717
		// Create footnote marker only if it has a corresponding footnote *and*
1718
		// the footnote hasn't been used by another marker.
1719 1
		if (isset($this->footnotes[$node_id])) {
1720 1
			$num =& $this->footnotes_numbers[$node_id];
1721 1
			if (!isset($num)) {
1722
				// Transfer footnote content to the ordered list and give it its
1723
				// number
1724 1
				$this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
1725 1
				$this->footnotes_ref_count[$node_id] = 1;
1726 1
				$num = $this->footnote_counter++;
1727 1
				$ref_count_mark = '';
1728
			} else {
1729 1
				$ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
1730
			}
1731
1732 1
			$attr = "";
1733 1
			if ($this->fn_link_class != "") {
1734 1
				$class = $this->fn_link_class;
1735 1
				$class = $this->encodeAttribute($class);
1736 1
				$attr .= " class=\"$class\"";
1737
			}
1738 1
			if ($this->fn_link_title != "") {
1739
				$title = $this->fn_link_title;
1740
				$title = $this->encodeAttribute($title);
1741
				$attr .= " title=\"$title\"";
1742
			}
1743 1
			$attr .= " role=\"doc-noteref\"";
1744
1745 1
			$attr = str_replace("%%", $num, $attr);
1746 1
			$node_id = $this->encodeAttribute($node_id);
1747
1748
			return
1749 1
				"<sup id=\"fnref$ref_count_mark:$node_id\">".
1750 1
				"<a href=\"#fn:$node_id\"$attr>$num</a>".
1751 1
				"</sup>";
1752
		}
1753
1754 1
		return "[^" . $matches[1] . "]";
1755
	}
1756
1757
1758
	/**
1759
	 * Abbreviations - strips abbreviations from text, stores titles in hash
1760
	 * references.
1761
	 * @param  string $text
1762
	 * @return string
1763
	 */
1764 59 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...
1765 59
		$less_than_tab = $this->tab_width - 1;
1766
1767
		// Link defs are in the form: [id]*: url "optional title"
1768 59
		$text = preg_replace_callback('{
1769 59
			^[ ]{0,' . $less_than_tab . '}\*\[(.+?)\][ ]?:	# abbr_id = $1
1770
			(.*)					# text = $2 (no blank lines allowed)
1771
			}xm',
1772 59
			array($this, '_stripAbbreviations_callback'),
1773 59
			$text);
1774 59
		return $text;
1775
	}
1776
1777
	/**
1778
	 * Callback for stripping abbreviations
1779
	 * @param  array $matches
1780
	 * @return string
1781
	 */
1782 1
	protected function _stripAbbreviations_callback($matches) {
1783 1
		$abbr_word = $matches[1];
1784 1
		$abbr_desc = $matches[2];
1785 1
		if ($this->abbr_word_re) {
1786 1
			$this->abbr_word_re .= '|';
1787
		}
1788 1
		$this->abbr_word_re .= preg_quote($abbr_word);
1789 1
		$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
1790 1
		return ''; // String that will replace the block
1791
	}
1792
1793
	/**
1794
	 * Find defined abbreviations in text and wrap them in <abbr> elements.
1795
	 * @param  string $text
1796
	 * @return string
1797
	 */
1798 59
	protected function doAbbreviations($text) {
1799 59
		if ($this->abbr_word_re) {
1800
			// cannot use the /x modifier because abbr_word_re may
1801
			// contain significant spaces:
1802 1
			$text = preg_replace_callback('{' .
1803
				'(?<![\w\x1A])' .
1804 1
				'(?:' . $this->abbr_word_re . ')' .
1805 1
				'(?![\w\x1A])' .
1806 1
				'}',
1807 1
				array($this, '_doAbbreviations_callback'), $text);
1808
		}
1809 59
		return $text;
1810
	}
1811
1812
	/**
1813
	 * Callback for processing abbreviations
1814
	 * @param  array $matches
1815
	 * @return string
1816
	 */
1817 1
	protected function _doAbbreviations_callback($matches) {
1818 1
		$abbr = $matches[0];
1819 1
		if (isset($this->abbr_desciptions[$abbr])) {
1820 1
			$desc = $this->abbr_desciptions[$abbr];
1821 1
			if (empty($desc)) {
1822
				return $this->hashPart("<abbr>$abbr</abbr>");
1823
			} else {
1824 1
				$desc = $this->encodeAttribute($desc);
1825 1
				return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
1826
			}
1827
		} else {
1828
			return $matches[0];
1829
		}
1830
	}
1831
}
1832