Test Failed
Pull Request — master (#18)
by Ylva
07:22
created
a/vendor/michelf/php-markdown/Michelf/MarkdownExtra.php 1 patch
Indentation   +1535 added lines, -1535 removed lines patch added patch discarded remove patch
@@ -14,287 +14,287 @@  discard block
 block discarded – undo
14 14
  * Markdown Extra Parser Class
15 15
  */
16 16
 class MarkdownExtra extends \Michelf\Markdown {
17
-	/**
18
-	 * Configuration variables
19
-	 */
20
-
21
-	/**
22
-	 * Prefix for footnote ids.
23
-	 * @var string
24
-	 */
25
-	public $fn_id_prefix = "";
26
-
27
-	/**
28
-	 * Optional title attribute for footnote links.
29
-	 * @var string
30
-	 */
31
-	public $fn_link_title = "";
32
-
33
-	/**
34
-	 * Optional class attribute for footnote links and backlinks.
35
-	 * @var string
36
-	 */
37
-	public $fn_link_class     = "footnote-ref";
38
-	public $fn_backlink_class = "footnote-backref";
39
-
40
-	/**
41
-	 * Content to be displayed within footnote backlinks. The default is '↩';
42
-	 * the U+FE0E on the end is a Unicode variant selector used to prevent iOS
43
-	 * from displaying the arrow character as an emoji.
44
-	 * Optionally use '^^' and '%%' to refer to the footnote number and
45
-	 * reference number respectively. {@see parseFootnotePlaceholders()}
46
-	 * @var string
47
-	 */
48
-	public $fn_backlink_html = '↩︎';
49
-
50
-	/**
51
-	 * Optional title and aria-label attributes for footnote backlinks for
52
-	 * added accessibility (to ensure backlink uniqueness).
53
-	 * Use '^^' and '%%' to refer to the footnote number and reference number
54
-	 * respectively. {@see parseFootnotePlaceholders()}
55
-	 * @var string
56
-	 */
57
-	public $fn_backlink_title = "";
58
-	public $fn_backlink_label = "";
59
-
60
-	/**
61
-	 * Class name for table cell alignment (%% replaced left/center/right)
62
-	 * For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center'
63
-	 * If empty, the align attribute is used instead of a class name.
64
-	 * @var string
65
-	 */
66
-	public $table_align_class_tmpl = '';
67
-
68
-	/**
69
-	 * Optional class prefix for fenced code block.
70
-	 * @var string
71
-	 */
72
-	public $code_class_prefix = "";
73
-
74
-	/**
75
-	 * Class attribute for code blocks goes on the `code` tag;
76
-	 * setting this to true will put attributes on the `pre` tag instead.
77
-	 * @var boolean
78
-	 */
79
-	public $code_attr_on_pre = false;
80
-
81
-	/**
82
-	 * Predefined abbreviations.
83
-	 * @var array
84
-	 */
85
-	public $predef_abbr = array();
86
-
87
-	/**
88
-	 * Only convert atx-style headers if there's a space between the header and #
89
-	 * @var boolean
90
-	 */
91
-	public $hashtag_protection = false;
92
-
93
-	/**
94
-	 * Determines whether footnotes should be appended to the end of the document.
95
-	 * If true, footnote html can be retrieved from $this->footnotes_assembled.
96
-	 * @var boolean
97
-	 */
98
-	public $omit_footnotes = false;
99
-
100
-
101
-	/**
102
-	 * After parsing, the HTML for the list of footnotes appears here.
103
-	 * This is available only if $omit_footnotes == true.
104
-	 *
105
-	 * Note: when placing the content of `footnotes_assembled` on the page,
106
-	 * consider adding the attribute `role="doc-endnotes"` to the `div` or
107
-	 * `section` that will enclose the list of footnotes so they are
108
-	 * reachable to accessibility tools the same way they would be with the
109
-	 * default HTML output.
110
-	 * @var null|string
111
-	 */
112
-	public $footnotes_assembled = null;
113
-
114
-	/**
115
-	 * Parser implementation
116
-	 */
117
-
118
-	/**
119
-	 * Constructor function. Initialize the parser object.
120
-	 * @return void
121
-	 */
122
-	public function __construct() {
123
-		// Add extra escapable characters before parent constructor
124
-		// initialize the table.
125
-		$this->escape_chars .= ':|';
126
-
127
-		// Insert extra document, block, and span transformations.
128
-		// Parent constructor will do the sorting.
129
-		$this->document_gamut += array(
130
-			"doFencedCodeBlocks" => 5,
131
-			"stripFootnotes"     => 15,
132
-			"stripAbbreviations" => 25,
133
-			"appendFootnotes"    => 50,
134
-		);
135
-		$this->block_gamut += array(
136
-			"doFencedCodeBlocks" => 5,
137
-			"doTables"           => 15,
138
-			"doDefLists"         => 45,
139
-		);
140
-		$this->span_gamut += array(
141
-			"doFootnotes"        => 5,
142
-			"doAbbreviations"    => 70,
143
-		);
144
-
145
-		$this->enhanced_ordered_list = true;
146
-		parent::__construct();
147
-	}
148
-
149
-
150
-	/**
151
-	 * Extra variables used during extra transformations.
152
-	 * @var array
153
-	 */
154
-	protected $footnotes = array();
155
-	protected $footnotes_ordered = array();
156
-	protected $footnotes_ref_count = array();
157
-	protected $footnotes_numbers = array();
158
-	protected $abbr_desciptions = array();
159
-	/** @var string */
160
-	protected $abbr_word_re = '';
161
-
162
-	/**
163
-	 * Give the current footnote number.
164
-	 * @var integer
165
-	 */
166
-	protected $footnote_counter = 1;
17
+    /**
18
+     * Configuration variables
19
+     */
20
+
21
+    /**
22
+     * Prefix for footnote ids.
23
+     * @var string
24
+     */
25
+    public $fn_id_prefix = "";
26
+
27
+    /**
28
+     * Optional title attribute for footnote links.
29
+     * @var string
30
+     */
31
+    public $fn_link_title = "";
32
+
33
+    /**
34
+     * Optional class attribute for footnote links and backlinks.
35
+     * @var string
36
+     */
37
+    public $fn_link_class     = "footnote-ref";
38
+    public $fn_backlink_class = "footnote-backref";
39
+
40
+    /**
41
+     * Content to be displayed within footnote backlinks. The default is '↩';
42
+     * the U+FE0E on the end is a Unicode variant selector used to prevent iOS
43
+     * from displaying the arrow character as an emoji.
44
+     * Optionally use '^^' and '%%' to refer to the footnote number and
45
+     * reference number respectively. {@see parseFootnotePlaceholders()}
46
+     * @var string
47
+     */
48
+    public $fn_backlink_html = '↩︎';
49
+
50
+    /**
51
+     * Optional title and aria-label attributes for footnote backlinks for
52
+     * added accessibility (to ensure backlink uniqueness).
53
+     * Use '^^' and '%%' to refer to the footnote number and reference number
54
+     * respectively. {@see parseFootnotePlaceholders()}
55
+     * @var string
56
+     */
57
+    public $fn_backlink_title = "";
58
+    public $fn_backlink_label = "";
59
+
60
+    /**
61
+     * Class name for table cell alignment (%% replaced left/center/right)
62
+     * For instance: 'go-%%' becomes 'go-left' or 'go-right' or 'go-center'
63
+     * If empty, the align attribute is used instead of a class name.
64
+     * @var string
65
+     */
66
+    public $table_align_class_tmpl = '';
67
+
68
+    /**
69
+     * Optional class prefix for fenced code block.
70
+     * @var string
71
+     */
72
+    public $code_class_prefix = "";
73
+
74
+    /**
75
+     * Class attribute for code blocks goes on the `code` tag;
76
+     * setting this to true will put attributes on the `pre` tag instead.
77
+     * @var boolean
78
+     */
79
+    public $code_attr_on_pre = false;
80
+
81
+    /**
82
+     * Predefined abbreviations.
83
+     * @var array
84
+     */
85
+    public $predef_abbr = array();
86
+
87
+    /**
88
+     * Only convert atx-style headers if there's a space between the header and #
89
+     * @var boolean
90
+     */
91
+    public $hashtag_protection = false;
92
+
93
+    /**
94
+     * Determines whether footnotes should be appended to the end of the document.
95
+     * If true, footnote html can be retrieved from $this->footnotes_assembled.
96
+     * @var boolean
97
+     */
98
+    public $omit_footnotes = false;
99
+
100
+
101
+    /**
102
+     * After parsing, the HTML for the list of footnotes appears here.
103
+     * This is available only if $omit_footnotes == true.
104
+     *
105
+     * Note: when placing the content of `footnotes_assembled` on the page,
106
+     * consider adding the attribute `role="doc-endnotes"` to the `div` or
107
+     * `section` that will enclose the list of footnotes so they are
108
+     * reachable to accessibility tools the same way they would be with the
109
+     * default HTML output.
110
+     * @var null|string
111
+     */
112
+    public $footnotes_assembled = null;
113
+
114
+    /**
115
+     * Parser implementation
116
+     */
117
+
118
+    /**
119
+     * Constructor function. Initialize the parser object.
120
+     * @return void
121
+     */
122
+    public function __construct() {
123
+        // Add extra escapable characters before parent constructor
124
+        // initialize the table.
125
+        $this->escape_chars .= ':|';
126
+
127
+        // Insert extra document, block, and span transformations.
128
+        // Parent constructor will do the sorting.
129
+        $this->document_gamut += array(
130
+            "doFencedCodeBlocks" => 5,
131
+            "stripFootnotes"     => 15,
132
+            "stripAbbreviations" => 25,
133
+            "appendFootnotes"    => 50,
134
+        );
135
+        $this->block_gamut += array(
136
+            "doFencedCodeBlocks" => 5,
137
+            "doTables"           => 15,
138
+            "doDefLists"         => 45,
139
+        );
140
+        $this->span_gamut += array(
141
+            "doFootnotes"        => 5,
142
+            "doAbbreviations"    => 70,
143
+        );
144
+
145
+        $this->enhanced_ordered_list = true;
146
+        parent::__construct();
147
+    }
148
+
149
+
150
+    /**
151
+     * Extra variables used during extra transformations.
152
+     * @var array
153
+     */
154
+    protected $footnotes = array();
155
+    protected $footnotes_ordered = array();
156
+    protected $footnotes_ref_count = array();
157
+    protected $footnotes_numbers = array();
158
+    protected $abbr_desciptions = array();
159
+    /** @var string */
160
+    protected $abbr_word_re = '';
161
+
162
+    /**
163
+     * Give the current footnote number.
164
+     * @var integer
165
+     */
166
+    protected $footnote_counter = 1;
167 167
 
168 168
     /**
169 169
      * Ref attribute for links
170 170
      * @var array
171 171
      */
172
-	protected $ref_attr = array();
173
-
174
-	/**
175
-	 * Setting up Extra-specific variables.
176
-	 */
177
-	protected function setup() {
178
-		parent::setup();
179
-
180
-		$this->footnotes = array();
181
-		$this->footnotes_ordered = array();
182
-		$this->footnotes_ref_count = array();
183
-		$this->footnotes_numbers = array();
184
-		$this->abbr_desciptions = array();
185
-		$this->abbr_word_re = '';
186
-		$this->footnote_counter = 1;
187
-		$this->footnotes_assembled = null;
188
-
189
-		foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
190
-			if ($this->abbr_word_re)
191
-				$this->abbr_word_re .= '|';
192
-			$this->abbr_word_re .= preg_quote($abbr_word);
193
-			$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
194
-		}
195
-	}
196
-
197
-	/**
198
-	 * Clearing Extra-specific variables.
199
-	 */
200
-	protected function teardown() {
201
-		$this->footnotes = array();
202
-		$this->footnotes_ordered = array();
203
-		$this->footnotes_ref_count = array();
204
-		$this->footnotes_numbers = array();
205
-		$this->abbr_desciptions = array();
206
-		$this->abbr_word_re = '';
207
-
208
-		if ( ! $this->omit_footnotes )
209
-			$this->footnotes_assembled = null;
210
-
211
-		parent::teardown();
212
-	}
213
-
214
-
215
-	/**
216
-	 * Extra attribute parser
217
-	 */
218
-
219
-	/**
220
-	 * Expression to use to catch attributes (includes the braces)
221
-	 * @var string
222
-	 */
223
-	protected $id_class_attr_catch_re = '\{((?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}';
224
-
225
-	/**
226
-	 * Expression to use when parsing in a context when no capture is desired
227
-	 * @var string
228
-	 */
229
-	protected $id_class_attr_nocatch_re = '\{(?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}';
230
-
231
-	/**
232
-	 * Parse attributes caught by the $this->id_class_attr_catch_re expression
233
-	 * and return the HTML-formatted list of attributes.
234
-	 *
235
-	 * Currently supported attributes are .class and #id.
236
-	 *
237
-	 * In addition, this method also supports supplying a default Id value,
238
-	 * which will be used to populate the id attribute in case it was not
239
-	 * overridden.
240
-	 * @param  string $tag_name
241
-	 * @param  string $attr
242
-	 * @param  mixed  $defaultIdValue
243
-	 * @param  array  $classes
244
-	 * @return string
245
-	 */
246
-	protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = array()) {
247
-		if (empty($attr) && !$defaultIdValue && empty($classes)) {
248
-			return "";
249
-		}
250
-
251
-		// Split on components
252
-		preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches);
253
-		$elements = $matches[0];
254
-
255
-		// Handle classes and IDs (only first ID taken into account)
256
-		$attributes = array();
257
-		$id = false;
258
-		foreach ($elements as $element) {
259
-			if ($element[0] === '.') {
260
-				$classes[] = substr($element, 1);
261
-			} else if ($element[0] === '#') {
262
-				if ($id === false) $id = substr($element, 1);
263
-			} else if (strpos($element, '=') > 0) {
264
-				$parts = explode('=', $element, 2);
265
-				$attributes[] = $parts[0] . '="' . $parts[1] . '"';
266
-			}
267
-		}
268
-
269
-		if ($id === false || $id === '') {
270
-			$id = $defaultIdValue;
271
-		}
272
-
273
-		// Compose attributes as string
274
-		$attr_str = "";
275
-		if (!empty($id)) {
276
-			$attr_str .= ' id="'.$this->encodeAttribute($id) .'"';
277
-		}
278
-		if (!empty($classes)) {
279
-			$attr_str .= ' class="'. implode(" ", $classes) . '"';
280
-		}
281
-		if (!$this->no_markup && !empty($attributes)) {
282
-			$attr_str .= ' '.implode(" ", $attributes);
283
-		}
284
-		return $attr_str;
285
-	}
286
-
287
-	/**
288
-	 * Strips link definitions from text, stores the URLs and titles in
289
-	 * hash references.
290
-	 * @param  string $text
291
-	 * @return string
292
-	 */
293
-	protected function stripLinkDefinitions($text) {
294
-		$less_than_tab = $this->tab_width - 1;
295
-
296
-		// Link defs are in the form: ^[id]: url "optional title"
297
-		$text = preg_replace_callback('{
172
+    protected $ref_attr = array();
173
+
174
+    /**
175
+     * Setting up Extra-specific variables.
176
+     */
177
+    protected function setup() {
178
+        parent::setup();
179
+
180
+        $this->footnotes = array();
181
+        $this->footnotes_ordered = array();
182
+        $this->footnotes_ref_count = array();
183
+        $this->footnotes_numbers = array();
184
+        $this->abbr_desciptions = array();
185
+        $this->abbr_word_re = '';
186
+        $this->footnote_counter = 1;
187
+        $this->footnotes_assembled = null;
188
+
189
+        foreach ($this->predef_abbr as $abbr_word => $abbr_desc) {
190
+            if ($this->abbr_word_re)
191
+                $this->abbr_word_re .= '|';
192
+            $this->abbr_word_re .= preg_quote($abbr_word);
193
+            $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
194
+        }
195
+    }
196
+
197
+    /**
198
+     * Clearing Extra-specific variables.
199
+     */
200
+    protected function teardown() {
201
+        $this->footnotes = array();
202
+        $this->footnotes_ordered = array();
203
+        $this->footnotes_ref_count = array();
204
+        $this->footnotes_numbers = array();
205
+        $this->abbr_desciptions = array();
206
+        $this->abbr_word_re = '';
207
+
208
+        if ( ! $this->omit_footnotes )
209
+            $this->footnotes_assembled = null;
210
+
211
+        parent::teardown();
212
+    }
213
+
214
+
215
+    /**
216
+     * Extra attribute parser
217
+     */
218
+
219
+    /**
220
+     * Expression to use to catch attributes (includes the braces)
221
+     * @var string
222
+     */
223
+    protected $id_class_attr_catch_re = '\{((?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,})[ ]*\}';
224
+
225
+    /**
226
+     * Expression to use when parsing in a context when no capture is desired
227
+     * @var string
228
+     */
229
+    protected $id_class_attr_nocatch_re = '\{(?>[ ]*[#.a-z][-_:a-zA-Z0-9=]+){1,}[ ]*\}';
230
+
231
+    /**
232
+     * Parse attributes caught by the $this->id_class_attr_catch_re expression
233
+     * and return the HTML-formatted list of attributes.
234
+     *
235
+     * Currently supported attributes are .class and #id.
236
+     *
237
+     * In addition, this method also supports supplying a default Id value,
238
+     * which will be used to populate the id attribute in case it was not
239
+     * overridden.
240
+     * @param  string $tag_name
241
+     * @param  string $attr
242
+     * @param  mixed  $defaultIdValue
243
+     * @param  array  $classes
244
+     * @return string
245
+     */
246
+    protected function doExtraAttributes($tag_name, $attr, $defaultIdValue = null, $classes = array()) {
247
+        if (empty($attr) && !$defaultIdValue && empty($classes)) {
248
+            return "";
249
+        }
250
+
251
+        // Split on components
252
+        preg_match_all('/[#.a-z][-_:a-zA-Z0-9=]+/', $attr, $matches);
253
+        $elements = $matches[0];
254
+
255
+        // Handle classes and IDs (only first ID taken into account)
256
+        $attributes = array();
257
+        $id = false;
258
+        foreach ($elements as $element) {
259
+            if ($element[0] === '.') {
260
+                $classes[] = substr($element, 1);
261
+            } else if ($element[0] === '#') {
262
+                if ($id === false) $id = substr($element, 1);
263
+            } else if (strpos($element, '=') > 0) {
264
+                $parts = explode('=', $element, 2);
265
+                $attributes[] = $parts[0] . '="' . $parts[1] . '"';
266
+            }
267
+        }
268
+
269
+        if ($id === false || $id === '') {
270
+            $id = $defaultIdValue;
271
+        }
272
+
273
+        // Compose attributes as string
274
+        $attr_str = "";
275
+        if (!empty($id)) {
276
+            $attr_str .= ' id="'.$this->encodeAttribute($id) .'"';
277
+        }
278
+        if (!empty($classes)) {
279
+            $attr_str .= ' class="'. implode(" ", $classes) . '"';
280
+        }
281
+        if (!$this->no_markup && !empty($attributes)) {
282
+            $attr_str .= ' '.implode(" ", $attributes);
283
+        }
284
+        return $attr_str;
285
+    }
286
+
287
+    /**
288
+     * Strips link definitions from text, stores the URLs and titles in
289
+     * hash references.
290
+     * @param  string $text
291
+     * @return string
292
+     */
293
+    protected function stripLinkDefinitions($text) {
294
+        $less_than_tab = $this->tab_width - 1;
295
+
296
+        // Link defs are in the form: ^[id]: url "optional title"
297
+        $text = preg_replace_callback('{
298 298
 							^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?:	# id = $1
299 299
 							  [ ]*
300 300
 							  \n?				# maybe *one* newline
@@ -317,138 +317,138 @@  discard block
 block discarded – undo
317 317
 					(?:[ ]* '.$this->id_class_attr_catch_re.' )?  # $5 = extra id & class attr
318 318
 							(?:\n+|\Z)
319 319
 			}xm',
320
-			array($this, '_stripLinkDefinitions_callback'),
321
-			$text);
322
-		return $text;
323
-	}
324
-
325
-	/**
326
-	 * Strip link definition callback
327
-	 * @param  array $matches
328
-	 * @return string
329
-	 */
330
-	protected function _stripLinkDefinitions_callback($matches) {
331
-		$link_id = strtolower($matches[1]);
332
-		$url = $matches[2] == '' ? $matches[3] : $matches[2];
333
-		$this->urls[$link_id] = $url;
334
-		$this->titles[$link_id] =& $matches[4];
335
-		$this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]);
336
-		return ''; // String that will replace the block
337
-	}
338
-
339
-
340
-	/**
341
-	 * HTML block parser
342
-	 */
343
-
344
-	/**
345
-	 * Tags that are always treated as block tags
346
-	 * @var string
347
-	 */
348
-	protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure';
349
-
350
-	/**
351
-	 * Tags treated as block tags only if the opening tag is alone on its line
352
-	 * @var string
353
-	 */
354
-	protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
355
-
356
-	/**
357
-	 * Tags where markdown="1" default to span mode:
358
-	 * @var string
359
-	 */
360
-	protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
361
-
362
-	/**
363
-	 * Tags which must not have their contents modified, no matter where
364
-	 * they appear
365
-	 * @var string
366
-	 */
367
-	protected $clean_tags_re = 'script|style|math|svg';
368
-
369
-	/**
370
-	 * Tags that do not need to be closed.
371
-	 * @var string
372
-	 */
373
-	protected $auto_close_tags_re = 'hr|img|param|source|track';
374
-
375
-	/**
376
-	 * Hashify HTML Blocks and "clean tags".
377
-	 *
378
-	 * We only want to do this for block-level HTML tags, such as headers,
379
-	 * lists, and tables. That's because we still want to wrap <p>s around
380
-	 * "paragraphs" that are wrapped in non-block-level tags, such as anchors,
381
-	 * phrase emphasis, and spans. The list of tags we're looking for is
382
-	 * hard-coded.
383
-	 *
384
-	 * This works by calling _HashHTMLBlocks_InMarkdown, which then calls
385
-	 * _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
386
-	 * attribute is found within a tag, _HashHTMLBlocks_InHTML calls back
387
-	 *  _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
388
-	 * These two functions are calling each other. It's recursive!
389
-	 * @param  string $text
390
-	 * @return string
391
-	 */
392
-	protected function hashHTMLBlocks($text) {
393
-		if ($this->no_markup) {
394
-			return $text;
395
-		}
396
-
397
-		// Call the HTML-in-Markdown hasher.
398
-		list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
399
-
400
-		return $text;
401
-	}
402
-
403
-	/**
404
-	 * Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
405
-	 *
406
-	 * *   $indent is the number of space to be ignored when checking for code
407
-	 *     blocks. This is important because if we don't take the indent into
408
-	 *     account, something like this (which looks right) won't work as expected:
409
-	 *
410
-	 *     <div>
411
-	 *         <div markdown="1">
412
-	 *         Hello World.  <-- Is this a Markdown code block or text?
413
-	 *         </div>  <-- Is this a Markdown code block or a real tag?
414
-	 *     <div>
415
-	 *
416
-	 *     If you don't like this, just don't indent the tag on which
417
-	 *     you apply the markdown="1" attribute.
418
-	 *
419
-	 * *   If $enclosing_tag_re is not empty, stops at the first unmatched closing
420
-	 *     tag with that name. Nested tags supported.
421
-	 *
422
-	 * *   If $span is true, text inside must treated as span. So any double
423
-	 *     newline will be replaced by a single newline so that it does not create
424
-	 *     paragraphs.
425
-	 *
426
-	 * Returns an array of that form: ( processed text , remaining text )
427
-	 *
428
-	 * @param  string  $text
429
-	 * @param  integer $indent
430
-	 * @param  string  $enclosing_tag_re
431
-	 * @param  boolean $span
432
-	 * @return array
433
-	 */
434
-	protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
435
-										$enclosing_tag_re = '', $span = false)
436
-	{
437
-
438
-		if ($text === '') return array('', '');
439
-
440
-		// Regex to check for the presense of newlines around a block tag.
441
-		$newline_before_re = '/(?:^\n?|\n\n)*$/';
442
-		$newline_after_re =
443
-			'{
320
+            array($this, '_stripLinkDefinitions_callback'),
321
+            $text);
322
+        return $text;
323
+    }
324
+
325
+    /**
326
+     * Strip link definition callback
327
+     * @param  array $matches
328
+     * @return string
329
+     */
330
+    protected function _stripLinkDefinitions_callback($matches) {
331
+        $link_id = strtolower($matches[1]);
332
+        $url = $matches[2] == '' ? $matches[3] : $matches[2];
333
+        $this->urls[$link_id] = $url;
334
+        $this->titles[$link_id] =& $matches[4];
335
+        $this->ref_attr[$link_id] = $this->doExtraAttributes("", $dummy =& $matches[5]);
336
+        return ''; // String that will replace the block
337
+    }
338
+
339
+
340
+    /**
341
+     * HTML block parser
342
+     */
343
+
344
+    /**
345
+     * Tags that are always treated as block tags
346
+     * @var string
347
+     */
348
+    protected $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend|article|section|nav|aside|hgroup|header|footer|figcaption|figure';
349
+
350
+    /**
351
+     * Tags treated as block tags only if the opening tag is alone on its line
352
+     * @var string
353
+     */
354
+    protected $context_block_tags_re = 'script|noscript|style|ins|del|iframe|object|source|track|param|math|svg|canvas|audio|video';
355
+
356
+    /**
357
+     * Tags where markdown="1" default to span mode:
358
+     * @var string
359
+     */
360
+    protected $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address';
361
+
362
+    /**
363
+     * Tags which must not have their contents modified, no matter where
364
+     * they appear
365
+     * @var string
366
+     */
367
+    protected $clean_tags_re = 'script|style|math|svg';
368
+
369
+    /**
370
+     * Tags that do not need to be closed.
371
+     * @var string
372
+     */
373
+    protected $auto_close_tags_re = 'hr|img|param|source|track';
374
+
375
+    /**
376
+     * Hashify HTML Blocks and "clean tags".
377
+     *
378
+     * We only want to do this for block-level HTML tags, such as headers,
379
+     * lists, and tables. That's because we still want to wrap <p>s around
380
+     * "paragraphs" that are wrapped in non-block-level tags, such as anchors,
381
+     * phrase emphasis, and spans. The list of tags we're looking for is
382
+     * hard-coded.
383
+     *
384
+     * This works by calling _HashHTMLBlocks_InMarkdown, which then calls
385
+     * _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1"
386
+     * attribute is found within a tag, _HashHTMLBlocks_InHTML calls back
387
+     *  _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag.
388
+     * These two functions are calling each other. It's recursive!
389
+     * @param  string $text
390
+     * @return string
391
+     */
392
+    protected function hashHTMLBlocks($text) {
393
+        if ($this->no_markup) {
394
+            return $text;
395
+        }
396
+
397
+        // Call the HTML-in-Markdown hasher.
398
+        list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text);
399
+
400
+        return $text;
401
+    }
402
+
403
+    /**
404
+     * Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags.
405
+     *
406
+     * *   $indent is the number of space to be ignored when checking for code
407
+     *     blocks. This is important because if we don't take the indent into
408
+     *     account, something like this (which looks right) won't work as expected:
409
+     *
410
+     *     <div>
411
+     *         <div markdown="1">
412
+     *         Hello World.  <-- Is this a Markdown code block or text?
413
+     *         </div>  <-- Is this a Markdown code block or a real tag?
414
+     *     <div>
415
+     *
416
+     *     If you don't like this, just don't indent the tag on which
417
+     *     you apply the markdown="1" attribute.
418
+     *
419
+     * *   If $enclosing_tag_re is not empty, stops at the first unmatched closing
420
+     *     tag with that name. Nested tags supported.
421
+     *
422
+     * *   If $span is true, text inside must treated as span. So any double
423
+     *     newline will be replaced by a single newline so that it does not create
424
+     *     paragraphs.
425
+     *
426
+     * Returns an array of that form: ( processed text , remaining text )
427
+     *
428
+     * @param  string  $text
429
+     * @param  integer $indent
430
+     * @param  string  $enclosing_tag_re
431
+     * @param  boolean $span
432
+     * @return array
433
+     */
434
+    protected function _hashHTMLBlocks_inMarkdown($text, $indent = 0,
435
+                                        $enclosing_tag_re = '', $span = false)
436
+    {
437
+
438
+        if ($text === '') return array('', '');
439
+
440
+        // Regex to check for the presense of newlines around a block tag.
441
+        $newline_before_re = '/(?:^\n?|\n\n)*$/';
442
+        $newline_after_re =
443
+            '{
444 444
 				^						# Start of text following the tag.
445 445
 				(?>[ ]*<!--.*?-->)?		# Optional comment.
446 446
 				[ ]*\n					# Must be followed by newline.
447 447
 			}xs';
