├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── pack.pl ├── prolog └── md │ ├── md_blocks.pl │ ├── md_escape.pl │ ├── md_header.pl │ ├── md_hr.pl │ ├── md_line.pl │ ├── md_links.pl │ ├── md_list_item.pl │ ├── md_parse.pl │ ├── md_span.pl │ ├── md_span_decorate.pl │ ├── md_span_link.pl │ └── md_trim.pl └── tests ├── block.pl ├── file.md ├── file.pl ├── header.pl ├── html.pl ├── link.pl ├── list_item.pl ├── span.pl ├── span_link.pl └── tests.pl /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | *.tgz 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - sudo apt-add-repository ppa:swi-prolog/devel -y 3 | - sudo apt-get update -q 4 | - sudo apt-get install swi-prolog-nox 5 | 6 | script: make test 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Raivo Laanemets 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 17 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 20 | IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | version:=$(shell swipl -q -s pack -g 'version(V),writeln(V)' -t halt) 2 | packfile=markdown-$(version).tgz 3 | remote=sites@rlaanemets.com:/sites/packs.rlaanemets.com/public/markdown 4 | 5 | test: 6 | swipl -s tests/tests.pl -g run_tests,halt -t 'halt(1)' 7 | 8 | package: test 9 | tar cvzf $(packfile) prolog tests pack.pl README.md LICENSE 10 | 11 | doc: 12 | swipl -q -t 'doc_save(prolog/md, [doc_root(doc),format(html),title(markdown),if(true),recursive(true)])' 13 | 14 | upload: doc package 15 | scp $(packfile) $(remote)/$(packfile) 16 | rsync -avz -e ssh doc $(remote) 17 | 18 | .PHONY: test package doc upload 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prolog-markdown 2 | 3 | Markdown parser implemented in Prolog. Compatible with [SWI-Prolog](http://www.swi-prolog.org/) as the 4 | output tree is for direct use by [html//1](https://www.swi-prolog.org/pldoc/man?predicate=html//1). 5 | The specification for the parser was taken from 6 | (Gruber's Markdown). 7 | 8 | [![Build Status](https://travis-ci.org/rla/prolog-markdown.svg)](https://travis-ci.org/rla/prolog-markdown) 9 | 10 | ## Example usage 11 | 12 | Parse into a structure usable by 13 | [html//1](https://www.swi-prolog.org/pldoc/man?predicate=html//1). 14 | 15 | :- use_module(library(md/md_parse)). 16 | 17 | ?- md_parse_string("# Hello #", Blocks). 18 | Blocks = [h1("Hello")]. 19 | 20 | Convert into an HTML string: 21 | 22 | :- use_module(library(md/md_parse)). 23 | 24 | ?- md_html_string("# Hello #", Html). 25 | Html = "

Hello

". 26 | 27 | ## Deviations from the Gruber's Markdown 28 | 29 | * Some cases for tight markup (no separate lines between blocks). 30 | * No clever encoding for mail addresses. 31 | * Line break rule creates `
` not `
`. 32 | * No in-word emphasis with underscores. 33 | * Strikethrough as `~~text~~`. 34 | * Added escape sequences `\~` and `` \` ``. 35 | * Github-styled fenced code blocks (). 36 | No syntax highlighting is provided but the `data-language` attribute is set. 37 | * Plain link recognizion. 38 | 39 | ## Performance 40 | 41 | Example [document](http://daringfireball.net/projects/markdown/syntax.text) (about 800 lines) is parsed 42 | in 80ms on 2.4GHz Q6600. 43 | 44 | ## Installation 45 | 46 | Requires SWI-Prolog 7.x. 47 | 48 | pack_install('http://packs.rlaanemets.com/markdown/markdown-*.tgz') 49 | 50 | ## API documentation 51 | 52 | See for the top-level module documentation. 53 | 54 | ## Changelog 55 | 56 | * 2020-01-02 version 0.0.3. Fixed mode declaration in documentation. 57 | * 2014-02-26 version 0.0.2. Fixed HTTPS link in angled brackets. Trimmed bottom of code block. 58 | * 2014-01-14 version 0.0.1 59 | 60 | ## Bug reports/feature requests 61 | 62 | Please send bug reports/feature request through the GitHub 63 | project [page](https://github.com/rla/prolog-markdown). 64 | 65 | ## License 66 | 67 | The MIT License. See the LICENSE file. 68 | -------------------------------------------------------------------------------- /pack.pl: -------------------------------------------------------------------------------- 1 | name(markdown). 2 | version('0.0.3'). 3 | title('Markdown parser for SWI-Prolog'). 4 | author('Raivo Laanemets', 'http://rlaanemets.com/'). 5 | home('https://github.com/rla/prolog-markdown'). 6 | -------------------------------------------------------------------------------- /prolog/md/md_blocks.pl: -------------------------------------------------------------------------------- 1 | :- module(md_blocks, [ 2 | md_blocks//1 % -Blocks 3 | ]). 4 | 5 | /** Block-level parser for Markdown 6 | 7 | Parses Markdown block-level constructs like 8 | paragraphs, lists, code blocks, blockquotes etc. 9 | Applies span-level parsing for all blocks. 10 | */ 11 | 12 | :- use_module(library(dcg/basics)). 13 | 14 | :- use_module(md_list_item). 15 | :- use_module(md_header). 16 | :- use_module(md_span). 17 | :- use_module(md_trim). 18 | :- use_module(md_line). 19 | :- use_module(md_hr). 20 | 21 | %! md_blocks(-Blocks)// is det. 22 | % 23 | % Parses given Markdown into a structure 24 | % accepted by html//1. 25 | 26 | md_blocks(Blocks) --> 27 | blocks(top, [], Blocks). 28 | 29 | % Contextified block parsing. 30 | % Some types of blocks are not 31 | % allowed in contexts other than top. 32 | % Currently used contexts are: top, 33 | % list and bq. 34 | 35 | md_blocks(Ctx, Blocks) --> 36 | blocks(Ctx, [], Blocks). 37 | 38 | % Recognizes all blocks 39 | % in the input. When a block is not 40 | % recognized, one line as removed and 41 | % added into accumulator. These accumulated 42 | % lines are added as paragraph blocks. 43 | % This matches better the sematics of 44 | % http://daringfireball.net/projects/markdown/dingus 45 | 46 | blocks(Ctx, Acc, Result) --> 47 | empty_lines, 48 | block(Ctx, Block), !, 49 | { 50 | ( Acc = [] 51 | -> Result = [Block|Blocks] 52 | ; acc_block(Acc, AccBlock), 53 | Result = [AccBlock,Block|Blocks]) 54 | }, 55 | blocks(Ctx, [], Blocks). 56 | 57 | blocks(Ctx, Acc, Blocks) --> 58 | non_empty_line(Line), !, 59 | blocks(Ctx, [Line|Acc], Blocks). 60 | 61 | blocks(_, Acc, Result) --> 62 | empty_lines, 63 | eos, !, 64 | { 65 | ( Acc = [] 66 | -> Result = [] 67 | ; Result = [Block], 68 | acc_block(Acc, Block)) 69 | }. 70 | 71 | blocks(Ctx, Acc, Result) --> 72 | empty_line, 73 | { 74 | ( Acc = [] 75 | -> Result = Blocks 76 | ; Result = [Block|Blocks], 77 | acc_block(Acc, Block)) 78 | }, 79 | blocks(Ctx, [], Blocks). 80 | 81 | % Converts lines into a

82 | % element and applies span-level 83 | % parsing. 84 | 85 | acc_block(Acc, p(Span)):- 86 | reverse(Acc, AccLines), 87 | merge_lines(AccLines, Block), 88 | md_span_codes(Block, Span). 89 | 90 | % Recognizes a single block. 91 | % Tries to parse in the following 92 | % order: headers, horisontal ruler, 93 | % lists, blockquote, html. 94 | 95 | block(_, Block) --> 96 | md_header(Block), !. 97 | 98 | block(_, Block) --> 99 | code(Block), !. 100 | 101 | block(top, hr([])) --> 102 | md_hr, !. 103 | 104 | block(_, Block) --> 105 | list(Block), !. 106 | 107 | block(top, Block) --> 108 | blockquote(Block), !. 109 | 110 | block(_, Block) --> 111 | html(Block), !. 112 | 113 | block(_, Block) --> 114 | fenced_code(Block). 115 | 116 | code(pre(code(String))) --> 117 | indented_lines(Codes), !, 118 | { 119 | trim_right(Codes, Trimmed), 120 | string_codes(String, Trimmed) 121 | }. 122 | 123 | % Recognizes fenced code blocks. 124 | % The language is put into the 125 | % `data-language` attribute of the 126 | % `code` tag. 127 | 128 | fenced_code(Block) --> 129 | "```", inline_string(LangCodes), ln, 130 | string(Codes), 131 | ln, "```", whites, ln_or_eos, !, 132 | { 133 | trim(LangCodes, Trimmed), 134 | atom_codes(Lang, Trimmed), 135 | string_codes(Code, Codes), 136 | ( Lang = '' 137 | -> Block = pre(code(Code)) 138 | ; Block = pre(code(['data-language'=Lang], Code))) 139 | }. 140 | 141 | % Optimizes generated HTML structure. 142 | % Applied after parsing different blocks. 143 | % Mostly deals with excessive

elements 144 | % removal. 145 | 146 | optimize(blockquote([p(Block)]), blockquote(Block)):- !. 147 | 148 | optimize(li([p(Block)]), li(Block)):- !. 149 | 150 | optimize(li([p(Block1), ul(Block2)]), li(Block)):- !, 151 | append(Block1, [ul(Block2)], Block). 152 | 153 | optimize(li([p(Block1), ol(Block2)]), li(Block)):- !, 154 | append(Block1, [ol(Block2)], Block). 155 | 156 | optimize(Block, Block). 157 | 158 | % Recognizes a sequence of one or more 159 | % indented lines. Gives back codes of 160 | % whole sequence. 161 | 162 | indented_lines(Codes) --> 163 | indented_lines_collect(Lines), 164 | { 165 | Lines \= [], 166 | merge_lines(Lines, Codes) 167 | }. 168 | 169 | % Recognizes a sequence of indented lines. 170 | % There might be empty lines between 171 | % indented lines. 172 | 173 | indented_lines_collect([Line|Lines]) --> 174 | indented_line(Line), !, 175 | indented_lines_collect(Lines). 176 | 177 | indented_lines_collect([]) --> 178 | eos, !. 179 | 180 | indented_lines_collect([[]|Lines]) --> 181 | empty_line, !, 182 | indented_lines_collect(Lines). 183 | 184 | indented_lines_collect([]) --> "". 185 | 186 | indented_line(Line) --> 187 | indent, inline_string(Line), ln_or_eos. 188 | 189 | % Recognizes block-level HTML. 190 | % No Markdown inside it is processed. 191 | % Gives term that write_html's html//1 192 | % does not escape. 193 | 194 | html(\[String]) --> 195 | [0'<, Code], { code_type(Code, alpha) }, !, 196 | non_empty_lines(Html), 197 | { string_codes(String, [0'<,Code|Html]) }. 198 | 199 | % Recognizes either ordered list 200 | % or bulleted list. 201 | 202 | list(List) --> 203 | bullet_list(List), !. 204 | 205 | list(List) --> 206 | ordered_list(List). 207 | 208 | % Recognizes ordered list. 209 | % Gives term like ol(Term) 210 | % where Items is non-empty list. 211 | 212 | ordered_list(ol(Items)) --> 213 | ordered_list_collect(Items, _), !, 214 | { Items \= [] }. 215 | 216 | ordered_list_collect([Item|Items], Mode) --> 217 | ordered_list_item(Item, Mode), !, 218 | empty_lines, 219 | ordered_list_collect(Items, Mode). 220 | 221 | ordered_list_collect([], _) --> "". 222 | 223 | % Recognizes a single ordered list item. 224 | 225 | ordered_list_item(Item, ListMode) --> 226 | md_ordered_list_item(Codes, ItemMode), 227 | { postproc_list_item(Codes, ItemMode, ListMode, Item) }. 228 | 229 | % Recognizes bulleted list. 230 | % Gives a term like ul(Items) 231 | % where Items is non-empty list. 232 | 233 | bullet_list(ul(Items)) --> 234 | bullet_list_collect(Items, _), !, 235 | { Items \= [] }. 236 | 237 | bullet_list_collect([Item|Items], Mode) --> 238 | bullet_list_item(Item, Mode), !, 239 | empty_lines, 240 | bullet_list_collect(Items, Mode). 241 | 242 | bullet_list_collect([], _) --> "". 243 | 244 | % Recognizes a single bulleted list item. 245 | 246 | bullet_list_item(Item, ListMode) --> 247 | md_bullet_list_item(Codes, ItemMode), 248 | { postproc_list_item(Codes, ItemMode, ListMode, Item) }. 249 | 250 | % Postprocesses a list item. 251 | % In paragraph mode, no optimizations are 252 | % applied (preserves `

` in `

  • `). 253 | % The actual list mode is set by the first 254 | % list item. 255 | 256 | postproc_list_item(Codes, ItemMode, ListMode, Item):- 257 | phrase(md_blocks(list, Blocks), Codes), 258 | list_mode(ListMode, ItemMode, Mode), 259 | ( Mode = normal 260 | -> optimize(li(Blocks), Item) 261 | ; Item = li(Blocks)). 262 | 263 | % List mode setup. When ListMode 264 | % is set, its value is used. Otherwise 265 | % ListMode is set to ItemMode. 266 | 267 | list_mode(ListMode, ItemMode, Mode):- 268 | ( var(ListMode) 269 | -> ListMode = ItemMode 270 | ; true), 271 | Mode = ListMode. 272 | 273 | % Recognizes a blockquote. 274 | % Strips > from line beginnings. 275 | % Output is a term like blockquote(Blocks). 276 | 277 | blockquote(Opt) --> 278 | ">", string(Codes), 279 | empty_line, 280 | empty_line, !, 281 | { 282 | trim_left(Codes, Trimmed), 283 | phrase(bq_strip(Stripped), Trimmed), !, 284 | phrase(md_blocks(top, Blocks), Stripped), 285 | optimize(blockquote(Blocks), Opt) 286 | }. 287 | 288 | % Strips > from blockquote line 289 | % beginnings. 290 | 291 | bq_strip([0'\n|Codes]) --> 292 | ln, "> ", !, bq_strip(Codes). 293 | 294 | bq_strip([0'\n|Codes]) --> 295 | ln, ">", !, bq_strip(Codes). 296 | 297 | bq_strip([Code|Codes]) --> 298 | [Code], !, bq_strip(Codes). 299 | 300 | bq_strip([]) --> 301 | eos. 302 | 303 | % List of consequtive non-empty lines. 304 | % Consumes as many non-empty lines 305 | % as possible. Gives flattened list 306 | % of codes. 307 | 308 | non_empty_lines(Codes) --> 309 | non_empty_lines_collect(Lines), 310 | { merge_lines(Lines, Codes) }, !. 311 | 312 | non_empty_lines_collect([Line|Lines]) --> 313 | non_empty_line(Line), !, 314 | non_empty_lines_collect(Lines). 315 | 316 | non_empty_lines_collect([]) --> "". 317 | -------------------------------------------------------------------------------- /prolog/md/md_escape.pl: -------------------------------------------------------------------------------- 1 | :- module(md_escape, [ 2 | md_escaped_string//1, % -Codes, 3 | md_escaped_code//1, % ?Code 4 | md_escaped_code/1 % ?Code 5 | ]). 6 | 7 | /** Markdown slash-escaped sequences 8 | 9 | Recognizes Markdown slash-escaped sequences. More info: 10 | http://daringfireball.net/projects/markdown/syntax#backslash 11 | */ 12 | 13 | %! md_escaped_string(-Codes)// is det. 14 | % 15 | % Recognizes string with escapes inside it. 16 | % Consumes new code/escape sequence on backtracking. 17 | 18 | md_escaped_string([]) --> "". 19 | 20 | md_escaped_string([Code|Codes]) --> 21 | "\\", md_escaped_code(Code), !, 22 | md_escaped_string(Codes). 23 | 24 | md_escaped_string([Code|Codes]) --> 25 | [Code], md_escaped_string(Codes). 26 | 27 | %! md_escaped_code(-Codes)// is det. 28 | % 29 | % Recognizes single code that could 30 | % have been escaped. 31 | 32 | md_escaped_code(Code) --> 33 | [Code], { md_escaped_code(Code) }. 34 | 35 | %! md_escaped_code(?Code) is nondet. 36 | % 37 | % List of possibly escaped symbols. 38 | % More info: 39 | % http://daringfireball.net/projects/markdown/syntax#backslash 40 | 41 | md_escaped_code(0'\\). 42 | md_escaped_code(0'`). 43 | md_escaped_code(0'*). 44 | md_escaped_code(0'_). 45 | md_escaped_code(0'{). 46 | md_escaped_code(0'}). 47 | md_escaped_code(0'[). 48 | md_escaped_code(0']). 49 | md_escaped_code(0'(). 50 | md_escaped_code(0')). 51 | md_escaped_code(0'#). 52 | md_escaped_code(0'+). 53 | md_escaped_code(0'-). 54 | md_escaped_code(0'.). 55 | md_escaped_code(0'!). 56 | md_escaped_code(0'~). 57 | -------------------------------------------------------------------------------- /prolog/md/md_header.pl: -------------------------------------------------------------------------------- 1 | :- module(md_header, [ 2 | md_header//1 % -Header 3 | ]). 4 | 5 | /** Markdown header parser. 6 | 7 | Recognizes atx and setext-styled headers. 8 | */ 9 | 10 | :- use_module(library(dcg/basics)). 11 | 12 | :- use_module(md_line). 13 | :- use_module(md_trim). 14 | 15 | %! md_header(-Header)// is semidet. 16 | % 17 | % Recognizes either setext 18 | % or atx-styled headings. 19 | 20 | md_header(Header) --> 21 | setext_header(Header), !. 22 | 23 | md_header(Header) --> 24 | atx_header(Header). 25 | 26 | % Recognizes setext-styled headings. 27 | % Output is a term like h1("heading"). 28 | 29 | setext_header(Header) --> 30 | non_empty_line(Codes), 31 | ( equals_line, 32 | { Name = h1 } 33 | ; dashes_line, 34 | { Name = h2 }), 35 | { 36 | string_codes(Title, Codes), 37 | Header =.. [Name, Title] 38 | }. 39 | 40 | % Recognizes atx-styled heading. 41 | % Output is a term like h1(heading). 42 | 43 | atx_header(Header) --> 44 | atx_level(Level), whites, 45 | atx_header_text(Codes), !, 46 | discard_to_line_end, 47 | { 48 | trim(Codes, Trimmed), 49 | string_codes(Title, Trimmed), 50 | atomic_concat(h, Level, Name), 51 | Header =.. [Name,Title] 52 | }. 53 | 54 | % Recognizes atx-styled header 55 | % prefix. 56 | 57 | atx_level(1) --> "# ". 58 | atx_level(2) --> "## ". 59 | atx_level(3) --> "### ". 60 | atx_level(4) --> "#### ". 61 | atx_level(5) --> "##### ". 62 | atx_level(6) --> "###### ". 63 | 64 | % Recognizes header text for 65 | % atx-styles headings. Such text 66 | % ends with #, line end, or eos. 67 | % Escapes \# must be also processed. 68 | % Line ending is not consumed. 69 | 70 | atx_header_text([]) --> 71 | "#", !. 72 | 73 | atx_header_text([]) --> 74 | lookahead_ln_or_eos, !. 75 | 76 | atx_header_text([0'#|Codes]) --> 77 | "\\#", !, 78 | atx_header_text(Codes). 79 | 80 | atx_header_text([Code|Codes]) --> 81 | [Code], { Code \= 0'# }, !, 82 | atx_header_text(Codes). 83 | 84 | % Line filles with one or more dashes. 85 | 86 | dashes_line --> 87 | "-", dashes_line_rest. 88 | 89 | dashes_line_rest --> 90 | eos, !. 91 | 92 | dashes_line_rest --> 93 | "\n", !. 94 | 95 | dashes_line_rest --> 96 | "-", dashes_line_rest. 97 | 98 | equals_line --> 99 | "=", equals_line_rest. 100 | 101 | equals_line_rest --> 102 | eos, !. 103 | 104 | equals_line_rest --> 105 | "\n", !. 106 | 107 | equals_line_rest --> 108 | "=", equals_line_rest. 109 | -------------------------------------------------------------------------------- /prolog/md/md_hr.pl: -------------------------------------------------------------------------------- 1 | :- module(md_hr, [ 2 | md_hr//0, 3 | md_lookahead_hr//0 4 | ]). 5 | 6 | /** Parser for Markdown horizontal rulers 7 | 8 | Recognizes horizontal rulers. 9 | */ 10 | 11 | :- use_module(library(dcg/basics)). 12 | :- use_module(md_line). 13 | 14 | %! md_hr// is semidet. 15 | % 16 | % Recognizes an horizontal ruler. 17 | 18 | md_hr --> 19 | code_hr(0'*, 3, _), !. 20 | 21 | md_hr --> 22 | code_hr(0'-, 3, _). 23 | 24 | %! md_lookahead_hr// is semidet. 25 | % 26 | % Looks ahead an horizontal ruler. 27 | 28 | md_lookahead_hr, Codes --> 29 | code_hr(0'*, 3, Codes), !. 30 | 31 | md_lookahead_hr, Codes --> 32 | code_hr(0'-, 3, Codes). 33 | 34 | % Recognizes given number of codes 35 | % separated on a single line by 0 36 | % or more spaces or tabs. 37 | 38 | code_hr(_, 0, []) --> 39 | eos, !. 40 | 41 | code_hr(_, 0, [0'\n]) --> 42 | "\n", !. 43 | 44 | code_hr(Code, 0, [Code|Codes]) --> 45 | [Code], !, code_hr(Code, 0, Codes). 46 | 47 | code_hr(Code, N, [0' |Codes]) --> 48 | " ", !, code_hr(Code, N, Codes). 49 | 50 | code_hr(Code, N, [0'\t|Codes]) --> 51 | "\t", !, code_hr(Code, N, Codes). 52 | 53 | code_hr(Code, N, [Code|Codes]) --> 54 | [Code], !, { N1 is N - 1 }, 55 | code_hr(Code, N1, Codes). 56 | -------------------------------------------------------------------------------- /prolog/md/md_line.pl: -------------------------------------------------------------------------------- 1 | :- module(md_line, [ 2 | merge_lines/2, % +Lines, -Codes 3 | indent//0, 4 | non_empty_line//1, % -Codes 5 | discard_to_line_end//0, 6 | empty_lines//0, 7 | empty_line//0, 8 | inline_string//1, % -Codes 9 | ln_or_eos//0, 10 | ln//0, 11 | string_limit//2, % -Codes, +Limit 12 | lookahead//1, % ?Code 13 | lookahead_ln//0, 14 | lookahead_ln_or_eos//0 15 | ]). 16 | 17 | /** Line-based parsing primitives. 18 | 19 | Contains line-based parsing primitives. 20 | */ 21 | 22 | :- use_module(library(dcg/basics)). 23 | 24 | %! merge_lines(+Lines, -Codes) is det. 25 | % 26 | % Merges list of lines into 27 | % a flat code list. 28 | 29 | merge_lines([], []). 30 | 31 | merge_lines([Line], Line):- !. 32 | 33 | merge_lines([Line|Lines], Codes):- 34 | merge_lines(Lines, Merged), 35 | append(Line, [0'\n|Merged], Codes). 36 | 37 | %! indent// is semidet. 38 | % 39 | % Recognizes normal indent which 40 | % is a tab or 4 spaces. 41 | 42 | indent --> "\t". 43 | indent --> " ". 44 | 45 | %! non_empty_line(-Codes)// is semidet. 46 | % 47 | % Single non-empty line ending with newline 48 | % or end-of-stream. 49 | 50 | non_empty_line([Code|Codes]) --> 51 | [Code], { Code \= 0'\n }, 52 | non_empty_line_rest(Codes). 53 | 54 | non_empty_line_rest([Code|Codes]) --> 55 | [Code], { Code \= 0'\n }, !, 56 | non_empty_line_rest(Codes). 57 | 58 | non_empty_line_rest([]) --> 59 | "\n", !. 60 | 61 | non_empty_line_rest([]) --> 62 | "". 63 | 64 | %! discard_to_line_end// is det. 65 | % 66 | % Discards zero or more symbol 67 | % codes untill the first line 68 | % end or eos is reached. 69 | 70 | discard_to_line_end --> 71 | ln_or_eos, !. 72 | 73 | discard_to_line_end --> 74 | [_], discard_to_line_end. 75 | 76 | %! empty_lines// is det. 77 | % 78 | % List of consequtive empty lines. 79 | % Consumes as many empty lines as 80 | % possible. 81 | 82 | empty_lines --> 83 | eos, !. 84 | 85 | empty_lines --> 86 | empty_line, !, 87 | empty_lines. 88 | 89 | empty_lines --> "". 90 | 91 | %! empty_line// is semidet. 92 | % 93 | % Recognizes a single empty line. 94 | 95 | empty_line --> 96 | whites, ln_or_eos. 97 | 98 | %! lookahead(?Code)// is semidet. 99 | % 100 | % Looks ahead a single symbol code. 101 | 102 | lookahead(Code), [Code] --> 103 | [Code]. 104 | 105 | %! string_limit(-Codes, +Limit)// is multi. 106 | % 107 | % Same as string//1 but with 108 | % a length limit. 109 | 110 | string_limit([], Limit) --> 111 | { Limit =< 0 }, !. 112 | 113 | string_limit([], Limit) --> 114 | { Limit >= 0 }. 115 | 116 | string_limit([Code|Codes], Limit) --> 117 | [Code], 118 | { Next is Limit - 1 }, 119 | string_limit(Codes, Next). 120 | 121 | %! inline_string(-Codes)// is multi. 122 | % 123 | % Takes as few symbol codes as possible 124 | % up to line end. 125 | 126 | inline_string([]) --> "". 127 | 128 | inline_string([]) --> 129 | lookahead_ln, !. 130 | 131 | inline_string([Code|Codes]) --> 132 | [Code], 133 | inline_string(Codes). 134 | 135 | %! lookahead_ln_or_eos// is semidet. 136 | % 137 | % Looks ahead a line end or 138 | % end-of-stream. Puts back `\n` 139 | % when a line end is recognized. 140 | 141 | lookahead_ln_or_eos --> 142 | lookahead_ln, !. 143 | 144 | lookahead_ln_or_eos --> 145 | eos. 146 | 147 | %! lookahead_ln// is semidet. 148 | % 149 | % Looks ahead a line end. Puts 150 | % back `\n` when it is recognized. 151 | 152 | lookahead_ln, "\n" --> ln. 153 | 154 | %! ln_or_eos// is semidet. 155 | % 156 | % Recognizes either a line end 157 | % or eos. 158 | 159 | ln_or_eos --> 160 | "\n", !. 161 | 162 | ln_or_eos --> 163 | eos. 164 | 165 | %! ln// is semidet. 166 | % 167 | % Recognizes line ending. 168 | 169 | ln --> "\n". 170 | -------------------------------------------------------------------------------- /prolog/md/md_links.pl: -------------------------------------------------------------------------------- 1 | :- module(md_links, [ 2 | md_links/3, % +CodesIn, -CodesOut, -Links 3 | md_links/2, % +CodesIn, -CodesOut, 4 | md_link/3 % ?Id, ?Url, ?Title 5 | ]). 6 | 7 | /** Markdown reference link parser 8 | 9 | Parses and removes reference links from 10 | the stream of symbol codes. Replaces 11 | line ends with canonical line ends. 12 | */ 13 | 14 | :- use_module(library(dcg/basics)). 15 | :- use_module(md_line). 16 | 17 | % link_definition(Id, Url, Title). 18 | 19 | :- thread_local(link_definition/3). 20 | 21 | %! md_link(?Id, ?Url, ?Title) is det. 22 | % 23 | % Retrieves recorded link from the last 24 | % invocation of md_links/2. 25 | 26 | md_link(Id, Url, Title):- 27 | link_definition(Id, Url, Title). 28 | 29 | %! md_links(+CodesIn, -CodesOut) is det. 30 | % 31 | % Same as md_links/3 but stores links 32 | % in threadlocal predicate which is cleared 33 | % on each invocation of this predicate. 34 | 35 | md_links(CodesIn, CodesOut):- 36 | retractall(link_definition(_, _, _)), 37 | md_links(CodesIn, CodesOut, Links), 38 | maplist(assert_link, Links). 39 | 40 | assert_link(link(Id, Url, Title)):- 41 | assertz(link_definition(Id, Url, Title)). 42 | 43 | %! md_links(+CodesIn, -CodesOut, -Links) is det. 44 | % 45 | % Markdown reference link definition parser. 46 | % Removes link definitions from the symbol code list. 47 | 48 | md_links(CodesIn, CodesOut, Links):- 49 | phrase(links_begin(TmpCodes, TmpLinks), CodesIn), 50 | CodesOut = TmpCodes, 51 | Links = TmpLinks. 52 | 53 | links_begin(Codes, [Link|Links]) --> 54 | link(Link), !, links(Codes, Links). 55 | 56 | links_begin(Codes, Links) --> 57 | links(Codes, Links). 58 | 59 | links([Code|Codes], Links) --> 60 | [Code], { \+code_type(Code, end_of_line) }, !, 61 | links(Codes, Links). 62 | 63 | links(Codes, [Link|Links]) --> 64 | ln_full, link(Link), !, links(Codes, Links). 65 | 66 | links([0'\n|Codes], Links) --> 67 | ln_full, !, links(Codes, Links). 68 | 69 | links([], []) --> eos, !. 70 | 71 | ln_full --> "\r\n", !. 72 | ln_full --> "\n", !. 73 | ln_full --> "\r". 74 | 75 | % Recognizes a reference link definition. 76 | % Example: [foo]: http://example.com/ "Optional Title Here" 77 | % Records the link but outputs nothing. 78 | 79 | link(link(Id, Url, Title)) --> 80 | link_indent, link_id(Id), 81 | whites, link_url(Url), 82 | whites, link_title(Title). 83 | 84 | % Link might be indented with 85 | % up to 3 spaces. More info: 86 | % http://daringfireball.net/projects/markdown/syntax#link 87 | 88 | link_indent --> " ". 89 | link_indent --> " ". 90 | link_indent --> " ". 91 | link_indent --> "". 92 | 93 | % Recognizes a link title. 94 | % When no title is found, Title is 95 | % an empty atom (''). 96 | 97 | link_title(Title) --> 98 | link_title_same_line(Title), !. 99 | 100 | link_title(Title) --> 101 | ln_full, whites, link_title_same_line(Title), !. 102 | 103 | link_title('') --> "". 104 | 105 | link_title_same_line(Title) --> 106 | "'", !, inline_string(Codes), "'", 107 | whites, lookahead_ln_or_eos, 108 | { atom_codes(Title, Codes) }. 109 | 110 | link_title_same_line(Title) --> 111 | "(", !, inline_string(Codes), ")", 112 | whites, lookahead_ln_or_eos, 113 | { atom_codes(Title, Codes) }. 114 | 115 | link_title_same_line(Title) --> 116 | "\"", inline_string(Codes), "\"", 117 | whites, lookahead_ln_or_eos, 118 | { atom_codes(Title, Codes) }. 119 | 120 | % Recognizes a link identifier. 121 | 122 | link_id(Id) --> 123 | "[", whites, inline_string(Codes), whites, "]:", 124 | { 125 | atom_codes(Tmp, Codes), 126 | downcase_atom(Tmp, Id) 127 | }. 128 | 129 | % Recognizes a link URL. 130 | 131 | link_url(Url) --> 132 | "<", !, inline_string(Codes), ">", 133 | { atom_codes(Url, Codes) }. 134 | 135 | link_url(Url) --> 136 | string_without([0'\n, 0'\t, 0' ], Codes), 137 | { atom_codes(Url, Codes) }. 138 | -------------------------------------------------------------------------------- /prolog/md/md_list_item.pl: -------------------------------------------------------------------------------- 1 | :- module(md_list_item, [ 2 | md_bullet_list_item//2, % -Codes 3 | md_ordered_list_item//2 % -Codes 4 | ]). 5 | 6 | /** List item parser 7 | 8 | Parser for items of bulleted and ordered lists. 9 | Separated into its own module for code clarity. 10 | */ 11 | 12 | :- use_module(library(dcg/basics)). 13 | :- use_module(md_line). 14 | :- use_module(md_hr). 15 | 16 | %! md_bullet_list_item(+Codes, -Mode)// is det. 17 | % 18 | % Recognizes a single bulleted list item. 19 | 20 | % Lookahead for horisontal ruler prevents 21 | % recognizing * * * as a list item. 22 | 23 | md_bullet_list_item(Codes, Mode) --> 24 | ( md_lookahead_hr 25 | -> { fail } 26 | ; bullet_start(Indent, _), whites, !, 27 | list_item_unintented(Indent, Codes, Mode)). 28 | 29 | %! md_ordered_list_item(-Codes, -Mode)// is det. 30 | % 31 | % Recognizes a single ordered list item. 32 | 33 | md_ordered_list_item(Codes, Mode) --> 34 | ordered_start(Indent, _), whites, !, 35 | list_item_unintented(Indent, Codes, Mode). 36 | 37 | % Bulleted-list item start. 38 | % Gives codes that make up the start. 39 | 40 | bullet_start(Indent, Codes) --> 41 | item_indent(Indent), 42 | list_bullet(Bullet), 43 | marker_follow(Follow), 44 | { flatten([Indent, Bullet, Follow], Codes) }. 45 | 46 | % Ordered-list item start. 47 | % Gives codes that make up the start. 48 | 49 | ordered_start(Indent, Codes) --> 50 | item_indent(Indent), 51 | one_or_more_digits(Number), ".", 52 | marker_follow(Follow), 53 | { flatten([Indent, Number, [0'.|Follow]], Codes) }. 54 | 55 | % Looks ahead an item start. 56 | % Used for detecting where the 57 | % previous list item ends. 58 | 59 | lookahead_item_start(Indent), Codes --> 60 | item_start(Indent, Codes). 61 | 62 | item_start(Indent, Codes) --> 63 | bullet_start(Indent, Codes), !. 64 | 65 | item_start(Indent, Codes) --> 66 | ordered_start(Indent, Codes). 67 | 68 | % List bullet might be indented 69 | % with up to 3 spaces. 70 | 71 | item_indent([0' ,0' ,0' ]) --> " ". 72 | item_indent([0' ,0' ]) --> " ". 73 | item_indent([0' ]) --> " ". 74 | item_indent([]) --> "". 75 | 76 | % List item marker must be followed 77 | % by a space or tab. 78 | 79 | marker_follow([0' ]) --> " ". 80 | marker_follow([0'\t]) --> "\t". 81 | 82 | % Sames as list_item_text but 83 | % removes possible indentation. 84 | 85 | list_item_unintented(Indent, Codes, Mode) --> 86 | list_item_text(Indent, Indented, Mode), !, 87 | { 88 | ( phrase(find_indent(BodyIndent), Indented, _) 89 | -> phrase(strip_indent(BodyIndent, Codes), Indented) 90 | ; Codes = Indented) 91 | }. 92 | 93 | % Recognizes list item body and mode. 94 | % Mode can be either normal or para. 95 | % This is implemented by recognizing 96 | % end conditions first. 97 | 98 | list_item_text(Indent, [], Mode) --> 99 | list_item_end(Indent, Mode), !. 100 | 101 | % Other cases, just consume input. 102 | 103 | list_item_text(Indent, [Code|Codes], Mode) --> 104 | [Code], list_item_text(Indent, Codes, Mode). 105 | 106 | % Recognizes list item end and 107 | % item mode. 108 | 109 | list_item_end(_, normal) --> 110 | eos. 111 | 112 | % Item and next item are separated 113 | % with an empty line. 114 | 115 | list_item_end(Indent, para) --> 116 | ln, empty_line, 117 | lookahead_item_start(StartIndent), 118 | { 119 | length(Indent, I), 120 | length(StartIndent, S), 121 | S =< I 122 | }. 123 | 124 | % No empty line before next item. 125 | 126 | list_item_end(Indent, normal) --> 127 | ln, lookahead_item_start(StartIndent), 128 | { 129 | length(Indent, I), 130 | length(StartIndent, S), 131 | S =< I 132 | }. 133 | 134 | % Next line is horisontal ruler. 135 | 136 | list_item_end(_, normal) --> 137 | ln, md_lookahead_hr. 138 | 139 | % Empty line and next line has 140 | % no indent. 141 | 142 | list_item_end(_, normal) --> 143 | ln, empty_line, lookahead_no_indent. 144 | 145 | % Looks ahead non-indented line begin. 146 | 147 | lookahead_no_indent --> 148 | lookahead_no_white. 149 | 150 | lookahead_no_white, [Code] --> 151 | [Code], { \+ code_type(Code, white) }. 152 | 153 | % Recognizes bulleted list item 154 | % token. 155 | 156 | list_bullet(0'*) --> "*". 157 | list_bullet(0'-) --> "-". 158 | list_bullet(0'+) --> "+". 159 | 160 | % Recognizes sequence of 161 | % one or more digits. Used for 162 | % recognizing ordered list items. 163 | 164 | one_or_more_digits([Digit]) --> 165 | digit(Digit). 166 | 167 | one_or_more_digits([Digit|Digits]) --> 168 | digit(Digit), 169 | one_or_more_digits(Digits). 170 | 171 | % Detects indent from the second 172 | % line. 173 | 174 | find_indent(Indent) --> 175 | non_empty_line(_), 176 | detect_indent(Indent). 177 | 178 | detect_indent([0'\t]) --> "\t". 179 | 180 | detect_indent([0' ,0' ,0' ,0' ]) --> " ". 181 | 182 | detect_indent([0' ,0' ,0' ]) --> " ". 183 | 184 | detect_indent([0' ,0' ]) --> " ". 185 | 186 | detect_indent([0' ]) --> " ". 187 | 188 | detect_indent([]) --> "". 189 | 190 | % Strips indent 191 | % from line beginnings. 192 | 193 | strip_indent(BodyIndent, Codes) --> 194 | strip_indent_begin(BodyIndent, Codes). 195 | 196 | strip_indent_begin(BodyIndent, Codes) --> 197 | strip_line_indent(BodyIndent), !, 198 | strip_rest_indent(BodyIndent, Codes). 199 | 200 | strip_indent_begin(BodyIndent, Codes) --> 201 | strip_rest_indent(BodyIndent, Codes). 202 | 203 | strip_rest_indent(BodyIndent, [0'\n|Codes]) --> 204 | ln, strip_line_indent(BodyIndent), !, 205 | strip_rest_indent(BodyIndent, Codes). 206 | 207 | strip_rest_indent(BodyIndent, [Code|Codes]) --> 208 | [Code], !, strip_rest_indent(BodyIndent, Codes). 209 | 210 | strip_rest_indent(_, []) --> 211 | eos. 212 | 213 | % Strip a tab when the target indent 214 | % was also a tab. 215 | 216 | strip_line_indent([0'\t]) --> "\t". 217 | 218 | % Strip a tab when the target indent 219 | % was 4 spaces. 220 | 221 | strip_line_indent([0' ,0' ,0' ,0' ]) --> "\t". 222 | 223 | % Strip 4 spaces when the target indent 224 | % was 4 spaces. 225 | 226 | strip_line_indent([0' ,0' ,0' ,0' ]) --> " ". 227 | 228 | % Strip 4 spaces when the target indent 229 | % was a tab. 230 | 231 | strip_line_indent([0'\t]) --> " ". 232 | 233 | % Strip 3 spaces when the target indent 234 | % was at least 3 spaces. 235 | 236 | strip_line_indent([0' ,0' ,0' |_]) --> " ". 237 | 238 | % Strip 2 spaces when the target indent 239 | % was 2 spaces. 240 | 241 | strip_line_indent([0' ,0' |_]) --> " ". 242 | 243 | % Strip a space when the target indent 244 | % was at least 1 space. 245 | 246 | strip_line_indent([0' |_]) --> " ". 247 | -------------------------------------------------------------------------------- /prolog/md/md_parse.pl: -------------------------------------------------------------------------------- 1 | :- module(md_parse, [ 2 | md_parse_codes/2, % +Codes, -Blocks 3 | md_parse_stream/2, % +Stream, -Blocks 4 | md_parse_file/2, % +File, -Blocks 5 | md_parse_string/2, % +String, -Blocks 6 | md_html_codes/2, % +Codes, -HtmlString 7 | md_html_stream/2, % +Stream, -HtmlString 8 | md_html_file/2, % +File, -HtmlString 9 | md_html_string/2 % +String, -HtmlString 10 | ]). 11 | 12 | /** Prolog Markdown parser 13 | 14 | Top-level module for parsing Markdown. Contains 15 | some convenience predicated. 16 | */ 17 | 18 | :- use_module(library(http/html_write)). 19 | :- use_module(library(readutil)). 20 | 21 | :- use_module(md_links). 22 | :- use_module(md_blocks). 23 | 24 | %! md_parse_string(+String, -Blocks) is det. 25 | % 26 | % Same as md_parse_codes/2 but takes 27 | % a string instead. 28 | 29 | md_parse_string(String, Blocks):- 30 | string_codes(String, Codes), 31 | md_parse_codes(Codes, Blocks). 32 | 33 | %! md_parse_codes(+Codes, -Blocks) is det. 34 | % 35 | % Parses Markdown into a structure suitable in use 36 | % with html//1. 37 | 38 | md_parse_codes(Codes, Blocks):- 39 | md_links(Codes, Tmp), 40 | phrase(md_blocks(Out), Tmp), !, 41 | Blocks = Out. 42 | 43 | %! md_parse_stream(+Stream, -Blocks) is det. 44 | % 45 | % Same as md_parse_codes/2 but reads input from stream. 46 | 47 | md_parse_stream(Stream, Blocks):- 48 | read_stream_to_codes(Stream, Codes), 49 | md_parse_codes(Codes, Blocks). 50 | 51 | %! md_parse_file(+Name, -Blocks) is det. 52 | % 53 | % Same as md_parse_codes/2 but reads input from file. 54 | 55 | md_parse_file(File, Blocks):- 56 | read_file_to_codes(File, Codes, []), 57 | md_parse_codes(Codes, Blocks). 58 | 59 | %! md_html_codes(+Codes, -Html) is det. 60 | % 61 | % Converts Markdown into HTML string. 62 | 63 | md_html_codes(Codes, Html):- 64 | md_parse_codes(Codes, Blocks), 65 | phrase(html(Blocks), Tokens), 66 | with_output_to(string(Html), print_html(Tokens)). 67 | 68 | %! md_html_string(+String, -Html) is det. 69 | % 70 | % Same as md_html_codes/2 but takes 71 | % input as string. 72 | 73 | md_html_string(String, Html):- 74 | string_codes(String, Codes), 75 | md_html_codes(Codes, Html). 76 | 77 | %! md_html_stream(+Stream, -Html) is det. 78 | % 79 | % Same as md_html_codes/2 but reads input from stream. 80 | 81 | md_html_stream(Stream, Html):- 82 | read_stream_to_codes(Stream, Codes), 83 | md_html_codes(Codes, Html). 84 | 85 | %! md_html_file(+Name, -Html) is det. 86 | % 87 | % Same as md_html_codes/2 but reads input from file. 88 | 89 | md_html_file(File, Html):- 90 | read_file_to_codes(File, Codes, []), 91 | md_html_codes(Codes, Html). 92 | -------------------------------------------------------------------------------- /prolog/md/md_span.pl: -------------------------------------------------------------------------------- 1 | :- module(md_span, [ 2 | md_span_codes/2, % +Codes, -HtmlTerms 3 | md_span_string/2 % +String, -HtmlTerms 4 | ]). 5 | 6 | /** Span-level Markdown parser 7 | 8 | Parses span-level Markdown elements: emphasis, 9 | inline-code, links and others. More info: 10 | http://daringfireball.net/projects/markdown/syntax#span 11 | */ 12 | 13 | :- use_module(library(dcg/basics)). 14 | :- use_module(library(apply)). 15 | 16 | :- use_module(md_trim). 17 | :- use_module(md_links). 18 | :- use_module(md_span_link). 19 | :- use_module(md_span_decorate). 20 | :- use_module(md_escape). 21 | :- use_module(md_line). 22 | 23 | %! md_span_string(+String, -HtmlTerms) is det. 24 | % 25 | % Same as md_span_codes/2 but uses a string 26 | % ans input. 27 | 28 | md_span_string(String, HtmlTerms):- 29 | string_codes(String, Codes), 30 | md_span_codes(Codes, HtmlTerms). 31 | 32 | %! md_span_codes(+Codes, -HtmlTerms) is det. 33 | % 34 | % Turns the list of codes into a structure acceptable 35 | % by SWI-Prolog's html//1 predicate. More info: 36 | % http://www.swi-prolog.org/pldoc/doc_for?object=html/1 37 | 38 | md_span_codes(Codes, HtmlTerms):- 39 | md_span_codes(Codes, [strong, em, code, del], HtmlTerms). 40 | 41 | md_span_codes(Codes, Allow, Out):- 42 | phrase(span(Spans, Allow), Codes), !, 43 | phrase(atomize(Out), Spans). 44 | 45 | % Optimized case for normal text. 46 | 47 | span([Code1,Code2|Spans], Allow) --> 48 | [Code1,Code2], 49 | { 50 | code_type(Code1, alnum), 51 | code_type(Code2, alnum), 52 | Code1 \= 0'h, 53 | Code2 \= 0't 54 | }, !, 55 | span(Spans, Allow). 56 | 57 | % Escape sequences. 58 | % More info: 59 | % http://daringfireball.net/projects/markdown/syntax#backslash 60 | % Processed first. 61 | 62 | span([Atom|Spans], Allow) --> 63 | "\\", [Code], 64 | { 65 | md_escaped_code(Code), 66 | atom_codes(Atom, [Code]) 67 | }, !, 68 | span(Spans, Allow). 69 | 70 | % Entities. These must be left alone. 71 | % More info: 72 | % http://daringfireball.net/projects/markdown/syntax#autoescape 73 | 74 | span([\[Atom]|Spans], Allow) --> 75 | "&", string_limit(Codes, 10), ";", 76 | { 77 | maplist(alnum, Codes), 78 | append([0'&|Codes], [0';], Entity), 79 | atom_codes(Atom, Entity) 80 | }, !, 81 | span(Spans, Allow). 82 | 83 | % Special characters & and <. 84 | % More info: 85 | % http://daringfireball.net/projects/markdown/syntax#autoescape 86 | 87 | span(['&'|Spans], Allow) --> 88 | "&", !, span(Spans, Allow). 89 | 90 | % As inline HTML is allowed, < is only escaped 91 | % when the following character is not a letter and / or 92 | % < appears at end of stream. 93 | 94 | span(['<'|Spans], Allow) --> 95 | "<", lookahead(Code), 96 | { 97 | \+ code_type(Code, alpha), 98 | Code \= 47 99 | }, !, 100 | span(Spans, Allow). 101 | 102 | span(['<'], _) --> 103 | "<", eos, !. 104 | 105 | % Line break with two or more spaces. 106 | % More info: 107 | % http://daringfireball.net/projects/markdown/syntax#p 108 | 109 | span([br([])|Spans], Allow) --> 110 | " ", whites, ln, !, 111 | span(Spans, Allow). 112 | 113 | % Recognizes links and images. 114 | 115 | span([Link|Spans], Allow) --> 116 | lookahead(Code), 117 | { 118 | % performance optimization 119 | ( Code = 0'[ 120 | ; Code = 0'! 121 | ; Code = 0'< 122 | ; Code = 0'h) 123 | }, 124 | md_span_link(Link), !, 125 | span(Spans, Allow). 126 | 127 | % Recognizes ", !, 132 | { 133 | string_codes(Content, Codes), 134 | atomics_to_string([''], String) 135 | }, 136 | span(Spans, Allow). 137 | 138 | % Prevent in-word underscores to trigger 139 | % emphasis. 140 | 141 | span([Code, 0'_, 0'_|Spans], Allow) --> 142 | [Code], "__", 143 | { code_type(Code, alnum) }, !, 144 | span(Spans, Allow). 145 | 146 | span([Code, 0'_|Spans], Allow) --> 147 | [Code], "_", 148 | { code_type(Code, alnum) }, !, 149 | span(Spans, Allow). 150 | 151 | % Recognizes text stylings like 152 | % strong, emphasis and inline code. 153 | 154 | span([Span|Spans], Allow) --> 155 | lookahead(Code), 156 | { 157 | % performance optimization 158 | ( Code = 0'` 159 | ; Code = 0'_ 160 | ; Code = 0'* 161 | ; Code = 0'~) 162 | }, 163 | md_span_decorate(Dec, Allow), !, 164 | { 165 | Dec =.. [Name, Codes], 166 | ( Name = code 167 | -> string_codes(Atom, Codes), 168 | Span =.. [Name, Atom] 169 | ; select(Name, Allow, AllowNest), 170 | md_span_codes(Codes, AllowNest, Nested), 171 | Span =.. [Name, Nested]) 172 | }, 173 | span(Spans, Allow). 174 | 175 | span([Code|Spans], Allow) --> 176 | [Code], !, 177 | span(Spans, Allow). 178 | 179 | span([], _) --> 180 | eos. 181 | 182 | % Collects remaining codes into atoms suitable 183 | % for SWI-s html//1. 184 | % Atoms will appear as \[text] as they can contain 185 | % raw HTML which must not be escaped. 186 | 187 | atomize([]) --> 188 | eos, !. 189 | 190 | atomize([\[Atom]|Tokens]) --> 191 | [Num], { number(Num) }, !, 192 | text_codes(Codes), 193 | { string_codes(Atom, [Num|Codes]) }, 194 | atomize(Tokens). 195 | 196 | atomize([Token|Tokens]) --> 197 | [Token], atomize(Tokens). 198 | 199 | text_codes([Code|Codes]) --> 200 | [Code], { number(Code) }, !, 201 | text_codes(Codes). 202 | 203 | text_codes([]) --> "". 204 | 205 | % Recognizes single symbol code of 206 | % type alnum. 207 | 208 | alnum(Code):- 209 | code_type(Code, alnum). 210 | -------------------------------------------------------------------------------- /prolog/md/md_span_decorate.pl: -------------------------------------------------------------------------------- 1 | :- module(md_span_decorate, [ 2 | md_span_decorate//2 % -Span 3 | ]). 4 | 5 | /** Parser for span-level styles 6 | 7 | Predicates for recognizing span-level 8 | style formatting like strong, emphasis and 9 | code. 10 | */ 11 | 12 | :- use_module(library(dcg/basics)). 13 | :- use_module(md_trim). 14 | 15 | %! md_span_decorate(-Span, +Allow)// is det. 16 | % 17 | % Recognizes style formatting 18 | % in the middle of span text. 19 | % Span is a term `functor(Codes)` 20 | % where the functor is one of: 21 | % `strong`, `em` or `code`. 22 | % Allow is a list of allowed 23 | % span elements. May contain 24 | % `strong`, `em`, `del` and 25 | % `code`. 26 | 27 | md_span_decorate(Span, Allow) --> 28 | { memberchk(strong, Allow) }, 29 | star_strong(Span), !. 30 | 31 | md_span_decorate(Span, Allow) --> 32 | { memberchk(strong, Allow) }, 33 | underscore_strong(Span), !. 34 | 35 | md_span_decorate(Span, Allow) --> 36 | { memberchk(em, Allow) }, 37 | star_emphasis(Span), !. 38 | 39 | md_span_decorate(Span, Allow) --> 40 | { memberchk(em, Allow) }, 41 | underscore_emphasis(Span), !. 42 | 43 | md_span_decorate(Span, Allow) --> 44 | { memberchk(code, Allow) }, 45 | code(Span). 46 | 47 | md_span_decorate(Span, Allow) --> 48 | { memberchk(del, Allow) }, 49 | strikethrough(Span). 50 | 51 | % Recognizes strikethrough ~~something~~. 52 | 53 | strikethrough(del(Codes)) --> 54 | "~~", string(Codes), "~~". 55 | 56 | % Recognizes strong **something**. 57 | 58 | star_strong(strong(Codes)) --> 59 | "**", string(Codes), "**". 60 | 61 | % Recognizes strong __something__. 62 | 63 | underscore_strong(strong(Codes)) --> 64 | "__", string(Codes), "__". 65 | 66 | % Recognizes emhasis *something*. 67 | % The first character following * must be a non-space. 68 | 69 | star_emphasis(em([Code|Codes])) --> 70 | "*", nonblank(Code), string(Codes), "*". 71 | 72 | % Recognizes emphasis _something_. 73 | % The first character following _ must be a non-space. 74 | 75 | underscore_emphasis(em([Code|Codes])) --> 76 | "_", nonblank(Code), string(Codes), "_". 77 | 78 | % Recognizes inline code ``code``. 79 | 80 | code(code(Trimmed)) --> 81 | "``", string(Raw), "``", 82 | { trim(Raw, Trimmed) }. 83 | 84 | % Recognizes inline code `code`. 85 | 86 | code(code(Trimmed)) --> 87 | "`", string(Raw), "`", 88 | { trim(Raw, Trimmed) }. 89 | -------------------------------------------------------------------------------- /prolog/md/md_span_link.pl: -------------------------------------------------------------------------------- 1 | :- module(md_span_link, [ 2 | md_span_link//1 % -Html 3 | ]). 4 | 5 | /** Markdown span-level link parsing 6 | 7 | Parses Markdown span-level links and images. Separated 8 | from the `md_span` module for code clarity. 9 | */ 10 | 11 | :- use_module(library(dcg/basics)). 12 | 13 | :- use_module(md_links). 14 | :- use_module(md_line). 15 | 16 | %! md_span_link(-Link)// is det. 17 | % 18 | % Recognizes different types of 19 | % links from the stream of symbol codes. 20 | 21 | md_span_link(Link) --> 22 | plain_http_link(Link), !. 23 | 24 | md_span_link(Link) --> 25 | angular_link(Link), !. 26 | 27 | md_span_link(Link) --> 28 | angular_mail_link(Link), !. 29 | 30 | md_span_link(Link) --> 31 | normal_link(Link), !. 32 | 33 | md_span_link(Link) --> 34 | reference_link(Link), !. 35 | 36 | md_span_link(Image) --> 37 | reference_image(Image), !. 38 | 39 | md_span_link(Image) --> 40 | image(Image). 41 | 42 | % Recognizes a plain link. 43 | 44 | plain_http_link(Link) --> 45 | http_prefix(Prefix), inline_string(Codes), link_end, 46 | { 47 | atom_codes(Atom, Codes), 48 | atom_concat(Prefix, Atom, Url), 49 | link(Url, '', Url, Link) 50 | }. 51 | 52 | link_end --> 53 | eos, !. 54 | 55 | link_end --> 56 | lookahead(Code), 57 | { code_type(Code, space) }. 58 | 59 | http_prefix('http://') --> 60 | "http://". 61 | 62 | http_prefix('https://') --> 63 | "https://". 64 | 65 | % Recognizes inline automatic http(s) . 66 | 67 | angular_link(Link) --> 68 | "<", http_prefix(Prefix), 69 | inline_string(Codes), ">", 70 | { 71 | atom_codes(Atom, Codes), 72 | atom_concat(Prefix, Atom, Url), 73 | link(Url, '', Url, Link) 74 | }. 75 | 76 | % Recognizes inline mail link 77 | 78 | angular_mail_link(Link) --> 79 | "<", inline_string(User), 80 | "@", inline_string(Host), ">", 81 | { 82 | append(User, [0'@|Host], Codes), 83 | atom_codes(Address, Codes), 84 | mail_link(Address, Link) 85 | }. 86 | 87 | % Recognizes a normal Markdown link. 88 | % Example: [an example](http://example.com/ "Title") 89 | % More info: 90 | % http://daringfireball.net/projects/markdown/syntax#link 91 | 92 | normal_link(Link) --> 93 | label(Label), 94 | url_title(Url, Title), 95 | { link(Url, Title, Label, Link) }. 96 | 97 | % Recognizes image ![Alt text](/path/to/img.jpg). 98 | % With optional title: ![Alt text](/path/to/img.jpg "Optional title"). 99 | % More info: http://daringfireball.net/projects/markdown/syntax#img 100 | 101 | image(Image) --> 102 | "!", label(Alt), 103 | url_title(Url, Title), 104 | { 105 | ( Title = '' 106 | -> Image = img([src=Url, alt=Alt]) 107 | ; Image = img([src=Url, alt=Alt, title=Title])) 108 | }. 109 | 110 | % Recognizes a reference link. 111 | % Example: This is [an example][id] reference-style link. 112 | % Fails when the reference is not defined. Identifier 113 | % can be empty, then the lowercase label is used as the 114 | % identifier. 115 | 116 | reference_link(Link) --> 117 | label(Label), 118 | whites, identifier(Id), 119 | { 120 | ( Id = '' 121 | -> downcase_atom(Label, RealId) 122 | ; RealId = Id), 123 | md_link(RealId, Url, Title), 124 | link(Url, Title, Label, Link) 125 | }. 126 | 127 | % Recognizes a reference-linked image. 128 | % Example: ![Alt text][id] 129 | 130 | reference_image(Image) --> 131 | "!", label(Label), 132 | whites, identifier(Id), 133 | { 134 | ( Id = '' 135 | -> downcase_atom(Label, RealId) 136 | ; RealId = Id), 137 | md_link(RealId, Url, Title), 138 | ( Title = '' 139 | -> Image = img([src=Url, alt=Label]) 140 | ; Image = img([src=Url, alt=Label, title=Title])) 141 | }. 142 | 143 | % Same as label block but the result 144 | % is lowercased. 145 | 146 | identifier(Id) --> 147 | label(Label), 148 | { downcase_atom(Label, Id) }. 149 | 150 | % Recognizes a label block. 151 | % Example: [This is a link]. 152 | 153 | label(Label) --> 154 | "[", whites, inline_string(Codes), whites, "]", !, 155 | { atom_codes(Label, Codes) }. 156 | 157 | % Recognizes a normal link URL/title 158 | % block. Example: (http://example.com "Title"). 159 | 160 | url_title(Url, Title) --> 161 | "(", inline_string(UrlCodes), 162 | whites, "\"", inline_string(TitleCodes), "\"", whites, ")", !, 163 | { 164 | atom_codes(Url, UrlCodes), 165 | atom_codes(Title, TitleCodes) 166 | }. 167 | 168 | url_title(Url, Title) --> 169 | "(", inline_string(UrlCodes), 170 | whites, "'", inline_string(TitleCodes), "'", whites, ")", !, 171 | { 172 | atom_codes(Url, UrlCodes), 173 | atom_codes(Title, TitleCodes) 174 | }. 175 | 176 | url_title(Url, '') --> 177 | "(", inline_string(Codes), ")", !, 178 | { atom_codes(Url, Codes) }. 179 | 180 | % Creates an HTML link element. Has no title 181 | % attribute when title is empty. 182 | 183 | link(Url, '', Label, Element):- !, 184 | Element = a([href=Url], Label). 185 | 186 | link(Url, Title, Label, Element):- 187 | Element = a([href=Url, title=Title], Label). 188 | 189 | % Creates an HTML link element for an email address. 190 | % Does not try to mangle the address as the original 191 | % spec. XXX maybe should do that? 192 | 193 | mail_link(Address, Element):- 194 | atom_concat('mailto:', Address, Href), 195 | Element = a([href=Href], Address). 196 | -------------------------------------------------------------------------------- /prolog/md/md_trim.pl: -------------------------------------------------------------------------------- 1 | :- module(md_trim, [ 2 | trim_left/2, % +Codes, -Result 3 | trim_right/2, % +Codes, -Result 4 | trim/2 % +Codes, -Result 5 | ]). 6 | 7 | /** Code list whitespace trimming 8 | 9 | Helper module to trim whitespaces from 10 | lists of codes. 11 | */ 12 | 13 | %! trim_left(+Codes, -Result) is det. 14 | % 15 | % Trims whitespaces from the beginning of 16 | % the list of codes. 17 | 18 | trim_left([Code|Codes], Result):- 19 | code_type(Code, space), !, 20 | trim_left(Codes, Result). 21 | 22 | trim_left(Codes, Codes). 23 | 24 | %! trim_right(+Codes, -Result) is det. 25 | % 26 | % Trims whitespace from the end of the 27 | % list of codes. 28 | 29 | trim_right(Codes, Result):- 30 | reverse(Codes, CodesR), 31 | trim_left(CodesR, ResultR), 32 | reverse(ResultR, Result). 33 | 34 | %! trim(+Codes, -Result) is det. 35 | % 36 | % Trims whitespace from both sides of the 37 | % list of codes. 38 | 39 | trim(Codes, Result):- 40 | trim_left(Codes, Tmp1), 41 | reverse(Tmp1, Tmp2), 42 | trim_left(Tmp2, Tmp3), 43 | reverse(Tmp3, Result). 44 | -------------------------------------------------------------------------------- /tests/block.pl: -------------------------------------------------------------------------------- 1 | :- begin_tests(md_block). 2 | :- use_module(prolog/md/md_parse). 3 | 4 | % Tests for block-level parser. 5 | 6 | % Setext-styled first-level heading. 7 | 8 | test(heading_1):- 9 | md_parse_string("abc\n===", [h1("abc")]). 10 | 11 | % Setext-styled second-level heading. 12 | 13 | test(heading_2):- 14 | md_parse_string("abc\n---", [h2("abc")]). 15 | 16 | % Atx-styled first-level heading. 17 | 18 | test(heading_3):- 19 | md_parse_string("# abc", [h1("abc")]). 20 | 21 | % Atx-styled second-level heading. 22 | 23 | test(heading_4):- 24 | md_parse_string("## abc", [h2("abc")]). 25 | 26 | % Atx-styled first-level heading. # at end. 27 | 28 | test(heading_5):- 29 | md_parse_string("# abc #", [h1("abc")]). 30 | 31 | % Setext-styled first-level heading following a paragraph. 32 | % With empty line. 33 | 34 | test(heading_6):- 35 | md_parse_string("para\n\nabc\n===", [p([\["para"]]), h1("abc")]). 36 | 37 | % Setext-styled first-level heading following a paragraph. 38 | % Without empty line. 39 | 40 | test(heading_7):- 41 | md_parse_string("para\nabc\n===", [p([\["para"]]), h1("abc")]). 42 | 43 | % Setext-styled first-level heading following a list. 44 | % With empty line. 45 | 46 | test(heading_8):- 47 | md_parse_string("* item\n\nabc\n===", [ul([li([\["item"]])]), h1("abc")]). 48 | 49 | % Setext-styled first-level heading following a list. 50 | % Without empty line. 51 | 52 | test(heading_9):- 53 | md_parse_string("* item\nabc\n===", [ul([li([p([\["item"]]), h1("abc")])])]). 54 | 55 | % Setext-styled first-level heading following a blockquote. 56 | % With empty line. 57 | 58 | test(heading_10):- 59 | md_parse_string("> bq\n\nabc\n===", [blockquote([\["bq"]]), h1("abc")]). 60 | 61 | % Setext-styled first-level heading following a blockquote. 62 | % Without empty line. 63 | 64 | test(heading_11):- 65 | md_parse_string("> bq\nabc\n===", [blockquote([p([\["bq"]]), h1("abc")])]). 66 | 67 | % Setext-styled first-level heading following a code block. 68 | % With empty line. 69 | 70 | test(heading_12):- 71 | md_parse_string("\ta+b\n\nabc\n===", [pre(code("a+b")), h1("abc")]). 72 | 73 | % Setext-styled first-level heading following a code block. 74 | % Without empty line. 75 | 76 | test(heading_13):- 77 | md_parse_string("\ta+b\nabc\n===", [pre(code("a+b")), h1("abc")]). 78 | 79 | % Setext-styled first-level heading following an HTML block. 80 | % With empty line. 81 | 82 | test(heading_14):- 83 | md_parse_string("
    html
    \n\nabc\n===", [\["
    html
    "], h1("abc")]). 84 | 85 | % Setext-styled first-level heading following a code block. 86 | % Without empty line. Merges header with html block. 87 | % XXX diverges from dingus. 88 | 89 | test(heading_15):- 90 | md_parse_string("
    html
    \nabc\n===", [\["
    html
    \nabc\n==="]]). 91 | 92 | % Single line blockquote. 93 | 94 | test(blockquote_1):- 95 | md_parse_string("> abc", [blockquote([\["abc"]])]). 96 | 97 | % Multiline blockquote. 98 | 99 | test(blockquote_2):- 100 | md_parse_string("> abc\n> def", [blockquote([\["abc\ndef"]])]). 101 | 102 | % Nested blockquote, 103 | 104 | test(blockquote_3):- 105 | md_parse_string("> abc\n>\n> def", [blockquote([p([\["abc"]]), p([\["def"]])])]). 106 | 107 | % Nested blockquote, single line. 108 | 109 | test(blockquote_4):- 110 | md_parse_string("> > abc", [blockquote([blockquote([\["abc"]])])]). 111 | 112 | % Blockquote following a paragraph. 113 | % With empty line. 114 | 115 | test(blockquote_5):- 116 | md_parse_string("para\n\n> abc", [p([\["para"]]), blockquote([\["abc"]])]). 117 | 118 | % Blockquote following a paragraph. 119 | % No empty line. 120 | 121 | test(blockquote_6):- 122 | md_parse_string("para\n> abc", [p([\["para"]]), blockquote([\["abc"]])]). 123 | 124 | % Simple paragraph. 125 | 126 | test(paragraph_1):- 127 | md_parse_string("abc", [p([\["abc"]])]). 128 | 129 | % Paragraph with two lines. 130 | 131 | test(paragraph_2):- 132 | md_parse_string("abc\ndef", [p([\["abc\ndef"]])]). 133 | 134 | % Two paragraphs. 135 | 136 | test(paragraph_3):- 137 | md_parse_string("abc\n\ndef", [p([\["abc"]]), p([\["def"]])]). 138 | 139 | % Simple list. 140 | 141 | test(list_1):- 142 | md_parse_string("+ a", [ul([ 143 | li([\["a"]]) 144 | ])]). 145 | 146 | % List with two items. 147 | 148 | test(list_2):- 149 | md_parse_string("+ a\n+ b", [ul([ 150 | li([\["a"]]), 151 | li([\["b"]]) 152 | ])]). 153 | 154 | % List with two items. Paragraph mode. 155 | 156 | test(list_3):- 157 | md_parse_string("+ a\n\n+ b", [ul([ 158 | li([p([\["a"]])]), 159 | li([p([\["b"]])]) 160 | ])]). 161 | 162 | % Multiline list item. 163 | 164 | test(list_4):- 165 | md_parse_string("+ a\n b", [ul([ 166 | li([\["a\nb"]]) 167 | ])]). 168 | 169 | % List with sublist. 170 | 171 | test(list_5):- 172 | md_parse_string("+ a\n - b", [ul([ 173 | li([ 174 | \["a"], 175 | ul([li([\["b"]])]) 176 | ]) 177 | ])]). 178 | 179 | % List with 3 items. 180 | 181 | test(list_6):- 182 | md_parse_string("+ a\n+ b\n+ c", [ul([ 183 | li([\["a"]]), 184 | li([\["b"]]), 185 | li([\["c"]]) 186 | ])]). 187 | 188 | % List with sublist and item after it. 189 | 190 | test(list_7):- 191 | md_parse_string("+ a\n + b\n+ c", [ul([ 192 | li([\["a"], ul([ 193 | li([\["b"]]) 194 | ])]), 195 | li([\["c"]]) 196 | ])]). 197 | 198 | % List following a paragraph. 199 | % With empty line. 200 | 201 | test(list_8):- 202 | md_parse_string("para\n\n+ a", [p([\["para"]]), ul([li([\["a"]])])]). 203 | 204 | % List following a paragraph. 205 | % Without empty line. 206 | 207 | test(list_9):- 208 | md_parse_string("para\n+ a", [p([\["para"]]), ul([li([\["a"]])])]). 209 | 210 | % Code block in list item. 211 | 212 | test(list_10):- 213 | md_parse_string("+ a\n code", [ul([li([p([\["a"]]), pre(code("code"))])])]). 214 | 215 | % Simple code block. 216 | 217 | test(code):- 218 | md_parse_string(" abc", [pre(code("abc"))]). 219 | 220 | % Code block with tabs. 221 | 222 | test(code_tabs):- 223 | md_parse_string("\tabc", [pre(code("abc"))]). 224 | 225 | % Multiline code block. 226 | 227 | test(code_multiline):- 228 | md_parse_string(" abc\n def", [pre(code("abc\ndef"))]). 229 | 230 | % Code block with an empty line. 231 | 232 | test(code_empty):- 233 | md_parse_string(" abc\n\n def", [pre(code("abc\n\ndef"))]). 234 | 235 | % Code block with tabs. 236 | 237 | test(code_4):- 238 | md_parse_string("\tabc\n\tdef", [pre(code("abc\ndef"))]). 239 | 240 | % Simple horisontal ruler. 241 | 242 | test(horisontal_rule_1):- 243 | md_parse_string("***", [hr([])]). 244 | 245 | % Simple horisontal ruler, dashes. 246 | 247 | test(horisontal_rule_2):- 248 | md_parse_string("---", [hr([])]). 249 | 250 | % Simple horisontal ruler, spaces between stars. 251 | 252 | test(horisontal_rule_3):- 253 | md_parse_string("* * *", [hr([])]). 254 | 255 | % An HTML block. 256 | 257 | test(block):- 258 | md_parse_string("
    abc
    ", [\["
    abc
    "]]). 259 | 260 | % Fenced code block, no language. 261 | 262 | test(fenced_code_no_language):- 263 | md_parse_string("```\nabc\n```", [pre(code("abc"))]). 264 | 265 | % Fenced code block, with language. 266 | 267 | test(fenced_code):- 268 | md_parse_string("```prolog\nabc\n```", [pre(code(['data-language'=prolog], "abc"))]). 269 | 270 | test(hr_after_list_1):- 271 | md_parse_string("* abc\n* * *\nrest", [ul([li([\["abc"]])]), hr([]), p([\["rest"]])]). 272 | 273 | % Horizontal ruler deeply nested. 274 | % XXX hr is not placed inside list item. 275 | 276 | test(hr_after_list_2):- 277 | md_parse_string("* abc\n * * *", [ul([li([\["abc"]])]), hr([])]). 278 | 279 | :- end_tests(md_block). 280 | -------------------------------------------------------------------------------- /tests/file.md: -------------------------------------------------------------------------------- 1 | # Test file 2 | 3 | > Blockquotes are used 4 | for testing different aspects as they can contain 5 | nested Markdown. 6 | 7 | ## Headings 8 | 9 | > With line 10 | > ========= 11 | > Second level 12 | > ------------ 13 | > ### Hashes at end ### 14 | 15 | ## Lists 16 | 17 | + item1 18 | + item2 19 | + item3 20 | 21 | ### Nested list 22 | 23 | * a 24 | + 1 25 | + 2 26 | * b 27 | + 3 28 | 29 | A paragraph. 30 | 31 | // code block 32 | p1 :- p2, p3. 33 | 34 |
    35 |
    HTML
    36 |
    37 | -------------------------------------------------------------------------------- /tests/file.pl: -------------------------------------------------------------------------------- 1 | :- begin_tests(md_file). 2 | :- use_module(prolog/md/md_parse). 3 | 4 | test(file):- 5 | md_parse_file('tests/file.md', Blocks), 6 | assertion(nth0(0, Blocks, h1("Test file"))), 7 | assertion(nth0(1, Blocks, blockquote([\[_]]))), 8 | assertion(nth0(2, Blocks, h2("Headings"))), 9 | assertion(nth0(3, Blocks, blockquote(Headings))), 10 | assertion(nth0(0, Headings, h1("With line"))), 11 | assertion(nth0(1, Headings, h2("Second level"))), 12 | assertion(nth0(3, Headings, h3("Hashed at end"))), 13 | assertion(nth0(4, Blocks, h2("Lists"))), 14 | assertion(nth0(5, Blocks, ul(Items))), 15 | assertion(nth0(0, Items, li(\["item1"]))), 16 | assertion(nth0(1, Items, li(\["item2"]))), 17 | assertion(nth0(2, Items, li(\["item3"]))), 18 | assertion(nth0(6, Blocks, h3("Nested list"))), 19 | assertion(nth0(7, Blocks, ul(NestItems))), 20 | assertion(NestItems = [li([ul(_)]), li(ul(_))]), 21 | assertion(nth0(8, Blocks, p([\["A paragraph."]]))), 22 | assertion(nth0(9, Blocks, pre(code("// code block\np1 :- p2, p3.")))), 23 | assertion(nth0(10, Blocks, \["
    \n
    HTML
    \n
    "])). 24 | 25 | :- end_tests(md_file). -------------------------------------------------------------------------------- /tests/header.pl: -------------------------------------------------------------------------------- 1 | :- begin_tests(md_header). 2 | :- use_module(prolog/md/md_header). 3 | 4 | % Tests for different headers. 5 | 6 | md_header_string(String, Header, Rest):- 7 | string_codes(String, Codes), 8 | phrase(md_header(Tmp), Codes, RestCodes), !, 9 | string_codes(Rest, RestCodes), 10 | Tmp = Header. 11 | 12 | % Setext-styled first-level header. 13 | 14 | test(header_1):- 15 | md_header_string("abc\n===\nrest", h1("abc"), "rest"). 16 | 17 | % Setext-styled second-level header. 18 | 19 | test(header_2):- 20 | md_header_string("abc\n---\nrest", h2("abc"), "rest"). 21 | 22 | % Atx-styled level 1. 23 | 24 | test(header_3):- 25 | md_header_string("# abc\nrest", h1("abc"), "rest"). 26 | 27 | % Atx-styled level 2. 28 | 29 | test(header_4):- 30 | md_header_string("## abc\nrest", h2("abc"), "rest"). 31 | 32 | % Atx-styled level 3. 33 | 34 | test(header_5):- 35 | md_header_string("### abc\nrest", h3("abc"), "rest"). 36 | 37 | % Atx-styled level 4. 38 | 39 | test(header_6):- 40 | md_header_string("#### abc\nrest", h4("abc"), "rest"). 41 | 42 | % Atx-styled level 5. 43 | 44 | test(header_7):- 45 | md_header_string("##### abc\nrest", h5("abc"), "rest"). 46 | 47 | % Atx-styled level 6. 48 | 49 | test(header_8):- 50 | md_header_string("###### abc\nrest", h6("abc"), "rest"). 51 | 52 | % Atx-styled, # at end. 53 | 54 | test(header_9):- 55 | md_header_string("# abc#\nrest", h1("abc"), "rest"). 56 | 57 | % Atx-styled, multiple # at end, space. 58 | 59 | test(header_10):- 60 | md_header_string("# abc ##\nrest", h1("abc"), "rest"). 61 | 62 | % Atx-styled, escaped #. 63 | 64 | test(header_11):- 65 | md_header_string("# abc\\#def\nrest", h1("abc#def"), "rest"). 66 | 67 | % Atx-styled, escaped #, one # at end. 68 | 69 | test(header_12):- 70 | md_header_string("# abc\\#def #\nrest", h1("abc#def"), "rest"). 71 | 72 | :- end_tests(md_header). 73 | -------------------------------------------------------------------------------- /tests/html.pl: -------------------------------------------------------------------------------- 1 | :- begin_tests(md_html). 2 | 3 | % Integration tests with SWI-Prolog's html_write 4 | % module. 5 | 6 | :- use_module(library(http/html_write)). 7 | :- use_module(library(apply)). 8 | 9 | :- use_module(prolog/md/md_span). 10 | :- use_module(prolog/md/md_parse). 11 | 12 | % Helper to turn Markdown span elements 13 | % into HTML code (atom). 14 | 15 | span_html(String, Html):- 16 | string_codes(String, Codes), 17 | md_span_codes(Codes, Spans), 18 | phrase(html(Spans), Tokens), 19 | strip_layout(Tokens, Clean), 20 | with_output_to(atom(Html), print_html(Clean)). 21 | 22 | % Helper to turn Markdown into HTML code (atom). 23 | 24 | html(String, Html):- 25 | md_parse_string(String, Blocks), 26 | phrase(html(Blocks), Tokens, []), 27 | strip_layout(Tokens, Clean), 28 | with_output_to(atom(Html), print_html(Clean)). 29 | 30 | strip_layout(In, Out):- 31 | exclude(layout, In, Out). 32 | 33 | layout(nl(_)). 34 | layout(mailbox(_, _)). 35 | 36 | test(special_amp):- 37 | span_html("AT&T", 'AT&T'). 38 | 39 | test(special_amp_noescape):- 40 | span_html("AT&T", 'AT&T'). 41 | 42 | test(special_lt):- 43 | span_html("1<2", '1<2'). 44 | 45 | test(strong):- 46 | span_html("**abc**", 'abc'). 47 | 48 | test(emphasis):- 49 | span_html("*abc*", 'abc'). 50 | 51 | test(link):- 52 | span_html("[label](http://google.com)", 'label'). 53 | 54 | test(link_entities):- 55 | span_html("[label](http://google.com?q=abc&s=def)", 'label'). 56 | 57 | test(code_2):- 58 | span_html("``abc``", 'abc'). 59 | 60 | test(code_2_escape):- 61 | span_html("````", '<blink>'). 62 | 63 | test(strong_emb_html):- 64 | span_html("**abcanchor**", 'abcanchor'). 65 | 66 | test(emphasis_escape):- 67 | span_html("\\*abc\\*", '*abc*'). 68 | 69 | test(paragraph):- 70 | html("abc", '

    abc

    '). 71 | 72 | test(two_paragraphs):- 73 | html("abc\n\ndef", '

    abc

    def

    '). 74 | 75 | test(html_block):- 76 | html("
    *abc*
    ", '
    *abc*
    '). 77 | 78 | test(span_paragraph):- 79 | html("abc **def** ghi", '

    abc def ghi

    '). 80 | 81 | test(heading1):- 82 | html("Hello\n=====", '

    Hello

    '). 83 | 84 | test(code_block):- 85 | html("\ta = 1;\n\tb = 2;\n\tc = a + b;", '
    a = 1;\nb = 2;\nc = a + b;
    '). 86 | 87 | test(code_block_escape):- 88 | html("\ta = 1;\n\tb = 2;\n\tc = a < b;", '
    a = 1;\nb = 2;\nc = a < b;
    '). 89 | 90 | test(script_span):- 91 | html("abc def", '

    abc def

    '). 92 | 93 | :- end_tests(md_html). -------------------------------------------------------------------------------- /tests/link.pl: -------------------------------------------------------------------------------- 1 | :- begin_tests(md_links). 2 | :- use_module(prolog/md/md_links). 3 | 4 | % Tests for reference link parsing. 5 | 6 | md_links_strings(In, Out, Links):- 7 | string_codes(In, InCodes), 8 | md_links(InCodes, OutCodes, Links), 9 | string_codes(Out, OutCodes). 10 | 11 | % No links. 12 | 13 | test(link_1):- 14 | md_links_strings("abc", "abc", []). 15 | 16 | % Link with title, double quotes. 17 | 18 | test(link_2):- 19 | md_links_strings("[id]: http://example.com \"Title\"", "", 20 | [link(id, 'http://example.com', 'Title')]). 21 | 22 | % Link with title, single quotes. 23 | 24 | test(link_3):- 25 | md_links_strings("[id]: http://example.com 'Title'", "", 26 | [link(id, 'http://example.com', 'Title')]). 27 | 28 | % Link with title, parentheses. 29 | 30 | test(link_4):- 31 | md_links_strings("[id]: http://example.com (Title)", "", 32 | [link(id, 'http://example.com', 'Title')]). 33 | 34 | % Link with title, on the next line. 35 | 36 | test(link_5):- 37 | md_links_strings("[id]: http://example.com\n 'Title'", "", 38 | [link(id, 'http://example.com', 'Title')]). 39 | 40 | % Some text before link. 41 | 42 | test(link_6):- 43 | md_links_strings("abc\n[id]: http://example.com 'Title'", "abc", 44 | [link(id, 'http://example.com', 'Title')]). 45 | 46 | % Some text before and after the link. 47 | 48 | test(link_7):- 49 | md_links_strings("abc\n[id]: http://example.com 'Title'\ndef", "abc\ndef", 50 | [link(id, 'http://example.com', 'Title')]). 51 | 52 | % Spaces around the identifier. 53 | 54 | test(link_8):- 55 | md_links_strings("[ id ]: http://example.com 'Title'", "", 56 | [link(id, 'http://example.com', 'Title')]). 57 | 58 | % Spaces before the identifier token. 59 | 60 | test(link_9):- 61 | md_links_strings(" [id]: http://example.com 'Title'", "", 62 | [link(id, 'http://example.com', 'Title')]). 63 | 64 | % URL inside < and >. 65 | 66 | test(link_10):- 67 | md_links_strings("[id]: 'Title'", "", 68 | [link(id, 'http://example.com/ a', 'Title')]). 69 | 70 | % Identifier must be lowercased. 71 | 72 | test(link_11):- 73 | md_links_strings("[ID]: http://example.com 'Title'", "", 74 | [link(id, 'http://example.com', 'Title')]). 75 | 76 | % No title. 77 | 78 | test(link_12):- 79 | md_links_strings("[id]: http://example.com", "", 80 | [link(id, 'http://example.com', '')]). 81 | 82 | % No title, text on the next line. 83 | 84 | test(link_13):- 85 | md_links_strings("[id]: http://example.com\nabc", "\nabc", 86 | [link(id, 'http://example.com', '')]). 87 | 88 | % Title delimiter in link title. 89 | 90 | test(link_14):- 91 | md_links_strings("[id]: http://example.com 'Title' 1'", "", 92 | [link(id, 'http://example.com', 'Title\' 1')]). 93 | 94 | % Link indented deeper than 3 spaces is ignored. 95 | 96 | test(link_15):- 97 | md_links_strings(" [id]: http://example.com 'Title'", 98 | " [id]: http://example.com 'Title'", []). 99 | 100 | % Line ends replaced with canonical line ends. 101 | 102 | test(link_16):- 103 | md_links_strings("\r\n\r", "\n\n", []). 104 | 105 | :- end_tests(md_links). 106 | -------------------------------------------------------------------------------- /tests/list_item.pl: -------------------------------------------------------------------------------- 1 | :- begin_tests(md_list_item). 2 | :- use_module(prolog/md/md_list_item). 3 | 4 | % Tests for span-level links. 5 | 6 | md_bullet_list_item_string(String, Item, Mode, Rest):- 7 | string_codes(String, Codes), 8 | phrase(md_bullet_list_item(ItemCodes, ModeTmp), Codes, RestCodes), !, 9 | string_codes(Rest, RestCodes), 10 | string_codes(Item, ItemCodes), 11 | Mode = ModeTmp. 12 | 13 | md_ordered_list_item_string(String, Item, Mode, Rest):- 14 | string_codes(String, Codes), 15 | phrase(md_ordered_list_item(ItemCodes, ModeTmp), Codes, RestCodes), !, 16 | string_codes(Rest, RestCodes), 17 | string_codes(Item, ItemCodes), 18 | Mode = ModeTmp. 19 | 20 | % Bullet list item. Star bullet. 21 | 22 | test(list_item_1):- 23 | md_bullet_list_item_string("* abc", "abc", normal, ""). 24 | 25 | % Bullet list item. Plus bullet. 26 | 27 | test(list_item_2):- 28 | md_bullet_list_item_string("+ abc", "abc", normal, ""). 29 | 30 | % Bullet list item. Minus bullet. 31 | 32 | test(list_item_3):- 33 | md_bullet_list_item_string("- abc", "abc", normal, ""). 34 | 35 | % Bullet list item, indent before bullet. 36 | 37 | test(list_item_4):- 38 | md_bullet_list_item_string(" * abc", "abc", normal, ""). 39 | 40 | % Bullet list item, more indent after bullet. 41 | 42 | test(list_item_5):- 43 | md_bullet_list_item_string("* abc", "abc", normal, ""). 44 | 45 | % Bullet list item, two lines. 46 | 47 | test(list_item_6):- 48 | md_bullet_list_item_string("* abc\ndef", "abc\ndef", normal, ""). 49 | 50 | % Bullet list item, ends with the beginning of new item. 51 | 52 | test(list_item_7):- 53 | md_bullet_list_item_string("* abc\n* def", "abc", normal, "* def"). 54 | 55 | % Bullet list item, has sublist 56 | 57 | test(list_item_8):- 58 | md_bullet_list_item_string("* abc\n * def", "abc\n* def", normal, ""). 59 | 60 | % Bullet list item, has blockquote. 61 | 62 | test(list_item_9):- 63 | md_bullet_list_item_string("* abc\n> def", "abc\n> def", normal, ""). 64 | 65 | % Bullet list item, has inline HTML. 66 | 67 | test(list_item_10):- 68 | md_bullet_list_item_string("* abc\ndef", "abc\ndef", normal, ""). 69 | 70 | % Bullet list item, ends with non-indented line. 71 | 72 | test(list_item_11):- 73 | md_bullet_list_item_string("* abc\ndef\n\nghi", "abc\ndef", normal, "ghi"). 74 | 75 | % Bullet list item, contains inline code. 76 | 77 | test(list_item_12):- 78 | md_bullet_list_item_string("* abc\n code", "abc\n code", normal, ""). 79 | 80 | % Ordered list item. 81 | 82 | test(list_item_13):- 83 | md_ordered_list_item_string("1. abc", "abc", normal, ""). 84 | 85 | % Ordered list item. Multiple lines. 86 | 87 | test(list_item_14):- 88 | md_ordered_list_item_string("1. abc\ndef", "abc\ndef", normal, ""). 89 | 90 | % Ordered list item. Indented. 91 | 92 | test(list_item_15):- 93 | md_ordered_list_item_string(" 1. abc\ndef", "abc\ndef", normal, ""). 94 | 95 | % Ordered list item. Ends with empty line. 96 | 97 | test(list_item_16):- 98 | md_ordered_list_item_string(" 1. abc\ndef\n\nghi", "abc\ndef", normal, "ghi"). 99 | 100 | % Ordered list item. Paragraph mode. 101 | 102 | test(list_item_17):- 103 | md_ordered_list_item_string(" 1. abc\ndef\n\n2. ghi", "abc\ndef", para, "2. ghi"). 104 | 105 | % No ordered list item. 106 | 107 | test(list_item_18):- 108 | ( md_ordered_list_item_string(" 1\\. abc", _, _, _) 109 | -> fail 110 | ; true). 111 | 112 | % Ordered list item. Ends with new list item. 113 | 114 | test(list_item_19):- 115 | md_ordered_list_item_string(" 1. abc\ndef\n2. ghi", "abc\ndef", normal, "2. ghi"). 116 | 117 | % Sublist indented with 1 space. 118 | 119 | test(list_item_20):- 120 | md_bullet_list_item_string("* abc\n * def\n* ghi", "abc\n* def", normal, "* ghi"). 121 | 122 | % Multiple sublists indented with 1 space. 123 | 124 | test(list_item_21):- 125 | md_bullet_list_item_string("* abc\n * def\n * ghi\n* rest", "abc\n* def\n * ghi", normal, "* rest"). 126 | 127 | :- end_tests(md_list_item). 128 | -------------------------------------------------------------------------------- /tests/span.pl: -------------------------------------------------------------------------------- 1 | :- begin_tests(md_span). 2 | :- use_module(prolog/md/md_span). 3 | 4 | test(entity):- 5 | md_span_string("&", [\['&']]). 6 | 7 | test(special_amp):- 8 | md_span_string("AT&T", [\['AT'],'&',\['T']]). 9 | 10 | test(special_lt):- 11 | md_span_string("1<2", [\['1'],'<',\['2']]). 12 | 13 | test(special_lt_end):- 14 | md_span_string("1<", [\['1'],'<']). 15 | 16 | test(preserve_html):- 17 | md_span_string("abc1", [\["abc1"]]). 18 | 19 | test(strong):- 20 | md_span_string("**abc**", [strong([\["abc"]])]). 21 | 22 | test(strong_in_text):- 23 | md_span_string("abc **def** ghi", [\["abc "],strong([\["def"]]),\[" ghi"]]). 24 | 25 | test(strong_emb_html):- 26 | md_span_string("**abcanchor**", [strong([\["abcanchor"]])]). 27 | 28 | test(strong_underscore):- 29 | md_span_string("__abc__", [strong([\["abc"]])]). 30 | 31 | test(strong_nonest):- 32 | md_span_string("**__abc__**", [strong([em([\["_abc"]]), \["_"]])]). 33 | 34 | test(strong_space):- 35 | md_span_string("** abc**", [strong([\[" abc"]])]). 36 | 37 | test(strong_space_underscore):- 38 | md_span_string("__ abc__", [strong([\[" abc"]])]). 39 | 40 | test(emphasis):- 41 | md_span_string("*abc*", [em([\["abc"]])]). 42 | 43 | test(emphasis_escape):- 44 | md_span_string("abc \\*def\\* ghi", [\["abc "], '*', \["def"], '*', \[" ghi"]]). 45 | 46 | test(emphasis_underscore):- 47 | md_span_string("_abc_", [em([\["abc"]])]). 48 | 49 | test(no_emphasis):- 50 | md_span_string("* abc*", [\["* abc*"]]). 51 | 52 | test(no_emphasis_underscore):- 53 | md_span_string("_ abc_", [\["_ abc_"]]). 54 | 55 | test(code_1):- 56 | md_span_string("`p1:- p2, p3.`", [code("p1:- p2, p3.")]). 57 | 58 | test(code_1_entities):- 59 | md_span_string("``", [code("")]). % escaped by html//1. 60 | 61 | test(code_2):- 62 | md_span_string("``p1:- p2, p3.``", [code("p1:- p2, p3.")]). 63 | 64 | test(code_2_entities):- 65 | md_span_string("````", [code("")]). % escaped by html//1. 66 | 67 | test(code_2_tick):- 68 | md_span_string("`` ` ``", [code("`")]). 69 | 70 | test(code_2_ticks):- 71 | md_span_string("`` `foo` ``", [code("`foo`")]). 72 | 73 | test(link_with_title):- 74 | md_span_string("[label](http://google.com \"Google\")", [a([href='http://google.com', title='Google'], label)]). 75 | 76 | test(link_without_title):- 77 | md_span_string("[label](http://google.com)", [a([href='http://google.com'], label)]). 78 | 79 | test(inline_link):- 80 | md_span_string("", [a([href='http://google.com'], 'http://google.com')]). 81 | 82 | test(script):- 83 | md_span_string("", [\[""]]). 84 | 85 | test(script_markdown):- 86 | md_span_string("", [\[""]]). % contains emphasis *2*. 87 | 88 | test(line_break):- 89 | md_span_string("abc \ndef", [\["abc"],br([]),\["def"]]). 90 | 91 | test(no_inline_emphasis):- 92 | md_span_string("abc_def_ghi", [\["abc_def_ghi"]]). 93 | 94 | test(link_in_strong):- 95 | md_span_string("**[label](http://google.com)**", [strong([a([href='http://google.com'], label)])]). 96 | 97 | test(strong_in_emphasis):- 98 | md_span_string("_**abc**_", [em([strong([\["abc"]])])]). 99 | 100 | test(strikethrough):- 101 | md_span_string("~~abc~~", [del([\["abc"]])]). 102 | 103 | test(strong_in_strikethrough):- 104 | md_span_string("~~**abc**~~", [del([strong([\["abc"]])])]). 105 | 106 | test(strikethrough_in_strong):- 107 | md_span_string("**~~abc~~**", [strong([del([\["abc"]])])]). 108 | 109 | test(escaped_strikethrough):- 110 | md_span_string("\\~~abc\\~~", [~, \["~abc"], ~, \[~]]). 111 | 112 | test(plain_link_http):- 113 | md_span_string("abc http://google.com rest", 114 | [\['abc '], a([href='http://google.com'], 'http://google.com'), \[" rest"]]). 115 | 116 | test(plain_link_https):- 117 | md_span_string("abc https://google.com rest", 118 | [\['abc '], a([href='https://google.com'], 'https://google.com'), \[" rest"]]). 119 | 120 | :- end_tests(md_span). 121 | -------------------------------------------------------------------------------- /tests/span_link.pl: -------------------------------------------------------------------------------- 1 | :- begin_tests(md_span_links). 2 | :- use_module(prolog/md/md_span_link). 3 | 4 | % Tests for span-level links. 5 | 6 | md_span_link_string(String, Link, Rest):- 7 | string_codes(String, Codes), 8 | phrase(md_span_link(Tmp), Codes, RestCodes), !, 9 | string_codes(Rest, RestCodes), 10 | Tmp = Link. 11 | 12 | % Tests a normal link. 13 | 14 | test(span_link_1):- 15 | md_span_link_string("[abc](http://example.com \"Title\") def", 16 | a([href='http://example.com', title='Title'], abc), " def"). 17 | 18 | % Tests a normal link without title. 19 | 20 | test(span_link_2):- 21 | md_span_link_string("[abc](http://example.com) def", 22 | a([href='http://example.com'], abc), " def"). 23 | 24 | % Tests an angular bracket link. 25 | 26 | test(span_link_3):- 27 | md_span_link_string(" def", 28 | a([href='http://example.com'], 'http://example.com'), " def"). 29 | 30 | % Tests a reference link (undefined). 31 | 32 | test(span_link_4):- 33 | retractall(md_links:link_definition(_, _, _)), 34 | ( md_span_link_string("[Google][g] def", _, _) 35 | -> fail 36 | ; true). 37 | 38 | % Tests a reference link. 39 | 40 | test(span_link_5):- 41 | retractall(md_links:link_definition(_, _, _)), 42 | assertz(md_links:link_definition(g, 'http://google.com', 'Title')), 43 | md_span_link_string("[Google][g] def", 44 | a([href='http://google.com', title='Title'], 'Google'), " def"). 45 | 46 | % Tests a reference link. Uppercase identifier. 47 | 48 | test(span_link_6):- 49 | retractall(md_links:link_definition(_, _, _)), 50 | assertz(md_links:link_definition(g, 'http://google.com', 'Title')), 51 | md_span_link_string("[Google][G] def", 52 | a([href='http://google.com', title='Title'], 'Google'), " def"). 53 | 54 | % Tests a reference link. Identifier from label. 55 | 56 | test(span_link_7):- 57 | retractall(md_links:link_definition(_, _, _)), 58 | assertz(md_links:link_definition(google, 'http://google.com', 'Title')), 59 | md_span_link_string("[Google][] def", 60 | a([href='http://google.com', title='Title'], 'Google'), " def"). 61 | 62 | % Tests a reference link. No title. 63 | 64 | test(span_link_8):- 65 | retractall(md_links:link_definition(_, _, _)), 66 | assertz(md_links:link_definition(g, 'http://google.com', '')), 67 | md_span_link_string("[Google][g] def", 68 | a([href='http://google.com'], 'Google'), " def"). 69 | 70 | % Tests an angular mail link. 71 | 72 | test(span_link_9):- 73 | md_span_link_string(" def", 74 | a([href='mailto:hel+lo@example.com'], 'hel+lo@example.com'), " def"). 75 | 76 | % Tests an image. 77 | 78 | test(span_link_10):- 79 | md_span_link_string("![Alt](/photo.jpg) def", 80 | img([src='/photo.jpg', alt='Alt']), " def"). 81 | 82 | % Tests an image with title. 83 | 84 | test(span_link_11):- 85 | md_span_link_string("![Alt](/photo.jpg \"Title\") def", 86 | img([src='/photo.jpg', alt='Alt', title='Title']), " def"). 87 | 88 | % Tests a reference-linked image. 89 | 90 | test(span_link_12):- 91 | retractall(md_links:link_definition(_, _, _)), 92 | assertz(md_links:link_definition(i, 'http://example.com/photo.jpg', 'Title')), 93 | md_span_link_string("![Photo][i] def", 94 | img([src='http://example.com/photo.jpg', alt='Photo', title='Title']), " def"). 95 | 96 | % Tests a reference link. Identifier from label. 97 | 98 | test(span_link_13):- 99 | retractall(md_links:link_definition(_, _, _)), 100 | assertz(md_links:link_definition(photo, 'http://example.com/photo.jpg', 'Title')), 101 | md_span_link_string("![Photo][] def", 102 | img([src='http://example.com/photo.jpg', alt='Photo', title='Title']), " def"). 103 | 104 | % Tests a reference-linked image. No title. 105 | 106 | test(span_link_14):- 107 | retractall(md_links:link_definition(_, _, _)), 108 | assertz(md_links:link_definition(i, 'http://example.com/photo.jpg', '')), 109 | md_span_link_string("![Photo][i] def", 110 | img([src='http://example.com/photo.jpg', alt='Photo']), " def"). 111 | 112 | % Tests a normal link, delimited with single quotes. 113 | 114 | test(span_link_15):- 115 | md_span_link_string("[abc](http://example.com 'Title') def", 116 | a([href='http://example.com', title='Title'], abc), " def"). 117 | 118 | % Tests an angular bracket link for HTTPS. 119 | 120 | test(span_link_16):- 121 | md_span_link_string(" def", 122 | a([href='https://example.com'], 'https://example.com'), " def"). 123 | 124 | :- end_tests(md_span_links). 125 | -------------------------------------------------------------------------------- /tests/tests.pl: -------------------------------------------------------------------------------- 1 | % Loads all tests. Names are relative to CWD. 2 | 3 | :- load_files([ 4 | tests/link, 5 | tests/header, 6 | tests/list_item, 7 | tests/block, 8 | tests/span_link, 9 | tests/span, 10 | tests/html, 11 | tests/file 12 | ], [ if(not_loaded) ]). 13 | --------------------------------------------------------------------------------