1 #!/usr/bin/env dmd -run 2 3 module app; 4 5 import std.algorithm; 6 import std.exception; 7 import std.format; 8 import std.getopt; 9 import std.path; 10 import std.range; 11 import std.string; 12 import std.stdio : println = writeln; 13 14 struct Options 15 { 16 struct Defaults 17 { 18 enum templatePath = "template.html"; 19 enum outputPath = "index.html"; 20 enum slidesOrderingPath = "slides_ordering.txt"; 21 } 22 23 bool help; 24 25 string templatePath = Defaults.templatePath; 26 string outputPath = Defaults.outputPath; 27 string slidesOrderingPath = Defaults.slidesOrderingPath; 28 string[] slides; 29 } 30 31 struct Slide 32 { 33 string filename; 34 string content; 35 36 bool isValid() 37 { 38 return filename.length > 0; 39 } 40 } 41 42 auto readSlidesOrder(string path) 43 { 44 return readFile(path).slidesOrder; 45 } 46 47 auto slidesOrder(string slidesOrderingContent) 48 { 49 return slidesOrderingContent 50 .lineSplitter 51 .filter!(line => !line.empty); 52 } 53 54 unittest 55 { 56 assert(slidesOrder("foo.md\nbar.md").equal(["foo.md", "bar.md"])); 57 58 // with extra empty newline 59 assert(slidesOrder("foo.md\nbar.md\n\n").equal(["foo.md", "bar.md"])); 60 } 61 62 auto readSlides(string[] paths) 63 { 64 return paths.map!(path => Slide(path, readFile(path))); 65 } 66 67 auto sort(SlideRange, OrderRange)(SlideRange slides, OrderRange order) 68 if ( 69 is(ElementType!SlideRange == Slide) && 70 is(ElementType!OrderRange == string) 71 ) 72 { 73 auto equalBaseName(string key) 74 { 75 auto slide = slides.find!(e => e.filename.baseName == key); 76 return slide.empty ? Slide() : slide.front; 77 } 78 79 return order 80 .map!(equalBaseName) 81 .filter!(e => e.isValid); 82 } 83 84 unittest 85 { 86 auto slides = [Slide("test/foo.md"), Slide("test/bar.md")]; 87 auto order = ["bar.md", "foo.md"]; 88 89 auto expedted = [Slide("test/bar.md"), Slide("test/foo.md")]; 90 assert(slides.sort(order).equal(expedted)); 91 } 92 93 // with missing slides 94 unittest 95 { 96 auto slides = [Slide("test/foo.md"), Slide("test/bar.md")]; 97 auto order = ["bar.md", "a.md", "foo.md"]; 98 99 auto expedted = [Slide("test/bar.md"), Slide("test/foo.md")]; 100 assert(slides.sort(order).equal(expedted)); 101 } 102 103 string stripSlideSeparator(string str) 104 { 105 import std.regex; 106 enum regex = ctRegex!r"(\s|---)+$"; 107 108 return str.replaceFirst(regex, ""); 109 } 110 111 unittest 112 { 113 assert("foo".stripSlideSeparator == "foo"); 114 assert("foo ".stripSlideSeparator == "foo"); 115 assert("foo\n\n\n".stripSlideSeparator == "foo"); 116 assert("foo\n---\n".stripSlideSeparator == "foo"); 117 assert("foo\n\n---\n\n".stripSlideSeparator == "foo"); 118 assert("foo\n\n---\n\n\n\n---\n\n".stripSlideSeparator == "foo"); 119 } 120 121 auto combine(Range)(Range slides) 122 if (is(ElementType!Range == Slide)) 123 { 124 return slides 125 .map!(e => e.content) 126 .map!(stripSlideSeparator) 127 .joiner("\n---\n"); 128 } 129 130 unittest 131 { 132 auto slides = [Slide("", "foo\n"), Slide("", "bar\n---")]; 133 assert(slides.combine.equal("foo\n---\nbar")); 134 } 135 136 string generateTemplate(Range)(Range data, string templatePath) 137 { 138 import std.conv : to; 139 140 return readFile(templatePath).evaluateTemplate(data.to!string); 141 } 142 143 string evaluateTemplate(string templateData, string slidesData) 144 { 145 enum placeholder = "{{ slides }}"; 146 return templateData.replace(placeholder, slidesData); 147 } 148 149 unittest 150 { 151 enum templateData = q"HTML 152 <textarea id="source"> 153 {{ slides }} 154 </textarea> 155 HTML"; 156 157 enum expected = q"HTML 158 <textarea id="source"> 159 foo 160 </textarea> 161 HTML"; 162 163 assert(templateData.evaluateTemplate("foo") == expected); 164 } 165 166 void output(string data, string path) 167 { 168 import std.file : write; 169 write(path, data); 170 } 171 172 string readFile(string path) 173 { 174 import std.file : read; 175 import std.conv : to; 176 177 return read(path).to!string; 178 } 179 180 struct Application 181 { 182 string[] args; 183 184 void run() 185 { 186 import core.stdc.stdlib : exit, EXIT_FAILURE; 187 debug 188 _run(); 189 else 190 { 191 try 192 _run(); 193 catch (Exception e) 194 { 195 println(e.msg); 196 exit(EXIT_FAILURE); 197 } 198 } 199 } 200 201 private: 202 203 Options parseCli() 204 { 205 enum usage = "Usage: remarkify [options] <slides/*.md> 206 207 Remarkify will combine Markdown files to be used together with remark. It takes 208 a list of Markdown files, combines the content of those and outputs the result 209 to an HTML file. 210 211 An HTML template file is used to generate the final result. This can be any file 212 containing: \033[1m{{ slides }}\033[0m. \033[1m{{ slides }}\033[0m acts as a placeholder and will be 213 replaced with the combined Markdown files. The template is expected to contain 214 the HTML boilerplate necessary for remakr. 215 216 A plain text file containing the ordering of the slides is necessary. The file 217 should contain the basename (without the directory path) of the Markdown files, 218 one file per line.\n"; 219 220 Options options; 221 222 auto getoptResult = getopt( 223 args, 224 "template|t", format("The path to the HTML template to use (defaults to %s).", Options.Defaults.templatePath), &options.templatePath, 225 "output|o", format("Put the result in this file (defaults to %s).", Options.Defaults.outputPath), &options.outputPath, 226 "slides-ordering|s", format("A file contaning the order of the slides, one filename per line (defaults to %s).", Options.Defaults.slidesOrderingPath), &options.slidesOrderingPath 227 ); 228 229 if (getoptResult.helpWanted) 230 { 231 defaultGetoptPrinter(usage, getoptResult.options); 232 return Options(true); 233 } 234 235 options.slides = args[1 .. $]; 236 enforce(!options.slides.empty, "No Markdown files given"); 237 238 return options; 239 } 240 241 void _run() 242 { 243 auto options = parseCli(); 244 245 if (options.help) 246 return; 247 248 auto slidesOrder = readSlidesOrder(options.slidesOrderingPath); 249 250 readSlides(options.slides) 251 .sort(slidesOrder) 252 .combine 253 .generateTemplate(options.templatePath) 254 .output(options.outputPath); 255 } 256 } 257 258 version (unittest) 259 void main() {} 260 else 261 262 void main(string[] args) 263 { 264 Application(args).run(); 265 }