448 448
 
449
-		// Regex to match any tag.
450
-		$block_tag_re =
451
-			'{
449
+        // Regex to match any tag.
450
+        $block_tag_re =
451
+            '{
452 452
 				(					# $2: Capture whole tag.
453 453
 					</?					# Any opening or closing tag.
454 454
 						(?>				# Tag name.
@@ -501,156 +501,156 @@  discard block
 block discarded – undo
501 501
 			}xs';
502 502
 
503 503
 
504
-		$depth = 0;		// Current depth inside the tag tree.
505
-		$parsed = "";	// Parsed text that will be returned.
506
-
507
-		// Loop through every tag until we find the closing tag of the parent
508
-		// or loop until reaching the end of text if no parent tag specified.
509
-		do {
510
-			// Split the text using the first $tag_match pattern found.
511
-			// Text before  pattern will be first in the array, text after
512
-			// pattern will be at the end, and between will be any catches made
513
-			// by the pattern.
514
-			$parts = preg_split($block_tag_re, $text, 2,
515
-								PREG_SPLIT_DELIM_CAPTURE);
516
-
517
-			// If in Markdown span mode, add a empty-string span-level hash
518
-			// after each newline to prevent triggering any block element.
519
-			if ($span) {
520
-				$void = $this->hashPart("", ':');
521
-				$newline = "\n$void";
522
-				$parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
523
-			}
524
-
525
-			$parsed .= $parts[0]; // Text before current tag.
526
-
527
-			// If end of $text has been reached. Stop loop.
528
-			if (count($parts) < 3) {
529
-				$text = "";
530
-				break;
531
-			}
532
-
533
-			$tag  = $parts[1]; // Tag to handle.
534
-			$text = $parts[2]; // Remaining text after current tag.
535
-
536
-			// Check for: Fenced code block marker.
537
-			// Note: need to recheck the whole tag to disambiguate backtick
538
-			// fences from code spans
539
-			if (preg_match('{^\n?([ ]{0,' . ($indent + 3) . '})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+)?[ ]*(?:' . $this->id_class_attr_nocatch_re . ')?[ ]*\n?$}', $tag, $capture)) {
540
-				// Fenced code block marker: find matching end marker.
541
-				$fence_indent = strlen($capture[1]); // use captured indent in re
542
-				$fence_re = $capture[2]; // use captured fence in re
543
-				if (preg_match('{^(?>.*\n)*?[ ]{' . ($fence_indent) . '}' . $fence_re . '[ ]*(?:\n|$)}', $text,
544
-					$matches))
545
-				{
546
-					// End marker found: pass text unchanged until marker.
547
-					$parsed .= $tag . $matches[0];
548
-					$text = substr($text, strlen($matches[0]));
549
-				}
550
-				else {
551
-					// No end marker: just skip it.
552
-					$parsed .= $tag;
553
-				}
554
-			}
555
-			// Check for: Indented code block.
556
-			else if ($tag[0] === "\n" || $tag[0] === " ") {
557
-				// Indented code block: pass it unchanged, will be handled
558
-				// later.
559
-				$parsed .= $tag;
560
-			}
561
-			// Check for: Code span marker
562
-			// Note: need to check this after backtick fenced code blocks
563
-			else if ($tag[0] === "`") {
564
-				// Find corresponding end marker.
565
-				$tag_re = preg_quote($tag);
566
-				if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)' . $tag_re . '(?!`)}',
567
-					$text, $matches))
568
-				{
569
-					// End marker found: pass text unchanged until marker.
570
-					$parsed .= $tag . $matches[0];
571
-					$text = substr($text, strlen($matches[0]));
572
-				}
573
-				else {
574
-					// Unmatched marker: just skip it.
575
-					$parsed .= $tag;
576
-				}
577
-			}
578
-			// Check for: Opening Block level tag or
579
-			//            Opening Context Block tag (like ins and del)
580
-			//               used as a block tag (tag is alone on it's line).
581
-			else if (preg_match('{^<(?:' . $this->block_tags_re . ')\b}', $tag) ||
582
-				(	preg_match('{^<(?:' . $this->context_block_tags_re . ')\b}', $tag) &&
583
-					preg_match($newline_before_re, $parsed) &&
584
-					preg_match($newline_after_re, $text)	)
585
-				)
586
-			{
587
-				// Need to parse tag and following text using the HTML parser.
588
-				list($block_text, $text) =
589
-					$this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
590
-
591
-				// Make sure it stays outside of any paragraph by adding newlines.
592
-				$parsed .= "\n\n$block_text\n\n";
593
-			}
594
-			// Check for: Clean tag (like script, math)
595
-			//            HTML Comments, processing instructions.
596
-			else if (preg_match('{^<(?:' . $this->clean_tags_re . ')\b}', $tag) ||
597
-				$tag[1] === '!' || $tag[1] === '?')
598
-			{
599
-				// Need to parse tag and following text using the HTML parser.
600
-				// (don't check for markdown attribute)
601
-				list($block_text, $text) =
602
-					$this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
603
-
604
-				$parsed .= $block_text;
605
-			}
606
-			// Check for: Tag with same name as enclosing tag.
607
-			else if ($enclosing_tag_re !== '' &&
608
-				// Same name as enclosing tag.
609
-				preg_match('{^</?(?:' . $enclosing_tag_re . ')\b}', $tag))
610
-			{
611
-				// Increase/decrease nested tag count.
612
-				if ($tag[1] === '/') {
613
-					$depth--;
614
-				} else if ($tag[strlen($tag)-2] !== '/') {
615
-					$depth++;
616
-				}
617
-
618
-				if ($depth < 0) {
619
-					// Going out of parent element. Clean up and break so we
620
-					// return to the calling function.
621
-					$text = $tag . $text;
622
-					break;
623
-				}
624
-
625
-				$parsed .= $tag;
626
-			}
627
-			else {
628
-				$parsed .= $tag;
629
-			}
630
-		} while ($depth >= 0);
631
-
632
-		return array($parsed, $text);
633
-	}
634
-
635
-	/**
636
-	 * Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
637
-	 *
638
-	 * *   Calls $hash_method to convert any blocks.
639
-	 * *   Stops when the first opening tag closes.
640
-	 * *   $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
641
-	 *     (it is not inside clean tags)
642
-	 *
643
-	 * Returns an array of that form: ( processed text , remaining text )
644
-	 * @param  string $text
645
-	 * @param  string $hash_method
646
-	 * @param  bool $md_attr Handle `markdown="1"` attribute
647
-	 * @return array
648
-	 */
649
-	protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
650
-		if ($text === '') return array('', '');
651
-
652
-		// Regex to match `markdown` attribute inside of a tag.
653
-		$markdown_attr_re = '
504
+        $depth = 0;		// Current depth inside the tag tree.
505
+        $parsed = "";	// Parsed text that will be returned.
506
+
507
+        // Loop through every tag until we find the closing tag of the parent
508
+        // or loop until reaching the end of text if no parent tag specified.
509
+        do {
510
+            // Split the text using the first $tag_match pattern found.
511
+            // Text before  pattern will be first in the array, text after
512
+            // pattern will be at the end, and between will be any catches made
513
+            // by the pattern.
514
+            $parts = preg_split($block_tag_re, $text, 2,
515
+                                PREG_SPLIT_DELIM_CAPTURE);
516
+
517
+            // If in Markdown span mode, add a empty-string span-level hash
518
+            // after each newline to prevent triggering any block element.
519
+            if ($span) {
520
+                $void = $this->hashPart("", ':');
521
+                $newline = "\n$void";
522
+                $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void;
523
+            }
524
+
525
+            $parsed .= $parts[0]; // Text before current tag.
526
+
527
+            // If end of $text has been reached. Stop loop.
528
+            if (count($parts) < 3) {
529
+                $text = "";
530
+                break;
531
+            }
532
+
533
+            $tag  = $parts[1]; // Tag to handle.
534
+            $text = $parts[2]; // Remaining text after current tag.
535
+
536
+            // Check for: Fenced code block marker.
537
+            // Note: need to recheck the whole tag to disambiguate backtick
538
+            // fences from code spans
539
+            if (preg_match('{^\n?([ ]{0,' . ($indent + 3) . '})(~{3,}|`{3,})[ ]*(?:\.?[-_:a-zA-Z0-9]+)?[ ]*(?:' . $this->id_class_attr_nocatch_re . ')?[ ]*\n?$}', $tag, $capture)) {
540
+                // Fenced code block marker: find matching end marker.
541
+                $fence_indent = strlen($capture[1]); // use captured indent in re
542
+                $fence_re = $capture[2]; // use captured fence in re
543
+                if (preg_match('{^(?>.*\n)*?[ ]{' . ($fence_indent) . '}' . $fence_re . '[ ]*(?:\n|$)}', $text,
544
+                    $matches))
545
+                {
546
+                    // End marker found: pass text unchanged until marker.
547
+                    $parsed .= $tag . $matches[0];
548
+                    $text = substr($text, strlen($matches[0]));
549
+                }
550
+                else {
551
+                    // No end marker: just skip it.
552
+                    $parsed .= $tag;
553
+                }
554
+            }
555
+            // Check for: Indented code block.
556
+            else if ($tag[0] === "\n" || $tag[0] === " ") {
557
+                // Indented code block: pass it unchanged, will be handled
558
+                // later.
559
+                $parsed .= $tag;
560
+            }
561
+            // Check for: Code span marker
562
+            // Note: need to check this after backtick fenced code blocks
563
+            else if ($tag[0] === "`") {
564
+                // Find corresponding end marker.
565
+                $tag_re = preg_quote($tag);
566
+                if (preg_match('{^(?>.+?|\n(?!\n))*?(?<!`)' . $tag_re . '(?!`)}',
567
+                    $text, $matches))
568
+                {
569
+                    // End marker found: pass text unchanged until marker.
570
+                    $parsed .= $tag . $matches[0];
571
+                    $text = substr($text, strlen($matches[0]));
572
+                }
573
+                else {
574
+                    // Unmatched marker: just skip it.
575
+                    $parsed .= $tag;
576
+                }
577
+            }
578
+            // Check for: Opening Block level tag or
579
+            //            Opening Context Block tag (like ins and del)
580
+            //               used as a block tag (tag is alone on it's line).
581
+            else if (preg_match('{^<(?:' . $this->block_tags_re . ')\b}', $tag) ||
582
+                (	preg_match('{^<(?:' . $this->context_block_tags_re . ')\b}', $tag) &&
583
+                    preg_match($newline_before_re, $parsed) &&
584
+                    preg_match($newline_after_re, $text)	)
585
+                )
586
+            {
587
+                // Need to parse tag and following text using the HTML parser.
588
+                list($block_text, $text) =
589
+                    $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true);
590
+
591
+                // Make sure it stays outside of any paragraph by adding newlines.
592
+                $parsed .= "\n\n$block_text\n\n";
593
+            }
594
+            // Check for: Clean tag (like script, math)
595
+            //            HTML Comments, processing instructions.
596
+            else if (preg_match('{^<(?:' . $this->clean_tags_re . ')\b}', $tag) ||
597
+                $tag[1] === '!' || $tag[1] === '?')
598
+            {
599
+                // Need to parse tag and following text using the HTML parser.
600
+                // (don't check for markdown attribute)
601
+                list($block_text, $text) =
602
+                    $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false);
603
+
604
+                $parsed .= $block_text;
605
+            }
606
+            // Check for: Tag with same name as enclosing tag.
607
+            else if ($enclosing_tag_re !== '' &&
608
+                // Same name as enclosing tag.
609
+                preg_match('{^</?(?:' . $enclosing_tag_re . ')\b}', $tag))
610
+            {
611
+                // Increase/decrease nested tag count.
612
+                if ($tag[1] === '/') {
613
+                    $depth--;
614
+                } else if ($tag[strlen($tag)-2] !== '/') {
615
+                    $depth++;
616
+                }
617
+
618
+                if ($depth < 0) {
619
+                    // Going out of parent element. Clean up and break so we
620
+                    // return to the calling function.
621
+                    $text = $tag . $text;
622
+                    break;
623
+                }
624
+
625
+                $parsed .= $tag;
626
+            }
627
+            else {
628
+                $parsed .= $tag;
629
+            }
630
+        } while ($depth >= 0);
631
+
632
+        return array($parsed, $text);
633
+    }
634
+
635
+    /**
636
+     * Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags.
637
+     *
638
+     * *   Calls $hash_method to convert any blocks.
639
+     * *   Stops when the first opening tag closes.
640
+     * *   $md_attr indicate if the use of the `markdown="1"` attribute is allowed.
641
+     *     (it is not inside clean tags)
642
+     *
643
+     * Returns an array of that form: ( processed text , remaining text )
644
+     * @param  string $text
645
+     * @param  string $hash_method
646
+     * @param  bool $md_attr Handle `markdown="1"` attribute
647
+     * @return array
648
+     */
649
+    protected function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) {
650
+        if ($text === '') return array('', '');
651
+
652
+        // Regex to match `markdown` attribute inside of a tag.
653
+        $markdown_attr_re = '
654 654
 			{
655 655
 				\s*			# Eat whitespace before the `markdown` attribute
656 656
 				markdown
@@ -665,8 +665,8 @@  discard block
 block discarded – undo
665 665
 				()				# $4: make $3 always defined (avoid warnings)
666 666
 			}xs';
667 667
 
668
-		// Regex to match any tag.
669
-		$tag_re = '{
668
+        // Regex to match any tag.
669
+        $tag_re = '{
670 670
 				(					# $2: Capture whole tag.
671 671
 					</?					# Any opening or closing tag.
672 672
 						[\w:$]+			# Tag name.
@@ -688,143 +688,143 @@  discard block
 block discarded – undo
688 688
 				)
689 689
 			}xs';
690 690
 
691
-		$original_text = $text;		// Save original text in case of faliure.
692
-
693
-		$depth		= 0;	// Current depth inside the tag tree.
694
-		$block_text	= "";	// Temporary text holder for current text.
695
-		$parsed		= "";	// Parsed text that will be returned.
696
-		$base_tag_name_re = '';
697
-
698
-		// Get the name of the starting tag.
699
-		// (This pattern makes $base_tag_name_re safe without quoting.)
700
-		if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
701
-			$base_tag_name_re = $matches[1];
702
-
703
-		// Loop through every tag until we find the corresponding closing tag.
704
-		do {
705
-			// Split the text using the first $tag_match pattern found.
706
-			// Text before  pattern will be first in the array, text after
707
-			// pattern will be at the end, and between will be any catches made
708
-			// by the pattern.
709
-			$parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
710
-
711
-			if (count($parts) < 3) {
712
-				// End of $text reached with unbalenced tag(s).
713
-				// In that case, we return original text unchanged and pass the
714
-				// first character as filtered to prevent an infinite loop in the
715
-				// parent function.
716
-				return array($original_text[0], substr($original_text, 1));
717
-			}
718
-
719
-			$block_text .= $parts[0]; // Text before current tag.
720
-			$tag         = $parts[1]; // Tag to handle.
721
-			$text        = $parts[2]; // Remaining text after current tag.
722
-
723
-			// Check for: Auto-close tag (like <hr/>)
724
-			//			 Comments and Processing Instructions.
725
-			if (preg_match('{^</?(?:' . $this->auto_close_tags_re . ')\b}', $tag) ||
726
-				$tag[1] === '!' || $tag[1] === '?')
727
-			{
728
-				// Just add the tag to the block as if it was text.
729
-				$block_text .= $tag;
730
-			}
731
-			else {
732
-				// Increase/decrease nested tag count. Only do so if
733
-				// the tag's name match base tag's.
734
-				if (preg_match('{^</?' . $base_tag_name_re . '\b}', $tag)) {
735
-					if ($tag[1] === '/') {
736
-						$depth--;
737
-					} else if ($tag[strlen($tag)-2] !== '/') {
738
-						$depth++;
739
-					}
740
-				}
741
-
742
-				// Check for `markdown="1"` attribute and handle it.
743
-				if ($md_attr &&
744
-					preg_match($markdown_attr_re, $tag, $attr_m) &&
745
-					preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
746
-				{
747
-					// Remove `markdown` attribute from opening tag.
748
-					$tag = preg_replace($markdown_attr_re, '', $tag);
749
-
750
-					// Check if text inside this tag must be parsed in span mode.
751
-					$mode = $attr_m[2] . $attr_m[3];
752
-					$span_mode = $mode === 'span' || ($mode !== 'block' &&
753
-						preg_match('{^<(?:' . $this->contain_span_tags_re . ')\b}', $tag));
754
-
755
-					// Calculate indent before tag.
756
-					if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
757
-						$strlen = $this->utf8_strlen;
758
-						$indent = $strlen($matches[1], 'UTF-8');
759
-					} else {
760
-						$indent = 0;
761
-					}
762
-
763
-					// End preceding block with this tag.
764
-					$block_text .= $tag;
765
-					$parsed .= $this->$hash_method($block_text);
766
-
767
-					// Get enclosing tag name for the ParseMarkdown function.
768
-					// (This pattern makes $tag_name_re safe without quoting.)
769
-					preg_match('/^<([\w:$]*)\b/', $tag, $matches);
770
-					$tag_name_re = $matches[1];
771
-
772
-					// Parse the content using the HTML-in-Markdown parser.
773
-					list ($block_text, $text)
774
-						= $this->_hashHTMLBlocks_inMarkdown($text, $indent,
775
-							$tag_name_re, $span_mode);
776
-
777
-					// Outdent markdown text.
778
-					if ($indent > 0) {
779
-						$block_text = preg_replace("/^[ ]{1,$indent}/m", "",
780
-													$block_text);
781
-					}
782
-
783
-					// Append tag content to parsed text.
784
-					if (!$span_mode) {
785
-						$parsed .= "\n\n$block_text\n\n";
786
-					} else {
787
-						$parsed .= (string) $block_text;
788
-					}
789
-
790
-					// Start over with a new block.
791
-					$block_text = "";
792
-				}
793
-				else $block_text .= $tag;
794
-			}
795
-
796
-		} while ($depth > 0);
797
-
798
-		// Hash last block text that wasn't processed inside the loop.
799
-		$parsed .= $this->$hash_method($block_text);
800
-
801
-		return array($parsed, $text);
802
-	}
803
-
804
-	/**
805
-	 * Called whenever a tag must be hashed when a function inserts a "clean" tag
806
-	 * in $text, it passes through this function and is automaticaly escaped,
807
-	 * blocking invalid nested overlap.
808
-	 * @param  string $text
809
-	 * @return string
810
-	 */
811
-	protected function hashClean($text) {
812
-		return $this->hashPart($text, 'C');
813
-	}
814
-
815
-	/**
816
-	 * Turn Markdown link shortcuts into XHTML <a> tags.
817
-	 * @param  string $text
818
-	 * @return string
819
-	 */
820
-	protected function doAnchors($text) {
821
-		if ($this->in_anchor) {
822
-			return $text;
823
-		}
824
-		$this->in_anchor = true;
825
-
826
-		// First, handle reference-style links: [link text] [id]
827
-		$text = preg_replace_callback('{
691
+        $original_text = $text;		// Save original text in case of faliure.
692
+
693
+        $depth		= 0;	// Current depth inside the tag tree.
694
+        $block_text	= "";	// Temporary text holder for current text.
695
+        $parsed		= "";	// Parsed text that will be returned.
696
+        $base_tag_name_re = '';
697
+
698
+        // Get the name of the starting tag.
699
+        // (This pattern makes $base_tag_name_re safe without quoting.)
700
+        if (preg_match('/^<([\w:$]*)\b/', $text, $matches))
701
+            $base_tag_name_re = $matches[1];
702
+
703
+        // Loop through every tag until we find the corresponding closing tag.
704
+        do {
705
+            // Split the text using the first $tag_match pattern found.
706
+            // Text before  pattern will be first in the array, text after
707
+            // pattern will be at the end, and between will be any catches made
708
+            // by the pattern.
709
+            $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE);
710
+
711
+            if (count($parts) < 3) {
712
+                // End of $text reached with unbalenced tag(s).
713
+                // In that case, we return original text unchanged and pass the
714
+                // first character as filtered to prevent an infinite loop in the
715
+                // parent function.
716
+                return array($original_text[0], substr($original_text, 1));
717
+            }
718
+
719
+            $block_text .= $parts[0]; // Text before current tag.
720
+            $tag         = $parts[1]; // Tag to handle.
721
+            $text        = $parts[2]; // Remaining text after current tag.
722
+
723
+            // Check for: Auto-close tag (like <hr/>)
724
+            //			 Comments and Processing Instructions.
725
+            if (preg_match('{^</?(?:' . $this->auto_close_tags_re . ')\b}', $tag) ||
726
+                $tag[1] === '!' || $tag[1] === '?')
727
+            {
728
+                // Just add the tag to the block as if it was text.
729
+                $block_text .= $tag;
730
+            }
731
+            else {
732
+                // Increase/decrease nested tag count. Only do so if
733
+                // the tag's name match base tag's.
734
+                if (preg_match('{^</?' . $base_tag_name_re . '\b}', $tag)) {
735
+                    if ($tag[1] === '/') {
736
+                        $depth--;
737
+                    } else if ($tag[strlen($tag)-2] !== '/') {
738
+                        $depth++;
739
+                    }
740
+                }
741
+
742
+                // Check for `markdown="1"` attribute and handle it.
743
+                if ($md_attr &&
744
+                    preg_match($markdown_attr_re, $tag, $attr_m) &&
745
+                    preg_match('/^1|block|span$/', $attr_m[2] . $attr_m[3]))
746
+                {
747
+                    // Remove `markdown` attribute from opening tag.
748
+                    $tag = preg_replace($markdown_attr_re, '', $tag);
749
+
750
+                    // Check if text inside this tag must be parsed in span mode.
751
+                    $mode = $attr_m[2] . $attr_m[3];
752
+                    $span_mode = $mode === 'span' || ($mode !== 'block' &&
753
+                        preg_match('{^<(?:' . $this->contain_span_tags_re . ')\b}', $tag));
754
+
755
+                    // Calculate indent before tag.
756
+                    if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) {
757
+                        $strlen = $this->utf8_strlen;
758
+                        $indent = $strlen($matches[1], 'UTF-8');
759
+                    } else {
760
+                        $indent = 0;
761
+                    }
762
+
763
+                    // End preceding block with this tag.
764
+                    $block_text .= $tag;
765
+                    $parsed .= $this->$hash_method($block_text);
766
+
767
+                    // Get enclosing tag name for the ParseMarkdown function.
768
+                    // (This pattern makes $tag_name_re safe without quoting.)
769
+                    preg_match('/^<([\w:$]*)\b/', $tag, $matches);
770
+                    $tag_name_re = $matches[1];
771
+
772
+                    // Parse the content using the HTML-in-Markdown parser.
773
+                    list ($block_text, $text)
774
+                        = $this->_hashHTMLBlocks_inMarkdown($text, $indent,
775
+                            $tag_name_re, $span_mode);
776
+
777
+                    // Outdent markdown text.
778
+                    if ($indent > 0) {
779
+                        $block_text = preg_replace("/^[ ]{1,$indent}/m", "",
780
+                                                    $block_text);
781
+                    }
782
+
783
+                    // Append tag content to parsed text.
784
+                    if (!$span_mode) {
785
+                        $parsed .= "\n\n$block_text\n\n";
786
+                    } else {
787
+                        $parsed .= (string) $block_text;
788
+                    }
789
+
790
+                    // Start over with a new block.
791
+                    $block_text = "";
792
+                }
793
+                else $block_text .= $tag;
794
+            }
795
+
796
+        } while ($depth > 0);
797
+
798
+        // Hash last block text that wasn't processed inside the loop.
799
+        $parsed .= $this->$hash_method($block_text);
800
+
801
+        return array($parsed, $text);
802
+    }
803
+
804
+    /**
805
+     * Called whenever a tag must be hashed when a function inserts a "clean" tag
806
+     * in $text, it passes through this function and is automaticaly escaped,
807
+     * blocking invalid nested overlap.
808
+     * @param  string $text
809
+     * @return string
810
+     */
811
+    protected function hashClean($text) {
812
+        return $this->hashPart($text, 'C');
813
+    }
814
+
815
+    /**
816
+     * Turn Markdown link shortcuts into XHTML <a> tags.
817
+     * @param  string $text
818
+     * @return string
819
+     */
820
+    protected function doAnchors($text) {
821
+        if ($this->in_anchor) {
822
+            return $text;
823
+        }
824
+        $this->in_anchor = true;
825
+
826
+        // First, handle reference-style links: [link text] [id]
827
+        $text = preg_replace_callback('{
828 828
 			(					# wrap whole match in $1
829 829
 			  \[
830 830
 				(' . $this->nested_brackets_re . ')	# link text = $2
@@ -838,10 +838,10 @@  discard block
 block discarded – undo
838 838
 			  \]
839 839
 			)
840 840
 			}xs',
841
-			array($this, '_doAnchors_reference_callback'), $text);
841
+            array($this, '_doAnchors_reference_callback'), $text);
842 842
 
843
-		// Next, inline-style links: [link text](url "optional title")
844
-		$text = preg_replace_callback('{
843
+        // Next, inline-style links: [link text](url "optional title")
844
+        $text = preg_replace_callback('{
845 845
 			(				# wrap whole match in $1
846 846
 			  \[
847 847
 				(' . $this->nested_brackets_re . ')	# link text = $2
@@ -864,106 +864,106 @@  discard block
 block discarded – undo
864 864
 			  (?:[ ]? ' . $this->id_class_attr_catch_re . ' )?	 # $8 = id/class attributes
865 865
 			)
866 866
 			}xs',
867
-			array($this, '_doAnchors_inline_callback'), $text);
867
+            array($this, '_doAnchors_inline_callback'), $text);
868 868
 
