├── 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("", [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("", "
").
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, ...
").
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", "- Hola
- 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 |", ""),
115 | djota:djot("|fruit|price|\n|---|---:|\n|apple|4|\n|banana|10|", ""),
116 | djota:djot("| just two \\| `|` | cells in this table |", " just two | | | cells in this table |
"),
117 | djota:djot("| ```~`Ct``` | 1 |", "").
118 |
119 | test(block_attributes) :-
120 | djota:djot("{#water}\n# Coffee\n\nCoffee 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_("", [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_("
", [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 | "", Type, ">",
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 | "", Type, ">",
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 | "", Type, ">",
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 | "", Type, ">",
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 | "", Type, ">",
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 | ",
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 |
--------------------------------------------------------------------------------