1 // html help color correction -- James Harkins
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.
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.
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
43 Library.put(\colorFix, Environment.make {
45 // these are the real colors, from SuperCollider.app syntax colorize
50 ~signatures = [~red, ~green, ~blue];
52 ~reportErrors = false;
54 ~alwaysWrite = true; // recommended for non-destructive
55 ~operation = \writeFileNonDestructive;
61 ~scanDirectoryRecursive.(~root, ~root, ~outRoot, ~operation)
63 "Set ~root and ~outRoot before running.".postln;
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 };
72 if(~scanDirectory.value(path, root, outRoot, op).not) { success = false };
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 };
85 ~scanFile = { |path, root, outRoot, op|
86 var file = File(path, "r"), outFile, contents, stream, success = true,
91 contents = file.readAllString;
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);
101 "Error occurred while scanning %. Output file was not written.".format(path).warn;
105 "Could not open file %.".format(path).warn;
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;
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;
129 if((outFile = File(outPath, "w")).isOpen) {
131 outFile.putAll(stream.contents)
134 "Could not open output file %.".format(outPath).warn;
140 ~scanStream = { |stream, numFixes|
141 var keepGoing = true;
144 while { keepGoing and: { ~scanToColorKey.(stream) } } {
145 keepGoing = ~fixColor.(stream, numFixes);
149 if(~haltOnError) { error.throw };
150 if(~reportErrors) { error.reportError };
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;
163 (ch = stream.getChar).notNil
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" };
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
176 stillSearching = false;
178 // otherwise, back up one and resume from the non-space char
179 ~fseek.(stream, -2, 1);
180 lastCh = stream.getChar;
183 // olor: not matched, go back to the "o"
184 ~fseek.(stream, -6, 1);
185 lastCh = stream.getChar;
187 }; // else: keep scanning with next char
189 notEOF and: { ch.notNil } // anything left?
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;
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
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)
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 }
235 ~cocoaMenu = 'CocoaMenuItem'.asClass;
236 if(~cocoaMenu.notNil) {
238 var w, continue, sb = Window.screenBounds;
239 if(Document.current.path.splitext.last.collect(_.toLower)[0..2] == "htm") {
240 if(Document.current.isEdited) {
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;
250 ~confirmWrite.("Confirm whether or not to execute the fix.\nYES will overwrite the existing disk file!", true);
253 "Not an HTML file. Cannot proceed.".postln;
257 ~confirmWrite = { |prompt, checkFunc = true|
259 bigFont = Font(GUI.skin.fontSpecs[0], 28);
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)
272 StaticText(c, 50@25);
273 c.decorator.nextLine;
274 Button(c, ((c.bounds.width - 50) * 0.5)@50)
278 if(checkFunc.value) {
281 var saveAlwaysWrite = ~alwaysWrite;
282 docPath = Document.current.path;
283 Document.current.close;
285 // writing in place, don't need to write if no changes
286 ~alwaysWrite = false;
287 ~scanFile.(docPath, "", "", \writeFileDestructive);
288 ~alwaysWrite = saveAlwaysWrite;
290 Document.open(docPath);
292 } { w.close }; // checkFunc should have posted a message
294 StaticText(c, 30@50); // spacer
295 Button(c, ((c.bounds.width - 50) * 0.5)@50)
298 .action_({ w.close; "Color fix canceled.".postln });
302 ~cocoaMenu.add(["HTML Color Fix"], {
303 Library.at(\colorFix).use {
310 "HTML color fixer loaded into Library.at(\\colorFix).".postln; ""