Procházet zdrojové kódy

Sample playground markdown working in PHP

main
Rocketsoup před 1 rokem
rodič
revize
e6972770e9
3 změnil soubory, kde provedl 149 přidání a 108 odebrání
  1. 5
    4
      js/markdown.js
  2. 1
    1
      js/markdown.min.js
  3. 143
    103
      php/markdown.php

+ 5
- 4
js/markdown.js Zobrazit soubor

@@ -1951,10 +1951,10 @@ class MDReader {
1951 1951
 		var maxPass = 1;
1952 1952
 		for (const reader of readers) {
1953 1953
 			const passCount = reader.substitutionPassCount;
1954
+			maxPass = Math.max(maxPass, passCount);
1954 1955
 			for (var pass = 1; pass <= passCount; pass++) {
1955 1956
 				tuples.push([ pass, reader ]);
1956 1957
 			}
1957
-			maxPass = Math.max(maxPass, pass);
1958 1958
 		}
1959 1959
 		var result = [];
1960 1960
 		for (var pass = 1; pass <= maxPass; pass++) {
@@ -2583,7 +2583,7 @@ class MDAbbreviationReader extends MDReader {
2583 2583
 		state.abbreviations[abbreviation] = definition;
2584 2584
 		const regex = new RegExp("\\b(" + MDUtils.escapeRegex(abbreviation) + ")\\b", "ig");
2585 2585
 		state.abbreviationRegexes[abbreviation] = regex;
2586
-}
2586
+	}
2587 2587
 
2588 2588
 	preProcess(state) {
2589 2589
 		state.root['abbreviations'] = {};
@@ -2644,7 +2644,7 @@ class MDParagraphReader extends MDReader {
2644 2644
 	readBlock(state) {
2645 2645
 		var paragraphLines = [];
2646 2646
 		var p = state.p;
2647
-		while (state.hasLines(1, $p)) {
2647
+		while (state.hasLines(1, p)) {
2648 2648
 			let line = state.lines[p++];
2649 2649
 			if (line.trim().length == 0) {
2650 2650
 				break;
@@ -2870,6 +2870,7 @@ class MDCodeSpanReader extends MDSimplePairInlineReader {
2870 2870
 	substituteTokens(state, pass, tokens) {
2871 2871
 		if (this.attemptPair(state, pass, tokens, MDCodeNode, MDTokenType.Backtick, 2, true)) return true;
2872 2872
 		if (this.attemptPair(state, pass, tokens, MDCodeNode, MDTokenType.Backtick, 1, true)) return true;
2873
+		return false;
2873 2874
 	}
2874 2875
 }
2875 2876
 
@@ -3671,7 +3672,7 @@ class MDTableCellNode extends MDBlockNode {
3671 3672
 /**
3672 3673
  * Node for a header cell in a header table row.
3673 3674
  */
3674
-class MDTableHeaderCellNode extends MDBlockNode {
3675
+class MDTableHeaderCellNode extends MDTableCellNode {
3675 3676
 	toHTML(state) {
3676 3677
 		return this._simplePairedTagHTML(state, 'th');
3677 3678
 	}

+ 1
- 1
js/markdown.min.js
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 143
- 103
php/markdown.php Zobrazit soubor

@@ -90,6 +90,11 @@ class MDUtils {
90 90
 		return false;
91 91
 	}
92 92
 
93
+	public static function typename($value): string {
94
+		$tn = gettype($value);
95
+		return ($tn === 'object') ? get_class($value) : $tn;
96
+	}
97
+
93 98
 	public static function equalAssocArrays(array &$a, array &$b) {
94 99
 		return empty(array_diff_assoc($a, $b));
95 100
 	}
@@ -217,8 +222,8 @@ class MDToken {
217 222
 	}
218 223
 
219 224
 	public function __toString(): string {
220
-		$classname = get_class($this);
221
-		return "({$classname} type={$this->type} content={$this->content})";
225
+		$classname = MDUtils::typename($this);
226
+		return "<{$classname} type={$this->type->name} content=\"{$this->content}\">";
222 227
 	}
223 228
 
224 229
 	/**
@@ -321,12 +326,15 @@ class MDToken {
321 326
 	 * @return ?MDTokenMatch match object, or `null` if not found
322 327
 	 */
323 328
 	public static function findFirstTokens(array $tokensToSearch, array $pattern, int $startIndex=0): ?MDTokenMatch {
329
+		if (sizeof($pattern) == 0) {
330
+			throw new Error("pattern empty");
331
+		}
324 332
 		$matched = [];
325 333
 		for ($t = $startIndex; $t < sizeof($tokensToSearch); $t++) {
326 334
 			$matchedAll = true;
327 335
 			$matched = [];
328 336
 			$patternOffset = 0;
329
-			for ($p = 0; $p < mb_strlen($pattern); $p++) {
337
+			for ($p = 0; $p < sizeof($pattern); $p++) {
330 338
 				$t0 = $t + $p + $patternOffset;
331 339
 				if ($t0 >= sizeof($tokensToSearch)) return null;
332 340
 				$token = $tokensToSearch[$t0];
@@ -383,11 +391,11 @@ class MDToken {
383 391
 			array $startPattern, array $endPattern, ?callable $contentValidator=null,
384 392
 			int $startIndex=0): ?MDPairedTokenMatch {
385 393
 		for ($s = $startIndex; $s < sizeof($tokensToSearch); $s++) {
386
-			$startMatch = findFirstTokens($tokensToSearch, $startPattern, $s);
394
+			$startMatch = self::findFirstTokens($tokensToSearch, $startPattern, $s);
387 395
 			if ($startMatch === null) return null;
388 396
 			$endStart = $startMatch->index + sizeof($startMatch->tokens);
389 397
 			while ($endStart < sizeof($tokensToSearch)) {
390
-				$endMatch = findFirstTokens($tokensToSearch, $endPattern, $endStart);
398
+				$endMatch = self::findFirstTokens($tokensToSearch, $endPattern, $endStart);
391 399
 				if ($endMatch === null) break;
392 400
 				$contentStart = $startMatch->index + sizeof($startMatch->tokens);
393 401
 				$contentLength = $endMatch->index - $contentStart;
@@ -506,6 +514,7 @@ class MDState {
506 514
 	 */
507 515
 	public function __construct(array $lines) {
508 516
 		$this->lines = $lines;
517
+		$this->startTime = microtime(true);
509 518
 	}
510 519
 
511 520
 	/**
@@ -573,8 +582,8 @@ class MDState {
573 582
 			$block = $reader->readBlock($this);
574 583
 			if ($block) {
575 584
 				if ($this->p == $startP) {
576
-					$readerClassName = get_class($reader);
577
-					$blockClassName = get_class($block);
585
+					$readerClassName = MDUtils::typename($reader);
586
+					$blockClassName = MDUtils::typename($block);
578 587
 					throw new Error("{$readerClassName} returned an " .
579 588
 						"{$blockClassName} without incrementing MDState.p. " .
580 589
 						"This could lead to an infinite loop.");
@@ -598,16 +607,16 @@ class MDState {
598 607
 		$expectLiteral = false;
599 608
 
600 609
 		/**
601
-		 * Flushes accumulated content in `text` to `tokens`.
610
+		 * Flushes accumulated content in `$text` to `$tokens`.
602 611
 		 */
603 612
 		$endText = function() use (&$tokens, &$text) {
604 613
 			if (mb_strlen($text) == 0) return;
605 614
 			$textGroups = [];
606
-			if (mb_eregi('^(\s+)(.*?)$', $text, $textGroups)) {
615
+			if (mb_eregi('^(\\s+)(.*?)$', $text, $textGroups)) {
607 616
 				array_push($tokens, new MDToken($textGroups[1], MDTokenType::Whitespace, $textGroups[1]));
608
-				$text = $textGroups[2];
617
+				$text = is_string($textGroups[2]) ? $textGroups[2] : '';
609 618
 			}
610
-			if (mb_eregi('^(.*?)(\s+)$', $text, $textGroups)) {
619
+			if (mb_eregi('^(.*?)(\\s+)$', $text, $textGroups)) {
611 620
 				array_push($tokens, new MDToken($textGroups[1], MDTokenType::Text, $textGroups[1]));
612 621
 				array_push($tokens, new MDToken($textGroups[2], MDTokenType::Whitespace, $textGroups[2]));
613 622
 			} else {
@@ -635,7 +644,7 @@ class MDState {
635 644
 				$endText();
636 645
 				array_push($tokens, $token);
637 646
 				if ($token->original == null || mb_strlen($token->original) == 0) {
638
-					$readerClassName = get_class($reader);
647
+					$readerClassName = MDUtils::typename($reader);
639 648
 					throw new Error(`{$readerClassName} returned a token with an empty .original. This would cause an infinite loop.`);
640 649
 				}
641 650
 				$p += mb_strlen($token->original) - 1;
@@ -703,7 +712,7 @@ class MDState {
703 712
 		// CSS modifiers.
704 713
 		$lastNode = null;
705 714
 		$me = $this;
706
-		$nodes = array_map(function($node) use ($lastNode, $me) {
715
+		$nodes = array_map(function($node) use (&$lastNode, $me, $nodes) {
707 716
 			if ($node instanceof MDToken) {
708 717
 				/** @var MDToken */
709 718
 				$token = $node;
@@ -719,7 +728,7 @@ class MDState {
719 728
 				$lastNode = ($node instanceof MDTextNode) ? null : $node;
720 729
 				return $node;
721 730
 			} else {
722
-				$nodeClassName = get_class($node);
731
+				$nodeClassName = MDUtils::typename($node);
723 732
 				throw new Error("Unexpected node type {$nodeClassName}");
724 733
 			}
725 734
 		}, $nodes);
@@ -727,6 +736,19 @@ class MDState {
727 736
 		return $nodes;
728 737
 	}
729 738
 
739
+	public $startTime;
740
+
741
+	/**
742
+	 * Checks if parsing has taken an excessive length of time. Because I'm not
743
+	 * fully confident in my loops yet. :)
744
+	 */
745
+	public function checkExecutionTime(float $maxSeconds=1.0) {
746
+		$elapsed = microtime(true) - $this->root()->startTime;
747
+		if ($elapsed > $maxSeconds) {
748
+			throw new Error("Markdown parsing taking too long. Infinite loop?");
749
+		}
750
+	}
751
+
730 752
 	/**
731 753
 	 * Mapping of reference symbols to URLs. Used by `MDReferencedLinkReader`
732 754
 	 * and `MDReferencedImageReader`.
@@ -1671,7 +1693,7 @@ class MDReader {
1671 1693
 		$sorted = $readers;
1672 1694
 		return self::kahnTopologicalSort($sorted, function(MDReader $a, MDReader $b): int {
1673 1695
 			return $a->compareBlockOrdering($b);
1674
-		}, fn($elem) => get_class($elem));
1696
+		}, fn($elem) => MDUtils::typename($elem));
1675 1697
 	}
1676 1698
 
1677 1699
 	/**
@@ -1684,7 +1706,7 @@ class MDReader {
1684 1706
 		$sorted = $readers;
1685 1707
 		return self::kahnTopologicalSort($sorted, function(MDReader $a, MDReader $b): int {
1686 1708
 			return $a->compareTokenizeOrdering($b);
1687
-		}, fn($elem) => get_class($elem));
1709
+		}, fn($elem) => MDUtils::typename($elem));
1688 1710
 	}
1689 1711
 
1690 1712
 	/**
@@ -1704,19 +1726,19 @@ class MDReader {
1704 1726
 		$maxPass = 1;
1705 1727
 		foreach ($readers as $reader) {
1706 1728
 			$passCount = $reader->substitutionPassCount();
1729
+			$maxPass = max($paxPass, $passCount);
1707 1730
 			for ($pass = 1; $pass <= $passCount; $pass++) {
1708
-				array_push($tuples, [[ $pass, $reader ]]);
1731
+				array_push($tuples, [ $pass, $reader ]);
1709 1732
 			}
1710
-			$maxPass = max($maxPass, $pass);
1711 1733
 		}
1712 1734
 		$result = [];
1713 1735
 		for ($pass = 1; $pass <= $maxPass; $pass++) {
1714
-			$readersThisPass = array_filter($tuples, fn($tup) => $tup[0] == $pass);
1736
+			$readersThisPass = array_values(array_filter($tuples, fn($tup) => $tup[0] === $pass));
1715 1737
 			$passResult = self::kahnTopologicalSort($readersThisPass, function(array $a, array $b) use ($pass): int {
1716 1738
 				$aReader = $a[1];
1717 1739
 				$bReader = $b[1];
1718 1740
 				return $aReader->compareSubstituteOrdering($bReader, $pass);
1719
-			}, fn($elem) => get_class($elem[1]));
1741
+			}, fn($elem) => MDUtils::typename($elem[1]));
1720 1742
 			$result = array_merge($result, $passResult);
1721 1743
 		}
1722 1744
 		return $result;
@@ -1870,14 +1892,14 @@ class _MDListReader extends MDReader {
1870 1892
 		$state->p += max(sizeof($itemLines), 1);
1871 1893
 
1872 1894
 		if (sizeof($itemLines) == 1) {
1873
-			return $state->inlineMarkdownToNode($itemLines[0]);
1895
+			return new MDBlockNode($state->inlineMarkdownToNodes($itemLines[0]));
1874 1896
 		}
1875 1897
 
1876 1898
 		$hasBlankLines = sizeof(array_filter($itemLines, fn($line) => trim($line) == '')) > 0;
1877 1899
 		if ($hasBlankLines) {
1878 1900
 			$substate = $state->copy($itemLines);
1879 1901
 			$blocks = $substate->readBlocks();
1880
-			return (sizeof($blocks) == 1) ? $blocks[0] : new MDNode($blocks);
1902
+			return (sizeof($blocks) == 1) ? $blocks[0] : new MBlockDNode($blocks);
1881 1903
 		}
1882 1904
 
1883 1905
 		// Multiline content with no blank lines. Search for new block
@@ -1886,10 +1908,10 @@ class _MDListReader extends MDReader {
1886 1908
 			$line = $itemLines[$p];
1887 1909
 			if (mb_eregi('^(?:\\*|\\-|\\+|\\d+\\.)\\s+', $line)) {
1888 1910
 				// Nested list found
1889
-				$firstBlock = $state->inlineMarkdownToNode(implode("\n", array_slice($itemLines, 0, $p)));
1911
+				$firstBlock = new MDBlockNode($state->inlineMarkdownToNodes(implode("\n", array_slice($itemLines, 0, $p))));
1890 1912
 				$substate = $state->copy(array_slice($itemLines, $p));
1891 1913
 				$blocks = $substate->readBlocks();
1892
-				return array_merge([ $firstBlock, $blocks ]);
1914
+				return new MDBlockNode(array_merge([ $firstBlock ], $blocks));
1893 1915
 			}
1894 1916
 		}
1895 1917
 
@@ -1897,12 +1919,12 @@ class _MDListReader extends MDReader {
1897 1919
 		{
1898 1920
 			$substate = $state->copy($itemLines);
1899 1921
 			$blocks = $substate->readBlocks();
1900
-			return (sizeof($blocks) == 1) ? $blocks[0] : new MDNode($blocks);
1922
+			return (sizeof($blocks) == 1) ? $blocks[0] : new MDBlockNode($blocks);
1901 1923
 		}
1902 1924
 	}
1903 1925
 
1904 1926
 	public function readBlock(MDState $state): ?MDBlockNode {
1905
-		$className = get_class($this);
1927
+		$className = MDUtils::typename($this);
1906 1928
 		throw new Error("Abstract readBlock must be overridden in {$className}");
1907 1929
 	}
1908 1930
 }
@@ -1914,6 +1936,7 @@ class MDUnorderedListReader extends _MDListReader {
1914 1936
 	private static string $unorderedListRegex = '^([\\*\\+\\-]\\s+)(.*)$';  // 1=bullet, 2=content
1915 1937
 
1916 1938
 	private function readUnorderedListItem(MDState $state): ?MDListItemNode {
1939
+		if (!$state->hasLines(1)) return null;
1917 1940
 		$p = $state->p;
1918 1941
 		$line = $state->lines[$p];
1919 1942
 		if (!mb_eregi(self::$unorderedListRegex, $line, $groups)) return null;
@@ -1942,6 +1965,7 @@ class MDOrderedListReader extends _MDListReader {
1942 1965
 	private static string $orderedListRegex = '^(\\d+)(\\.\\s+)(.*)$';  // 1=number, 2=dot, 3=content
1943 1966
 
1944 1967
 	private function readOrderedListItem(MDState $state): ?MDListItemNode {
1968
+		if (!$state->hasLines(1)) return null;
1945 1969
 		$p = $state->p;
1946 1970
 		$line = $state->lines[$p];
1947 1971
 		if (!mb_eregi(self::$orderedListRegex, $line, $groups)) return null;
@@ -1976,7 +2000,7 @@ class MDFencedCodeBlockReader extends MDReader {
1976 2000
 		$p = $state->p;
1977 2001
 		$openFenceLine = $state->lines[$p++];
1978 2002
 		[$openFenceLine, $modifier] = MDTagModifier::fromLine($openFenceLine, $state);
1979
-		if (!mb_eregi('```\s*([a-z0-9]*)\s*$', $openFenceLine, $groups)) return null;
2003
+		if (!mb_eregi('```\\s*([a-z0-9]*)\\s*$', $openFenceLine, $groups)) return null;
1980 2004
 		$language = mb_strlen($groups[1]) > 0 ? $groups[1] : null;
1981 2005
 		$codeLines = [];
1982 2006
 		while ($state->hasLines(1, $p)) {
@@ -2056,7 +2080,7 @@ class MDTableReader extends MDReader {
2056 2080
 		if (str_starts_with($line, '|')) $line = mb_substr($line, 1);
2057 2081
 		if (str_ends_with($line, '|')) $line = mb_substr($line, 0, mb_strlen($line) - 1);
2058 2082
 		$cellTokens = explode('|', $line);
2059
-		$cells = array_map(function($token) use ($isHeader) {
2083
+		$cells = array_map(function($token) use ($isHeader, $state) {
2060 2084
 			$content = $state->inlineMarkdownToNode(trim($token));
2061 2085
 			return $isHeader ? new MDTableHeaderCellNode($content) : new MDTableCellNode($content);
2062 2086
 		}, $cellTokens);
@@ -2144,8 +2168,8 @@ class MDDefinitionListReader extends MDReader {
2144 2168
 			}
2145 2169
 		}
2146 2170
 		if ($termCount == 0 || $definitionCount == 0) return null;
2147
-		$blocks = array_map(function($line) {
2148
-			if (mb_eregi('^:\\s+(.*?)$', $line)) {
2171
+		$blocks = array_map(function($line) use ($state) {
2172
+			if (mb_eregi('^:\\s+(.*?)$', $line, $groups)) {
2149 2173
 				return new MDDefinitionListDefinitionNode($state->inlineMarkdownToNodes($groups[1]));
2150 2174
 			} else {
2151 2175
 				return new MDDefinitionListTermNode($state->inlineMarkdownToNodes($line));
@@ -2181,16 +2205,18 @@ class MDFootnoteReader extends MDReader {
2181 2205
 		$instances = $footnoteInstances[$symbol] ?? [];
2182 2206
 		array_push($instances, $unique);
2183 2207
 		$footnoteInstances[$symbol] = $instances;
2208
+		$state->root()->userInfo['footnoteInstances'] = $footnoteInstances;
2184 2209
 	}
2185 2210
 
2186 2211
 	private function idForFootnoteSymbol(MDState $state, string $symbol): int {
2187
-		$footnoteIds = $state->root()->userInfo['footnoteIds'];
2188
-		$existing = $footnoteIds[$symbol];
2189
-		if ($existing) return $existing;
2190
-		$nextFootnoteId = $state->root()->userInfo['nextFootnoteId'];
2212
+		$footnoteIds = $state->root()->userInfo['footnoteIds'] ?? [];
2213
+		$existing = $footnoteIds[$symbol] ?? null;
2214
+		if ($existing !== null) return $existing;
2215
+		$nextFootnoteId = $state->root()->userInfo['nextFootnoteId'] ?? 1;
2191 2216
 		$id = $nextFootnoteId++;
2192 2217
 		$footnoteIds[$symbol] = $id;
2193 2218
 		$state->root()->userInfo['nextFootnoteId'] = $nextFootnoteId;
2219
+		$state->root()->userInfo['footnoteIds'] = $footnoteIds;
2194 2220
 		return $id;
2195 2221
 	}
2196 2222
 
@@ -2218,7 +2244,7 @@ class MDFootnoteReader extends MDReader {
2218 2244
 		$content = $state->inlineMarkdownToNodes($def);
2219 2245
 		$this->defineFootnote($state, $symbol, $content);
2220 2246
 		$state->p = $p;
2221
-		return new MDNode(); // empty
2247
+		return new MDBlockNode(); // empty
2222 2248
 	}
2223 2249
 
2224 2250
 	public function readToken(MDState $state, string $line): ?MDToken {
@@ -2233,9 +2259,9 @@ class MDFootnoteReader extends MDReader {
2233 2259
 	}
2234 2260
 
2235 2261
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
2236
-		if ($match = self::findFirstTokens($tokens, [ MDTokenType::Footnote ])) {
2262
+		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::Footnote ])) {
2237 2263
 			$symbol = $match->tokens[0]->content;
2238
-			array_splice($tokens, $match->index, 1, new MDFootnoteNode($symbol));
2264
+			array_splice($tokens, $match->index, 1, [new MDFootnoteNode($symbol)]);
2239 2265
 			return true;
2240 2266
 		}
2241 2267
 		return false;
@@ -2248,12 +2274,12 @@ class MDFootnoteReader extends MDReader {
2248 2274
 	public function postProcess(MDState $state, array &$blocks) {
2249 2275
 		$nextOccurrenceId = 1;
2250 2276
 		foreach ($blocks as $block) {
2251
-			$block->visitChildren(function($node) use (&$nextOccurrenceId) {
2277
+			$block->visitChildren(function($node) use (&$nextOccurrenceId, $state) {
2252 2278
 				if (!($node instanceof MDFootnoteNode)) return;
2253 2279
 				$node->footnoteId = $this->idForFootnoteSymbol($state, $node->symbol);
2254 2280
 				$node->occurrenceId = $nextOccurrenceId++;
2255 2281
 				$node->displaySymbol = strval($node->footnoteId);
2256
-				$this->$registerUniqueInstance($state, $node->symbol, $node->occurrenceId);
2282
+				$this->registerUniqueInstance($state, $node->symbol, $node->occurrenceId);
2257 2283
 			});
2258 2284
 		}
2259 2285
 		if (sizeof($state->userInfo['footnotes']) == 0) return;
@@ -2290,14 +2316,13 @@ class MDFootnoteReader extends MDReader {
2290 2316
  */
2291 2317
 class MDAbbreviationReader extends MDReader {
2292 2318
 	private function defineAbbreviation(MDState $state, string $abbreviation, string $definition) {
2293
-		$state->root()->abbreviations[$abbreviation] = $definition;
2294
-		$regex = "\\b(" . preg_quote($abbreviation) . ")\\b";
2295
-		$state->root()->abbreviationRegexes[$abbreviation] = $regex;
2319
+		$abbrevs = $state->root()->userInfo['abbreviations'];
2320
+		$abbrevs[$abbreviation] = $definition;
2321
+		$state->root()->userInfo['abbreviations'] = $abbrevs;
2296 2322
 	}
2297 2323
 
2298 2324
 	public function preProcess(MDState $state) {
2299 2325
 		$state->root()->userInfo['abbreviations'] = [];
2300
-		$state->root()->userInfo['abbreviationRegexes'] = [];
2301 2326
 	}
2302 2327
 
2303 2328
 	public function readBlock(MDState $state): ?MDBlockNode {
@@ -2308,7 +2333,7 @@ class MDAbbreviationReader extends MDReader {
2308 2333
 		$def = $groups[2];
2309 2334
 		$this->defineAbbreviation($state, $abbrev, $def);
2310 2335
 		$state->p = $p;
2311
-		return new MDNode(); // empty
2336
+		return new MDBlockNode(); // empty
2312 2337
 	}
2313 2338
 
2314 2339
 	/**
@@ -2317,28 +2342,26 @@ class MDAbbreviationReader extends MDReader {
2317 2342
 	 */
2318 2343
 	public function postProcess(MDState $state, array &$blocks) {
2319 2344
 		$abbreviations = $state->root()->userInfo['abbreviations'];
2320
-		$regexes = $state->root()->userInfo['abbreviationRegexes'];
2321
-		MDNode::replaceNodes($state, $blocks, function($original) use ($abbreviations, $regexes) {
2345
+		MDNode::replaceNodes($state, $blocks, function($original) use ($abbreviations) {
2322 2346
 			if (!($original instanceof MDTextNode)) return null;
2323 2347
 			$changed = false;
2324 2348
 			$elems = [ $original->text ]; // mix of strings and MDNodes
2325 2349
 			for ($i = 0; $i < sizeof($elems); $i++) {
2326 2350
 				$text = $elems[$i];
2327 2351
 				if (!is_string($text)) continue;
2328
-				foreach ($abbreviations as $abbreviation) {
2352
+				foreach ($abbreviations as $abbreviation => $definition) {
2329 2353
 					$index = strpos($text, $abbreviation);
2330
-					if ($index === false) break;
2354
+					if ($index === false) continue;
2331 2355
 					$prefix = substr($text, 0, $index);
2332 2356
 					$suffix = substr($text, $index + strlen($abbreviation));
2333
-					$definition = $abbreviations[$abbreviation];
2334
-					array_splice($elems, $i, 1, [ $prefix, new MDAbbreviationNode($abbreviation, $definition), $suffix ]);
2357
+					array_splice($elems, $i, 1, [$prefix, new MDAbbreviationNode($abbreviation, $definition), $suffix]);
2335 2358
 					$i = -1; // start over
2336 2359
 					$changed = true;
2337 2360
 					break;
2338 2361
 				}
2339 2362
 			}
2340 2363
 			if (!$changed) return null;
2341
-			$nodes = array_map(fn($elem) => is_string($elem) ? new MDTextNode($elem) : $elem);
2364
+			$nodes = array_map(fn($elem) => is_string($elem) ? new MDTextNode($elem) : $elem, $elems);
2342 2365
 			return new MDNode($nodes);
2343 2366
 		});
2344 2367
 	}
@@ -2417,17 +2440,16 @@ class MDSimplePairInlineReader extends MDReader {
2417 2440
 		// #4: singles with paired inner tokens
2418 2441
 		if ($count == 1 && $pass != 2 && $pass != 4) return false;
2419 2442
 		if ($count > 1 && $pass != 1 && $pass != 3) return false;
2420
-		$delimiters = [];
2421
-		array_fill(0, $count, $delimiter);
2443
+		$delimiters = array_fill(0, $count, $delimiter);
2422 2444
 		$isFirstOfMultiplePasses = $this->substitutionPassCount() > 1 && $pass == 1;
2423
-		$match = MDToken::findPairedTokens($tokens, $delimiters, $delimiters, function($content) {
2445
+		$match = MDToken::findPairedTokens($tokens, $delimiters, $delimiters, function($content) use ($nodeClass, $isFirstOfMultiplePasses, $delimiter) {
2424 2446
 			$firstType = $content[0] instanceof MDToken ? $content[0]->type : null;
2425 2447
 			$lastType = $content[sizeof($content) - 1] instanceof MDToken ? $content[sizeof($content) - 1]->type : null;
2426 2448
 			if ($firstType == MDTokenType::Whitespace) return false;
2427 2449
 			if ($lastType == MDTokenType::Whitespace) return false;
2428 2450
 			foreach ($content as $token) {
2429 2451
 				// Don't allow nesting
2430
-				if (get_class($token) == $nodeClass) return false;
2452
+				if (MDUtils::typename($token) == $nodeClass) return false;
2431 2453
 			}
2432 2454
 			if ($isFirstOfMultiplePasses) {
2433 2455
 				$innerCount = 0;
@@ -2439,14 +2461,19 @@ class MDSimplePairInlineReader extends MDReader {
2439 2461
 			return true;
2440 2462
 		});
2441 2463
 		if ($match === null) return false;
2442
-		$content = ($plaintext)
2443
-			? implode('', array_map(fn($token) => $token->original, $match->contentTokens))
2444
-			: $state->tokensToNodes($match->contentTokens);
2464
+		$state->checkExecutionTime();
2465
+		if ($plaintext) {
2466
+			$content = implode('', array_map(fn($token) => $token instanceof MDToken ? $token->original : $token->toPlaintext($state), $match->contentTokens));
2467
+		} else {
2468
+			$content = $state->tokensToNodes($match->contentTokens);
2469
+		}
2445 2470
 		$ref = new ReflectionClass($nodeClass);
2446 2471
 		$node = $ref->newInstanceArgs([ $content ]);
2447
-		array_splice($tokens, $match->startIndex, $match->totalLength, [ $node ]);
2472
+		array_splice($tokens, $match->startIndex, $match->totalLength, [$node]);
2448 2473
 		return true;
2449 2474
 	}
2475
+
2476
+	private static $firstTime = null;
2450 2477
 }
2451 2478
 
2452 2479
 /**
@@ -2582,6 +2609,7 @@ class MDCodeSpanReader extends MDSimplePairInlineReader {
2582 2609
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
2583 2610
 		if ($this->attemptPair($state, $pass, $tokens, 'MDCodeNode', MDTokenType::Backtick, 2, true)) return true;
2584 2611
 		if ($this->attemptPair($state, $pass, $tokens, 'MDCodeNode', MDTokenType::Backtick, 1, true)) return true;
2612
+		return false;
2585 2613
 	}
2586 2614
 }
2587 2615
 
@@ -2654,7 +2682,7 @@ class MDLinkReader extends MDReader {
2654 2682
 			$text = $match->tokens[0]->content;
2655 2683
 			$url = $match->tokens[sizeof($match->tokens) - 1]->content;
2656 2684
 			$title = $match->tokens[sizeof($match->tokens) - 1]->extra;
2657
-			array_splice($tokens, $match->index, sizeof($match->tokens), new MDLinkNode($url, $state->inlineMarkdownToNode($text), $title));
2685
+			array_splice($tokens, $match->index, sizeof($match->tokens), [new MDLinkNode($url, $state->inlineMarkdownToNode($text), $title)]);
2658 2686
 			return true;
2659 2687
 		}
2660 2688
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::Label, MDTokenType::META_OptionalWhitespace, MDTokenType::Email ])) {
@@ -2662,21 +2690,21 @@ class MDLinkReader extends MDReader {
2662 2690
 			$email = $match->tokens[sizeof($match->tokens) - 1]->content;
2663 2691
 			$url = "mailto:{$email}";
2664 2692
 			$title = $match->tokens[sizeof($match->tokens) - 1]->extra;
2665
-			array_splice($tokens, $match->index, sizeof($match->tokens), new MDLinkNode($url, $state->inlineMarkdownToNodes($text), $title));
2693
+			array_splice($tokens, $match->index, sizeof($match->tokens), [new MDLinkNode($url, $state->inlineMarkdownToNodes($text), $title)]);
2666 2694
 			return true;
2667 2695
 		}
2668 2696
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::SimpleEmail ])) {
2669 2697
 			$token = $match->tokens[0];
2670 2698
 			$link = "mailto:{$token->content}";
2671 2699
 			$node = new MDLinkNode($link, new MDObfuscatedTextNode($token->content));
2672
-			array_splice($tokens, $match->index, 1, $node);
2700
+			array_splice($tokens, $match->index, 1, [$node]);
2673 2701
 			return true;
2674 2702
 		}
2675 2703
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::SimpleLink ])) {
2676 2704
 			$token = $match->tokens[0];
2677 2705
 			$link = $token->content;
2678 2706
 			$node = new MDLinkNode($link, new MDTextNode($link));
2679
-			array_splice($tokens, $match->index, 1, $node);
2707
+			array_splice($tokens, $match->index, 1, [$node]);
2680 2708
 			return true;
2681 2709
 		}
2682 2710
 		return false;
@@ -2701,20 +2729,21 @@ class MDReferencedLinkReader extends MDLinkReader {
2701 2729
 			if (mb_eregi('^\\s*\\[(.+?)]:\\s*(\\S+)\\s*$', $line, $groups)) {
2702 2730
 				$symbol = $groups[1];
2703 2731
 				$url = $groups[2];
2732
+				$title = null;
2704 2733
 			} else {
2705 2734
 				return null;
2706 2735
 			}
2707 2736
 		}
2708 2737
 		$state->defineURL($symbol, $url, $title);
2709 2738
 		$state->p = $p;
2710
-		return new MDNode([]); // empty
2739
+		return new MDBlockNode([]); // empty
2711 2740
 	}
2712 2741
 
2713 2742
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
2714 2743
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::Label, MDTokenType::META_OptionalWhitespace, MDTokenType::Label ])) {
2715 2744
 			$text = $match->tokens[0]->content;
2716 2745
 			$ref = $match->tokens[sizeof($match->tokens) - 1]->content;
2717
-			array_splice($tokens, $match->index, sizeof($match->tokens), new MDReferencedLinkNode($ref, $state->inlineMarkdownToNodes($text)));
2746
+			array_splice($tokens, $match->index, sizeof($match->tokens), [new MDReferencedLinkNode($ref, $state->inlineMarkdownToNodes($text))]);
2718 2747
 			return true;
2719 2748
 		}
2720 2749
 		return false;
@@ -2742,7 +2771,7 @@ class MDImageReader extends MDLinkReader {
2742 2771
 			if ($title !== null) {
2743 2772
 				$node->attributes['title'] = $title;
2744 2773
 			}
2745
-			array_splice($tokens, $match->index, sizeof($match->tokens), $node);
2774
+			array_splice($tokens, $match->index, sizeof($match->tokens), [$node]);
2746 2775
 			return true;
2747 2776
 		}
2748 2777
 		return false;
@@ -2773,7 +2802,7 @@ class MDReferencedImageReader extends MDReferencedLinkReader {
2773 2802
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::Bang, MDTokenType::Label, MDTokenType::META_OptionalWhitespace, MDTokenType::Label ])) {
2774 2803
 			$alt = $match->tokens[1]->content;
2775 2804
 			$ref = $match->tokens[sizeof($match->tokens) - 1]->content;
2776
-			array_splice($tokens, $match->index, sizeof($match->tokens), new MDReferencedImageNode($ref, $alt));
2805
+			array_splice($tokens, $match->index, sizeof($match->tokens), [new MDReferencedImageNode($ref, $alt)]);
2777 2806
 			return true;
2778 2807
 		}
2779 2808
 		return false;
@@ -2827,7 +2856,7 @@ class MDHTMLTagReader extends MDReader {
2827 2856
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
2828 2857
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::HTMLTag ])) {
2829 2858
 			$tag = $match->tokens[0]->tag;
2830
-			array_splice($tokens, $match->index, sizeof($match->tokens), new MDHTMLTagNode($tag));
2859
+			array_splice($tokens, $match->index, 1, [new MDHTMLTagNode($tag)]);
2831 2860
 			return true;
2832 2861
 		}
2833 2862
 		return false;
@@ -2897,8 +2926,8 @@ class MDNode {
2897 2926
 		if (is_array($children)) {
2898 2927
 			foreach ($children as $elem) {
2899 2928
 				if (!($elem instanceof MDNode)) {
2900
-					$thisClassName = get_class($this);
2901
-					$elemClassName = get_class($elem);
2929
+					$thisClassName = MDUtils::typename($this);
2930
+					$elemClassName = MDUtils::typename($elem);
2902 2931
 					throw new Error("{$thisClassName} expects children of type MDNode[] or MDNode, got array with {$elemClassName} element");
2903 2932
 				}
2904 2933
 			}
@@ -2906,12 +2935,21 @@ class MDNode {
2906 2935
 		} elseif ($children instanceof MDNode) {
2907 2936
 			$this->children = [ $children ];
2908 2937
 		} else {
2909
-			$thisClassName = get_class($this);
2910
-			$elemClassName = gettype($children) == 'object' ? get_class($children) : gettype($children);
2938
+			$thisClassName = MDUtils::typename($this);
2939
+			$elemClassName = MDUtils::typename($children);
2911 2940
 			throw new Error("{$thisClassName} expects children of type MDNode[] or MDNode, got {$elemClassName}");
2912 2941
 		}
2913 2942
 	}
2914 2943
 
2944
+	public function __toString(): string {
2945
+		$s = "<" . get_class($this);
2946
+		foreach ($this->children as $child) {
2947
+			$s .= " {$child}";
2948
+		}
2949
+		$s .= ">";
2950
+		return $s;
2951
+	}
2952
+
2915 2953
 	/**
2916 2954
 	 * Adds a CSS class. If already present it will not be duplicated.
2917 2955
 	 */
@@ -2969,25 +3007,25 @@ class MDNode {
2969 3007
 	protected function htmlAttributes(): string {
2970 3008
 		$html = '';
2971 3009
 		if (sizeof($this->cssClasses) > 0) {
2972
-			$classlist = implode(' ', $this->cssClasses);
2973
-			$html .= " class=\"{$classList}\"";
3010
+			$classlist = htmlentities(implode(' ', $this->cssClasses));
3011
+			$html .= " class=\"{$classlist}\"";
2974 3012
 		}
2975 3013
 		if ($this->cssId !== null && mb_strlen($this->cssId) > 0) {
2976
-			$html .= " id=\"{$this->cssId}\"";
3014
+			$html .= " id=\"" . htmlentities($this->cssId) . "\"";
2977 3015
 		}
2978 3016
 		$styles = [];
2979 3017
 		foreach ($this->cssStyles as $key => $value) {
2980 3018
 			array_push($styles, "{$key}: {$value};");
2981 3019
 		}
2982 3020
 		if (sizeof($styles) > 0) {
2983
-			$escaped = htmlspecialchars(implode(' ', $styles));
3021
+			$escaped = htmlentities(implode(' ', $styles));
2984 3022
 			$html .= " style=\"{$escaped}\"";
2985 3023
 		}
2986 3024
 		foreach ($this->attributes as $key => $value) {
2987 3025
 			if ($key === 'class' || $key === 'id' || $key === 'style') continue;
2988 3026
 			$cleanKey = MDUtils::scrubAttributeName($key);
2989 3027
 			if (mb_strlen($cleanKey) == 0) continue;
2990
-			$cleanValue = htmlspecialchars($value);
3028
+			$cleanValue = htmlentities($value);
2991 3029
 			$html .= " {$cleanKey}=\"{$cleanValue}\"";
2992 3030
 		}
2993 3031
 		return $html;
@@ -3106,7 +3144,7 @@ class MDHeadingNode extends MDBlockNode {
3106 3144
 	public function __construct(int $level, array $children) {
3107 3145
 		parent::__construct($children);
3108 3146
 		if (!is_int($level) || ($level < 1 || $level > 6)) {
3109
-			$thisClassName = get_class($this);
3147
+			$thisClassName = MDUtils::typename($this);
3110 3148
 			throw new Error("{$thisClassName} requires heading level 1 to 6");
3111 3149
 		}
3112 3150
 		$this->level = $level;
@@ -3162,7 +3200,7 @@ class MDUnorderedListNode extends MDBlockNode {
3162 3200
 class MDOrderedListNode extends MDBlockNode {
3163 3201
 	/** @var MDListItemNode[] $children */
3164 3202
 
3165
-	public int $startOrdinal;
3203
+	public ?int $startOrdinal;
3166 3204
 
3167 3205
 	/**
3168 3206
 	 * @param MDListItemNode[] $children
@@ -3185,13 +3223,13 @@ class MDOrderedListNode extends MDBlockNode {
3185 3223
  * An item in a bulleted or numbered list.
3186 3224
  */
3187 3225
 class MDListItemNode extends MDBlockNode {
3188
-	public int $ordinal;
3226
+	public ?int $ordinal;
3189 3227
 
3190 3228
 	/**
3191 3229
 	 * @param MDNode|MDNode[] $children
3192 3230
 	 * @param ?int $ordinal
3193 3231
 	 */
3194
-	public function __construct(array $children, ?int $ordinal=null) {
3232
+	public function __construct(array|MDNode $children, ?int $ordinal=null) {
3195 3233
 		parent::__construct($children);
3196 3234
 		$this->ordinal = $ordinal;
3197 3235
 	}
@@ -3213,7 +3251,7 @@ class MDCodeBlockNode extends MDBlockNode {
3213 3251
 	public ?string $language;
3214 3252
 
3215 3253
 	public function __construct(string $text, ?string $language=null) {
3216
-		super([]);
3254
+		parent::__construct([]);
3217 3255
 		$this->text = $text;
3218 3256
 		$this->language = $language;
3219 3257
 	}
@@ -3251,7 +3289,7 @@ class MDTableNode extends MDBlockNode {
3251 3289
 	 * @param MDTableRowNode $headerRow
3252 3290
 	 * @param MDTableRowNode[] $bodyRows
3253 3291
 	 */
3254
-	public function __construct(MDTableRow $headerRow, array $bodyRows) {
3292
+	public function __construct(MDTableRowNode $headerRow, array $bodyRows) {
3255 3293
 		parent::__construct(array_merge([ $headerRow ], $bodyRows));
3256 3294
 	}
3257 3295
 
@@ -3280,11 +3318,11 @@ class MDTableNode extends MDBlockNode {
3280 3318
 		$this->applyAlignments();
3281 3319
 		$html = '';
3282 3320
 		$html .= "<table" . $this->htmlAttributes() . ">\n";
3283
-		$html .= '<thead>\n';
3284
-		$html .= $this->headerRow->toHTML($state) . "\n";
3321
+		$html .= "<thead>\n";
3322
+		$html .= $this->headerRow()->toHTML($state) . "\n";
3285 3323
 		$html .= "</thead>\n";
3286 3324
 		$html .= "<tbody>\n";
3287
-		$html .= MDNode::toHTML($this->bodyRows, $state) . "\n";
3325
+		$html .= MDNode::arrayToHTML($this->bodyRows(), $state) . "\n";
3288 3326
 		$html .= "</tbody>\n";
3289 3327
 		$html .= "</table>\n";
3290 3328
 		return $html;
@@ -3314,7 +3352,7 @@ class MDTableCellNode extends MDBlockNode {
3314 3352
 /**
3315 3353
  * Node for a header cell in a header table row.
3316 3354
  */
3317
-class MDTableHeaderCellNode extends MDBlockNode {
3355
+class MDTableHeaderCellNode extends MDTableCellNode {
3318 3356
 	public function toHTML(MDState $state): string {
3319 3357
 		return $this->simplePairedTagHTML($state, 'th');
3320 3358
 	}
@@ -3354,27 +3392,28 @@ class MDDefinitionListDefinitionNode extends MDBlockNode {
3354 3392
  * content.
3355 3393
  */
3356 3394
 class MDFootnoteListNode extends MDBlockNode {
3357
-	private function footnoteId(MDState $state, string $symbol): int {
3395
+	private function footnoteId(MDState $state, string $symbol): ?int {
3358 3396
 		$lookup = $state->root()->userInfo['footnoteIds'];
3359 3397
 		if (!$lookup) return null;
3360 3398
 		return $lookup[$symbol] ?? null;
3361 3399
 	}
3362 3400
 
3363 3401
 	public function toHTML(MDState $state): string {
3364
-		$footnotes = $state->userInfo['footnotes'];
3402
+		$footnotes = $state->root()->userInfo['footnotes'];
3365 3403
 		$symbolOrder = array_keys($footnotes);
3366 3404
 		if (sizeof($footnotes) == 0) return '';
3367
-		$footnoteUniques = $state->root()->footnoteInstances;
3405
+		$footnoteUniques = $state->root()->userInfo['footnoteInstances'];
3368 3406
 		$html = '';
3369 3407
 		$html .= '<div class="footnotes">';
3370 3408
 		$html .= '<ol>';
3371
-		foreach ($symbolOrder as $symbol) {
3409
+		foreach ($symbolOrder as $symbolRaw) {
3410
+			$symbol = "{$symbolRaw}";
3372 3411
 			$content = $footnotes[$symbol];
3373 3412
 			if (!$content) continue;
3374 3413
 			$footnoteId = $this->footnoteId($state, $symbol);
3375
-			$contentHTML = MDNode::toHTML($content, $state);
3414
+			$contentHTML = MDNode::arrayToHTML($content, $state);
3376 3415
 			$html .= "<li value=\"{$footnoteId}\" id=\"{$state->root()->elementIdPrefix}footnote_{$footnoteId}\">{$contentHTML}";
3377
-			$uniques = $footnoteUniques[$symbol];
3416
+			$uniques = $footnoteUniques[$symbol] ?? null;
3378 3417
 			if ($uniques) {
3379 3418
 				foreach ($uniques as $unique) {
3380 3419
 					$html .= " <a href=\"#{$state->root()->elementIdPrefix}footnoteref_{$unique}\" class=\"footnote-backref\">↩︎</a>";
@@ -3384,7 +3423,7 @@ class MDFootnoteListNode extends MDBlockNode {
3384 3423
 		}
3385 3424
 		$html .= '</ol>';
3386 3425
 		$html .= '</div>';
3387
-		return html;
3426
+		return $html;
3388 3427
 	}
3389 3428
 
3390 3429
 	public function toPlaintext(MDState $state): string {
@@ -3392,7 +3431,8 @@ class MDFootnoteListNode extends MDBlockNode {
3392 3431
 		$symbolOrder = array_keys($footnotes);
3393 3432
 		if (sizeof($footnotes) == 0) return '';
3394 3433
 		$text = '';
3395
-		foreach ($symbolOrder as $symbol) {
3434
+		foreach ($symbolOrder as $symbolRaw) {
3435
+			$symbol = "{$symbolRaw}";
3396 3436
 			$content = $footnotes[$symbol];
3397 3437
 			if (!$content) continue;
3398 3438
 			$text .= "{$symbol}. " . $this->childPlaintext(state) . "\n";
@@ -3414,7 +3454,6 @@ class MDTextNode extends MDInlineNode {
3414 3454
 
3415 3455
 	public function __construct(string $text) {
3416 3456
 		parent::__construct([]);
3417
-		if (!is_string($text) || mb_strlen($text) == 0) throw new Error("Meh!");
3418 3457
 		$this->text = $text;
3419 3458
 	}
3420 3459
 
@@ -3513,7 +3552,7 @@ class MDCodeNode extends MDInlineNode {
3513 3552
 	}
3514 3553
 
3515 3554
 	public function toHTML(MDState $state): string {
3516
-		return "<code" . $this->htmlAttributes() . ">" . MDUtils::escapeHTML($this->text) . "</code>";
3555
+		return "<code" . $this->htmlAttributes() . ">" . htmlentities($this->text) . "</code>";
3517 3556
 	}
3518 3557
 }
3519 3558
 
@@ -3551,7 +3590,7 @@ class MDFootnoteNode extends MDInlineNode {
3551 3590
 	}
3552 3591
 
3553 3592
 	public function toHTML(MDState $state): string {
3554
-		if ($this->differentiator !== null) {
3593
+		if ($this->footnoteId !== null) {
3555 3594
 			return "<sup class=\"footnote\" id=\"{$state->root()->elementIdPrefix}footnoteref_{$this->occurrenceId}\"" . $this->htmlAttributes() . ">" .
3556 3595
 				"<a href=\"#{$state->root()->elementIdPrefix}footnote_{$this->footnoteId}\">" . htmlentities($this->displaySymbol ?? $this->symbol) . "</a></sup>";
3557 3596
 		}
@@ -3603,7 +3642,7 @@ class MDReferencedLinkNode extends MDLinkNode {
3603 3642
 			$title = $state->urlTitleForReference($this->reference);
3604 3643
 			if ($title) $this->attributes['title'] = $title;
3605 3644
 		}
3606
-		return $super->toHTML($state);
3645
+		return parent::toHTML($state);
3607 3646
 	}
3608 3647
 }
3609 3648
 
@@ -3616,7 +3655,7 @@ class MDImageNode extends MDInlineNode {
3616 3655
 	public ?string $alt;
3617 3656
 
3618 3657
 	public function __construct(string $src, ?string $alt) {
3619
-		super([]);
3658
+		parent::__construct([]);
3620 3659
 		$this->src = $src;
3621 3660
 		$this->alt = $alt;
3622 3661
 	}
@@ -3647,7 +3686,7 @@ class MDReferencedImageNode extends MDImageNode {
3647 3686
 			$title = $state->urlTitleForReference($this->reference);
3648 3687
 			if ($title !== null) $this->attributes['title'] = $title;
3649 3688
 		}
3650
-		return super.toHTML(state);
3689
+		return parent::toHTML($state);
3651 3690
 	}
3652 3691
 }
3653 3692
 
@@ -3663,7 +3702,7 @@ class MDAbbreviationNode extends MDInlineNode {
3663 3702
 	 * @param {string} definition
3664 3703
 	 */
3665 3704
 	public function __construct(string $abbreviation, string $definition) {
3666
-		super([]);
3705
+		parent::__construct([]);
3667 3706
 		$this->abbreviation = $abbreviation;
3668 3707
 		$this->attributes['title'] = $definition;
3669 3708
 	}
@@ -3865,6 +3904,7 @@ class Markdown {
3865 3904
 	 * @param string $elementIdPrefix
3866 3905
 	 */
3867 3906
 	private function investigateException(array $lines, string $elementIdPrefix) {
3907
+		print("Investigating error...\n");
3868 3908
 		$startIndex = 0;
3869 3909
 		$endIndex = sizeof($lines);
3870 3910
 		// Keep stripping away first line until an exception stops being thrown
@@ -3886,7 +3926,7 @@ class Markdown {
3886 3926
 			}
3887 3927
 		}
3888 3928
 		$problematicMarkdown = implode("\n", array_slice($lines, $startIndex, $endIndex));
3889
-		print("This portion of markdown caused an unexpected exception: {$problematicMarkdown}");
3929
+		print("This portion of markdown caused an unexpected exception:\n{$problematicMarkdown}\n");
3890 3930
 	}
3891 3931
 }
3892 3932
 ?>

Načítá se…
Zrušit
Uložit