├── Makefile ├── tester.lgt ├── .github └── workflows │ └── test.yml ├── README.md ├── LICENSE ├── test.lgt └── djota.pl /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | logtalk_tester -p scryer 4 | -------------------------------------------------------------------------------- /tester.lgt: -------------------------------------------------------------------------------- 1 | :- initialization(( 2 | logtalk_load(lgtunit(loader)), 3 | logtalk_load(test, [hook(lgtunit)]), 4 | test::run 5 | )). -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-22.04 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v3 10 | - name: Checkout Scryer Prolog 11 | uses: actions/checkout@v3 12 | with: 13 | repository: mthom/scryer-prolog 14 | path: scryer-prolog 15 | - name: Compile Scryer Prolog 16 | run: cargo build --release 17 | working-directory: scryer-prolog 18 | - name: Install Scryer Prolog 19 | run: sudo cp scryer-prolog/target/release/scryer-prolog /usr/bin/scryer-prolog 20 | - name: Install Logtalk 21 | uses: logtalk-actions/setup-logtalk@master 22 | with: 23 | logtalk-version: 3.70.0 24 | - name: Execute tests 25 | run: logtalk_tester -p scryer 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Djota 2 | 3 | A [Djot](https://djot.net/) implementation in Prolog. Djot is a markup language inspired by Markdown but different design goals. See [Beyond Markdown](https://johnmacfarlane.net/beyond-markdown.html) and [Design goals of Djot](https://github.com/jgm/djot#rationale) for more information. [Learn more about the syntax of Djot here](https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html) 4 | 5 | This is a ISO Prolog implementation of Djot, useful to generate HTML fragments. 6 | 7 | ## Usage 8 | 9 | ``` 10 | :- use_module(djota). 11 | 12 | ?- djota(+InputStr, -OutputHtml). 13 | ``` 14 | 15 | where InputStr is a string in Djot, and OutputHTML unifies with the output HTML corresponding to that file. 16 | 17 | ### Example 18 | 19 | Let's render this README with Djota! 20 | 21 | ``` 22 | :- use_module(djota). 23 | :- use_module(library(dcgs)). 24 | :- use_module(library(pio)). 25 | 26 | ? - phrase_from_file(seq(Input), "README.md"), djot(Input, HTML), phrase_to_file(seq(HTML), "README.html"). 27 | ``` 28 | 29 | Runs on: 30 | 31 | * [Scryer Prolog](https://github.com/mthom/scryer-prolog) 32 | * [Trealla Prolog](https://github.com/trealla-prolog/trealla) 33 | 34 | Supported stuff: 35 | 36 | * Paragraphs 37 | * Inline links 38 | * Inline images 39 | * Autolinks 40 | * Verbatim 41 | * Emphasis 42 | * Strong 43 | * Highlighted 44 | * Super/subscript 45 | * Insert/delete 46 | * Inline attributes 47 | * Headings 48 | * Blockquotes 49 | * Lists 50 | * Code blocks 51 | * Thematic breaks 52 | * Raw blocks 53 | * Divs 54 | * Tables 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Adrián Arroyo Calle 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /test.lgt: -------------------------------------------------------------------------------- 1 | :- use_module(djota). 2 | 3 | :- object(test, extends(lgtunit)). 4 | 5 | test(backslash_ast) :- 6 | djota:inline_text_ast("Hello \\* Djota", [str("Hello * Djota")]). 7 | 8 | test(backslash) :- 9 | djota:djot("Hello \\* Djota", "

Hello * Djota

"). 10 | 11 | test(link_ast) :- 12 | djota:inline_text_ast("My link [link](http://example.com) hola", [str("My link "),link([str("link")],"http://example.com", []),str(" hola")]), 13 | djota:inline_text_ast("[My link text][foo bar]", [link_ref("My link text","foo bar")]). 14 | 15 | test(link) :- 16 | djota:djot("My link [link](http://example.com) hola", "

My link link hola

"). 17 | 18 | test(image_ast) :- 19 | djota:inline_text_ast("![picture of a cat](cat.jpg)", [image("picture of a cat", "cat.jpg", [])]), 20 | djota:inline_text_ast("![picture of a cat][cat]", [image_ref("picture of a cat", "cat")]). 21 | 22 | test(image) :- 23 | djota:djot("![picture of a cat](cat.jpg)", "

\"picture

"). 24 | 25 | test(autolink_ast) :- 26 | djota:inline_text_ast("Welcome to !", [str("Welcome to "),link("https://www.scryer.pl","https://www.scryer.pl", []),str("!")]). 27 | 28 | test(verbatim_ast) :- 29 | djota:inline_text_ast("``Verbatim with a backtick` character``", [verbatim("Verbatim with a backtick` character", [])]), 30 | djota:inline_text_ast("`Verbatim with three backticks ``` character`", [verbatim("Verbatim with three backticks ``` character", [])]). 31 | 32 | test(verbatim) :- 33 | djota:djot("``Verbatim with a backtick` character``", "

Verbatim with a backtick` character

"), 34 | djota:djot("`Verbatim with three backticks ``` character`", "

Verbatim with three backticks ``` character

"). 35 | 36 | test(emphasis_ast) :- 37 | djota:inline_text_ast("Hello _Prolog_!", [str("Hello "),emphasis([str("Prolog")], []),str("!")]), 38 | djota:inline_text_ast("Hello _ Prolog_!", [str("Hello _ Prolog_!")]), 39 | djota:inline_text_ast("Hello _Hello_Prolog _ _!", [str("Hello "),emphasis([str("Hello")], []),str("Prolog _ _!")]), 40 | djota:inline_text_ast("Hello _Hello_Prolog_ _!", [str("Hello "),emphasis([str("Hello_Prolog")], []),str(" _!")]), 41 | djota:inline_text_ast("Hello _Hello_Prolog__!", [str("Hello "),emphasis([str("Hello"),emphasis([str("Prolog")], [])], []),str("!")]). 42 | 43 | test(strong_ast) :- 44 | djota:inline_text_ast("*HelloHello*Prolog*_!", [strong([str("HelloHello*Prolog")], []),str("_!")]). 45 | 46 | test(emphasis_strong) :- 47 | djota:djot("Hello _Prolog_! You said: _I *need* to_wake up__", "

Hello Prolog! You said: I need towake up__

"). 48 | 49 | test(highlight_ast) :- 50 | djota:inline_text_ast("Hello {=Prolog=}!", [str("Hello "), highlight("Prolog", []), str("!")]). 51 | 52 | test(highlight) :- 53 | djota:djot("Hello {=Prolog=}!", "

Hello Prolog!

"). 54 | 55 | test(super_subscript_ast) :- 56 | djota:inline_text_ast("H~2~O and djot^TM^", [str("H"),subscript("2", []),str("O and djot"),superscript("TM", [])]). 57 | 58 | test(super_subscript) :- 59 | djota:djot("H~2~O and djot^TM^", "

H2O and djotTM

"). 60 | 61 | test(insert_delete_ast) :- 62 | djota:inline_text_ast("My boss is {-mean-}{+nice+}.", [str("My boss is "),delete("mean", []),insert("nice", []),str(".")]). 63 | 64 | test(insert_delete) :- 65 | djota:djot("My boss is {-mean-}{+nice+}.", "

My boss is meannice.

"). 66 | 67 | test(paragraph) :- 68 | djota:djot("Hello friends\nof [YouTube](https://youtube.com){.video}", "

Hello friends of YouTube

"), 69 | djota:djot("Hello Prolog!\n\nHello Djot!\n\n", "

Hello Prolog!

Hello Djot!

"). 70 | 71 | test(thematic_break) :- 72 | djota:djot("Then they went to sleep.\n\n * * * * \n\nWhen they woke up, ...", "

Then they went to sleep.


When they woke up, ...

"). 73 | 74 | test(section) :- 75 | djota:djot("Then they went to\n\n# sleep.\n\n * * * * \n\nWhen they woke up, ...\n\n# Hola\n\nIt's me", "

Then they went to

sleep.


When they woke up, ...

Hola

It\'s me

"). 76 | 77 | test(blockquote) :- 78 | djota:djot("> This is a block quote.\n>\n> Hello again", "

This is a block quote.

Hello again

"), 79 | djota:djot("> This is a block quote.\nAnd lazy", "

This is a block quote. And lazy

"). 80 | 81 | test(list_ast) :- 82 | djota:djot_ast("- Hola", [list(type(0, bullet("-"), tight,""),[item([paragraph([str("Hola")], [])])], [])]), 83 | djota:djot_ast("- Hola\n- Adios", [list(type(0,bullet("-"),tight,""),[item([paragraph([str("Hola")],[])]),item([paragraph([str("Adios")],[])])],[])]), 84 | djota:djot_ast("- Hola\namigos\n- Adios\namigos", [list(type(0,bullet("-"),tight,""),[item([paragraph([str("Hola amigos")], [])]),item([paragraph([str("Adios amigos")], [])])], [])]), 85 | djota:djot_ast("- Hola\namigos\n\n- Adios\namigos", [list(type(0,bullet("-"),loose,""),[item([paragraph([str("Hola amigos")], [])]),item([paragraph([str("Adios amigos")], [])])], [])]), 86 | djota:djot_ast("- Hola\namigos\n\n - Sublist\n- Adios\namigos", [list(type(0, bullet("-"), loose,""),[item([paragraph([str("Hola amigos")], []),list(type(0, bullet("-"), tight,""),[item([paragraph([str("Sublist")], [])])], [])]),item([paragraph([str("Adios amigos")], [])])], [])]), 87 | djota:djot_ast("3. Hola\n7. Adios", [list(type(0,decimal("."),tight,"3"),[item([paragraph([str("Hola")],[])]),item([paragraph([str("Adios")],[])])],[])]). 88 | 89 | test(list) :- 90 | djota:djot("- Hola\namigos\n\n - Sublist\n- Adios\namigos", ""), 91 | djota:djot("3. Hola\n7. Adios", "
  1. Hola
  2. Adios
"). 92 | 93 | test(code_block) :- 94 | djota:djot("````\nThis is how you do a code block:\n\n``` ruby\nx = 5 * 6\n```\n````", "
\nThis is how you do a code block:\n\n``` ruby\nx = 5 * 6\n```
"), 95 | djota:djot("> ```\n> code in a\n> block quote\n\nParagraph.", "
\ncode in a\nblock quote

Paragraph.

"), 96 | djota:djot("````\nThis is ````", "
\nThis is <html></html>````
"). 97 | 98 | test(raw_block) :- 99 | djota:djot("``` =html\n\n```", "\n"). 100 | 101 | test(div_block) :- 102 | djota:djot("::: warning\nHere is a paragraph.\n\nAnd here is another.\n:::", "

Here is a paragraph.

And here is another.

"). 103 | 104 | test(attr_pairs) :- 105 | djota:djot_ast("[Hola](/){.blue}{#id hola=hello} hola", [paragraph([link([str("Hola")],"/",["class"-"blue","id"-"id","hola"-"hello"]),str(" hola")], [])]). 106 | 107 | test(link_emphasis) :- 108 | djota:djot("CLP(B) is an instance of the general [CLP(_X_) scheme](#clp)", "

CLP(B) is an instance of the general CLP(X) scheme

"). 109 | 110 | test(table_ast) :- 111 | djota:djot_ast("| 1 | 2 |\n| 3 | 4 | 5 |", [table([row([[str(" 1 ")],[str(" 2 ")]]),row([[str(" 3 ")],[str(" 4 ")],[str(" 5 ")]])], [])]). 112 | 113 | test(table) :- 114 | djota:djot("| 1 | 2 |\n| 3 | 4 | 5 |", "
1 2
3 4 5
"), 115 | djota:djot("|fruit|price|\n|---|---:|\n|apple|4|\n|banana|10|", "
fruitprice
apple4
banana10
"), 116 | djota:djot("| just two \\| `|` | cells in this table |", "
just two | | cells in this table
"), 117 | djota:djot("| ```~`Ct``` | 1 |", "
~`Ct 1
"). 118 | 119 | test(block_attributes) :- 120 | djota:djot("{#water}\n# Coffee\n\nCoffee is perfect", "

Coffee

Coffee is perfect

"). 121 | 122 | :- end_object. -------------------------------------------------------------------------------- /djota.pl: -------------------------------------------------------------------------------- 1 | :- module(djota, [djot/2, djot_ast/2, inline_text_ast/2]). 2 | 3 | :- use_module(library(format)). 4 | :- use_module(library(dcgs)). 5 | :- use_module(library(pio)). 6 | :- use_module(library(lists)). 7 | :- use_module(library(dif)). 8 | :- use_module(library(charsio)). 9 | 10 | % Block syntax 11 | 12 | djot(Djot, Html) :- 13 | djot_ast(Djot, Ast), 14 | once(phrase(ast_html_(Ast), Html)). 15 | 16 | djot_ast(Djot, Ast) :- 17 | once(phrase(lines(Lines), Djot)), 18 | once(phrase(djot_ast_(Lines, []), Ast)). 19 | 20 | % From Djot source to AST 21 | 22 | % Thematic break 23 | djot_ast_([Line|Lines], Attrs) --> 24 | { phrase(thematic_break_line(0), Line) }, 25 | [thematic_break(Attrs)], 26 | djot_ast_(Lines, []). 27 | 28 | % Heading 29 | djot_ast_([Line|Lines], Attrs) --> 30 | { phrase(heading_line(N, Header), Line) }, 31 | djot_heading_ast_(Lines, N, Header, Attrs). 32 | 33 | % Blockquote 34 | djot_ast_([Line|Lines], Attrs) --> 35 | { phrase(blockquote_line(Text), Line) }, 36 | djot_blockquote_ast_(Lines, Text, Attrs). 37 | 38 | % List 39 | djot_ast_([Line|Lines], Attrs) --> 40 | { phrase(list_line(Type, Text), Line) }, 41 | djot_list_ast_(Type, Lines, Text, [], continue, Attrs). 42 | 43 | % Code block 44 | djot_ast_([Line|Lines], Attrs) --> 45 | { phrase(((backticks(N, _), " ", seq(Spec)) | backticks(N, _), ... ), Line), N >= 3 }, 46 | djot_code_ast_(Lines, N, "", Spec, Attrs). 47 | 48 | % Div block 49 | djot_ast_([Line|Lines], Attrs) --> 50 | { phrase(((colons(N), " ", seq(ClassName)) | colons(N), ... ), Line), N >= 3 }, 51 | { append(["class"-ClassName], Attrs, Attrs1) }, 52 | djot_div_ast_(Lines, N, "", Attrs1). 53 | 54 | % Pipe table 55 | djot_ast_([Line|Lines], Attrs) --> 56 | { phrase(pipe_table(Row), Line) }, 57 | djot_table_ast_(Lines, [row(Row)], Attrs). 58 | 59 | % Block attribute 60 | djot_ast_([Line|Lines], Attrs) --> 61 | { phrase(inline_attr_ast_single_(Attrs1), Line), append(Attrs, Attrs1, Attrs2) }, 62 | djot_ast_(Lines, Attrs2). 63 | 64 | % Paragraph 65 | djot_ast_([Line|Lines], Attrs) --> 66 | { Line \= "" }, 67 | djot_paragraph_ast_([Line|Lines], "", Attrs). 68 | 69 | % Empty line 70 | djot_ast_([[]|Lines], Attrs) --> 71 | djot_ast_(Lines, Attrs). 72 | % No more lines 73 | djot_ast_([], _) --> []. 74 | 75 | % Code block 76 | djot_code_ast_([Line|Lines], N, Code0, Spec, Attrs) --> 77 | { \+ ( phrase(backticks(M, _), Line), M >= N), append(Code0, ['\n'|Line], Code) }, 78 | djot_code_ast_(Lines, N, Code, Spec, Attrs). 79 | 80 | djot_code_ast_([Line|Lines], N, Code0, Spec, Attrs) --> 81 | { phrase(backticks(M, _), Line), M >= N }, 82 | [code(Spec, Code0, Attrs)], 83 | djot_ast_(Lines, []). 84 | 85 | djot_code_ast_([], _, Code0, Spec, Attrs) --> 86 | [code(Spec, Code0, Attrs)]. 87 | 88 | % List: Line of list type same as current list 89 | djot_list_ast_(type(Level, Type, Mode, OrdStart), [Line|Lines], CurrentItem, Items, _, Attrs) --> 90 | { 91 | phrase(list_line(type(Level, Type, _, _), Text), Line), 92 | djot_ast(CurrentItem, ItemAst), 93 | append(Items, [item(ItemAst)], NewItems) 94 | }, 95 | djot_list_ast_(type(Level, Type, Mode, OrdStart), Lines, Text, NewItems, continue, Attrs). 96 | % List: Line non indented in continue mode 97 | djot_list_ast_(Type, [Line|Lines], CurrentItem, Items, continue, Attrs) --> 98 | { 99 | Line \= "", 100 | \+ phrase(list_line(_, _), Line), 101 | append(CurrentItem, ['\n'|Line], CurrentItem1) 102 | }, 103 | djot_list_ast_(Type, Lines, CurrentItem1, Items, continue, Attrs). 104 | % List: Empty line 105 | djot_list_ast_(type(Level, Type, _, OrdStart), [Line|Lines], CurrentItem, Items, _, Attrs) --> 106 | { 107 | phrase(whites(_), Line), 108 | append(CurrentItem, ['\n'|Line], CurrentItem1) 109 | }, 110 | djot_list_ast_(type(Level, Type, loose, OrdStart), Lines, CurrentItem1, Items, jump, Attrs). 111 | 112 | % List: Line indented in jump mode 113 | djot_list_ast_(type(Level, Type, Mode, OrdStart), [Line|Lines], CurrentItem, Items, jump, Attrs) --> 114 | { 115 | phrase((whites(W), seq(Text)), Line), 116 | W > Level, 117 | append(CurrentItem, ['\n'|Text], CurrentItem1) 118 | }, 119 | djot_list_ast_(type(Level, Type, Mode, OrdStart), Lines, CurrentItem1, Items, jump, Attrs). 120 | 121 | % List: Line not indented in jump mode 122 | djot_list_ast_(type(Level, Type, Mode, OrdStart), [Line|Lines], CurrentItem, Items, jump, Attrs) --> 123 | { 124 | phrase((whites(W), seq(_)), Line), 125 | W =< Level, 126 | djot_ast(CurrentItem, ItemAst), 127 | append(Items, [item(ItemAst)], NewItems) 128 | }, 129 | [list(type(Level, Type, Mode, OrdStart), NewItems, Attrs)], 130 | djot_ast_([Line|Lines], []). 131 | 132 | % List: No more lines 133 | djot_list_ast_(Type, [], CurrentItem, Items, _, Attrs) --> 134 | { 135 | djot_ast(CurrentItem, ItemAst), 136 | append(Items, [item(ItemAst)], NewItems) 137 | }, 138 | [list(Type, NewItems, Attrs)]. 139 | 140 | 141 | list_type(bullet("-")) --> "-". 142 | list_type(bullet("*")) --> "*". 143 | list_type(bullet("+")) --> "+". 144 | list_type(decimal(".", N)) --> number_(N), ".". 145 | list_type(decimal(")", N)) --> number_(N), ")". 146 | list_type(decimal("()", N)) --> "(", number_(N), ")". 147 | list_line(type(Level, bullet(BulletType), tight, ""), Text) --> 148 | whites(Level), list_type(bullet(BulletType)), " ", seq(Text). 149 | list_line(type(Level, decimal(DecimalType), tight, OrdStart), Text) --> 150 | whites(Level), list_type(decimal(DecimalType, OrdStart)), " ", seq(Text). 151 | 152 | whites(0) --> "". 153 | whites(N) --> 154 | " ", 155 | whites(N0), 156 | { N is N0 + 1}. 157 | 158 | djot_heading_ast_([Line|Lines], N, Header, Attrs) --> 159 | { phrase(heading_line(N, Header), Line), append(Header, [' '|Line], Header1) }, 160 | djot_heading_ast_(Lines, N, Header1, Attrs). 161 | 162 | djot_heading_ast_([[]|Lines], N, Header, Attrs) --> 163 | { 164 | append(SectionLines, [HeadingLine|Rest], Lines), 165 | phrase(heading_line(NextN, _), HeadingLine), 166 | NextN =< N, 167 | section_lines_block(SectionLines, SectionLines1, BlockAttrs), 168 | phrase(djot_ast_(SectionLines1, []), SectionAst), 169 | append(BlockAttrs, [HeadingLine|Rest], NextLines) 170 | }, 171 | [section(N, Header, SectionAst, Attrs)], 172 | djot_ast_(NextLines, []). 173 | 174 | djot_heading_ast_([[]|Lines], N, Header, Attrs) --> 175 | { 176 | phrase(djot_ast_(Lines, []), SectionAst) 177 | }, 178 | [section(N, Header, SectionAst, Attrs)]. 179 | 180 | djot_heading_ast_([], N, Header, Attrs) --> 181 | djot_heading_ast_([""], N, Header, Attrs). 182 | 183 | section_lines_block([], [], []). 184 | section_lines_block(Lines, LinesOut, Block) :- 185 | append(Lines1, [BlockLine], Lines), 186 | ( 187 | phrase(inline_attr_ast_single_(_), BlockLine) -> 188 | (section_lines_block(Lines1, LinesOut, Block0), Block = [BlockLine|Block0]) 189 | ; ( Lines = LinesOut, Block = [] ) 190 | ). 191 | 192 | heading_line(1, Header) --> "# ", seq(Header). 193 | heading_line(2, Header) --> "## ", seq(Header). 194 | heading_line(3, Header) --> "### ", seq(Header). 195 | heading_line(4, Header) --> "#### ", seq(Header). 196 | heading_line(5, Header) --> "##### ", seq(Header). 197 | heading_line(6, Header) --> "###### ", seq(Header). 198 | 199 | section_lines([X|Xs], N) --> 200 | [X], { \+ phrase(heading_line(N, _), X) }, 201 | section_lines(Xs, N). 202 | 203 | section_lines([], N) --> 204 | heading_line(N, _). 205 | section_lines([], _) --> []. 206 | 207 | djot_blockquote_ast_([Line|Lines], Block, Attrs) --> 208 | { phrase(blockquote_line(Text), Line), append(Block, ['\n'|Text], Block1) }, 209 | djot_blockquote_ast_(Lines, Block1, Attrs). 210 | 211 | djot_blockquote_ast_([Line|Lines], Block, Attrs) --> 212 | { Line \= "", \+ phrase(blockquote_line(_), Line), append(Block, ['\n'|Line], Block1) }, 213 | djot_blockquote_ast_(Lines, Block1, Attrs). 214 | 215 | djot_blockquote_ast_([""|Lines], Block, Attrs) --> 216 | { djot_ast(Block, InsideAst) }, 217 | [blockquote(InsideAst, Attrs)], 218 | djot_ast_(Lines, []). 219 | 220 | djot_blockquote_ast_([], Block, Attrs) --> 221 | djot_blockquote_ast_([""], Block, Attrs). 222 | 223 | blockquote_line(Text) --> 224 | "> ", seq(Text). 225 | 226 | blockquote_line("") --> 227 | ">". 228 | 229 | djot_div_ast_([Line|Lines], N, Block, Attrs) --> 230 | { \+ (phrase(colons(M), Line), M >= N), append(Block, ['\n'|Line], Block1) }, 231 | djot_div_ast_(Lines, N, Block1, Attrs). 232 | 233 | djot_div_ast_([Line|Lines], N, Block, Attrs) --> 234 | { phrase(colons(M), Line), M >= N, djot_ast(Block, InsideAst) }, 235 | [div_block(InsideAst, Attrs)], 236 | djot_ast_(Lines, []). 237 | 238 | djot_div_ast_([], _, Block, Attrs) --> 239 | { djot_ast(Block, InsideAst) }, 240 | [div_block(InsideAst, Attrs)]. 241 | 242 | pipe_table(Row) --> 243 | "|", 244 | table_row(Row), 245 | "|", 246 | whites(_). 247 | 248 | table_row([Ast|Xs]) --> 249 | table_str(X), 250 | { 251 | inline_text_ast(X, Ast), 252 | append(_, [T], X), 253 | dif(T, '\\') 254 | }, 255 | "|", 256 | table_row(Xs). 257 | 258 | table_row([Ast]) --> 259 | table_str(X), { length(X, N), N > 0, inline_text_ast(X, Ast) }. 260 | 261 | table_str([]) --> []. 262 | table_str([X|Xs]) --> 263 | [X], 264 | { dif(X, '`') }, 265 | table_str(Xs). 266 | table_str(X) --> 267 | backticks(N, Backticks), 268 | backticks_end(N, Str), 269 | table_str(Xs), 270 | { append(Backticks, Str, Str0), append(Str0, Backticks, Str1), append(Str1, Xs, X) }. 271 | 272 | separator_table(Style) --> 273 | "|", 274 | table_style(Style), 275 | "|", 276 | whites(_). 277 | table_style([none|Xs]) --> 278 | dashes, 279 | "|", 280 | table_style(Xs). 281 | table_style([none]) --> 282 | dashes. 283 | table_style([left|Xs]) --> 284 | ":", dashes, 285 | "|", 286 | table_style(Xs). 287 | table_style([left]) --> 288 | ":", dashes. 289 | table_style([right|Xs]) --> 290 | dashes, ":", 291 | "|", 292 | table_style(Xs). 293 | table_style([right]) --> 294 | dashes, ":". 295 | table_style([center|Xs]) --> 296 | ":", dashes, ":", 297 | "|", 298 | table_style(Xs). 299 | table_style([center]) --> 300 | ":", dashes, ":". 301 | 302 | dashes --> "-" | "-", dashes. 303 | 304 | 305 | djot_table_ast_([Line|Lines], Rows, Attrs) --> 306 | { 307 | phrase(separator_table(Style), Line), 308 | append(RestRows, [row(Row)], Rows), 309 | append(RestRows, [header(Row), set_style(Style)], Rows1) 310 | }, 311 | djot_table_ast_(Lines, Rows1, Attrs). 312 | 313 | djot_table_ast_([Line|Lines], Rows, Attrs) --> 314 | { 315 | phrase(pipe_table(Row), Line), 316 | append(Rows, [row(Row)], Rows1) 317 | }, 318 | djot_table_ast_(Lines, Rows1, Attrs). 319 | 320 | djot_table_ast_([Line|Lines], Rows, Attrs) --> 321 | { \+ phrase(pipe_table(_), Line) }, 322 | [table(Rows, Attrs)], 323 | djot_ast_(Lines, []). 324 | djot_table_ast_([], Rows, Attrs) --> 325 | djot_table_ast_([""], Rows, Attrs). 326 | 327 | djot_paragraph_ast_([Line|Lines], Paragraph0, Attrs) --> 328 | { 329 | Line \= "", 330 | ( 331 | Paragraph0 = "" -> 332 | Line = Paragraph1 333 | ; append(Paragraph0, [' '|Line], Paragraph1) 334 | ) 335 | }, 336 | djot_paragraph_ast_(Lines, Paragraph1, Attrs). 337 | 338 | djot_paragraph_ast_([""|Lines], Paragraph, Attrs) --> 339 | { phrase(inline_text_ast_(InlineAst), Paragraph) }, 340 | [paragraph(InlineAst, Attrs)], 341 | djot_ast_(Lines, []). 342 | 343 | djot_paragraph_ast_([], Paragraph, Attrs) --> 344 | djot_paragraph_ast_([""], Paragraph, Attrs). 345 | 346 | thematic_break_line(N) --> 347 | (" "|"\t"), 348 | thematic_break_line(N). 349 | 350 | thematic_break_line(N0) --> 351 | "*", 352 | { N is N0 + 1}, 353 | thematic_break_line(N). 354 | 355 | thematic_break_line(N) --> 356 | { N >= 3 }. 357 | 358 | % From AST to HTML 359 | 360 | ast_html_([]) --> []. 361 | ast_html_([X|Xs]) --> 362 | ast_html_node_(X), 363 | ast_html_(Xs). 364 | 365 | ast_html_node_(thematic_break(Attrs)) --> 366 | { attrs_html(Attrs, AttrsHtml) }, 367 | "". 368 | ast_html_node_(paragraph(InlineAst, Attrs)) --> 369 | { attrs_html(Attrs, AttrsHtml) }, 370 | "", 371 | ast_html_(InlineAst), 372 | "

". 373 | ast_html_node_(section(N, Header, Child, Attrs)) --> 374 | { phrase(ast_html_(Child), ChildHtml) }, 375 | { attrs_html(Attrs, AttrsHtml) }, 376 | format_("~s~s", [AttrsHtml, N, Header, N, ChildHtml]). 377 | ast_html_node_(blockquote(Child, Attrs)) --> 378 | { phrase(ast_html_(Child), ChildHtml) }, 379 | { attrs_html(Attrs, AttrsHtml) }, 380 | "", ChildHtml, "". 381 | ast_html_node_(list(type(_, bullet(_), Mode, _), Items, Attrs)) --> 382 | { attrs_html(Attrs, AttrsHtml) }, 383 | "", 384 | ast_html_node_items_(Items, Mode), 385 | "". 386 | ast_html_node_(list(type(_, decimal(_), Mode, OrdStart), Items, Attrs)) --> 387 | { attrs_html(["start"-OrdStart|Attrs], AttrsHtml) }, 388 | "", 389 | ast_html_node_items_(Items, Mode), 390 | "". 391 | ast_html_node_(code(Spec, Code, Attrs)) --> 392 | { dif(Spec, "=html"), phrase(escape_html_(Html), Code) }, 393 | { attrs_html(Attrs, AttrsHtml) }, 394 | "", Html, "". 395 | ast_html_node_(code("=html", Html, _)) --> 396 | Html. 397 | ast_html_node_(div_block(Block, Attrs)) --> 398 | { phrase(ast_html_(Block), Html) }, 399 | { attrs_html(Attrs, AttrsHtml) }, 400 | "", Html, "". 401 | ast_html_node_(table(Rows, Attrs)) --> 402 | { attrs_html(Attrs, AttrsHtml) }, 403 | "", 404 | ast_html_rows_(Rows, []), 405 | "". 406 | ast_html_node_(link(TextAst, Url, Attrs)) --> 407 | { phrase(ast_html_(TextAst), TextHtml) }, 408 | { attrs_html(Attrs, AttrsHtml) }, 409 | format_("~s", [Url, AttrsHtml, TextHtml]). 410 | ast_html_node_(image(AltText, Url, Attrs)) --> 411 | { attrs_html(Attrs, AttrsHtml) }, 412 | format_("\"~s\"", [AltText, Url, AttrsHtml]). 413 | ast_html_node_(verbatim(Text, Attrs)) --> 414 | { attrs_html(Attrs, AttrsHtml) }, 415 | "", Text, "". 416 | ast_html_node_(emphasis(Child, Attrs)) --> 417 | { phrase(ast_html_(Child), ChildHtml) }, 418 | { attrs_html(Attrs, AttrsHtml) }, 419 | "", ChildHtml, "". 420 | ast_html_node_(strong(Child, Attrs)) --> 421 | { phrase(ast_html_(Child), ChildHtml) }, 422 | { attrs_html(Attrs, AttrsHtml) }, 423 | "", ChildHtml, "". 424 | ast_html_node_(highlight(Str, Attrs)) --> 425 | { attrs_html(Attrs, AttrsHtml) }, 426 | "", Str, "". 427 | ast_html_node_(superscript(Str, Attrs)) --> 428 | { attrs_html(Attrs, AttrsHtml) }, 429 | "", Str, "". 430 | ast_html_node_(subscript(Str, Attrs)) --> 431 | { attrs_html(Attrs, AttrsHtml) }, 432 | "", Str, "". 433 | ast_html_node_(insert(Str, Attrs)) --> 434 | { attrs_html(Attrs, AttrsHtml) }, 435 | "", Str, "". 436 | ast_html_node_(delete(Str, Attrs)) --> 437 | { attrs_html(Attrs, AttrsHtml) }, 438 | "", Str, "". 439 | ast_html_node_(str(Str)) --> 440 | { phrase(escape_html_(Html), Str) }, 441 | Html. 442 | 443 | ast_html_node_items_([], _) --> "". 444 | ast_html_node_items_([item(Item)|Items], loose) --> 445 | { phrase(ast_html_(Item), Html) }, 446 | "
  • ", 447 | Html, 448 | "
  • ", 449 | ast_html_node_items_(Items, loose). 450 | ast_html_node_items_([item([paragraph(Item, _)])|Items], tight) --> 451 | { phrase(ast_html_(Item), Html) }, 452 | "
  • ", 453 | Html, 454 | "
  • ", 455 | ast_html_node_items_(Items, tight). 456 | 457 | ast_html_rows_([], _) --> "". 458 | ast_html_rows_([row(Row)|Rows], Style) --> 459 | "", 460 | ast_html_cell_("td", Row, Style), 461 | "", 462 | ast_html_rows_(Rows, Style). 463 | ast_html_rows_([header(Row)|Rows], Style) --> 464 | "", 465 | ast_html_cell_("th", Row, Style), 466 | "", 467 | ast_html_rows_(Rows, Style). 468 | 469 | ast_html_rows_([set_style(Style)|Rows], _) --> 470 | ast_html_rows_(Rows, Style). 471 | 472 | ast_html_cell_(_, [], _) --> "". 473 | ast_html_cell_(Type, [X|Xs], [none|Style]) --> 474 | { phrase(ast_html_(X), Html) }, 475 | "<", Type, ">", 476 | Html, 477 | "", 478 | ast_html_cell_(Type, Xs, Style). 479 | ast_html_cell_(Type, [X|Xs], [left|Style]) --> 480 | { phrase(ast_html_(X), Html) }, 481 | "<", Type, " style=\"text-align:left;\">", 482 | Html, 483 | "", 484 | ast_html_cell_(Type, Xs, Style). 485 | ast_html_cell_(Type, [X|Xs], [right|Style]) --> 486 | { phrase(ast_html_(X), Html) }, 487 | "<", Type, " style=\"text-align:right;\">", 488 | Html, 489 | "", 490 | ast_html_cell_(Type, Xs, Style). 491 | ast_html_cell_(Type, [X|Xs], [center|Style]) --> 492 | { phrase(ast_html_(X), Html) }, 493 | "<", Type, " style=\"text-align:center;\">", 494 | Html, 495 | "", 496 | ast_html_cell_(Type, Xs, Style). 497 | ast_html_cell_(Type, [X|Xs], []) --> 498 | { phrase(ast_html_(X), Html) }, 499 | "<", Type, ">", 500 | Html, 501 | "", 502 | ast_html_cell_(Type, Xs, []). 503 | 504 | escape_html_([]) --> []. 505 | escape_html_(Html) --> 506 | [Char], 507 | { 508 | Char = '&', append("&", Html0, Html) 509 | }, 510 | escape_html_(Html0). 511 | escape_html_(Html) --> 512 | [Char], 513 | { 514 | Char = (<), append("<", Html0, Html) 515 | }, 516 | escape_html_(Html0). 517 | escape_html_(Html) --> 518 | [Char], 519 | { 520 | Char = (>), append(">", Html0, Html) 521 | }, 522 | escape_html_(Html0). 523 | escape_html_([Char|Html0]) --> 524 | [Char], 525 | escape_html_(Html0). 526 | 527 | attrs_html([], ""). 528 | attrs_html([Key-Value|Xs], Html) :- 529 | attrs_html(Xs, Html1), 530 | phrase(format_(" ~s=\"~s\"", [Key, Value]), Html0), 531 | append(Html0, Html1, Html). 532 | 533 | char(X) --> 534 | [X], 535 | { 536 | \+ member(X, "\n\r") 537 | }. 538 | line_ending --> "\n" | "\r" | "\r\n". 539 | 540 | line_chars([X|Xs]) --> 541 | char(X), 542 | line_chars(Xs). 543 | line_chars([X]) --> char(X). 544 | 545 | line([X|Xs]) --> 546 | char(X), 547 | line(Xs). 548 | 549 | line([]) --> line_ending. 550 | 551 | lines([X|Xs]) --> 552 | line(X), 553 | lines(Xs). 554 | 555 | lines([X]) --> 556 | line_chars(X), 557 | file_ending. 558 | 559 | file_ending --> []. 560 | file_ending --> line_ending. 561 | file_ending --> line_ending, file_ending. 562 | 563 | % Inline syntax AST 564 | inline_text_ast(Text, Ast) :- 565 | once(phrase(inline_text_ast_(Ast), Text)). 566 | 567 | inline_text_ast_(Ast) --> 568 | insert_ast_(Ast). 569 | inline_text_ast_(Ast) --> 570 | delete_ast_(Ast). 571 | inline_text_ast_(Ast) --> 572 | superscript_ast_(Ast). 573 | inline_text_ast_(Ast) --> 574 | subscript_ast_(Ast). 575 | inline_text_ast_(Ast) --> 576 | highlight_ast_(Ast). 577 | inline_text_ast_(Ast) --> 578 | strong_ast_(Ast). 579 | inline_text_ast_(Ast) --> 580 | emphasis_ast_(Ast). 581 | inline_text_ast_(Ast) --> 582 | verbatim_ast_(Ast). 583 | inline_text_ast_(Ast) --> 584 | autolink_ast_(Ast). 585 | inline_text_ast_(Ast) --> 586 | reference_image_ast_(Ast). 587 | inline_text_ast_(Ast) --> 588 | inline_image_ast_(Ast). 589 | inline_text_ast_(Ast) --> 590 | reference_link_ast_(Ast). 591 | inline_text_ast_(Ast) --> 592 | inline_link_ast_(Ast). 593 | inline_text_ast_(Ast) --> 594 | str_ast_(Ast). 595 | inline_text_ast_([]) --> 596 | []. 597 | 598 | insert_ast_([insert(Str, Attrs)|Ast0]) --> 599 | "{+", 600 | seq(Str), 601 | "+}", 602 | inline_attr_ast_(Attrs), 603 | inline_text_ast_(Ast0). 604 | 605 | delete_ast_([delete(Str, Attrs)|Ast0]) --> 606 | "{-", 607 | seq(Str), 608 | "-}", 609 | inline_attr_ast_(Attrs), 610 | inline_text_ast_(Ast0). 611 | 612 | superscript_ast_([superscript(Str, Attrs)|Ast0]) --> 613 | ( "^" | "{^" ), 614 | seq(Str), 615 | ( "^" | "^}" ), 616 | inline_attr_ast_(Attrs), 617 | inline_text_ast_(Ast0). 618 | 619 | subscript_ast_([subscript(Str, Attrs)|Ast0]) --> 620 | ( "~" | "{~" ), 621 | seq(Str), 622 | ( "~" | "~}" ), 623 | inline_attr_ast_(Attrs), 624 | inline_text_ast_(Ast0). 625 | 626 | highlight_ast_([highlight(Str, Attrs)|Ast0]) --> 627 | "{=", 628 | seq(Str), 629 | "=}", 630 | inline_attr_ast_(Attrs), 631 | inline_text_ast_(Ast0). 632 | 633 | emphasis_ast_([emphasis(InlineAst, Attrs)|Ast0]) --> 634 | ( ("_", look_ahead(T), { T \= ' ' }) | "{_" ), 635 | seq(Inline), 636 | "_", 637 | { 638 | append(_, [G], Inline), 639 | G \= ' ', 640 | inline_text_ast(Inline, InlineAst) 641 | }, 642 | inline_attr_ast_(Attrs), 643 | inline_text_ast_(Ast0). 644 | 645 | emphasis_ast_([emphasis(InlineAst, Attrs)|Ast0]) --> 646 | ( ("_", look_ahead(T), { T \= ' ' }) | "{_" ), 647 | seq(Inline), 648 | "_}", 649 | { inline_text_ast(Inline, InlineAst) }, 650 | inline_attr_ast_(Attrs), 651 | inline_text_ast_(Ast0). 652 | 653 | strong_ast_([strong(InlineAst, Attrs)|Ast0]) --> 654 | ( ("*", look_ahead(T), { T \= ' ' }) | "{*" ), 655 | seq(Inline), 656 | "*", 657 | { 658 | append(_, [G], Inline), 659 | G \= ' ', 660 | inline_text_ast(Inline, InlineAst) 661 | }, 662 | inline_attr_ast_(Attrs), 663 | inline_text_ast_(Ast0). 664 | 665 | strong_ast_([strong(InlineAst, Attrs)|Ast0]) --> 666 | ( ("*", look_ahead(T), { T \= ' ' }) | "{*" ), 667 | seq(Inline), 668 | "*}", 669 | { inline_text_ast(Inline, InlineAst) }, 670 | inline_attr_ast_(Attrs), 671 | inline_text_ast_(Ast0). 672 | 673 | verbatim_ast_([verbatim(Str, Attrs)|Ast0]) --> 674 | backticks(N, _), 675 | backticks_end(N, Str), 676 | inline_attr_ast_(Attrs), 677 | inline_text_ast_(Ast0). 678 | 679 | backticks_end(N, Str) --> 680 | backticks(M, Str0), { M \= N }, 681 | backticks_end(N, Str1), 682 | { 683 | append(Str0, Str1, Str) 684 | }. 685 | backticks_end(N, "") --> 686 | backticks(N, _). 687 | backticks_end(N, [Char|Str0]) --> 688 | [Char], { Char \= "`" }, 689 | backticks_end(N, Str0). 690 | 691 | backticks(N, ['`'|Str]) --> 692 | "`", 693 | backticks(N0, Str), 694 | { N is N0 + 1 }. 695 | backticks(1, "`") --> "`". 696 | 697 | colons(N) --> 698 | ":", 699 | colons(N0), 700 | { N is N0 + 1 }. 701 | colons(1) --> ":". 702 | 703 | autolink_ast_([link(Url, Url, Attrs)|Ast0]) --> 704 | { append("http://", _, Url); append("https://", _, Url) }, 705 | "<", 706 | seq(Url), 707 | ">", 708 | inline_attr_ast_(Attrs), 709 | inline_text_ast_(Ast0). 710 | 711 | reference_image_ast_([Node|Ast0]) --> 712 | "![", 713 | seq(AltText), 714 | "][", 715 | seq(RefName), 716 | "]", 717 | inline_text_ast_(Ast0), 718 | { 719 | ( RefName = "" -> 720 | Node = image_ref(AltText) 721 | ; Node = image_ref(AltText, RefName) 722 | ) 723 | }. 724 | 725 | inline_image_ast_([image(AltText, Url, Attrs)|Ast0]) --> 726 | "![", 727 | seq(AltText), 728 | "](", 729 | seq(Url), 730 | ")", 731 | inline_attr_ast_(Attrs), 732 | inline_text_ast_(Ast0). 733 | 734 | reference_link_ast_([Node|Ast0]) --> 735 | "[", 736 | seq(LinkText), 737 | "][", 738 | seq(RefName), 739 | "]", 740 | inline_text_ast_(Ast0), 741 | { 742 | ( RefName = "" -> 743 | Node = link_ref(LinkText) 744 | ; Node = link_ref(LinkText, RefName) 745 | ) 746 | }. 747 | 748 | inline_link_ast_([link(LinkAst, LinkUrl, Attrs)|Ast0]) --> 749 | "[", 750 | seq(LinkText), 751 | "](", 752 | seq(LinkUrl), 753 | ")", 754 | { inline_text_ast(LinkText, LinkAst) }, 755 | inline_attr_ast_(Attrs), 756 | inline_text_ast_(Ast0). 757 | 758 | str_ast_(Ast) --> 759 | "\\", 760 | [Char], 761 | inline_text_ast_([PrevNode|Ast1]), 762 | { 763 | ( 764 | PrevNode = str(Str) -> 765 | Ast = [str([Char|Str])|Ast1] 766 | ; Ast = [str([Char]),PrevNode|Ast1] 767 | ) 768 | }. 769 | 770 | str_ast_([str([Char])]) --> 771 | "\\", 772 | [Char], 773 | inline_text_ast_([]). 774 | 775 | str_ast_(Ast) --> 776 | [Char], 777 | inline_text_ast_([PrevNode|Ast1]), 778 | { 779 | ( 780 | PrevNode = str(Str) -> 781 | Ast = [str([Char|Str])|Ast1] 782 | ; Ast = [str([Char]),PrevNode|Ast1] 783 | ) 784 | }. 785 | 786 | str_ast_([str([Char])]) --> 787 | [Char], 788 | inline_text_ast_([]). 789 | 790 | inline_attr_ast_(Attrs) --> 791 | inline_attr_ast_single_(Attr0), 792 | inline_attr_ast_(Attr1), 793 | { append(Attr0, Attr1, Attrs) }. 794 | inline_attr_ast_([]) --> []. 795 | 796 | inline_attr_ast_single_(Attrs) --> 797 | "{", 798 | seq(AttrText), 799 | "}", 800 | { phrase(attr_pairs(Attrs), AttrText) }. 801 | 802 | 803 | attr_pairs(["id"-Name|Xs]) --> 804 | "#", seq(Name), whites(N), { N > 0 }, attr_pairs(Xs). 805 | 806 | attr_pairs(["id"-Name]) --> 807 | "#", seq(Name), whites(0). 808 | 809 | attr_pairs(["class"-Name|Xs]) --> 810 | ".", seq(Name), whites(N), { N > 0 }, attr_pairs(Xs). 811 | attr_pairs(["class"-Name]) --> 812 | ".", seq(Name), whites(0). 813 | 814 | attr_pairs([Key-Value|Xs]) --> 815 | seq(Key), { length(Key, N), N > 0 }, 816 | "=", 817 | seq(Value), { length(Value, M), M > 0}, 818 | whites(O), { O > 0 }, attr_pairs(Xs). 819 | 820 | attr_pairs([Key-Value]) --> 821 | seq(Key), { length(Key, N), N > 0 }, 822 | "=", 823 | seq(Value), { length(Value, M), M > 0}, 824 | whites(0). 825 | 826 | 827 | look_ahead(T), [T] --> [T]. 828 | 829 | number_([D|Ds]) --> digit(D), number_(Ds). 830 | number_([D]) --> digit(D). 831 | 832 | digit(D) --> [D], { char_type(D, decimal_digit) }. 833 | --------------------------------------------------------------------------------