869
-		// Last, handle reference-style shortcuts: [link text]
870
-		// These must come last in case you've also got [link text][1]
871
-		// or [link text](/foo)
872
-		$text = preg_replace_callback('{
869
+        // Last, handle reference-style shortcuts: [link text]
870
+        // These must come last in case you've also got [link text][1]
871
+        // or [link text](/foo)
872
+        $text = preg_replace_callback('{
873 873
 			(					# wrap whole match in $1
874 874
 			  \[
875 875
 				([^\[\]]+)		# link text = $2; can\'t contain [ or ]
876 876
 			  \]
877 877
 			)
878 878
 			}xs',
879
-			array($this, '_doAnchors_reference_callback'), $text);
880
-
881
-		$this->in_anchor = false;
882
-		return $text;
883
-	}
884
-
885
-	/**
886
-	 * Callback for reference anchors
887
-	 * @param  array $matches
888
-	 * @return string
889
-	 */
890
-	protected function _doAnchors_reference_callback($matches) {
891
-		$whole_match =  $matches[1];
892
-		$link_text   =  $matches[2];
893
-		$link_id     =& $matches[3];
894
-
895
-		if ($link_id == "") {
896
-			// for shortcut links like [this][] or [this].
897
-			$link_id = $link_text;
898
-		}
899
-
900
-		// lower-case and turn embedded newlines into spaces
901
-		$link_id = strtolower($link_id);
902
-		$link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
903
-
904
-		if (isset($this->urls[$link_id])) {
905
-			$url = $this->urls[$link_id];
906
-			$url = $this->encodeURLAttribute($url);
907
-
908
-			$result = "<a href=\"$url\"";
909
-			if ( isset( $this->titles[$link_id] ) ) {
910
-				$title = $this->titles[$link_id];
911
-				$title = $this->encodeAttribute($title);
912
-				$result .=  " title=\"$title\"";
913
-			}
914
-			if (isset($this->ref_attr[$link_id]))
915
-				$result .= $this->ref_attr[$link_id];
916
-
917
-			$link_text = $this->runSpanGamut($link_text);
918
-			$result .= ">$link_text</a>";
919
-			$result = $this->hashPart($result);
920
-		}
921
-		else {
922
-			$result = $whole_match;
923
-		}
924
-		return $result;
925
-	}
926
-
927
-	/**
928
-	 * Callback for inline anchors
929
-	 * @param  array $matches
930
-	 * @return string
931
-	 */
932
-	protected function _doAnchors_inline_callback($matches) {
933
-		$link_text		=  $this->runSpanGamut($matches[2]);
934
-		$url			=  $matches[3] === '' ? $matches[4] : $matches[3];
935
-		$title			=& $matches[7];
936
-		$attr  = $this->doExtraAttributes("a", $dummy =& $matches[8]);
937
-
938
-		// if the URL was of the form <s p a c e s> it got caught by the HTML
939
-		// tag parser and hashed. Need to reverse the process before using the URL.
940
-		$unhashed = $this->unhash($url);
941
-		if ($unhashed !== $url)
942
-			$url = preg_replace('/^<(.*)>$/', '\1', $unhashed);
943
-
944
-		$url = $this->encodeURLAttribute($url);
945
-
946
-		$result = "<a href=\"$url\"";
947
-		if (isset($title)) {
948
-			$title = $this->encodeAttribute($title);
949
-			$result .=  " title=\"$title\"";
950
-		}
951
-		$result .= $attr;
952
-
953
-		$link_text = $this->runSpanGamut($link_text);
954
-		$result .= ">$link_text</a>";
955
-
956
-		return $this->hashPart($result);
957
-	}
958
-
959
-	/**
960
-	 * Turn Markdown image shortcuts into <img> tags.
961
-	 * @param  string $text
962
-	 * @return string
963
-	 */
964
-	protected function doImages($text) {
965
-		// First, handle reference-style labeled images: ![alt text][id]
966
-		$text = preg_replace_callback('{
879
+            array($this, '_doAnchors_reference_callback'), $text);
880
+
881
+        $this->in_anchor = false;
882
+        return $text;
883
+    }
884
+
885
+    /**
886
+     * Callback for reference anchors
887
+     * @param  array $matches
888
+     * @return string
889
+     */
890
+    protected function _doAnchors_reference_callback($matches) {
891
+        $whole_match =  $matches[1];
892
+        $link_text   =  $matches[2];
893
+        $link_id     =& $matches[3];
894
+
895
+        if ($link_id == "") {
896
+            // for shortcut links like [this][] or [this].
897
+            $link_id = $link_text;
898
+        }
899
+
900
+        // lower-case and turn embedded newlines into spaces
901
+        $link_id = strtolower($link_id);
902
+        $link_id = preg_replace('{[ ]?\n}', ' ', $link_id);
903
+
904
+        if (isset($this->urls[$link_id])) {
905
+            $url = $this->urls[$link_id];
906
+            $url = $this->encodeURLAttribute($url);
907
+
908
+            $result = "<a href=\"$url\"";
909
+            if ( isset( $this->titles[$link_id] ) ) {
910
+                $title = $this->titles[$link_id];
911
+                $title = $this->encodeAttribute($title);
912
+                $result .=  " title=\"$title\"";
913
+            }
914
+            if (isset($this->ref_attr[$link_id]))
915
+                $result .= $this->ref_attr[$link_id];
916
+
917
+            $link_text = $this->runSpanGamut($link_text);
918
+            $result .= ">$link_text</a>";
919
+            $result = $this->hashPart($result);
920
+        }
921
+        else {
922
+            $result = $whole_match;
923
+        }
924
+        return $result;
925
+    }
926
+
927
+    /**
928
+     * Callback for inline anchors
929
+     * @param  array $matches
930
+     * @return string
931
+     */
932
+    protected function _doAnchors_inline_callback($matches) {
933
+        $link_text		=  $this->runSpanGamut($matches[2]);
934
+        $url			=  $matches[3] === '' ? $matches[4] : $matches[3];
935
+        $title			=& $matches[7];
936
+        $attr  = $this->doExtraAttributes("a", $dummy =& $matches[8]);
937
+
938
+        // if the URL was of the form <s p a c e s> it got caught by the HTML
939
+        // tag parser and hashed. Need to reverse the process before using the URL.
940
+        $unhashed = $this->unhash($url);
941
+        if ($unhashed !== $url)
942
+            $url = preg_replace('/^<(.*)>$/', '\1', $unhashed);
943
+
944
+        $url = $this->encodeURLAttribute($url);
945
+
946
+        $result = "<a href=\"$url\"";
947
+        if (isset($title)) {
948
+            $title = $this->encodeAttribute($title);
949
+            $result .=  " title=\"$title\"";
950
+        }
951
+        $result .= $attr;
952
+
953
+        $link_text = $this->runSpanGamut($link_text);
954
+        $result .= ">$link_text</a>";
955
+
956
+        return $this->hashPart($result);
957
+    }
958
+
959
+    /**
960
+     * Turn Markdown image shortcuts into <img> tags.
961
+     * @param  string $text
962
+     * @return string
963
+     */
964
+    protected function doImages($text) {
965
+        // First, handle reference-style labeled images: ![alt text][id]
966
+        $text = preg_replace_callback('{
967 967
 			(				# wrap whole match in $1
968 968
 			  !\[
969 969
 				(' . $this->nested_brackets_re . ')		# alt text = $2
@@ -978,11 +978,11 @@  discard block
 block discarded – undo
978 978
 
979 979
 			)
980 980
 			}xs',
981
-			array($this, '_doImages_reference_callback'), $text);
981
+            array($this, '_doImages_reference_callback'), $text);
982 982
 
983
-		// Next, handle inline images:  ![alt text](url "optional title")
984
-		// Don't forget: encode * and _
985
-		$text = preg_replace_callback('{
983
+        // Next, handle inline images:  ![alt text](url "optional title")
984
+        // Don't forget: encode * and _
985
+        $text = preg_replace_callback('{
986 986
 			(				# wrap whole match in $1
987 987
 			  !\[
988 988
 				(' . $this->nested_brackets_re . ')		# alt text = $2
@@ -1006,101 +1006,101 @@  discard block
 block discarded – undo
1006 1006
 			  (?:[ ]? ' . $this->id_class_attr_catch_re . ' )?	 # $8 = id/class attributes
1007 1007
 			)
1008 1008
 			}xs',
1009
-			array($this, '_doImages_inline_callback'), $text);
1010
-
1011
-		return $text;
1012
-	}
1013
-
1014
-	/**
1015
-	 * Callback for referenced images
1016
-	 * @param  array $matches
1017
-	 * @return string
1018
-	 */
1019
-	protected function _doImages_reference_callback($matches) {
1020
-		$whole_match = $matches[1];
1021
-		$alt_text    = $matches[2];
1022
-		$link_id     = strtolower($matches[3]);
1023
-
1024
-		if ($link_id === "") {
1025
-			$link_id = strtolower($alt_text); // for shortcut links like ![this][].
1026
-		}
1027
-
1028
-		$alt_text = $this->encodeAttribute($alt_text);
1029
-		if (isset($this->urls[$link_id])) {
1030
-			$url = $this->encodeURLAttribute($this->urls[$link_id]);
1031
-			$result = "<img src=\"$url\" alt=\"$alt_text\"";
1032
-			if (isset($this->titles[$link_id])) {
1033
-				$title = $this->titles[$link_id];
1034
-				$title = $this->encodeAttribute($title);
1035
-				$result .=  " title=\"$title\"";
1036
-			}
1037
-			if (isset($this->ref_attr[$link_id])) {
1038
-				$result .= $this->ref_attr[$link_id];
1039
-			}
1040
-			$result .= $this->empty_element_suffix;
1041
-			$result = $this->hashPart($result);
1042
-		}
1043
-		else {
1044
-			// If there's no such link ID, leave intact:
1045
-			$result = $whole_match;
1046
-		}
1047
-
1048
-		return $result;
1049
-	}
1050
-
1051
-	/**
1052
-	 * Callback for inline images
1053
-	 * @param  array $matches
1054
-	 * @return string
1055
-	 */
1056
-	protected function _doImages_inline_callback($matches) {
1057
-		$alt_text		= $matches[2];
1058
-		$url			= $matches[3] === '' ? $matches[4] : $matches[3];
1059
-		$title			=& $matches[7];
1060
-		$attr  = $this->doExtraAttributes("img", $dummy =& $matches[8]);
1061
-
1062
-		$alt_text = $this->encodeAttribute($alt_text);
1063
-		$url = $this->encodeURLAttribute($url);
1064
-		$result = "<img src=\"$url\" alt=\"$alt_text\"";
1065
-		if (isset($title)) {
1066
-			$title = $this->encodeAttribute($title);
1067
-			$result .=  " title=\"$title\""; // $title already quoted
1068
-		}
1069
-		$result .= $attr;
1070
-		$result .= $this->empty_element_suffix;
1071
-
1072
-		return $this->hashPart($result);
1073
-	}
1074
-
1075
-	/**
1076
-	 * Process markdown headers. Redefined to add ID and class attribute support.
1077
-	 * @param  string $text
1078
-	 * @return string
1079
-	 */
1080
-	protected function doHeaders($text) {
1081
-		// Setext-style headers:
1082
-		//  Header 1  {#header1}
1083
-		//	  ========
1084
-		//
1085
-		//	  Header 2  {#header2 .class1 .class2}
1086
-		//	  --------
1087
-		//
1088
-		$text = preg_replace_callback(
1089
-			'{
1009
+            array($this, '_doImages_inline_callback'), $text);
1010
+
1011
+        return $text;
1012
+    }
1013
+
1014
+    /**
1015
+     * Callback for referenced images
1016
+     * @param  array $matches
1017
+     * @return string
1018
+     */
1019
+    protected function _doImages_reference_callback($matches) {
1020
+        $whole_match = $matches[1];
1021
+        $alt_text    = $matches[2];
1022
+        $link_id     = strtolower($matches[3]);
1023
+
1024
+        if ($link_id === "") {
1025
+            $link_id = strtolower($alt_text); // for shortcut links like ![this][].
1026
+        }
1027
+
1028
+        $alt_text = $this->encodeAttribute($alt_text);
1029
+        if (isset($this->urls[$link_id])) {
1030
+            $url = $this->encodeURLAttribute($this->urls[$link_id]);
1031
+            $result = "<img src=\"$url\" alt=\"$alt_text\"";
1032
+            if (isset($this->titles[$link_id])) {
1033
+                $title = $this->titles[$link_id];
1034
+                $title = $this->encodeAttribute($title);
1035
+                $result .=  " title=\"$title\"";
1036
+            }
1037
+            if (isset($this->ref_attr[$link_id])) {
1038
+                $result .= $this->ref_attr[$link_id];
1039
+            }
1040
+            $result .= $this->empty_element_suffix;
1041
+            $result = $this->hashPart($result);
1042
+        }
1043
+        else {
1044
+            // If there's no such link ID, leave intact:
1045
+            $result = $whole_match;
1046
+        }
1047
+
1048
+        return $result;
1049
+    }
1050
+
1051
+    /**
1052
+     * Callback for inline images
1053
+     * @param  array $matches
1054
+     * @return string
1055
+     */
1056
+    protected function _doImages_inline_callback($matches) {
1057
+        $alt_text		= $matches[2];
1058
+        $url			= $matches[3] === '' ? $matches[4] : $matches[3];
1059
+        $title			=& $matches[7];
1060
+        $attr  = $this->doExtraAttributes("img", $dummy =& $matches[8]);
1061
+
1062
+        $alt_text = $this->encodeAttribute($alt_text);
1063
+        $url = $this->encodeURLAttribute($url);
1064
+        $result = "<img src=\"$url\" alt=\"$alt_text\"";
1065
+        if (isset($title)) {
1066
+            $title = $this->encodeAttribute($title);
1067
+            $result .=  " title=\"$title\""; // $title already quoted
1068
+        }
1069
+        $result .= $attr;
1070
+        $result .= $this->empty_element_suffix;
1071
+
1072
+        return $this->hashPart($result);
1073
+    }
1074
+
1075
+    /**
1076
+     * Process markdown headers. Redefined to add ID and class attribute support.
1077
+     * @param  string $text
1078
+     * @return string
1079
+     */
1080
+    protected function doHeaders($text) {
1081
+        // Setext-style headers:
1082
+        //  Header 1  {#header1}
1083
+        //	  ========
1084
+        //
1085
+        //	  Header 2  {#header2 .class1 .class2}
1086
+        //	  --------
1087
+        //
1088
+        $text = preg_replace_callback(
1089
+            '{
1090 1090
 				(^.+?)								# $1: Header text
1091 1091
 				(?:[ ]+ ' . $this->id_class_attr_catch_re . ' )?	 # $3 = id/class attributes
1092 1092
 				[ ]*\n(=+|-+)[ ]*\n+				# $3: Header footer
1093 1093
 			}mx',
1094
-			array($this, '_doHeaders_callback_setext'), $text);
1095
-
1096
-		// atx-style headers:
1097
-		//	# Header 1        {#header1}
1098
-		//	## Header 2       {#header2}
1099
-		//	## Header 2 with closing hashes ##  {#header3.class1.class2}
1100
-		//	...
1101
-		//	###### Header 6   {.class2}
1102
-		//
1103
-		$text = preg_replace_callback('{
1094
+            array($this, '_doHeaders_callback_setext'), $text);
1095
+
1096
+        // atx-style headers:
1097
+        //	# Header 1        {#header1}
1098
+        //	## Header 2       {#header2}
1099
+        //	## Header 2 with closing hashes ##  {#header3.class1.class2}
1100
+        //	...
1101
+        //	###### Header 6   {.class2}
1102
+        //
1103
+        $text = preg_replace_callback('{
1104 1104
 				^(\#{1,6})	# $1 = string of #\'s
1105 1105
 				[ ]'.($this->hashtag_protection ? '+' : '*').'
1106 1106
 				(.+?)		# $2 = Header text
@@ -1110,58 +1110,58 @@  discard block
 block discarded – undo
1110 1110
 				[ ]*
1111 1111
 				\n+
1112 1112
 			}xm',
1113
-			array($this, '_doHeaders_callback_atx'), $text);
1114
-
1115
-		return $text;
1116
-	}
1117
-
1118
-	/**
1119
-	 * Callback for setext headers
1120
-	 * @param  array $matches
1121
-	 * @return string
1122
-	 */
1123
-	protected function _doHeaders_callback_setext($matches) {
1124
-		if ($matches[3] === '-' && preg_match('{^- }', $matches[1])) {
1125
-			return $matches[0];
1126
-		}
1127
-
1128
-		$level = $matches[3][0] === '=' ? 1 : 2;
1129
-
1130
-		$defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null;
1131
-
1132
-		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId);
1133
-		$block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>";
1134
-		return "\n" . $this->hashBlock($block) . "\n\n";
1135
-	}
1136
-
1137
-	/**
1138
-	 * Callback for atx headers
1139
-	 * @param  array $matches
1140
-	 * @return string
1141
-	 */
1142
-	protected function _doHeaders_callback_atx($matches) {
1143
-		$level = strlen($matches[1]);
1144
-
1145
-		$defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null;
1146
-		$attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId);
1147
-		$block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>";
1148
-		return "\n" . $this->hashBlock($block) . "\n\n";
1149
-	}
1150
-
1151
-	/**
1152
-	 * Form HTML tables.
1153
-	 * @param  string $text
1154
-	 * @return string
1155
-	 */
1156
-	protected function doTables($text) {
1157
-		$less_than_tab = $this->tab_width - 1;
1158
-		// Find tables with leading pipe.
1159
-		//
1160
-		//	| Header 1 | Header 2
1161
-		//	| -------- | --------
1162
-		//	| Cell 1   | Cell 2
1163
-		//	| Cell 3   | Cell 4
1164
-		$text = preg_replace_callback('
1113
+            array($this, '_doHeaders_callback_atx'), $text);
1114
+
1115
+        return $text;
1116
+    }
1117
+
1118
+    /**
1119
+     * Callback for setext headers
1120
+     * @param  array $matches
1121
+     * @return string
1122
+     */
1123
+    protected function _doHeaders_callback_setext($matches) {
1124
+        if ($matches[3] === '-' && preg_match('{^- }', $matches[1])) {
1125
+            return $matches[0];
1126
+        }
1127
+
1128
+        $level = $matches[3][0] === '=' ? 1 : 2;
1129
+
1130
+        $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[1]) : null;
1131
+
1132
+        $attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[2], $defaultId);
1133
+        $block = "<h$level$attr>" . $this->runSpanGamut($matches[1]) . "</h$level>";
1134
+        return "\n" . $this->hashBlock($block) . "\n\n";
1135
+    }
1136
+
1137
+    /**
1138
+     * Callback for atx headers
1139
+     * @param  array $matches
1140
+     * @return string
1141
+     */
1142
+    protected function _doHeaders_callback_atx($matches) {
1143
+        $level = strlen($matches[1]);
1144
+
1145
+        $defaultId = is_callable($this->header_id_func) ? call_user_func($this->header_id_func, $matches[2]) : null;
1146
+        $attr  = $this->doExtraAttributes("h$level", $dummy =& $matches[3], $defaultId);
1147
+        $block = "<h$level$attr>" . $this->runSpanGamut($matches[2]) . "</h$level>";
1148
+        return "\n" . $this->hashBlock($block) . "\n\n";
1149
+    }
1150
+
1151
+    /**
1152
+     * Form HTML tables.
1153
+     * @param  string $text
1154
+     * @return string
1155
+     */
1156
+    protected function doTables($text) {
1157
+        $less_than_tab = $this->tab_width - 1;
1158
+        // Find tables with leading pipe.
1159
+        //
1160
+        //	| Header 1 | Header 2
1161
+        //	| -------- | --------
1162
+        //	| Cell 1   | Cell 2
1163
+        //	| Cell 3   | Cell 4
1164
+        $text = preg_replace_callback('
1165 1165
 			{
1166 1166
 				^							# Start of a line
1167 1167
 				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
@@ -1179,15 +1179,15 @@  discard block
 block discarded – undo
1179 1179
 				)
1180 1180
 				(?=\n|\Z)					# Stop at final double newline.
1181 1181
 			}xm',
1182
-			array($this, '_doTable_leadingPipe_callback'), $text);
1183
-
1184
-		// Find tables without leading pipe.
1185
-		//
1186
-		//	Header 1 | Header 2
1187
-		//	-------- | --------
1188
-		//	Cell 1   | Cell 2
1189
-		//	Cell 3   | Cell 4
1190
-		$text = preg_replace_callback('
1182
+            array($this, '_doTable_leadingPipe_callback'), $text);
1183
+
1184
+        // Find tables without leading pipe.
1185
+        //
1186
+        //	Header 1 | Header 2
1187
+        //	-------- | --------
1188
+        //	Cell 1   | Cell 2
1189
+        //	Cell 3   | Cell 4
1190
+        $text = preg_replace_callback('
1191 1191
 			{
1192 1192
 				^							# Start of a line
1193 1193
 				[ ]{0,' . $less_than_tab . '}	# Allowed whitespace.
@@ -1203,120 +1203,120 @@  discard block
 block discarded – undo
1203 1203
 				)
1204 1204
 				(?=\n|\Z)					# Stop at final double newline.
1205 1205
 			}xm',
1206
-			array($this, '_DoTable_callback'), $text);
1207
-
1208
-		return $text;
1209
-	}
1210
-
1211
-	/**
1212
-	 * Callback for removing the leading pipe for each row
1213
-	 * @param  array $matches
1214
-	 * @return string
1215
-	 */
1216
-	protected function _doTable_leadingPipe_callback($matches) {
1217
-		$head		= $matches[1];
1218
-		$underline	= $matches[2];
1219
-		$content	= $matches[3];
1220
-
1221
-		$content	= preg_replace('/^ *[|]/m', '', $content);
1222
-
1223
-		return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
1224
-	}
1225
-
1226
-	/**
1227
-	 * Make the align attribute in a table
1228
-	 * @param  string $alignname
1229
-	 * @return string
1230
-	 */
1231
-	protected function _doTable_makeAlignAttr($alignname) {
1232
-		if (empty($this->table_align_class_tmpl)) {
1233
-			return " align=\"$alignname\"";
1234
-		}
1235
-
1236
-		$classname = str_replace('%%', $alignname, $this->table_align_class_tmpl);
1237
-		return " class=\"$classname\"";
1238
-	}
1239
-
1240
-	/**
1241
-	 * Calback for processing tables
1242
-	 * @param  array $matches
1243
-	 * @return string
1244
-	 */
1245
-	protected function _doTable_callback($matches) {
1246
-		$head		= $matches[1];
1247
-		$underline	= $matches[2];
1248
-		$content	= $matches[3];
1249
-
1250
-		// Remove any tailing pipes for each line.
1251
-		$head		= preg_replace('/[|] *$/m', '', $head);
1252
-		$underline	= preg_replace('/[|] *$/m', '', $underline);
1253
-		$content	= preg_replace('/[|] *$/m', '', $content);
1254
-
1255
-		// Reading alignement from header underline.
1256
-		$separators	= preg_split('/ *[|] */', $underline);
1257
-		foreach ($separators as $n => $s) {
1258
-			if (preg_match('/^ *-+: *$/', $s))
1259
-				$attr[$n] = $this->_doTable_makeAlignAttr('right');
1260
-			else if (preg_match('/^ *:-+: *$/', $s))
1261
-				$attr[$n] = $this->_doTable_makeAlignAttr('center');
1262
-			else if (preg_match('/^ *:-+ *$/', $s))
1263
-				$attr[$n] = $this->_doTable_makeAlignAttr('left');
1264
-			else
1265
-				$attr[$n] = '';
1266
-		}
1267
-
1268
-		// Parsing span elements, including code spans, character escapes,
1269
-		// and inline HTML tags, so that pipes inside those gets ignored.
1270
-		$head		= $this->parseSpan($head);
1271
-		$headers	= preg_split('/ *[|] */', $head);
1272
-		$col_count	= count($headers);
1273
-		$attr       = array_pad($attr, $col_count, '');
1274
-
1275
-		// Write column headers.
1276
-		$text = "<table>\n";
1277
-		$text .= "<thead>\n";
1278
-		$text .= "<tr>\n";
1279
-		foreach ($headers as $n => $header) {
1280
-			$text .= "  <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n";
1281
-		}
1282
-		$text .= "</tr>\n";
1283
-		$text .= "</thead>\n";
1284
-
1285
-		// Split content by row.
1286
-		$rows = explode("\n", trim($content, "\n"));
1287
-
1288
-		$text .= "<tbody>\n";
1289
-		foreach ($rows as $row) {
1290
-			// Parsing span elements, including code spans, character escapes,
1291
-			// and inline HTML tags, so that pipes inside those gets ignored.
1292
-			$row = $this->parseSpan($row);
1293
-
1294
-			// Split row by cell.
1295
-			$row_cells = preg_split('/ *[|] */', $row, $col_count);
1296
-			$row_cells = array_pad($row_cells, $col_count, '');
1297
-
1298
-			$text .= "<tr>\n";
1299
-			foreach ($row_cells as $n => $cell) {
1300
-				$text .= "  <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n";
1301
-			}
1302
-			$text .= "</tr>\n";
1303
-		}
1304
-		$text .= "</tbody>\n";
1305
-		$text .= "</table>";
1306
-
1307
-		return $this->hashBlock($text) . "\n";
1308
-	}
1309
-
1310
-	/**
1311
-	 * Form HTML definition lists.
1312
-	 * @param  string $text
1313
-	 * @return string
1314
-	 */
1315
-	protected function doDefLists($text) {
1316
-		$less_than_tab = $this->tab_width - 1;
1317
-
1318
-		// Re-usable pattern to match any entire dl list:
1319
-		$whole_list_re = '(?>
1206
+            array($this, '_DoTable_callback'), $text);
1207
+
1208
+        return $text;
1209
+    }
1210
+
1211
+    /**
1212
+     * Callback for removing the leading pipe for each row
1213
+     * @param  array $matches
1214
+     * @return string
1215
+     */
1216
+    protected function _doTable_leadingPipe_callback($matches) {
1217
+        $head		= $matches[1];
1218
+        $underline	= $matches[2];
1219
+        $content	= $matches[3];
1220
+
1221
+        $content	= preg_replace('/^ *[|]/m', '', $content);
1222
+
1223
+        return $this->_doTable_callback(array($matches[0], $head, $underline, $content));
1224
+    }
1225
+
1226
+    /**
1227
+     * Make the align attribute in a table
1228
+     * @param  string $alignname
1229
+     * @return string
1230
+     */
1231
+    protected function _doTable_makeAlignAttr($alignname) {
1232
+        if (empty($this->table_align_class_tmpl)) {
1233
+            return " align=\"$alignname\"";
1234
+        }
1235
+
1236
+        $classname = str_replace('%%', $alignname, $this->table_align_class_tmpl);
1237
+        return " class=\"$classname\"";
1238
+    }
1239
+
1240
+    /**
1241
+     * Calback for processing tables
1242
+     * @param  array $matches
1243
+     * @return string
1244
+     */
1245
+    protected function _doTable_callback($matches) {
1246
+        $head		= $matches[1];
1247
+        $underline	= $matches[2];
1248
+        $content	= $matches[3];
1249
+
1250
+        // Remove any tailing pipes for each line.
1251
+        $head		= preg_replace('/[|] *$/m', '', $head);
1252
+        $underline	= preg_replace('/[|] *$/m', '', $underline);
1253
+        $content	= preg_replace('/[|] *$/m', '', $content);
1254
+
1255
+        // Reading alignement from header underline.
1256
+        $separators	= preg_split('/ *[|] */', $underline);
1257
+        foreach ($separators as $n => $s) {
1258
+            if (preg_match('/^ *-+: *$/', $s))
1259
+                $attr[$n] = $this->_doTable_makeAlignAttr('right');
1260
+            else if (preg_match('/^ *:-+: *$/', $s))
1261
+                $attr[$n] = $this->_doTable_makeAlignAttr('center');
1262
+            else if (preg_match('/^ *:-+ *$/', $s))
1263
+                $attr[$n] = $this->_doTable_makeAlignAttr('left');
1264
+            else
1265
+                $attr[$n] = '';
1266
+        }
1267
+
1268
+        // Parsing span elements, including code spans, character escapes,
1269
+        // and inline HTML tags, so that pipes inside those gets ignored.
1270
+        $head		= $this->parseSpan($head);
1271
+        $headers	= preg_split('/ *[|] */', $head);
1272
+        $col_count	= count($headers);
1273
+        $attr       = array_pad($attr, $col_count, '');
1274
+
1275
+        // Write column headers.
1276
+        $text = "<table>\n";
1277
+        $text .= "<thead>\n";
1278
+        $text .= "<tr>\n";
1279
+        foreach ($headers as $n => $header) {
1280
+            $text .= "  <th$attr[$n]>" . $this->runSpanGamut(trim($header)) . "</th>\n";
1281
+        }
1282
+        $text .= "</tr>\n";
1283
+        $text .= "</thead>\n";
1284
+
1285
+        // Split content by row.
1286
+        $rows = explode("\n", trim($content, "\n"));
1287
+
1288
+        $text .= "<tbody>\n";
1289
+        foreach ($rows as $row) {
1290
+            // Parsing span elements, including code spans, character escapes,
1291
+            // and inline HTML tags, so that pipes inside those gets ignored.
1292
+            $row = $this->parseSpan($row);
1293
+
1294
+            // Split row by cell.
1295
+            $row_cells = preg_split('/ *[|] */', $row, $col_count);
1296
+            $row_cells = array_pad($row_cells, $col_count, '');
1297
+
1298
+            $text .= "<tr>\n";
1299
+            foreach ($row_cells as $n => $cell) {
1300
+                $text .= "  <td$attr[$n]>" . $this->runSpanGamut(trim($cell)) . "</td>\n";
1301
+            }
1302
+            $text .= "</tr>\n";
1303
+        }
1304
+        $text .= "</tbody>\n";
1305
+        $text .= "</table>";
1306
+
1307
+        return $this->hashBlock($text) . "\n";
1308
+    }
1309
+
1310
+    /**
1311
+     * Form HTML definition lists.
1312
+     * @param  string $text
1313
+     * @return string
1314
+     */
1315
+    protected function doDefLists($text) {
1316
+        $less_than_tab = $this->tab_width - 1;
1317
+
1318
+        // Re-usable pattern to match any entire dl list:
1319
+        $whole_list_re = '(?>
1320 1320
 			(								# $1 = whole list
1321 1321
 			  (								# $2
1322 1322
 				[ ]{0,' . $less_than_tab . '}
@@ -1343,46 +1343,46 @@  discard block
 block discarded – undo
1343 1343
 			)
1344 1344
 		)'; // mx
1345 1345
 
1346
-		$text = preg_replace_callback('{
1346
+        $text = preg_replace_callback('{
1347 1347
 				(?>\A\n?|(?<=\n\n))
1348 1348
 				' . $whole_list_re . '
1349 1349
 			}mx',
1350
-			array($this, '_doDefLists_callback'), $text);
1351
-
1352
-		return $text;
1353
-	}
1354
-
1355
-	/**
1356
-	 * Callback for processing definition lists
1357
-	 * @param  array $matches
1358
-	 * @return string
1359
-	 */
1360
-	protected function _doDefLists_callback($matches) {
1361
-		// Re-usable patterns to match list item bullets and number markers:
1362
-		$list = $matches[1];
1363
-
1364
-		// Turn double returns into triple returns, so that we can make a
1365
-		// paragraph for the last item in a list, if necessary:
1366
-		$result = trim($this->processDefListItems($list));
1367
-		$result = "<dl>\n" . $result . "\n</dl>";
1368
-		return $this->hashBlock($result) . "\n\n";
1369
-	}
1370
-
1371
-	/**
1372
-	 * Process the contents of a single definition list, splitting it
1373
-	 * into individual term and definition list items.
1374
-	 * @param  string $list_str
1375
-	 * @return string
1376
-	 */
1377
-	protected function processDefListItems($list_str) {
1378
-
1379
-		$less_than_tab = $this->tab_width - 1;
1380
-
1381
-		// Trim trailing blank lines:
1382
-		$list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
1383
-
1384
-		// Process definition terms.
1385
-		$list_str = preg_replace_callback('{
1350
+            array($this, '_doDefLists_callback'), $text);
1351
+
1352
+        return $text;
1353
+    }
1354
+
1355
+    /**
1356
+     * Callback for processing definition lists
1357
+     * @param  array $matches
1358
+     * @return string
1359
+     */
1360
+    protected function _doDefLists_callback($matches) {
1361
+        // Re-usable patterns to match list item bullets and number markers:
1362
+        $list = $matches[1];
1363
+
1364
+        // Turn double returns into triple returns, so that we can make a
1365
+        // paragraph for the last item in a list, if necessary:
1366
+        $result = trim($this->processDefListItems($list));
1367
+        $result = "<dl>\n" . $result . "\n</dl>";
1368
+        return $this->hashBlock($result) . "\n\n";
1369
+    }
1370
+
1371
+    /**
1372
+     * Process the contents of a single definition list, splitting it
1373
+     * into individual term and definition list items.
1374
+     * @param  string $list_str
1375
+     * @return string
1376
+     */
1377
+    protected function processDefListItems($list_str) {
1378
+
1379
+        $less_than_tab = $this->tab_width - 1;
1380
+
1381
+        // Trim trailing blank lines:
1382
+        $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str);
1383
+
1384
+        // Process definition terms.
1385
+        $list_str = preg_replace_callback('{
1386 1386
 			(?>\A\n?|\n\n+)						# leading line
1387 1387
 			(									# definition terms = $1
1388 1388
 				[ ]{0,' . $less_than_tab . '}	# leading whitespace
@@ -1393,10 +1393,10 @@  discard block
 block discarded – undo
1393 1393
 			(?=\n?[ ]{0,3}:[ ])					# lookahead for following line feed
1394 1394
 												#   with a definition mark.
1395 1395
 			}xm',
1396
-			array($this, '_processDefListItems_callback_dt'), $list_str);
1396
+            array($this, '_processDefListItems_callback_dt'), $list_str);
1397 1397
 
