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 }