HelpBrowser: path box becomes a more conventional search box
[supercollider.git] / SCClassLibrary / SCDoc / SCDoc.sc
blob043819544695eb27bda6545e1a7783285be1b387
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 = 18;
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         doc_map.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     *prepareHelpForURL {|url|
307         var proto, path, anchor;
308         var subtarget, src, c, cmd;
309         var verpath = this.helpTargetDir +/+ "version";
311         doWait = thisThread.isKindOf(Routine);
313         if(isProcessing) {
314             "SCDoc: prepareHelpForURL already running.. waiting for the first to finish.".warn;
315             if(doWait.not) {
316                 Error("SCDoc: cannot wait for already running prepareHelpForURL, this call was not made inside a Routine").throw;
317             };
318             c = Condition.new;
319             Routine {
320                 while {0.5.wait; isProcessing};
321                 c.unhang;
322             }.play(AppClock);
323             c.hang;
324         };
325         protect {
326             isProcessing = true;
328             // parse URL
329             url = url.replace("%20"," ");
330             #proto, path, anchor = url.findRegexp("(^\\w+://)?([^#]+)(#.*)?")[1..].flop[1];
331             if(proto.isEmpty) {proto="file://"};
332             if(proto!="file://") {isProcessing = false; ^url}; // just pass through remote url's
333             if(path.beginsWith(helpTargetDir).not) {isProcessing = false; ^url}; // just pass through remote url's
335             if(File.exists(helpTargetDir).not) {
336                 this.cleanState;
337             };
339             // sync non-schelp files once every session
340             if(didRun.not) {
341                 didRun = true;
342                 this.getAllMetaData;
343             };
345             // strip to subfolder/basename (like Classes/SinOsc)
346             subtarget = path[helpTargetDir.size+1 .. path.findBackwards(".")?path.size-1];
348             // find help source file
349             block {|break|
350                 src = nil;
351                 helpSourceDirs.do {|dir|
352                     var x = dir+/+subtarget++".schelp";
353                     if(File.exists(x)) {
354                         src = x;
355                         break.value;
356                     };
357                 };
358             };
360             // create a simple stub if class was undocumented
361             if(src.isNil and: {subtarget.dirname=="Classes"}) {
362                 this.makeClassTemplate(subtarget.basename,path);
363             };
365             if(File.exists(path).not) {
366                 if(src.notNil) { // no target file, but helpsource found, parse and render.
367                     this.parseAndRender(src,path,subtarget);
368                     isProcessing = false;
369                     ^url;
370                 } {
371                     this.postProgress("Broken link:"+url);
372                     isProcessing = false;
373                     ^nil;
374                 };
375             } {
376                 cmd = {
377                     ("test" + ([src,verpath]++doc_map[subtarget].additions).collect {|x|
378                         x.escapeChar($ )+"-nt"+path.escapeChar($ )
379                     }.join(" -o ")).systemCmd == 0
380                 };
382                 if(src.notNil and: cmd) {
383                     // target file and helpsource exists, and helpsource is newer than target
384                     this.parseAndRender(src,path,subtarget);
385                     isProcessing = false;
386                     ^url;
387                 };
388                 // we reach here if the target URL exists and there's no schelp source file,
389                 // or if the target URL does not need update.
390             };
391         } {
392             isProcessing = false;
393         };
394         ^url;
395     }
397     *makeClassTemplate {|name,path|
398         var class = name.asSymbol.asClass;
399         var n, m, cats, f;
401         doWait = thisThread.isKindOf(Routine);
403         if(class.notNil and: {path.isNil
404             or: {
405                 ("test"+(helpTargetDir+/+"version").escapeChar($ )+"-nt"+path.escapeChar($ )+"-o ! -e"+path.escapeChar($ )).systemCmd==0
406             }
407         }) {
408             this.postProgress("Undocumented class:"+name+", generating stub and template");
409             cats = "Undocumented classes";
410             if(this.classHasArKrIr(class)) {
411                 cats = cats ++ ", UGens>Undocumented";
412             };
413             if(class.categories.notNil) {
414                 cats = cats ++ ", "++class.categories.join(", ");
415             };
417             m=[(
418                 tag:\prose,
419                 text:"This class is missing documentation. See the ",
420                 display:\block
421             ),(
422                 tag:\link,
423                 text:"#help_template#generated help template",
424                 display:\inline
425             )];
427             n = [
428                 (tag:\class, text:name),
429                 (tag:\summary, text:"(not documented)"),
430                 (tag:\categories, text:cats),
431                 (tag:\description, children:m)
432             ];
434             n = this.addMethodList(class.class,n,\classmethods);
435             n = this.addMethodList(class,n,\instancemethods);
437             f = {|tag|
438                 var txt,c,m,l,last,sym;
439                 if(tag==\classmethods) {
440                     txt = "\nCLASSMETHODS::\n\n";
441                     c = class.class;
442                 } {
443                     txt = "\nINSTANCEMETHODS::\n\n";
444                     c = class;
445                 };
446                 p.findNode(tag,n).children.do {|x|
447                     if(x.tag==\method) {
448                         txt = txt ++ "METHOD::" + x.text ++ "\n(describe method here)\n\n";
449                         sym = x.text.asSymbol;
450                         m = c.findRespondingMethodFor(sym.asSetter);
451                         m = m ?? {c.findRespondingMethodFor(sym)};
452                         m !? {
453                             l = m.argNames;
454                             last = l.size-1;
455                             l.do {|a,i|
456                                 if (i>0) { //skip 'this' (first arg)
457                                     txt = txt ++ "argument:: ";
458                                     if(i==last and: {m.varArgs}) {
459                                         txt = txt ++ " ... ";
460                                     };
461                                     txt = txt ++ a ++ "\n(describe argument here)\n\n";
462                                 }
463                             }
464                         }
465                     };
466                     txt = txt ++ "returns:: (returnvalue)\n\n";
467                 };
468                 txt;
469             };
471             m = "CLASS::"+name
472                 ++"\nsummary:: (put short description here)\n"
473                 ++"categories::"+cats
474                 ++"\nrelated:: Classes/SomeRelatedClass, Reference/SomeRelatedStuff, etc.\n\n"
475                 ++"DESCRIPTION::\n(put long description here)\n\n"
476                 ++ f.(\classmethods) ++ f.(\instancemethods)
477                 ++"\nEXAMPLES::\n\ncode::\n(some example code)\n::\n";
479             n = n.add((
480                 tag:\section, text:"Help Template", children:[
481                     (tag:\prose, display:\block,
482                     text:"Copy the template below or run"),
483                     (tag:\code,
484                     text:"Document.new(string:SCDoc.makeClassTemplate(\\"++name++"))",
485                     display:\block),
486                     (tag:\prose,
487                     text:"to open a new Document with the template.\nSave it to HelpSource/Classes/"++name++".schelp",
488                     display:\block),
489                     (tag:\code,
490                     text:m,
491                     display:\block)
492                 ]
493             ));
495             if(path.notNil) {
496                 p.root = n;
497                 p.currentFile = nil;
498                 r.render(p,path,"Classes/"++name);
499                 ^true;
500             } {
501                 ^m;
502             };
503         };
504         ^if(path.notNil,false,nil);
505     }
507     *checkSystemCmd {|cmd|
508         if(("which"+cmd+"> /dev/null").systemCmd != 0) {
509             Error("'"++cmd++"' is not installed. Please install it and try again.").throw;
510         };
511     }
513     *findHelpSourceDirs {
514         if(helpSourceDirs.notNil) {^this};
515         this.postProgress("Finding HelpSource folders...");
516         this.checkSystemCmd("find");
517         helpSourceDirs = Set[helpSourceDir];
518         [thisProcess.platform.userExtensionDir, thisProcess.platform.systemExtensionDir].do {|dir|
519             helpSourceDirs = helpSourceDirs | ("find -L"+dir.escapeChar($ )+"-name 'HelpSource' -type d -prune")
520                 .unixCmdGetStdOutLines.asSet;
521         };
522         this.postProgress(helpSourceDirs.asString);
523     }
525     *syncNonHelpFiles {
526         var cmd, c;
528         doWait = thisThread.isKindOf(Routine);
529         this.findHelpSourceDirs;
530         this.postProgress("Synchronizing non-schelp files...");
531         this.checkSystemCmd("rsync");
533         cmd = "rsync -rlt --exclude '*.schelp' --exclude '.*' %/ %";
535         if(doWait) {
536             c = Condition.new;
537             helpSourceDirs.do {|dir|
538                 cmd.format(dir.escapeChar($ ),helpTargetDir.escapeChar($ )).unixCmd({c.unhang},false);
539                 c.hang;
540             };
541         } {
542             helpSourceDirs.do {|dir|
543                 cmd.format(dir.escapeChar($ ),helpTargetDir.escapeChar($ )).systemCmd;
544             };
545         };
546         this.postProgress("Synchronizing non-schelp files: Done");
547     }
549     *getAllMetaData {
550         var subtarget, classes, cats, t = Main.elapsedTime;
551         var update = false, doc, ndocs;
553         this.syncNonHelpFiles; // ensure that helpTargetDir exist
554         classes = Class.allClasses.collectAs(_.name,IdentitySet).reject(_.isMetaClassName);
555         this.readDocMap;
556         this.postProgress("Getting metadata for all docs...");
558         //FIXME: if classtree changed, force total re-render (touch version timestamp)
560         ndocs = 0;
561         helpSourceDirs.do {|dir|
562             var x, path, mtime, ext, sym, class;
563             ext = (dir != helpSourceDir);
564             this.postProgress("- Collecting from"+dir);
565             Platform.case(
566 //                \linux, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -printf '%p;%T@\n'"},
567                 \linux, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -not -name '*.ext.schelp' -exec stat -c \"%n;%Z\" {} +"},
568                 \osx, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -not -name '*.ext.schelp' -exec stat -f \"%N;%m\" {} +"}
569             ).unixCmdGetStdOutLines.do {|line|
570                 #path, mtime = line.split($;);
571                 subtarget = path[dir.size+1 ..].drop(-7);
572                 doc = doc_map[subtarget];
574                 if(subtarget.dirname=="Classes") {
575                     sym = subtarget.basename.asSymbol;
576                     class = sym.asClass;
577                 } {
578                     sym = nil;
579                 };
581                 //FIXME: if implementor class changed since last time, force a re-render.
582                 //if doc.redirect && doc.implementor != class.tryPerform(doc.redirect.asSymbol).asSymbol
584                 if(doc.isNil or: {mtime != doc.mtime}) {
585                     p.parseMetaData(path);
586                     this.addToDocMap(p,subtarget);
587                     doc = doc_map[subtarget];
588                     doc.methods = p.methodList;
589                     doc.keywords = p.keywordList;
590                     doc.mtime = mtime;
591                     doc.installed = if(ext){\extension}{\standard};
592                     if(sym.notNil) { // doc is a class-doc
593                         if(class.notNil) { // class exists
594                             doc.superclasses = class.superclasses.collect(_.name).reject(_.isMetaClassName);
595                             doc.subclasses = class.subclasses.collect(_.name).reject(_.isMetaClassName);
596                             x = p.findNode(\redirect).text.stripWhiteSpace;
597                             if(x.notEmpty) {
598                                 x = class.tryPerform(x.asSymbol);
599                                 x !? { doc.implementor = x.asSymbol };
600                             };
601                         } {
602                             doc.installed = \missing;
603                         };
604                     };
605                     update = true;
606                     ndocs = ndocs + 1;
607                 };
608                 doc.keep = true;
609                 if(sym.notNil) {
610                     classes.remove(sym);
611                 };
612                 this.maybeWait;
613             };
614         };
615         this.postProgress("Added"+ndocs+"new documents");
617         ndocs = 0;
618         helpSourceDirs.do {|dir|
619             var old;
620             ("find -L"+dir.escapeChar($ )+"-type f -name '*.ext.schelp'").unixCmdGetStdOutLines.do {|file|
621                 subtarget = file[dir.size+1 ..].drop(-11);
622                 doc = doc_map[subtarget];
623                 if(doc.notNil) {
624                     // FIXME: if this doc adds a method to a non-class doc, it will not show up in doc.methods...
625                     old = doc.additions.copy;
626                     doc.additions = doc.additions.add(file).asSet;
627                     update = update or: {doc.additions != old};
628                     ndocs = ndocs + 1;
629                     this.postProgress("Addition for"+subtarget+":"+file);
630                 } {
631                     warn("SCDoc: Ignoring additions for non-existing document:"+file);
632                 };
633                 this.maybeWait;
634             }
635         };
636         this.postProgress("Found"+ndocs+"document additions");
638         this.postProgress("Processing"+classes.size+"undocumented classes");
639         undocumentedClasses = classes;
640         ndocs = 0;
641         classes.do {|name|
642             var class;
643             subtarget = "Classes/"++name.asString;
644             if(doc_map[subtarget].isNil) {
645                 cats = "Undocumented classes";
646                 class = name.asClass;
647                 if(this.classHasArKrIr(class)) {
648                     cats = cats ++ ", UGens>Undocumented";
649                 };
650                 if(class.categories.notNil) {
651                     cats = cats ++ ", "++class.categories.join(", ");
652                 };
654                 p.root = [
655                     (tag:\class, text:name.asString),
656                     (tag:\categories, text:cats),
657                     (tag:\summary, text:"(not documented)")
658                 ];
659                 this.addToDocMap(p,subtarget);
660                 doc = doc_map[subtarget];
661                 doc.methods =
662                     (this.makeMethodList(class.class).collect{|m| "_*"++m}
663                     ++ this.makeMethodList(class).collect{|m| "_-"++m});
665                 doc.installed = if(class.filenameSymbol.asString.beginsWith(thisProcess.platform.classLibraryDir).not)
666                     {\extension}
667                     {\standard};
669                 doc.superclasses = class.superclasses.collect(_.name).reject(_.isMetaClassName);
670                 doc.subclasses = class.subclasses.collect(_.name).reject(_.isMetaClassName);
672                 ndocs = ndocs + 1;
673                 update = true;
674             };
675             doc_map[subtarget].keep = true;
676             this.maybeWait;
677         };
678         this.postProgress("Generated metadata for"+ndocs+"undocumented classes");
679         // NOTE: If we remove a Classes/Name.schelp for an existing class, the doc_map won't get updated.
680         // but this shouldn't happen in real-life anyhow..
682         ndocs = 0;
683         doc_map.pairsDo{|k,e|
684             if(e.keep!=true, {
685                 doc_map.removeAt(k);
686                 ndocs = ndocs + 1;
687                 update = true;
688             });
689             e.removeAt(\keep); //remove the key since we don't need it anymore
690         };
691         this.postProgress("Removed"+ndocs+"documents from cache");
693         if(update) {
694             this.writeDocMap;
695             this.postProgress("Writing JSON doc map");
696             this.docMapToJSON(helpTargetDir +/+ "docmap.js");
697             NotificationCenter.notify(SCDoc, \docMapDidUpdate);
698         };
699         this.postProgress("Done! time spent:"+(Main.elapsedTime-t)+"sec");
700     }
702     *findHelpFile {|str|
703         ^r.findHelpFile(str);
704     }
707 + String {
708     stripWhiteSpace {
709         var ws = [$\n, $\r, $\t, $\ ];
710         var a=0, b=this.size-1;
711         while({ ws.includes(this[a])},{a=a+1});
712         while({ ws.includes(this[b])},{b=b-1});
713         ^this.copyRange(a,b);
714     }
715         unixCmdGetStdOutLines {
716                 var pipe, lines, line;
718                 pipe = Pipe.new(this, "r");
719                 lines = Array.new;
720                 line = pipe.getLine;
721                 while({line.notNil}, {lines = lines.add(line); line = pipe.getLine; });
722                 pipe.close;
724                 ^lines;
725         }
729 + Method {
730     isExtensionOf {|class|
731         ^(
732             (this.filenameSymbol != class.filenameSymbol)
733             and:
734                 if((class!=Object) and: (class!=Meta_Object),
735                     {class.superclasses.includes(this.ownerClass).not},
736                     {true})
737         );
738     }