1398
-		// Process actual definitions.
1399
-		$list_str = preg_replace_callback('{
1398
+        // Process actual definitions.
1399
+        $list_str = preg_replace_callback('{
1400 1400
 			\n(\n+)?							# leading line = $1
1401 1401
 			(									# marker space = $2
1402 1402
 				[ ]{0,' . $less_than_tab . '}	# whitespace before colon
@@ -1410,63 +1410,63 @@  discard block
 block discarded – undo
1410 1410
 				)
1411 1411
 			)
1412 1412
 			}xm',
1413
-			array($this, '_processDefListItems_callback_dd'), $list_str);
1414
-
1415
-		return $list_str;
1416
-	}
1417
-
1418
-	/**
1419
-	 * Callback for <dt> elements in definition lists
1420
-	 * @param  array $matches
1421
-	 * @return string
1422
-	 */
1423
-	protected function _processDefListItems_callback_dt($matches) {
1424
-		$terms = explode("\n", trim($matches[1]));
1425
-		$text = '';
1426
-		foreach ($terms as $term) {
1427
-			$term = $this->runSpanGamut(trim($term));
1428
-			$text .= "\n<dt>" . $term . "</dt>";
1429
-		}
1430
-		return $text . "\n";
1431
-	}
1432
-
1433
-	/**
1434
-	 * Callback for <dd> elements in definition lists
1435
-	 * @param  array $matches
1436
-	 * @return string
1437
-	 */
1438
-	protected function _processDefListItems_callback_dd($matches) {
1439
-		$leading_line	= $matches[1];
1440
-		$marker_space	= $matches[2];
1441
-		$def			= $matches[3];
1442
-
1443
-		if ($leading_line || preg_match('/\n{2,}/', $def)) {
1444
-			// Replace marker with the appropriate whitespace indentation
1445
-			$def = str_repeat(' ', strlen($marker_space)) . $def;
1446
-			$def = $this->runBlockGamut($this->outdent($def . "\n\n"));
1447
-			$def = "\n". $def ."\n";
1448
-		}
1449
-		else {
1450
-			$def = rtrim($def);
1451
-			$def = $this->runSpanGamut($this->outdent($def));
1452
-		}
1453
-
1454
-		return "\n<dd>" . $def . "</dd>\n";
1455
-	}
1456
-
1457
-	/**
1458
-	 * Adding the fenced code block syntax to regular Markdown:
1459
-	 *
1460
-	 * ~~~
1461
-	 * Code block
1462
-	 * ~~~
1463
-	 *
1464
-	 * @param  string $text
1465
-	 * @return string
1466
-	 */
1467
-	protected function doFencedCodeBlocks($text) {
1468
-
1469
-		$text = preg_replace_callback('{
1413
+            array($this, '_processDefListItems_callback_dd'), $list_str);
1414
+
1415
+        return $list_str;
1416
+    }
1417
+
1418
+    /**
1419
+     * Callback for <dt> elements in definition lists
1420
+     * @param  array $matches
1421
+     * @return string
1422
+     */
1423
+    protected function _processDefListItems_callback_dt($matches) {
1424
+        $terms = explode("\n", trim($matches[1]));
1425
+        $text = '';
1426
+        foreach ($terms as $term) {
1427
+            $term = $this->runSpanGamut(trim($term));
1428
+            $text .= "\n<dt>" . $term . "</dt>";
1429
+        }
1430
+        return $text . "\n";
1431
+    }
1432
+
1433
+    /**
1434
+     * Callback for <dd> elements in definition lists
1435
+     * @param  array $matches
1436
+     * @return string
1437
+     */
1438
+    protected function _processDefListItems_callback_dd($matches) {
1439
+        $leading_line	= $matches[1];
1440
+        $marker_space	= $matches[2];
1441
+        $def			= $matches[3];
1442
+
1443
+        if ($leading_line || preg_match('/\n{2,}/', $def)) {
1444
+            // Replace marker with the appropriate whitespace indentation
1445
+            $def = str_repeat(' ', strlen($marker_space)) . $def;
1446
+            $def = $this->runBlockGamut($this->outdent($def . "\n\n"));
1447
+            $def = "\n". $def ."\n";
1448
+        }
1449
+        else {
1450
+            $def = rtrim($def);
1451
+            $def = $this->runSpanGamut($this->outdent($def));
1452
+        }
1453
+
1454
+        return "\n<dd>" . $def . "</dd>\n";
1455
+    }
1456
+
1457
+    /**
1458
+     * Adding the fenced code block syntax to regular Markdown:
1459
+     *
1460
+     * ~~~
1461
+     * Code block
1462
+     * ~~~
1463
+     *
1464
+     * @param  string $text
1465
+     * @return string
1466
+     */
1467
+    protected function doFencedCodeBlocks($text) {
1468
+
1469
+        $text = preg_replace_callback('{
1470 1470
 				(?:\n|\A)
1471 1471
 				# 1: Opening marker
1472 1472
 				(
@@ -1493,123 +1493,123 @@  discard block
 block discarded – undo
1493 1493
 				# Closing marker.
1494 1494
 				\1 [ ]* (?= \n )
1495 1495
 			}xm',
1496
-			array($this, '_doFencedCodeBlocks_callback'), $text);
1497
-
1498
-		return $text;
1499
-	}
1500
-
1501
-	/**
1502
-	 * Callback to process fenced code blocks
1503
-	 * @param  array $matches
1504
-	 * @return string
1505
-	 */
1506
-	protected function _doFencedCodeBlocks_callback($matches) {
1507
-		$classname =& $matches[2];
1508
-		$attrs     =& $matches[3];
1509
-		$codeblock = $matches[4];
1510
-
1511
-		if ($this->code_block_content_func) {
1512
-			$codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname);
1513
-		} else {
1514
-			$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
1515
-		}
1516
-
1517
-		$codeblock = preg_replace_callback('/^\n+/',
1518
-			array($this, '_doFencedCodeBlocks_newlines'), $codeblock);
1519
-
1520
-		$classes = array();
1521
-		if ($classname !== "") {
1522
-			if ($classname[0] === '.') {
1523
-				$classname = substr($classname, 1);
1524
-			}
1525
-			$classes[] = $this->code_class_prefix . $classname;
1526
-		}
1527
-		$attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes);
1528
-		$pre_attr_str  = $this->code_attr_on_pre ? $attr_str : '';
1529
-		$code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
1530
-		$codeblock  = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
1531
-
1532
-		return "\n\n".$this->hashBlock($codeblock)."\n\n";
1533
-	}
1534
-
1535
-	/**
1536
-	 * Replace new lines in fenced code blocks
1537
-	 * @param  array $matches
1538
-	 * @return string
1539
-	 */
1540
-	protected function _doFencedCodeBlocks_newlines($matches) {
1541
-		return str_repeat("<br$this->empty_element_suffix",
1542
-			strlen($matches[0]));
1543
-	}
1544
-
1545
-	/**
1546
-	 * Redefining emphasis markers so that emphasis by underscore does not
1547
-	 * work in the middle of a word.
1548
-	 * @var array
1549
-	 */
1550
-	protected $em_relist = array(
1551
-		''  => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)',
1552
-		'*' => '(?<![\s*])\*(?!\*)',
1553
-		'_' => '(?<![\s_])_(?![a-zA-Z0-9_])',
1554
-	);
1555
-	protected $strong_relist = array(
1556
-		''   => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)',
1557
-		'**' => '(?<![\s*])\*\*(?!\*)',
1558
-		'__' => '(?<![\s_])__(?![a-zA-Z0-9_])',
1559
-	);
1560
-	protected $em_strong_relist = array(
1561
-		''    => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)',
1562
-		'***' => '(?<![\s*])\*\*\*(?!\*)',
1563
-		'___' => '(?<![\s_])___(?![a-zA-Z0-9_])',
1564
-	);
1565
-
1566
-	/**
1567
-	 * Parse text into paragraphs
1568
-	 * @param  string $text String to process in paragraphs
1569
-	 * @param  boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags
1570
-	 * @return string       HTML output
1571
-	 */
1572
-	protected function formParagraphs($text, $wrap_in_p = true) {
1573
-		// Strip leading and trailing lines:
1574
-		$text = preg_replace('/\A\n+|\n+\z/', '', $text);
1575
-
1576
-		$grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
1577
-
1578
-		// Wrap <p> tags and unhashify HTML blocks
1579
-		foreach ($grafs as $key => $value) {
1580
-			$value = trim($this->runSpanGamut($value));
1581
-
1582
-			// Check if this should be enclosed in a paragraph.
1583
-			// Clean tag hashes & block tag hashes are left alone.
1584
-			$is_p = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
1585
-
1586
-			if ($is_p) {
1587
-				$value = "<p>$value</p>";
1588
-			}
1589
-			$grafs[$key] = $value;
1590
-		}
1591
-
1592
-		// Join grafs in one text, then unhash HTML tags.
1593
-		$text = implode("\n\n", $grafs);
1594
-
1595
-		// Finish by removing any tag hashes still present in $text.
1596
-		$text = $this->unhash($text);
1597
-
1598
-		return $text;
1599
-	}
1600
-
1601
-
1602
-	/**
1603
-	 * Footnotes - Strips link definitions from text, stores the URLs and
1604
-	 * titles in hash references.
1605
-	 * @param  string $text
1606
-	 * @return string
1607
-	 */
1608
-	protected function stripFootnotes($text) {
1609
-		$less_than_tab = $this->tab_width - 1;
1610
-
1611
-		// Link defs are in the form: [^id]: url "optional title"
1612
-		$text = preg_replace_callback('{
1496
+            array($this, '_doFencedCodeBlocks_callback'), $text);
1497
+
1498
+        return $text;
1499
+    }
1500
+
1501
+    /**
1502
+     * Callback to process fenced code blocks
1503
+     * @param  array $matches
1504
+     * @return string
1505
+     */
1506
+    protected function _doFencedCodeBlocks_callback($matches) {
1507
+        $classname =& $matches[2];
1508
+        $attrs     =& $matches[3];
1509
+        $codeblock = $matches[4];
1510
+
1511
+        if ($this->code_block_content_func) {
1512
+            $codeblock = call_user_func($this->code_block_content_func, $codeblock, $classname);
1513
+        } else {
1514
+            $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
1515
+        }
1516
+
1517
+        $codeblock = preg_replace_callback('/^\n+/',
1518
+            array($this, '_doFencedCodeBlocks_newlines'), $codeblock);
1519
+
1520
+        $classes = array();
1521
+        if ($classname !== "") {
1522
+            if ($classname[0] === '.') {
1523
+                $classname = substr($classname, 1);
1524
+            }
1525
+            $classes[] = $this->code_class_prefix . $classname;
1526
+        }
1527
+        $attr_str = $this->doExtraAttributes($this->code_attr_on_pre ? "pre" : "code", $attrs, null, $classes);
1528
+        $pre_attr_str  = $this->code_attr_on_pre ? $attr_str : '';
1529
+        $code_attr_str = $this->code_attr_on_pre ? '' : $attr_str;
1530
+        $codeblock  = "<pre$pre_attr_str><code$code_attr_str>$codeblock</code></pre>";
1531
+
1532
+        return "\n\n".$this->hashBlock($codeblock)."\n\n";
1533
+    }
1534
+
1535
+    /**
1536
+     * Replace new lines in fenced code blocks
1537
+     * @param  array $matches
1538
+     * @return string
1539
+     */
1540
+    protected function _doFencedCodeBlocks_newlines($matches) {
1541
+        return str_repeat("<br$this->empty_element_suffix",
1542
+            strlen($matches[0]));
1543
+    }
1544
+
1545
+    /**
1546
+     * Redefining emphasis markers so that emphasis by underscore does not
1547
+     * work in the middle of a word.
1548
+     * @var array
1549
+     */
1550
+    protected $em_relist = array(
1551
+        ''  => '(?:(?<!\*)\*(?!\*)|(?<![a-zA-Z0-9_])_(?!_))(?![\.,:;]?\s)',
1552
+        '*' => '(?<![\s*])\*(?!\*)',
1553
+        '_' => '(?<![\s_])_(?![a-zA-Z0-9_])',
1554
+    );
1555
+    protected $strong_relist = array(
1556
+        ''   => '(?:(?<!\*)\*\*(?!\*)|(?<![a-zA-Z0-9_])__(?!_))(?![\.,:;]?\s)',
1557
+        '**' => '(?<![\s*])\*\*(?!\*)',
1558
+        '__' => '(?<![\s_])__(?![a-zA-Z0-9_])',
1559
+    );
1560
+    protected $em_strong_relist = array(
1561
+        ''    => '(?:(?<!\*)\*\*\*(?!\*)|(?<![a-zA-Z0-9_])___(?!_))(?![\.,:;]?\s)',
1562
+        '***' => '(?<![\s*])\*\*\*(?!\*)',
1563
+        '___' => '(?<![\s_])___(?![a-zA-Z0-9_])',
1564
+    );
1565
+
1566
+    /**
1567
+     * Parse text into paragraphs
1568
+     * @param  string $text String to process in paragraphs
1569
+     * @param  boolean $wrap_in_p Whether paragraphs should be wrapped in <p> tags
1570
+     * @return string       HTML output
1571
+     */
1572
+    protected function formParagraphs($text, $wrap_in_p = true) {
1573
+        // Strip leading and trailing lines:
1574
+        $text = preg_replace('/\A\n+|\n+\z/', '', $text);
1575
+
1576
+        $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
1577
+
1578
+        // Wrap <p> tags and unhashify HTML blocks
1579
+        foreach ($grafs as $key => $value) {
1580
+            $value = trim($this->runSpanGamut($value));
1581
+
1582
+            // Check if this should be enclosed in a paragraph.
1583
+            // Clean tag hashes & block tag hashes are left alone.
1584
+            $is_p = $wrap_in_p && !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value);
1585
+
1586
+            if ($is_p) {
1587
+                $value = "<p>$value</p>";
1588
+            }
1589
+            $grafs[$key] = $value;
1590
+        }
1591
+
1592
+        // Join grafs in one text, then unhash HTML tags.
1593
+        $text = implode("\n\n", $grafs);
1594
+
1595
+        // Finish by removing any tag hashes still present in $text.
1596
+        $text = $this->unhash($text);
1597
+
1598
+        return $text;
1599
+    }
1600
+
1601
+
1602
+    /**
1603
+     * Footnotes - Strips link definitions from text, stores the URLs and
1604
+     * titles in hash references.
1605
+     * @param  string $text
1606
+     * @return string
1607
+     */
1608
+    protected function stripFootnotes($text) {
1609
+        $less_than_tab = $this->tab_width - 1;
1610
+
1611
+        // Link defs are in the form: [^id]: url "optional title"
1612
+        $text = preg_replace_callback('{
1613 1613
 			^[ ]{0,' . $less_than_tab . '}\[\^(.+?)\][ ]?:	# note_id = $1
1614 1614
 			  [ ]*
1615 1615
 			  \n?					# maybe *one* newline
@@ -1624,270 +1624,270 @@  discard block
 block discarded – undo
1624 1624
 				)*
1625 1625
 			)
1626 1626
 			}xm',
