@@ -8,10 +8,12 @@ use HTML::Escape qw( escape_html );
88use HTML::Restrict ();
99use URI ();
1010use Digest::MD5 ();
11+ use CommonMark qw( :node :event ) ;
1112
1213our @EXPORT_OK = qw(
1314 filter_html
1415 gravatar_image
16+ render_markdown
1517) ;
1618
1719sub filter_html {
@@ -146,4 +148,66 @@ sub gravatar_image {
146148 $grav_id , $size // 80;
147149}
148150
151+ my @is_leaf ;
152+ $is_leaf [$_ ] = 1
153+ for (
154+ NODE_HTML, NODE_HRULE, NODE_CODE_BLOCK, NODE_TEXT,
155+ NODE_SOFTBREAK, NODE_LINEBREAK, NODE_CODE, NODE_INLINE_HTML,
156+ );
157+
158+ sub render_markdown {
159+ my ($markdown ) = @_ ;
160+
161+ my $doc = CommonMark-> parse_document($markdown );
162+
163+ my ( $html , $header_content , %seen_header );
164+
165+ my $iter = $doc -> iterator;
166+ while ( my ( $ev_type , $node ) = $iter -> next ) {
167+ my $node_type = $node -> get_type;
168+
169+ if ( $node_type == NODE_DOCUMENT ) {
170+ next ;
171+ }
172+
173+ if ( $node_type == NODE_HEADER ) {
174+ if ( $ev_type == EVENT_ENTER ) {
175+ $header_content = ' ' ;
176+ }
177+ if ( $ev_type == EVENT_EXIT ) {
178+ $header_content =~ s { (?:-(\d +))?$} { '-' . (($1 // 1) + 1)} e
179+ while $seen_header {$header_content }++;
180+
181+ my $header_html = $node -> render_html;
182+ $header_html
183+ =~ s / ^<h[0-9]+\b\K / ' id="'.escape_html($header_content ).'"'/ e ;
184+ $html .= $header_html ;
185+
186+ undef $header_content ;
187+ }
188+ }
189+ elsif ($ev_type == EVENT_ENTER
190+ && $node -> parent-> get_type == NODE_DOCUMENT )
191+ {
192+ $html .= $node -> render_html;
193+ }
194+
195+ if ( defined $header_content ) {
196+ if ( $is_leaf [$node_type ] ) {
197+ my $content = lc ( $node -> get_literal );
198+ $content =~ s /\A\s +// ;
199+ $content =~ s /\s +\z // ;
200+ $content =~ s /\s +/ -/ g ;
201+
202+ if ( length $content ) {
203+ $header_content .= ' -' if length $header_content ;
204+ $header_content .= $content ;
205+ }
206+ }
207+ }
208+ }
209+
210+ return $html ;
211+ }
212+
1492131;
0 commit comments