1 <?php
2
3 4 5 6 7 8 9
10 class rex_markdown
11 {
12 use rex_factory_trait;
13
14 private function __construct()
15 {
16 }
17
18 19 20
21 public static function factory()
22 {
23 $class = static::getFactoryClass();
24 return new $class();
25 }
26
27 28 29 30 31 32 33
34 public function parse($code)
35 {
36 $parser = new ParsedownExtra();
37 $parser->setBreaksEnabled(true);
38
39 return $parser->text($code);
40 }
41
42 43 44 45 46 47 48 49 50
51 public function parseWithToc($code, $topLevel = 2, $bottomLevel = 3)
52 {
53 $parser = new rex_parsedown_with_toc();
54 $parser->setBreaksEnabled(true);
55 $parser->topLevel = $topLevel;
56 $parser->bottomLevel = $bottomLevel;
57
58 $content = $parser->text($code);
59 $headers = $parser->headers;
60
61 $previous = $topLevel - 1;
62 $toc = '';
63
64 foreach ($headers as $header) {
65 $level = $header['level'];
66
67 if ($level > $previous) {
68 if ($level > $previous + 1) {
69 $message = 'The headline structure in the given markdown document is malformed, ';
70 if ($previous < $topLevel) {
71 $message .= "it starts with a h$level instead of a h$topLevel.";
72 } else {
73 $message .= "a h$previous is followed by a h$level, but only a h".($previous + 1).' or lower is allowed.';
74 }
75
76 throw new rex_exception($message);
77 }
78
79 $toc .= "<ul>\n";
80 $previous = $level;
81 } elseif ($level < $previous) {
82 for (; $level < $previous; --$previous) {
83 $toc .= "</li>\n";
84 $toc .= "</ul>\n";
85 }
86 } else {
87 $toc .= "</li>\n";
88 }
89
90 $toc .= "<li>\n";
91 $toc .= '<a href="#'.rex_escape($header['id']).'">'.rex_escape($header['text'])."</a>\n";
92 }
93
94 for (; $previous > $topLevel - 1; --$previous) {
95 $toc .= "</li>\n";
96 $toc .= "</ul>\n";
97 }
98
99 return [$toc, $content];
100 }
101 }
102
103 104 105
106 final class rex_parsedown_with_toc extends ParsedownExtra
107 {
108 private $ids = [];
109
110 public $topLevel = 2;
111 public $bottomLevel = 3;
112 public = [];
113
114 protected function ($line)
115 {
116 $block = parent::blockHeader($line);
117
118 return $this->handleHeader($block);
119 }
120
121 protected function ($line, array $block = null)
122 {
123 $block = parent::blockSetextHeader($line, $block);
124
125 return $this->handleHeader($block);
126 }
127
128 private function handleHeader(array $block = null)
129 {
130 if (!$block) {
131 return $block;
132 }
133
134 list($level) = sscanf($block['element']['name'], 'h%d');
135
136 if ($level < $this->topLevel || $level > $this->bottomLevel) {
137 return $block;
138 }
139
140 if (!isset($block['element']['attributes']['id'])) {
141 $baseId = $id = 'header-'.rex_string::normalize($block['element']['text'], '-');
142
143 for ($i = 2; isset($this->ids[$id]); ++$i) {
144 $id = $baseId.'-'.$i;
145 }
146
147 $block['element']['attributes']['id'] = $id;
148 }
149
150 $id = $block['element']['attributes']['id'];
151 $this->ids[$id] = true;
152
153 $this->headers[] = [
154 'level' => $level,
155 'id' => $id,
156 'text' => $block['element']['text'],
157 ];
158
159 return $block;
160 }
161 }
162