Browse Source

Sample playground markdown working in PHP

main
Rocketsoup 1 year ago
parent
commit
e6972770e9
3 changed files with 149 additions and 108 deletions
  1. 5
    4
      js/markdown.js
  2. 1
    1
      js/markdown.min.js
  3. 143
    103
      php/markdown.php

+ 5
- 4
js/markdown.js View File

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

+ 1
- 1
js/markdown.min.js
File diff suppressed because it is too large
View File


+ 143
- 103
php/markdown.php View File

90
 		return false;
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
 	public static function equalAssocArrays(array &$a, array &$b) {
98
 	public static function equalAssocArrays(array &$a, array &$b) {
94
 		return empty(array_diff_assoc($a, $b));
99
 		return empty(array_diff_assoc($a, $b));
95
 	}
100
 	}
217
 	}
222
 	}
218
 
223
 
219
 	public function __toString(): string {
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
 	 * @return ?MDTokenMatch match object, or `null` if not found
326
 	 * @return ?MDTokenMatch match object, or `null` if not found
322
 	 */
327
 	 */
323
 	public static function findFirstTokens(array $tokensToSearch, array $pattern, int $startIndex=0): ?MDTokenMatch {
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
 		$matched = [];
332
 		$matched = [];
325
 		for ($t = $startIndex; $t < sizeof($tokensToSearch); $t++) {
333
 		for ($t = $startIndex; $t < sizeof($tokensToSearch); $t++) {
326
 			$matchedAll = true;
334
 			$matchedAll = true;
327
 			$matched = [];
335
 			$matched = [];
328
 			$patternOffset = 0;
336
 			$patternOffset = 0;
329
-			for ($p = 0; $p < mb_strlen($pattern); $p++) {
337
+			for ($p = 0; $p < sizeof($pattern); $p++) {
330
 				$t0 = $t + $p + $patternOffset;
338
 				$t0 = $t + $p + $patternOffset;
331
 				if ($t0 >= sizeof($tokensToSearch)) return null;
339
 				if ($t0 >= sizeof($tokensToSearch)) return null;
332
 				$token = $tokensToSearch[$t0];
340
 				$token = $tokensToSearch[$t0];
383
 			array $startPattern, array $endPattern, ?callable $contentValidator=null,
391
 			array $startPattern, array $endPattern, ?callable $contentValidator=null,
384
 			int $startIndex=0): ?MDPairedTokenMatch {
392
 			int $startIndex=0): ?MDPairedTokenMatch {
385
 		for ($s = $startIndex; $s < sizeof($tokensToSearch); $s++) {
393
 		for ($s = $startIndex; $s < sizeof($tokensToSearch); $s++) {
386
-			$startMatch = findFirstTokens($tokensToSearch, $startPattern, $s);
394
+			$startMatch = self::findFirstTokens($tokensToSearch, $startPattern, $s);
387
 			if ($startMatch === null) return null;
395
 			if ($startMatch === null) return null;
388
 			$endStart = $startMatch->index + sizeof($startMatch->tokens);
396
 			$endStart = $startMatch->index + sizeof($startMatch->tokens);
389
 			while ($endStart < sizeof($tokensToSearch)) {
397
 			while ($endStart < sizeof($tokensToSearch)) {
390
-				$endMatch = findFirstTokens($tokensToSearch, $endPattern, $endStart);
398
+				$endMatch = self::findFirstTokens($tokensToSearch, $endPattern, $endStart);
391
 				if ($endMatch === null) break;
399
 				if ($endMatch === null) break;
392
 				$contentStart = $startMatch->index + sizeof($startMatch->tokens);
400
 				$contentStart = $startMatch->index + sizeof($startMatch->tokens);
393
 				$contentLength = $endMatch->index - $contentStart;
401
 				$contentLength = $endMatch->index - $contentStart;
506
 	 */
514
 	 */
507
 	public function __construct(array $lines) {
515
 	public function __construct(array $lines) {
508
 		$this->lines = $lines;
516
 		$this->lines = $lines;
517
+		$this->startTime = microtime(true);
509
 	}
518
 	}
510
 
519
 
511
 	/**
520
 	/**
573
 			$block = $reader->readBlock($this);
582
 			$block = $reader->readBlock($this);
574
 			if ($block) {
583
 			if ($block) {
575
 				if ($this->p == $startP) {
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
 					throw new Error("{$readerClassName} returned an " .
587
 					throw new Error("{$readerClassName} returned an " .
579
 						"{$blockClassName} without incrementing MDState.p. " .
588
 						"{$blockClassName} without incrementing MDState.p. " .
580
 						"This could lead to an infinite loop.");
589
 						"This could lead to an infinite loop.");
598
 		$expectLiteral = false;
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
 		$endText = function() use (&$tokens, &$text) {
612
 		$endText = function() use (&$tokens, &$text) {
604
 			if (mb_strlen($text) == 0) return;
613
 			if (mb_strlen($text) == 0) return;
605
 			$textGroups = [];
614
 			$textGroups = [];
606
-			if (mb_eregi('^(\s+)(.*?)$', $text, $textGroups)) {
615
+			if (mb_eregi('^(\\s+)(.*?)$', $text, $textGroups)) {
607
 				array_push($tokens, new MDToken($textGroups[1], MDTokenType::Whitespace, $textGroups[1]));
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
 				array_push($tokens, new MDToken($textGroups[1], MDTokenType::Text, $textGroups[1]));
620
 				array_push($tokens, new MDToken($textGroups[1], MDTokenType::Text, $textGroups[1]));
612
 				array_push($tokens, new MDToken($textGroups[2], MDTokenType::Whitespace, $textGroups[2]));
621
 				array_push($tokens, new MDToken($textGroups[2], MDTokenType::Whitespace, $textGroups[2]));
613
 			} else {
622
 			} else {
635
 				$endText();
644
 				$endText();
636
 				array_push($tokens, $token);
645
 				array_push($tokens, $token);
637
 				if ($token->original == null || mb_strlen($token->original) == 0) {
646
 				if ($token->original == null || mb_strlen($token->original) == 0) {
638
-					$readerClassName = get_class($reader);
647
+					$readerClassName = MDUtils::typename($reader);
639
 					throw new Error(`{$readerClassName} returned a token with an empty .original. This would cause an infinite loop.`);
648
 					throw new Error(`{$readerClassName} returned a token with an empty .original. This would cause an infinite loop.`);
640
 				}
649
 				}
641
 				$p += mb_strlen($token->original) - 1;
650
 				$p += mb_strlen($token->original) - 1;
703
 		// CSS modifiers.
712
 		// CSS modifiers.
704
 		$lastNode = null;
713
 		$lastNode = null;
705
 		$me = $this;
714
 		$me = $this;
706
-		$nodes = array_map(function($node) use ($lastNode, $me) {
715
+		$nodes = array_map(function($node) use (&$lastNode, $me, $nodes) {
707
 			if ($node instanceof MDToken) {
716
 			if ($node instanceof MDToken) {
708
 				/** @var MDToken */
717
 				/** @var MDToken */
709
 				$token = $node;
718
 				$token = $node;
719
 				$lastNode = ($node instanceof MDTextNode) ? null : $node;
728
 				$lastNode = ($node instanceof MDTextNode) ? null : $node;
720
 				return $node;
729
 				return $node;
721
 			} else {
730
 			} else {
722
-				$nodeClassName = get_class($node);
731
+				$nodeClassName = MDUtils::typename($node);
723
 				throw new Error("Unexpected node type {$nodeClassName}");
732
 				throw new Error("Unexpected node type {$nodeClassName}");
724
 			}
733
 			}
725
 		}, $nodes);
734
 		}, $nodes);
727
 		return $nodes;
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
 	 * Mapping of reference symbols to URLs. Used by `MDReferencedLinkReader`
753
 	 * Mapping of reference symbols to URLs. Used by `MDReferencedLinkReader`
732
 	 * and `MDReferencedImageReader`.
754
 	 * and `MDReferencedImageReader`.
1671
 		$sorted = $readers;
1693
 		$sorted = $readers;
1672
 		return self::kahnTopologicalSort($sorted, function(MDReader $a, MDReader $b): int {
1694
 		return self::kahnTopologicalSort($sorted, function(MDReader $a, MDReader $b): int {
1673
 			return $a->compareBlockOrdering($b);
1695
 			return $a->compareBlockOrdering($b);
1674
-		}, fn($elem) => get_class($elem));
1696
+		}, fn($elem) => MDUtils::typename($elem));
1675
 	}
1697
 	}
1676
 
1698
 
1677
 	/**
1699
 	/**
1684
 		$sorted = $readers;
1706
 		$sorted = $readers;
1685
 		return self::kahnTopologicalSort($sorted, function(MDReader $a, MDReader $b): int {
1707
 		return self::kahnTopologicalSort($sorted, function(MDReader $a, MDReader $b): int {
1686
 			return $a->compareTokenizeOrdering($b);
1708
 			return $a->compareTokenizeOrdering($b);
1687
-		}, fn($elem) => get_class($elem));
1709
+		}, fn($elem) => MDUtils::typename($elem));
1688
 	}
1710
 	}
1689
 
1711
 
1690
 	/**
1712
 	/**
1704
 		$maxPass = 1;
1726
 		$maxPass = 1;
1705
 		foreach ($readers as $reader) {
1727
 		foreach ($readers as $reader) {
1706
 			$passCount = $reader->substitutionPassCount();
1728
 			$passCount = $reader->substitutionPassCount();
1729
+			$maxPass = max($paxPass, $passCount);
1707
 			for ($pass = 1; $pass <= $passCount; $pass++) {
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
 		$result = [];
1734
 		$result = [];
1713
 		for ($pass = 1; $pass <= $maxPass; $pass++) {
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
 			$passResult = self::kahnTopologicalSort($readersThisPass, function(array $a, array $b) use ($pass): int {
1737
 			$passResult = self::kahnTopologicalSort($readersThisPass, function(array $a, array $b) use ($pass): int {
1716
 				$aReader = $a[1];
1738
 				$aReader = $a[1];
1717
 				$bReader = $b[1];
1739
 				$bReader = $b[1];
1718
 				return $aReader->compareSubstituteOrdering($bReader, $pass);
1740
 				return $aReader->compareSubstituteOrdering($bReader, $pass);
1719
-			}, fn($elem) => get_class($elem[1]));
1741
+			}, fn($elem) => MDUtils::typename($elem[1]));
1720
 			$result = array_merge($result, $passResult);
1742
 			$result = array_merge($result, $passResult);
1721
 		}
1743
 		}
1722
 		return $result;
1744
 		return $result;
1870
 		$state->p += max(sizeof($itemLines), 1);
1892
 		$state->p += max(sizeof($itemLines), 1);
1871
 
1893
 
1872
 		if (sizeof($itemLines) == 1) {
1894
 		if (sizeof($itemLines) == 1) {
1873
-			return $state->inlineMarkdownToNode($itemLines[0]);
1895
+			return new MDBlockNode($state->inlineMarkdownToNodes($itemLines[0]));
1874
 		}
1896
 		}
1875
 
1897
 
1876
 		$hasBlankLines = sizeof(array_filter($itemLines, fn($line) => trim($line) == '')) > 0;
1898
 		$hasBlankLines = sizeof(array_filter($itemLines, fn($line) => trim($line) == '')) > 0;
1877
 		if ($hasBlankLines) {
1899
 		if ($hasBlankLines) {
1878
 			$substate = $state->copy($itemLines);
1900
 			$substate = $state->copy($itemLines);
1879
 			$blocks = $substate->readBlocks();
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
 		// Multiline content with no blank lines. Search for new block
1905
 		// Multiline content with no blank lines. Search for new block
1886
 			$line = $itemLines[$p];
1908
 			$line = $itemLines[$p];
1887
 			if (mb_eregi('^(?:\\*|\\-|\\+|\\d+\\.)\\s+', $line)) {
1909
 			if (mb_eregi('^(?:\\*|\\-|\\+|\\d+\\.)\\s+', $line)) {
1888
 				// Nested list found
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
 				$substate = $state->copy(array_slice($itemLines, $p));
1912
 				$substate = $state->copy(array_slice($itemLines, $p));
1891
 				$blocks = $substate->readBlocks();
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
 		{
1919
 		{
1898
 			$substate = $state->copy($itemLines);
1920
 			$substate = $state->copy($itemLines);
1899
 			$blocks = $substate->readBlocks();
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
 	public function readBlock(MDState $state): ?MDBlockNode {
1926
 	public function readBlock(MDState $state): ?MDBlockNode {
1905
-		$className = get_class($this);
1927
+		$className = MDUtils::typename($this);
1906
 		throw new Error("Abstract readBlock must be overridden in {$className}");
1928
 		throw new Error("Abstract readBlock must be overridden in {$className}");
1907
 	}
1929
 	}
1908
 }
1930
 }
1914
 	private static string $unorderedListRegex = '^([\\*\\+\\-]\\s+)(.*)$';  // 1=bullet, 2=content
1936
 	private static string $unorderedListRegex = '^([\\*\\+\\-]\\s+)(.*)$';  // 1=bullet, 2=content
1915
 
1937
 
1916
 	private function readUnorderedListItem(MDState $state): ?MDListItemNode {
1938
 	private function readUnorderedListItem(MDState $state): ?MDListItemNode {
1939
+		if (!$state->hasLines(1)) return null;
1917
 		$p = $state->p;
1940
 		$p = $state->p;
1918
 		$line = $state->lines[$p];
1941
 		$line = $state->lines[$p];
1919
 		if (!mb_eregi(self::$unorderedListRegex, $line, $groups)) return null;
1942
 		if (!mb_eregi(self::$unorderedListRegex, $line, $groups)) return null;
1942
 	private static string $orderedListRegex = '^(\\d+)(\\.\\s+)(.*)$';  // 1=number, 2=dot, 3=content
1965
 	private static string $orderedListRegex = '^(\\d+)(\\.\\s+)(.*)$';  // 1=number, 2=dot, 3=content
1943
 
1966
 
1944
 	private function readOrderedListItem(MDState $state): ?MDListItemNode {
1967
 	private function readOrderedListItem(MDState $state): ?MDListItemNode {
1968
+		if (!$state->hasLines(1)) return null;
1945
 		$p = $state->p;
1969
 		$p = $state->p;
1946
 		$line = $state->lines[$p];
1970
 		$line = $state->lines[$p];
1947
 		if (!mb_eregi(self::$orderedListRegex, $line, $groups)) return null;
1971
 		if (!mb_eregi(self::$orderedListRegex, $line, $groups)) return null;
1976
 		$p = $state->p;
2000
 		$p = $state->p;
1977
 		$openFenceLine = $state->lines[$p++];
2001
 		$openFenceLine = $state->lines[$p++];
1978
 		[$openFenceLine, $modifier] = MDTagModifier::fromLine($openFenceLine, $state);
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
 		$language = mb_strlen($groups[1]) > 0 ? $groups[1] : null;
2004
 		$language = mb_strlen($groups[1]) > 0 ? $groups[1] : null;
1981
 		$codeLines = [];
2005
 		$codeLines = [];
1982
 		while ($state->hasLines(1, $p)) {
2006
 		while ($state->hasLines(1, $p)) {
2056
 		if (str_starts_with($line, '|')) $line = mb_substr($line, 1);
2080
 		if (str_starts_with($line, '|')) $line = mb_substr($line, 1);
2057
 		if (str_ends_with($line, '|')) $line = mb_substr($line, 0, mb_strlen($line) - 1);
2081
 		if (str_ends_with($line, '|')) $line = mb_substr($line, 0, mb_strlen($line) - 1);
2058
 		$cellTokens = explode('|', $line);
2082
 		$cellTokens = explode('|', $line);
2059
-		$cells = array_map(function($token) use ($isHeader) {
2083
+		$cells = array_map(function($token) use ($isHeader, $state) {
2060
 			$content = $state->inlineMarkdownToNode(trim($token));
2084
 			$content = $state->inlineMarkdownToNode(trim($token));
2061
 			return $isHeader ? new MDTableHeaderCellNode($content) : new MDTableCellNode($content);
2085
 			return $isHeader ? new MDTableHeaderCellNode($content) : new MDTableCellNode($content);
2062
 		}, $cellTokens);
2086
 		}, $cellTokens);
2144
 			}
2168
 			}
2145
 		}
2169
 		}
2146
 		if ($termCount == 0 || $definitionCount == 0) return null;
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
 				return new MDDefinitionListDefinitionNode($state->inlineMarkdownToNodes($groups[1]));
2173
 				return new MDDefinitionListDefinitionNode($state->inlineMarkdownToNodes($groups[1]));
2150
 			} else {
2174
 			} else {
2151
 				return new MDDefinitionListTermNode($state->inlineMarkdownToNodes($line));
2175
 				return new MDDefinitionListTermNode($state->inlineMarkdownToNodes($line));
2181
 		$instances = $footnoteInstances[$symbol] ?? [];
2205
 		$instances = $footnoteInstances[$symbol] ?? [];
2182
 		array_push($instances, $unique);
2206
 		array_push($instances, $unique);
2183
 		$footnoteInstances[$symbol] = $instances;
2207
 		$footnoteInstances[$symbol] = $instances;
2208
+		$state->root()->userInfo['footnoteInstances'] = $footnoteInstances;
2184
 	}
2209
 	}
2185
 
2210
 
2186
 	private function idForFootnoteSymbol(MDState $state, string $symbol): int {
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
 		$id = $nextFootnoteId++;
2216
 		$id = $nextFootnoteId++;
2192
 		$footnoteIds[$symbol] = $id;
2217
 		$footnoteIds[$symbol] = $id;
2193
 		$state->root()->userInfo['nextFootnoteId'] = $nextFootnoteId;
2218
 		$state->root()->userInfo['nextFootnoteId'] = $nextFootnoteId;
2219
+		$state->root()->userInfo['footnoteIds'] = $footnoteIds;
2194
 		return $id;
2220
 		return $id;
2195
 	}
2221
 	}
2196
 
2222
 
2218
 		$content = $state->inlineMarkdownToNodes($def);
2244
 		$content = $state->inlineMarkdownToNodes($def);
2219
 		$this->defineFootnote($state, $symbol, $content);
2245
 		$this->defineFootnote($state, $symbol, $content);
2220
 		$state->p = $p;
2246
 		$state->p = $p;
2221
-		return new MDNode(); // empty
2247
+		return new MDBlockNode(); // empty
2222
 	}
2248
 	}
2223
 
2249
 
2224
 	public function readToken(MDState $state, string $line): ?MDToken {
2250
 	public function readToken(MDState $state, string $line): ?MDToken {
2233
 	}
2259
 	}
2234
 
2260
 
2235
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
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
 			$symbol = $match->tokens[0]->content;
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
 			return true;
2265
 			return true;
2240
 		}
2266
 		}
2241
 		return false;
2267
 		return false;
2248
 	public function postProcess(MDState $state, array &$blocks) {
2274
 	public function postProcess(MDState $state, array &$blocks) {
2249
 		$nextOccurrenceId = 1;
2275
 		$nextOccurrenceId = 1;
2250
 		foreach ($blocks as $block) {
2276
 		foreach ($blocks as $block) {
2251
-			$block->visitChildren(function($node) use (&$nextOccurrenceId) {
2277
+			$block->visitChildren(function($node) use (&$nextOccurrenceId, $state) {
2252
 				if (!($node instanceof MDFootnoteNode)) return;
2278
 				if (!($node instanceof MDFootnoteNode)) return;
2253
 				$node->footnoteId = $this->idForFootnoteSymbol($state, $node->symbol);
2279
 				$node->footnoteId = $this->idForFootnoteSymbol($state, $node->symbol);
2254
 				$node->occurrenceId = $nextOccurrenceId++;
2280
 				$node->occurrenceId = $nextOccurrenceId++;
2255
 				$node->displaySymbol = strval($node->footnoteId);
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
 		if (sizeof($state->userInfo['footnotes']) == 0) return;
2285
 		if (sizeof($state->userInfo['footnotes']) == 0) return;
2290
  */
2316
  */
2291
 class MDAbbreviationReader extends MDReader {
2317
 class MDAbbreviationReader extends MDReader {
2292
 	private function defineAbbreviation(MDState $state, string $abbreviation, string $definition) {
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
 	public function preProcess(MDState $state) {
2324
 	public function preProcess(MDState $state) {
2299
 		$state->root()->userInfo['abbreviations'] = [];
2325
 		$state->root()->userInfo['abbreviations'] = [];
2300
-		$state->root()->userInfo['abbreviationRegexes'] = [];
2301
 	}
2326
 	}
2302
 
2327
 
2303
 	public function readBlock(MDState $state): ?MDBlockNode {
2328
 	public function readBlock(MDState $state): ?MDBlockNode {
2308
 		$def = $groups[2];
2333
 		$def = $groups[2];
2309
 		$this->defineAbbreviation($state, $abbrev, $def);
2334
 		$this->defineAbbreviation($state, $abbrev, $def);
2310
 		$state->p = $p;
2335
 		$state->p = $p;
2311
-		return new MDNode(); // empty
2336
+		return new MDBlockNode(); // empty
2312
 	}
2337
 	}
2313
 
2338
 
2314
 	/**
2339
 	/**
2317
 	 */
2342
 	 */
2318
 	public function postProcess(MDState $state, array &$blocks) {
2343
 	public function postProcess(MDState $state, array &$blocks) {
2319
 		$abbreviations = $state->root()->userInfo['abbreviations'];
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
 			if (!($original instanceof MDTextNode)) return null;
2346
 			if (!($original instanceof MDTextNode)) return null;
2323
 			$changed = false;
2347
 			$changed = false;
2324
 			$elems = [ $original->text ]; // mix of strings and MDNodes
2348
 			$elems = [ $original->text ]; // mix of strings and MDNodes
2325
 			for ($i = 0; $i < sizeof($elems); $i++) {
2349
 			for ($i = 0; $i < sizeof($elems); $i++) {
2326
 				$text = $elems[$i];
2350
 				$text = $elems[$i];
2327
 				if (!is_string($text)) continue;
2351
 				if (!is_string($text)) continue;
2328
-				foreach ($abbreviations as $abbreviation) {
2352
+				foreach ($abbreviations as $abbreviation => $definition) {
2329
 					$index = strpos($text, $abbreviation);
2353
 					$index = strpos($text, $abbreviation);
2330
-					if ($index === false) break;
2354
+					if ($index === false) continue;
2331
 					$prefix = substr($text, 0, $index);
2355
 					$prefix = substr($text, 0, $index);
2332
 					$suffix = substr($text, $index + strlen($abbreviation));
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
 					$i = -1; // start over
2358
 					$i = -1; // start over
2336
 					$changed = true;
2359
 					$changed = true;
2337
 					break;
2360
 					break;
2338
 				}
2361
 				}
2339
 			}
2362
 			}
2340
 			if (!$changed) return null;
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
 			return new MDNode($nodes);
2365
 			return new MDNode($nodes);
2343
 		});
2366
 		});
2344
 	}
2367
 	}
2417
 		// #4: singles with paired inner tokens
2440
 		// #4: singles with paired inner tokens
2418
 		if ($count == 1 && $pass != 2 && $pass != 4) return false;
2441
 		if ($count == 1 && $pass != 2 && $pass != 4) return false;
2419
 		if ($count > 1 && $pass != 1 && $pass != 3) return false;
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
 		$isFirstOfMultiplePasses = $this->substitutionPassCount() > 1 && $pass == 1;
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
 			$firstType = $content[0] instanceof MDToken ? $content[0]->type : null;
2446
 			$firstType = $content[0] instanceof MDToken ? $content[0]->type : null;
2425
 			$lastType = $content[sizeof($content) - 1] instanceof MDToken ? $content[sizeof($content) - 1]->type : null;
2447
 			$lastType = $content[sizeof($content) - 1] instanceof MDToken ? $content[sizeof($content) - 1]->type : null;
2426
 			if ($firstType == MDTokenType::Whitespace) return false;
2448
 			if ($firstType == MDTokenType::Whitespace) return false;
2427
 			if ($lastType == MDTokenType::Whitespace) return false;
2449
 			if ($lastType == MDTokenType::Whitespace) return false;
2428
 			foreach ($content as $token) {
2450
 			foreach ($content as $token) {
2429
 				// Don't allow nesting
2451
 				// Don't allow nesting
2430
-				if (get_class($token) == $nodeClass) return false;
2452
+				if (MDUtils::typename($token) == $nodeClass) return false;
2431
 			}
2453
 			}
2432
 			if ($isFirstOfMultiplePasses) {
2454
 			if ($isFirstOfMultiplePasses) {
2433
 				$innerCount = 0;
2455
 				$innerCount = 0;
2439
 			return true;
2461
 			return true;
2440
 		});
2462
 		});
