old quark gui: openOS is not osx only
[supercollider.git] / SCClassLibrary / SCDoc / SCDoc.sc
blobfdee8cf7366e56434279d38e74b549651562eeb0
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 = 21;
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         doc_map[subtarget].additions.do {|ext|
222             p2 = p2 ?? {p.class.new};
223             p2.parseFile(ext);
224             p.merge(p2);
225         };
227         r.render(p,dest,subtarget);
228     }
230     *renderAll {|force=false,threaded=true,findExtensions=true,doneFunc|
231         var count, count2, func, x, fileList, subtarget, dest, t = Main.elapsedTime, oldVerbose = verbose;
233         func = {
234             progressMax = 100;
235             progressCount = 0;
236             verbose = true;
238             this.cleanState(force);
239             if(findExtensions) {
240                 this.findHelpSourceDirs;
241             } {
242                 helpSourceDirs = Set[helpSourceDir];
243             };
244             this.tickProgress;
245             this.getAllMetaData;
246             this.tickProgress;
248             fileList = Dictionary.new;
249             count = 0;
250             this.postProgress("Updating all files");
251             helpSourceDirs.do {|dir|
252                 fileList[dir] = ("find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -not -name '*.ext.schelp'")
253                     .unixCmdGetStdOutLines.reject(_.isEmpty).asSet;
254                 count = count + fileList[dir].size;
255             };
256             count2 = undocumentedClasses.size;
257             progressCount = 3 * ((count+count2)/progressMax);
258             progressMax = count+count2+progressCount;
260             this.postProgress("Found"+count+"help files");
261             helpSourceDirs.do {|dir|
262                 x = fileList[dir].size;
263                 if(x>0) {
264                     fileList[dir].do {|path|
265                         subtarget = path[dir.size+1 .. path.findBackwards(".")?path.size-1];
266                         dest = helpTargetDir+/+subtarget++".html";
267                         if(force
268                         or: {("test"+path.escapeChar($ )+"-nt"+dest.escapeChar($ )+"-o ! -e"+dest.escapeChar($ )).systemCmd==0}) {
269                             this.parseAndRender(path,dest,subtarget);
270                         } {
271                             this.postProgress("Skipping"+dest);
272                         };
273                         this.tickProgress;
274                     };
275                 };
276             };
277             this.postProgress("Found"+count2+"undocumented classes");
278             undocumentedClasses.do {|name|
279                 dest = helpTargetDir+/+"Classes"+/+name++".html";
280                 if(this.makeClassTemplate(name.asString,dest).not) {
281                     this.postProgress("Skipping"+dest);
282                 };
283                 this.tickProgress;
284             };
285             this.postProgress("Done! time spent:"+(Main.elapsedTime-t)+"sec");
286             verbose = oldVerbose;
287             doneFunc.value();
288         };
290         doWait = threaded;
291         if(doWait, {
292             Routine(func).play(AppClock);
293         }, func);
294     }
296     *cleanState {|noCache=false|
297         didRun = false;
298         helpSourceDirs = nil;
299         if(noCache) {
300             doWait = thisThread.isKindOf(Routine);
301             this.syncNonHelpFiles; // ensure helpTargetDir exists
302             ("touch"+(helpTargetDir+/+"version").escapeChar($ )).systemCmd;
303         }
304     }
306     *findHelpSource {|subtarget|
307         var src;
308         this.findHelpSourceDirs;
309         block {|break|
310             helpSourceDirs.do {|dir|
311                 var x = dir+/+subtarget++".schelp";
312                 if(File.exists(x)) {
313                     src = x;
314                     break.value;
315                 };
316             };
317         };
318         ^src
319     }
321     *prepareHelpForURL {|url|
322         var proto, path, query, anchor;
323         var subtarget, src, c, cmd;
324         var verpath = this.helpTargetDir +/+ "version";
326         doWait = thisThread.isKindOf(Routine);
328         if(isProcessing) {
329             "SCDoc: prepareHelpForURL already running.. waiting for the first to finish.".warn;
330             if(doWait.not) {
331                 Error("SCDoc: cannot wait for already running prepareHelpForURL, this call was not made inside a Routine").throw;
332             };
333             c = Condition.new;
334             Routine {
335                 while {0.5.wait; isProcessing};
336                 c.unhang;
337             }.play(AppClock);
338             c.hang;
339         };
340         protect {
341             isProcessing = true;
343             // parse URL
344             url = url.replace("%20"," ");
345             #proto, path, query, anchor = url.findRegexp("(^\\w+://)?([^#?]+)(\\?[^#]+)?(#.*)?")[1..].flop[1];
346             if(proto.isEmpty) {proto="file://"};
347             if(proto!="file://") {isProcessing = false; ^url}; // just pass through remote url's
348             if(path.beginsWith(helpTargetDir).not) {isProcessing = false; ^url}; // just pass through remote url's
350             if(File.exists(helpTargetDir).not) {
351                 this.cleanState;
352             };
354             // sync non-schelp files once every session
355             if(didRun.not) {
356                 didRun = true;
357                 this.getAllMetaData;
358             };
360             // strip to subfolder/basename (like Classes/SinOsc)
361             subtarget = path[helpTargetDir.size+1 .. path.findBackwards(".")?path.size-1];
363             // find help source file
364             src = this.findHelpSource(subtarget);
366             // create a simple stub if class was undocumented
367             if(src.isNil and: {subtarget.dirname=="Classes"}) {
368                 this.makeClassTemplate(subtarget.basename,path);
369             };
371             if(File.exists(path).not) {
372                 if(src.notNil) { // no target file, but helpsource found, parse and render.
373                     this.parseAndRender(src,path,subtarget);
374                     isProcessing = false;
375                     ^url;
376                 } {
377                     this.postProgress("Broken link:"+url);
378                     isProcessing = false;
379                     ^nil;
380                 };
381             } {
382                 cmd = {
383                     ("test" + ([src,verpath]++doc_map[subtarget].additions).collect {|x|
384                         x.escapeChar($ )+"-nt"+path.escapeChar($ )
385                     }.join(" -o ")).systemCmd == 0
386                 };
388                 if(src.notNil and: cmd) {
389                     // target file and helpsource exists, and helpsource is newer than target
390                     this.parseAndRender(src,path,subtarget);
391                     isProcessing = false;
392                     ^url;
393                 };
394                 // we reach here if the target URL exists and there's no schelp source file,
395                 // or if the target URL does not need update.
396             };
397         } {
398             isProcessing = false;
399         };
400         ^url;
401     }
403     *makeClassTemplate {|name,path|
404         var class = name.asSymbol.asClass;
405         var n, m, cats, f;
407         doWait = thisThread.isKindOf(Routine);
409         if(class.notNil and: {path.isNil
410             or: {
411                 ("test"+(helpTargetDir+/+"version").escapeChar($ )+"-nt"+path.escapeChar($ )+"-o ! -e"+path.escapeChar($ )).systemCmd==0
412             }
413         }) {
414             this.postProgress("Undocumented class:"+name+", generating stub and template");
415             cats = "Undocumented classes";
416             if(this.classHasArKrIr(class)) {
417                 cats = cats ++ ", UGens>Undocumented";
418             };
419             if(class.categories.notNil) {
420                 cats = cats ++ ", "++class.categories.join(", ");
421             };
423             m=[(
424                 tag:\prose,
425                 text:"This class is missing documentation. See the ",
426                 display:\block
427             ),(
428                 tag:\link,
429                 text:"#help_template#generated help template",
430                 display:\inline
431             )];
433             n = [
434                 (tag:\class, text:name),
435                 (tag:\summary, text:"(not documented)"),
436                 (tag:\categories, text:cats),
437                 (tag:\description, children:m)
438             ];
440             n = this.addMethodList(class.class,n,\classmethods);
441             n = this.addMethodList(class,n,\instancemethods);
443             f = {|tag|
444                 var txt,c,m,l,last,sym;
445                 if(tag==\classmethods) {
446                     txt = "\nCLASSMETHODS::\n\n";
447                     c = class.class;
448                 } {
449                     txt = "\nINSTANCEMETHODS::\n\n";
450                     c = class;
451                 };
452                 p.findNode(tag,n).children.do {|x|
453                     if(x.tag==\method) {
454                         txt = txt ++ "METHOD::" + x.text ++ "\n(describe method here)\n\n";
455                         sym = x.text.asSymbol;
456                         m = c.findRespondingMethodFor(sym.asSetter);
457                         m = m ?? {c.findRespondingMethodFor(sym)};
458                         m !? {
459                             l = m.argNames;
460                             last = l.size-1;
461                             l.do {|a,i|
462                                 if (i>0) { //skip 'this' (first arg)
463                                     txt = txt ++ "argument:: ";
464                                     if(i==last and: {m.varArgs}) {
465                                         txt = txt ++ " ... ";
466                                     };
467                                     txt = txt ++ a ++ "\n(describe argument here)\n\n";
468                                 }
469                             }
470                         }
471                     };
472                     txt = txt ++ "returns:: (returnvalue)\n\n";
473                 };
474                 txt;
475             };
477             m = "CLASS::"+name
478                 ++"\nsummary:: (put short description here)\n"
479                 ++"categories::"+cats
480                 ++"\nrelated:: Classes/SomeRelatedClass, Reference/SomeRelatedStuff, etc.\n\n"
481                 ++"DESCRIPTION::\n(put long description here)\n\n"
482                 ++ f.(\classmethods) ++ f.(\instancemethods)
483                 ++"\nEXAMPLES::\n\ncode::\n(some example code)\n::\n";
485             n = n.add((
486                 tag:\section, text:"Help Template", children:[
487                     (tag:\prose, display:\block,
488                     text:"Copy the template below or run"),
489                     (tag:\code,
490                     text:"Document.new(string:SCDoc.makeClassTemplate(\\"++name++"))",
491                     display:\block),
492                     (tag:\prose,
493                     text:"to open a new Document with the template.\nSave it to HelpSource/Classes/"++name++".schelp",
494                     display:\block),
495                     (tag:\code,
496                     text:m,
497                     display:\block)
498                 ]
499             ));
501             if(path.notNil) {
502                 p.root = n;
503                 p.currentFile = nil;
504                 r.render(p,path,"Classes/"++name);
505                 ^true;
506             } {
507                 ^m;
508             };
509         };
510         ^if(path.notNil,false,nil);
511     }
513     *checkSystemCmd {|cmd|
514         if(("which"+cmd+"> /dev/null").systemCmd != 0) {
515             Error("'"++cmd++"' is not installed. Please install it and try again.").throw;
516         };
517     }
519     *findHelpSourceDirs {
520         if(helpSourceDirs.notNil) {^this};
521         this.postProgress("Finding HelpSource folders...");
522         this.checkSystemCmd("find");
523         helpSourceDirs = Set[helpSourceDir];
524         [thisProcess.platform.userExtensionDir, thisProcess.platform.systemExtensionDir].do {|dir|
525             helpSourceDirs = helpSourceDirs | ("find -L"+dir.escapeChar($ )+"-name 'HelpSource' -type d -prune")
526                 .unixCmdGetStdOutLines.asSet;
527         };
528         this.postProgress(helpSourceDirs.asString);
529     }
531     *syncNonHelpFiles {
532         var cmd, c;
534         doWait = thisThread.isKindOf(Routine);
535         this.findHelpSourceDirs;
536         this.postProgress("Synchronizing non-schelp files...");
537         this.checkSystemCmd("rsync");
539         cmd = "rsync -rlt --chmod=u+rwX --exclude '*.schelp' --exclude '.*' %/ %";
541         if(doWait) {
542             c = Condition.new;
543             helpSourceDirs.do {|dir|
544                 cmd.format(dir.escapeChar($ ),helpTargetDir.escapeChar($ )).unixCmd({c.unhang},false);
545                 c.hang;
546             };
547         } {
548             helpSourceDirs.do {|dir|
549                 cmd.format(dir.escapeChar($ ),helpTargetDir.escapeChar($ )).systemCmd;
550             };
551         };
552         this.postProgress("Synchronizing non-schelp files: Done");
553     }
555     *getAllMetaData {
556         var subtarget, classes, cats, t = Main.elapsedTime;
557         var update = false, doc, ndocs;
559         this.syncNonHelpFiles; // ensure that helpTargetDir exist
560         classes = Class.allClasses.collectAs(_.name,IdentitySet).reject(_.isMetaClassName);
561         this.readDocMap;
562         this.postProgress("Getting metadata for all docs...");
564         //FIXME: if classtree changed, force total re-render (touch version timestamp)
566         ndocs = 0;
567         helpSourceDirs.do {|dir|
568             var x, path, mtime, ext, sym, class;
569             ext = (dir != helpSourceDir);
570             this.postProgress("- Collecting from"+dir);
571             Platform.case(
572 //                \linux, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -printf '%p;%T@\n'"},
573                 \linux, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -not -name '*.ext.schelp' -exec stat -c \"%n;%Z\" {} +"},
574                 \osx, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -not -name '*.ext.schelp' -exec stat -f \"%N;%m\" {} +"}
575             ).unixCmdGetStdOutLines.do {|line|
576                 #path, mtime = line.split($;);
577                 subtarget = path[dir.size+1 ..].drop(-7);
578                 doc = doc_map[subtarget];
580                 if(subtarget.dirname=="Classes") {
581                     sym = subtarget.basename.asSymbol;
582                     class = sym.asClass;
583                 } {
584                     sym = nil;
585                 };
587                 //FIXME: if implementor class changed since last time, force a re-render.
588                 //if doc.redirect && doc.implementor != class.tryPerform(doc.redirect.asSymbol).asSymbol
590                 if(doc.isNil or: {mtime != doc.mtime}) {
591                     p.parseMetaData(path);
592                     this.addToDocMap(p,subtarget);
593                     doc = doc_map[subtarget];
594                     doc.methods = p.methodList;
595                     doc.keywords = p.keywordList;
596                     doc.mtime = mtime;
597                     doc.installed = if(ext){\extension}{\standard};
598                     if(sym.notNil) { // doc is a class-doc
599                         if(class.notNil) { // class exists
600                             doc.superclasses = class.superclasses.collect(_.name).reject(_.isMetaClassName);
601                             doc.subclasses = class.subclasses.collect(_.name).reject(_.isMetaClassName);
602                             x = p.findNode(\redirect).text.stripWhiteSpace;
603                             if(x.notEmpty) {
604                                 x = class.tryPerform(x.asSymbol);
605                                 x !? { doc.implementor = x.asSymbol };
606                             };
607                         } {
608                             doc.installed = \missing;
609                         };
610                     };
611                     update = true;
612                     ndocs = ndocs + 1;
613                 };
614                 doc.keep = true;
615                 if(sym.notNil) {
616                     classes.remove(sym);
617                 };
618                 this.maybeWait;
619             };
620         };
621         this.postProgress("Added"+ndocs+"new documents");
623         ndocs = 0;
624         helpSourceDirs.do {|dir|
625             var old;
626             ("find -L"+dir.escapeChar($ )+"-type f -name '*.ext.schelp'").unixCmdGetStdOutLines.do {|file|
627                 subtarget = file[dir.size+1 ..].drop(-11);
628                 doc = doc_map[subtarget];
629                 if(doc.notNil) {
630                     // FIXME: if this doc adds a method to a non-class doc, it will not show up in doc.methods...
631                     old = doc.additions.copy;
632                     doc.additions = doc.additions.add(file).asSet;
633                     update = update or: {doc.additions != old};
634                     ndocs = ndocs + 1;
635                     this.postProgress("Addition for"+subtarget+":"+file);
636                 } {
637                     warn("SCDoc: Ignoring additions for non-existing document:"+file);
638                 };
639                 this.maybeWait;
640             }
641         };
642         this.postProgress("Found"+ndocs+"document additions");
644         this.postProgress("Processing"+classes.size+"undocumented classes");
645         undocumentedClasses = classes;
646         ndocs = 0;
647         classes.do {|name|
648             var class;
649             subtarget = "Classes/"++name.asString;
650             if(doc_map[subtarget].isNil) {
651                 cats = "Undocumented classes";
652                 class = name.asClass;
653                 if(this.classHasArKrIr(class)) {
654                     cats = cats ++ ", UGens>Undocumented";
655                 };
656                 if(class.categories.notNil) {
657                     cats = cats ++ ", "++class.categories.join(", ");
658                 };
660                 p.root = [
661                     (tag:\class, text:name.asString),
662                     (tag:\categories, text:cats),
663                     (tag:\summary, text:"(not documented)")
664                 ];
665                 this.addToDocMap(p,subtarget);
666                 doc = doc_map[subtarget];
667                 doc.methods =
668                     (this.makeMethodList(class.class).collect{|m| "_*"++m}
669                     ++ this.makeMethodList(class).collect{|m| "_-"++m});
671                 doc.installed = if(class.filenameSymbol.asString.beginsWith(thisProcess.platform.classLibraryDir).not)
672                     {\extension}
673                     {\standard};
675                 doc.superclasses = class.superclasses.collect(_.name).reject(_.isMetaClassName);
676                 doc.subclasses = class.subclasses.collect(_.name).reject(_.isMetaClassName);
678                 ndocs = ndocs + 1;
679                 update = true;
680             };
681             doc_map[subtarget].keep = true;
682             this.maybeWait;
683         };
684         this.postProgress("Generated metadata for"+ndocs+"undocumented classes");
685         // NOTE: If we remove a Classes/Name.schelp for an existing class, the doc_map won't get updated.
686         // but this shouldn't happen in real-life anyhow..
688         ndocs = 0;
689         doc_map.pairsDo{|k,e|
690             if(e.keep!=true, {
691                 doc_map.removeAt(k);
692                 ndocs = ndocs + 1;
693                 update = true;
694             });
695             e.removeAt(\keep); //remove the key since we don't need it anymore
696         };
697         this.postProgress("Removed"+ndocs+"documents from cache");
699         if(update) {
700             this.writeDocMap;
701             this.postProgress("Writing JSON doc map");
702             this.docMapToJSON(helpTargetDir +/+ "docmap.js");
703             NotificationCenter.notify(SCDoc, \docMapDidUpdate);
704         };
705         this.postProgress("Done! time spent:"+(Main.elapsedTime-t)+"sec");
706     }
708     *findHelpFile {|str|
709         ^r.findHelpFile(str.stripWhiteSpace);
710     }
713 + String {
714     stripWhiteSpace {
715         var ws = [$\n, $\r, $\t, $\ ];
716         var a=0, b=this.size-1;
717         while({ ws.includes(this[a])},{a=a+1});
718         while({ ws.includes(this[b])},{b=b-1});
719         ^this.copyRange(a,b);
720     }
721         unixCmdGetStdOutLines {
722                 var pipe, lines, line;
724                 pipe = Pipe.new(this, "r");
725                 lines = Array.new;
726                 line = pipe.getLine;
727                 while({line.notNil}, {lines = lines.add(line); line = pipe.getLine; });
728                 pipe.close;
730                 ^lines;
731         }
735 + Method {
736     isExtensionOf {|class|
737         ^(
738             (this.filenameSymbol != class.filenameSymbol)
739             and:
740                 if((class!=Object) and: (class!=Meta_Object),
741                     {class.superclasses.includes(this.ownerClass).not},
742                     {true})
743         );
744     }
747 + Help {
748         *dir {
749                 ^SCDoc.helpTargetDir
750         }