2 // Increment this whenever we make a change to the SCDoc system so that all help-files should be processed again
5 classvar <helpTargetDir;
6 classvar <helpSourceDir;
7 classvar helpSourceDirs;
8 classvar doc_map = nil;
10 classvar <undocumentedClasses;
11 classvar <>verbose = false;
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;
24 this.findHelpSourceDirs;
28 *helpTargetDir_ {|path|
29 if(path!=helpTargetDir) {didRun = false};
30 helpTargetDir = path.standardizePath;
33 *postProgress {|string|
35 if(progressMax>0) {prg = (progressCount/progressMax*100).round(0.1).asString ++ "% "};
36 if(verbose) {("SCDoc:"+prg++string).postln};
44 f = File.open(path,"w");
46 f.write("docmap = {\n");
47 doc_map.pairsDo {|k,v|
48 f.write("'"++k++"':"+"{\n");
50 if(v2.isKindOf(Array)) {
51 v2 = "["+v2.collect{|x|"'"++x++"'"}.join(",")+"]";
53 v2 = "'"++v2.asString.replace("'","\\'")++"'";
55 f.write("'"++k2++"': "++v2++",\n");
65 ^txt.findRegexp("[^, ]+[^,]*[^, ]+").flop[1];
69 this.helpSourceDir_(thisProcess.platform.classLibraryDir.dirname +/+ "HelpSource");
70 this.helpTargetDir_(thisProcess.platform.userAppSupportDir +/+ "Help");
71 r = SCDocHTMLRenderer.new;
76 *addMethodList {|c,n,tag|
77 var l, x = this.makeMethodList(c);
79 l = x.collect{|m| (tag:\method, text:m)};
80 n = n.add((tag:tag, children:l));
86 var l, mets, name, syms;
88 (mets = c.methods) !? {
89 syms = mets.collectAs(_.name,IdentitySet);
90 mets.do {|m| //need to iterate over mets to keep the order
92 if (name.isSetter.not or: {syms.includes(name.asGetter).not}) {
93 l = l.add(name.asGetter.asString);
101 ^#[\ar,\kr,\ir].collect {|m| c.class.findRespondingMethodFor(m).notNil }.reduce {|a,b| a or: b};
104 *addToDocMap {|parser, path|
105 var folder = path.dirname, classname = parser.findNode(\class).text;
108 summary:parser.findNode(\summary).text,
109 categories:parser.findNode(\categories).text
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::");
118 if(doc.summary.isEmpty) {
119 warn("Document at"+path+"has no summary::");
121 if(doc.categories.isEmpty) {
122 warn("Document at"+path+"has no categories::");
125 if(classname.notEmpty) {
126 if(path.basename != doc.title) {
127 warn("Document at"+path+"is not named according to class name:"+doc.title);
129 if(folder != "Classes") {
130 warn("Document at"+path+"is a class doc but is not in Classes/ folder");
133 if(folder == "Classes") {
134 warn("Document at"+path+"is not a class doc but is in Classes/ folder");
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"];
149 if (map[cat].isNil) {
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);
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");
185 doc_map = Dictionary.new;
192 var f, path = this.helpTargetDir +/+ "scdoc_cache";
194 f = File.open(path,"w");
195 f.write(doc_map.asCompileString);
206 *tickProgress { progressCount = progressCount + 1 }
210 if(doWait and: {(t = Main.elapsedTime)-lastUITick > 0.1}) {
216 *parseAndRender {|src,dest,subtarget|
218 SCDoc.postProgress(src+"->"+dest);
221 // hackish workaround: if a doc addition is removed, it is not detected
222 // and removed from the 'additions' set in the docmap entry.
223 // so here we check that the file exists..
224 doc_map[subtarget].additions.select(File.exists(_)).do {|ext|
225 p2 = p2 ?? {p.class.new};
230 r.render(p,dest,subtarget);
233 *renderAll {|force=false,threaded=true,findExtensions=true,doneFunc|
234 var count, count2, func, x, fileList, subtarget, dest, t = Main.elapsedTime, oldVerbose = verbose;
241 this.cleanState(force);
243 this.findHelpSourceDirs;
245 helpSourceDirs = Set[helpSourceDir];
251 fileList = Dictionary.new;
253 this.postProgress("Updating all files");
254 helpSourceDirs.do {|dir|
255 fileList[dir] = ("find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -not -name '*.ext.schelp'")
256 .unixCmdGetStdOutLines.reject(_.isEmpty).asSet;
257 count = count + fileList[dir].size;
259 count2 = undocumentedClasses.size;
260 progressCount = 3 * ((count+count2)/progressMax);
261 progressMax = count+count2+progressCount;
263 this.postProgress("Found"+count+"help files");
264 helpSourceDirs.do {|dir|
265 x = fileList[dir].size;
267 fileList[dir].do {|path|
268 subtarget = path[dir.size+1 .. path.findBackwards(".")?path.size-1];
269 dest = helpTargetDir+/+subtarget++".html";
271 or: {("test"+path.escapeChar($ )+"-nt"+dest.escapeChar($ )+"-o ! -e"+dest.escapeChar($ )).systemCmd==0}) {
272 this.parseAndRender(path,dest,subtarget);
274 this.postProgress("Skipping"+dest);
280 this.postProgress("Found"+count2+"undocumented classes");
281 undocumentedClasses.do {|name|
282 dest = helpTargetDir+/+"Classes"+/+name++".html";
283 if(this.makeClassTemplate(name.asString,dest).not) {
284 this.postProgress("Skipping"+dest);
288 this.postProgress("Done! time spent:"+(Main.elapsedTime-t)+"sec");
289 verbose = oldVerbose;
295 Routine(func).play(AppClock);
299 *cleanState {|noCache=false|
301 helpSourceDirs = nil;
303 doWait = thisThread.isKindOf(Routine);
304 this.syncNonHelpFiles; // ensure helpTargetDir exists
305 ("touch"+(helpTargetDir+/+"version").escapeChar($ )).systemCmd;
309 *findHelpSource {|subtarget|
311 this.findHelpSourceDirs;
313 helpSourceDirs.do {|dir|
314 var x = dir+/+subtarget++".schelp";
324 *prepareHelpForURL {|url|
325 var proto, path, query, anchor;
326 var subtarget, src, c, cmd;
327 var verpath = this.helpTargetDir +/+ "version";
329 doWait = thisThread.isKindOf(Routine);
332 "SCDoc: prepareHelpForURL already running.. waiting for the first to finish.".warn;
334 Error("SCDoc: cannot wait for already running prepareHelpForURL, this call was not made inside a Routine").throw;
338 while {0.5.wait; isProcessing};
347 url = url.replace("%20"," ");
348 #proto, path, query, anchor = url.findRegexp("(^\\w+://)?([^#?]+)(\\?[^#]+)?(#.*)?")[1..].flop[1];
349 if(proto.isEmpty) {proto="file://"};
350 if(proto!="file://") {isProcessing = false; ^url}; // just pass through remote url's
351 if(path.beginsWith(helpTargetDir).not) {isProcessing = false; ^url}; // just pass through remote url's
353 if(File.exists(helpTargetDir).not) {
357 // sync non-schelp files once every session
363 // strip to subfolder/basename (like Classes/SinOsc)
364 subtarget = path[helpTargetDir.size+1 .. path.findBackwards(".")?path.size-1];
366 // find help source file
367 src = this.findHelpSource(subtarget);
369 // create a simple stub if class was undocumented
370 if(src.isNil and: {subtarget.dirname=="Classes"}) {
371 this.makeClassTemplate(subtarget.basename,path);
374 if(File.exists(path).not) {
375 if(src.notNil) { // no target file, but helpsource found, parse and render.
376 this.parseAndRender(src,path,subtarget);
377 isProcessing = false;
380 this.postProgress("Broken link:"+url);
381 isProcessing = false;
386 ("test" + ([src,verpath]++doc_map[subtarget].additions).collect {|x|
387 x.escapeChar($ )+"-nt"+path.escapeChar($ )
388 }.join(" -o ")).systemCmd == 0
391 if(src.notNil and: cmd) {
392 // target file and helpsource exists, and helpsource is newer than target
393 this.parseAndRender(src,path,subtarget);
394 isProcessing = false;
397 // we reach here if the target URL exists and there's no schelp source file,
398 // or if the target URL does not need update.
401 isProcessing = false;
406 *makeClassTemplate {|name,path|
407 var class = name.asSymbol.asClass;
410 doWait = thisThread.isKindOf(Routine);
412 if(class.notNil and: {path.isNil
414 ("test"+(helpTargetDir+/+"version").escapeChar($ )+"-nt"+path.escapeChar($ )+"-o ! -e"+path.escapeChar($ )).systemCmd==0
417 this.postProgress("Undocumented class:"+name+", generating stub and template");
418 cats = "Undocumented classes";
419 if(this.classHasArKrIr(class)) {
420 cats = cats ++ ", UGens>Undocumented";
422 if(class.categories.notNil) {
423 cats = cats ++ ", "++class.categories.join(", ");
428 text:"This class is missing documentation. See the ",
432 text:"#help_template#generated help template",
437 (tag:\class, text:name),
438 (tag:\summary, text:"(not documented)"),
439 (tag:\categories, text:cats),
440 (tag:\description, children:m)
443 n = this.addMethodList(class.class,n,\classmethods);
444 n = this.addMethodList(class,n,\instancemethods);
447 var txt,c,m,l,last,sym;
448 if(tag==\classmethods) {
449 txt = "\nCLASSMETHODS::\n\n";
452 txt = "\nINSTANCEMETHODS::\n\n";
455 p.findNode(tag,n).children.do {|x|
457 txt = txt ++ "METHOD::" + x.text ++ "\n(describe method here)\n\n";
458 sym = x.text.asSymbol;
459 m = c.findRespondingMethodFor(sym.asSetter);
460 m = m ?? {c.findRespondingMethodFor(sym)};
465 if (i>0) { //skip 'this' (first arg)
466 txt = txt ++ "argument:: ";
467 if(i==last and: {m.varArgs}) {
468 txt = txt ++ " ... ";
470 txt = txt ++ a ++ "\n(describe argument here)\n\n";
475 txt = txt ++ "returns:: (returnvalue)\n\n";
481 ++"\nsummary:: (put short description here)\n"
482 ++"categories::"+cats
483 ++"\nrelated:: Classes/SomeRelatedClass, Reference/SomeRelatedStuff, etc.\n\n"
484 ++"DESCRIPTION::\n(put long description here)\n\n"
485 ++ f.(\classmethods) ++ f.(\instancemethods)
486 ++"\nEXAMPLES::\n\ncode::\n(some example code)\n::\n";
489 tag:\section, text:"Help Template", children:[
490 (tag:\prose, display:\block,
491 text:"Copy the template below or run"),
493 text:"Document.new(string:SCDoc.makeClassTemplate(\\"++name++"))",
496 text:"to open a new Document with the template.\nSave it to HelpSource/Classes/"++name++".schelp",
507 r.render(p,path,"Classes/"++name);
513 ^if(path.notNil,false,nil);
516 *checkSystemCmd {|cmd|
517 if(("which"+cmd+"> /dev/null").systemCmd != 0) {
518 Error("'"++cmd++"' is not installed. Please install it and try again.").throw;
522 *findHelpSourceDirs {
523 if(helpSourceDirs.notNil) {^this};
524 this.postProgress("Finding HelpSource folders...");
525 this.checkSystemCmd("find");
526 helpSourceDirs = Set[helpSourceDir];
527 [thisProcess.platform.userExtensionDir, thisProcess.platform.systemExtensionDir].do {|dir|
528 helpSourceDirs = helpSourceDirs | ("find -L"+dir.escapeChar($ )+"-name 'HelpSource' -type d -prune")
529 .unixCmdGetStdOutLines.asSet;
531 this.postProgress(helpSourceDirs.asString);
537 doWait = thisThread.isKindOf(Routine);
538 this.findHelpSourceDirs;
539 this.postProgress("Synchronizing non-schelp files...");
540 this.checkSystemCmd("rsync");
542 cmd = "rsync -rlt --chmod=u+rwX --exclude '*.schelp' --exclude '.*' %/ %";
546 helpSourceDirs.do {|dir|
547 cmd.format(dir.escapeChar($ ),helpTargetDir.escapeChar($ )).unixCmd({c.unhang},false);
551 helpSourceDirs.do {|dir|
552 cmd.format(dir.escapeChar($ ),helpTargetDir.escapeChar($ )).systemCmd;
555 this.postProgress("Synchronizing non-schelp files: Done");
559 var subtarget, classes, cats, t = Main.elapsedTime;
560 var update = false, doc, ndocs;
562 this.syncNonHelpFiles; // ensure that helpTargetDir exist
563 classes = Class.allClasses.collectAs(_.name,IdentitySet).reject(_.isMetaClassName);
565 this.postProgress("Getting metadata for all docs...");
567 //FIXME: if classtree changed, force total re-render (touch version timestamp)
570 helpSourceDirs.do {|dir|
571 var x, path, mtime, ext, sym, class;
572 ext = (dir != helpSourceDir);
573 this.postProgress("- Collecting from"+dir);
575 // \linux, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -printf '%p;%T@\n'"},
576 \linux, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -not -name '*.ext.schelp' -exec stat -c \"%n;%Z\" {} +"},
577 \osx, {"find -L"+dir.escapeChar($ )+"-type f -name '*.schelp' -not -name '*.ext.schelp' -exec stat -f \"%N;%m\" {} +"}
578 ).unixCmdGetStdOutLines.do {|line|
579 #path, mtime = line.split($;);
580 subtarget = path[dir.size+1 ..].drop(-7);
581 doc = doc_map[subtarget];
583 if(subtarget.dirname=="Classes") {
584 sym = subtarget.basename.asSymbol;
590 //FIXME: if implementor class changed since last time, force a re-render.
591 //if doc.redirect && doc.implementor != class.tryPerform(doc.redirect.asSymbol).asSymbol
593 if(doc.isNil or: {mtime != doc.mtime}) {
594 p.parseMetaData(path);
595 this.addToDocMap(p,subtarget);
596 doc = doc_map[subtarget];
597 if(dir.beginsWith(Platform.userExtensionDir +/+ "quarks")) {
598 x = "Quarks>"++dir.dirname.basename;
599 if(doc.categories.notEmpty) { x = x ++ ", " ++ doc.categories };
602 doc.methods = p.methodList;
603 doc.keywords = p.keywordList;
605 doc.installed = if(ext){\extension}{\standard};
606 if(sym.notNil) { // doc is a class-doc
607 if(class.notNil) { // class exists
608 doc.superclasses = class.superclasses.collect(_.name).reject(_.isMetaClassName);
609 doc.subclasses = class.subclasses.collect(_.name).reject(_.isMetaClassName);
610 x = p.findNode(\redirect).text.stripWhiteSpace;
612 x = class.tryPerform(x.asSymbol);
613 x !? { doc.implementor = x.asSymbol };
616 doc.installed = \missing;
629 this.postProgress("Added"+ndocs+"new documents");
632 helpSourceDirs.do {|dir|
634 ("find -L"+dir.escapeChar($ )+"-type f -name '*.ext.schelp'").unixCmdGetStdOutLines.do {|file|
635 subtarget = file[dir.size+1 ..].drop(-11);
636 doc = doc_map[subtarget];
638 // FIXME: if this doc adds a method to a non-class doc, it will not show up in doc.methods...
639 old = doc.additions.copy;
640 doc.additions = doc.additions.add(file).asSet;
641 // filter out non-existing files in case helpsource moved..
642 // this is more a hackish workaround than a good solution.
643 // it will not work if a doc addition is removed instead of moved.
644 doc.additions = doc.additions.select(File.exists(_));
645 update = update or: {doc.additions != old};
647 this.postProgress("Addition for"+subtarget+":"+file);
649 warn("SCDoc: Ignoring additions for non-existing document:"+file);
654 this.postProgress("Found"+ndocs+"document additions");
656 this.postProgress("Processing"+classes.size+"undocumented classes");
657 undocumentedClasses = classes;
661 subtarget = "Classes/"++name.asString;
662 if(doc_map[subtarget].isNil) {
663 cats = "Undocumented classes";
664 class = name.asClass;
665 dir = class.filenameSymbol.asString;
666 if(dir.beginsWith(Platform.userExtensionDir +/+ "quarks")) {
667 cats = cats ++ ", " ++ "Quarks>"++class.package;
670 if(this.classHasArKrIr(class)) {
671 cats = cats ++ ", UGens>Undocumented";
673 if(class.categories.notNil) {
674 cats = cats ++ ", "++class.categories.join(", ");
678 (tag:\class, text:name.asString),
679 (tag:\categories, text:cats),
680 (tag:\summary, text:"(not documented)")
682 this.addToDocMap(p,subtarget);
683 doc = doc_map[subtarget];
685 (this.makeMethodList(class.class).collect{|m| "_*"++m}
686 ++ this.makeMethodList(class).collect{|m| "_-"++m});
688 doc.installed = if(class.filenameSymbol.asString.beginsWith(thisProcess.platform.classLibraryDir).not)
692 doc.superclasses = class.superclasses.collect(_.name).reject(_.isMetaClassName);
693 doc.subclasses = class.subclasses.collect(_.name).reject(_.isMetaClassName);
698 doc_map[subtarget].keep = true;
701 this.postProgress("Generated metadata for"+ndocs+"undocumented classes");
702 // NOTE: If we remove a Classes/Name.schelp for an existing class, the doc_map won't get updated.
703 // but this shouldn't happen in real-life anyhow..
706 doc_map.pairsDo{|k,e|
712 e.removeAt(\keep); //remove the key since we don't need it anymore
714 this.postProgress("Removed"+ndocs+"documents from cache");
718 this.postProgress("Writing JSON doc map");
719 this.docMapToJSON(helpTargetDir +/+ "docmap.js");
720 NotificationCenter.notify(SCDoc, \docMapDidUpdate);
722 this.postProgress("Done! time spent:"+(Main.elapsedTime-t)+"sec");
726 ^r.findHelpFile(str.stripWhiteSpace);
732 var ws = [$\n, $\r, $\t, $\ ];
733 var a=0, b=this.size-1;
734 while({ ws.includes(this[a])},{a=a+1});
735 while({ ws.includes(this[b])},{b=b-1});
736 ^this.copyRange(a,b);
738 unixCmdGetStdOutLines {
739 var pipe, lines, line;
741 pipe = Pipe.new(this, "r");
744 while({line.notNil}, {lines = lines.add(line); line = pipe.getLine; });
753 isExtensionOf {|class|
755 (this.filenameSymbol != class.filenameSymbol)
757 if((class!=Object) and: (class!=Meta_Object),
758 {class.superclasses.includes(this.ownerClass).not},