2441
 		if ($match === null) return false;
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
 		$ref = new ReflectionClass($nodeClass);
2470
 		$ref = new ReflectionClass($nodeClass);
2446
 		$node = $ref->newInstanceArgs([ $content ]);
2471
 		$node = $ref->newInstanceArgs([ $content ]);
2447
-		array_splice($tokens, $match->startIndex, $match->totalLength, [ $node ]);
2472
+		array_splice($tokens, $match->startIndex, $match->totalLength, [$node]);
2448
 		return true;
2473
 		return true;
2449
 	}
2474
 	}
2475
+
2476
+	private static $firstTime = null;
2450
 }
2477
 }
2451
 
2478
 
2452
 /**
2479
 /**
2582
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
2609
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
2583
 		if ($this->attemptPair($state, $pass, $tokens, 'MDCodeNode', MDTokenType::Backtick, 2, true)) return true;
2610
 		if ($this->attemptPair($state, $pass, $tokens, 'MDCodeNode', MDTokenType::Backtick, 2, true)) return true;
2584
 		if ($this->attemptPair($state, $pass, $tokens, 'MDCodeNode', MDTokenType::Backtick, 1, true)) return true;
2611
 		if ($this->attemptPair($state, $pass, $tokens, 'MDCodeNode', MDTokenType::Backtick, 1, true)) return true;
2612
+		return false;
2585
 	}
2613
 	}
2586
 }
2614
 }
2587
 
2615
 
2654
 			$text = $match->tokens[0]->content;
2682
 			$text = $match->tokens[0]->content;
2655
 			$url = $match->tokens[sizeof($match->tokens) - 1]->content;
2683
 			$url = $match->tokens[sizeof($match->tokens) - 1]->content;
2656
 			$title = $match->tokens[sizeof($match->tokens) - 1]->extra;
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
 			return true;
2686
 			return true;
2659
 		}
2687
 		}
2660
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::Label, MDTokenType::META_OptionalWhitespace, MDTokenType::Email ])) {
2688
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::Label, MDTokenType::META_OptionalWhitespace, MDTokenType::Email ])) {
2662
 			$email = $match->tokens[sizeof($match->tokens) - 1]->content;
2690
 			$email = $match->tokens[sizeof($match->tokens) - 1]->content;
2663
 			$url = "mailto:{$email}";
2691
 			$url = "mailto:{$email}";
2664
 			$title = $match->tokens[sizeof($match->tokens) - 1]->extra;
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
 			return true;
2694
 			return true;
2667
 		}
2695
 		}
2668
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::SimpleEmail ])) {
2696
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::SimpleEmail ])) {
2669
 			$token = $match->tokens[0];
2697
 			$token = $match->tokens[0];
2670
 			$link = "mailto:{$token->content}";
2698
 			$link = "mailto:{$token->content}";
2671
 			$node = new MDLinkNode($link, new MDObfuscatedTextNode($token->content));
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
 			return true;
2701
 			return true;
2674
 		}
2702
 		}
2675
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::SimpleLink ])) {
2703
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::SimpleLink ])) {
2676
 			$token = $match->tokens[0];
2704
 			$token = $match->tokens[0];
2677
 			$link = $token->content;
2705
 			$link = $token->content;
2678
 			$node = new MDLinkNode($link, new MDTextNode($link));
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
 			return true;
2708
 			return true;
2681
 		}
2709
 		}
2682
 		return false;
2710
 		return false;
2701
 			if (mb_eregi('^\\s*\\[(.+?)]:\\s*(\\S+)\\s*$', $line, $groups)) {
2729
 			if (mb_eregi('^\\s*\\[(.+?)]:\\s*(\\S+)\\s*$', $line, $groups)) {
2702
 				$symbol = $groups[1];
2730
 				$symbol = $groups[1];
2703
 				$url = $groups[2];
2731
 				$url = $groups[2];
2732
+				$title = null;
2704
 			} else {
2733
 			} else {
2705
 				return null;
2734
 				return null;
2706
 			}
2735
 			}
2707
 		}
2736
 		}
2708
 		$state->defineURL($symbol, $url, $title);
2737
 		$state->defineURL($symbol, $url, $title);
2709
 		$state->p = $p;
2738
 		$state->p = $p;
2710
-		return new MDNode([]); // empty
2739
+		return new MDBlockNode([]); // empty
2711
 	}
2740
 	}
2712
 
2741
 
2713
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
2742
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
2714
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::Label, MDTokenType::META_OptionalWhitespace, MDTokenType::Label ])) {
2743
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::Label, MDTokenType::META_OptionalWhitespace, MDTokenType::Label ])) {
2715
 			$text = $match->tokens[0]->content;
2744
 			$text = $match->tokens[0]->content;
2716
 			$ref = $match->tokens[sizeof($match->tokens) - 1]->content;
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
 			return true;
2747
 			return true;
2719
 		}
2748
 		}
2720
 		return false;
2749
 		return false;
2742
 			if ($title !== null) {
2771
 			if ($title !== null) {
2743
 				$node->attributes['title'] = $title;
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
 			return true;
2775
 			return true;
2747
 		}
2776
 		}
2748
 		return false;
2777
 		return false;
2773
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::Bang, MDTokenType::Label, MDTokenType::META_OptionalWhitespace, MDTokenType::Label ])) {
2802
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::Bang, MDTokenType::Label, MDTokenType::META_OptionalWhitespace, MDTokenType::Label ])) {
2774
 			$alt = $match->tokens[1]->content;
2803
 			$alt = $match->tokens[1]->content;
2775
 			$ref = $match->tokens[sizeof($match->tokens) - 1]->content;
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
 			return true;
2806
 			return true;
2778
 		}
2807
 		}
2779
 		return false;
2808
 		return false;
2827
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
2856
 	public function substituteTokens(MDState $state, int $pass, array &$tokens): bool {
2828
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::HTMLTag ])) {
2857
 		if ($match = MDToken::findFirstTokens($tokens, [ MDTokenType::HTMLTag ])) {
2829
 			$tag = $match->tokens[0]->tag;
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
 			return true;
2860
 			return true;
2832
 		}
2861
 		}
2833
 		return false;
2862
 		return false;
2897
 		if (is_array($children)) {
2926
 		if (is_array($children)) {
2898
 			foreach ($children as $elem) {
2927
 			foreach ($children as $elem) {
2899
 				if (!($elem instanceof MDNode)) {
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
 					throw new Error("{$thisClassName} expects children of type MDNode[] or MDNode, got array with {$elemClassName} element");
2931
 					throw new Error("{$thisClassName} expects children of type MDNode[] or MDNode, got array with {$elemClassName} element");
2903
 				}
2932
 				}
2904
 			}
2933
 			}
2906
 		} elseif ($children instanceof MDNode) {
2935
 		} elseif ($children instanceof MDNode) {
2907
 			$this->children = [ $children ];
2936
 			$this->children = [ $children ];
2908
 		} else {
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
 			throw new Error("{$thisClassName} expects children of type MDNode[] or MDNode, got {$elemClassName}");
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
 	 * Adds a CSS class. If already present it will not be duplicated.
2954
 	 * Adds a CSS class. If already present it will not be duplicated.
2917
 	 */
