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 }