common: win32utils - compile fix
[supercollider.git] / SCClassLibrary / SCDoc / SCDoc.sc
blob4d5be555705df8be53387db23960d9d6adbdabbd
1 SCDoc {
2     // Increment this whenever we make a change to the SCDoc system so that all help-files should be processed again
3     classvar version = 24;
5     classvar <helpTargetDir;
6     classvar <helpSourceDir;
7     classvar helpSourceDirs;
8     classvar doc_map = nil;
9     classvar <p, <r;
10     classvar <undocumentedClasses;
11     classvar <>verbose = false;
12     classvar doWait;
13     classvar progressCount = 0, progressMax = 0;
14     classvar new_classes = nil;
15     classvar didRun = false;
16     classvar isProcessing = false;
17     classvar lastUITick = 0;
19     *helpSourceDir_ {|path|
20         helpSourceDir = path.standardizePath;
21     }
23     *helpSourceDirs {
24         this.findHelpSourceDirs;
25         ^helpSourceDirs;
26     }
28     *helpTargetDir_ {|path|
29         if(path!=helpTargetDir) {didRun = false};
30         helpTargetDir = path.standardizePath;
31     }
33     *postProgress {|string|
34         var prg = "";
35         if(progressMax>0) {prg = (progressCount/progressMax*100).round(0.1).asString ++ "% "};
36         if(verbose) {("SCDoc:"+prg++string).postln};
37         this.maybeWait;
38     }
40     *docMapToJSON {|path|
41         var f;
43         File.delete(path);
44         f = File.open(path,"w");
46         f.write("docmap = {\n");
47         doc_map.pairsDo {|k,v|
48             f.write("'"++k++"':"+"{\n");
49             v.pairsDo {|k2,v2|
50                 if(v2.isKindOf(Array)) {
51                     v2 = "["+v2.collect{|x|"'"++x++"'"}.join(",")+"]";
52                 } {
53                     v2 = "'"++v2.asString.replace("'","\\'")++"'";
54                 };
55                 f.write("'"++k2++"': "++v2++",\n");
56             };
58             f.write("},\n");
59         };
60         f.write("}\n");
61         f.close;
62     }
64     *splitList {|txt|
65         ^txt.findRegexp("[^, ]+[^,]*[^, ]+").flop[1];
66     }
68     *initClass {
69         this.helpSourceDir_(thisProcess.platform.classLibraryDir.dirname +/+ "HelpSource");
70         this.helpTargetDir_(thisProcess.platform.userAppSupportDir +/+ "Help");
71         r = SCDocHTMLRenderer.new;
72         p = SCDocParser.new;
73         doWait = false;
74     }
76     *addMethodList {|c,n,tag|
77         var l, x = this.makeMethodList(c);
78         if(x.notEmpty) {
79             l = x.collect{|m| (tag:\method, text:m)};
80             n = n.add((tag:tag, children:l));
81         };
82         ^n;
83     }
85     *makeMethodList {|c|
86         var l, mets, name, syms;
87         l = Array.new;
88         (mets = c.methods) !? {
89             syms = mets.collectAs(_.name,IdentitySet);
90             mets.do {|m| //need to iterate over mets to keep the order
91                 name = m.name;
92                 if (name.isSetter.not or: {syms.includes(name.asGetter).not}) {
93                     l = l.add(name.asGetter.asString);
94                 };
95             };
96         };
97         ^l;
98     }
100     *classHasArKrIr {|c|
101         ^#[\ar,\kr,\ir].collect {|m| c.class.findRespondingMethodFor(m).notNil }.reduce {|a,b| a or: b};
102     }
104     *addToDocMap {|parser, path|
105         var folder = path.dirname, classname = parser.findNode(\class).text;
106         var doc = (
107             path:path,
108             summary:parser.findNode(\summary).text,
109             categories:parser.findNode(\categories).text
110         );
112         doc[\title] = if(classname.notEmpty,classname,{parser.findNode(\title).text});
114         if(doc[\title].isEmpty) {
115             doc[\title] = "NO TITLE:"+path;
116             warn("Document at"+path+"has no title:: or class::");
117         };
118         if(doc[\summary].isEmpty) {
119             warn("Document at"+path+"has no summary::");
120         };
121         if(doc[\categories].isEmpty) {
122             warn("Document at"+path+"has no categories::");
123         };
125         if(classname.notEmpty) {
126             if(path.basename != doc[\title]) {
127                 warn("Document at"+path+"is not named according to class name:"+doc[\title]);
128             };
129             if(folder != "Classes") {
130                 warn("Document at"+path+"is a class doc but is not in Classes/ folder");
131             };
132         } {
133             if(folder == "Classes") {
134                 warn("Document at"+path+"is not a class doc but is in Classes/ folder");
135             };
136         };
138         doc_map[path] = doc;
139     }
141     *makeCategoryMap {
142         var cats, c, map;
143         this.postProgress("Creating category map...");
144         map = Dictionary.new;
145         this.docMap.pairsDo {|k,v|
146             cats = this.splitList(v.categories);
147             cats = cats ? ["Uncategorized"];
148             cats.do {|cat|
149                 if (map[cat].isNil) {
150                     map[cat] = List.new;
151                 };
152                 map[cat].add(v);
153             };
155         };
156         ^map;
157     }
159     *checkVersion {
160         var f, path = this.helpTargetDir +/+ "version";
161         if(path.load != version) {
162             // FIXME: should we call this.syncNonHelpFiles here to ensure that helpTargetDir exists?
163             this.postProgress("version update, refreshing timestamp");
164             // this will update the mtime of the version file, triggering re-rendering and clean doc_map
165             f = File.open(path,"w");
166             f.write(version.asCompileString);
167             f.close;
168             ^true;
169         };
170         ^false;
171     }
173     *readDocMap {
174         var path = this.helpTargetDir +/+ "scdoc_cache";
175         var verpath = this.helpTargetDir +/+ "version";
177         if(this.checkVersion or: {("test"+verpath.shellQuote+"-nt"+path.shellQuote).systemCmd==0}) {
178             this.postProgress("not reading scdoc_cache due to version timestamp update");
179             doc_map = nil;
180         } {
181             doc_map = path.load;
182         };
184         if(doc_map.isNil) {
185             doc_map = Dictionary.new;
186             ^true;
187         };
188         ^false;
189     }
191     *writeDocMap {
192         var f, path = this.helpTargetDir +/+ "scdoc_cache";
193         File.delete(path);
194         f = File.open(path,"w");
195         f.write(doc_map.asCompileString);
196         f.close;
197     }
199     *docMap {
200         if(doc_map.isNil) {
201             this.getAllMetaData;
202         };
203         ^doc_map;
204     }
206     *tickProgress { progressCount = progressCount + 1 }
208     *maybeWait {
209         var t;
210         if(doWait and: {(t = Main.elapsedTime)-lastUITick > 0.1}) {
211             0.wait;
212             lastUITick = t;
213         }
214     }
216     *parseAndRender {|src,dest,subtarget|
217         var p2;
218         SCDoc.postProgress(src+"->"+dest);
219         p.parseFile(src);
221         // hackish workaround: if a doc addition is removed, it is not detected
222         // and removed from the 'additions' set in the docmap entry.
223         // so here we check that the file exists..
224         doc_map[subtarget][\additions].select(File.exists(_)).do {|ext|
225             p2 = p2 ?? {p.class.new};
226             p2.parseFile(ext);
227             p.merge(p2);
228         };
230         r.render(p,dest,subtarget);
231     }
233     *renderAll {|force=false,threaded=true,findExtensions=true,doneFunc|
234         var count, count2, func, x, fileList, subtarget, dest, t = Main.elapsedTime, oldVerbose = verbose;
236         func = {
237             progressMax = 100;
238             progressCount = 0;
239             verbose = true;
241             this.cleanState(force);
242             if(findExtensions) {
243                 this.findHelpSourceDirs;
244             } {
245                 helpSourceDirs = Set[helpSourceDir];
246             };
247             this.tickProgress;
248             this.getAllMetaData;
249             this.tickProgress;
251             fileList = Dictionary.new;
252             count = 0;
253             this.postProgress("Updating all files");
254             helpSourceDirs.do {|dir|
255                 fileList[dir] = ("find -L"+dir.shellQuote+"-type f -name '*.schelp' -not -name '*.ext.schelp'")
256                     .unixCmdGetStdOutLines.reject(_.isEmpty).asSet;
257                 count = count + fileList[dir].size;
258             };
259             count2 = undocumentedClasses.size;
260             progressCount = 3 * ((count+count2)/progressMax);
261             progressMax = count+count2+progressCount;
263             this.postProgress("Found"+count+"help files");
264             helpSourceDirs.do {|dir|
265                 x = fileList[dir].size;
266                 if(x>0) {
267                     fileList[dir].do {|path|
268                         subtarget = path[dir.size+1 .. path.findBackwards(".")?path.size-1];
269                         dest = helpTargetDir+/+subtarget++".html";
270                         if(force
271                         or: {("test"+path.shellQuote+"-nt"+dest.shellQuote+"-o ! -e"+dest.shellQuote).systemCmd==0}) {
272                             this.parseAndRender(path,dest,subtarget);
273                         } {
274                             this.postProgress("Skipping"+dest);
275                         };
276                         this.tickProgress;
277                     };
278                 };
279             };
280             this.postProgress("Found"+count2+"undocumented classes");
281             undocumentedClasses.do {|name|
282                 dest = helpTargetDir+/+"Classes"+/+name++".html";
283                 if(this.makeClassTemplate(name.asString,dest).not) {
284                     this.postProgress("Skipping"+dest);
285                 };
286                 this.tickProgress;
287             };
288             this.postProgress("Done! time spent:"+(Main.elapsedTime-t)+"sec");
289             verbose = oldVerbose;
290             doneFunc.value();
291         };
293         doWait = threaded;
294         if(doWait, {
295             Routine(func).play(AppClock);
296         }, func);
297     }
299     *cleanState {|noCache=false|
300         didRun = false;
301         helpSourceDirs = nil;
302         if(noCache) {
303             doWait = thisThread.isKindOf(Routine);
304             this.syncNonHelpFiles; // ensure helpTargetDir exists
305             ("touch"+(helpTargetDir+/+"version").shellQuote).systemCmd;
306         }
307     }
309     *findHelpSource {|subtarget|
310         var src;
311         this.findHelpSourceDirs;
312         block {|break|
313             helpSourceDirs.do {|dir|
314                 var x = dir+/+subtarget++".schelp";
315                 if(File.exists(x)) {
316                     src = x;
317                     break.value;
318                 };
319             };
320         };
321         ^src
322     }
324     *prepareHelpForURL {|url|
325         var proto, path, query, anchor;
326         var subtarget, src, c, cmd;
327         var verpath = this.helpTargetDir +/+ "version";
329         doWait = thisThread.isKindOf(Routine);
331         if(isProcessing) {
332             "SCDoc: prepareHelpForURL already running.. waiting for the first to finish.".warn;
333             if(doWait.not) {
334                 Error("SCDoc: cannot wait for already running prepareHelpForURL, this call was not made inside a Routine").throw;
335             };
336             c = Condition.new;
337             Routine {
338                 while {0.5.wait; isProcessing};
339                 c.unhang;
340             }.play(AppClock);
341             c.hang;
342         };
343         protect {
344             isProcessing = true;
346             // parse URL
347             url = url.replace("%20"," ");
348             #proto, path, query, anchor = url.findRegexp("(^\\w+://)?([^#?]+)(\\?[^#]+)?(#.*)?")[1..].flop[1];
349             if(proto.isEmpty) {proto="file://"};
350             if(proto!="file://") {isProcessing = false; ^url}; // just pass through remote url's
351             if(path.beginsWith(helpTargetDir).not) {isProcessing = false; ^url}; // just pass through remote url's
353             if(File.exists(helpTargetDir).not) {
354                 this.cleanState;
355             };
357             // sync non-schelp files once every session
358             if(didRun.not) {
359                 didRun = true;
360                 this.getAllMetaData;
361             };
363             // strip to subfolder/basename (like Classes/SinOsc)
364             subtarget = path[helpTargetDir.size+1 .. path.findBackwards(".")?path.size-1];
366             // find help source file
367             src = this.findHelpSource(subtarget);
369             // create a simple stub if class was undocumented
370             if(src.isNil and: {subtarget.dirname=="Classes"}) {
371                 this.makeClassTemplate(subtarget.basename,path);
372             };
374             if(File.exists(path).not) {
375                 if(src.notNil) { // no target file, but helpsource found, parse and render.
376                     this.parseAndRender(src,path,subtarget);
377                     isProcessing = false;
378                     ^url;
379                 } {
380                     this.postProgress("Broken link:"+url);
381                     isProcessing = false;
382                     ^nil;
383                 };
384             } {
385                 cmd = {
386                     ("test" + ([src,verpath]++doc_map[subtarget][\additions]).collect {|x|
387                         x.shellQuote+"-nt"+path.shellQuote
388                     }.join(" -o ")).systemCmd == 0
389                 };
391                 if(src.notNil and: cmd) {
392                     // target file and helpsource exists, and helpsource is newer than target
393                     this.parseAndRender(src,path,subtarget);
394                     isProcessing = false;
395                     ^url;
396                 };
397                 // we reach here if the target URL exists and there's no schelp source file,
398                 // or if the target URL does not need update.
399             };
400         } {
401             isProcessing = false;
402         };
403         ^url;
404     }
406     *makeClassTemplate {|name,path|
407         var class = name.asSymbol.asClass;
408         var n, m, cats, f;
410         doWait = thisThread.isKindOf(Routine);
412         if(class.notNil and: {path.isNil
413             or: {
414                 ("test"+(helpTargetDir+/+"version").shellQuote+"-nt"+path.shellQuote+"-o ! -e"+path.shellQuote).systemCmd==0
415             }
416         }) {
417             this.postProgress("Undocumented class:"+name+", generating stub and template");
418             cats = "Undocumented classes";
419             if(this.classHasArKrIr(class)) {
420                 cats = cats ++ ", UGens>Undocumented";
421             };
422             if(class.categories.notNil) {
423                 cats = cats ++ ", "++class.categories.join(", ");
424             };
426             m=[(
427                 tag:\prose,
428                 text:"This class is missing documentation. See the ",
429                 display:\block
430             ),(
431                 tag:\link,
432                 text:"#help_template#generated help template",
433                 display:\inline
434             )];
436             n = [
437                 (tag:\class, text:name),
438                 (tag:\summary, text:"(not documented)"),
439                 (tag:\categories, text:cats),
440                 (tag:\description, children:m)
441             ];
443             n = this.addMethodList(class.class,n,\classmethods);
444             n = this.addMethodList(class,n,\instancemethods);
446             f = {|tag|
447                 var txt,c,m,l,last,sym;
448                 if(tag==\classmethods) {
449                     txt = "\nCLASSMETHODS::\n\n";
450                     c = class.class;
451                 } {
452                     txt = "\nINSTANCEMETHODS::\n\n";
453                     c = class;
454                 };
455                 p.findNode(tag,n).children.do {|x|
456                     if(x.tag==\method) {
457                         txt = txt ++ "METHOD::" + x.text ++ "\n(describe method here)\n\n";
458                         sym = x.text.asSymbol;
459                         m = c.findRespondingMethodFor(sym.asSetter);
460                         m = m ?? {c.findRespondingMethodFor(sym)};
461                         m !? {
462                             l = m.argNames;
463                             last = l.size-1;
464                             l.do {|a,i|
465                                 if (i>0) { //skip 'this' (first arg)
466                                     txt = txt ++ "argument:: ";
467                                     if(i==last and: {m.varArgs}) {
468                                         txt = txt ++ " ... ";
469                                     };
470                                     txt = txt ++ a ++ "\n(describe argument here)\n\n";
471                                 }
472                             }
473                         }
474                     };
475                     txt = txt ++ "returns:: (returnvalue)\n\n";
476                 };
477                 txt;
478             };
480             m = "CLASS::"+name
481                 ++"\nsummary:: (put short description here)\n"
482                 ++"categories::"+cats
483                 ++"\nrelated:: Classes/SomeRelatedClass, Reference/SomeRelatedStuff, etc.\n\n"
484                 ++"DESCRIPTION::\n(put long description here)\n\n"
485                 ++ f.(\classmethods) ++ f.(\instancemethods)
486                 ++"\nEXAMPLES::\n\ncode::\n(some example code)\n::\n";
488             n = n.add((
489                 tag:\section, text:"Help Template", children:[
490                     (tag:\prose, display:\block,
491                     text:"Copy the template below or run"),
492                     (tag:\code,
493                     text:"Document.new(string:SCDoc.makeClassTemplate(\\"++name++"))",
494                     display:\block),
495                     (tag:\prose,
496                     text:"to open a new Document with the template.\nSave it to HelpSource/Classes/"++name++".schelp",
497                     display:\block),
498                     (tag:\code,
499                     text:m,
500                     display:\block)
501                 ]
502             ));
504             if(path.notNil) {
505                 p.root = n;
506                 p.currentFile = nil;
507                 r.render(p,path,"Classes/"++name);
508                 ^true;
509             } {
510                 ^m;
511             };
512         };
513         ^if(path.notNil,false,nil);
514     }
516     *checkSystemCmd {|cmd|
517         if(("which"+cmd+"> /dev/null").systemCmd != 0) {
518             Error("'"++cmd++"' is not installed. Please install it and try again.").throw;
519         };
520     }
522     *findHelpSourceDirs {
523         if(helpSourceDirs.notNil) {^this};
524         this.postProgress("Finding HelpSource folders...");
525         this.checkSystemCmd("find");
526         helpSourceDirs = Set[helpSourceDir];
527         [thisProcess.platform.userExtensionDir, thisProcess.platform.systemExtensionDir].do {|dir|
528             helpSourceDirs = helpSourceDirs | ("find -L"+dir.shellQuote+"-name 'HelpSource' -type d -prune")
529                 .unixCmdGetStdOutLines.asSet;
530         };
531         this.postProgress(helpSourceDirs.asString);
532     }
534     *syncNonHelpFiles {
535         var cmd, c;
537         doWait = thisThread.isKindOf(Routine);
538         this.findHelpSourceDirs;
539         this.postProgress("Synchronizing non-schelp files...");
540         this.checkSystemCmd("rsync");
542         cmd = "rsync -rlt --chmod=u+rwX --exclude '*.schelp' --exclude '.*' %/ %";
544         if(doWait) {
545             c = Condition.new;
546             helpSourceDirs.do {|dir|
547                 cmd.format(dir.shellQuote,helpTargetDir.shellQuote).unixCmd({c.unhang},false);
548                 c.hang;
549             };
550         } {
551             helpSourceDirs.do {|dir|
552                 cmd.format(dir.shellQuote,helpTargetDir.shellQuote).systemCmd;
553             };
554         };
555         this.postProgress("Synchronizing non-schelp files: Done");
556     }
558     *getAllMetaData {
559         var subtarget, classes, cats, t = Main.elapsedTime;
560         var update = false, doc, ndocs;
562         this.syncNonHelpFiles; // ensure that helpTargetDir exist
563         classes = Class.allClasses.collectAs(_.name,IdentitySet).reject(_.isMetaClassName);
564         this.readDocMap;
565         this.postProgress("Getting metadata for all docs...");
567         //FIXME: if classtree changed, force total re-render (touch version timestamp)
569         ndocs = 0;
570         helpSourceDirs.do {|dir|
571             var x, path, mtime, ext, sym, class;
572             ext = (dir != helpSourceDir);
573             this.postProgress("- Collecting from"+dir);
574             Platform.case(
575                 \linux, {"find -L"+dir.shellQuote+"-type f -name '*.schelp' -not -name '*.ext.schelp' -exec stat -c \"%n;%Z\" {} +"},
576                 \osx, {"find -L"+dir.shellQuote+"-type f -name '*.schelp' -not -name '*.ext.schelp' -exec stat -f \"%N;%m\" {} +"}
577             ).unixCmdGetStdOutLines.do {|line|
578                 #path, mtime = line.split($;);
579                 subtarget = path[dir.size+1 ..].drop(-7);
580                 doc = doc_map[subtarget];
582                 if(subtarget.dirname=="Classes") {
583                     sym = subtarget.basename.asSymbol;
584                     class = sym.asClass;
585                 } {
586                     sym = nil;
587                 };
589                 //FIXME: if implementor class changed since last time, force a re-render.
590                 //if doc.redirect && doc.implementor != class.tryPerform(doc.redirect.asSymbol).asSymbol
592                 if(doc.isNil or: {mtime != doc[\mtime]}) {
593                     p.parseMetaData(path);
594                     this.addToDocMap(p,subtarget);
595                     doc = doc_map[subtarget];
596                     if(dir.beginsWith(Platform.userExtensionDir +/+ "quarks")) {
597                         x = "Quarks>"++dir.dirname.basename;
598                         if(doc[\categories].notEmpty) { x = x ++ ", " ++ doc[\categories] };
599                         doc[\categories] = x;
600                     };
601                     doc[\methods] = p.methodList;
602                     doc[\keywords] = p.keywordList;
603                     doc[\mtime] = mtime;
604                     doc[\installed] = if(ext){\extension}{\standard};
605                     if(sym.notNil) { // doc is a class-doc
606                         if(class.notNil) { // class exists
607                             doc[\superclasses] = class.superclasses.collect(_.name).reject(_.isMetaClassName);
608                             doc[\subclasses] = class.subclasses.collect(_.name).reject(_.isMetaClassName);
609                             x = p.findNode(\redirect).text.stripWhiteSpace;
610                             if(x.notEmpty) {
611                                 x = try { class.perform(x.asSymbol) };
612                                 x !? { doc[\implementor] = x.asSymbol };
613                             };
614                         } {
615                             doc[\installed] = \missing;
616                         };
617                     };
618                     update = true;
619                     ndocs = ndocs + 1;
620                 };
621                 doc[\keep] = true;
622                 if(sym.notNil) {
623                     classes.remove(sym);
624                 };
625                 this.maybeWait;
626             };
627         };
628         this.postProgress("Added"+ndocs+"new documents");
630         ndocs = 0;
631         helpSourceDirs.do {|dir|
632             var old;
633             ("find -L"+dir.shellQuote+"-type f -name '*.ext.schelp'").unixCmdGetStdOutLines.do {|file|
634                 subtarget = file[dir.size+1 ..].drop(-11);
635                 doc = doc_map[subtarget];
636                 if(doc.notNil) {
637                     // FIXME: if this doc adds a method to a non-class doc, it will not show up in doc.methods...
638                     old = doc[\additions].copy;
639                     doc[\additions] = doc[\additions].add(file).asSet;
640                     // filter out non-existing files in case helpsource moved..
641                     // this is more a hackish workaround than a good solution.
642                     // it will not work if a doc addition is removed instead of moved.
643                     doc[\additions] = doc[\additions].select(File.exists(_));
644                     update = update or: {doc[\additions] != old};
645                     ndocs = ndocs + 1;
646                     this.postProgress("Addition for"+subtarget+":"+file);
647                 } {
648                     warn("SCDoc: Ignoring additions for non-existing document:"+file);
649                 };
650                 this.maybeWait;
651             }
652         };
653         this.postProgress("Found"+ndocs+"document additions");
655         this.postProgress("Processing"+classes.size+"undocumented classes");
656         undocumentedClasses = classes;
657         ndocs = 0;
658         classes.do {|name|
659             var class, dir;
660             subtarget = "Classes/"++name.asString;
661             if(doc_map[subtarget].isNil) {
662                 cats = "Undocumented classes";
663                 class = name.asClass;
664                 dir = class.filenameSymbol.asString;
665                 if(dir.beginsWith(Platform.userExtensionDir +/+ "quarks")) {
666                     cats = cats ++ ", " ++ "Quarks>"++class.package;
667                 };
669                 if(this.classHasArKrIr(class)) {
670                     cats = cats ++ ", UGens>Undocumented";
671                 };
672                 if(class.categories.notNil) {
673                     cats = cats ++ ", "++class.categories.join(", ");
674                 };
676                 p.root = [
677                     (tag:\class, text:name.asString),
678                     (tag:\categories, text:cats),
679                     (tag:\summary, text:"(not documented)")
680                 ];
681                 this.addToDocMap(p,subtarget);
682                 doc = doc_map[subtarget];
683                 doc[\methods] =
684                     (this.makeMethodList(class.class).collect{|m| "_*"++m}
685                     ++ this.makeMethodList(class).collect{|m| "_-"++m});
687                 doc[\installed] = if(class.filenameSymbol.asString.beginsWith(thisProcess.platform.classLibraryDir).not)
688                     {\extension}
689                     {\standard};
691                 doc[\superclasses] = class.superclasses.collect(_.name).reject(_.isMetaClassName);
692                 doc[\subclasses] = class.subclasses.collect(_.name).reject(_.isMetaClassName);
694                 ndocs = ndocs + 1;
695                 update = true;
696             };
697             doc_map[subtarget][\keep] = true;
698             this.maybeWait;
699         };
700         this.postProgress("Generated metadata for"+ndocs+"undocumented classes");
701         // NOTE: If we remove a Classes/Name.schelp for an existing class, the doc_map won't get updated.
702         // but this shouldn't happen in real-life anyhow..
704         ndocs = 0;
705         doc_map.pairsDo{|k,e|
706             if(e[\keep]!=true, {
707                 doc_map.removeAt(k);
708                 ndocs = ndocs + 1;
709                 update = true;
710             });
711             e.removeAt(\keep); //remove the key since we don't need it anymore
712         };
713         this.postProgress("Removed"+ndocs+"documents from cache");
715         if(update) {
716             this.writeDocMap;
717             this.postProgress("Writing JSON doc map");
718             this.docMapToJSON(helpTargetDir +/+ "docmap.js");
719             NotificationCenter.notify(SCDoc, \docMapDidUpdate);
720         };
721         this.postProgress("Done! time spent:"+(Main.elapsedTime-t)+"sec");
722     }
724     *findHelpFile {|str|
725         ^r.findHelpFile(str.stripWhiteSpace);
726     }
729 + String {
730     stripWhiteSpace {
731         var ws = [$\n, $\r, $\t, $\ ];
732         var a=0, b=this.size-1;
733         while({ ws.includes(this[a])},{a=a+1});
734         while({ ws.includes(this[b])},{b=b-1});
735         ^this.copyRange(a,b);
736     }
737         unixCmdGetStdOutLines {
738                 var pipe, lines, line;
740                 pipe = Pipe.new(this, "r");
741                 lines = Array.new;
742                 line = pipe.getLine;
743                 while({line.notNil}, {lines = lines.add(line); line = pipe.getLine; });
744                 pipe.close;
746                 ^lines;
747         }
751 + Method {
752     isExtensionOf {|class|
753         ^(
754             (this.filenameSymbol != class.filenameSymbol)
755             and:
756                 if((class!=Object) and: (class!=Meta_Object),
757                     {class.superclasses.includes(this.ownerClass).not},
758                     {true})
759         );
760     }
763 + Help {
764         *dir {
765                 ^SCDoc.helpTargetDir
766         }