2 var <>path, // document "path" without basedir and extension, like "Classes/SinOsc"
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
15 <>keywords, // a list of keywords
23 var <>klass, <>implKlass, <>implements;
24 var <>isUndocumentedClass;
27 stream << "SCDocEntry(" << path.cs << ", " << title.cs << ", " << summary.cs << ")";
30 prJSONString {|stream, key, x|
31 if(x.isNil) { x = "" };
32 stream << "'" << key << "': \"" << x.escapeChar(34.asAscii) << "\",\n";
35 prJSONList {|stream, key, v|
36 if(v.isNil) { v = "" };
37 stream << "'" << key << "': [ " << v.collect{|x|"\""++x.escapeChar(34.asAscii)++"\""}.join(",") << " ],\n";
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);
52 this.prJSONString(stream, "oldhelp", oldHelp);
55 klass.superclasses !? {
56 this.prJSONList(stream, "superclasses", klass.superclasses.collect {|c|
61 this.prJSONList(stream, "subclasses", klass.subclasses.collect {|c|
66 this.prJSONString(stream, "implementor", implKlass.name.asString);
73 ^super.new.init(node,path);
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;
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) !? {
92 doc.summary = "Implements "++implements;
93 doc.categories = ["Redirect Class Implementors"];
97 doc.summary = "(Undocumented class)";
98 cats = ["Undocumented classes"];
99 if(SCDoc.classHasArKrIr(doc.klass)) {
100 cats = cats.add("UGens>Undocumented");
102 doc.klass.categories !? {
103 cats = cats ++ doc.klass.categories;
105 doc.categories = cats;
113 if(node.isNil) {^this};
114 #hdr, bdy = node.children;
116 isUndocumentedClass = false;
117 doccmethods = IdentitySet();
118 docimethods = IdentitySet();
119 docmethods = IdentitySet();
120 privcmethods = IdentitySet();
121 privimethods = IdentitySet();
122 undoccmethods = IdentitySet();
123 undocimethods = IdentitySet();
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} },
134 this.prScanMethodsKeywords(bdy);
136 warn("SCDoc:"+path+"has no title!");
137 title = "(Untitled)";
139 if(isClassDoc = (path.dirname=="Classes")) {
140 klass = title.asSymbol.asClass;
142 // warn("SCDoc:"+path++": No such class!");
144 if(title != path.basename ) {
145 warn("SCDoc:"+path++": Title and filename mismatch. Must be same for class docs!");
148 if(categories.isNil) {
149 warn("SCDoc:"+path+"has no categories!");
150 categories = ["Uncategorized"];
153 warn("SCDoc:"+path+"has no summary!");
154 summary = "(Missing summary)";
160 ^SCDoc.helpTargetDir +/+ path ++ ".html";
165 //FIXME: no need for the extra _ char..
166 docimethods.do {|name|
167 list = list.add("_-"++name.asString);
169 doccmethods.do {|name|
170 list = list.add("_*"++name.asString);
172 undocimethods.do {|name|
173 list = list.add("?-"++name.asString);
175 undoccmethods.do {|name|
176 list = list.add("?*"++name.asString);
178 docmethods.do {|name|
179 list = list.add("_."++name.asString);
188 (x = SCDoc.parseFilePartial(f)) !? {
189 this.prScanMethodsKeywords(x);
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;
203 if(redirect.notNil) {
204 try { implKlass = klass.perform(redirect.asSymbol) };
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);
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);
229 prAddMethodNames {|node, list|
230 node.children.do {|n|
231 list = list.add(n.text.asSymbol);
236 prAddCopyMethod {|node, list|
237 ^list.add(node.text.split($ )[1].drop(1))
240 prScanMethodsKeywords {|node|
242 // warn("FIXME: for some reason prScanMethodsKeywords was called on a nil node")
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) },
257 node.children.do {|n|
258 keywords = keywords.add(n.text);
262 node.children.do {|n|
263 this.prScanMethodsKeywords(n);
271 var <>id, <>text, <>children, <>makeDiv, notPrivOnly, <>sort;
274 stream << "SCDocNode(" << id << ", " << text.cs << ", " << children << ")";
278 ^children.detect {|node| node.id===id}
282 if(notPrivOnly.isNil) {
283 notPrivOnly = (children.detect {|x| x.id != \CPRIVATE and: {x.id != \IPRIVATE}}.notNil)
288 addDivAfter {|id, div, title, childs|
289 var node = this.findChild(id);
290 var mets = SCDocNode()
295 if(node.isNil) { //no subtree, create one
296 children = children.add(
297 node = SCDocNode().id_(id)
300 node.children = node.children.add(mets);
305 // FIXME: does this work correctly for prose before first section, etc?
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 }
316 children = children.sort {|a,b| a.sort<b.sort};
321 var sects = IdentitySet[\BODY,\CLASSMETHODS,\INSTANCEMETHODS,\SECTION,\SUBSECTION,\EXAMPLES];
322 var do_children = {|dest,childs|
326 if(sects.includes(node.id)) {
327 n = dest.detect {|x| (x.id==node.id) and: {x.text==node.text}};
329 dest = dest.add(node);
331 n.children = do_children.(n.children,node.children);
334 dest = dest.add(node);
340 children = do_children.(children,root2.children);
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)
350 node.children = node.children.add(node2);
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;
364 classvar helpSourceDirs;
366 *parseFileFull {|path|
367 ^this.prParseFile(path, 0)
369 *parseFilePartial {|path|
370 ^this.prParseFile(path, 1)
372 *parseFileMetaData {|dir,path|
373 var fullPath = dir +/+ path;
374 var subpath = path.drop(-7);
375 var entry, x = this.prParseFile(fullPath, 2);
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];
388 *prParseFile {|path, mode|
390 ^this.primitiveFailed
393 var f = {|x,cat="Old Helpfiles",indent=0|
396 if(v.isKindOf(Dictionary)) {
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);
406 doc = SCDocEntry(nil,"Old Help"+/+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;
420 *indexAllDocuments { |clearCache=false|
421 var now = Main.elapsedTime;
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|
433 {f.fullPath.endsWith(".ext.schelp")} {
435 key = f[dir.size+1 ..].drop(-11);
436 additions[key] = additions[key].add(f);
438 {f.extension=="schelp"} {
439 doc = this.parseFileMetaData(dir, f.fullPath.drop(dir.size+1));
441 documents[doc.path] = doc;
443 undocClasses.remove(doc.title.asSymbol);
449 nonHelpFiles = nonHelpFiles.add([f,f.drop(dir.size+1)]);
453 this.postMsg("Handling"+additions.size+"document additions...",1);
454 additions.pairsDo {|key, val|
455 doc = documents[key];
457 doc.setAdditions(val);
459 warn("SCDoc: Additions % for non-existent help file".format(val));
462 this.postMsg("Indexing undocumented methods...",1);
464 if(d.isClassDoc) { d.indexUndocumentedMethods };
466 this.postMsg("Adding entries for"+undocClasses.size+"undocumented classes...",1);
468 doc = SCDocEntry.newUndocClass(x);
469 documents[doc.path] = doc;
471 this.postMsg("Copying"+nonHelpFiles.size+"non-help files...",1);
473 var dest = SCDoc.helpTargetDir+/+x[1];
474 var folder = dest.dirname;
476 if(File.exists(dest).not or: {File.mtime(x[0]) > File.mtime(dest)}) {
478 File.copy(x[0],dest);
481 this.postMsg("Indexing old helpfiles...");
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);
492 if(documents.isNil) {
493 this.indexAllDocuments;
499 if(helpSourceDirs.isNil) {
500 this.postMsg("locating HelpSource folders...",2);
501 helpSourceDirs = [helpSourceDir]; // Note: an array will keep the order.
504 if(f.folderName=="HelpSource") {
505 helpSourceDirs = helpSourceDirs.add(f.fullPath.withoutTrailingSlash);
511 [thisProcess.platform.userExtensionDir, thisProcess.platform.systemExtensionDir].do {|dir|
512 find.(PathName(dir));
517 *exportDocMapJS {|path|
518 var f = File.open(path,"w");
520 this.documents.do {|doc|
527 *helpSourceDir_ {|path|
528 helpSourceDir = path.standardizePath;
531 *helpTargetDir_ {|path|
532 // if(path!=helpTargetDir) {didRun = false};
533 helpTargetDir = path.standardizePath;
536 *postMsg {|txt, lvl=0|
538 postln("SCDoc: "++txt);
540 if(thisThread.isKindOf(Routine)) { 0.yield; }
545 (root = this.parseFileFull(doc.fullPath)) !? {
546 doc.additions.do {|f|
547 (add = this.parseFilePartial(f)) !? {
548 root.children[1].merge(add);
551 this.handleCopyMethods(root);
556 *parseAndRender {|doc|
557 var dest = doc.destPath;
558 var root = this.parseDoc(doc);
560 this.postMsg("% -> %".format(doc.fullPath, dest),2);
561 this.renderer.renderToFile(dest, doc, root);
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);
580 if(path.endsWith(".html")) {
581 subtarget = path.drop(this.helpTargetDir.size+1).drop(-5);
582 doc = this.documents[subtarget];
584 if(doc.isUndocumentedClass) {
586 this.renderUndocClass(doc);
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);
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}}
602 this.parseAndRender(doc);
612 warn("SCDoc: Broken link:"+url);
617 this.helpSourceDir_(thisProcess.platform.classLibraryDir.dirname +/+ "HelpSource");
618 this.helpTargetDir_(thisProcess.platform.userAppSupportDir +/+ "Help");
619 renderer = SCDocHTMLRenderer;
623 ^#[\ar,\kr,\ir].collect {|m| c.class.findRespondingMethodFor(m).notNil }.reduce {|a,b| a or: b};
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);
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);
647 this.parseAndRender(doc);
651 this.postMsg("Done!");
654 *makeClassTemplate {|doc|
655 var name = doc.title;
656 var cats = doc.categories;
657 var class = doc.klass;
661 var txt,c,m,l,last,sym;
663 txt = "\nCLASSMETHODS::\n\n";
664 n = doc.undoccmethods;
667 txt = "\nINSTANCEMETHODS::\n\n";
668 n = doc.undocimethods;
672 txt = txt ++ "METHOD::" + x ++ "\n(describe method here)\n\n";
674 m = c.findRespondingMethodFor(sym.asSetter);
675 m = m ?? {c.findRespondingMethodFor(sym)};
680 if (i>0) { //skip 'this' (first arg)
681 txt = txt ++ "ARGUMENT:: ";
682 if(i==last and: {m.varArgs}) {
683 txt = txt ++ " ... ";
685 txt = txt ++ a ++ "\n(describe argument here)\n\n";
689 txt = txt ++ "returns:: (describe returnvalue here)\n\n";
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";
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..
710 body = SCDocNode().id_(\BODY).children_([
711 SCDocNode().id_(\DESCRIPTION).children_([
712 desc = SCDocNode().id_(\PROSE)
717 if(doc.implements.notNil) {
718 this.postMsg("Generating class redirect implementor doc: % for %".format(doc.title,doc.implements.name),2);
720 SCDocNode().id_(\TEXT).text_("Implements "),
721 SCDocNode().id_(\LINK).text_("Classes/"++doc.implements.name)
724 this.postMsg("Undocumented class: "++doc.title++", generating stub and template",2);
726 SCDocNode().id_(\TEXT).text_("This class is missing documentation.")
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")
733 SCDocNode().id_(\CODEBLOCK).text_(this.makeClassTemplate(doc))
738 this.renderer.renderToFile(doc.destPath, doc, node);
742 *getMethodDoc {|classname,methodname|
743 var doc, id, node, mname;
746 if((n.id == id) and: {n.children[0].children.detect{|x|x.text==mname}.notNil}) {
751 n2 = findmet.(n2,id);
762 warn("SCDoc.getMethodDoc(%, %): %".format(classname,methodname,txt));
765 doc = this.documents["Classes/"++classname];
766 if(doc.isNil or: {doc.fullPath.isNil}) {
767 err.("class document not found");
770 id = switch(methodname[0],
775 err.("methodname must be prefixed with '*', '-' or '.'");
779 mname = methodname.drop(1);
780 node = this.parseDoc(doc);
782 err.("could not parse class document");
785 node = findmet.(node);
787 err.("method not found");
793 *handleCopyMethods {|node|
796 #name, met = n.text.findRegexp("[^ ,]+").flop[1];
797 this.getMethodDoc(name, met);
800 node.children.do{|n,i|
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} },
806 this.handleCopyMethods(n);
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" };
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"
825 } ?? { "Classes" +/+ str ++ ".html" })
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 }
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);
843 unixCmdGetStdOutLines {
844 var pipe, lines, line;
846 pipe = Pipe.new(this, "r");
849 while({line.notNil}, {lines = lines.add(line); line = pipe.getLine; });
858 isExtensionOf {|class|
860 (this.filenameSymbol != class.filenameSymbol)
862 if((class!=Object) and: (class!=Meta_Object),
863 {class.superclasses.includes(this.ownerClass).not},