1627
-			array($this, '_stripFootnotes_callback'),
1628
-			$text);
1629
-		return $text;
1630
-	}
1631
-
1632
-	/**
1633
-	 * Callback for stripping footnotes
1634
-	 * @param  array $matches
1635
-	 * @return string
1636
-	 */
1637
-	protected function _stripFootnotes_callback($matches) {
1638
-		$note_id = $this->fn_id_prefix . $matches[1];
1639
-		$this->footnotes[$note_id] = $this->outdent($matches[2]);
1640
-		return ''; // String that will replace the block
1641
-	}
1642
-
1643
-	/**
1644
-	 * Replace footnote references in $text [^id] with a special text-token
1645
-	 * which will be replaced by the actual footnote marker in appendFootnotes.
1646
-	 * @param  string $text
1647
-	 * @return string
1648
-	 */
1649
-	protected function doFootnotes($text) {
1650
-		if (!$this->in_anchor) {
1651
-			$text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
1652
-		}
1653
-		return $text;
1654
-	}
1655
-
1656
-	/**
1657
-	 * Append footnote list to text
1658
-	 * @param  string $text
1659
-	 * @return string
1660
-	 */
1661
-	protected function appendFootnotes($text) {
1662
-		$text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1663
-			array($this, '_appendFootnotes_callback'), $text);
1664
-
1665
-		if ( ! empty( $this->footnotes_ordered ) ) {
1666
-			$this->_doFootnotes();
1667
-			if ( ! $this->omit_footnotes ) {
1668
-				$text .= "\n\n";
1669
-				$text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n";
1670
-				$text .= "<hr" . $this->empty_element_suffix . "\n";
1671
-				$text .= $this->footnotes_assembled;
1672
-				$text .= "</div>";
1673
-			}
1674
-		}
1675
-		return $text;
1676
-	}
1677
-
1678
-
1679
-	/**
1680
-	 * Generates the HTML for footnotes.  Called by appendFootnotes, even if
1681
-	 * footnotes are not being appended.
1682
-	 * @return void
1683
-	 */
1684
-	protected function _doFootnotes() {
1685
-		$attr = array();
1686
-		if ($this->fn_backlink_class !== "") {
1687
-			$class = $this->fn_backlink_class;
1688
-			$class = $this->encodeAttribute($class);
1689
-			$attr['class'] = " class=\"$class\"";
1690
-		}
1691
-		$attr['role'] = " role=\"doc-backlink\"";
1692
-		$num = 0;
1693
-
1694
-		$text = "<ol>\n\n";
1695
-		while (!empty($this->footnotes_ordered)) {
1696
-			$footnote = reset($this->footnotes_ordered);
1697
-			$note_id = key($this->footnotes_ordered);
1698
-			unset($this->footnotes_ordered[$note_id]);
1699
-			$ref_count = $this->footnotes_ref_count[$note_id];
1700
-			unset($this->footnotes_ref_count[$note_id]);
1701
-			unset($this->footnotes[$note_id]);
1702
-
1703
-			$footnote .= "\n"; // Need to append newline before parsing.
1704
-			$footnote = $this->runBlockGamut("$footnote\n");
1705
-			$footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1706
-				array($this, '_appendFootnotes_callback'), $footnote);
1707
-
1708
-			$num++;
1709
-			$note_id = $this->encodeAttribute($note_id);
1710
-
1711
-			// Prepare backlink, multiple backlinks if multiple references
1712
-			// Do not create empty backlinks if the html is blank
1713
-			$backlink = "";
1714
-			if (!empty($this->fn_backlink_html)) {
1715
-				for ($ref_num = 1; $ref_num <= $ref_count; ++$ref_num) {
1716
-					if (!empty($this->fn_backlink_title)) {
1717
-						$attr['title'] = ' title="' . $this->encodeAttribute($this->fn_backlink_title) . '"';
1718
-					}
1719
-					if (!empty($this->fn_backlink_label)) {
1720
-						$attr['label'] = ' aria-label="' . $this->encodeAttribute($this->fn_backlink_label) . '"';
1721
-					}
1722
-					$parsed_attr = $this->parseFootnotePlaceholders(
1723
-						implode('', $attr),
1724
-						$num,
1725
-						$ref_num
1726
-					);
1727
-					$backlink_text = $this->parseFootnotePlaceholders(
1728
-						$this->fn_backlink_html,
1729
-						$num,
1730
-						$ref_num
1731
-					);
1732
-					$ref_count_mark = $ref_num > 1 ? $ref_num : '';
1733
-					$backlink .= " <a href=\"#fnref$ref_count_mark:$note_id\"$parsed_attr>$backlink_text</a>";
1734
-				}
1735
-				$backlink = trim($backlink);
1736
-			}
1737
-
1738
-			// Add backlink to last paragraph; create new paragraph if needed.
1739
-			if (!empty($backlink)) {
1740
-				if (preg_match('{</p>$}', $footnote)) {
1741
-					$footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
1742
-				} else {
1743
-					$footnote .= "\n\n<p>$backlink</p>";
1744
-				}
1745
-			}
1746
-
1747
-			$text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n";
1748
-			$text .= $footnote . "\n";
1749
-			$text .= "</li>\n\n";
1750
-		}
1751
-		$text .= "</ol>\n";
1752
-
1753
-		$this->footnotes_assembled = $text;
1754
-	}
1755
-
1756
-	/**
1757
-	 * Callback for appending footnotes
1758
-	 * @param  array $matches
1759
-	 * @return string
1760
-	 */
1761
-	protected function _appendFootnotes_callback($matches) {
1762
-		$node_id = $this->fn_id_prefix . $matches[1];
1763
-
1764
-		// Create footnote marker only if it has a corresponding footnote *and*
1765
-		// the footnote hasn't been used by another marker.
1766
-		if (isset($this->footnotes[$node_id])) {
1767
-			$num =& $this->footnotes_numbers[$node_id];
1768
-			if (!isset($num)) {
1769
-				// Transfer footnote content to the ordered list and give it its
1770
-				// number
1771
-				$this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
1772
-				$this->footnotes_ref_count[$node_id] = 1;
1773
-				$num = $this->footnote_counter++;
1774
-				$ref_count_mark = '';
1775
-			} else {
1776
-				$ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
1777
-			}
1778
-
1779
-			$attr = "";
1780
-			if ($this->fn_link_class !== "") {
1781
-				$class = $this->fn_link_class;
1782
-				$class = $this->encodeAttribute($class);
1783
-				$attr .= " class=\"$class\"";
1784
-			}
1785
-			if ($this->fn_link_title !== "") {
1786
-				$title = $this->fn_link_title;
1787
-				$title = $this->encodeAttribute($title);
1788
-				$attr .= " title=\"$title\"";
1789
-			}
1790
-			$attr .= " role=\"doc-noteref\"";
1791
-
1792
-			$attr = str_replace("%%", $num, $attr);
1793
-			$node_id = $this->encodeAttribute($node_id);
1794
-
1795
-			return
1796
-				"<sup id=\"fnref$ref_count_mark:$node_id\">".
1797
-				"<a href=\"#fn:$node_id\"$attr>$num</a>".
1798
-				"</sup>";
1799
-		}
1800
-
1801
-		return "[^" . $matches[1] . "]";
1802
-	}
1803
-
1804
-	/**
1805
-	 * Build footnote label by evaluating any placeholders.
1806
-	 * - ^^  footnote number
1807
-	 * - %%  footnote reference number (Nth reference to footnote number)
1808
-	 * @param  string $label
1809
-	 * @param  int    $footnote_number
1810
-	 * @param  int    $reference_number
1811
-	 * @return string
1812
-	 */
1813
-	protected function parseFootnotePlaceholders($label, $footnote_number, $reference_number) {
1814
-		return str_replace(
1815
-			array('^^', '%%'),
1816
-			array($footnote_number, $reference_number),
1817
-			$label
1818
-		);
1819
-	}
1820
-
1821
-
1822
-	/**
1823
-	 * Abbreviations - strips abbreviations from text, stores titles in hash
1824
-	 * references.
1825
-	 * @param  string $text
1826
-	 * @return string
1827
-	 */
1828
-	protected function stripAbbreviations($text) {
1829
-		$less_than_tab = $this->tab_width - 1;
1830
-
1831
-		// Link defs are in the form: [id]*: url "optional title"
1832
-		$text = preg_replace_callback('{
1627
+            array($this, '_stripFootnotes_callback'),
1628
+            $text);
1629
+        return $text;
1630
+    }
1631
+
1632
+    /**
1633
+     * Callback for stripping footnotes
1634
+     * @param  array $matches
1635
+     * @return string
1636
+     */
1637
+    protected function _stripFootnotes_callback($matches) {
1638
+        $note_id = $this->fn_id_prefix . $matches[1];
1639
+        $this->footnotes[$note_id] = $this->outdent($matches[2]);
1640
+        return ''; // String that will replace the block
1641
+    }
1642
+
1643
+    /**
1644
+     * Replace footnote references in $text [^id] with a special text-token
1645
+     * which will be replaced by the actual footnote marker in appendFootnotes.
1646
+     * @param  string $text
1647
+     * @return string
1648
+     */
1649
+    protected function doFootnotes($text) {
1650
+        if (!$this->in_anchor) {
1651
+            $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text);
1652
+        }
1653
+        return $text;
1654
+    }
1655
+
1656
+    /**
1657
+     * Append footnote list to text
1658
+     * @param  string $text
1659
+     * @return string
1660
+     */
1661
+    protected function appendFootnotes($text) {
1662
+        $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1663
+            array($this, '_appendFootnotes_callback'), $text);
1664
+
1665
+        if ( ! empty( $this->footnotes_ordered ) ) {
1666
+            $this->_doFootnotes();
1667
+            if ( ! $this->omit_footnotes ) {
1668
+                $text .= "\n\n";
1669
+                $text .= "<div class=\"footnotes\" role=\"doc-endnotes\">\n";
1670
+                $text .= "<hr" . $this->empty_element_suffix . "\n";
1671
+                $text .= $this->footnotes_assembled;
1672
+                $text .= "</div>";
1673
+            }
1674
+        }
1675
+        return $text;
1676
+    }
1677
+
1678
+
1679
+    /**
1680
+     * Generates the HTML for footnotes.  Called by appendFootnotes, even if
1681
+     * footnotes are not being appended.
1682
+     * @return void
1683
+     */
1684
+    protected function _doFootnotes() {
1685
+        $attr = array();
1686
+        if ($this->fn_backlink_class !== "") {
1687
+            $class = $this->fn_backlink_class;
1688
+            $class = $this->encodeAttribute($class);
1689
+            $attr['class'] = " class=\"$class\"";
1690
+        }
1691
+        $attr['role'] = " role=\"doc-backlink\"";
1692
+        $num = 0;
1693
+
1694
+        $text = "<ol>\n\n";
1695
+        while (!empty($this->footnotes_ordered)) {
1696
+            $footnote = reset($this->footnotes_ordered);
1697
+            $note_id = key($this->footnotes_ordered);
1698
+            unset($this->footnotes_ordered[$note_id]);
1699
+            $ref_count = $this->footnotes_ref_count[$note_id];
1700
+            unset($this->footnotes_ref_count[$note_id]);
1701
+            unset($this->footnotes[$note_id]);
1702
+
1703
+            $footnote .= "\n"; // Need to append newline before parsing.
1704
+            $footnote = $this->runBlockGamut("$footnote\n");
1705
+            $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}',
1706
+                array($this, '_appendFootnotes_callback'), $footnote);
1707
+
1708
+            $num++;
1709
+            $note_id = $this->encodeAttribute($note_id);
1710
+
1711
+            // Prepare backlink, multiple backlinks if multiple references
1712
+            // Do not create empty backlinks if the html is blank
1713
+            $backlink = "";
1714
+            if (!empty($this->fn_backlink_html)) {
1715
+                for ($ref_num = 1; $ref_num <= $ref_count; ++$ref_num) {
1716
+                    if (!empty($this->fn_backlink_title)) {
1717
+                        $attr['title'] = ' title="' . $this->encodeAttribute($this->fn_backlink_title) . '"';
1718
+                    }
1719
+                    if (!empty($this->fn_backlink_label)) {
1720
+                        $attr['label'] = ' aria-label="' . $this->encodeAttribute($this->fn_backlink_label) . '"';
1721
+                    }
1722
+                    $parsed_attr = $this->parseFootnotePlaceholders(
1723
+                        implode('', $attr),
1724
+                        $num,
1725
+                        $ref_num
1726
+                    );
1727
+                    $backlink_text = $this->parseFootnotePlaceholders(
1728
+                        $this->fn_backlink_html,
1729
+                        $num,
1730
+                        $ref_num
1731
+                    );
1732
+                    $ref_count_mark = $ref_num > 1 ? $ref_num : '';
1733
+                    $backlink .= " <a href=\"#fnref$ref_count_mark:$note_id\"$parsed_attr>$backlink_text</a>";
1734
+                }
1735
+                $backlink = trim($backlink);
1736
+            }
1737
+
1738
+            // Add backlink to last paragraph; create new paragraph if needed.
1739
+            if (!empty($backlink)) {
1740
+                if (preg_match('{</p>$}', $footnote)) {
1741
+                    $footnote = substr($footnote, 0, -4) . "&#160;$backlink</p>";
1742
+                } else {
1743
+                    $footnote .= "\n\n<p>$backlink</p>";
1744
+                }
1745
+            }
1746
+
1747
+            $text .= "<li id=\"fn:$note_id\" role=\"doc-endnote\">\n";
1748
+            $text .= $footnote . "\n";
1749
+            $text .= "</li>\n\n";
1750
+        }
1751
+        $text .= "</ol>\n";
1752
+
1753
+        $this->footnotes_assembled = $text;
1754
+    }
1755
+
1756
+    /**
1757
+     * Callback for appending footnotes
1758
+     * @param  array $matches
1759
+     * @return string
1760
+     */
1761
+    protected function _appendFootnotes_callback($matches) {
1762
+        $node_id = $this->fn_id_prefix . $matches[1];
1763
+
1764
+        // Create footnote marker only if it has a corresponding footnote *and*
1765
+        // the footnote hasn't been used by another marker.
1766
+        if (isset($this->footnotes[$node_id])) {
1767
+            $num =& $this->footnotes_numbers[$node_id];
1768
+            if (!isset($num)) {
1769
+                // Transfer footnote content to the ordered list and give it its
1770
+                // number
1771
+                $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id];
1772
+                $this->footnotes_ref_count[$node_id] = 1;
1773
+                $num = $this->footnote_counter++;
1774
+                $ref_count_mark = '';
1775
+            } else {
1776
+                $ref_count_mark = $this->footnotes_ref_count[$node_id] += 1;
1777
+            }
1778
+
1779
+            $attr = "";
1780
+            if ($this->fn_link_class !== "") {
1781
+                $class = $this->fn_link_class;
1782
+                $class = $this->encodeAttribute($class);
1783
+                $attr .= " class=\"$class\"";
1784
+            }
1785
+            if ($this->fn_link_title !== "") {
1786
+                $title = $this->fn_link_title;
1787
+                $title = $this->encodeAttribute($title);
1788
+                $attr .= " title=\"$title\"";
1789
+            }
1790
+            $attr .= " role=\"doc-noteref\"";
1791
+
1792
+            $attr = str_replace("%%", $num, $attr);
1793
+            $node_id = $this->encodeAttribute($node_id);
1794
+
1795
+            return
1796
+                "<sup id=\"fnref$ref_count_mark:$node_id\">".
1797
+                "<a href=\"#fn:$node_id\"$attr>$num</a>".
1798
+                "</sup>";
1799
+        }
1800
+
1801
+        return "[^" . $matches[1] . "]";
1802
+    }
1803
+
1804
+    /**
1805
+     * Build footnote label by evaluating any placeholders.
1806
+     * - ^^  footnote number
1807
+     * - %%  footnote reference number (Nth reference to footnote number)
1808
+     * @param  string $label
1809
+     * @param  int    $footnote_number
1810
+     * @param  int    $reference_number
1811
+     * @return string
1812
+     */
1813
+    protected function parseFootnotePlaceholders($label, $footnote_number, $reference_number) {
1814
+        return str_replace(
1815
+            array('^^', '%%'),
1816
+            array($footnote_number, $reference_number),
1817
+            $label
1818
+        );
1819
+    }
1820
+
1821
+
1822
+    /**
1823
+     * Abbreviations - strips abbreviations from text, stores titles in hash
1824
+     * references.
1825
+     * @param  string $text
1826
+     * @return string
1827
+     */
1828
+    protected function stripAbbreviations($text) {
1829
+        $less_than_tab = $this->tab_width - 1;
1830
+
1831
+        // Link defs are in the form: [id]*: url "optional title"
1832
+        $text = preg_replace_callback('{
1833 1833
 			^[ ]{0,' . $less_than_tab . '}\*\[(.+?)\][ ]?:	# abbr_id = $1
1834 1834
 			(.*)					# text = $2 (no blank lines allowed)
1835 1835
 			}xm',
1836
-			array($this, '_stripAbbreviations_callback'),
1837
-			$text);
1838
-		return $text;
1839
-	}
1840
-
1841
-	/**
1842
-	 * Callback for stripping abbreviations
1843
-	 * @param  array $matches
1844
-	 * @return string
1845
-	 */
1846
-	protected function _stripAbbreviations_callback($matches) {
1847
-		$abbr_word = $matches[1];
1848
-		$abbr_desc = $matches[2];
1849
-		if ($this->abbr_word_re) {
1850
-			$this->abbr_word_re .= '|';
1851
-		}
1852
-		$this->abbr_word_re .= preg_quote($abbr_word);
1853
-		$this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
1854
-		return ''; // String that will replace the block
1855
-	}
1856
-
1857
-	/**
1858
-	 * Find defined abbreviations in text and wrap them in <abbr> elements.
1859
-	 * @param  string $text
1860
-	 * @return string
1861
-	 */
1862
-	protected function doAbbreviations($text) {
1863
-		if ($this->abbr_word_re) {
1864
-			// cannot use the /x modifier because abbr_word_re may
1865
-			// contain significant spaces:
1866
-			$text = preg_replace_callback('{' .
1867
-				'(?<![\w\x1A])' .
1868
-				'(?:' . $this->abbr_word_re . ')' .
1869
-				'(?![\w\x1A])' .
1870
-				'}',
1871
-				array($this, '_doAbbreviations_callback'), $text);
1872
-		}
1873
-		return $text;
1874
-	}
1875
-
1876
-	/**
1877
-	 * Callback for processing abbreviations
1878
-	 * @param  array $matches
1879
-	 * @return string
1880
-	 */
1881
-	protected function _doAbbreviations_callback($matches) {
1882
-		$abbr = $matches[0];
1883
-		if (isset($this->abbr_desciptions[$abbr])) {
1884
-			$desc = $this->abbr_desciptions[$abbr];
1885
-			if (empty($desc)) {
1886
-				return $this->hashPart("<abbr>$abbr</abbr>");
1887
-			}
1888
-			$desc = $this->encodeAttribute($desc);
1889
-			return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
1890
-		}
1891
-		return $matches[0];
1892
-	}
1836
+            array($this, '_stripAbbreviations_callback'),
1837
+            $text);
1838
+        return $text;
1839
+    }
1840
+
1841
+    /**
1842
+     * Callback for stripping abbreviations
1843
+     * @param  array $matches
1844
+     * @return string
1845
+     */
1846
+    protected function _stripAbbreviations_callback($matches) {
1847
+        $abbr_word = $matches[1];
1848
+        $abbr_desc = $matches[2];
1849
+        if ($this->abbr_word_re) {
1850
+            $this->abbr_word_re .= '|';
1851
+        }
1852
+        $this->abbr_word_re .= preg_quote($abbr_word);
1853
+        $this->abbr_desciptions[$abbr_word] = trim($abbr_desc);
1854
+        return ''; // String that will replace the block
1855
+    }
1856
+
1857
+    /**
1858
+     * Find defined abbreviations in text and wrap them in <abbr> elements.
1859
+     * @param  string $text
1860
+     * @return string
1861
+     */
1862
+    protected function doAbbreviations($text) {
1863
+        if ($this->abbr_word_re) {
1864
+            // cannot use the /x modifier because abbr_word_re may
1865
+            // contain significant spaces:
1866
+            $text = preg_replace_callback('{' .
1867
+                '(?<![\w\x1A])' .
1868
+                '(?:' . $this->abbr_word_re . ')' .
1869
+                '(?![\w\x1A])' .
1870
+                '}',
1871
+                array($this, '_doAbbreviations_callback'), $text);
1872
+        }
1873
+        return $text;
1874
+    }
1875
+
1876
+    /**
1877
+     * Callback for processing abbreviations
1878
+     * @param  array $matches
1879
+     * @return string
1880
+     */
1881
+    protected function _doAbbreviations_callback($matches) {
1882
+        $abbr = $matches[0];
1883
+        if (isset($this->abbr_desciptions[$abbr])) {
1884
+            $desc = $this->abbr_desciptions[$abbr];
1885
+            if (empty($desc)) {
1886
+                return $this->hashPart("<abbr>$abbr</abbr>");
1887
+            }
1888
+            $desc = $this->encodeAttribute($desc);
1889
+            return $this->hashPart("<abbr title=\"$desc\">$abbr</abbr>");
1890
+        }
1891
+        return $matches[0];
1892
+    }
1893 1893
 }
Please login to merge, or discard this patch.
a/vendor/michelf/php-markdown/Michelf/MarkdownInterface.php 1 patch
Indentation   +20 added lines, -20 removed lines patch added patch discarded remove patch
@@ -14,25 +14,25 @@
 block discarded – undo
14 14
  * Markdown Parser Interface
15 15
  */
16 16
 interface MarkdownInterface {
17
-	/**
18
-	 * Initialize the parser and return the result of its transform method.
19
-	 * This will work fine for derived classes too.
20
-	 *
21
-	 * @api
22
-	 *
23
-	 * @param  string $text
24
-	 * @return string
25
-	 */
26
-	public static function defaultTransform($text);
17
+    /**
18
+     * Initialize the parser and return the result of its transform method.
19
+     * This will work fine for derived classes too.
20
+     *
21
+     * @api
22
+     *
23
+     * @param  string $text
24
+     * @return string
25
+     */
26
+    public static function defaultTransform($text);
27 27
 
28
-	/**
29
-	 * Main function. Performs some preprocessing on the input text
30
-	 * and pass it through the document gamut.
31
-	 *
32
-	 * @api
33
-	 *
34
-	 * @param  string $text
35
-	 * @return string
36
-	 */
37
-	public function transform($text);
28
+    /**
29
+     * Main function. Performs some preprocessing on the input text
30
+     * and pass it through the document gamut.
31
+     *
32
+     * @api
33
+     *
34
+     * @param  string $text
35
+     * @return string
36
+     */
37
+    public function transform($text);
38 38
 }
Please login to merge, or discard this patch.
a/vendor/mos/cimage/CImage.php 1 patch
Indentation   +20 added lines, -20 removed lines patch added patch discarded remove patch
@@ -223,8 +223,8 @@  discard block
 block discarded – undo
223 223
     /**
224 224
      * Path to command to optimize jpeg images, for example jpegtran or null.
225 225
      */
226
-     private $jpegOptimize;
227
-     private $jpegOptimizeCmd;
226
+        private $jpegOptimize;
227
+        private $jpegOptimizeCmd;
228 228
 
229 229
 
230 230
 
@@ -395,8 +395,8 @@  discard block
 block discarded – undo
395 395
 
396 396
 
397 397
     /**
398
-    * Disable the fasttrackCacke to start with, inject an object to enable it.
399
-    */
398
+     * Disable the fasttrackCacke to start with, inject an object to enable it.
399
+     */
400 400
     private $fastTrackCache = null;
401 401
 
402 402
 
@@ -426,9 +426,9 @@  discard block
 block discarded – undo
426 426
     /*
427 427
      * Image copy strategy, defaults to RESAMPLE.
428 428
      */
429
-     const RESIZE = 1;
430
-     const RESAMPLE = 2;
431
-     private $copyStrategy = NULL;
429
+        const RESIZE = 1;
430
+        const RESAMPLE = 2;
431
+        private $copyStrategy = NULL;
432 432
 
433 433
 
434 434
 
@@ -1253,7 +1253,7 @@  discard block
 block discarded – undo
1253 1253
         $this->crop      = $this->cropOrig;
1254 1254
 
1255 1255
         $this->initDimensions()
1256
-             ->calculateNewWidthAndHeight();
1256
+                ->calculateNewWidthAndHeight();
1257 1257
 
1258 1258
         return $this;
1259 1259
     }
@@ -1684,14 +1684,14 @@  discard block
 block discarded – undo
1684 1684
         if ($this->rotateBefore) {
1685 1685
             $this->log("Rotating image.");
1686 1686
             $this->rotate($this->rotateBefore, $this->bgColor)
1687
-                 ->reCalculateDimensions();
1687
+                    ->reCalculateDimensions();
1688 1688
         }
1689 1689
 
1690 1690
         // Auto-rotate image
1691 1691
         if ($this->autoRotate) {
1692 1692
             $this->log("Auto rotating image.");
1693 1693
             $this->rotateExif()
1694
-                 ->reCalculateDimensions();
1694
+                    ->reCalculateDimensions();
1695 1695
         }
1696 1696
 
1697 1697
         // Scale the original image before starting
@@ -1718,11 +1718,11 @@  discard block
 block discarded – undo
1718 1718
      *
1719 1719
      * @return $this
1720 1720
      */
1721
-     public function setCopyResizeStrategy($strategy)
1722
-     {
1723
-         $this->copyStrategy = $strategy;
1724
-         return $this;
1725
-     }
1721
+        public function setCopyResizeStrategy($strategy)
1722
+        {
1723
+            $this->copyStrategy = $strategy;
1724
+            return $this;
1725
+        }
1726 1726
 
1727 1727
 
1728 1728
 
@@ -2233,7 +2233,7 @@  discard block
 block discarded – undo
2233 2233
      * @param string $color as hex value.
2234 2234
      *
2235 2235
      * @return $this
2236
-    */
2236
+     */
2237 2237
     public function setDefaultBackgroundColor($color)
