├── .github └── workflows │ └── test.yaml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── META6.json ├── README.md ├── lib └── Text │ ├── Markdown.pm6 │ └── Markdown │ ├── Document.pm6 │ └── to │ └── HTML.pm6 └── t ├── parse-escape.t ├── parse-from-file.t ├── parse-link-with-code.t ├── parse-with-underscore.t ├── parse.t ├── structured.t └── text.md /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: "Test + coverage" 2 | on: [ push, pull_request ] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Test with coverage 8 | with: 9 | coverage: true 10 | uses: JJ/raku-test-action@main 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.precomp 2 | 3 | *~ 4 | 5 | workspace.xml 6 | .idea 7 | *.iml 8 | 9 | #fez 10 | sdist/ 11 | 12 | .racoco 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Want to help? 2 | 3 | You can really help in any number of ways. One of the things I would 4 | like is to have the module done completely using Perl 6 Grammars, 5 | which are out of my reach right now. 6 | 7 | If it's too much for you 8 | too, 9 | [take a look at the issues](https://github.com/retupmoca/p6-markdown/issues). Some 10 | of them are old... And some of them are more urgent than others, for 11 | instance 12 | the 13 | [one on nested lists](https://github.com/retupmoca/p6-markdown/issues/25). 14 | 15 | New features are also welcome, but first thing first. If you have any 16 | doubt, just raise an issue and I'll get back to you. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrew Egeler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /META6.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Text::Markdown", 3 | "description": "Markdown parser / HTML generator", 4 | "version": "1.1.2", 5 | "perl": "6.*", 6 | "authors": [ 7 | "Andrew Egeler", 8 | "JJ Merelo" 9 | ], 10 | "auth": "zef:jjmerelo", 11 | "depends": [ 12 | "HTML::Escape" 13 | ], 14 | "provides": { 15 | "Text::Markdown": "lib/Text/Markdown.pm6", 16 | "Text::Markdown::Document": "lib/Text/Markdown/Document.pm6", 17 | "Text::Markdown::to::HTML": "lib/Text/Markdown/to/HTML.pm6" 18 | }, 19 | "license": "MIT", 20 | "source-url": "git://github.com/retupmoca/p6-markdown.git" 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | `Text::Markdown`, a Markdown parsing module for Raku. 5 | 6 | Synopsis 7 | ======== 8 | 9 | use Text::Markdown; 10 | # Using raw Markdown directly 11 | my $md = Text::Markdown.new($raw-md); 12 | say $md.render; 13 | 14 | # Or alternatively 15 | my $md = parse-markdown($raw-md); 16 | say $md.to_html; 17 | 18 | # Using file 19 | use Text::Markdown; 20 | my $md = parse-markdown-from-file($filename); 21 | 22 | Description 23 | =========== 24 | 25 | This module parses Markdown (MD) and generates HTML from it. It can be used to extract certain elements from a MD document or to generate other kind of things. 26 | 27 | Installation 28 | ============ 29 | 30 | Using `zef` 31 | ----------- 32 | 33 | zef update && zef install Text::Markdown 34 | 35 | Dependencies 36 | ------------ 37 | 38 | This modules depends on [`HTML::Escape`](https://github.com/moznion/p6-HTML-Escape). Install it locally with 39 | 40 | zef install HTML::Escape 41 | 42 | Routines 43 | ======== 44 | 45 | Methods 46 | ------- 47 | 48 | The following methods can be invoked on a `Text::Markdown` instance object: 49 | 50 | * `render` - Render the Markdown text provided to the instance object during its construction. 51 | 52 | * `to_html` - An alias for the `render` method. 53 | 54 | * `to-html` - Same as the `to_html` method. 55 | 56 | Subroutines 57 | ----------- 58 | 59 | * `parse-markdown($text)` - Render the Markdown `$text`. 60 | 61 | * `parse-markdown-from-file(Str $filename)` - Render the Markdown text in file `$filename`. 62 | 63 | Who 64 | === 65 | 66 | Initial version by [Andrew Egeler](https://github.com/retupmoca), with 67 | extensive additions by [JMERELO](https://github.com/JJ) and 68 | [Altai-man](https://github.com/Altai-man). Some help from [Luis Uceta](https://github.com/uzluisf) 69 | 70 | Want to lend a hand? 71 | ==================== 72 | 73 | Check out the [contributing guidelines](CONTRIBUTING.md). All contributions are welcome, and will be addressed. 74 | 75 | License 76 | ======= 77 | 78 | You can redistribute this module and/or modify it under the terms of the MIT License. 79 | 80 | -------------------------------------------------------------------------------- /lib/Text/Markdown.pm6: -------------------------------------------------------------------------------- 1 | use Text::Markdown::Document; 2 | use Text::Markdown::to::HTML; 3 | 4 | # wrapper/utility class (mostly for easy no-argument .render 5 | # as well as compatability with masak's Text::Markdown) 6 | class Text::Markdown { 7 | has $.document; 8 | 9 | multi method new($text) { 10 | self.bless(:document(Text::Markdown::Document.new($text))); 11 | } 12 | 13 | multi method render($class) { 14 | $.document.render($class); 15 | } 16 | 17 | multi method render() { 18 | $.document.render(Text::Markdown::to::HTML); 19 | } 20 | 21 | method to_html { self.render } 22 | method to-html { self.render } 23 | 24 | method Str { 25 | $.document.Str; 26 | } 27 | } 28 | 29 | our sub parse-markdown($text) is export { 30 | Text::Markdown.new($text); 31 | } 32 | 33 | our sub parse-markdown-from-file(Str $filename) is export { 34 | die "Can't locate $filename !" unless $filename.IO ~~ :e; 35 | 36 | my Str $text = slurp $filename; 37 | Text::Markdown.new($text); 38 | } 39 | 40 | =begin pod 41 | 42 | =head1 Name 43 | 44 | C«Text::Markdown», a Markdown parsing module for Perl6. 45 | 46 | =head1 Synopsis 47 | 48 | =begin code 49 | use Text::Markdown; 50 | # Using raw Markdown directly 51 | my $md = Text::Markdown.new($raw-md); 52 | say $md.render; 53 | 54 | # Or alternatively 55 | my $md = parse-markdown($raw-md); 56 | say $md.to_html; 57 | 58 | # Using file 59 | use Text::Markdown; 60 | my $md = parse-markdown-from-file($filename); 61 | =end code 62 | 63 | =head1 Description 64 | 65 | This module parses Markdown (MD) and generates HTML from it. It can be used 66 | to extract certain elements from a MD document or to generate other 67 | kind of things. 68 | 69 | =head1 Installation 70 | 71 | =head2 Using C«zef» 72 | 73 | =for code 74 | zef update && zef install Text::Markdown 75 | 76 | =head2 Dependencies 77 | 78 | This modules depends on 79 | L«C«HTML::Escape»|https://github.com/moznion/p6-HTML-Escape». Install 80 | it locally with 81 | 82 | =for code 83 | zef install HTML::Escape 84 | 85 | =head1 Routines 86 | 87 | =head2 Methods 88 | 89 | The following methods can be invoked on a C«Text::Markdown» instance object: 90 | 91 | =item C«render» - Render the Markdown text provided to the instance object during its construction. 92 | =item C«to_html» - An alias for the C«render» method. 93 | =item C«to-html» - Same as the C«to_html» method. 94 | 95 | =head2 Subroutines 96 | 97 | =item C«parse-markdown($text)» - Render the Markdown C«$text». 98 | =item C«parse-markdown-from-file(Str $filename)» - Render the Markdown text in file C«$filename». 99 | 100 | =head1 Who 101 | 102 | Initial version by L«Andrew Egeler|https://github.com/retupmoca», with 103 | extensive additions by L«JMERELO|https://github.com/JJ» 104 | and L«Altai-man|https://github.com/Altai-man». 105 | 106 | =head1 Want to lend a hand? 107 | 108 | Check out the L«contributing guidelines|CONTRIBUTING.md». All 109 | contributions are welcome, and will be addressed. 110 | 111 | =head1 License 112 | 113 | You can redistribute this module and/or modify it under the terms of the 114 | MIT License. 115 | =end pod 116 | -------------------------------------------------------------------------------- /lib/Text/Markdown/Document.pm6: -------------------------------------------------------------------------------- 1 | class Text::Markdown::Paragraph { 2 | has @.items; 3 | 4 | # TODO: wrapping? 5 | method Str { @.items>>.Str.join ~ "\n\n" } 6 | 7 | method items-of-type( Str $type ) { 8 | return self.items.grep( { .^name ~~ / «$type» / } ); 9 | } 10 | } 11 | 12 | 13 | class Text::Markdown::Code { 14 | has $.text; 15 | 16 | # TODO: handle case where $.text contains '`' 17 | method Str { '`' ~ $.text ~ '`' } 18 | } 19 | 20 | class Text::Markdown::CodeBlock { 21 | has $.text; 22 | has $.lang; 23 | 24 | method Str { 25 | my $ret; 26 | for $.text.lines { 27 | $ret ~= ' ' ~ $_; 28 | } 29 | $ret ~ "\n\n"; 30 | } 31 | } 32 | 33 | class Text::Markdown::List { 34 | has $.numbered; 35 | has @.items; 36 | 37 | method Str { 38 | my $ret; 39 | for @.items.kv -> $i, $_ { 40 | my $text = $_.Str; 41 | for $text.lines.kv -> $j, $l { 42 | if $j { 43 | $ret ~= ' ' ~ $l; 44 | } 45 | else { 46 | if $.numbered { 47 | $ret ~= ' ' ~ ($i + 1) ~ '. ' ~ $l; 48 | } 49 | else { 50 | $ret ~= ' - ' ~ $l; 51 | } 52 | } 53 | } 54 | $ret ~= "\n\n"; 55 | } 56 | } 57 | } 58 | 59 | class Text::Markdown::Heading { 60 | has $.text; 61 | has $.level; 62 | 63 | method Str { ("#" x $.level) ~ ' ' ~ $.text ~ "\n\n" } 64 | } 65 | 66 | class Text::Markdown::Rule { method Str { "---\n\n" } } 67 | 68 | class Text::Markdown::Blockquote { 69 | has @.items; 70 | 71 | method Str { 72 | ...; 73 | } 74 | } 75 | 76 | class Text::Markdown::Link { 77 | has $.url; 78 | has $.text; 79 | has $.ref; 80 | 81 | method Str { 82 | ...; 83 | } 84 | } 85 | 86 | class Text::Markdown::EmailLink { 87 | has $.url; 88 | 89 | method Str { 90 | '<' ~ $.url ~ '>'; 91 | } 92 | } 93 | 94 | class Text::Markdown::Image { 95 | has $.url; 96 | has $.text; 97 | has $.ref; 98 | 99 | method Str { 100 | ...; 101 | } 102 | } 103 | 104 | class Text::Markdown::Emphasis { 105 | has $.text; 106 | has $.level; 107 | 108 | method Str { 109 | ...; 110 | } 111 | } 112 | 113 | class Text::Markdown::HtmlBlock { 114 | has @.items; 115 | 116 | method Str { 117 | @.items>>.Str.join; 118 | } 119 | } 120 | 121 | class Text::Markdown::HtmlTag { 122 | has $.tag; 123 | 124 | method Str { 125 | ... 126 | } 127 | } 128 | 129 | class Text::Markdown::Document { 130 | has @.items; 131 | has %.references; 132 | 133 | method Str { @.items>>.Str.join } 134 | 135 | multi method render($class) { 136 | my $c = $class; 137 | $c = $class.new unless defined($class); 138 | 139 | return $c.render(self); 140 | } 141 | 142 | method items-of-type( Str $type ) { 143 | return self.items.grep( { .^name ~~ / «$type» / } ); 144 | } 145 | 146 | method parse-inline($chunk) { 147 | my @ret = $chunk; 148 | my $changed = False; 149 | repeat { 150 | $changed = False; 151 | my @tmp = @ret; 152 | @ret = (); 153 | 154 | for @tmp -> $_ is rw { 155 | if $_ ~~ Str { 156 | # regex stolen shamelessly from masak's Text::Markdown 157 | if $_ ~~ s/ \! \[ (.+?) \] \( (.+?) \) (.*) // { 158 | @ret.push($_); 159 | @ret.push(Text::Markdown::Image.new(:text(~$0), :url(~$1))); 160 | @ret.push(~$2); 161 | $changed = True; 162 | } 163 | elsif $_ ~~ s/ \! \[ (.+?) \] \[ (.*?) \] (.*) // { 164 | @ret.push($_); 165 | @ret.push(Text::Markdown::Image.new(:text(~$0), :ref(~$1 || ~$0))); 166 | @ret.push(~$2); 167 | $changed = True; 168 | } 169 | elsif $_ ~~ s/ \[ (.+?) \] \( (.+?) \) (.*) // { 170 | @ret.push($_); 171 | @ret.push(Text::Markdown::Link.new(:text(~$0), :url(~$1))); 172 | @ret.push(~$2); 173 | $changed = True; 174 | } 175 | elsif $_ ~~ s/ \[ (.+?) \] \[ (.*?) \] (.*) // { 176 | @ret.push($_); 177 | @ret.push(Text::Markdown::Link.new(:text(~$0), :ref(~$1 || ~$0))); 178 | @ret.push(~$2); 179 | $changed = True; 180 | } 181 | elsif $_ ~~ s/ \< ( .+? \:\/\/ .*? ) \> (.*) // { 182 | @ret.push($_); 183 | @ret.push(Text::Markdown::Link.new(:text(~$0), :url(~$0))); 184 | @ret.push(~$1); 185 | $changed = True; 186 | } 187 | elsif $_ ~~ s/ ('`'+) (.+?) $0 (.*) // { 188 | @ret.push($_); 189 | @ret.push(Text::Markdown::Code.new(:text(~$1))); 190 | @ret.push(~$2); 191 | $changed = True; 192 | } 193 | elsif $_ ~~ s/ \< ( .*? \@ .*? ) \> (.*) // { 194 | @ret.push($_); 195 | @ret.push(Text::Markdown::EmailLink.new(:url(~$0))); 196 | @ret.push(~$1); 197 | $changed = True; 198 | } 199 | elsif $_ ~~ s/ ( \< .+? \> ) (.*) // { 200 | @ret.push($_); 201 | @ret.push(Text::Markdown::HtmlTag.new(:tag(~$0))); 202 | @ret.push(~$1); 203 | $changed = True; 204 | } 205 | elsif $_ ~~ s[ ('**'||'__') (.+?<[*_]>*) $0 (.*) ] = "" { 206 | @ret.push($_); 207 | @ret.push(Text::Markdown::Emphasis.new(:text(~$1), :level(2))); 208 | @ret.push(~$2); 209 | $changed = True; 210 | } 211 | elsif $_ ~~ s[ ('*'||'_') (.+?) $0 (.*) ] = "" { 212 | @ret.push($_); 213 | @ret.push(Text::Markdown::Emphasis.new(:text(~$1), :level(1))); 214 | @ret.push(~$2); 215 | $changed = True; 216 | } 217 | else { 218 | @ret.push($_); 219 | } 220 | } 221 | else { 222 | @ret.push($_); 223 | } 224 | } 225 | 226 | } until !$changed; 227 | 228 | @ret.grep({ $_ }); 229 | } 230 | 231 | method item-from-chunk($chunk is rw) { 232 | 233 | if $chunk ~~ /^(\#+)/ { 234 | my $level = $0.chars; $chunk ~~ s/^\#+\s+//; 235 | $chunk ~~ s/\s+\#+$//; 236 | return Text::Markdown::Heading.new(:text($chunk), 237 | :level($level)); 238 | } 239 | elsif all($chunk.lines.map({ so $_ ~~ /^\h ** 4/ })) { 240 | $chunk ~~ s:g/^^\h ** 4//; 241 | return Text::Markdown::CodeBlock.new(:text($chunk)); 242 | } 243 | elsif all($chunk.lines.map({ so $_ ~~ /^\>\s/ })) { 244 | $chunk ~~ s:g/^^\>\s+//; 245 | return Text::Markdown::Blockquote.new( 246 | :items(self.new($chunk).items)); 247 | } 248 | elsif $chunk.lines.first ~~ /^^'`'+/ && $chunk.lines.tail ~~ /^^'`'+/ { 249 | if $chunk.lines.elems > 1 { 250 | my regex fenced-block { 251 | ^^ 252 | $=['`' ** 3..*] ' '* # opening ``` 253 | [$=<[\w # \. + -]>*]? ' '* # optional code's language 254 | \n $=(.*?) # block's body 255 | $ # closing ``` 256 | ' '* 257 | $$ 258 | } 259 | 260 | my $lang; 261 | $chunk.match(//); 262 | 263 | given $/ { 264 | $chunk = ..trim; 265 | $lang = ..trim; 266 | } 267 | 268 | return Text::Markdown::CodeBlock.new(:text($chunk), :$lang); 269 | } else { 270 | return self.parse-inline($chunk); 271 | } 272 | } 273 | elsif $chunk.lines == 1 && $chunk ~~ /^\-\-\-/ { 274 | return Text::Markdown::Rule.new; 275 | } 276 | elsif all($chunk.lines.map({ so $_ ~~ /^\[ .+? \]\: .+/ })) { 277 | for $chunk.lines { 278 | $_ ~~ /^\[ (.+?) \]\: \s* (.+)/; 279 | %!references{$0} = $1; 280 | } 281 | return '' 282 | } 283 | elsif $chunk { 284 | $chunk ~~ s:g/\n/ /; 285 | my @items = self.parse-inline($chunk); 286 | if @items[0] ~~ Text::Markdown::HtmlTag && 287 | @items[*-1] ~~ Text::Markdown::HtmlTag 288 | { 289 | return Text::Markdown::HtmlBlock.new(:@items); 290 | } 291 | else 292 | { 293 | return Text::Markdown::Paragraph.new(:@items); 294 | } 295 | } 296 | } 297 | 298 | multi method new($text) { 299 | self.bless(:$text); 300 | } 301 | 302 | submethod BUILD(:$text) { 303 | return unless $text; 304 | my @lines = $text.lines; 305 | 306 | my $chunk = ''; 307 | my @items; 308 | my $in-list; 309 | my $in-fenced = False; 310 | my $list-ordered; 311 | my @list-items; 312 | for @lines -> $l { 313 | if !$in-list && !$in-fenced && $l ~~ /^\s*$/ { 314 | @items.push(self.item-from-chunk($chunk)) if $chunk.chars; 315 | $chunk = ''; 316 | } 317 | else { 318 | if !$in-fenced && $l ~~ /^\s+\-\s/ { 319 | if $in-list && $list-ordered { 320 | $chunk ~~ s/^\s+\d+\.?\s+//; 321 | @list-items.push(self.new($chunk)); 322 | $chunk = ''; 323 | @items.push(Text::Markdown::List.new(:items(@list-items), :numbered($list-ordered))); 324 | @list-items = (); 325 | } 326 | $in-list = True; 327 | $list-ordered = False; 328 | if $chunk { 329 | $chunk ~~ s/^\s+\-\s+//; 330 | @list-items.push(self.new($chunk)); 331 | $chunk = ''; 332 | } 333 | } 334 | elsif !$in-fenced && $l ~~ /^\s+\d+\.?\s/ { 335 | if $in-list && !$list-ordered { 336 | $chunk ~~ s/^\s+\-\s+//; 337 | @list-items.push(self.new($chunk)); 338 | $chunk = ''; 339 | @items.push(Text::Markdown::List.new(:items(@list-items), :numbered($list-ordered))); 340 | @list-items = (); 341 | } 342 | $in-list = True; 343 | $list-ordered = True; 344 | if $chunk { 345 | $chunk ~~ s/^\s+\d+\.?\s+//; 346 | @list-items.push(self.new($chunk)); 347 | $chunk = ''; 348 | } 349 | } 350 | elsif !$in-fenced && $l ~~ /^\*\s/ { 351 | if $in-list && $list-ordered { 352 | $chunk ~~ s/^\*\s+//; 353 | @list-items.push(self.new($chunk)); 354 | $chunk = ''; 355 | @items.push(Text::Markdown::List.new(:items(@list-items), :numbered($list-ordered))); 356 | @list-items = (); 357 | } 358 | $in-list = True; 359 | $list-ordered = False; 360 | if $chunk { 361 | $chunk ~~ s/^\*\s+//; 362 | @list-items.push(self.new($chunk)); 363 | $chunk = ''; 364 | } 365 | } 366 | elsif !$in-fenced && $in-list && $l ~~ /^\S/ { 367 | $in-list = False; 368 | if $list-ordered { 369 | $chunk ~~ s/^\s+\d+\.?\s+//; 370 | } 371 | else { 372 | $chunk ~~ s/^\s+\-\s+//; 373 | $chunk ~~ s/^\*\s+//; 374 | } 375 | @list-items.push(self.new($chunk)); 376 | $chunk = ''; 377 | @items.push(Text::Markdown::List.new(:items(@list-items), :numbered($list-ordered))); 378 | @list-items = (); 379 | } 380 | $in-fenced = not $in-fenced if $l ~~ /^^ '```' /; 381 | $chunk ~= "\n" if $chunk; 382 | $chunk ~= $l; 383 | } 384 | } 385 | @items.push(self.item-from-chunk($chunk)) if $chunk && !$in-list; 386 | if $list-ordered { 387 | $chunk ~~ s/^\s+\d+\.?\s+//; 388 | } 389 | else { 390 | $chunk ~~ s/^\s+\-\s+//; 391 | $chunk ~~ s/^\*\s+//; 392 | } 393 | @list-items.push(self.new($chunk)) if $chunk && $in-list; 394 | @items.push(Text::Markdown::List.new(:items(@list-items), :numbered($list-ordered))) if @list-items; 395 | 396 | @items .= grep({ $_ }); 397 | 398 | @!items = @items; 399 | #self.bless(:@items); 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /lib/Text/Markdown/to/HTML.pm6: -------------------------------------------------------------------------------- 1 | use HTML::Escape; 2 | use Text::Markdown::Document; 3 | 4 | class Text::Markdown::to::HTML { 5 | has $!document; 6 | 7 | multi method render(Text::Markdown::Document $d) { 8 | unless $!document { 9 | $!document = $d; 10 | } 11 | my $ret; 12 | for $d.items { 13 | $ret ~= self.render($_); 14 | } 15 | $ret; 16 | } 17 | 18 | multi method render(Text::Markdown::Paragraph $p) { 19 | my $ret; 20 | for $p.items { 21 | $ret ~= self.render($_); 22 | } 23 | '

