The OpenD Programming Language

1 module commonmarkd;
2 
3 /// Options for Markdown parsing.
4 enum MarkdownFlag : int
5 {
6      collapseWhitespace       = 0x0001,  /** Collapse non-trivial whitespace into single ' ' */
7      permissiveATXHeaders     = 0x0002,  /** Do not require space in ATX headers ( ###header ) */
8      permissiveURLAutoLinks   = 0x0004,  /** Recognize URLs as autolinks even without '<', '>' */
9      permissiveEmailAutoLinks = 0x0008,  /** Recognize e-mails as autolinks even without '<', '>' and 'mailto:' */
10      noIndentedCodeBlocks     = 0x0010,  /** Disable indented code blocks. (Only fenced code works.) */
11      noHTMLBlocks             = 0x0020,  /** Disable raw HTML blocks. */
12      noHTMLSpans              = 0x0040,  /** Disable raw HTML (inline). */
13      tablesExtension          = 0x0100,  /** Enable tables extension. */
14      enableStrikeThrough      = 0x0200,  /** Enable strikethrough extension. */
15      permissiveWWWAutoLinks   = 0x0400,  /** Enable WWW autolinks (even without any scheme prefix, if they begin with 'www.') */
16      enableTaskLists          = 0x0800,  /** Enable task list extension. */
17      latexMathSpans           = 0x1000,  /** Enable $ and $$ containing LaTeX equations. */
18 
19      permissiveAutoLinks      = permissiveEmailAutoLinks | permissiveURLAutoLinks | permissiveWWWAutoLinks, /** Recognize e-mails, URL and WWW links */
20      noHTML                   = noHTMLBlocks | noHTMLSpans, /** Disable raw HTML. */
21 
22     /* Convenient sets of flags corresponding to well-known Markdown dialects.
23      *
24      * Note we may only support subset of features of the referred dialect.
25      * The constant just enables those extensions which bring us as close as
26      * possible given what features we implement.
27      *
28      * ABI compatibility note: Meaning of these can change in time as new
29      * extensions, bringing the dialect closer to the original, are implemented.
30      */
31     dialectCommonMark          = 0, /** CommonMark */
32     dialectGitHub              = (permissiveAutoLinks | tablesExtension | enableStrikeThrough | enableTaskLists), /** Github Flavoured Markdown */
33 }
34 
35 deprecated("Use convertMarkdownToHTML instead") alias convertCommonMarkToHTML = convertMarkdownToHTML;
36 
37 /// Parses a Markdown input, returns HTML. `flags` set the particular Markdown dialect that is used.
38 string convertMarkdownToHTML(const(char)[] input, MarkdownFlag flags = MarkdownFlag.dialectCommonMark)
39 {
40     import commonmarkd.md4c;
41     import core.stdc.stdlib;
42 
43     static struct GrowableBuffer
44     {
45     nothrow:
46     @nogc:
47         char* buf = null;
48         size_t size = 0;
49         size_t allocated = 0;
50 
51         void ensureSize(size_t atLeastthisSize)
52         {
53             if (atLeastthisSize > allocated)
54             {
55                 allocated = 2 * allocated + atLeastthisSize + 1; // TODO: enhancing this estimation probably beneficial to performance
56                 buf = cast(char*) realloc(buf, allocated);
57             }
58 
59         }
60 
61         ~this()
62         {
63             if (buf)
64             {
65                 free(buf);
66                 buf = null;
67                 size = 0;
68                 allocated = 0;
69             }
70         }
71 
72         void append(const(char)[] suffix)
73         {
74             size_t L = suffix.length;
75             ensureSize(size + L);            
76             buf[size..size+L] = suffix[0..L];
77             size += L;
78         }
79 
80         const(char)[] getData()
81         {
82             return buf[0..size];
83         }
84 
85         static void appendCallback(const(char)* chars, uint size, void* userData)
86         {
87             GrowableBuffer* gb = cast(GrowableBuffer*) userData;
88             gb.append(chars[0..size]);
89         }
90     }
91 
92     GrowableBuffer gb;
93     gb.ensureSize(input.length); // TODO: enhancing this estimation probably beneficial to performance
94 
95     //int renderFlags = MD_RENDER_FLAG_DEBUG;
96     int renderFlags = 0;
97 
98     int ret = md_render_html(input.ptr, 
99                              cast(uint) input.length,
100                              &GrowableBuffer.appendCallback,
101                              &gb, flags, renderFlags);
102     return gb.getData.idup; // Note: this is the only GC-using stuff
103 }
104 
105 // Execute the CommonMark specification test suite
106 unittest
107 //void main()
108 {
109     import std.file;
110     import std.json;
111     import std.stdio;
112     import std.string;
113 
114     const(char)[] json = cast(char[]) std.file.read("spec-tests.json");
115     JSONValue root = parseJSON(json);
116 
117     static if (__VERSION__ >= 2082) 
118         assert(root.type() == JSONType.array);
119     
120     JSONValue[] tests = root.array;
121 
122     writefln("%s tests parsed.", tests.length);
123 
124     int numPASS = 0;
125     int numFAIL = 0;
126     for (size_t n = 0; n < tests.length; ++n)
127     {
128         JSONValue test = tests[n];
129         string markdown = test["markdown"].str;
130         string expectedHTML = test["html"].str;
131         long example = test["example"].integer;
132 
133         string html;
134         try
135         {
136             html = convertMarkdownToHTML(markdown, MarkdownFlag.dialectCommonMark);
137         }
138         catch(Throwable t)
139         {
140             html = t.msg;
141         }
142 
143         // Note: It seems Markdown spec says nothing about what line endings should get generated.
144         // So we replace every \r\n by just \n before comparison, else it create bugs depending
145         // on which system CommonMark test suite has been generated.
146         html = html.replace("\r\n", "\n");
147         expectedHTML = expectedHTML.replace("\r\n", "\n");
148 
149         // Poor attempt at HTML normalization
150         html = html.replace("\n", "");
151         expectedHTML = expectedHTML.replace("\n", "");
152 
153         if ( html == expectedHTML )
154         {
155             numPASS++;
156         }
157         else
158         {
159             long start_line = test["start_line"].integer; 
160             long end_line = test["end_line"].integer;
161             string section = test["section"].str;
162             writef("Test %d: ", example);
163             writefln("FAIL\n***Markdown:\n\n%s\n\n*** Expected (length %s):\n\n%s\n\n*** Got instead (length %s):\n\n%s\n\nSee specifications lines %d-%d section %s", 
164                       markdown, expectedHTML.length, expectedHTML, html.length, html, start_line, end_line, section);
165             numFAIL++;
166         }
167     }
168 
169     writefln("%s / %s tests passed.", numPASS, tests.length);
170 
171 }