Merge pull request #506 from andrewcsmith/patch-2
[supercollider.git] / examples / research_and_tools / html-help-color-fixer.scd
blob8b904ec67f5803cae716bb5da5b659aecdac792e
1 // html help color correction -- James Harkins
3 /*
4 Purpose:
5 OSX applies a color correction profile to HTML files as they are opened in SC. Then, when the document is resaved to disk, the colors are slightly altered. Over time, they fade. This utility scans the HTML and replaces colors with the standard OSX syntax-colorize scheme.
7 Usage:
8 This code loads all the functions into an environment saved in the global library. In OSX, a menu item is added to fix the colors in the current open document -- just make sure the document is the frontmost window and go to the Library menu > HTML Color Fix to process that document. It must be an HTML document.
10 To use the commandline utility, you need to enter the environment first.
12 1. Push the environment.
13 2. Set parameters using the environment variable names at the top of the code block.
14 3. Execute "~go.value" to scan all .html files in the root directory and subdirectories (recursively).
15 4. Pop the environment.
17 e.g.,
19 Library.at(\colorFix).push;
21 ~root = "... path to source file directory...";
23         // outRoot is for non-destructive writing (the default)
24         // should be different from root
25         // if using destructive writing, outRoot is ignored
26 ~outRoot = "... path to output file directory...";
28         // either \writeFileNonDestructive or \writeFileDestructive
29 ~operation = \writeFileNonDestructive;
31         // if using destructive writing, this could be false
32         // but it should be true for non-destructive
33         // otherwise your target directory will be missing unchanged files
34 ~alwaysWrite = true;
36 ~go.value;              // run it!
38 Environment.pop;
43 Library.put(\colorFix, Environment.make {
45 // these are the real colors, from SuperCollider.app syntax colorize
46 ~red = "bf0000";
47 ~green = "007300";
48 ~blue = "0000bf"; 
49 ~gray = "606060";
50 ~signatures = [~red, ~green, ~blue];
52 ~reportErrors = false;
53 ~haltOnError = false;
54 ~alwaysWrite = true;    // recommended for non-destructive
55 ~operation = \writeFileNonDestructive;
56 ~root = "";
57 ~outRoot = "";
59 ~go = {
60         if(~root.size > 0) {
61                 ~scanDirectoryRecursive.(~root, ~root, ~outRoot, ~operation)
62         } {
63                 "Set ~root and ~outRoot before running.".postln;
64         };
67 ~scanDirectoryRecursive = { |path, root, outRoot, op|
68         var     dirs = (path ++ "/*/").pathMatch, success = true;
69         dirs.reject({ |dir| dir.basename[0] == $. }).do({ |dir|
70                 if(~scanDirectoryRecursive.value(dir[ .. dir.size-2], root, outRoot, op).not) { success = false };
71         });
72         if(~scanDirectory.value(path, root, outRoot, op).not) { success = false };
73         success
76 ~scanDirectory = { |path, root, outRoot, op|
77         var     files = (path ++ "/*.html").pathMatch, success = true;
78         files.reject({ |file| file.basename[0] == $. }).do({ |filepath|
79                 filepath.debug("processing");
80                 if(~scanFile.value(filepath, root, outRoot, op).not) { success = false };
81         });
82         success
85 ~scanFile = { |path, root, outRoot, op|
86         var file = File(path, "r"), outFile, contents, stream, success = true,
87                 outPath, outDir;
88         var     numFixes;
89         if(file.isOpen) {
90                 protect {
91                         contents = file.readAllString;
92                 } { file.close };
93                 stream = CollStream(contents);
94                 numFixes = Ref(0);      // pass by reference
95                 if(~scanStream.(stream, numFixes)) {
96                                 // write the file only if anything changed
97                         if(~alwaysWrite or: { numFixes.value > 0 }) {
98                                 op.envirGet.value(stream, path, root, outRoot);
99                         };
100                 } {
101                         "Error occurred while scanning %. Output file was not written.".format(path).warn;
102                         success = false;
103                 };
104         } {
105                 "Could not open file %.".format(path).warn;
106                 success = false;
107         };
108         success
111 ~writeFileNonDestructive = { |stream, path, root, outRoot|
112         ~writeFile.(stream, outRoot +/+ path[root.size ..], true);
115 ~writeFileDestructive = { |stream, path|
116         ~writeFile.(stream, path, false);
119 ~writeFile = { |stream, outPath, checkDir = false|
120         var     outDir, outFile, success = true;
121         if(checkDir) {
122                 outDir = outPath.dirname;
123                 if(File.exists(outDir).not) {
124                                 // if it fails, you'll get "Could not open output file" warnings
125                                 // need to use the blocking systemCmd method
126                         "mkdir %".format(outDir.escapeChar($ )).systemCmd;
127                 };
128         };
129         if((outFile = File(outPath, "w")).isOpen) {
130                 protect {
131                         outFile.putAll(stream.contents)
132                 } { outFile.close };
133         } {
134                 "Could not open output file %.".format(outPath).warn;
135                 success = false;
136         };
137         success
138 };      
140 ~scanStream = { |stream, numFixes|
141         var     keepGoing = true;
142         
143         try {
144                 while { keepGoing and: { ~scanToColorKey.(stream) } } {
145                         keepGoing = ~fixColor.(stream, numFixes); 
146                 };
147                 true    // == success
148         } { |error|
149                 if(~haltOnError) { error.throw };
150                 if(~reportErrors) { error.reportError };
151                 false   // == fail
152         };
155 // leaves stream at the position of the first hex digit of the 24-bit color
156 ~scanToColorKey = { |stream|
157         var     ch, lastCh, temp, stillSearching = true, notEOF = true;
158         ~fseek.(stream, -1, 1);
159         lastCh = stream.getChar;
160         while {
161                 if(stillSearching) {
162                         lastCh = ch;
163                         (ch = stream.getChar).notNil
164                 } { false }
165         } {
166                 if(lastCh != $- and: { ch == $c or: { ch == $C } }) {
167                         temp = try { stream.nextN(5) }
168                                         // on error, need to stop searching AND return some string (anything)
169                                 { stillSearching = notEOF = false; "fail" };
170                         lastCh = temp.last;
171                         if(temp.collect(_.toLower) == "olor:") {
172                                         // skip spaces after :
173                                 while { lastCh = ch; (ch = stream.getChar).isSpace };
174                                         // is this a #? if yes, we're done
175                                 if(ch == $#) {
176                                         stillSearching = false;
177                                 } {
178                                                 // otherwise, back up one and resume from the non-space char
179                                         ~fseek.(stream, -2, 1);
180                                         lastCh = stream.getChar;
181                                 };
182                         } {
183                                         // olor: not matched, go back to the "o"
184                                 ~fseek.(stream, -6, 1);
185                                 lastCh = stream.getChar;
186                         };
187                 };      // else: keep scanning with next char
188         };
189         notEOF and: { ch.notNil }       // anything left?
190 };              
192 // assumes stream is at the position of a 6-digit color
193 ~fixColor = { |stream, numFixes|
194         var     colorStr, color, signature;
195         colorStr = try { stream.nextN(6) } { "" };
196         if(colorStr.size == 6) {
197                         // if any of the 6 chars is not a digit, an error gets thrown here
198                         //  and caught by ~scanStream
199                 color = colorStr.collectAs(_.digit, Array);
200                         // they all must be hex digits
201                 if(color.every(_ < 16)) {
202                         color = color.clump(2)
203                                 .collect({ |row| (row[0] << 4) | row[1] });
204                         signature = color.maxIndex;
205                         if(colorStr != ~signatures[signature] and:
206                                         { signature != 0 or: { color.differentiate[1..].abs.sum != 0 } }) {
207                                 ~fseek.(stream, -6, 1);
208                                 ~signatures[signature].do({ |ch| stream.put(ch) });
209                                 numFixes.value = numFixes.value + 1;
210                         };
211                 };
212                         // we must return true even if non-hex digits found
213                         // because in this func, 'false' means we hit the end of the file
214                 true
215         } { false };    // if the color string was too short, we're at EOF
219 // ghastly workaround because interface of IOStream is not good enough
220 ~fseek = { |stream, offset = 0, origin = 1|
221         if(stream.isKindOf(File)) {
222                 stream.seek(offset, origin)
223         } {
224                 switch(origin)
225                         { 0 } { stream.pos = offset }
226                         { 1 } { stream.pos = stream.pos + offset }
227                         { 2 } { stream.pos = stream.collection.size + offset }
228                         { Error("Seek on CollStream failed, origin % not valid.".format(origin)).throw }
229         };
230         stream
234 // menu item for OSX
235 ~cocoaMenu = 'CocoaMenuItem'.asClass;
236 if(~cocoaMenu.notNil) {
237         ~checkSaved = {
238                 var     w, continue, sb = Window.screenBounds;
239                 if(Document.current.path.splitext.last.collect(_.toLower)[0..2] == "htm") {
240                         if(Document.current.isEdited) {
241                                 ~confirmWrite.(
242                                         "This document is not yet saved.\nPlease save first, then confirm action.\nYES will overwrite the existing disk file!",
243                                         {       if(Document.current.isEdited) {
244                                                         "Naughty! You clicked YES without saving! Color fix not done.".postln;
245                                                         false
246                                                 } { true }
247                                         }
248                                 );
249                         } {
250                                 ~confirmWrite.("Confirm whether or not to execute the fix.\nYES will overwrite the existing disk file!", true);
251                         };
252                 } {
253                         "Not an HTML file. Cannot proceed.".postln;
254                 };
255         };
256         
257         ~confirmWrite = { |prompt, checkFunc = true|
258                 var     w, c,
259                         bigFont = Font(GUI.skin.fontSpecs[0], 28);
260                 var     docPath;
261                 var     continue, sb = Window.screenBounds;
263                 w = Window("Confirm HTML color fix",
264                         Rect.aboutPoint(sb.extent * 0.5, 200, 150));
265                 c = CompositeView(w, w.view.bounds.insetBy(25, 25));
266                 c.decorator = FlowLayout(c.bounds);
267                 prompt.split($\n).do { |line|
268                         StaticText(c, (c.bounds.width - 5)@17)
269                                 .align_(\center)
270                                 .string_(line);
271                 };
272                 StaticText(c, 50@25);
273                 c.decorator.nextLine;
274                 Button(c, ((c.bounds.width - 50) * 0.5)@50)
275                         .font_(bigFont)
276                         .states_([["YES"]])
277                         .action_(inEnvir {
278                                 if(checkFunc.value) {
279                                         w.close;
280                                         fork({
281                                                 var     saveAlwaysWrite = ~alwaysWrite;
282                                                 docPath = Document.current.path;
283                                                 Document.current.close;
284                                                 0.01.wait;
285                                                         // writing in place, don't need to write if no changes
286                                                 ~alwaysWrite = false;
287                                                 ~scanFile.(docPath, "", "", \writeFileDestructive);
288                                                 ~alwaysWrite = saveAlwaysWrite;
289                                                 0.1.wait;
290                                                 Document.open(docPath);
291                                         }, AppClock);
292                                 } { w.close }; // checkFunc should have posted a message
293                         });
294                 StaticText(c, 30@50);   // spacer
295                 Button(c, ((c.bounds.width - 50) * 0.5)@50)
296                         .font_(bigFont)
297                         .states_([["NO"]])
298                         .action_({ w.close; "Color fix canceled.".postln });
299                 w.front;
300         };
301         
302         ~cocoaMenu.add(["HTML Color Fix"], {
303                 Library.at(\colorFix).use {
304                         ~checkSaved.()
305                 };
306         })
310 "HTML color fixer loaded into Library.at(\\colorFix).".postln; ""