2238 2238
     {
2239 2239
         $this->log("Setting default background color to '$color'.");
@@ -2285,7 +2285,7 @@  discard block
 block discarded – undo
2285 2285
      * @param resource $img the image to work with or null if using $this->image.
2286 2286
      *
2287 2287
      * @return color value or null if no background color is set.
2288
-    */
2288
+     */
2289 2289
     private function getBackgroundColor($img = null)
2290 2290
     {
2291 2291
         $img = isset($img) ? $img : $this->image;
@@ -2319,7 +2319,7 @@  discard block
 block discarded – undo
2319 2319
      * @param int $height of the new image.
2320 2320
      *
2321 2321
      * @return image resource.
2322
-    */
2322
+     */
2323 2323
     private function createImageKeepTransparency($width, $height)
2324 2324
     {
2325 2325
         $this->log("Creating a new working image width={$width}px, height={$height}px.");
@@ -2564,8 +2564,8 @@  discard block
 block discarded – undo
2564 2564
 
2565 2565
         // Prepare
2566 2566
         $this->setSaveFolder($cache)
2567
-             ->setSource($src, $dir)
2568
-             ->generateFilename(null, false, 'srgb_');
2567
+                ->setSource($src, $dir)
2568
+                ->generateFilename(null, false, 'srgb_');
2569 2569
 
2570 2570
         // Check if the cached version is accurate.
2571 2571
         if ($useCache && is_readable($this->cacheFileName)) {
Please login to merge, or discard this patch.
a/vendor/mos/cimage/webroot/img_config.php 1 patch
Indentation   +105 added lines, -105 removed lines patch added patch discarded remove patch
@@ -40,9 +40,9 @@  discard block
 block discarded – undo
40 40
      * Default values:
41 41
      *  mode: 'production'
42 42
      */
43
-     //'mode' => 'production',
44
-     'mode' => 'development',
45
-     //'mode' => 'strict',
43
+        //'mode' => 'production',
44
+        'mode' => 'development',
45
+        //'mode' => 'strict',
46 46
 
47 47
 
48 48
 
@@ -93,9 +93,9 @@  discard block
 block discarded – undo
93 93
      *  CCache: CCache
94 94
      *  CFastTrackCache: CFastTrackCache
95 95
      */
96
-     //'CImage' => 'CImage',
97
-     //'CCache' => 'CCache',
98
-     //'CFastTrackCache' => 'CFastTrackCache',
96
+        //'CImage' => 'CImage',
97
+        //'CCache' => 'CCache',
98
+        //'CFastTrackCache' => 'CFastTrackCache',
99 99
 
100 100
 
101 101
 
@@ -160,8 +160,8 @@  discard block
 block discarded – undo
160 160
      * Default value:
161 161
      *  src_alt:  null //disabled by default
162 162
      */
163
-     //'src_alt' => 'car.png',
164
-     //'src_alt' => 'dummy',
163
+        //'src_alt' => 'car.png',
164
+        //'src_alt' => 'dummy',
165 165
 
166 166
 
167 167
 
@@ -172,62 +172,62 @@  discard block
 block discarded – undo
172 172
      *  valid_filename:  '#^[a-z0-9A-Z-/_ \.:]+$#'
173 173
      *  valid_aliasname: '#^[a-z0-9A-Z-_]+$#'
174 174
      */
175
-     //'valid_filename'  => '#^[a-z0-9A-Z-/_ \.:]+$#',
176
-     //'valid_aliasname' => '#^[a-z0-9A-Z-_]+$#',
177
-
178
-
179
-
180
-     /**
181
-      * Change the default values for CImage quality and compression used
182
-      * when saving images.
183
-      *
184
-      * Default value:
185
-      *  jpg_quality:     null, integer between 0-100
186
-      *  png_compression: null, integer between 0-9
187
-      */
188
-      //'jpg_quality'  => 75,
189
-      //'png_compression' => 1,
190
-
191
-
192
-
193
-      /**
194
-       * Convert the image to srgb before processing. Saves the converted
195
-       * image in a cache subdir 'srgb'. This option is default false but can
196
-       * be changed to default true to do this conversion for all images.
197
-       * This option requires PHP extension imagick and will silently fail
198
-       * if that is not installed.
199
-       *
200
-       * Default value:
201
-       *  srgb_default:      false
202
-       *  srgb_colorprofile: __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc'
203
-       */
204
-       //'srgb_default' => false,
205
-       //'srgb_colorprofile' => __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc',
206
-
207
-
208
-
209
-       /**
210
-        * Set skip-original to true to always process the image and use
211
-        * the cached version. Default is false and to use the original
212
-        * image when its no processing needed.
213
-        *
214
-        * Default value:
215
-        *  skip_original: false
216
-        */
175
+        //'valid_filename'  => '#^[a-z0-9A-Z-/_ \.:]+$#',
176
+        //'valid_aliasname' => '#^[a-z0-9A-Z-_]+$#',
177
+
178
+
179
+
180
+        /**
181
+         * Change the default values for CImage quality and compression used
182
+         * when saving images.
183
+         *
184
+         * Default value:
185
+         *  jpg_quality:     null, integer between 0-100
186
+         *  png_compression: null, integer between 0-9
187
+         */
188
+        //'jpg_quality'  => 75,
189
+        //'png_compression' => 1,
190
+
191
+
192
+
193
+        /**
194
+         * Convert the image to srgb before processing. Saves the converted
195
+         * image in a cache subdir 'srgb'. This option is default false but can
196
+         * be changed to default true to do this conversion for all images.
197
+         * This option requires PHP extension imagick and will silently fail
198
+         * if that is not installed.
199
+         *
200
+         * Default value:
201
+         *  srgb_default:      false
202
+         *  srgb_colorprofile: __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc'
203
+         */
204
+        //'srgb_default' => false,
205
+        //'srgb_colorprofile' => __DIR__ . '/../icc/sRGB_IEC61966-2-1_black_scaled.icc',
206
+
207
+
208
+
209
+        /**
210
+         * Set skip-original to true to always process the image and use
211
+         * the cached version. Default is false and to use the original
212
+         * image when its no processing needed.
213
+         *
214
+         * Default value:
215
+         *  skip_original: false
216
+         */
217 217
         //'skip_original' => true,
218 218
 
219 219
 
220 220
 
221
-      /**
222
-       * A function (hook) can be called after img.php has processed all
223
-       * configuration options and before processing the image using CImage.
224
-       * The function receives the $img variabel and an array with the
225
-       * majority of current settings.
226
-       *
227
-       * Default value:
228
-       *  hook_before_CImage:     null
229
-       */
230
-       /*'hook_before_CImage' => function (CImage $img, Array $allConfig) {
221
+        /**
222
+         * A function (hook) can be called after img.php has processed all
223
+         * configuration options and before processing the image using CImage.
224
+         * The function receives the $img variabel and an array with the
225
+         * majority of current settings.
226
+         *
227
+         * Default value:
228
+         *  hook_before_CImage:     null
229
+         */
230
+        /*'hook_before_CImage' => function (CImage $img, Array $allConfig) {
231 231
            if ($allConfig['newWidth'] > 10) {
232 232
                $allConfig['newWidth'] *= 2;
233 233
            }
@@ -236,53 +236,53 @@  discard block
 block discarded – undo
236 236
 
237 237
 
238 238
 
239
-       /**
240
-        * Add header for cache control when outputting images.
241
-        *
242
-        * Default value:
243
-        *  cache_control: null, or set to string
244
-        */
239
+        /**
240
+         * Add header for cache control when outputting images.
241
+         *
242
+         * Default value:
243
+         *  cache_control: null, or set to string
244
+         */
245 245
         //'cache_control' => "max-age=86400",
246 246
 
247 247
 
248 248
 
249
-     /**
250
-      * The name representing a dummy image which is automatically created
251
-      * and stored as a image in the dir CACHE_PATH/dummy. The dummy image
252
-      * can then be used as a placeholder image.
253
-      * The dir CACHE_PATH/dummy is automatically created when needed.
254
-      * Write protect the CACHE_PATH/dummy to prevent creation of new
255
-      * dummy images, but continue to use the existing ones.
256
-      *
257
-      * Default value:
258
-      *  dummy_enabled:  true as default, disable dummy feature by setting
259
-      *                  to false.
260
-      *  dummy_filename: 'dummy' use this as ?src=dummy to create a dummy image.
261
-      */
262
-      //'dummy_enabled' => true,
263
-      //'dummy_filename' => 'dummy',
264
-
265
-
266
-
267
-     /**
268
-     * Check that the imagefile is a file below 'image_path' using realpath().
269
-     * Security constraint to avoid reaching images outside image_path.
270
-     * This means that symbolic links to images outside the image_path will
271
-     * fail.
272
-     *
273
-     * Default value:
274
-     *  image_path_constraint: true
275
-     */
276
-     //'image_path_constraint' => false,
277
-
278
-
279
-
280
-     /**
281
-     * Set default timezone.
282
-     *
283
-     * Default values.
284
-     *  default_timezone: ini_get('default_timezone') or 'UTC'
285
-     */
249
+        /**
250
+         * The name representing a dummy image which is automatically created
251
+         * and stored as a image in the dir CACHE_PATH/dummy. The dummy image
252
+         * can then be used as a placeholder image.
253
+         * The dir CACHE_PATH/dummy is automatically created when needed.
254
+         * Write protect the CACHE_PATH/dummy to prevent creation of new
255
+         * dummy images, but continue to use the existing ones.
256
+         *
257
+         * Default value:
258
+         *  dummy_enabled:  true as default, disable dummy feature by setting
259
+         *                  to false.
260
+         *  dummy_filename: 'dummy' use this as ?src=dummy to create a dummy image.
261
+         */
262
+        //'dummy_enabled' => true,
263
+        //'dummy_filename' => 'dummy',
264
+
265
+
266
+
267
+        /**
268
+         * Check that the imagefile is a file below 'image_path' using realpath().
269
+         * Security constraint to avoid reaching images outside image_path.
270
+         * This means that symbolic links to images outside the image_path will
271
+         * fail.
272
+         *
273
+         * Default value:
274
+         *  image_path_constraint: true
275
+         */
276
+        //'image_path_constraint' => false,
277
+
278
+
279
+
280
+        /**
281
+         * Set default timezone.
282
+         *
283
+         * Default values.
284
+         *  default_timezone: ini_get('default_timezone') or 'UTC'
285
+         */
286 286
     //'default_timezone' => 'UTC',
287 287
 
288 288
 
@@ -388,7 +388,7 @@  discard block
 block discarded – undo
388 388
      *  allow_hotlinking:     true
389 389
      *  hotlinking_whitelist: array()
390 390
      */
391
-     /*
391
+        /*
392 392
     'allow_hotlinking' => false,
393 393
     'hotlinking_whitelist' => array(
394 394
         '^dbwebb\.se$',
@@ -404,7 +404,7 @@  discard block
 block discarded – undo
404 404
      *      'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
405 405
      *  )
406 406
      */
407
-     /*
407
+        /*
408 408
     'shortcut' => array(
409 409
         'sepia' => "&f=grayscale&f0=brightness,-10&f1=contrast,-20&f2=colorize,120,60,0,0&sharpen",
410 410
     ),*/
Please login to merge, or discard this patch.
a/vendor/mos/cimage/webroot/imgp.php 1 patch
Indentation   +23 added lines, -23 removed lines patch added patch discarded remove patch
@@ -234,9 +234,9 @@  discard block
 block discarded – undo
234 234
 
235 235
 
236 236
     /**
237
-    * Constructor
238
-    *
239
-    */
237
+     * Constructor
238
+     *
239
+     */
240 240
     public function __construct()
241 241
     {
242 242
         $this->request['header'] = array();
@@ -1333,8 +1333,8 @@  discard block
 block discarded – undo
1333 1333
     /**
1334 1334
      * Path to command to optimize jpeg images, for example jpegtran or null.
1335 1335
      */
1336
-     private $jpegOptimize;
1337
-     private $jpegOptimizeCmd;
1336
+        private $jpegOptimize;
1337
+        private $jpegOptimizeCmd;
1338 1338
 
1339 1339
 
1340 1340
 
@@ -1505,8 +1505,8 @@  discard block
 block discarded – undo
1505 1505
 
1506 1506
 
1507 1507
     /**
1508
-    * Disable the fasttrackCacke to start with, inject an object to enable it.
1509
-    */
1508
+     * Disable the fasttrackCacke to start with, inject an object to enable it.
1509
+     */
1510 1510
     private $fastTrackCache = null;
1511 1511
 
1512 1512
 
@@ -1536,9 +1536,9 @@  discard block
 block discarded – undo
1536 1536
     /*
1537 1537
      * Image copy strategy, defaults to RESAMPLE.
1538 1538
      */
1539
-     const RESIZE = 1;
1540
-     const RESAMPLE = 2;
1541
-     private $copyStrategy = NULL;
1539
+        const RESIZE = 1;
1540
+        const RESAMPLE = 2;
1541
+        private $copyStrategy = NULL;
1542 1542
 
1543 1543
 
1544 1544
 
@@ -2363,7 +2363,7 @@  discard block
 block discarded – undo
2363 2363
         $this->crop      = $this->cropOrig;
2364 2364
 
2365 2365
         $this->initDimensions()
2366
-             ->calculateNewWidthAndHeight();
2366
+                ->calculateNewWidthAndHeight();
2367 2367
 
2368 2368
         return $this;
2369 2369
     }
@@ -2794,14 +2794,14 @@  discard block
 block discarded – undo
2794 2794
         if ($this->rotateBefore) {
2795 2795
             $this->log("Rotating image.");
2796 2796
             $this->rotate($this->rotateBefore, $this->bgColor)
2797
-                 ->reCalculateDimensions();
2797
+                    ->reCalculateDimensions();
2798 2798
         }
2799 2799
 
2800 2800
         // Auto-rotate image
2801 2801
         if ($this->autoRotate) {
2802 2802
             $this->log("Auto rotating image.");
2803 2803
             $this->rotateExif()
2804
-                 ->reCalculateDimensions();
2804
+                    ->reCalculateDimensions();
2805 2805
         }
2806 2806
 
2807 2807
         // Scale the original image before starting
@@ -2828,11 +2828,11 @@  discard block
 block discarded – undo
2828 2828
      *
2829 2829
      * @return $this
2830 2830
      */
2831
-     public function setCopyResizeStrategy($strategy)
2832
-     {
2833
-         $this->copyStrategy = $strategy;
2834
-         return $this;
2835
-     }
2831
+        public function setCopyResizeStrategy($strategy)
2832
+        {
2833
+            $this->copyStrategy = $strategy;
2834
+            return $this;
2835
+        }
2836 2836
 
2837 2837
 
2838 2838
 
@@ -3343,7 +3343,7 @@  discard block
 block discarded – undo
3343 3343
      * @param string $color as hex value.
3344 3344
      *
3345 3345
      * @return $this
3346
-    */
3346
+     */
3347 3347
     public function setDefaultBackgroundColor($color)
3348 3348
     {
3349 3349
         $this->log("Setting default background color to '$color'.");
@@ -3395,7 +3395,7 @@  discard block
 block discarded – undo
3395 3395
      * @param resource $img the image to work with or null if using $this->image.
3396 3396
      *
3397 3397
      * @return color value or null if no background color is set.
3398
-    */
3398
+     */
3399 3399
     private function getBackgroundColor($img = null)
3400 3400
     {
3401 3401
         $img = isset($img) ? $img : $this->image;
@@ -3429,7 +3429,7 @@  discard block
 block discarded – undo
3429 3429
      * @param int $height of the new image.
3430 3430
      *
3431 3431
      * @return image resource.
3432
-    */
3432
+     */
3433 3433
     private function createImageKeepTransparency($width, $height)
3434 3434
     {
3435 3435
         $this->log("Creating a new working image width={$width}px, height={$height}px.");
@@ -3674,8 +3674,8 @@  discard block
 block discarded – undo
3674 3674
 
3675 3675
         // Prepare
3676 3676
         $this->setSaveFolder($cache)
3677
-             ->setSource($src, $dir)
3678
-             ->generateFilename(null, false, 'srgb_');
3677
+                ->setSource($src, $dir)
3678
+                ->generateFilename(null, false, 'srgb_');
3679 3679
 
3680 3680
         // Check if the cached version is accurate.
3681 3681
         if ($useCache && is_readable($this->cacheFileName)) {
Please login to merge, or discard this patch.
a/vendor/mos/cimage/webroot/test/test.php 1 patch
Indentation   +31 added lines, -31 removed lines patch added patch discarded remove patch
@@ -10,33 +10,33 @@  discard block
 block discarded – undo
10 10
 
11 11
 <?php
12 12
 $testcase = array(
13
-  array('text'=>'Original image', 'query'=>''),
14
-  array('text'=>'Crop out a rectangle of 100x100, start by position 200x200.', 'query'=>'&crop=100,100,200,200'),
15
-  array('text'=>'Crop out a full width rectangle with height of 200, start by position 0x100.', 'query'=>'&crop=0,200,0,100'),
16
-  array('text'=>'Max width 200.', 'query'=>'&w=200'),
17
-  array('text'=>'Max height 200.', 'query'=>'&h=200'),
18
-  array('text'=>'Max width 200 and max height 200.', 'query'=>'&w=200&h=200'),
19
-  array('text'=>'No-ratio makes image fit in area of width 200 and height 200.', 'query'=>'&w=200&h=200&no-ratio'),
20
-  array('text'=>'Crop to fit in width 200 and height 200.', 'query'=>'&w=200&h=200&crop-to-fit'),
21
-  array('text'=>'Crop to fit in width 200 and height 100.', 'query'=>'&w=200&h=100&crop-to-fit'),
22
-  array('text'=>'Crop to fit in width 100 and height 200.', 'query'=>'&w=100&h=200&crop-to-fit'),
23
-  array('text'=>'Quality 70', 'query'=>'&w=200&h=200&quality=70'),
24
-  array('text'=>'Quality 40', 'query'=>'&w=200&h=200&quality=40'),
25
-  array('text'=>'Quality 10', 'query'=>'&w=200&h=200&quality=10'),
26
-  array('text'=>'Filter: Negate', 'query'=>'&w=200&h=200&f=negate'),
27
-  array('text'=>'Filter: Grayscale', 'query'=>'&w=200&h=200&f=grayscale'),
28
-  array('text'=>'Filter: Brightness 90', 'query'=>'&w=200&h=200&f=brightness,90'),
29
-  array('text'=>'Filter: Contrast 50', 'query'=>'&w=200&h=200&f=contrast,50'),
30
-  array('text'=>'Filter: Colorize 0,255,0,0', 'query'=>'&w=200&h=200&f=colorize,0,255,0,0'),
31
-  array('text'=>'Filter: Edge detect', 'query'=>'&w=200&h=200&f=edgedetect'),
32
-  array('text'=>'Filter: Emboss', 'query'=>'&w=200&h=200&f=emboss'),
33
-  array('text'=>'Filter: Gaussian blur', 'query'=>'&w=200&h=200&f=gaussian_blur'),
34
-  array('text'=>'Filter: Selective blur', 'query'=>'&w=200&h=200&f=selective_blur'),
35
-  array('text'=>'Filter: Mean removal', 'query'=>'&w=200&h=200&f=mean_removal'),
36
-  array('text'=>'Filter: Smooth 2', 'query'=>'&w=200&h=200&f=smooth,2'),
37
-  array('text'=>'Filter: Pixelate 10,10', 'query'=>'&w=200&h=200&f=pixelate,10,10'),
38
-  array('text'=>'Multiple filter: Negate, Grayscale and Pixelate 10,10', 'query'=>'&w=200&h=200&&f=negate&f0=grayscale&f1=pixelate,10,10'),
39
-  array('text'=>'Crop with width & height and crop-to-fit with quality and filter', 'query'=>'&crop=100,100,100,100&w=200&h=200&crop-to-fit&q=70&f0=grayscale'),
13
+    array('text'=>'Original image', 'query'=>''),
14
+    array('text'=>'Crop out a rectangle of 100x100, start by position 200x200.', 'query'=>'&crop=100,100,200,200'),
15
+    array('text'=>'Crop out a full width rectangle with height of 200, start by position 0x100.', 'query'=>'&crop=0,200,0,100'),
16
+    array('text'=>'Max width 200.', 'query'=>'&w=200'),
17
+    array('text'=>'Max height 200.', 'query'=>'&h=200'),
18
+    array('text'=>'Max width 200 and max height 200.', 'query'=>'&w=200&h=200'),
19
+    array('text'=>'No-ratio makes image fit in area of width 200 and height 200.', 'query'=>'&w=200&h=200&no-ratio'),
20
+    array('text'=>'Crop to fit in width 200 and height 200.', 'query'=>'&w=200&h=200&crop-to-fit'),
21
+    array('text'=>'Crop to fit in width 200 and height 100.', 'query'=>'&w=200&h=100&crop-to-fit'),
22
+    array('text'=>'Crop to fit in width 100 and height 200.', 'query'=>'&w=100&h=200&crop-to-fit'),
23
+    array('text'=>'Quality 70', 'query'=>'&w=200&h=200&quality=70'),
24
+    array('text'=>'Quality 40', 'query'=>'&w=200&h=200&quality=40'),
25
+    array('text'=>'Quality 10', 'query'=>'&w=200&h=200&quality=10'),
26
+    array('text'=>'Filter: Negate', 'query'=>'&w=200&h=200&f=negate'),
27
+    array('text'=>'Filter: Grayscale', 'query'=>'&w=200&h=200&f=grayscale'),
28
+    array('text'=>'Filter: Brightness 90', 'query'=>'&w=200&h=200&f=brightness,90'),
29
+    array('text'=>'Filter: Contrast 50', 'query'=>'&w=200&h=200&f=contrast,50'),
30
+    array('text'=>'Filter: Colorize 0,255,0,0', 'query'=>'&w=200&h=200&f=colorize,0,255,0,0'),
31
+    array('text'=>'Filter: Edge detect', 'query'=>'&w=200&h=200&f=edgedetect'),
32
+    array('text'=>'Filter: Emboss', 'query'=>'&w=200&h=200&f=emboss'),
33
+    array('text'=>'Filter: Gaussian blur', 'query'=>'&w=200&h=200&f=gaussian_blur'),
34
+    array('text'=>'Filter: Selective blur', 'query'=>'&w=200&h=200&f=selective_blur'),
35
+    array('text'=>'Filter: Mean removal', 'query'=>'&w=200&h=200&f=mean_removal'),
36
+    array('text'=>'Filter: Smooth 2', 'query'=>'&w=200&h=200&f=smooth,2'),
37
+    array('text'=>'Filter: Pixelate 10,10', 'query'=>'&w=200&h=200&f=pixelate,10,10'),
38
+    array('text'=>'Multiple filter: Negate, Grayscale and Pixelate 10,10', 'query'=>'&w=200&h=200&&f=negate&f0=grayscale&f1=pixelate,10,10'),
39
+    array('text'=>'Crop with width & height and crop-to-fit with quality and filter', 'query'=>'&crop=100,100,100,100&w=200&h=200&crop-to-fit&q=70&f0=grayscale'),
40 40
 );
41 41
 ?>
42 42
 
@@ -47,8 +47,8 @@  discard block
 block discarded – undo
47 47
 <tbody>
48 48
 <?php
49 49
 foreach($testcase as $key => $val) {
50
-  $url = "../img.php?src=wider.jpg{$val['query']}";
51
-  echo "<tr><td id=w$key><a href=#w$key>$key</a></br>{$val['text']}</br><code><a href='$url'>".htmlentities($url)."</a></code></td><td><img src='$url' /></td></tr>";
50
+    $url = "../img.php?src=wider.jpg{$val['query']}";
51
+    echo "<tr><td id=w$key><a href=#w$key>$key</a></br>{$val['text']}</br><code><a href='$url'>".htmlentities($url)."</a></code></td><td><img src='$url' /></td></tr>";
52 52
 }
53 53
 ?>
54 54
 </tbody>
@@ -61,8 +61,8 @@  discard block
 block discarded – undo
61 61
 <tbody>
62 62
 <?php
63 63
 foreach($testcase as $key => $val) {
64
-  $url = "../img.php?src=higher.jpg{$val['query']}";
65
-  echo "<tr><td id=h$key><a href=#h$key>$key</a></br>{$val['text']}</br><code><a href='$url'>".htmlentities($url)."</a></code></td><td><img src='$url' /></td></tr>";
64
+    $url = "../img.php?src=higher.jpg{$val['query']}";
65
+    echo "<tr><td id=h$key><a href=#h$key>$key</a></br>{$val['text']}</br><code><a href='$url'>".htmlentities($url)."</a></code></td><td><img src='$url' /></td></tr>";
66 66
 }
67 67
 ?>
68 68
 </tbody>
Please login to merge, or discard this patch.
a/vendor/mos/cimage/webroot/imgs.php 1 patch
Indentation   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -1,5 +1,5 @@
 block discarded – undo
1 1
 <?php
2
- define("CIMAGE_BUNDLE", true); $config = array( 'mode' => 'strict', ); define("CIMAGE_VERSION", "v0.7.23 (2020-05-06)"); define("CIMAGE_USER_AGENT", "CImage/" . CIMAGE_VERSION); if (!defined("IMG_WEBP")) { define("IMG_WEBP", -1); } function trace($msg) { $file = CIMAGE_DEBUG_FILE; if (!is_writable($file)) { return; } $timer = number_format((microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]), 6); $details = "{$timer}ms"; $details .= ":" . round(memory_get_peak_usage()/1024/1024, 3) . "MB"; $details .= ":" . count(get_included_files()); file_put_contents($file, "$details:$msg\n", FILE_APPEND); } function errorPage($msg, $type = 500) { global $mode; switch ($type) { case 403: $header = "403 Forbidden"; break; case 404: $header = "404 Not Found"; break; default: $header = "500 Internal Server Error"; } if ($mode == "strict") { $header = "404 Not Found"; } header("HTTP/1.0 $header"); if ($mode == "development") { die("[img.php] $msg"); } error_log("[img.php] $msg"); die("HTTP/1.0 $header"); } function get($key, $default = null) { if (is_array($key)) { foreach ($key as $val) { if (isset($_GET[$val])) { return $_GET[$val]; } } } elseif (isset($_GET[$key])) { return $_GET[$key]; } return $default; } function getDefined($key, $defined, $undefined) { return get($key) === null ? $undefined : $defined; } function getConfig($key, $default) { global $config; return isset($config[$key]) ? $config[$key] : $default; } function verbose($msg = null) { global $verbose, $verboseFile; static $log = array(); if (!($verbose || $verboseFile)) { return; } if (is_null($msg)) { return $log; } $log[] = $msg; } function checkExternalCommand($what, $enabled, $commandString) { $no = $enabled ? null : 'NOT'; $text = "Post processing $what is $no enabled.<br>"; list($command) = explode(" ", $commandString); $no = is_executable($command) ? null : 'NOT'; $text .= "The command for $what is $no an executable.<br>"; return $text; } class CHttpGet { private $request = array(); private $response = array(); public function __construct() { $this->request['header'] = array(); } public function buildUrl($baseUrl, $merge) { $parts = parse_url($baseUrl); $parts = array_merge($parts, $merge); $url = $parts['scheme']; $url .= "://"; $url .= $parts['host']; $url .= isset($parts['port']) ? ":" . $parts['port'] : "" ; $url .= $parts['path']; return $url; } public function setUrl($url) { $parts = parse_url($url); $path = ""; if (isset($parts['path'])) { $pathParts = explode('/', $parts['path']); unset($pathParts[0]); foreach ($pathParts as $value) { $path .= "/" . rawurlencode($value); } } $url = $this->buildUrl($url, array("path" => $path)); $this->request['url'] = $url; return $this; } public function setHeader($field, $value) { $this->request['header'][] = "$field: $value"; return $this; } public function parseHeader() { $rawHeaders = rtrim($this->response['headerRaw'], "\r\n"); $headerGroups = explode("\r\n\r\n", $rawHeaders); $header = explode("\r\n", end($headerGroups)); $output = array(); if ('HTTP' === substr($header[0], 0, 4)) { list($output['version'], $output['status']) = explode(' ', $header[0]); unset($header[0]); } foreach ($header as $entry) { $pos = strpos($entry, ':'); $output[trim(substr($entry, 0, $pos))] = trim(substr($entry, $pos + 1)); } $this->response['header'] = $output; return $this; } public function doGet($debug = false) { $options = array( CURLOPT_URL => $this->request['url'], CURLOPT_HEADER => 1, CURLOPT_HTTPHEADER => $this->request['header'], CURLOPT_AUTOREFERER => true, CURLOPT_RETURNTRANSFER => true, CURLINFO_HEADER_OUT => $debug, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 5, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 2, ); $ch = curl_init(); curl_setopt_array($ch, $options); $response = curl_exec($ch); if (!$response) { throw new Exception("Failed retrieving url, details follows: " . curl_error($ch)); } $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $this->response['headerRaw'] = substr($response, 0, $headerSize); $this->response['body'] = substr($response, $headerSize); $this->parseHeader(); if ($debug) { $info = curl_getinfo($ch); echo "Request header<br><pre>", var_dump($info['request_header']), "</pre>"; echo "Response header (raw)<br><pre>", var_dump($this->response['headerRaw']), "</pre>"; echo "Response header (parsed)<br><pre>", var_dump($this->response['header']), "</pre>"; } curl_close($ch); return true; } public function getStatus() { return isset($this->response['header']['status']) ? (int) $this->response['header']['status'] : null; } public function getLastModified() { return isset($this->response['header']['Last-Modified']) ? strtotime($this->response['header']['Last-Modified']) : null; } public function getContentType() { $type = isset($this->response['header']['Content-Type']) ? $this->response['header']['Content-Type'] : null; return preg_match('#[a-z]+/[a-z]+#', $type) ? $type : null; } public function getDate($default = false) { return isset($this->response['header']['Date']) ? strtotime($this->response['header']['Date']) : $default; } public function getMaxAge($default = false) { $cacheControl = isset($this->response['header']['Cache-Control']) ? $this->response['header']['Cache-Control'] : null; $maxAge = null; if ($cacheControl) { $part = explode('=', $cacheControl); $maxAge = ($part[0] == "max-age") ? (int) $part[1] : null; } if ($maxAge) { return $maxAge; } $expire = isset($this->response['header']['Expires']) ? strtotime($this->response['header']['Expires']) : null; return $expire ? $expire : $default; } public function getBody() { return $this->response['body']; } } class CRemoteImage { private $saveFolder = null; private $useCache = true; private $http; private $status; private $defaultMaxAge = 604800; private $url; private $fileName; private $fileJson; private $cache; public function getStatus() { return $this->status; } public function getDetails() { return $this->cache; } public function setCache($path) { $this->saveFolder = rtrim($path, "/") . "/"; return $this; } public function isCacheWritable() { if (!is_writable($this->saveFolder)) { throw new Exception("Cache folder is not writable for downloaded files."); } return $this; } public function useCache($use = true) { $this->useCache = $use; return $this; } public function setHeaderFields() { $cimageVersion = "CImage"; if (defined("CIMAGE_USER_AGENT")) { $cimageVersion = CIMAGE_USER_AGENT; } $this->http->setHeader("User-Agent", "$cimageVersion (PHP/". phpversion() . " cURL)"); $this->http->setHeader("Accept", "image/jpeg,image/png,image/gif"); if ($this->useCache) { $this->http->setHeader("Cache-Control", "max-age=0"); } else { $this->http->setHeader("Cache-Control", "no-cache"); $this->http->setHeader("Pragma", "no-cache"); } } public function save() { $this->cache = array(); $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $type = $this->http->getContentType(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; $this->cache['Content-Type'] = $type; $this->cache['Url'] = $this->url; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } $body = $this->http->getBody(); $img = imagecreatefromstring($body); if ($img !== false) { file_put_contents($this->fileName, $body); file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileName; } return false; } public function updateCacheDetails() { $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileName; } public function download($url) { $this->http = new CHttpGet(); $this->url = $url; $this->loadCacheDetails(); if ($this->useCache) { $src = $this->getCachedSource(); if ($src) { $this->status = 1; return $src; } } $this->setHeaderFields(); $this->http->setUrl($this->url); $this->http->doGet(); $this->status = $this->http->getStatus(); if ($this->status === 200) { $this->isCacheWritable(); return $this->save(); } elseif ($this->status === 304) { $this->isCacheWritable(); return $this->updateCacheDetails(); } throw new Exception("Unknown statuscode when downloading remote image: " . $this->status); } public function loadCacheDetails() { $cacheFile = md5($this->url); $this->fileName = $this->saveFolder . $cacheFile; $this->fileJson = $this->fileName . ".json"; if (is_readable($this->fileJson)) { $this->cache = json_decode(file_get_contents($this->fileJson), true); } } public function getCachedSource() { $imageExists = is_readable($this->fileName); $date = strtotime($this->cache['Date']); $maxAge = $this->cache['Max-Age']; $now = time(); if ($imageExists && $date + $maxAge > $now) { return $this->fileName; } if ($imageExists && isset($this->cache['Last-Modified'])) { $this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']); } return false; } } class CWhitelist { private $whitelist = array(); public function set($whitelist = array()) { if (!is_array($whitelist)) { throw new Exception("Whitelist is not of a supported format."); } $this->whitelist = $whitelist; return $this; } public function check($item, $whitelist = null) { if ($whitelist !== null) { $this->set($whitelist); } if (empty($item) or empty($this->whitelist)) { return false; } foreach ($this->whitelist as $regexp) { if (preg_match("#$regexp#", $item)) { return true; } } return false; } } class CAsciiArt { private $characterSet = array( 'one' => "#0XT|:,.' ", 'two' => "@%#*+=-:. ", 'three' => "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. " ); private $characters = null; private $charCount = null; private $scale = null; private $luminanceStrategy = null; public function __construct() { $this->setOptions(); } public function addCharacterSet($key, $value) { $this->characterSet[$key] = $value; return $this; } public function setOptions($options = array()) { $default = array( "characterSet" => 'two', "scale" => 14, "luminanceStrategy" => 3, "customCharacterSet" => null, ); $default = array_merge($default, $options); if (!is_null($default['customCharacterSet'])) { $this->addCharacterSet('custom', $default['customCharacterSet']); $default['characterSet'] = 'custom'; } $this->scale = $default['scale']; $this->characters = $this->characterSet[$default['characterSet']]; $this->charCount = strlen($this->characters); $this->luminanceStrategy = $default['luminanceStrategy']; return $this; } public function createFromFile($filename) { $img = imagecreatefromstring(file_get_contents($filename)); list($width, $height) = getimagesize($filename); $ascii = null; $incY = $this->scale; $incX = $this->scale / 2; for ($y = 0; $y < $height - 1; $y += $incY) { for ($x = 0; $x < $width - 1; $x += $incX) { $toX = min($x + $this->scale / 2, $width - 1); $toY = min($y + $this->scale, $height - 1); $luminance = $this->luminanceAreaAverage($img, $x, $y, $toX, $toY); $ascii .= $this->luminance2character($luminance); } $ascii .= PHP_EOL; } return $ascii; } public function luminanceAreaAverage($img, $x1, $y1, $x2, $y2) { $numPixels = ($x2 - $x1 + 1) * ($y2 - $y1 + 1); $luminance = 0; for ($x = $x1; $x <= $x2; $x++) { for ($y = $y1; $y <= $y2; $y++) { $rgb = imagecolorat($img, $x, $y); $red = (($rgb >> 16) & 0xFF); $green = (($rgb >> 8) & 0xFF); $blue = ($rgb & 0xFF); $luminance += $this->getLuminance($red, $green, $blue); } } return $luminance / $numPixels; } public function getLuminance($red, $green, $blue) { switch ($this->luminanceStrategy) { case 1: $luminance = ($red * 0.2126 + $green * 0.7152 + $blue * 0.0722) / 255; break; case 2: $luminance = ($red * 0.299 + $green * 0.587 + $blue * 0.114) / 255; break; case 3: $luminance = sqrt(0.299 * pow($red, 2) + 0.587 * pow($green, 2) + 0.114 * pow($blue, 2)) / 255; break; case 0: default: $luminance = ($red + $green + $blue) / (255 * 3); } return $luminance; } public function luminance2character($luminance) { $position = (int) round($luminance * ($this->charCount - 1)); $char = $this->characters[$position]; return $char; } } class CImage { const PNG_GREYSCALE = 0; const PNG_RGB = 2; const PNG_RGB_PALETTE = 3; const PNG_GREYSCALE_ALPHA = 4; const PNG_RGB_ALPHA = 6; const JPEG_QUALITY_DEFAULT = 60; private $quality; private $useQuality = false; const PNG_COMPRESSION_DEFAULT = -1; private $compress; private $useCompress = false; private $HTTPHeader = array(); private $bgColorDefault = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, ); private $bgColor; private $saveFolder; private $image; private $imageSrc; private $pathToImage; private $fileType; private $extension; private $outputFormat = null; private $lossy = null; private $verbose = false; private $log = array(); private $palette; private $cacheFileName; private $saveAs; private $pngLossy; private $pngLossyCmd; private $pngFilter; private $pngFilterCmd; private $pngDeflate; private $pngDeflateCmd; private $jpegOptimize; private $jpegOptimizeCmd; private $width; private $height; private $newWidth; private $newWidthOrig; private $newHeight; private $newHeightOrig; private $dpr = 1; const UPSCALE_DEFAULT = true; private $upscale = self::UPSCALE_DEFAULT; public $crop; public $cropOrig; private $convolve; private $convolves = array( 'lighten' => '0,0,0, 0,12,0, 0,0,0, 9, 0', 'darken' => '0,0,0, 0,6,0, 0,0,0, 9, 0', 'sharpen' => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0', 'sharpen-alt' => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0', 'emboss' => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0', 'emboss-alt' => '-2,-1,0, -1,1,1, 0,1,2, 1, 0', 'blur' => '1,1,1, 1,15,1, 1,1,1, 23, 0', 'gblur' => '1,2,1, 2,4,2, 1,2,1, 16, 0', 'edge' => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0', 'edge-alt' => '0,1,0, 1,-4,1, 0,1,0, 1, 0', 'draw' => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0', 'mean' => '1,1,1, 1,1,1, 1,1,1, 9, 0', 'motion' => '1,0,0, 0,1,0, 0,0,1, 3, 0', ); private $fillToFit; private $scale; private $rotateBefore; private $rotateAfter; private $autoRotate; private $sharpen; private $emboss; private $blur; private $offset; private $fillWidth; private $fillHeight; private $allowRemote = false; private $remoteCache; private $remotePattern = '#^https?://#'; private $useCache = true; private $fastTrackCache = null; private $remoteHostWhitelist = null; private $verboseFileName = null; private $asciiOptions = array(); const RESIZE = 1; const RESAMPLE = 2; private $copyStrategy = NULL; public $keepRatio; public $cropToFit; private $cropWidth; private $cropHeight; public $crop_x; public $crop_y; public $filters; private $attr; public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null) { $this->setSource($imageSrc, $imageFolder); $this->setTarget($saveFolder, $saveName); } public function injectDependency($property, $object) { if (!property_exists($this, $property)) { $this->raiseError("Injecting unknown property."); } $this->$property = $object; return $this; } public function setVerbose($mode = true) { $this->verbose = $mode; return $this; } public function setSaveFolder($path) { $this->saveFolder = $path; return $this; } public function useCache($use = true) { $this->useCache = $use; return $this; } public function createDummyImage($width = null, $height = null) { $this->newWidth = $this->newWidth ?: $width ?: 100; $this->newHeight = $this->newHeight ?: $height ?: 100; $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); return $this; } public function setRemoteDownload($allow, $cache, $pattern = null) { $this->allowRemote = $allow; $this->remoteCache = $cache; $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern; $this->log( "Set remote download to: " . ($this->allowRemote ? "true" : "false") . " using pattern " . $this->remotePattern ); return $this; } public function isRemoteSource($src) { $remote = preg_match($this->remotePattern, $src); $this->log("Detected remote image: " . ($remote ? "true" : "false")); return !!$remote; } public function setRemoteHostWhitelist($whitelist = null) { $this->remoteHostWhitelist = $whitelist; $this->log( "Setting remote host whitelist to: " . (is_null($whitelist) ? "null" : print_r($whitelist, 1)) ); return $this; } public function isRemoteSourceOnWhitelist($src) { if (is_null($this->remoteHostWhitelist)) { $this->log("Remote host on whitelist not configured - allowing."); return true; } $whitelist = new CWhitelist(); $hostname = parse_url($src, PHP_URL_HOST); $allow = $whitelist->check($hostname, $this->remoteHostWhitelist); $this->log( "Remote host is on whitelist: " . ($allow ? "true" : "false") ); return $allow; } private function checkFileExtension($extension) { $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp'); in_array(strtolower($extension), $valid) or $this->raiseError('Not a valid file extension.'); return $this; } private function normalizeFileExtension($extension = null) { $extension = strtolower($extension ? $extension : $this->extension); if ($extension == 'jpeg') { $extension = 'jpg'; } return $extension; } public function downloadRemoteSource($src) { if (!$this->isRemoteSourceOnWhitelist($src)) { throw new Exception("Hostname is not on whitelist for remote sources."); } $remote = new CRemoteImage(); if (!is_writable($this->remoteCache)) { $this->log("The remote cache is not writable."); } $remote->setCache($this->remoteCache); $remote->useCache($this->useCache); $src = $remote->download($src); $this->log("Remote HTTP status: " . $remote->getStatus()); $this->log("Remote item is in local cache: $src"); $this->log("Remote details on cache:" . print_r($remote->getDetails(), true)); return $src; } public function setSource($src, $dir = null) { if (!isset($src)) { $this->imageSrc = null; $this->pathToImage = null; return $this; } if ($this->allowRemote && $this->isRemoteSource($src)) { $src = $this->downloadRemoteSource($src); $dir = null; } if (!isset($dir)) { $dir = dirname($src); $src = basename($src); } $this->imageSrc = ltrim($src, '/'); $imageFolder = rtrim($dir, '/'); $this->pathToImage = $imageFolder . '/' . $this->imageSrc; return $this; } public function setTarget($src = null, $dir = null) { if (!isset($src)) { $this->cacheFileName = null; return $this; } if (isset($dir)) { $this->saveFolder = rtrim($dir, '/'); } $this->cacheFileName = $this->saveFolder . '/' . $src; $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); $this->log("The cache file name is: " . $this->cacheFileName); return $this; } public function getTarget() { return $this->cacheFileName; } public function setOptions($args) { $this->log("Set new options for processing image."); $defaults = array( 'newWidth' => null, 'newHeight' => null, 'aspectRatio' => null, 'keepRatio' => true, 'cropToFit' => false, 'fillToFit' => null, 'crop' => null, 'area' => null, 'upscale' => self::UPSCALE_DEFAULT, 'useCache' => true, 'useOriginal' => true, 'scale' => null, 'rotateBefore' => null, 'autoRotate' => false, 'bgColor' => null, 'palette' => null, 'filters' => null, 'sharpen' => null, 'emboss' => null, 'blur' => null, 'convolve' => null, 'rotateAfter' => null, 'outputFormat' => null, 'dpr' => 1, 'lossy' => null, ); if (isset($args['crop']) && !is_array($args['crop'])) { $pices = explode(',', $args['crop']); $args['crop'] = array( 'width' => $pices[0], 'height' => $pices[1], 'start_x' => $pices[2], 'start_y' => $pices[3], ); } if (isset($args['area']) && !is_array($args['area'])) { $pices = explode(',', $args['area']); $args['area'] = array( 'top' => $pices[0], 'right' => $pices[1], 'bottom' => $pices[2], 'left' => $pices[3], ); } if (isset($args['filters']) && is_array($args['filters'])) { foreach ($args['filters'] as $key => $filterStr) { $parts = explode(',', $filterStr); $filter = $this->mapFilter($parts[0]); $filter['str'] = $filterStr; for ($i=1; $i<=$filter['argc']; $i++) { if (isset($parts[$i])) { $filter["arg{$i}"] = $parts[$i]; } else { throw new Exception( 'Missing arg to filter, review how many arguments are needed at
2
+    define("CIMAGE_BUNDLE", true); $config = array( 'mode' => 'strict', ); define("CIMAGE_VERSION", "v0.7.23 (2020-05-06)"); define("CIMAGE_USER_AGENT", "CImage/" . CIMAGE_VERSION); if (!defined("IMG_WEBP")) { define("IMG_WEBP", -1); } function trace($msg) { $file = CIMAGE_DEBUG_FILE; if (!is_writable($file)) { return; } $timer = number_format((microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]), 6); $details = "{$timer}ms"; $details .= ":" . round(memory_get_peak_usage()/1024/1024, 3) . "MB"; $details .= ":" . count(get_included_files()); file_put_contents($file, "$details:$msg\n", FILE_APPEND); } function errorPage($msg, $type = 500) { global $mode; switch ($type) { case 403: $header = "403 Forbidden"; break; case 404: $header = "404 Not Found"; break; default: $header = "500 Internal Server Error"; } if ($mode == "strict") { $header = "404 Not Found"; } header("HTTP/1.0 $header"); if ($mode == "development") { die("[img.php] $msg"); } error_log("[img.php] $msg"); die("HTTP/1.0 $header"); } function get($key, $default = null) { if (is_array($key)) { foreach ($key as $val) { if (isset($_GET[$val])) { return $_GET[$val]; } } } elseif (isset($_GET[$key])) { return $_GET[$key]; } return $default; } function getDefined($key, $defined, $undefined) { return get($key) === null ? $undefined : $defined; } function getConfig($key, $default) { global $config; return isset($config[$key]) ? $config[$key] : $default; } function verbose($msg = null) { global $verbose, $verboseFile; static $log = array(); if (!($verbose || $verboseFile)) { return; } if (is_null($msg)) { return $log; } $log[] = $msg; } function checkExternalCommand($what, $enabled, $commandString) { $no = $enabled ? null : 'NOT'; $text = "Post processing $what is $no enabled.<br>"; list($command) = explode(" ", $commandString); $no = is_executable($command) ? null : 'NOT'; $text .= "The command for $what is $no an executable.<br>"; return $text; } class CHttpGet { private $request = array(); private $response = array(); public function __construct() { $this->request['header'] = array(); } public function buildUrl($baseUrl, $merge) { $parts = parse_url($baseUrl); $parts = array_merge($parts, $merge); $url = $parts['scheme']; $url .= "://"; $url .= $parts['host']; $url .= isset($parts['port']) ? ":" . $parts['port'] : "" ; $url .= $parts['path']; return $url; } public function setUrl($url) { $parts = parse_url($url); $path = ""; if (isset($parts['path'])) { $pathParts = explode('/', $parts['path']); unset($pathParts[0]); foreach ($pathParts as $value) { $path .= "/" . rawurlencode($value); } } $url = $this->buildUrl($url, array("path" => $path)); $this->request['url'] = $url; return $this; } public function setHeader($field, $value) { $this->request['header'][] = "$field: $value"; return $this; } public function parseHeader() { $rawHeaders = rtrim($this->response['headerRaw'], "\r\n"); $headerGroups = explode("\r\n\r\n", $rawHeaders); $header = explode("\r\n", end($headerGroups)); $output = array(); if ('HTTP' === substr($header[0], 0, 4)) { list($output['version'], $output['status']) = explode(' ', $header[0]); unset($header[0]); } foreach ($header as $entry) { $pos = strpos($entry, ':'); $output[trim(substr($entry, 0, $pos))] = trim(substr($entry, $pos + 1)); } $this->response['header'] = $output; return $this; } public function doGet($debug = false) { $options = array( CURLOPT_URL => $this->request['url'], CURLOPT_HEADER => 1, CURLOPT_HTTPHEADER => $this->request['header'], CURLOPT_AUTOREFERER => true, CURLOPT_RETURNTRANSFER => true, CURLINFO_HEADER_OUT => $debug, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 5, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 2, ); $ch = curl_init(); curl_setopt_array($ch, $options); $response = curl_exec($ch); if (!$response) { throw new Exception("Failed retrieving url, details follows: " . curl_error($ch)); } $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $this->response['headerRaw'] = substr($response, 0, $headerSize); $this->response['body'] = substr($response, $headerSize); $this->parseHeader(); if ($debug) { $info = curl_getinfo($ch); echo "Request header<br><pre>", var_dump($info['request_header']), "</pre>"; echo "Response header (raw)<br><pre>", var_dump($this->response['headerRaw']), "</pre>"; echo "Response header (parsed)<br><pre>", var_dump($this->response['header']), "</pre>"; } curl_close($ch); return true; } public function getStatus() { return isset($this->response['header']['status']) ? (int) $this->response['header']['status'] : null; } public function getLastModified() { return isset($this->response['header']['Last-Modified']) ? strtotime($this->response['header']['Last-Modified']) : null; } public function getContentType() { $type = isset($this->response['header']['Content-Type']) ? $this->response['header']['Content-Type'] : null; return preg_match('#[a-z]+/[a-z]+#', $type) ? $type : null; } public function getDate($default = false) { return isset($this->response['header']['Date']) ? strtotime($this->response['header']['Date']) : $default; } public function getMaxAge($default = false) { $cacheControl = isset($this->response['header']['Cache-Control']) ? $this->response['header']['Cache-Control'] : null; $maxAge = null; if ($cacheControl) { $part = explode('=', $cacheControl); $maxAge = ($part[0] == "max-age") ? (int) $part[1] : null; } if ($maxAge) { return $maxAge; } $expire = isset($this->response['header']['Expires']) ? strtotime($this->response['header']['Expires']) : null; return $expire ? $expire : $default; } public function getBody() { return $this->response['body']; } } class CRemoteImage { private $saveFolder = null; private $useCache = true; private $http; private $status; private $defaultMaxAge = 604800; private $url; private $fileName; private $fileJson; private $cache; public function getStatus() { return $this->status; } public function getDetails() { return $this->cache; } public function setCache($path) { $this->saveFolder = rtrim($path, "/") . "/"; return $this; } public function isCacheWritable() { if (!is_writable($this->saveFolder)) { throw new Exception("Cache folder is not writable for downloaded files."); } return $this; } public function useCache($use = true) { $this->useCache = $use; return $this; } public function setHeaderFields() { $cimageVersion = "CImage"; if (defined("CIMAGE_USER_AGENT")) { $cimageVersion = CIMAGE_USER_AGENT; } $this->http->setHeader("User-Agent", "$cimageVersion (PHP/". phpversion() . " cURL)"); $this->http->setHeader("Accept", "image/jpeg,image/png,image/gif"); if ($this->useCache) { $this->http->setHeader("Cache-Control", "max-age=0"); } else { $this->http->setHeader("Cache-Control", "no-cache"); $this->http->setHeader("Pragma", "no-cache"); } } public function save() { $this->cache = array(); $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $type = $this->http->getContentType(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; $this->cache['Content-Type'] = $type; $this->cache['Url'] = $this->url; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } $body = $this->http->getBody(); $img = imagecreatefromstring($body); if ($img !== false) { file_put_contents($this->fileName, $body); file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileName; } return false; } public function updateCacheDetails() { $date = $this->http->getDate(time()); $maxAge = $this->http->getMaxAge($this->defaultMaxAge); $lastModified = $this->http->getLastModified(); $this->cache['Date'] = gmdate("D, d M Y H:i:s T", $date); $this->cache['Max-Age'] = $maxAge; if ($lastModified) { $this->cache['Last-Modified'] = gmdate("D, d M Y H:i:s T", $lastModified); } file_put_contents($this->fileJson, json_encode($this->cache)); return $this->fileName; } public function download($url) { $this->http = new CHttpGet(); $this->url = $url; $this->loadCacheDetails(); if ($this->useCache) { $src = $this->getCachedSource(); if ($src) { $this->status = 1; return $src; } } $this->setHeaderFields(); $this->http->setUrl($this->url); $this->http->doGet(); $this->status = $this->http->getStatus(); if ($this->status === 200) { $this->isCacheWritable(); return $this->save(); } elseif ($this->status === 304) { $this->isCacheWritable(); return $this->updateCacheDetails(); } throw new Exception("Unknown statuscode when downloading remote image: " . $this->status); } public function loadCacheDetails() { $cacheFile = md5($this->url); $this->fileName = $this->saveFolder . $cacheFile; $this->fileJson = $this->fileName . ".json"; if (is_readable($this->fileJson)) { $this->cache = json_decode(file_get_contents($this->fileJson), true); } } public function getCachedSource() { $imageExists = is_readable($this->fileName); $date = strtotime($this->cache['Date']); $maxAge = $this->cache['Max-Age']; $now = time(); if ($imageExists && $date + $maxAge > $now) { return $this->fileName; } if ($imageExists && isset($this->cache['Last-Modified'])) { $this->http->setHeader("If-Modified-Since", $this->cache['Last-Modified']); } return false; } } class CWhitelist { private $whitelist = array(); public function set($whitelist = array()) { if (!is_array($whitelist)) { throw new Exception("Whitelist is not of a supported format."); } $this->whitelist = $whitelist; return $this; } public function check($item, $whitelist = null) { if ($whitelist !== null) { $this->set($whitelist); } if (empty($item) or empty($this->whitelist)) { return false; } foreach ($this->whitelist as $regexp) { if (preg_match("#$regexp#", $item)) { return true; } } return false; } } class CAsciiArt { private $characterSet = array( 'one' => "#0XT|:,.' ", 'two' => "@%#*+=-:. ", 'three' => "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. " ); private $characters = null; private $charCount = null; private $scale = null; private $luminanceStrategy = null; public function __construct() { $this->setOptions(); } public function addCharacterSet($key, $value) { $this->characterSet[$key] = $value; return $this; } public function setOptions($options = array()) { $default = array( "characterSet" => 'two', "scale" => 14, "luminanceStrategy" => 3, "customCharacterSet" => null, ); $default = array_merge($default, $options); if (!is_null($default['customCharacterSet'])) { $this->addCharacterSet('custom', $default['customCharacterSet']); $default['characterSet'] = 'custom'; } $this->scale = $default['scale']; $this->characters = $this->characterSet[$default['characterSet']]; $this->charCount = strlen($this->characters); $this->luminanceStrategy = $default['luminanceStrategy']; return $this; } public function createFromFile($filename) { $img = imagecreatefromstring(file_get_contents($filename)); list($width, $height) = getimagesize($filename); $ascii = null; $incY = $this->scale; $incX = $this->scale / 2; for ($y = 0; $y < $height - 1; $y += $incY) { for ($x = 0; $x < $width - 1; $x += $incX) { $toX = min($x + $this->scale / 2, $width - 1); $toY = min($y + $this->scale, $height - 1); $luminance = $this->luminanceAreaAverage($img, $x, $y, $toX, $toY); $ascii .= $this->luminance2character($luminance); } $ascii .= PHP_EOL; } return $ascii; } public function luminanceAreaAverage($img, $x1, $y1, $x2, $y2) { $numPixels = ($x2 - $x1 + 1) * ($y2 - $y1 + 1); $luminance = 0; for ($x = $x1; $x <= $x2; $x++) { for ($y = $y1; $y <= $y2; $y++) { $rgb = imagecolorat($img, $x, $y); $red = (($rgb >> 16) & 0xFF); $green = (($rgb >> 8) & 0xFF); $blue = ($rgb & 0xFF); $luminance += $this->getLuminance($red, $green, $blue); } } return $luminance / $numPixels; } public function getLuminance($red, $green, $blue) { switch ($this->luminanceStrategy) { case 1: $luminance = ($red * 0.2126 + $green * 0.7152 + $blue * 0.0722) / 255; break; case 2: $luminance = ($red * 0.299 + $green * 0.587 + $blue * 0.114) / 255; break; case 3: $luminance = sqrt(0.299 * pow($red, 2) + 0.587 * pow($green, 2) + 0.114 * pow($blue, 2)) / 255; break; case 0: default: $luminance = ($red + $green + $blue) / (255 * 3); } return $luminance; } public function luminance2character($luminance) { $position = (int) round($luminance * ($this->charCount - 1)); $char = $this->characters[$position]; return $char; } } class CImage { const PNG_GREYSCALE = 0; const PNG_RGB = 2; const PNG_RGB_PALETTE = 3; const PNG_GREYSCALE_ALPHA = 4; const PNG_RGB_ALPHA = 6; const JPEG_QUALITY_DEFAULT = 60; private $quality; private $useQuality = false; const PNG_COMPRESSION_DEFAULT = -1; private $compress; private $useCompress = false; private $HTTPHeader = array(); private $bgColorDefault = array( 'red' => 0, 'green' => 0, 'blue' => 0, 'alpha' => null, ); private $bgColor; private $saveFolder; private $image; private $imageSrc; private $pathToImage; private $fileType; private $extension; private $outputFormat = null; private $lossy = null; private $verbose = false; private $log = array(); private $palette; private $cacheFileName; private $saveAs; private $pngLossy; private $pngLossyCmd; private $pngFilter; private $pngFilterCmd; private $pngDeflate; private $pngDeflateCmd; private $jpegOptimize; private $jpegOptimizeCmd; private $width; private $height; private $newWidth; private $newWidthOrig; private $newHeight; private $newHeightOrig; private $dpr = 1; const UPSCALE_DEFAULT = true; private $upscale = self::UPSCALE_DEFAULT; public $crop; public $cropOrig; private $convolve; private $convolves = array( 'lighten' => '0,0,0, 0,12,0, 0,0,0, 9, 0', 'darken' => '0,0,0, 0,6,0, 0,0,0, 9, 0', 'sharpen' => '-1,-1,-1, -1,16,-1, -1,-1,-1, 8, 0', 'sharpen-alt' => '0,-1,0, -1,5,-1, 0,-1,0, 1, 0', 'emboss' => '1,1,-1, 1,3,-1, 1,-1,-1, 3, 0', 'emboss-alt' => '-2,-1,0, -1,1,1, 0,1,2, 1, 0', 'blur' => '1,1,1, 1,15,1, 1,1,1, 23, 0', 'gblur' => '1,2,1, 2,4,2, 1,2,1, 16, 0', 'edge' => '-1,-1,-1, -1,8,-1, -1,-1,-1, 9, 0', 'edge-alt' => '0,1,0, 1,-4,1, 0,1,0, 1, 0', 'draw' => '0,-1,0, -1,5,-1, 0,-1,0, 0, 0', 'mean' => '1,1,1, 1,1,1, 1,1,1, 9, 0', 'motion' => '1,0,0, 0,1,0, 0,0,1, 3, 0', ); private $fillToFit; private $scale; private $rotateBefore; private $rotateAfter; private $autoRotate; private $sharpen; private $emboss; private $blur; private $offset; private $fillWidth; private $fillHeight; private $allowRemote = false; private $remoteCache; private $remotePattern = '#^https?://#'; private $useCache = true; private $fastTrackCache = null; private $remoteHostWhitelist = null; private $verboseFileName = null; private $asciiOptions = array(); const RESIZE = 1; const RESAMPLE = 2; private $copyStrategy = NULL; public $keepRatio; public $cropToFit; private $cropWidth; private $cropHeight; public $crop_x; public $crop_y; public $filters; private $attr; public function __construct($imageSrc = null, $imageFolder = null, $saveFolder = null, $saveName = null) { $this->setSource($imageSrc, $imageFolder); $this->setTarget($saveFolder, $saveName); } public function injectDependency($property, $object) { if (!property_exists($this, $property)) { $this->raiseError("Injecting unknown property."); } $this->$property = $object; return $this; } public function setVerbose($mode = true) { $this->verbose = $mode; return $this; } public function setSaveFolder($path) { $this->saveFolder = $path; return $this; } public function useCache($use = true) { $this->useCache = $use; return $this; } public function createDummyImage($width = null, $height = null) { $this->newWidth = $this->newWidth ?: $width ?: 100; $this->newHeight = $this->newHeight ?: $height ?: 100; $this->image = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); return $this; } public function setRemoteDownload($allow, $cache, $pattern = null) { $this->allowRemote = $allow; $this->remoteCache = $cache; $this->remotePattern = is_null($pattern) ? $this->remotePattern : $pattern; $this->log( "Set remote download to: " . ($this->allowRemote ? "true" : "false") . " using pattern " . $this->remotePattern ); return $this; } public function isRemoteSource($src) { $remote = preg_match($this->remotePattern, $src); $this->log("Detected remote image: " . ($remote ? "true" : "false")); return !!$remote; } public function setRemoteHostWhitelist($whitelist = null) { $this->remoteHostWhitelist = $whitelist; $this->log( "Setting remote host whitelist to: " . (is_null($whitelist) ? "null" : print_r($whitelist, 1)) ); return $this; } public function isRemoteSourceOnWhitelist($src) { if (is_null($this->remoteHostWhitelist)) { $this->log("Remote host on whitelist not configured - allowing."); return true; } $whitelist = new CWhitelist(); $hostname = parse_url($src, PHP_URL_HOST); $allow = $whitelist->check($hostname, $this->remoteHostWhitelist); $this->log( "Remote host is on whitelist: " . ($allow ? "true" : "false") ); return $allow; } private function checkFileExtension($extension) { $valid = array('jpg', 'jpeg', 'png', 'gif', 'webp'); in_array(strtolower($extension), $valid) or $this->raiseError('Not a valid file extension.'); return $this; } private function normalizeFileExtension($extension = null) { $extension = strtolower($extension ? $extension : $this->extension); if ($extension == 'jpeg') { $extension = 'jpg'; } return $extension; } public function downloadRemoteSource($src) { if (!$this->isRemoteSourceOnWhitelist($src)) { throw new Exception("Hostname is not on whitelist for remote sources."); } $remote = new CRemoteImage(); if (!is_writable($this->remoteCache)) { $this->log("The remote cache is not writable."); } $remote->setCache($this->remoteCache); $remote->useCache($this->useCache); $src = $remote->download($src); $this->log("Remote HTTP status: " . $remote->getStatus()); $this->log("Remote item is in local cache: $src"); $this->log("Remote details on cache:" . print_r($remote->getDetails(), true)); return $src; } public function setSource($src, $dir = null) { if (!isset($src)) { $this->imageSrc = null; $this->pathToImage = null; return $this; } if ($this->allowRemote && $this->isRemoteSource($src)) { $src = $this->downloadRemoteSource($src); $dir = null; } if (!isset($dir)) { $dir = dirname($src); $src = basename($src); } $this->imageSrc = ltrim($src, '/'); $imageFolder = rtrim($dir, '/'); $this->pathToImage = $imageFolder . '/' . $this->imageSrc; return $this; } public function setTarget($src = null, $dir = null) { if (!isset($src)) { $this->cacheFileName = null; return $this; } if (isset($dir)) { $this->saveFolder = rtrim($dir, '/'); } $this->cacheFileName = $this->saveFolder . '/' . $src; $this->cacheFileName = preg_replace('/^a-zA-Z0-9\.-_/', '', $this->cacheFileName); $this->log("The cache file name is: " . $this->cacheFileName); return $this; } public function getTarget() { return $this->cacheFileName; } public function setOptions($args) { $this->log("Set new options for processing image."); $defaults = array( 'newWidth' => null, 'newHeight' => null, 'aspectRatio' => null, 'keepRatio' => true, 'cropToFit' => false, 'fillToFit' => null, 'crop' => null, 'area' => null, 'upscale' => self::UPSCALE_DEFAULT, 'useCache' => true, 'useOriginal' => true, 'scale' => null, 'rotateBefore' => null, 'autoRotate' => false, 'bgColor' => null, 'palette' => null, 'filters' => null, 'sharpen' => null, 'emboss' => null, 'blur' => null, 'convolve' => null, 'rotateAfter' => null, 'outputFormat' => null, 'dpr' => 1, 'lossy' => null, ); if (isset($args['crop']) && !is_array($args['crop'])) { $pices = explode(',', $args['crop']); $args['crop'] = array( 'width' => $pices[0], 'height' => $pices[1], 'start_x' => $pices[2], 'start_y' => $pices[3], ); } if (isset($args['area']) && !is_array($args['area'])) { $pices = explode(',', $args['area']); $args['area'] = array( 'top' => $pices[0], 'right' => $pices[1], 'bottom' => $pices[2], 'left' => $pices[3], ); } if (isset($args['filters']) && is_array($args['filters'])) { foreach ($args['filters'] as $key => $filterStr) { $parts = explode(',', $filterStr); $filter = $this->mapFilter($parts[0]); $filter['str'] = $filterStr; for ($i=1; $i<=$filter['argc']; $i++) { if (isset($parts[$i])) { $filter["arg{$i}"] = $parts[$i]; } else { throw new Exception( 'Missing arg to filter, review how many arguments are needed at
3 3
                             http://php.net/manual/en/function.imagefilter.php' ); } } $args['filters'][$key] = $filter; } } $args = array_merge($defaults, $args); foreach ($defaults as $key => $val) { $this->{$key} = $args[$key]; } if ($this->bgColor) { $this->setDefaultBackgroundColor($this->bgColor); } $this->newWidthOrig = $this->newWidth; $this->newHeightOrig = $this->newHeight; $this->cropOrig = $this->crop; return $this; } private function mapFilter($name) { $map = array( 'negate' => array('id'=>0, 'argc'=>0, 'type'=>IMG_FILTER_NEGATE), 'grayscale' => array('id'=>1, 'argc'=>0, 'type'=>IMG_FILTER_GRAYSCALE), 'brightness' => array('id'=>2, 'argc'=>1, 'type'=>IMG_FILTER_BRIGHTNESS), 'contrast' => array('id'=>3, 'argc'=>1, 'type'=>IMG_FILTER_CONTRAST), 'colorize' => array('id'=>4, 'argc'=>4, 'type'=>IMG_FILTER_COLORIZE), 'edgedetect' => array('id'=>5, 'argc'=>0, 'type'=>IMG_FILTER_EDGEDETECT), 'emboss' => array('id'=>6, 'argc'=>0, 'type'=>IMG_FILTER_EMBOSS), 'gaussian_blur' => array('id'=>7, 'argc'=>0, 'type'=>IMG_FILTER_GAUSSIAN_BLUR), 'selective_blur' => array('id'=>8, 'argc'=>0, 'type'=>IMG_FILTER_SELECTIVE_BLUR), 'mean_removal' => array('id'=>9, 'argc'=>0, 'type'=>IMG_FILTER_MEAN_REMOVAL), 'smooth' => array('id'=>10, 'argc'=>1, 'type'=>IMG_FILTER_SMOOTH), 'pixelate' => array('id'=>11, 'argc'=>2, 'type'=>IMG_FILTER_PIXELATE), ); if (isset($map[$name])) { return $map[$name]; } else { throw new Exception('No such filter.'); } } public function loadImageDetails($file = null) { $file = $file ? $file : $this->pathToImage; is_readable($file) or $this->raiseError('Image file does not exist.'); $info = list($this->width, $this->height, $this->fileType) = getimagesize($file); if (empty($info)) { $this->fileType = false; if (function_exists("exif_imagetype")) { $this->fileType = exif_imagetype($file); if ($this->fileType === false) { if (function_exists("imagecreatefromwebp")) { $webp = imagecreatefromwebp($file); if ($webp !== false) { $this->width = imagesx($webp); $this->height = imagesy($webp); $this->fileType = IMG_WEBP; } } } } } if (!$this->fileType) { throw new Exception("Loading image details, the file doesn't seem to be a valid image."); } if ($this->verbose) { $this->log("Loading image details for: {$file}"); $this->log(" Image width x height (type): {$this->width} x {$this->height} ({$this->fileType})."); $this->log(" Image filesize: " . filesize($file) . " bytes."); $this->log(" Image mimetype: " . $this->getMimeType()); } return $this; } protected function getMimeType() { if ($this->fileType === IMG_WEBP) { return "image/webp"; } return image_type_to_mime_type($this->fileType); } public function initDimensions() { $this->log("Init dimension (before) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); if ($this->newWidth && $this->newWidth[strlen($this->newWidth)-1] == '%') { $this->newWidth = $this->width * substr($this->newWidth, 0, -1) / 100; $this->log("Setting new width based on % to {$this->newWidth}"); } if ($this->newHeight && $this->newHeight[strlen($this->newHeight)-1] == '%') { $this->newHeight = $this->height * substr($this->newHeight, 0, -1) / 100; $this->log("Setting new height based on % to {$this->newHeight}"); } is_null($this->aspectRatio) or is_numeric($this->aspectRatio) or $this->raiseError('Aspect ratio out of range'); if ($this->aspectRatio && is_null($this->newWidth) && is_null($this->newHeight)) { if ($this->aspectRatio >= 1) { $this->newWidth = $this->width; $this->newHeight = $this->width / $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (>=1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } else { $this->newHeight = $this->height; $this->newWidth = $this->height * $this->aspectRatio; $this->log("Setting new width & height based on width & aspect ratio (<1) to (w x h) {$this->newWidth} x {$this->newHeight}"); } } elseif ($this->aspectRatio && is_null($this->newWidth)) { $this->newWidth = $this->newHeight * $this->aspectRatio; $this->log("Setting new width based on aspect ratio to {$this->newWidth}"); } elseif ($this->aspectRatio && is_null($this->newHeight)) { $this->newHeight = $this->newWidth / $this->aspectRatio; $this->log("Setting new height based on aspect ratio to {$this->newHeight}"); } if ($this->dpr != 1) { if (!is_null($this->newWidth)) { $this->newWidth = round($this->newWidth * $this->dpr); $this->log("Setting new width based on dpr={$this->dpr} - w={$this->newWidth}"); } if (!is_null($this->newHeight)) { $this->newHeight = round($this->newHeight * $this->dpr); $this->log("Setting new height based on dpr={$this->dpr} - h={$this->newHeight}"); } } is_null($this->newWidth) or is_numeric($this->newWidth) or $this->raiseError('Width not numeric'); is_null($this->newHeight) or is_numeric($this->newHeight) or $this->raiseError('Height not numeric'); $this->log("Init dimension (after) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); return $this; } public function calculateNewWidthAndHeight() { $this->log("Calculate new width and height."); $this->log("Original width x height is {$this->width} x {$this->height}."); $this->log("Target dimension (before calculating) newWidth x newHeight is {$this->newWidth} x {$this->newHeight}."); if (isset($this->area)) { $this->offset['top'] = round($this->area['top'] / 100 * $this->height); $this->offset['right'] = round($this->area['right'] / 100 * $this->width); $this->offset['bottom'] = round($this->area['bottom'] / 100 * $this->height); $this->offset['left'] = round($this->area['left'] / 100 * $this->width); $this->offset['width'] = $this->width - $this->offset['left'] - $this->offset['right']; $this->offset['height'] = $this->height - $this->offset['top'] - $this->offset['bottom']; $this->width = $this->offset['width']; $this->height = $this->offset['height']; $this->log("The offset for the area to use is top {$this->area['top']}%, right {$this->area['right']}%, bottom {$this->area['bottom']}%, left {$this->area['left']}%."); $this->log("The offset for the area to use is top {$this->offset['top']}px, right {$this->offset['right']}px, bottom {$this->offset['bottom']}px, left {$this->offset['left']}px, width {$this->offset['width']}px, height {$this->offset['height']}px."); } $width = $this->width; $height = $this->height; if ($this->crop) { $width = $this->crop['width'] = $this->crop['width'] <= 0 ? $this->width + $this->crop['width'] : $this->crop['width']; $height = $this->crop['height'] = $this->crop['height'] <= 0 ? $this->height + $this->crop['height'] : $this->crop['height']; if ($this->crop['start_x'] == 'left') { $this->crop['start_x'] = 0; } elseif ($this->crop['start_x'] == 'right') { $this->crop['start_x'] = $this->width - $width; } elseif ($this->crop['start_x'] == 'center') { $this->crop['start_x'] = round($this->width / 2) - round($width / 2); } if ($this->crop['start_y'] == 'top') { $this->crop['start_y'] = 0; } elseif ($this->crop['start_y'] == 'bottom') { $this->crop['start_y'] = $this->height - $height; } elseif ($this->crop['start_y'] == 'center') { $this->crop['start_y'] = round($this->height / 2) - round($height / 2); } $this->log("Crop area is width {$width}px, height {$height}px, start_x {$this->crop['start_x']}px, start_y {$this->crop['start_y']}px."); } if ($this->keepRatio) { $this->log("Keep aspect ratio."); if (($this->cropToFit || $this->fillToFit) && isset($this->newWidth) && isset($this->newHeight)) { $this->log("Use newWidth and newHeigh as width/height, image should fit in box."); } elseif (isset($this->newWidth) && isset($this->newHeight)) { $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; $ratio = ($ratioWidth > $ratioHeight) ? $ratioWidth : $ratioHeight; $this->newWidth = round($width / $ratio); $this->newHeight = round($height / $ratio); $this->log("New width and height was set."); } elseif (isset($this->newWidth)) { $factor = (float)$this->newWidth / (float)$width; $this->newHeight = round($factor * $height); $this->log("New width was set."); } elseif (isset($this->newHeight)) { $factor = (float)$this->newHeight / (float)$height; $this->newWidth = round($factor * $width); $this->log("New height was set."); } else { $this->newWidth = $width; $this->newHeight = $height; } if ($this->cropToFit || $this->fillToFit) { $ratioWidth = $width / $this->newWidth; $ratioHeight = $height / $this->newHeight; if ($this->cropToFit) { $this->log("Crop to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioWidth : $ratioHeight; $this->cropWidth = round($width / $ratio); $this->cropHeight = round($height / $ratio); $this->log("Crop width, height, ratio: $this->cropWidth x $this->cropHeight ($ratio)."); } elseif ($this->fillToFit) { $this->log("Fill to fit."); $ratio = ($ratioWidth < $ratioHeight) ? $ratioHeight : $ratioWidth; $this->fillWidth = round($width / $ratio); $this->fillHeight = round($height / $ratio); $this->log("Fill width, height, ratio: $this->fillWidth x $this->fillHeight ($ratio)."); } } } if ($this->crop) { $this->log("Crop."); $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->crop['width']); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->crop['height']); } $this->newWidth = round(isset($this->newWidth) ? $this->newWidth : $this->width); $this->newHeight = round(isset($this->newHeight) ? $this->newHeight : $this->height); $this->log("Calculated new width x height as {$this->newWidth} x {$this->newHeight}."); return $this; } public function reCalculateDimensions() { $this->log("Re-calculate image dimensions, newWidth x newHeigh was: " . $this->newWidth . " x " . $this->newHeight); $this->newWidth = $this->newWidthOrig; $this->newHeight = $this->newHeightOrig; $this->crop = $this->cropOrig; $this->initDimensions() ->calculateNewWidthAndHeight(); return $this; } public function setSaveAsExtension($saveAs = null) { if (isset($saveAs)) { $saveAs = strtolower($saveAs); $this->checkFileExtension($saveAs); $this->saveAs = $saveAs; $this->extension = $saveAs; } $this->log("Prepare to save image as: " . $this->extension); return $this; } public function setJpegQuality($quality = null) { if ($quality) { $this->useQuality = true; } $this->quality = isset($quality) ? $quality : self::JPEG_QUALITY_DEFAULT; (is_numeric($this->quality) and $this->quality > 0 and $this->quality <= 100) or $this->raiseError('Quality not in range.'); $this->log("Setting JPEG quality to {$this->quality}."); return $this; } public function setPngCompression($compress = null) { if ($compress) { $this->useCompress = true; } $this->compress = isset($compress) ? $compress : self::PNG_COMPRESSION_DEFAULT; (is_numeric($this->compress) and $this->compress >= -1 and $this->compress <= 9) or $this->raiseError('Quality not in range.'); $this->log("Setting PNG compression level to {$this->compress}."); return $this; } public function useOriginalIfPossible($useOrig = true) { if ($useOrig && ($this->newWidth == $this->width) && ($this->newHeight == $this->height) && !$this->area && !$this->crop && !$this->cropToFit && !$this->fillToFit && !$this->filters && !$this->sharpen && !$this->emboss && !$this->blur && !$this->convolve && !$this->palette && !$this->useQuality && !$this->useCompress && !$this->saveAs && !$this->rotateBefore && !$this->rotateAfter && !$this->autoRotate && !$this->bgColor && ($this->upscale === self::UPSCALE_DEFAULT) && !$this->lossy ) { $this->log("Using original image."); $this->output($this->pathToImage); } return $this; } public function generateFilename($base = null, $useSubdir = true, $prefix = null) { $filename = basename($this->pathToImage); $cropToFit = $this->cropToFit ? '_cf' : null; $fillToFit = $this->fillToFit ? '_ff' : null; $crop_x = $this->crop_x ? "_x{$this->crop_x}" : null; $crop_y = $this->crop_y ? "_y{$this->crop_y}" : null; $scale = $this->scale ? "_s{$this->scale}" : null; $bgColor = $this->bgColor ? "_bgc{$this->bgColor}" : null; $quality = $this->quality ? "_q{$this->quality}" : null; $compress = $this->compress ? "_co{$this->compress}" : null; $rotateBefore = $this->rotateBefore ? "_rb{$this->rotateBefore}" : null; $rotateAfter = $this->rotateAfter ? "_ra{$this->rotateAfter}" : null; $lossy = $this->lossy ? "_l" : null; $saveAs = $this->normalizeFileExtension(); $saveAs = $saveAs ? "_$saveAs" : null; $copyStrat = null; if ($this->copyStrategy === self::RESIZE) { $copyStrat = "_rs"; } $width = $this->newWidth ? '_' . $this->newWidth : null; $height = $this->newHeight ? '_' . $this->newHeight : null; $offset = isset($this->offset) ? '_o' . $this->offset['top'] . '-' . $this->offset['right'] . '-' . $this->offset['bottom'] . '-' . $this->offset['left'] : null; $crop = $this->crop ? '_c' . $this->crop['width'] . '-' . $this->crop['height'] . '-' . $this->crop['start_x'] . '-' . $this->crop['start_y'] : null; $filters = null; if (isset($this->filters)) { foreach ($this->filters as $filter) { if (is_array($filter)) { $filters .= "_f{$filter['id']}"; for ($i=1; $i<=$filter['argc']; $i++) { $filters .= "-".$filter["arg{$i}"]; } } } } $sharpen = $this->sharpen ? 's' : null; $emboss = $this->emboss ? 'e' : null; $blur = $this->blur ? 'b' : null; $palette = $this->palette ? 'p' : null; $autoRotate = $this->autoRotate ? 'ar' : null; $optimize = $this->jpegOptimize ? 'o' : null; $optimize .= $this->pngFilter ? 'f' : null; $optimize .= $this->pngDeflate ? 'd' : null; $convolve = null; if ($this->convolve) { $convolve = '_conv' . preg_replace('/[^a-zA-Z0-9]/', '', $this->convolve); } $upscale = null; if ($this->upscale !== self::UPSCALE_DEFAULT) { $upscale = '_nu'; } $subdir = null; if ($useSubdir === true) { $subdir = str_replace('/', '-', dirname($this->imageSrc)); $subdir = ($subdir == '.') ? '_.' : $subdir; $subdir .= '_'; } $file = $prefix . $subdir . $filename . $width . $height . $offset . $crop . $cropToFit . $fillToFit . $crop_x . $crop_y . $upscale . $quality . $filters . $sharpen . $emboss . $blur . $palette . $optimize . $compress . $scale . $rotateBefore . $rotateAfter . $autoRotate . $bgColor . $convolve . $copyStrat . $lossy . $saveAs; return $this->setTarget($file, $base); } public function useCacheIfPossible($useCache = true) { if ($useCache && is_readable($this->cacheFileName)) { $fileTime = filemtime($this->pathToImage); $cacheTime = filemtime($this->cacheFileName); if ($fileTime <= $cacheTime) { if ($this->useCache) { if ($this->verbose) { $this->log("Use cached file."); $this->log("Cached image filesize: " . filesize($this->cacheFileName) . " bytes."); } $this->output($this->cacheFileName, $this->outputFormat); } else { $this->log("Cache is valid but ignoring it by intention."); } } else { $this->log("Original file is modified, ignoring cache."); } } else { $this->log("Cachefile does not exists or ignoring it."); } return $this; } public function load($src = null, $dir = null) { if (isset($src)) { $this->setSource($src, $dir); } $this->loadImageDetails(); if ($this->fileType === IMG_WEBP) { $this->image = imagecreatefromwebp($this->pathToImage); } else { $imageAsString = file_get_contents($this->pathToImage); $this->image = imagecreatefromstring($imageAsString); } if ($this->image === false) { throw new Exception("Could not load image."); } if ($this->verbose) { $this->log("### Image successfully loaded from file."); $this->log(" imageistruecolor() : " . (imageistruecolor($this->image) ? 'true' : 'false')); $this->log(" imagecolorstotal() : " . imagecolorstotal($this->image)); $this->log(" Number of colors in image = " . $this->colorsTotal($this->image)); $index = imagecolortransparent($this->image); $this->log(" Detected transparent color = " . ($index >= 0 ? implode(", ", imagecolorsforindex($this->image, $index)) : "NONE") . " at index = $index"); } return $this; } public function getPngType($filename = null) { $filename = $filename ? $filename : $this->pathToImage; $pngType = ord(file_get_contents($filename, false, null, 25, 1)); if ($this->verbose) { $this->log("Checking png type of: " . $filename); $this->log($this->getPngTypeAsString($pngType)); } return $pngType; } private function getPngTypeAsString($pngType = null, $filename = null) { if ($filename || !$pngType) { $pngType = $this->getPngType($filename); } $index = imagecolortransparent($this->image); $transparent = null; if ($index != -1) { $transparent = " (transparent)"; } switch ($pngType) { case self::PNG_GREYSCALE: $text = "PNG is type 0, Greyscale$transparent"; break; case self::PNG_RGB: $text = "PNG is type 2, RGB$transparent"; break; case self::PNG_RGB_PALETTE: $text = "PNG is type 3, RGB with palette$transparent"; break; case self::PNG_GREYSCALE_ALPHA: $text = "PNG is type 4, Greyscale with alpha channel"; break; case self::PNG_RGB_ALPHA: $text = "PNG is type 6, RGB with alpha channel (PNG 32-bit)"; break; default: $text = "PNG is UNKNOWN type, is it really a PNG image?"; } return $text; } private function colorsTotal($im) { if (imageistruecolor($im)) { $this->log("Colors as true color."); $h = imagesy($im); $w = imagesx($im); $c = array(); for ($x=0; $x < $w; $x++) { for ($y=0; $y < $h; $y++) { @$c['c'.imagecolorat($im, $x, $y)]++; } } return count($c); } else { $this->log("Colors as palette."); return imagecolorstotal($im); } } public function preResize() { $this->log("### Pre-process before resizing"); if ($this->rotateBefore) { $this->log("Rotating image."); $this->rotate($this->rotateBefore, $this->bgColor) ->reCalculateDimensions(); } if ($this->autoRotate) { $this->log("Auto rotating image."); $this->rotateExif() ->reCalculateDimensions(); } if (isset($this->scale)) { $this->log("Scale by {$this->scale}%"); $newWidth = $this->width * $this->scale / 100; $newHeight = $this->height * $this->scale / 100; $img = $this->CreateImageKeepTransparency($newWidth, $newHeight); imagecopyresampled($img, $this->image, 0, 0, 0, 0, $newWidth, $newHeight, $this->width, $this->height); $this->image = $img; $this->width = $newWidth; $this->height = $newHeight; } return $this; } public function setCopyResizeStrategy($strategy) { $this->copyStrategy = $strategy; return $this; } public function imageCopyResampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h) { if($this->copyStrategy == self::RESIZE) { $this->log("Copy by resize"); imagecopyresized($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); } else { $this->log("Copy by resample"); imagecopyresampled($dst_image, $src_image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h); } } public function resize() { $this->log("### Starting to Resize()"); $this->log("Upscale = '$this->upscale'"); if (isset($this->offset)) { $this->log("Offset for area to use, cropping it width={$this->offset['width']}, height={$this->offset['height']}, start_x={$this->offset['left']}, start_y={$this->offset['top']}"); $img = $this->CreateImageKeepTransparency($this->offset['width'], $this->offset['height']); imagecopy($img, $this->image, 0, 0, $this->offset['left'], $this->offset['top'], $this->offset['width'], $this->offset['height']); $this->image = $img; $this->width = $this->offset['width']; $this->height = $this->offset['height']; } if ($this->crop) { $this->log("Cropping area width={$this->crop['width']}, height={$this->crop['height']}, start_x={$this->crop['start_x']}, start_y={$this->crop['start_y']}"); $img = $this->CreateImageKeepTransparency($this->crop['width'], $this->crop['height']); imagecopy($img, $this->image, 0, 0, $this->crop['start_x'], $this->crop['start_y'], $this->crop['width'], $this->crop['height']); $this->image = $img; $this->width = $this->crop['width']; $this->height = $this->crop['height']; } if (!$this->upscale) { } if ($this->cropToFit) { $this->log("Resizing using strategy - Crop to fit"); if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight)) { $this->log("Resizing - smaller image, do not upscale."); $posX = 0; $posY = 0; $cropX = 0; $cropY = 0; if ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); } if ($this->newWidth < $this->width) { $cropX = round(($this->width/2) - ($this->newWidth/2)); } if ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); } if ($this->newHeight < $this->height) { $cropY = round(($this->height/2) - ($this->newHeight/2)); } $this->log(" cwidth: $this->cropWidth"); $this->log(" cheight: $this->cropHeight"); $this->log(" nwidth: $this->newWidth"); $this->log(" nheight: $this->newHeight"); $this->log(" width: $this->width"); $this->log(" height: $this->height"); $this->log(" posX: $posX"); $this->log(" posY: $posY"); $this->log(" cropX: $cropX"); $this->log(" cropY: $cropY"); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); } else { $cropX = round(($this->cropWidth/2) - ($this->newWidth/2)); $cropY = round(($this->cropHeight/2) - ($this->newHeight/2)); $imgPreCrop = $this->CreateImageKeepTransparency($this->cropWidth, $this->cropHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imgPreCrop, $this->image, 0, 0, 0, 0, $this->cropWidth, $this->cropHeight, $this->width, $this->height); imagecopy($imageResized, $imgPreCrop, 0, 0, $cropX, $cropY, $this->newWidth, $this->newHeight); } $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } elseif ($this->fillToFit) { $this->log("Resizing using strategy - Fill to fit"); $posX = 0; $posY = 0; $ratioOrig = $this->width / $this->height; $ratioNew = $this->newWidth / $this->newHeight; if ($ratioOrig < $ratioNew) { $posX = round(($this->newWidth - $this->fillWidth) / 2); } else { $posY = round(($this->newHeight - $this->fillHeight) / 2); } if (!$this->upscale && ($this->width < $this->newWidth && $this->height < $this->newHeight) ) { $this->log("Resizing - smaller image, do not upscale."); $posX = round(($this->newWidth - $this->width) / 2); $posY = round(($this->newHeight - $this->height) / 2); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, 0, 0, $this->width, $this->height); } else { $imgPreFill = $this->CreateImageKeepTransparency($this->fillWidth, $this->fillHeight); $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imgPreFill, $this->image, 0, 0, 0, 0, $this->fillWidth, $this->fillHeight, $this->width, $this->height); imagecopy($imageResized, $imgPreFill, $posX, $posY, 0, 0, $this->fillWidth, $this->fillHeight); } $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } elseif (!($this->newWidth == $this->width && $this->newHeight == $this->height)) { $this->log("Resizing, new height and/or width"); if (!$this->upscale && ($this->width < $this->newWidth || $this->height < $this->newHeight) ) { $this->log("Resizing - smaller image, do not upscale."); if (!$this->keepRatio) { $this->log("Resizing - stretch to fit selected."); $posX = 0; $posY = 0; $cropX = 0; $cropY = 0; if ($this->newWidth > $this->width && $this->newHeight > $this->height) { $posX = round(($this->newWidth - $this->width) / 2); $posY = round(($this->newHeight - $this->height) / 2); } elseif ($this->newWidth > $this->width) { $posX = round(($this->newWidth - $this->width) / 2); $cropY = round(($this->height - $this->newHeight) / 2); } elseif ($this->newHeight > $this->height) { $posY = round(($this->newHeight - $this->height) / 2); $cropX = round(($this->width - $this->newWidth) / 2); } $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); imagecopy($imageResized, $this->image, $posX, $posY, $cropX, $cropY, $this->width, $this->height); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } else { $imageResized = $this->CreateImageKeepTransparency($this->newWidth, $this->newHeight); $this->imageCopyResampled($imageResized, $this->image, 0, 0, 0, 0, $this->newWidth, $this->newHeight, $this->width, $this->height); $this->image = $imageResized; $this->width = $this->newWidth; $this->height = $this->newHeight; } } return $this; } public function postResize() { $this->log("### Post-process after resizing"); if ($this->rotateAfter) { $this->log("Rotating image."); $this->rotate($this->rotateAfter, $this->bgColor); } if (isset($this->filters) && is_array($this->filters)) { foreach ($this->filters as $filter) { $this->log("Applying filter {$filter['type']}."); switch ($filter['argc']) { case 0: imagefilter($this->image, $filter['type']); break; case 1: imagefilter($this->image, $filter['type'], $filter['arg1']); break; case 2: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2']); break; case 3: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3']); break; case 4: imagefilter($this->image, $filter['type'], $filter['arg1'], $filter['arg2'], $filter['arg3'], $filter['arg4']); break; } } } if ($this->palette) { $this->log("Converting to palette image."); $this->trueColorToPalette(); } if ($this->blur) { $this->log("Blur."); $this->blurImage(); } if ($this->emboss) { $this->log("Emboss."); $this->embossImage(); } if ($this->sharpen) { $this->log("Sharpen."); $this->sharpenImage(); } if ($this->convolve) { $this->imageConvolution(); } return $this; } public function rotate($angle, $bgColor) { $this->log("Rotate image " . $angle . " degrees with filler color."); $color = $this->getBackgroundColor(); $this->image = imagerotate($this->image, $angle, $color); $this->width = imagesx($this->image); $this->height = imagesy($this->image); $this->log("New image dimension width x height: " . $this->width . " x " . $this->height); return $this; } public function rotateExif() { if (!in_array($this->fileType, array(IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM))) { $this->log("Autorotate ignored, EXIF not supported by this filetype."); return $this; } $exif = exif_read_data($this->pathToImage); if (!empty($exif['Orientation'])) { switch ($exif['Orientation']) { case 3: $this->log("Autorotate 180."); $this->rotate(180, $this->bgColor); break; case 6: $this->log("Autorotate -90."); $this->rotate(-90, $this->bgColor); break; case 8: $this->log("Autorotate 90."); $this->rotate(90, $this->bgColor); break; default: $this->log("Autorotate ignored, unknown value as orientation."); } } else { $this->log("Autorotate ignored, no orientation in EXIF."); } return $this; } public function trueColorToPalette() { $img = imagecreatetruecolor($this->width, $this->height); $bga = imagecolorallocatealpha($img, 0, 0, 0, 127); imagecolortransparent($img, $bga); imagefill($img, 0, 0, $bga); imagecopy($img, $this->image, 0, 0, 0, 0, $this->width, $this->height); imagetruecolortopalette($img, false, 255); imagesavealpha($img, true); if (imageistruecolor($this->image)) { $this->log("Matching colors with true color image."); imagecolormatch($this->image, $img); } $this->image = $img; } public function sharpenImage() { $this->imageConvolution('sharpen'); return $this; } public function embossImage() { $this->imageConvolution('emboss'); return $this; } public function blurImage() { $this->imageConvolution('blur'); return $this; } public function createConvolveArguments($expression) { if (isset($this->convolves[$expression])) { $expression = $this->convolves[$expression]; } $part = explode(',', $expression); $this->log("Creating convolution expressen: $expression"); if (count($part) != 11) { throw new Exception( "Missmatch in argument convolve. Expected comma-separated string with
4 4
                 11 float values. Got $expression." ); } array_walk($part, function ($item, $key) { if (!is_numeric($item)) { throw new Exception("Argument to convolve expression should be float but is not."); } }); return array( array( array($part[0], $part[1], $part[2]), array($part[3], $part[4], $part[5]), array($part[6], $part[7], $part[8]), ), $part[9], $part[10], ); } public function addConvolveExpressions($options) { $this->convolves = array_merge($this->convolves, $options); return $this; } public function imageConvolution($options = null) { $options = $options ? $options : $this->convolve; $this->log("Convolution with '$options'"); $options = explode(":", $options); foreach ($options as $option) { list($matrix, $divisor, $offset) = $this->createConvolveArguments($option); imageconvolution($this->image, $matrix, $divisor, $offset); } return $this; } public function setDefaultBackgroundColor($color) { $this->log("Setting default background color to '$color'."); if (!(strlen($color) == 6 || strlen($color) == 8)) { throw new Exception( "Background color needs a hex value of 6 or 8
5 5
                 digits. 000000-FFFFFF or 00000000-FFFFFF7F.
Please login to merge, or discard this patch.
a/vendor/mos/cimage/CHttpGet.php 1 patch
Indentation   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -11,9 +11,9 @@
 block discarded – undo
11 11
 
12 12
 
13 13
     /**
14
-    * Constructor
15
-    *
16
-    */
14
+     * Constructor
15
+     *
16
+     */
17 17
     public function __construct()
18 18
     {
19 19
         $this->request['header'] = array();
Please login to merge, or discard this patch.
a/vendor/anax/database/test/Database/DatabaseGeneralFailTest.php 1 patch
Indentation   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -29,7 +29,7 @@  discard block
 block discarded – undo
29 29
     {
30 30
         $this->expectException("Anax\Database\Exception\Exception");
31 31
         $db = new Database([
32
-           "dsn" => "NO DNS"
32
+            "dsn" => "NO DNS"
33 33
         ]);
34 34
         $db->connect();
35 35
     }
@@ -43,8 +43,8 @@  discard block
 block discarded – undo
43 43
     {
44 44
         $this->expectException("\PDOException");
45 45
         $db = new Database([
46
-           "dsn" => "NO DNS",
47
-           "debug_connect" => true,
46
+            "dsn" => "NO DNS",
47
+            "debug_connect" => true,
48 48
         ]);
49 49
         $db->connect();
50 50
     }
Please login to merge, or discard this patch.