1 /**
2  * Parser for standalone ".dd" files.
3  *
4  * See_Also: dlang.org/ddoc.html ("Using Ddoc for other Documentation" section).
5  * Copyright: © 2014 Economic Modeling Specialists, Intl.
6  * Authors: Mathias Lang
7  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt Boost License 1.0)
8  */
9 module ddoc.standalone;
10 
11 import std.stdio;
12 
13 import ddoc.lexer;
14 import ddoc.sections;
15 import ddoc.macros;
16 
17 string parseFile(string path, in string[string] context) {
18 	import std.conv : to;
19 	import std.datetime : Clock;
20 	import std.file : readText;
21 	import std.path : baseName, stripExtension;
22 
23 	auto text = readText(path);
24 	// Predefined DDOC macros.
25 	// The user might want to provide his macros.
26 	// It makes sense for some (e.g. TITLE), not really for
27 	// DATETIME / YEAR, but the macros file are supposed to
28 	// override the predefinition, not the other way around.
29 	string[string] macros = context.aaDup;
30 	if (("TITLE" in macros) is null)
31 		macros["TITLE"] = baseName(path).stripExtension;
32 	if (("DATETIME" in macros) is null)
33 		macros["DATETIME"] = Clock.currTime.toSimpleString;
34 	if (("YEAR" in macros) is null)
35 		macros["YEAR"] = to!string(Clock.currTime.year);
36 	// parseDDString has to fill up COPYRIGHT
37 	if (("DOCFILENAME" in macros) is null) // FIXME ??
38 		macros["DOCFILENAME"] = path.stripExtension~".html";
39 	if (("SRCFILENAME" in macros) is null)
40 		macros["SRCFILENAME"] = baseName(path);
41 
42 	macros["BODY"] = parseDDString(text, macros);
43 	return parseDdocBody(macros);
44 }
45 
46 string parseDDString(string text, string[string] macros, bool removeUnknown = true)
47 {
48 	import ddoc.highlight;
49 	import std.string : strip;
50 	import std.algorithm : startsWith;
51 	import std.array : appender;
52 
53 	assert(text.startsWith("Ddoc"), "the string should start with 'Ddoc'");
54 	text = text[4 .. $];
55 
56 	// The doc is between "Ddoc" (which must be at the beginning of the file)
57 	// and the "Macros" sections. So first we need to find the later.
58 	// Get macros and expand them.
59 	parseMacrosSection(text, macros);
60 
61 	// Get the copyright section
62 	//auto copyright = getSection("Copyright", text, macros).content;
63 	//if (copyright !is null)
64 	//	macros["COPYRIGHT"] = copyright;
65 	text = highlight(text);
66 	auto lexer = Lexer(text, true);
67 	return expand(lexer, macros, removeUnknown);
68 }
69 
70 ///
71 unittest {
72 	import std.stdio, std.string;
73 
74 	auto text = `Ddoc
75 	This file is a $(unknown standalone) Ddoc file. It can contain any kind of
76 	$(MAC macros), defined in the $(MAC 'Macros:' section).
77 
78 Macros:
79 	MAC=$0
80 	_=
81 `;
82 
83 	auto expected1 = `This file is a  Ddoc file. It can contain any kind of
84 	macros, defined in the 'Macros:' section.`;
85 	auto expected2 = `This file is a $(unknown standalone) Ddoc file. It can contain any kind of
86 	macros, defined in the 'Macros:' section.`;
87 
88 	auto lex = Lexer(text, true);
89 	// Whitespace and newline before / after not taken into account.
90 	auto res = parseDDString(text, null).strip;
91 	assert(res == expected1, res);
92 	res = parseDDString(text, null, false).strip;
93 	assert(res == expected2, res);
94 }
95 
96 // Warning: Does not support embedded code / inlining.
97 private string parseDdocBody(string[string] macros) {
98 	auto lexer = Lexer("$(DDOC)", true);
99 	return expandMacro(lexer, macros);
100 }
101 
102 private void parseMacrosSection(ref string text, ref string[string] macros) {
103 	import std.string : indexOf;
104 	enum macSection = "\nMacros:\n";
105 	auto idx = text.indexOf(macSection);
106 	if (idx >= 0) {
107 		auto macroSection = text[idx + macSection.length .. $];
108 		KeyValuePair[] kvp;
109 		auto lex = Lexer(macroSection, true);
110 		assert(parseKeyValuePair(lex, kvp));
111 		foreach (kv; kvp) macros[kv[0]] = kv[1];
112 		text = text[0..idx];
113 	}
114 }
115 
116 // BUG #14148
117 private auto aaDup(in string[string] aa) {
118 	string[string] ret;
119 	foreach (k, v; aa)
120 		ret[k] = v;
121 	return ret;
122 }
123 
124 version (LIBDDOC_CONFIG_EXE):
125 int main(string[] args) {
126 	import std.algorithm;
127 	import std.getopt;
128 	import std.path;
129 	static import file = std.file;
130 
131 	if (args.length == 1) {
132 		stderr.writeln(`Usage: `, args[0], ` [options] [macros.ddoc]* file.dd`);
133 		stderr.writeln();
134 		stderr.writeln(`Process standalone documentation files and write them to a file.`);
135 		stderr.writeln(`'.ddoc' files are macros definition file. Order matters.`);
136 		stderr.writeln();
137 		stderr.writeln(`Options:`);
138 		stderr.writeln(`-o|--output-file=path\tOutput a (single) parsed file to 'path';`);
139 		stderr.writeln(`-D|--output-dir=dir\tOutput all parsed file(s) to directory 'dir';`);
140 	}
141 
142 	args = args[1..$];
143 	string outDir, outFile;
144 	getopt(args,
145 	       "output-dir|D", &outDir,
146 	       "output-file|o", &outFile
147 	       );
148 	auto ddocFiles = args.filter!((f) => f.extension == ".ddoc");
149 
150 	if (!args.any!((f) => f.extension == ".dd")) {
151 		stderr.writeln("No .dd file provided");
152 		return 1;
153 	}
154 
155 	bool oneFile;
156 	auto ctx = parseMacrosFile(ddocFiles);
157 	foreach (f; args) {
158 		if (f.extension == ".ddoc")
159 			continue;
160 		assert(f.extension == ".dd", "Don't know what to do with "~f);
161 		if (oneFile) {
162 			stderr.writeln("Only one .dd file allowed with o|output-file.");
163 			return 1;
164 		}
165 		oneFile = (outFile !is null);
166 		writeln("Processing file : ", f);
167 		auto data = parseFile(f, ctx);
168 		if (outFile !is null) {
169 			writeln("Writing ", data.length, " bytes to ", outFile);
170 			file.write(outFile, data);
171 		} else if (outDir !is null) {
172 			auto of = buildPath(outDir, baseName(f.setExtension(".html")));
173 			writeln("Writing ", data.length, " bytes to ", of);
174 			file.write(of, data);
175 		} else {
176 			auto of = baseName(f.setExtension(".html"));
177 			writeln("Writing ", data.length, " bytes to ", of);
178 			file.write(of, data);
179 		}
180 	}
181 	return 0;
182 }