adds missing Qt support for viewExtensions (.horz .vert .comp .flow .deepDo .asFlowView)
[supercollider.git] / SCClassLibrary / SCDoc / SCDoc.sc
blobc58ee5d9ab5634fba9ebc6d124fdcb71cbe7da7e
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 = 22;
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.escapeChar($ )+"-nt"+path.escapeChar($ )).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.escapeChar($ )+"-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.escapeChar($ )+"-nt"+dest.escapeChar($ )+"-o ! -e"+dest.escapeChar($ )).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").escapeChar($ )).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.escapeChar($ )+"-nt"+path.escapeChar($ )
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").escapeChar($ )+"-nt"+path.escapeChar($ )+"-o ! -e"+path.escapeChar($ )).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.escapeChar($ )+"-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.escapeChar($ ),helpTargetDir.escapeChar($ )).unixCmd({c.unhang},false);
548                 c.hang;
549             };
550         } {
551             helpSourceDirs.do {|dir|
552                 cmd.format(dir.escapeChar($ ),helpTargetDir.escapeChar($ )).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.escapeChar($ )+"-type f -name '*.schelp' -printf '%p;%T@\n'"},
576                 \linux, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -not -name '*.ext.schelp' -exec stat -c \"%n;%Z\" {} +"},
577                 \osx, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -not -name '*.ext.schelp' -exec stat -f \"%N;%m\" {} +"}
578             ).unixCmdGetStdOutLines.do {|line|
579                 #path, mtime = line.split($;);
580                 subtarget = path[dir.size+1 ..].drop(-7);
581                 doc = doc_map[subtarget];
583                 if(subtarget.dirname=="Classes") {
584                     sym = subtarget.basename.asSymbol;
585                     class = sym.asClass;
586                 } {
587                     sym = nil;
588                 };
590                 //FIXME: if implementor class changed since last time, force a re-render.
591                 //if doc.redirect && doc.implementor != class.tryPerform(doc.redirect.asSymbol).asSymbol
593                 if(doc.isNil or: {mtime != doc.mtime}) {
594                     p.parseMetaData(path);
595                     this.addToDocMap(p,subtarget);
596                     doc = doc_map[subtarget];
597                     if(dir.beginsWith(Platform.userExtensionDir +/+ "quarks")) {
598                         x = "Quarks>"++dir.dirname.basename;
599                         if(doc.categories.notEmpty) { x = x ++ ", " ++ doc.categories };
600                         doc.categories = x;
601                     };
602                     doc.methods = p.methodList;
603                     doc.keywords = p.keywordList;
604                     doc.mtime = mtime;
605                     doc.installed = if(ext){\extension}{\standard};
606                     if(sym.notNil) { // doc is a class-doc
607                         if(class.notNil) { // class exists
608                             doc.superclasses = class.superclasses.collect(_.name).reject(_.isMetaClassName);
609                             doc.subclasses = class.subclasses.collect(_.name).reject(_.isMetaClassName);
610                             x = p.findNode(\redirect).text.stripWhiteSpace;
611                             if(x.notEmpty) {
612                                 x = class.tryPerform(x.asSymbol);
613                                 x !? { doc.implementor = x.asSymbol };
614                             };
615                         } {
616                             doc.installed = \missing;
617                         };
618                     };
619                     update = true;
620                     ndocs = ndocs + 1;
621                 };
622                 doc.keep = true;
623                 if(sym.notNil) {
624                     classes.remove(sym);
625                 };
626                 this.maybeWait;
627             };
628         };
629         this.postProgress("Added"+ndocs+"new documents");
631         ndocs = 0;
632         helpSourceDirs.do {|dir|
633             var old;
634             ("find -L"+dir.escapeChar($ )+"-type f -name '*.ext.schelp'").unixCmdGetStdOutLines.do {|file|
635                 subtarget = file[dir.size+1 ..].drop(-11);
636                 doc = doc_map[subtarget];
637                 if(doc.notNil) {
638                     // FIXME: if this doc adds a method to a non-class doc, it will not show up in doc.methods...
639                     old = doc.additions.copy;
640                     doc.additions = doc.additions.add(file).asSet;
641                     // filter out non-existing files in case helpsource moved..
642                     // this is more a hackish workaround than a good solution.
643                     // it will not work if a doc addition is removed instead of moved.
644                     doc.additions = doc.additions.select(File.exists(_));
645                     update = update or: {doc.additions != old};
646                     ndocs = ndocs + 1;
647                     this.postProgress("Addition for"+subtarget+":"+file);
648                 } {
649                     warn("SCDoc: Ignoring additions for non-existing document:"+file);
650                 };
651                 this.maybeWait;
652             }
653         };
654         this.postProgress("Found"+ndocs+"document additions");
656         this.postProgress("Processing"+classes.size+"undocumented classes");
657         undocumentedClasses = classes;
658         ndocs = 0;
659         classes.do {|name|
660             var class, dir;
661             subtarget = "Classes/"++name.asString;
662             if(doc_map[subtarget].isNil) {
663                 cats = "Undocumented classes";
664                 class = name.asClass;
665                 dir = class.filenameSymbol.asString;
666                 if(dir.beginsWith(Platform.userExtensionDir +/+ "quarks")) {
667                     cats = cats ++ ", " ++ "Quarks>"++class.package;
668                 };
670                 if(this.classHasArKrIr(class)) {
671                     cats = cats ++ ", UGens>Undocumented";
672                 };
673                 if(class.categories.notNil) {
674                     cats = cats ++ ", "++class.categories.join(", ");
675                 };
677                 p.root = [
678                     (tag:\class, text:name.asString),
679                     (tag:\categories, text:cats),
680                     (tag:\summary, text:"(not documented)")
681                 ];
682                 this.addToDocMap(p,subtarget);
683                 doc = doc_map[subtarget];
684                 doc.methods =
685                     (this.makeMethodList(class.class).collect{|m| "_*"++m}
686                     ++ this.makeMethodList(class).collect{|m| "_-"++m});
688                 doc.installed = if(class.filenameSymbol.asString.beginsWith(thisProcess.platform.classLibraryDir).not)
689                     {\extension}
690                     {\standard};
692                 doc.superclasses = class.superclasses.collect(_.name).reject(_.isMetaClassName);
693                 doc.subclasses = class.subclasses.collect(_.name).reject(_.isMetaClassName);
695                 ndocs = ndocs + 1;
696                 update = true;
697             };
698             doc_map[subtarget].keep = true;
699             this.maybeWait;
700         };
701         this.postProgress("Generated metadata for"+ndocs+"undocumented classes");
702         // NOTE: If we remove a Classes/Name.schelp for an existing class, the doc_map won't get updated.
703         // but this shouldn't happen in real-life anyhow..
705         ndocs = 0;
706         doc_map.pairsDo{|k,e|
707             if(e.keep!=true, {
708                 doc_map.removeAt(k);
709                 ndocs = ndocs + 1;
710                 update = true;
711             });
712             e.removeAt(\keep); //remove the key since we don't need it anymore
713         };
714         this.postProgress("Removed"+ndocs+"documents from cache");
716         if(update) {
717             this.writeDocMap;
718             this.postProgress("Writing JSON doc map");
719             this.docMapToJSON(helpTargetDir +/+ "docmap.js");
720             NotificationCenter.notify(SCDoc, \docMapDidUpdate);
721         };
722         this.postProgress("Done! time spent:"+(Main.elapsedTime-t)+"sec");
723     }
725     *findHelpFile {|str|
726         ^r.findHelpFile(str.stripWhiteSpace);
727     }
730 + String {
731     stripWhiteSpace {
732         var ws = [$\n, $\r, $\t, $\ ];
733         var a=0, b=this.size-1;
734         while({ ws.includes(this[a])},{a=a+1});
735         while({ ws.includes(this[b])},{b=b-1});
736         ^this.copyRange(a,b);
737     }
738         unixCmdGetStdOutLines {
739                 var pipe, lines, line;
741                 pipe = Pipe.new(this, "r");
742                 lines = Array.new;
743                 line = pipe.getLine;
744                 while({line.notNil}, {lines = lines.add(line); line = pipe.getLine; });
745                 pipe.close;
747                 ^lines;
748         }
752 + Method {
753     isExtensionOf {|class|
754         ^(
755             (this.filenameSymbol != class.filenameSymbol)
756             and:
757                 if((class!=Object) and: (class!=Meta_Object),
758                     {class.superclasses.includes(this.ownerClass).not},
759                     {true})
760         );
761     }
764 + Help {
765         *dir {
766                 ^SCDoc.helpTargetDir
767         }