Completed
Pull Request — lib (#313)
by
unknown
02:44
created

MarkdownExtra   F

Complexity

Total Complexity 161

Size/Duplication

Total Lines 1856
Duplicated Lines 12.07 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 96.19%

Importance

Changes 0
Metric Value
wmc 161
lcom 1
cbo 1
dl 224
loc 1856
ccs 580
cts 603
cp 0.9619
rs 0.8
c 0
b 0
f 0

43 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 26 1
A setup() 0 19 3
A teardown() 0 13 2
C doExtraAttributes() 0 40 15
A stripLinkDefinitions() 31 31 1
A _stripLinkDefinitions_callback() 0 8 2
A hashHTMLBlocks() 0 10 2
F _hashHTMLBlocks_inMarkdown() 27 200 24
F _hashHTMLBlocks_inHTML() 5 154 19
A hashClean() 0 3 1
B doAnchors() 64 64 2
A _doAnchors_reference_callback() 5 36 5
A _doAnchors_inline_callback() 0 26 4
A doImages() 49 49 1
A _doImages_reference_callback() 5 31 5
A _doImages_inline_callback() 0 18 3
A doHeaders() 0 37 2
A _doHeaders_callback_setext() 3 13 5
A _doHeaders_callback_atx() 0 8 2
A doTables() 0 54 1
A _doTable_leadingPipe_callback() 0 9 1
A _doTable_makeAlignAttr() 0 9 2
B _doTable_callback() 0 64 8
A doDefLists() 0 39 1
A _doDefLists_callback() 0 10 1
A processDefListItems() 0 40 1
A _processDefListItems_callback_dt() 0 9 2
A _processDefListItems_callback_dd() 0 18 3
A doFencedCodeBlocks() 0 33 1
B _doFencedCodeBlocks_callback() 0 28 7
A _doFencedCodeBlocks_newlines() 0 4 1
A formParagraphs() 0 28 4
A stripFootnotes() 23 23 1
A _stripFootnotes_callback() 0 5 1
A doFootnotes() 0 6 2
A appendFootnotes() 0 16 3
B _doFootnotes() 0 59 8
B _appendFootnotes_callback() 0 42 5
A buildFootnoteLabel() 0 3 1
A stripAbbreviations() 12 12 1
A _stripAbbreviations_callback() 0 10 2
A doAbbreviations() 0 13 2
A _doAbbreviations_callback() 0 12 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MarkdownExtra often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MarkdownExtra, and based on these observations, apply Extract Interface, too.

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

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

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

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