1 module commands.main;
2 
3 import commonmarkd.md4c;
4 import jcli;
5 import std.array;
6 import std.container.array : Array;
7 import std.stdio;
8 import std.typecons;
9 import std.conv;
10 import std.range;
11 
12 @CommandDefault("Execute code block in markdown.")
13 struct DefaultCommand
14 {
15     @CommandPositionalArg(0, "file", "Markdown file (.md)")
16     Nullable!string file;
17 
18     @CommandArgGroup("Options")
19     {
20         @CommandNamedArg("quiet|q", "Only print warnings and errors")
21         Nullable!bool quiet;
22 
23         @CommandNamedArg("verbose|v", "Print diagnostic output")
24         Nullable!bool verbose;
25     }
26 
27     int onExecute()
28     {
29         auto packageName = loadCurrentProjectName();
30         if (verbose.isTrue)
31             writeln("packageName: ", packageName);
32 
33         string filepath = !file.isNull ? file.get() : "README.md";
34         auto result = parseMarkdown(filepath);
35 
36         Appender!(string)[string] blocks;
37         string[] singleBlocks;
38         string[] globalBlocks;
39         foreach (block; result.blocks)
40         {
41             if (block.lang == "d" || block.lang == "D")
42             {
43                 if (isDisabledBlock(block))
44                     continue;
45                 if (isSingleBlock(block))
46                 {
47                     singleBlocks ~= block.code[].to!string();
48                     continue;
49                 }
50                 if (isGlobalBlock(block))
51                 {
52                     globalBlocks ~= block.code[].to!string();
53                     continue;
54                 }
55 
56                 auto name = getBlockName(block);
57                 if (!(name in blocks))
58                 {
59                     blocks[name] = appender!string;
60                 }
61                 blocks[name].put(block.code[]);
62             }
63         }
64 
65         // evaluate all
66         size_t totalCount;
67         size_t errorCount;
68         foreach (key, value; blocks)
69         {
70             totalCount++;
71             if (!quiet.get(false))
72                 writeln("begin: ", key);
73             scope (exit)
74                 if (!quiet.get(false))
75                     writeln("end: ", key);
76 
77             const status = evaluate(value.data, packageName, BlockType.Single, verbose.isTrue);
78             errorCount += status != 0;
79         }
80 
81         foreach (i, source; singleBlocks)
82         {
83             totalCount++;
84             if (!quiet.get(false))
85                 writeln("begin single: ", i);
86             scope (exit)
87                 if (!quiet.get(false))
88                     writeln("end single: ", i);
89 
90             const status = evaluate(source, packageName, BlockType.Single, verbose.isTrue);
91             errorCount += status != 0;
92         }
93 
94         foreach (i, source; globalBlocks)
95         {
96             totalCount++;
97             if (!quiet.get(false))
98                 writeln("begin global :", i);
99             scope (exit)
100                 if (!quiet.get(false))
101                     writeln("end global :", i);
102 
103             const status = evaluate(source, packageName, BlockType.Global, verbose.isTrue);
104             errorCount += status != 0;
105         }
106 
107         if (!quiet.get(false))
108             UserIO.logInfof("Total blocks: %d", totalCount);
109         if (errorCount != 0)
110         {
111             UserIO.logErrorf("Errors: %d", errorCount);
112             return 1;
113         }
114 
115         if (!quiet.get(false))
116             UserIO.logInfof("Success all blocks.");
117         return 0;
118     }
119 }
120 
121 struct ParseResult
122 {
123     int status;
124     Code[] blocks;
125 }
126 
127 ParseResult parseMarkdown(in const(char)[] filepath)
128 {
129     import std.file : readText;
130 
131     auto text = readText(filepath);
132 
133     MD_PARSER parser;
134     parser.enter_block = (MD_BLOCKTYPE type, void* detail, void* userdata) {
135         CodeAggregator* aggregator = cast(CodeAggregator*) userdata;
136         return aggregator.enterBlock(type, detail);
137     };
138     parser.leave_block = (MD_BLOCKTYPE type, void* detail, void* userdata) {
139         CodeAggregator* aggregator = cast(CodeAggregator*) userdata;
140         return aggregator.leaveBlock(type, detail);
141     };
142     parser.enter_span = (MD_BLOCKTYPE type, void*, void*) {
143         // debug writeln("enter_span: ", type);
144         return 0;
145     };
146     parser.leave_span = (MD_BLOCKTYPE type, void*, void*) {
147         // debug writeln("leave_span: ", type);
148         return 0;
149     };
150     parser.text = (MD_TEXTTYPE type, const(MD_CHAR*) text, MD_SIZE size, void* userdata) {
151         CodeAggregator* aggregator = cast(CodeAggregator*) userdata;
152         return aggregator.text(type, text, size);
153     };
154 
155     CodeAggregator aggregator;
156     auto status = md_parse(text.ptr, cast(uint) text.length, &parser, &aggregator);
157 
158     return ParseResult(status, aggregator.codes[].array());
159 }
160 
161 struct Code
162 {
163     const(char)[] lang;
164     const(char)[] info;
165     Array!char code;
166 }
167 
168 struct CodeAggregator
169 {
170     bool isCode;
171     Code current;
172     Array!Code codes;
173 
174     int enterBlock()(MD_BLOCKTYPE type, void* detail)
175     {
176         isCode = type == MD_BLOCK_CODE;
177         if (isCode && detail !is null)
178         {
179             MD_BLOCK_CODE_DETAIL* data = cast(MD_BLOCK_CODE_DETAIL*) detail;
180             setAttribute(current.lang, data.lang, 0);
181             setAttribute(current.info, data.info, data.lang.size + 1);
182             current.code.clear();
183         }
184         return 0;
185     }
186 
187     int leaveBlock()(MD_BLOCKTYPE type, void* detail)
188     {
189         if (isCode)
190         {
191             codes.insertBack(current);
192         }
193         isCode = false;
194         return 0;
195     }
196 
197     int text()(MD_TEXTTYPE type, const(MD_CHAR*) text, MD_SIZE size)
198     {
199         if (isCode && size != 0)
200         {
201             current.code.reserve(size);
202             foreach (i; 0 .. size)
203             {
204                 current.code.insertBack(text[i]);
205             }
206         }
207         return 0;
208     }
209 }
210 
211 void setAttribute()(ref const(char)[] data, MD_ATTRIBUTE attr, size_t offset = 0) nothrow @nogc
212 {
213     import std.algorithm : min;
214 
215     if (attr.text !is null && attr.size != 0)
216     {
217         offset = min(offset, attr.size);
218         data = attr.text[offset .. attr.size];
219     }
220     else
221         data = null;
222 }
223 
224 bool isDisabledBlock(const ref Code code)
225 {
226     import std.regex : regex, matchFirst;
227 
228     auto pat = regex(`(?<=^|\s)disabled(?=\s|$)`);
229     if (auto m = matchFirst(code.info, pat))
230     {
231         return true;
232     }
233     return false;
234 }
235 
236 bool isSingleBlock(const ref Code code)
237 {
238     import std.regex : regex, matchFirst;
239 
240     auto pat = regex(`(?<=^|\s)single(?=\s|$)`);
241     if (auto m = matchFirst(code.info, pat))
242     {
243         return true;
244     }
245     return false;
246 }
247 
248 bool isGlobalBlock(const ref Code code)
249 {
250     import std.regex : regex, matchFirst;
251 
252     auto pat = regex(`(?<=^|\s)global(?=\s|$)`);
253     if (auto m = matchFirst(code.info, pat))
254     {
255         return true;
256     }
257     return false;
258 }
259 
260 string getBlockName(const ref Code code)
261 {
262     import std.regex : regex, matchFirst;
263 
264     auto pat = regex(`(?<=^|\s)name=(\w+)(?=\s|$)`);
265     if (auto m = matchFirst(code.info, pat))
266     {
267         if (m[1].length != 0)
268             return m[1].idup;
269     }
270     return "main";
271 }
272 
273 enum BlockType
274 {
275     Single,
276     Global,
277 }
278 
279 int evaluate(string source, string packageName, BlockType type, bool verbose)
280 {
281     import std.stdio : stdin, stdout, stderr;
282     import std.process : spawnProcess, wait;
283 
284     string header;
285     if (packageName)
286     {
287         import std.format : format;
288         import std.file : getcwd;
289 
290         header = format!"/+ dub.sdl:\n    dependency \"%s\" path=\"%s\"\n+/"(packageName,
291                 escapeSystemPath(getcwd()));
292     }
293     else
294     {
295         header = "/+ dub.sdl:\n +/";
296     }
297 
298     import std.file : tempDir, write, remove, mkdirRecurse, chdir;
299     import std.path : buildNormalizedPath;
300     import std : toHexString, to, text;
301 
302     auto workDir = buildNormalizedPath(tempDir(), ".md");
303     mkdirRecurse(workDir);
304 
305     import std.digest.murmurhash : MurmurHash3;
306     import std.string : representation;
307 
308     MurmurHash3!128 hasher;
309     hasher.start();
310     hasher.put(source.representation);
311     hasher.put(packageName.representation);
312     auto hash = hasher.finish();
313 
314     auto moduleName = text("md_", hash.toHexString());
315     auto filename = moduleName ~ ".d";
316     auto tempFilePath = buildNormalizedPath(workDir, filename);
317     if (verbose)
318     {
319         writeln("tempFilePath: ", tempFilePath);
320         writeln("tempFileName: ", filename);
321     }
322 
323     {
324         auto sourceFile = File(tempFilePath, "w");
325         sourceFile.writeln(header);
326         if (type == BlockType.Single)
327         {
328             sourceFile.writeln("module ", moduleName, ";");
329             sourceFile.writeln("void main() {");
330         }
331         sourceFile.writeln(source);
332         if (type == BlockType.Single)
333         {
334             sourceFile.writeln("}");
335         }
336         sourceFile.flush();
337     }
338 
339     string[] args = [
340         "dub", "run", "--quiet", "--single", "--root", workDir, filename
341     ];
342     if (verbose)
343         writeln("dub args: ", args);
344 
345     auto result = spawnProcess(args, stdin, stdout);
346     return wait(result);
347 }
348 
349 string loadCurrentProjectName()
350 {
351     import std.file : exists;
352 
353     if (exists("dub.json"))
354     {
355         import std.json : parseJSON;
356         import std.file : readText;
357 
358         auto jsonText = readText("dub.json");
359         auto json = parseJSON(jsonText);
360 
361         return json["name"].get!string();
362     }
363 
364     if (exists("dub.sdl"))
365     {
366         import std.regex : ctRegex, matchFirst;
367 
368         enum pattern = ctRegex!"^name \"(\\w+)\"$";
369         auto f = File("dub.sdl", "r");
370         foreach (line; f.byLine())
371         {
372             if (auto m = matchFirst(line, pattern))
373             {
374                 import std.conv : to;
375 
376                 return m[1].to!string();
377             }
378         }
379     }
380 
381     return null;
382 }
383 
384 string escapeSystemPath(string path)
385 {
386     import std.path : dirSeparator;
387     import std.array : replace;
388 
389     version (Windows)
390     {
391         return replace(path, dirSeparator, dirSeparator ~ dirSeparator);
392     }
393     else
394     {
395         return path;
396     }
397 }
398 
399 bool isTrue(in Nullable!bool value)
400 {
401     if (value.isNull)
402         return false;
403 
404     return value.get();
405 }