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 }