|
|
@@ -890,7 +890,7 @@ class HTMLPage {
|
|
890
|
890
|
}
|
|
891
|
891
|
|
|
892
|
892
|
public static function render_post(Post $post): void {
|
|
893
|
|
- $body_html = self::post_body_html($post->body);
|
|
|
893
|
+ $body_html = Markdown::markdown_to_html($post->body);
|
|
894
|
894
|
$date = localized_date_string($post->created);
|
|
895
|
895
|
print(<<<HTML
|
|
896
|
896
|
<div class="post">
|
|
|
@@ -1000,6 +1000,205 @@ class HTMLPage {
|
|
1000
|
1000
|
}
|
|
1001
|
1001
|
}
|
|
1002
|
1002
|
|
|
|
1003
|
+class HTMLBlock {
|
|
|
1004
|
+ public HTMLBlockType $type;
|
|
|
1005
|
+ public int $indent;
|
|
|
1006
|
+ public string $content_markdown;
|
|
|
1007
|
+
|
|
|
1008
|
+ function __construct(HTMLBlockType $type, int $indent, string $content_markdown) {
|
|
|
1009
|
+ $this->type = $type;
|
|
|
1010
|
+ $this->indent = $indent;
|
|
|
1011
|
+ $this->content_markdown = $content_markdown;
|
|
|
1012
|
+ }
|
|
|
1013
|
+}
|
|
|
1014
|
+
|
|
|
1015
|
+enum HTMLBlockType {
|
|
|
1016
|
+ case Plain;
|
|
|
1017
|
+ case ListItem;
|
|
|
1018
|
+ case BlockQuote;
|
|
|
1019
|
+ case Preformatted;
|
|
|
1020
|
+ case H1;
|
|
|
1021
|
+ case H2;
|
|
|
1022
|
+ case H3;
|
|
|
1023
|
+ case H4;
|
|
|
1024
|
+ case H5;
|
|
|
1025
|
+ case H6;
|
|
|
1026
|
+}
|
|
|
1027
|
+
|
|
|
1028
|
+class Markdown {
|
|
|
1029
|
+ /// Converts one line of markdown to HTML.
|
|
|
1030
|
+ /// @param string $markdown Markdown string
|
|
|
1031
|
+ /// @return string HTML
|
|
|
1032
|
+ public static function line_markdown_to_html(string $markdown): string {
|
|
|
1033
|
+ $html = htmlentities($markdown);
|
|
|
1034
|
+ // Explicit URL [label](URL)
|
|
|
1035
|
+ $html = preg_replace('|\[(.*?)\]\((.*?)\)|',
|
|
|
1036
|
+ '<a referrerpolicy="no-referrer" target="_new" href="$2">$1</a>', $html);
|
|
|
1037
|
+ // Implicit URL
|
|
|
1038
|
+ $html = preg_replace('|(?<!href=")(http(?:s)?://)(\S+[^\s\.,\?!:;"\'\)])|',
|
|
|
1039
|
+ '<a referrerpolicy="no-referrer" target="_new" href="$1$2">$2</a>', $html);
|
|
|
1040
|
+ // Italic
|
|
|
1041
|
+ $html = preg_replace('/__(\S|\S.*?)__/', '<em>$1</em>', $html);
|
|
|
1042
|
+ // Bold
|
|
|
1043
|
+ $html = preg_replace('/\*\*(\S|\S.*?\S)\*\*/', '<strong>$1</strong>', $html);
|
|
|
1044
|
+ // Strikethrough
|
|
|
1045
|
+ $html = preg_replace('/~~(\S|\S.*?\S)~~/', '<strike>$1</strike>', $html);
|
|
|
1046
|
+ // Code
|
|
|
1047
|
+ $html = preg_replace('/`(\S|\S.*?\S)`/', '<code>$1</code>', $html);
|
|
|
1048
|
+ return $html;
|
|
|
1049
|
+ }
|
|
|
1050
|
+
|
|
|
1051
|
+ /// Converts markdown into an array of HTMLBlocks.
|
|
|
1052
|
+ /// @param string $markdown Markdown string
|
|
|
1053
|
+ /// @return array Array of HTMLBlocks
|
|
|
1054
|
+ private static function markdown_to_blocks(string $markdown): array {
|
|
|
1055
|
+ $prefix_to_linetype = array(
|
|
|
1056
|
+ '*' => HTMLBlockType::ListItem,
|
|
|
1057
|
+ '-' => HTMLBlockType::ListItem,
|
|
|
1058
|
+ '+' => HTMLBlockType::ListItem,
|
|
|
1059
|
+ '>' => HTMLBlockType::BlockQuote,
|
|
|
1060
|
+ '######' => HTMLBlockType::H6,
|
|
|
1061
|
+ '#####' => HTMLBlockType::H5,
|
|
|
1062
|
+ '####' => HTMLBlockType::H4,
|
|
|
1063
|
+ '###' => HTMLBlockType::H3,
|
|
|
1064
|
+ '##' => HTMLBlockType::H2,
|
|
|
1065
|
+ '#' => HTMLBlockType::H1,
|
|
|
1066
|
+ );
|
|
|
1067
|
+ $blocks = array();
|
|
|
1068
|
+ foreach (explode("\n", $markdown) as $line) {
|
|
|
1069
|
+ $trimmed_line = trim($line);
|
|
|
1070
|
+ $indent = intval(round((strlen($line) - strlen($trimmed_line)) / 4));
|
|
|
1071
|
+ $block_type = HTMLBlockType::Plain;
|
|
|
1072
|
+ $block_content = $trimmed_line;
|
|
|
1073
|
+ foreach ($prefix_to_linetype as $prefix => $type) {
|
|
|
1074
|
+ if ($trimmed_line == $prefix ||
|
|
|
1075
|
+ str_starts_with($trimmed_line, $prefix . ' ')) {
|
|
|
1076
|
+ $block_content = substr($trimmed_line, strlen($prefix));
|
|
|
1077
|
+ $block_type = $type;
|
|
|
1078
|
+ break;
|
|
|
1079
|
+ }
|
|
|
1080
|
+ }
|
|
|
1081
|
+ $blocks[] = new HTMLBlock($block_type, $indent, $block_content);
|
|
|
1082
|
+ }
|
|
|
1083
|
+ return $blocks;
|
|
|
1084
|
+ }
|
|
|
1085
|
+
|
|
|
1086
|
+ /// Converts markdown to HTML
|
|
|
1087
|
+ public static function markdown_to_html(string $markdown): string {
|
|
|
1088
|
+ return self::blocks_to_html(self::markdown_to_blocks($markdown));
|
|
|
1089
|
+ }
|
|
|
1090
|
+
|
|
|
1091
|
+ /// Converts an array of HTMLBlocks to HTML.
|
|
|
1092
|
+ private static function blocks_to_html(array $blocks): string {
|
|
|
1093
|
+ $html = '';
|
|
|
1094
|
+ $last_block = null;
|
|
|
1095
|
+ $tag_stack = array(); // stack of end tag strings for current open blocks
|
|
|
1096
|
+ foreach ($blocks as $block) {
|
|
|
1097
|
+ $is_empty = strlen($block->content_markdown) == 0;
|
|
|
1098
|
+ $is_last_empty = strlen($last_block?->content_markdown ?? '') == 0;
|
|
|
1099
|
+ $is_same_block = $block->type == $last_block?->type;
|
|
|
1100
|
+ if (!$is_same_block && sizeof($tag_stack) > 0) {
|
|
|
1101
|
+ foreach (array_reverse($tag_stack) as $tag) {
|
|
|
1102
|
+ $html .= $tag;
|
|
|
1103
|
+ }
|
|
|
1104
|
+ $tag_stack = array();
|
|
|
1105
|
+ }
|
|
|
1106
|
+ switch ($block->type) {
|
|
|
1107
|
+ case HTMLBlockType::Plain:
|
|
|
1108
|
+ if ($is_empty) {
|
|
|
1109
|
+ if ($is_last_empty) {
|
|
|
1110
|
+ // ignore two consecutive empty lines
|
|
|
1111
|
+ } else {
|
|
|
1112
|
+ $html .= array_pop($tag_stack);
|
|
|
1113
|
+ }
|
|
|
1114
|
+ } elseif ($is_last_empty) {
|
|
|
1115
|
+ $html .= "<p>" . self::line_markdown_to_html($block->content_markdown);
|
|
|
1116
|
+ $tag_stack[] = "</p>\n\n";
|
|
|
1117
|
+ } else {
|
|
|
1118
|
+ $html .= "<br/>\n" . self::line_markdown_to_html($block->content_markdown);
|
|
|
1119
|
+ }
|
|
|
1120
|
+ break;
|
|
|
1121
|
+ case HTMLBlockType::ListItem:
|
|
|
1122
|
+ if (!$is_same_block) {
|
|
|
1123
|
+ foreach (array_reverse($tag_stack) as $tag) {
|
|
|
1124
|
+ $html .= $tag;
|
|
|
1125
|
+ }
|
|
|
1126
|
+ $tag_stack = array();
|
|
|
1127
|
+ for ($i = 0; $i <= $block->indent; $i++) {
|
|
|
1128
|
+ $html .= "<ul>\n";
|
|
|
1129
|
+ $html .= "<li>";
|
|
|
1130
|
+ $tag_stack[] = "</ul>\n";
|
|
|
1131
|
+ $tag_stack[] = "</li>\n";
|
|
|
1132
|
+ }
|
|
|
1133
|
+ $html .= self::line_markdown_to_html($block->content_markdown);
|
|
|
1134
|
+ } elseif ($block->indent == $last_block->indent) {
|
|
|
1135
|
+ $html .= "</li>\n";
|
|
|
1136
|
+ $html .= "<li>" . self::line_markdown_to_html($block->content_markdown);
|
|
|
1137
|
+ } elseif ($block->indent > $last_block->indent) {
|
|
|
1138
|
+ // Deeper indent level
|
|
|
1139
|
+ for ($i = $last_block->indent; $i < $block->indent; $i++) {
|
|
|
1140
|
+ $html .= "<ul>\n<li>" . self::line_markdown_to_html($block->content_markdown);
|
|
|
1141
|
+ $tag_stack[] = "</ul>\n";
|
|
|
1142
|
+ $tag_stack[] = "</li>\n";
|
|
|
1143
|
+ }
|
|
|
1144
|
+ } elseif ($block->indent < $last_block->indent) {
|
|
|
1145
|
+ // Shallower indent level
|
|
|
1146
|
+ for ($i = $block->indent; $i < $last_block->indent; $i++) {
|
|
|
1147
|
+ $html .= array_pop($tag_stack);
|
|
|
1148
|
+ $html .= array_pop($tag_stack);
|
|
|
1149
|
+ }
|
|
|
1150
|
+ $html .= "</li>\n";
|
|
|
1151
|
+ $html .= "<li>" . self::line_markdown_to_html($block->content_markdown);
|
|
|
1152
|
+ }
|
|
|
1153
|
+ break;
|
|
|
1154
|
+ case HTMLBlockType::BlockQuote:
|
|
|
1155
|
+ if ($is_same_block) {
|
|
|
1156
|
+ $html .= "<br/>\n";
|
|
|
1157
|
+ } else {
|
|
|
1158
|
+ $html .= "<blockquote>";
|
|
|
1159
|
+ $tag_stack[] = "</blockquote>\n\n";
|
|
|
1160
|
+ }
|
|
|
1161
|
+ $html .= self::line_markdown_to_html($block->content_markdown);
|
|
|
1162
|
+ break;
|
|
|
1163
|
+ case HTMLBlockType::Preformatted:
|
|
|
1164
|
+ if ($is_same_block) {
|
|
|
1165
|
+ $html .= "\n";
|
|
|
1166
|
+ } else {
|
|
|
1167
|
+ $html .= "<pre>";
|
|
|
1168
|
+ $tag_stack[] = "</pre>\n\n";
|
|
|
1169
|
+ }
|
|
|
1170
|
+ $html .= htmlentities($block->content_markdown);
|
|
|
1171
|
+ break;
|
|
|
1172
|
+ case HTMLBlockType::H1:
|
|
|
1173
|
+ $html .= '<h1>' . self::line_markdown_to_html($block->content_markdown) . "</h1>\n\n";
|
|
|
1174
|
+ break;
|
|
|
1175
|
+ case HTMLBlockType::H2:
|
|
|
1176
|
+ $html .= '<h2>' . self::line_markdown_to_html($block->content_markdown) . "</h2>\n\n";
|
|
|
1177
|
+ break;
|
|
|
1178
|
+ case HTMLBlockType::H3:
|
|
|
1179
|
+ $html .= '<h3>' . self::line_markdown_to_html($block->content_markdown) . "</h3>\n\n";
|
|
|
1180
|
+ break;
|
|
|
1181
|
+ case HTMLBlockType::H4:
|
|
|
1182
|
+ $html .= '<h4>' . self::line_markdown_to_html($block->content_markdown) . "</h4>\n\n";
|
|
|
1183
|
+ break;
|
|
|
1184
|
+ case HTMLBlockType::H5:
|
|
|
1185
|
+ $html .= '<h5>' . self::line_markdown_to_html($block->content_markdown) . "</h5>\n\n";
|
|
|
1186
|
+ break;
|
|
|
1187
|
+ case HTMLBlockType::H6:
|
|
|
1188
|
+ $html .= '<h6>' . self::line_markdown_to_html($block->content_markdown) . "</h6>\n\n";
|
|
|
1189
|
+ break;
|
|
|
1190
|
+ }
|
|
|
1191
|
+ $last_block = $block;
|
|
|
1192
|
+ }
|
|
|
1193
|
+ if (sizeof($tag_stack) > 0) {
|
|
|
1194
|
+ foreach (array_reverse($tag_stack) as $tag) {
|
|
|
1195
|
+ $html .= $tag;
|
|
|
1196
|
+ }
|
|
|
1197
|
+ }
|
|
|
1198
|
+ return $html;
|
|
|
1199
|
+ }
|
|
|
1200
|
+}
|
|
|
1201
|
+
|
|
1003
|
1202
|
// -- Main logic ------------------------------------------
|
|
1004
|
1203
|
|
|
1005
|
1204
|
check_setup();
|