1 module nudelo;
2 
3 import std.stdio: File;
4 
5 /// Nudelo configuration
6 struct Config
7 {
8 	/// Input file stream
9 	File inputFile;
10 	/// Output file stream
11 	File outputFile;
12 	/// Javascript processing command
13 	string[] processJavascript;
14 	/// CSS processing command
15 	string[] processCss;
16 	/// Fix unknown server-side scripts to this value
17 	string fixUnknown = "";
18 }
19 
20 private struct TagItem
21 {
22 	string tagName;
23 	string tagArgs;
24 	string tagBody;
25 
26 	pure nothrow string glue() @safe
27 	{
28 		if (tagArgs.length == 0) {
29 			return "<"~tagName~">"~tagBody~"</"~tagName~">";
30 		}
31 		return "<"~tagName~" "~tagArgs~">"~tagBody~"</"~tagName~">";
32 	}
33 }
34 
35 private struct ServerScriptItem
36 {
37 	string scriptType;
38 	string scriptBody;
39 
40 	pure nothrow string glue() @safe
41 	{
42 		return "<?"~scriptType~scriptBody~"?>";
43 	}
44 }
45 
46 private string getFileContents(File f) @trusted
47 {
48 	import std.array: appender, join;
49 	immutable fSize = f.size();
50 
51 	if (fSize == 0) return "";
52 
53 	if (fSize == ulong.max) {
54 		//string[] ret;
55 		auto ret = appender!(string[]);
56 		foreach (line; f.byLine) ret ~= line.idup;
57 		return ret.data.join;
58 	}
59 
60 	auto buffer = new ubyte[cast(size_t)(fSize)];
61 	f.rawRead(buffer);
62 	return cast(string)(buffer);
63 }
64 
65 /// minifies html file
66 void minify(Config cfg) @safe
67 {
68 	import std.array: replace;
69 	import std.regex: ctRegex, regex, replaceAll, replaceFirst;
70 
71 	if (cfg.inputFile.size == 0 || cfg.inputFile.size == ulong.max) {
72 		return;
73 	}
74 
75 	string htmlContent = cfg.inputFile.getFileContents();
76 
77 	ServerScriptItem[] server;
78 	TagItem[] pre;
79 	TagItem[] script;
80 	TagItem[] style;
81 
82 	htmlContent = htmlContent.stripServerScripts(server);
83 
84 	htmlContent = htmlContent.stripTag!("pre")(pre);
85 	htmlContent = htmlContent.stripTag!("script")(script);
86 	htmlContent = htmlContent.stripTag!("style")(style);
87 
88 	htmlContent = htmlContent
89 		.replaceChars(['\r', '\n', '\t'], [' ', ' ', ' ']) //replace line feeds and tabs with spaces
90 		.replaceAll(ctRegex!(r"<!--[^\[].+?-->"), "") //delete all comments except conditional
91 		.replace(" >", ">")
92 		.replaceAll(ctRegex!(r" +"), " ")
93 
94 		.replaceAll(regex(r"\s*<(?!(\/|a|abbr|acronym|b|bdi|bdo|big|button|cite|code|del|dfn|em|font|i|ins|kbd|mark|math|nobr|q|rt|rp|s|samp|small|span|strike|strong|sub|sup|svg|time|tt|u|var))(.*?)>\s*"), "<$1$2>")
95 
96 		.replaceFirst(ctRegex!(r"^(<!DOCTYPE.+?>)"), "$1\n")
97 	;
98 
99 	if (style.length > 0) {
100 		if (cfg.processCss.length != 0 ) {
101 			style.processTags(cfg.processCss);
102 		}
103 		htmlContent = htmlContent.insertTag!("style")(style);
104 	}
105 
106 	if (script.length > 0) {
107 		if (cfg.processJavascript.length != 0 ) {
108 			script.processTags(cfg.processJavascript);
109 		}
110 		htmlContent = htmlContent.insertTag!("script")(script);
111 	}
112 
113 	if (pre.length > 0) htmlContent = htmlContent.insertTag!("pre")(pre);
114 
115 	if (server.length > 0) {
116 		if (cfg.fixUnknown.length != 0) {
117 			server.fixServerScripts(cfg.fixUnknown);
118 		}
119 		htmlContent = htmlContent.insertServerScripts(server);
120 	}
121 
122 	cfg.outputFile.write(htmlContent);
123 }
124 
125 private string stripServerScripts(string str, ref ServerScriptItem[] pocket) @safe
126 {
127 	import std.array: replace;
128 	import std.conv: to;
129 	import std.regex: ctRegex, matchAll, regex, replaceAll;
130 
131 	foreach (m; matchAll(str, regex(r"<\?(=|\p{L}*)(.+?)\?>","s")))
132 	{
133 		pocket ~= ServerScriptItem(m[1], m[2]);
134 		str = str.replace(m[0], "[SERVER:"~to!string(pocket.length)~"]");
135 	}
136 
137 	return str;
138 }
139 
140 
141 private string insertServerScripts(string str, ServerScriptItem[] pocket) @safe
142 {
143 	import std.array: replace;
144 	import std.conv: to;
145 
146 	for (size_t i = 0; i < pocket.length; i++ )
147 	{
148 		str = str.replace("[SERVER:"~to!string(i+1)~"]", pocket[i].glue);
149 	}
150 
151 	return str;
152 }
153 private void fixServerScripts(ServerScriptItem[] pocket, string fix) @safe
154 {
155 	foreach(ref s; pocket) {
156 		if (s.scriptType.length == 0 ) {
157 			s.scriptType = fix;
158 		}
159 	}
160 }
161 
162 
163 private @safe string stripTag(string TAG)(string str, ref TagItem[] pocket)
164 {
165 	import std.array: replace;
166 	import std.conv: to;
167 	import std.regex: ctRegex, matchAll, regex, replaceAll;
168 	import std.string: strip;
169 
170 
171 	foreach (m; matchAll(str, regex(r"<"~TAG~"(\\b[^>]*)>([\\s\\S]*?)<\\/"~TAG~">","s"))) {
172 		if (m.length < 3 || m[2].length == 0) continue;
173 		pocket ~= TagItem(
174 			TAG,
175 			m[1].replaceAll(ctRegex!(r" +"), " ").strip,
176 			m[2]
177 		);
178 		str = str.replace(m[0], "["~TAG~"#"~to!string(pocket.length)~"]");
179 	}
180 
181 	return str;
182 }
183 
184 private string insertTag(string TAG)(string str, TagItem[] pocket)
185 {
186 	import std.array: replace;
187 	import std.conv: to;
188 
189 	for (size_t i = 0; i < pocket.length; i++ ) {
190 		str = str.replace("["~TAG~"#"~to!string(i+1)~"]", pocket[i].glue);
191 	}
192 
193 	return str;
194 }
195 
196 private string replaceChars(string src, char[] from, char[] to) @trusted
197 {
198 	auto str = cast(ubyte[])(src);
199 	foreach (ref c; str) {
200 		for (size_t i = 0; i < from.length; i++) {
201 			if (c == from[i]) c = to[i];
202 		}
203 	}
204 	return cast(string)(str);
205 }
206 
207 private void processTags(ref TagItem[] js, string[] command) @safe
208 {
209 	import std.array: join;
210 	import std.process: pipeProcess, wait;
211 
212 	foreach (ref tag; js) {
213 		auto pipes = pipeProcess(command);
214 		pipes.stdin.write(tag.tagBody);
215 		pipes.stdin.flush();
216 		pipes.stdin.close();
217 
218 		immutable ret = wait(pipes.pid);
219 		if (ret == 0) {
220 			tag.tagBody = pipes.stdout.getFileContents();
221 		} else {
222 			reportErrorTag(command.join(), tag.tagBody, pipes.stderr);
223 		}
224 	}
225 }
226 
227 private void reportErrorTag(string command, string src, File err) @trusted
228 {
229 	import std.algorithm.iteration: each;
230 	import std.stdio: stderr, writefln, writeln;
231 	import std.string: lineSplitter;
232 
233 	writefln("--- '%s' error:", command);
234 	lineSplitter(src).each!(s => stderr.writeln("> ", s));
235 	stderr.writeln("\n", err.getFileContents(), "\n");
236 }