' ~ $ret ~ '

'; 24 | } 25 | 26 | multi method render(Str $s) { $s } 27 | 28 | multi method render(Text::Markdown::Code $c) { 29 | '' ~ escape-html($c.text) ~ ''; 30 | } 31 | 32 | multi method render(Text::Markdown::CodeBlock $c) { 33 | my $lang-class = $c.lang ?? ' class="' ~ $c.lang ~ '"' !! ''; 34 | "
" ~ escape-html($c.text) ~ '
'; 35 | } 36 | 37 | multi method render(Text::Markdown::List $l) { 38 | my $ret; 39 | for $l.items { 40 | $ret ~= '
  • ' ~ self.render($_) ~ '
  • '; 41 | } 42 | if $l.numbered { 43 | '
      ' ~ $ret ~ '
    '; 44 | } 45 | else { 46 | '
      ' ~ $ret ~ '
    '; 47 | } 48 | } 49 | 50 | multi method render(Text::Markdown::Heading $h) { 51 | '' ~ $h.text ~ ''; 52 | } 53 | 54 | multi method render(Text::Markdown::Rule $r) { '
    ' } 55 | 56 | multi method render(Text::Markdown::Blockquote $p) { 57 | my $ret; 58 | for $p.items { 59 | $ret ~= self.render($_); 60 | } 61 | '
    ' ~ $ret ~ '
    '; 62 | } 63 | 64 | multi method render(Text::Markdown::Link $r) { 65 | my $url = $r.url; 66 | unless $url { 67 | $url = $!document.references{$r.ref}; 68 | } 69 | '' ~ $r.text ~ ''; 70 | } 71 | 72 | multi method render(Text::Markdown::EmailLink $r) { 73 | '< href="mailto:' ~ $r.url ~ '">' ~ $r.url ~ ''; 74 | } 75 | 76 | multi method render(Text::Markdown::Image $r) { 77 | my $url = $r.url; 78 | unless $url { 79 | $url = $!document.references{$r.ref}; 80 | } 81 | '' ~ $r.text ~ ''; 82 | } 83 | multi method render(Text::Markdown::Emphasis $r) { 84 | if $r.level == 1 { 85 | '' ~ $r.text ~ ''; 86 | } 87 | else { 88 | '' ~ $r.text ~ ''; 89 | } 90 | } 91 | 92 | multi method render(Text::Markdown::HtmlBlock $r) { 93 | $r.items.map( -> $child { self.render($child) } ).join(""); 94 | } 95 | 96 | multi method render(Text::Markdown::HtmlTag $r) { 97 | $r.tag; 98 | } 99 | 100 | multi method render(Seq $values) { 101 | $values.map({ self.render($_) }).join(''); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /t/parse-escape.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | use Text::Markdown::Document; 3 | use Test; 4 | 5 | plan 2; 6 | 7 | my $text = 'This text includes escapes: \* \# \_'; 8 | 9 | my $document = Text::Markdown::Document.new($text); 10 | ok $document ~~ Text::Markdown::Document, 'Able to parse'; 11 | ok $document.items[0] ~~ Text::Markdown::Paragraph, 12 | 'second element is a paragraph'; 13 | say $document.raku; 14 | -------------------------------------------------------------------------------- /t/parse-from-file.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | 3 | use Test; 4 | 5 | use lib 'lib'; 6 | use Text::Markdown; 7 | 8 | plan 2; 9 | 10 | my Str $filename = 't/text.md'; 11 | my $md = parse-markdown-from-file($filename); 12 | 13 | ok $md ~~ Text::Markdown; 14 | isa-ok $md, 'Text::Markdown'; 15 | -------------------------------------------------------------------------------- /t/parse-link-with-code.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | use Text::Markdown::Document; 3 | use Test; 4 | 5 | plan 4; 6 | 7 | my $text = q:to/TEXT/; 8 | [`grep`](https://docs.perl6.org/routine/grep) is 9 | one of them 10 | TEXT 11 | 12 | my $document = Text::Markdown::Document.new($text); 13 | ok $document ~~ Text::Markdown::Document, 'Able to parse'; 14 | is $document.items.elems, 1, 'has correct number of items'; 15 | 16 | my $p = $document.items[0]; 17 | ok $p ~~ Text::Markdown::Paragraph, 'It is a Paragraph'; 18 | is $p.items[0] ~~ Text::Markdown::Link, True, "First element is a link"; 19 | -------------------------------------------------------------------------------- /t/parse-with-underscore.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | use Text::Markdown::Document; 3 | use Test; 4 | 5 | plan 4; 6 | 7 | my $text = q:to/TEXT/; 8 | this character belongs to this 9 | [category](https://en.wikipedia.org/wiki/Unicode_character_property#General_Category), 10 | TEXT 11 | 12 | my $document = Text::Markdown::Document.new($text); 13 | ok $document ~~ Text::Markdown::Document, 'Able to parse'; 14 | is $document.items.elems, 1, 'has correct number of items'; 15 | 16 | my $p = $document.items[0]; 17 | ok $p ~~ Text::Markdown::Paragraph, 'It is a Paragraph'; 18 | is $p.items.elems, 3, "Slurped link correctly"; 19 | -------------------------------------------------------------------------------- /t/parse.t: -------------------------------------------------------------------------------- 1 | use v6; 2 | use Text::Markdown::Document; 3 | use Test; 4 | 5 | plan 190; 6 | 7 | my $text = q:to/TEXT/; 8 | ## Markdown Test ## 9 | 10 | This is a simple markdown document. 11 | 12 | --- 13 | 14 | It has two 15 | paragraphs. 16 | TEXT 17 | 18 | my $document = Text::Markdown::Document.new($text); 19 | ok $document ~~ Text::Markdown::Document, 'Able to parse'; 20 | is $document.items.elems, 4, 'has correct number of items'; 21 | 22 | ok $document.items[0] ~~ Text::Markdown::Heading, 'first element is a header'; 23 | is $document.items[0].text, 'Markdown Test', '...with the right data'; 24 | is $document.items[0].level, 2, '...and the right heading level'; 25 | 26 | ok $document.items[1] ~~ Text::Markdown::Paragraph, 'second element is a paragraph'; 27 | is $document.items[1].items[0], 'This is a simple markdown document.' 28 | , '...with the right data'; 29 | 30 | ok $document.items[2] ~~ Text::Markdown::Rule, 'third element is a rule'; 31 | 32 | ok $document.items[3] ~~ Text::Markdown::Paragraph, 'fourth element is a paragraph'; 33 | is $document.items[3].items[0], 'It has two paragraphs.', '...with the right data'; 34 | 35 | is $document.items-of-type("Paragraph").elems, 2, "Correct number of paragraphs"; 36 | 37 | ## next text with lists 38 | 39 | $text = q:to/TEXT/; 40 | - List One 41 | - List Two 42 | 43 | > blockquote 44 | > fun 45 | 46 | code 47 | block 48 | 49 | - Block List One 50 | 51 | - Block List Two 52 | 53 | 1. ol One 54 | 2. ol Two 55 | 56 | * Other List One 57 | * Other List Two 58 | 59 | TEXT 60 | 61 | $document = Text::Markdown::Document.new($text); 62 | ok $document ~~ Text::Markdown::Document, 'Able to parse'; 63 | is $document.items.elems, 6, 'has correct number of items'; 64 | 65 | my $li = $document.items[0]; 66 | ok $li ~~ Text::Markdown::List, 'first element is a list'; 67 | ok !$li.numbered, '...that is unordered'; 68 | ok $li.items == 2, '...with two items'; 69 | 70 | # not sure how I want to represent simple elements, since I need to support 71 | # inline... 72 | # 73 | # maybe just a list of arrays? ::ListItem? 74 | # (would also be good to get ::Document out of the list) 75 | 76 | #ok $li.items[0] ~~ Str, '...with simple elements'; 77 | #is $li.items[1], 'List Two', '...and correct data'; 78 | 79 | my $bi = $document.items[1]; 80 | ok $bi ~~ Text::Markdown::Blockquote, 'second element is a blockquote'; 81 | ok $bi.items == 1, '...with one item'; 82 | ok $bi.items[0] ~~ Text::Markdown::Paragraph, '...which is a paragraph'; 83 | is $bi.items[0].items[0], 'blockquote fun', '...with the correct data'; 84 | 85 | my $ci = $document.items[2]; 86 | ok $ci ~~ Text::Markdown::CodeBlock, 'third element is a code block'; 87 | is $ci.text, "code\nblock", '...with correct data'; 88 | 89 | $li = $document.items[3]; 90 | ok $li ~~ Text::Markdown::List, 'fourth element is a list'; 91 | ok $li.items == 2, '...with two items'; 92 | $li = $li.items[1]; 93 | ok $li ~~ Text::Markdown::Document, '...with complex elements'; 94 | is $li.items[0].items[0], 'Block List Two', '...with correct data'; 95 | 96 | $li = $document.items[4]; 97 | ok $li ~~ Text::Markdown::List, 'fifth element is a list'; 98 | ok $li.numbered, '...which is ordered'; 99 | ok $li.items == 2, '...with two items'; 100 | 101 | $li = $document.items[5]; 102 | ok $li ~~ Text::Markdown::List, 'sixth element is a list'; 103 | ok $li.items == 2, '...with two items'; 104 | 105 | ## next text with fenced code blocks 106 | $text = q:to/TEXT/; 107 | ``` 108 | # unknown 109 | code 110 | ``` 111 | 112 | ```raku 113 | # raku code 114 | ``` 115 | 116 | TEXT 117 | 118 | $document = Text::Markdown::Document.new($text); 119 | ok $document ~~ Text::Markdown::Document, 'Able to parse'; 120 | is $document.items.elems, 2, 'has correct number of items'; 121 | 122 | $ci = $document.items[0]; 123 | ok $ci ~~ Text::Markdown::CodeBlock, 'first element is a code block'; 124 | is $ci.text, "# unknown\ncode", '...with correct data'; 125 | 126 | $ci = $document.items[1]; 127 | ok $ci ~~ Text::Markdown::CodeBlock, 'second element is a code block'; 128 | is $ci.text, "# raku code", '...with correct data'; 129 | 130 | ## next text with lists 131 | $text = q:to/TEXT/; 132 | This is a *paragraph* with **many** `different` ``inline` elements``. 133 | [Links](http://google.com), for [example][], as well as ![Images](/bad/path.jpg) 134 | (including ![Reference][] style) 135 | 136 | [example]: http://example.com 137 | [Reference]: /another/bad/image.jpg 138 | TEXT 139 | 140 | $document = Text::Markdown::Document.new($text); 141 | ok $document ~~ Text::Markdown::Document, 'Able to parse'; 142 | is $document.items.elems, 1, 'has correct number of items'; 143 | my $p = $document.items[0]; 144 | ok $p ~~ Text::Markdown::Paragraph, '...which is a single paragraph'; 145 | 146 | is $p.items.elems, 18, 'with the right number of sub-items'; 147 | 148 | is $p.items[0], 'This is a ', 'first text chunk'; 149 | 150 | ok $p.items[1] ~~ Text::Markdown::Emphasis, 'first emphasis chunk'; 151 | is $p.items[1].level, 1, '...with correct emphasis'; 152 | is $p.items[1].text, 'paragraph', '...and text'; 153 | 154 | is $p.items[2], ' with ', 'second text chunk'; 155 | 156 | ok $p.items[3] ~~ Text::Markdown::Emphasis, 'second emphasis chunk'; 157 | is $p.items[3].level, 2, '...with correct emphasis'; 158 | is $p.items[3].text, 'many', '...and text'; 159 | 160 | is $p.items[4], ' ', 'third text chunk'; 161 | 162 | ok $p.items[5] ~~ Text::Markdown::Code, 'first code chunk'; 163 | is $p.items[5].text, 'different', '...with correct text'; 164 | 165 | is $p.items[6], ' ', 'fourth text chunk'; 166 | 167 | ok $p.items[7] ~~ Text::Markdown::Code, 'second code chunk'; 168 | is $p.items[7].text, 'inline` elements', '...with correct text'; 169 | 170 | is $p.items[8], '. ', 'fifth text chunk'; 171 | 172 | ok $p.items[9] ~~ Text::Markdown::Link, 'first link'; 173 | is $p.items[9].url, 'http://google.com', '...with correct link'; 174 | is $p.items[9].text, 'Links', '...with correct text'; 175 | ok !$p.items[9].ref, '...with correct ref'; 176 | 177 | is $p.items[10], ', for ', 'sixth text chunk'; 178 | 179 | ok $p.items[11] ~~ Text::Markdown::Link, 'second link'; 180 | ok !$p.items[11].url, '...with correct link'; 181 | is $p.items[11].text, 'example', '...with correct text'; 182 | is $p.items[11].ref, 'example', '...with correct ref'; 183 | 184 | is $p.items[12], ', as well as ', 'seventh text chunk'; 185 | 186 | is $p.items-of-type("Image").elems, 2, "Correct number of images"; 187 | 188 | ok $p.items[13] ~~ Text::Markdown::Image, 'first image'; 189 | is $p.items[13].url, '/bad/path.jpg', '...with correct link'; 190 | is $p.items[13].text, 'Images', '...with correct text'; 191 | ok !$p.items[13].ref, '...with correct ref'; 192 | 193 | is $p.items[14], ' (including ', 'eighth text chunk'; 194 | 195 | ok $p.items[15] ~~ Text::Markdown::Image, 'second image'; 196 | ok !$p.items[15].url, '...with correct link'; 197 | is $p.items[15].text, 'Reference', '...with correct text'; 198 | is $p.items[15].ref, 'Reference', '...with correct ref'; 199 | 200 | is $p.items[16], ' style) ', 'ninth text chunk'; 201 | 202 | ok $p.items[17] ~~ Text::Markdown::Link, 'third link'; 203 | is $p.items[17].url, 'http://google.com', '...with correct link'; 204 | is $p.items[17].text, 'http://google.com', '...with correct text'; 205 | ok !$p.items[17].ref, '...with correct ref'; 206 | 207 | is $document.references.elems, 2, 'got correct reference count'; 208 | is $document.references, 'http://example.com', 'first ref'; 209 | is $document.references, '/another/bad/image.jpg', 'second ref'; 210 | is $document.items-of-type("Paragraph").elems, 1, "Correct number of paragraphs"; 211 | 212 | $text = q:to/TEXT/; 213 | This one tests links like or or 214 | , as well as mail addresses like 215 | or . 216 | 217 | Finally, we test inline html elements like example 218 | or block elements like 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 |
    AB
    XY
    229 | TEXT 230 | 231 | $document = Text::Markdown::Document.new($text); 232 | ok $document ~~ Text::Markdown::Document, 'Able to parse'; 233 | is $document.items.elems, 3, 'has correct number of items'; 234 | $p = $document.items[0]; 235 | ok $p ~~ Text::Markdown::Paragraph, 'first block is a paragraph'; 236 | is $p.items.elems, 12, 'correct number of elements in paragraph'; 237 | is $p.items[0], 'This one tests links like ', 'starts with some text'; 238 | ok $p.items[1] ~~ Text::Markdown::Link, 'inline link'; 239 | is $p.items[1].url, 'http://example.com', 'inline link has correct url'; 240 | is $p.items[1].text, 'http://example.com', 'inline link has correct text'; 241 | ok !$p.items[1].ref, 'inline link has no ref'; 242 | is $p.items[2], ' or ', 'separator word between links.'; 243 | ok $p.items[3] ~~ Text::Markdown::Link, 'inline link no domain name'; 244 | is $p.items[3].url, 'http://a', 'inline link that is not a domain name has correct url'; 245 | is $p.items[3].text, 'http://a', 'inline link that is not a domain name has correct text'; 246 | ok !$p.items[3].ref, 'inline link has no ref'; 247 | is $p.items[4], ' or ', 'separator word between links.'; 248 | ok $p.items[5] ~~ Text::Markdown::Link, 'inline link no domain name'; 249 | is $p.items[5].url, 'http://a/', 'inline link that is not a domain name has correct url'; 250 | is $p.items[5].text, 'http://a/', 'inline link that is not a domain name has correct text'; 251 | ok !$p.items[5].ref, 'inline link has no ref'; 252 | ok $p.items[6] ~~ Text::Markdown::Link, 'inline link no domain name'; 253 | is $p.items[6].url, 'https://b', 'inline link that is not a domain name has correct url'; 254 | is $p.items[6].text, 'https://b', 'inline link that is not a domain name has correct text'; 255 | ok !$p.items[6].ref, 'inline link has no ref'; 256 | is $p.items[7], ', as well as mail addresses like ', 'separator text between links.'; 257 | ok $p.items[8] ~~ Text::Markdown::EmailLink, 'inline link to mail address'; 258 | is $p.items[8].url, 'example@example.com', 'email link contains correct url'; 259 | is $p.items[9], ' or ', 'separator text between links.'; 260 | ok $p.items[10] ~~ Text::Markdown::EmailLink, 'inline link to mail address'; 261 | is $p.items[10].url, 'camelia.is.the.best@perl6.org', 'email link contains correct url'; 262 | is $p.items[11], '.', 'text at end of paragraph'; 263 | 264 | $p = $document.items[1]; 265 | ok $p ~~ Text::Markdown::Paragraph, 'second block is a paragraph'; 266 | is $p.items.elems, 7, 'second paragraph contains correct number of elements'; 267 | is $p.items[0], 'Finally, we test inline html elements like ', 'text before html tags'; 268 | ok $p.items[1] ~~ Text::Markdown::HtmlTag, 'tags are parsed'; 269 | is $p.items[1].tag, '', 'tag content is correct'; 270 | ok $p.items[2] ~~ Text::Markdown::HtmlTag, 'tags are parsed'; 271 | is $p.items[2].tag, '', 'tag content is correct'; 272 | is $p.items[3], 'example', 'text between tags'; 273 | ok $p.items[4] ~~ Text::Markdown::HtmlTag, 'tags are parsed'; 274 | is $p.items[4].tag, '', 'tag content is correct'; 275 | ok $p.items[5] ~~ Text::Markdown::HtmlTag, 'tags are parsed'; 276 | is $p.items[5].tag, '', 'tag content is correct'; 277 | is $p.items[6], ' or block elements like', 'text before html tags'; 278 | is $p.items-of-type("HtmlTag").elems, 4, "Correct number of elements"; 279 | 280 | $p = $document.items[2]; 281 | is $p.items.elems, 26, 'correct number of elements in html block'; 282 | ok $p ~~ Text::Markdown::HtmlBlock, 'third block is an html block'; 283 | my %tags = 284 | 0 => '', 285 | 2 => '', 286 | 4 => '', 288 | 7 => '', 290 | 11 => '', 291 | 13 => '', 292 | 15 => '', 294 | 19 => '', 296 | 23 => '', 297 | 25 => '
    ', 287 | 6 => '', 289 | 9 => '
    ', 293 | 17 => '', 295 | 21 => '
    '; 298 | for %tags.kv -> $position, $value { 299 | ok $p.items[$position] ~~ Text::Markdown::HtmlTag, 'html tags parsed correctly'; 300 | is $p.items[$position].tag, $value, 'html tag contents correct'; 301 | } 302 | my %strings = 303 | 5 => 'A', 304 | 8 => 'B', 305 | 16 => 'X', 306 | 20 => 'Y'; 307 | for %strings.kv -> $position, $value { 308 | is $p.items[$position], $value, 'text parts in html block correct'; 309 | } 310 | my @spaces = 1, 3, 10, 12, 14, 18, 22, 24; 311 | for @spaces -> $position { 312 | ok $p.items[$position] ~~ /\s+/, 'white spaces detected correctly'; 313 | } 314 | 315 | $text = q:to/TEXT/; 316 | My paragraph. 317 | 318 | ``` 319 | my $code = self; 320 | ``` 321 | 322 | The list is: 323 | 324 | * First! 325 | * Second! 326 | * `third!` 327 | 328 | The end. 329 | TEXT 330 | 331 | $document = Text::Markdown::Document.new($text); 332 | ok $document ~~ Text::Markdown::Document, 'Able to parse'; 333 | is $document.items.elems, 5, 'has correct number of items'; 334 | ok $document.items[0] ~~ Text::Markdown::Paragraph, 'first element is a paragraph'; 335 | is $document.items[0].items[0], 'My paragraph.', '...with the right data'; 336 | 337 | 338 | ok $document.items[1] ~~ Text::Markdown::CodeBlock, 'second element is a code block'; 339 | is $document.items[1].text, 'my $code = self;', '...with the right data'; 340 | 341 | ok $document.items[2] ~~ Text::Markdown::Paragraph, 'third element is a paragraph'; 342 | is $document.items[2].items[0], 'The list is:', '...with the right data'; 343 | 344 | ok $document.items[3] ~~ Text::Markdown::List, 'fourth element is a list'; 345 | ok not $document.items[3].numbered, '...which is not ordered'; 346 | ok $document.items[3].items == 3, '...with three items'; 347 | 348 | ok $document.items[4] ~~ Text::Markdown::Paragraph, 'fifth element is a paragraph'; 349 | is $document.items[4].items[0], 'The end.', '...with the right data'; 350 | 351 | $text = q:to/TEXT/; 352 | My `VALUE_WITH_UNDERSCORE`. 353 | 354 | My ``. 355 | TEXT 356 | 357 | $document = Text::Markdown::Document.new($text); 358 | ok $document ~~ Text::Markdown::Document, 'Able to parse'; 359 | is $document.items.elems, 2, 'has correct number of items'; 360 | ok $document.items[0].items[1] ~~ Text::Markdown::Code, 'code chunk is parsed'; 361 | is $document.items[0].items[1], '`VALUE_WITH_UNDERSCORE`', 'value is correct'; 362 | ok $document.items[1].items[1] ~~ Text::Markdown::Code, 'tag is parsed'; 363 | is $document.items[1].items[1], '``', 'value is correct'; 364 | 365 | 366 | -------------------------------------------------------------------------------- /t/structured.t: -------------------------------------------------------------------------------- 1 | use Text::Markdown; 2 | use Test; 3 | 4 | my $text = q:to/TEXT/; 5 | # Tuna risotto 6 | 7 | A relatively simple version of this rich, creamy dish of Italian origin. 8 | 9 | ## Ingredients (for 4 persons) 10 | 11 | * 500g tuna 12 | * 250g rice 13 | * Half an onion 14 | * 250g cheese (parmegiano reggiano or granapadano, or manchego) 15 | * Extra virgin olive oil 16 | * 4 cloves garlic 17 | TEXT 18 | 19 | my $md = parse-markdown($text); 20 | isa-ok( $md, Text::Markdown, "Instantiation OK"); 21 | isa-ok( $md.document.items[2], Text::Markdown::Heading, "Second level OK"); 22 | is( $md.document.items[2].level, 2, "Level heading OK"); 23 | isa-ok( $md.document.items[3], Text::Markdown::List, "Third object is a list"); 24 | is( $md.document.items[3].items.elems, 6, "List has the correct length"); 25 | 26 | $text = q:to/TEXT/; 27 | * a list item 28 | TEXT 29 | 30 | $md = parse-markdown( $text ); 31 | is( $md.document.items.first.items.elems, 1, "There's one element in the list"); 32 | 33 | done-testing; 34 | -------------------------------------------------------------------------------- /t/text.md: -------------------------------------------------------------------------------- 1 | ## Markdown Test ## 2 | 3 | This is a simple markdown document. 4 | 5 | --- 6 | --------------------------------------------------------------------------------