scide: LookupDialog - redo lookup on classes after partial lookup
[supercollider.git] / SCClassLibrary / SCDoc / SCDoc.sc
blobf51533c20dd62b2186aa846ee7339c9a437add34
1 SCDocEntry {
2     var <>path,     // document "path" without basedir and extension, like "Classes/SinOsc"
3         <>title,
4         <>summary,
5         <>redirect,
6         <>categories, // a list of categories
7         <>related,    // a list of related docs
8         <>docimethods, // a set of documented methods
9         <>doccmethods, // a set of documented methods
10         <>docmethods, // a set of documented methods
11         <>privcmethods, // a set of private methods
12         <>privimethods, // a set of private methods
13         <>undoccmethods,
14         <>undocimethods,
15         <>keywords,   // a list of keywords
16         <>additions,
17         <>isExtension,
18         <>mtime,
19         <>fullPath,
20         <>oldHelp;
22     var <>isClassDoc;
23     var <>klass, <>implKlass, <>implements;
24     var <>isUndocumentedClass;
26     printOn {|stream|
27         stream << "SCDocEntry(" << path.cs << ", " << title.cs << ", " << summary.cs << ")";
28     }
30     prJSONString {|stream, key, x|
31         if(x.isNil) { x = "" };
32         stream << "'" << key << "': \"" << x.escapeChar(34.asAscii) << "\",\n";
34     }
35     prJSONList {|stream, key, v|
36         if(v.isNil) { v = "" };
37         stream << "'" << key << "': [ " << v.collect{|x|"\""++x.escapeChar(34.asAscii)++"\""}.join(",") << " ],\n";
38     }
40     toJSON {|stream|
41         stream << "\"" << path.escapeChar(34.asAscii) << "\": {\n";
42         this.prJSONString(stream, "title", title);
43         this.prJSONString(stream, "path", path);
44         this.prJSONString(stream, "summary", summary);
45         this.prJSONString(stream, "installed", if(isExtension,"extension","standard")); //FIXME: also 'missing'.. better to have separate extension and missing booleans..
46         this.prJSONString(stream, "categories",
47             if(categories.notNil) {categories.join(", ")} {""}); // FIXME: export list instead
48         this.prJSONList(stream, "keywords", keywords);
49         this.prJSONList(stream, "related", related);
50         this.prJSONList(stream, "methods", this.makeMethodList);
51         if(oldHelp.notNil) {
52             this.prJSONString(stream, "oldhelp", oldHelp);
53         };
54         if(klass.notNil) {
55             klass.superclasses !? {
56                 this.prJSONList(stream, "superclasses", klass.superclasses.collect {|c|
57                     c.name.asString
58                 })
59             };
60             klass.subclasses !? {
61                 this.prJSONList(stream, "subclasses", klass.subclasses.collect {|c|
62                     c.name.asString
63                 })
64             };
65             implKlass !? {
66                 this.prJSONString(stream, "implementor", implKlass.name.asString);
67             }
68         };
69         stream << "},\n";
70     }
72     *new {|node,path|
73         ^super.new.init(node,path);
74     }
76     *newUndocClass {|name|
77         var doc = super.new.init(nil,"Classes/"++name.asString);
78         var f, cats, implements, c;
79         doc.klass = name.asSymbol.asClass;
80         doc.isClassDoc = true;
81         doc.isUndocumentedClass = true;
82         doc.title = name.asString;
83         f = doc.klass.filenameSymbol.asString;
84         doc.mtime = 0;
85         doc.isExtension = f.beginsWith(Platform.classLibraryDir).not;
86         doc.undoccmethods = doc.klass.class.methods.collectAs({|m|m.name.asGetter},IdentitySet);
87         doc.undocimethods = doc.klass.methods.collectAs({|m|m.name.asGetter},IdentitySet);
89         if((implements=doc.klass.tryPerform(\implementsClass)).class===Symbol) {
90             (c = implements.asClass) !? {
91                 doc.implements = c;
92                 doc.summary = "Implements "++implements;
93                 doc.categories = ["Redirect Class Implementors"];
94                 ^doc;
95             };
96         };
97         doc.summary = "(Undocumented class)";
98         cats = ["Undocumented classes"];
99         if(SCDoc.classHasArKrIr(doc.klass)) {
100             cats = cats.add("UGens>Undocumented");
101         };
102         doc.klass.categories !? {
103             cats = cats ++ doc.klass.categories;
104         };
105         doc.categories = cats;
107         ^doc;
108     }
110     init {|node,aPath|
111         var hdr, bdy;
112         path = aPath;
113         if(node.isNil) {^this};
114         #hdr, bdy = node.children;
115         isExtension = false;
116         isUndocumentedClass = false;
117         doccmethods = IdentitySet();
118         docimethods = IdentitySet();
119         docmethods = IdentitySet();
120         privcmethods = IdentitySet();
121         privimethods = IdentitySet();
122         undoccmethods = IdentitySet();
123         undocimethods = IdentitySet();
124         hdr.children.do {|n|
125             switch(n.id,
126                 \TITLE, { title = n.text },
127                 \CLASS, { title = n.text }, // not used anymore?
128                 \SUMMARY, { summary = n.text },
129                 \REDIRECT, { redirect = n.text },
130                 \CATEGORIES, { categories = n.children.collect {|child| child.text} },
131                 \RELATED, { related = n.children.collect {|child| child.text} },
132             );
133         };
134         this.prScanMethodsKeywords(bdy);
135         if(title.isNil) {
136             warn("SCDoc:"+path+"has no title!");
137             title = "(Untitled)";
138         };
139         if(isClassDoc = (path.dirname=="Classes")) {
140             klass = title.asSymbol.asClass;
141 //            if(klass.isNil) {
142 //                warn("SCDoc:"+path++": No such class!");
143 //            };
144             if(title != path.basename ) {
145                 warn("SCDoc:"+path++": Title and filename mismatch. Must be same for class docs!");
146             };
147         };
148         if(categories.isNil) {
149             warn("SCDoc:"+path+"has no categories!");
150             categories = ["Uncategorized"];
151         };
152         if(summary.isNil) {
153             warn("SCDoc:"+path+"has no summary!");
154             summary = "(Missing summary)";
155         };
157     }
159     destPath {
160         ^SCDoc.helpTargetDir +/+ path ++ ".html";
161     }
163     makeMethodList {
164         var list;
165         //FIXME: no need for the extra _ char..
166         docimethods.do {|name|
167             list = list.add("_-"++name.asString);
168         };
169         doccmethods.do {|name|
170             list = list.add("_*"++name.asString);
171         };
172         undocimethods.do {|name|
173             list = list.add("?-"++name.asString);
174         };
175         undoccmethods.do {|name|
176             list = list.add("?*"++name.asString);
177         };
178         docmethods.do {|name|
179             list = list.add("_."++name.asString);
180         };
181         ^list;
182     }
184     setAdditions {|a|
185         var x;
186         additions = a;
187         a.do {|f|
188             (x = SCDoc.parseFilePartial(f)) !? {
189                 this.prScanMethodsKeywords(x);
190             }
191         }
192     }
194     indexUndocumentedMethods {
195         var ignoreMethods = IdentitySet[\categories, \init, \checkInputs, \new1, \argNamesInputsOffset, \initClass, \storeArgs, \storeOn, \printOn];
196         var syms, name, mets, l = Array.new;
197         var docmets = IdentitySet.new;
199         if(klass.isNil) {
200             ^this
201         };
203         if(redirect.notNil) {
204             try { implKlass = klass.perform(redirect.asSymbol) };
205         } {
206             implKlass = nil;
207         };
209         docmets = docimethods | privimethods | ignoreMethods;
210         (mets = klass.methods) !? {
211             mets.collectAs({|m|m.name.asGetter},IdentitySet).do {|name|
212                 if(docmets.includes(name).not) {
213                     undocimethods = undocimethods.add(name);
214                 };
215             };
216         };
218         docmets = doccmethods | privcmethods | ignoreMethods;
219         (mets = klass.class.methods) !? {
220             mets.collectAs({|m|m.name.asGetter},IdentitySet).do {|name|
221                 if(docmets.includes(name).not) {
222                     undoccmethods = undoccmethods.add(name);
223                 };
224             };
225         };
227     }
229     prAddMethodNames {|node, list|
230         node.children.do {|n|
231             list = list.add(n.text.asSymbol);
232         }
233         ^list;
234     }
236     prAddCopyMethod {|node, list|
237         ^list.add(node.text.split($ )[1].drop(1))
238     }
240     prScanMethodsKeywords {|node|
241         if(node.isNil) {
242 //            warn("FIXME: for some reason prScanMethodsKeywords was called on a nil node")
243             ^this;
244         };
245         switch(node.id,
246             \METHOD, { docmethods = this.prAddMethodNames(node.children[0], docmethods) },
247             \CMETHOD, { doccmethods = this.prAddMethodNames(node.children[0], doccmethods) },
248             \IMETHOD, { docimethods = this.prAddMethodNames(node.children[0], docimethods) },
250             \COPYMETHOD, { docmethods = this.prAddCopyMethod(node,docmethods) },
251             \CCOPYMETHOD, { doccmethods = this.prAddCopyMethod(node,doccmethods) },
252             \ICOPYMETHOD, { docimethods = this.prAddCopyMethod(node,docimethods) },
254             \CPRIVATE, { privcmethods = this.prAddMethodNames(node, privcmethods) },
255             \IPRIVATE, { privimethods = this.prAddMethodNames(node, privimethods) },
256             \KEYWORD, {
257                 node.children.do {|n|
258                     keywords = keywords.add(n.text);
259                 }
260             },
261             {
262                 node.children.do {|n|
263                     this.prScanMethodsKeywords(n);
264                 }
265             }
266         );
267     }
270 SCDocNode {
271     var <>id, <>text, <>children, <>makeDiv, notPrivOnly, <>sort;
273     printOn {|stream|
274         stream << "SCDocNode(" << id << ", " << text.cs << ", " << children << ")";
275     }
277     findChild {|id|
278         ^children.detect {|node| node.id===id}
279     }
281     notPrivOnly {
282         if(notPrivOnly.isNil) {
283             notPrivOnly = (children.detect {|x| x.id != \CPRIVATE and: {x.id != \IPRIVATE}}.notNil)
284         };
285         ^notPrivOnly
286     }
288     addDivAfter {|id, div, title, childs|
289         var node = this.findChild(id);
290         var mets = SCDocNode()
291             .id_(\SUBSECTION)
292             .text_(title)
293             .children_(childs)
294             .makeDiv_(div);
295         if(node.isNil) { //no subtree, create one
296             children = children.add(
297                 node = SCDocNode().id_(id)
298             );
299         };
300         node.children = node.children.add(mets);
301     }
303     sortClassDoc {
304         var x = 0;
305         // FIXME: does this work correctly for prose before first section, etc?
306         children.do {|n|
307             switch(n.id,
308                 \DESCRIPTION,       { n.sort = 10 },
309                 \CLASSMETHODS,      { n.sort = 11 },
310                 \INSTANCEMETHODS,   { n.sort = 12 },
311                 \EXAMPLES,          { n.sort = 13 },
312                 \SECTION, { n.sort = 14 + x = x + 1 },
313                 { n.sort = x = x + 1 }
314             );
315         };
316         children = children.sort {|a,b| a.sort<b.sort};
317     }
319     merge {|root2|
320         var n;
321         var sects = IdentitySet[\BODY,\CLASSMETHODS,\INSTANCEMETHODS,\SECTION,\SUBSECTION,\EXAMPLES];
322         var do_children = {|dest,childs|
323             var res;
324             childs !? {
325                 childs.do {|node|
326                     if(sects.includes(node.id)) {
327                         n = dest.detect {|x| (x.id==node.id) and: {x.text==node.text}};
328                         if(n.isNil) {
329                             dest = dest.add(node);
330                         } {
331                             n.children = do_children.(n.children,node.children);
332                         }
333                     } {
334                         dest = dest.add(node);
335                     }
336                 }
337             };
338             dest;
339         };
340         children = do_children.(children,root2.children);
341     }
343 /*    addNodeAfter {|id, node2|
344         var node = this.findChild(id);
345         if(node.isNil, { //no subtree, create one
346             children = children.add(
347                 node = SCDocNode().id_(id)
348             );
349         });
350         node.children = node.children.add(node2);
351     }
355 SCDoc {
356     // Increment this whenever we make a change to the SCDoc system so that all help-files should be processed again
357     classvar version = 55;
359     classvar <helpTargetDir;
360     classvar <helpSourceDir;
361     classvar <>verbosity = 1;
362     classvar <>renderer;
363     classvar documents;
364     classvar helpSourceDirs;
366     *parseFileFull {|path|
367         ^this.prParseFile(path, 0)
368     }
369     *parseFilePartial {|path|
370         ^this.prParseFile(path, 1)
371     }
372     *parseFileMetaData {|dir,path|
373         var fullPath = dir +/+ path;
374         var subpath = path.drop(-7);
375         var entry, x = this.prParseFile(fullPath, 2);
376         if(x.isNil) {^nil};
377         entry = SCDocEntry(x, subpath);
378         entry.isExtension = (dir != this.helpSourceDir);
379         entry.fullPath = fullPath;
380         entry.mtime = File.mtime(fullPath);
382         if(dir.beginsWith(Platform.userExtensionDir +/+ "quarks")) {
383             entry.categories = entry.categories ++ ["Quarks>"++dir.dirname.basename];
384         };
386         ^entry;
387     }
388     *prParseFile {|path, mode|
389         _SCDoc_ParseFile
390         ^this.primitiveFailed
391     }
392     *indexOldHelp {
393         var f = {|x,cat="Old Helpfiles",indent=0|
394             var a,b,doc;
395             x.pairsDo {|k,v|
396                 if(v.isKindOf(Dictionary)) {
397                     k = k.asString;
398                     a = 0;
399                     b = k.size-1;
400                     while({ $[ == k[a]},{a=a+1});
401                     while({ $] == k[b]},{b=b-1});
402                     k = k.copyRange(a,b);
403                     f.(v,cat++">"++k.asString,indent+1);
404                 } {
405                     if(v.size>0) {
406                         doc = SCDocEntry(nil,"Old Help"+/+v);
407                         doc.oldHelp = v;
408                         doc.title = v.basename;
409                         doc.summary = "(not yet converted to new help format)";
410                         doc.categories = [cat];
411                         doc.isExtension = true;
412                         SCDoc.documents[doc.path] = doc;
413                     }
414                 }
415             }
416         };
417         Help.rebuildTree;
418         f.(Help.tree);
419     }
420     *indexAllDocuments { |clearCache=false|
421         var now = Main.elapsedTime;
422         var key, doc;
423         var nonHelpFiles;
424         var undocClasses = Class.allClasses.reject(_.isMetaClass).collectAs({|c|c.name},IdentitySet);
425         var additions = Dictionary();
426         this.checkVersion(clearCache);
427         this.postMsg("Indexing help-files...",0);
428         documents = Dictionary(); // or use IdDict and symbols as keys?
429         helpSourceDirs = nil; // force re-scan of HelpSource folders
430         this.helpSourceDirs.do {|dir|
431             PathName(dir).filesDo {|f|
432                 case
433                 {f.fullPath.endsWith(".ext.schelp")} {
434                     f = f.fullPath;
435                     key = f[dir.size+1 ..].drop(-11);
436                     additions[key] = additions[key].add(f);
437                 }
438                 {f.extension=="schelp"} {
439                     doc = this.parseFileMetaData(dir, f.fullPath.drop(dir.size+1));
440                     doc !? {
441                         documents[doc.path] = doc;
442                         if(doc.isClassDoc) {
443                             undocClasses.remove(doc.title.asSymbol);
444                         }
445                     }
446                 }
447                 {
448                     f = f.fullPath;
449                     nonHelpFiles = nonHelpFiles.add([f,f.drop(dir.size+1)]);
450                 };
451             }
452         };
453         this.postMsg("Handling"+additions.size+"document additions...",1);
454         additions.pairsDo {|key, val|
455             doc = documents[key];
456             if(doc.notNil) {
457                 doc.setAdditions(val);
458             } {
459                 warn("SCDoc: Additions % for non-existent help file".format(val));
460             }
461         };
462         this.postMsg("Indexing undocumented methods...",1);
463         documents.do {|d|
464             if(d.isClassDoc) { d.indexUndocumentedMethods };
465         };
466         this.postMsg("Adding entries for"+undocClasses.size+"undocumented classes...",1);
467         undocClasses.do {|x|
468             doc = SCDocEntry.newUndocClass(x);
469             documents[doc.path] = doc;
470         };
471         this.postMsg("Copying"+nonHelpFiles.size+"non-help files...",1);
472         nonHelpFiles.do {|x|
473             var dest = SCDoc.helpTargetDir+/+x[1];
474             var folder = dest.dirname;
475             File.mkdir(folder);
476             if(File.exists(dest).not or: {File.mtime(x[0]) > File.mtime(dest)}) {
477                 File.delete(dest);
478                 File.copy(x[0],dest);
479             };
480         };
481         this.postMsg("Indexing old helpfiles...");
482         this.indexOldHelp;
483         this.postMsg("Exporting docmap.js...",1);
484         this.exportDocMapJS(this.helpTargetDir +/+ "docmap.js");
485         this.postMsg("Indexed % documents in % seconds".format(documents.size,round(Main.elapsedTime-now,0.01)),0);
486         NotificationCenter.notify(SCDoc, \didIndexAllDocs);
487     }
488     *didIndexDocuments {
489         ^documents.notNil
490     }
491     *documents {
492         if(documents.isNil) {
493             this.indexAllDocuments;
494         };
495         ^documents;
496     }
497     *helpSourceDirs {
498         var find;
499         if(helpSourceDirs.isNil) {
500             this.postMsg("locating HelpSource folders...",2);
501             helpSourceDirs = [helpSourceDir]; // Note: an array will keep the order.
502             find = {|dir|
503                 dir.folders.do {|f|
504                     if(f.folderName=="HelpSource") {
505                         helpSourceDirs = helpSourceDirs.add(f.fullPath.withoutTrailingSlash);
506                     } {
507                         find.(f);
508                     };
509                 }
510             };
511             [thisProcess.platform.userExtensionDir, thisProcess.platform.systemExtensionDir].do {|dir|
512                 find.(PathName(dir));
513             };
514        };
515        ^helpSourceDirs
516     }
517     *exportDocMapJS {|path|
518         var f = File.open(path,"w");
519         f << "docmap = {\n";
520         this.documents.do {|doc|
521             doc.toJSON(f);
522         };
523         f << "}\n";
524         f.close;
525     }
527     *helpSourceDir_ {|path|
528         helpSourceDir = path.standardizePath;
529     }
531     *helpTargetDir_ {|path|
532 //        if(path!=helpTargetDir) {didRun = false};
533         helpTargetDir = path.standardizePath;
534     }
536     *postMsg {|txt, lvl=0|
537         if(verbosity>lvl) {
538             postln("SCDoc: "++txt);
539         };
540         if(thisThread.isKindOf(Routine)) { 0.yield; }
541     }
543     *parseDoc {|doc|
544         var add, root;
545         (root = this.parseFileFull(doc.fullPath)) !? {
546             doc.additions.do {|f|
547                 (add = this.parseFilePartial(f)) !? {
548                     root.children[1].merge(add);
549                 }
550             };
551             this.handleCopyMethods(root);
552         };
553         ^root;
554     }
556     *parseAndRender {|doc|
557         var dest = doc.destPath;
558         var root = this.parseDoc(doc);
559         root !? {
560             this.postMsg("% -> %".format(doc.fullPath, dest),2);
561             this.renderer.renderToFile(dest, doc, root);
562         }
563     }
565     *prepareHelpForURL {|url|
566         var proto, path, query, anchor;
567         var subtarget, src, c, cmd, doc, destExist, destMtime;
568         var verpath = this.helpTargetDir +/+ "version";
570         url = url.replace("%20"," ");
571         #proto, path, query, anchor = url.findRegexp("(^\\w+://)?([^#?]+)(\\?[^#]+)?(#.*)?")[1..].flop[1];
572         if(proto.isEmpty) {proto="file://"; url = proto++url};
573         if(proto!="file://") {^url}; // just pass through remote url's
574         if(path.beginsWith(helpTargetDir).not) {^url}; // just pass through non-help url's
576         if(destExist = File.existsCaseSensitive(path)) {
577             destMtime = File.mtime(path);
578         };
580         if(path.endsWith(".html")) {
581             subtarget = path.drop(this.helpTargetDir.size+1).drop(-5);
582             doc = this.documents[subtarget];
583             doc !? {
584                 if(doc.isUndocumentedClass) {
585                     if(doc.mtime == 0) {
586                         this.renderUndocClass(doc);
587                         doc.mtime = 1;
588                     };
589                     ^url;
590                 };
591                 if(File.mtime(doc.fullPath)>doc.mtime) { // src changed after indexing
592                     this.postMsg("% changed, re-indexing documents".format(doc.path),2);
593                     this.indexAllDocuments;
594                     ^this.prepareHelpForURL(url);
595                 };
596                 if(destExist.not
597                     or: {doc.mtime>destMtime}
598                     or: {doc.additions.detect {|f| File.mtime(f)>destMtime}.notNil}
599                     or: {File.mtime(this.helpTargetDir +/+ "scdoc_version")>destMtime}
600                     or: {doc.klass.notNil and: {File.mtime(doc.klass.filenameSymbol.asString)>destMtime}}
601                 ) {
602                     this.parseAndRender(doc);
603                 };
604                 ^url;
605             };
606         };
608         if(destExist) {
609             ^url;
610         };
612         warn("SCDoc: Broken link:"+url);
613         ^nil;
614     }
616     *initClass {
617         this.helpSourceDir_(thisProcess.platform.classLibraryDir.dirname +/+ "HelpSource");
618         this.helpTargetDir_(thisProcess.platform.userAppSupportDir +/+ "Help");
619         renderer = SCDocHTMLRenderer;
620     }
622     *classHasArKrIr {|c|
623         ^#[\ar,\kr,\ir].collect {|m| c.class.findRespondingMethodFor(m).notNil }.reduce {|a,b| a or: b};
624     }
626     *checkVersion {|clearCache=false|
627         var f, path = this.helpTargetDir +/+ "scdoc_version";
628         if(clearCache or: {path.load != version}) {
629             this.postMsg("refreshing scdoc version timestamp",1);
630             // this will update the mtime of the version file, triggering re-rendering of files older than now
631             File.mkdir(this.helpTargetDir);
632             f = File.open(path,"w");
633             f.write(version.asCompileString);
634             f.close;
635             ^true;
636         };
637         ^false;
638     }
640     *renderAll {|includeExtensions=true|
641         this.postMsg("Rendering all documents");
642         this.documents.do {|doc|
643             if(includeExtensions or: {doc.isExtension.not}) {
644                 if(doc.isUndocumentedClass) {
645                     this.renderUndocClass(doc);
646                 } {
647                     this.parseAndRender(doc);
648                 }
649             }
650         };
651         this.postMsg("Done!");
652     }
654     *makeClassTemplate {|doc|
655         var name = doc.title;
656         var cats = doc.categories;
657         var class = doc.klass;
658         var n, m, f, c;
660         f = {|cm|
661             var txt,c,m,l,last,sym;
662             if(cm) {
663                 txt = "\nCLASSMETHODS::\n\n";
664                 n = doc.undoccmethods;
665                 c = class.class;
666             } {
667                 txt = "\nINSTANCEMETHODS::\n\n";
668                 n = doc.undocimethods;
669                 c = class;
670             };
671             n.do {|x|
672                 txt = txt ++ "METHOD::" + x ++ "\n(describe method here)\n\n";
673                 sym = x.asSymbol;
674                 m = c.findRespondingMethodFor(sym.asSetter);
675                 m = m ?? {c.findRespondingMethodFor(sym)};
676                 m !? {
677                     l = m.argNames;
678                     last = l.size-1;
679                     l.do {|a,i|
680                         if (i>0) { //skip 'this' (first arg)
681                             txt = txt ++ "ARGUMENT:: ";
682                             if(i==last and: {m.varArgs}) {
683                                 txt = txt ++ " ... ";
684                             };
685                             txt = txt ++ a ++ "\n(describe argument here)\n\n";
686                         }
687                     }
688                 };
689                 txt = txt ++ "returns:: (describe returnvalue here)\n\n";
690             };
691             txt;
692         };
694         ^ "TITLE::"+name
695             ++"\nsummary:: (put short description here)\n"
696             ++"categories::"+cats.join(", ")
697             ++"\nrelated:: Classes/SomeRelatedClass, Reference/SomeRelatedStuff, etc.\n\n"
698             ++"DESCRIPTION::\n(put long description here)\n\n"
699             ++ f.(true) ++ f.(false)
700             ++"\nEXAMPLES::\n\ncode::\n(some example code)\n::\n";
701      }
703     *renderUndocClass {|doc|
704         var node, desc, body;
706         node = SCDocNode().id_(\DOCUMENT).children_([
707             SCDocNode().id_(\HEADER).children_([
708                 // the header content is already in the SCDocEntry..
709             ]),
710             body = SCDocNode().id_(\BODY).children_([
711                 SCDocNode().id_(\DESCRIPTION).children_([
712                     desc = SCDocNode().id_(\PROSE)
713                 ])
714             ])
715         ]);
717         if(doc.implements.notNil) {
718             this.postMsg("Generating class redirect implementor doc: % for %".format(doc.title,doc.implements.name),2);
719             desc.children = [
720                 SCDocNode().id_(\TEXT).text_("Implements "),
721                 SCDocNode().id_(\LINK).text_("Classes/"++doc.implements.name)
722             ];
723         } {
724             this.postMsg("Undocumented class: "++doc.title++", generating stub and template",2);
725             desc.children = [
726                 SCDocNode().id_(\TEXT).text_("This class is missing documentation.")
727             ];
728             body.children = body.children.add(
729                 SCDocNode().id_(\SECTION).text_("Help template").children_([
730                     SCDocNode().id_(\PROSE).children_([
731                         SCDocNode().id_(\TEXT).text_("Copy and paste the text below and save to HelpSource/Classes/"++doc.title++".schelp")
732                     ]),
733                     SCDocNode().id_(\CODEBLOCK).text_(this.makeClassTemplate(doc))
734                 ])
735             );
736         };
738         this.renderer.renderToFile(doc.destPath, doc, node);
739     }
742     *getMethodDoc {|classname,methodname|
743         var doc, id, node, mname;
745         var findmet = {|n|
746             if((n.id == id) and: {n.children[0].children.detect{|x|x.text==mname}.notNil}) {
747                 n;
748             } {
749                 block {|break|
750                     n.children.do {|n2|
751                         n2 = findmet.(n2,id);
752                         if(n2.notNil) {
753                             break.value(n2);
754                         }
755                     };
756                     nil;
757                 }
758             }
759         };
761         var err = {|txt|
762             warn("SCDoc.getMethodDoc(%, %): %".format(classname,methodname,txt));
763         };
765         doc = this.documents["Classes/"++classname];
766         if(doc.isNil or: {doc.fullPath.isNil}) {
767             err.("class document not found");
768             ^nil;
769         };
770         id = switch(methodname[0],
771             $*, \CMETHOD,
772             $-, \IMETHOD,
773             $., \METHOD,
774             {
775                 err.("methodname must be prefixed with '*', '-' or '.'");
776                 ^nil;
777             }
778         );
779         mname = methodname.drop(1);
780         node = this.parseDoc(doc);
781         if(node.isNil) {
782             err.("could not parse class document");
783             ^nil;
784         };
785         node = findmet.(node);
786         if(node.isNil) {
787             err.("method not found");
788             ^nil;
789         };
790         ^node;
791     }
793     *handleCopyMethods {|node|
794         var found = {|n|
795             var name, met;
796             #name, met = n.text.findRegexp("[^ ,]+").flop[1];
797             this.getMethodDoc(name, met);
798         };
800         node.children.do{|n,i|
801             switch(n.id,
802                 \CCOPYMETHOD, { n = found.(n); n !? {n.id_(\CMETHOD); node.children[i] = n} },
803                 \ICOPYMETHOD, { n = found.(n); n !? {n.id_(\IMETHOD); node.children[i] = n} },
804                 \COPYMETHOD, { n = found.(n); n !? {n.id_(\METHOD); node.children[i] = n} },
805                 {
806                     this.handleCopyMethods(n);
807                 }
808             );
809         };
810     }
812     *findHelpFile {|str|
813 //        ^r.findHelpFile(str.stripWhiteSpace);
814         var old, sym, pfx = "file://" ++ SCDoc.helpTargetDir;
816         if(str.isNil or: {str.isEmpty}) { ^pfx +/+ "Help.html" };
817         if(this.documents[str].notNil) { ^pfx +/+ str ++ ".html" };
819         sym = str.asSymbol;
820         if(sym.asClass.notNil) {
821             ^pfx +/+ (if(this.documents["Classes"+/+str].isUndocumentedClass) {
822                 (old = Help.findHelpFile(str)) !? {
823                     "OldHelpWrapper.html#"++old++"?"++SCDoc.helpTargetDir +/+ "Classes" +/+ str ++ ".html"
824                 }
825             } ?? { "Classes" +/+ str ++ ".html" })
826         };
828         if(str.last == $_) { str = str.drop(-1) };
829         ^pfx +/+ if("^[a-z][a-zA-Z0-9_]*$|^[-<>@|&%*+/!?=]+$".matchRegexp(str)) {
830             "Overviews/Methods.html#" ++ str
831         } { "Search.html#" ++ str }
832     }
835 + String {
836     stripWhiteSpace {
837         var ws = [$\n, $\r, $\t, $\ ];
838         var a=0, b=this.size-1;
839         while({ ws.includes(this[a])},{a=a+1});
840         while({ ws.includes(this[b])},{b=b-1});
841         ^this.copyRange(a,b);
842     }
843         unixCmdGetStdOutLines {
844                 var pipe, lines, line;
846                 pipe = Pipe.new(this, "r");
847                 lines = Array.new;
848                 line = pipe.getLine;
849                 while({line.notNil}, {lines = lines.add(line); line = pipe.getLine; });
850                 pipe.close;
852                 ^lines;
853         }
857 + Method {
858     isExtensionOf {|class|
859         ^(
860             (this.filenameSymbol != class.filenameSymbol)
861             and:
862                 if((class!=Object) and: (class!=Meta_Object),
863                     {class.superclasses.includes(this.ownerClass).not},
864                     {true})
865         );
866     }