2955
 	 */
2969
 	protected function htmlAttributes(): string {
3007
 	protected function htmlAttributes(): string {
2970
 		$html = '';
3008
 		$html = '';
2971
 		if (sizeof($this->cssClasses) > 0) {
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
 		if ($this->cssId !== null && mb_strlen($this->cssId) > 0) {
3013
 		if ($this->cssId !== null && mb_strlen($this->cssId) > 0) {
2976
-			$html .= " id=\"{$this->cssId}\"";
3014
+			$html .= " id=\"" . htmlentities($this->cssId) . "\"";
2977
 		}
3015
 		}
2978
 		$styles = [];
3016
 		$styles = [];
2979
 		foreach ($this->cssStyles as $key => $value) {
3017
 		foreach ($this->cssStyles as $key => $value) {
2980
 			array_push($styles, "{$key}: {$value};");
3018
 			array_push($styles, "{$key}: {$value};");
2981
 		}
3019
 		}
2982
 		if (sizeof($styles) > 0) {
3020
 		if (sizeof($styles) > 0) {
2983
-			$escaped = htmlspecialchars(implode(' ', $styles));
3021
+			$escaped = htmlentities(implode(' ', $styles));
2984
 			$html .= " style=\"{$escaped}\"";
3022
 			$html .= " style=\"{$escaped}\"";
2985
 		}
3023
 		}
2986
 		foreach ($this->attributes as $key => $value) {
3024
 		foreach ($this->attributes as $key => $value) {
2987
 			if ($key === 'class' || $key === 'id' || $key === 'style') continue;
3025
 			if ($key === 'class' || $key === 'id' || $key === 'style') continue;
2988
 			$cleanKey = MDUtils::scrubAttributeName($key);
3026
 			$cleanKey = MDUtils::scrubAttributeName($key);
2989
 			if (mb_strlen($cleanKey) == 0) continue;
3027
 			if (mb_strlen($cleanKey) == 0) continue;
2990
-			$cleanValue = htmlspecialchars($value);
3028
+			$cleanValue = htmlentities($value);
2991
 			$html .= " {$cleanKey}=\"{$cleanValue}\"";
3029
 			$html .= " {$cleanKey}=\"{$cleanValue}\"";
2992
 		}
3030
 		}
2993
 		return $html;
3031
 		return $html;
3106
 	public function __construct(int $level, array $children) {
3144
 	public function __construct(int $level, array $children) {
3107
 		parent::__construct($children);
3145
 		parent::__construct($children);
3108
 		if (!is_int($level) || ($level < 1 || $level > 6)) {
3146
 		if (!is_int($level) || ($level < 1 || $level > 6)) {
3109
-			$thisClassName = get_class($this);
3147
+			$thisClassName = MDUtils::typename($this);
3110
 			throw new Error("{$thisClassName} requires heading level 1 to 6");
3148
 			throw new Error("{$thisClassName} requires heading level 1 to 6");
3111
 		}
3149
 		}
3112
 		$this->level = $level;
3150
 		$this->level = $level;
3162
 class MDOrderedListNode extends MDBlockNode {
3200
 class MDOrderedListNode extends MDBlockNode {
3163
 	/** @var MDListItemNode[] $children */
3201
 	/** @var MDListItemNode[] $children */
3164
 
3202
 
3165
-	public int $startOrdinal;
3203
+	public ?int $startOrdinal;
3166
 
3204
 
3167
 	/**
3205
 	/**
3168
 	 * @param MDListItemNode[] $children
3206
 	 * @param MDListItemNode[] $children
3185
  * An item in a bulleted or numbered list.
3223
  * An item in a bulleted or numbered list.
3186
  */
3224
  */
3187
 class MDListItemNode extends MDBlockNode {
3225
 class MDListItemNode extends MDBlockNode {
3188
-	public int $ordinal;
3226
+	public ?int $ordinal;
3189
 
3227
 
3190
 	/**
3228
 	/**
3191
 	 * @param MDNode|MDNode[] $children
3229
 	 * @param MDNode|MDNode[] $children
3192
 	 * @param ?int $ordinal
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
 		parent::__construct($children);
3233
 		parent::__construct($children);
3196
 		$this->ordinal = $ordinal;
3234
 		$this->ordinal = $ordinal;
3197
 	}
3235
 	}
3213
 	public ?string $language;
3251
 	public ?string $language;
3214
 
3252
 
3215
 	public function __construct(string $text, ?string $language=null) {
3253
 	public function __construct(string $text, ?string $language=null) {
3216
-		super([]);
3254
+		parent::__construct([]);
3217
 		$this->text = $text;
3255
 		$this->text = $text;
3218
 		$this->language = $language;
3256
 		$this->language = $language;
3219
 	}
3257
 	}
3251
 	 * @param MDTableRowNode $headerRow
3289
 	 * @param MDTableRowNode $headerRow
3252
 	 * @param MDTableRowNode[] $bodyRows
3290
 	 * @param MDTableRowNode[] $bodyRows
3253
 	 */
3291
 	 */
3254
-	public function __construct(MDTableRow $headerRow, array $bodyRows) {
3292
+	public function __construct(MDTableRowNode $headerRow, array $bodyRows) {
3255
 		parent::__construct(array_merge([ $headerRow ], $bodyRows));
3293
 		parent::__construct(array_merge([ $headerRow ], $bodyRows));
3256
 	}
3294
 	}
3257
 
3295
 
3280
 		$this->applyAlignments();
3318
 		$this->applyAlignments();
3281
 		$html = '';
3319
 		$html = '';
3282
 		$html .= "<table" . $this->htmlAttributes() . ">\n";
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
 		$html .= "</thead>\n";
3323
 		$html .= "</thead>\n";
3286
 		$html .= "<tbody>\n";
3324
 		$html .= "<tbody>\n";
3287
-		$html .= MDNode::toHTML($this->bodyRows, $state) . "\n";
3325
+		$html .= MDNode::arrayToHTML($this->bodyRows(), $state) . "\n";
3288
 		$html .= "</tbody>\n";
3326
 		$html .= "</tbody>\n";
3289
 		$html .= "</table>\n";
3327
 		$html .= "</table>\n";
3290
 		return $html;
3328
 		return $html;
3314
 /**
3352
 /**
3315
  * Node for a header cell in a header table row.
3353
  * Node for a header cell in a header table row.
3316
  */
3354
  */
3317
-class MDTableHeaderCellNode extends MDBlockNode {
3355
+class MDTableHeaderCellNode extends MDTableCellNode {
3318
 	public function toHTML(MDState $state): string {
3356
 	public function toHTML(MDState $state): string {
3319
 		return $this->simplePairedTagHTML($state, 'th');
3357
 		return $this->simplePairedTagHTML($state, 'th');
3320
 	}
3358
 	}
3354
  * content.
3392
  * content.
3355
  */
3393
  */
3356
 class MDFootnoteListNode extends MDBlockNode {
3394
 class MDFootnoteListNode extends MDBlockNode {
3357
-	private function footnoteId(MDState $state, string $symbol): int {
3395
+	private function footnoteId(MDState $state, string $symbol): ?int {
3358
 		$lookup = $state->root()->userInfo['footnoteIds'];
3396
 		$lookup = $state->root()->userInfo['footnoteIds'];
3359
 		if (!$lookup) return null;
3397
 		if (!$lookup) return null;
3360
 		return $lookup[$symbol] ?? null;
3398
 		return $lookup[$symbol] ?? null;
3361
 	}
3399
 	}
3362
 
3400
 
3363
 	public function toHTML(MDState $state): string {
3401
 	public function toHTML(MDState $state): string {
3364
-		$footnotes = $state->userInfo['footnotes'];
3402
+		$footnotes = $state->root()->userInfo['footnotes'];
3365
 		$symbolOrder = array_keys($footnotes);
3403
 		$symbolOrder = array_keys($footnotes);
3366
 		if (sizeof($footnotes) == 0) return '';
3404
 		if (sizeof($footnotes) == 0) return '';
3367
-		$footnoteUniques = $state->root()->footnoteInstances;
3405
+		$footnoteUniques = $state->root()->userInfo['footnoteInstances'];
3368
 		$html = '';
3406
 		$html = '';
3369
 		$html .= '<div class="footnotes">';
3407
 		$html .= '<div class="footnotes">';
3370
 		$html .= '<ol>';
3408
 		$html .= '<ol>';
3371
-		foreach ($symbolOrder as $symbol) {
3409
+		foreach ($symbolOrder as $symbolRaw) {
3410
+			$symbol = "{$symbolRaw}";
3372
 			$content = $footnotes[$symbol];
3411
 			$content = $footnotes[$symbol];
3373
 			if (!$content) continue;
3412
 			if (!$content) continue;
3374
 			$footnoteId = $this->footnoteId($state, $symbol);
3413
 			$footnoteId = $this->footnoteId($state, $symbol);
3375
-			$contentHTML = MDNode::toHTML($content, $state);
3414
+			$contentHTML = MDNode::arrayToHTML($content, $state);
3376
 			$html .= "<li value=\"{$footnoteId}\" id=\"{$state->root()->elementIdPrefix}footnote_{$footnoteId}\">{$contentHTML}";
3415
 			$html .= "<li value=\"{$footnoteId}\" id=\"{$state->root()->elementIdPrefix}footnote_{$footnoteId}\">{$contentHTML}";
3377
-			$uniques = $footnoteUniques[$symbol];
3416
+			$uniques = $footnoteUniques[$symbol] ?? null;
3378
 			if ($uniques) {
3417
 			if ($uniques) {
3379
 				foreach ($uniques as $unique) {
3418
 				foreach ($uniques as $unique) {
3380
 					$html .= " <a href=\"#{$state->root()->elementIdPrefix}footnoteref_{$unique}\" class=\"footnote-backref\">↩︎</a>";
3419
 					$html .= " <a href=\"#{$state->root()->elementIdPrefix}footnoteref_{$unique}\" class=\"footnote-backref\">↩︎</a>";
3384
 		}
3423
 		}
3385
 		$html .= '</ol>';
3424
 		$html .= '</ol>';
3386
 		$html .= '</div>';
3425
 		$html .= '</div>';
3387
-		return html;
3426
+		return $html;
3388
 	}
3427
 	}
3389
 
3428
 
3390
 	public function toPlaintext(MDState $state): string {
3429
 	public function toPlaintext(MDState $state): string {
3392
 		$symbolOrder = array_keys($footnotes);
3431
 		$symbolOrder = array_keys($footnotes);
3393
 		if (sizeof($footnotes) == 0) return '';
3432
 		if (sizeof($footnotes) == 0) return '';
3394
 		$text = '';
3433
 		$text = '';
3395
-		foreach ($symbolOrder as $symbol) {
3434
+		foreach ($symbolOrder as $symbolRaw) {
3435
+			$symbol = "{$symbolRaw}";
3396
 			$content = $footnotes[$symbol];
3436
 			$content = $footnotes[$symbol];
3397
 			if (!$content) continue;
3437
 			if (!$content) continue;
3398
 			$text .= "{$symbol}. " . $this->childPlaintext(state) . "\n";
3438
 			$text .= "{$symbol}. " . $this->childPlaintext(state) . "\n";
3414
 
3454
 
3415
 	public function __construct(string $text) {
3455
 	public function __construct(string $text) {
3416
 		parent::__construct([]);
3456
 		parent::__construct([]);
3417
-		if (!is_string($text) || mb_strlen($text) == 0) throw new Error("Meh!");
3418
 		$this->text = $text;
3457
 		$this->text = $text;
3419
 	}
3458
 	}
3420
 
3459
 
3513
 	}
3552
 	}
3514
 
3553
 
3515
 	public function toHTML(MDState $state): string {
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
 	}
3590
 	}
3552
 
3591
 
3553
 	public function toHTML(MDState $state): string {
3592
 	public function toHTML(MDState $state): string {
3554
-		if ($this->differentiator !== null) {
3593
+		if ($this->footnoteId !== null) {
3555
 			return "<sup class=\"footnote\" id=\"{$state->root()->elementIdPrefix}footnoteref_{$this->occurrenceId}\"" . $this->htmlAttributes() . ">" .
3594
 			return "<sup class=\"footnote\" id=\"{$state->root()->elementIdPrefix}footnoteref_{$this->occurrenceId}\"" . $this->htmlAttributes() . ">" .
3556
 				"<a href=\"#{$state->root()->elementIdPrefix}footnote_{$this->footnoteId}\">" . htmlentities($this->displaySymbol ?? $this->symbol) . "</a></sup>";
3595
 				"<a href=\"#{$state->root()->elementIdPrefix}footnote_{$this->footnoteId}\">" . htmlentities($this->displaySymbol ?? $this->symbol) . "</a></sup>";
3557
 		}
3596
 		}
3603
 			$title = $state->urlTitleForReference($this->reference);
3642
 			$title = $state->urlTitleForReference($this->reference);
3604
 			if ($title) $this->attributes['title'] = $title;
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
 	public ?string $alt;
3655
 	public ?string $alt;
3617
 
3656
 
3618
 	public function __construct(string $src, ?string $alt) {
3657
 	public function __construct(string $src, ?string $alt) {
3619
-		super([]);
3658
+		parent::__construct([]);
3620
 		$this->src = $src;
3659
 		$this->src = $src;
3621
 		$this->alt = $alt;
3660
 		$this->alt = $alt;
3622
 	}
3661
 	}
3647
 			$title = $state->urlTitleForReference($this->reference);
3686
 			$title = $state->urlTitleForReference($this->reference);
3648
 			if ($title !== null) $this->attributes['title'] = $title;
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
 	 * @param {string} definition
3702
 	 * @param {string} definition
3664
 	 */
3703
 	 */
3665
 	public function __construct(string $abbreviation, string $definition) {
3704
 	public function __construct(string $abbreviation, string $definition) {
3666
-		super([]);
3705
+		parent::__construct([]);
3667
 		$this->abbreviation = $abbreviation;
3706
 		$this->abbreviation = $abbreviation;
3668
 		$this->attributes['title'] = $definition;
3707
 		$this->attributes['title'] = $definition;
3669
 	}
3708
 	}
3865
 	 * @param string $elementIdPrefix
3904
 	 * @param string $elementIdPrefix
3866
 	 */
3905
 	 */
3867
 	private function investigateException(array $lines, string $elementIdPrefix) {
3906
 	private function investigateException(array $lines, string $elementIdPrefix) {
3907
+		print("Investigating error...\n");
3868
 		$startIndex = 0;
3908
 		$startIndex = 0;
3869
 		$endIndex = sizeof($lines);
3909
 		$endIndex = sizeof($lines);
3870
 		// Keep stripping away first line until an exception stops being thrown
3910
 		// Keep stripping away first line until an exception stops being thrown
3886
 			}
3926
 			}
3887
 		}
3927
 		}
3888
 		$problematicMarkdown = implode("\n", array_slice($lines, $startIndex, $endIndex));
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
 ?>

Loading…
Cancel
Save