15 var <methodList, <keywordList;
16 classvar copyMethodCache;
19 root = tree = List.new;
21 stack.add([tree,0,nil]);
29 proseDisplay = \block;
30 // doingInlineTag = false;
34 // ^super.newCopyArgs(filename).init;
39 // isTag {|word| ^"^(::[a-zA-Z]+|[a-zA-Z]+::)$".matchRegexp(word)}
40 // isOpeningTag {|word| ^"^[a-zA-Z]+::$".matchRegexp(word)}
41 // isClosingTag {|word| ^"^::[a-zA-Z]+$".matchRegexp(word)}
56 p[2] !? {proseDisplay = p[2].display};
61 stack.add([tree,level,nil]);
65 stack[stack.size-1][2] = n;
76 proseDisplay = current.display;
81 addTag {|tag, text="", children=false, display=\block|
84 tag = tag.asString.drop(-2).asSymbol;
85 current = node = (tag:tag, display:display, text:text, children:if(children,{List.new},{nil}));
87 if(children, {tree = current.children}); //recurse into children list
88 if(text.isNil) {this.endCurrent}; //we don't have any text field to add to for this tag, so start fresh..
92 handleWord {|word,lineno,wordno|
93 var tag = word.toLower.asSymbol;
100 var noNameSection = {
101 singleline = true; //this doesn't actually matter here since we don't have a text field?
103 this.setTopNode(this.addTag(tag,nil,true));
105 var namedSection = {|lev|
108 this.enterLevel(lev);
109 this.setTopNode(this.addTag(tag,"",true));
113 var modalRangeTag = {
116 tree.add((tag:\prose,display:proseDisplay,text:"",children:nil));
119 lastTagLine = lineno;
123 singleline = false; //this doesn't actually matter here since we don't have a text field?
125 this.setTopNode(this.addTag(tag,nil,true));
126 lastTagLine = lineno;
129 // modal tags ignore all other tags until their closing tag occurs.
130 // here we check if we are in a modal tag (code, emphasis, link, teletype) and then
131 // if we got the closing tag.
132 if(modalTag.notNil, {
133 //only allow modal block tags to be closed with the closing tag as the first word on a line
134 if((tag==modalTag) and: ((wordno==0) or: (lastTagLine==lineno)),{
136 current.display = if(lastTagLine==lineno,\inline,\block);
142 if(("[^\\\\]::$".matchRegexp(word)) and: (lastTagLine==lineno), { //split unhandled tag-like word
143 this.addText(word.drop(-2));
144 this.handleWord("::",lineno,wordno+1);
146 this.addText(word.replace("\\::","::"));
151 'description::', noNameSection, //level 1
152 'classmethods::', noNameSection,
153 'instancemethods::', noNameSection,
154 'examples::', noNameSection,
155 'section::', namedSection.(1),
156 'subsection::', namedSection.(2),
157 'method::', namedSection.(3),
162 'keyword::', simpleTag,
163 'argument::', namedSection.(4),
165 singleline = true; //this doesn't actually matter here since we don't have a text field?
167 this.setTopNode(this.addTag(tag,nil,true));
170 singleline = true; //this doesn't actually matter here since we don't have a text field?
172 this.setTopNode(this.addTag(tag,nil,true));
174 'class::', simpleTag,
175 'redirect::', simpleTag,
176 'title::', simpleTag,
177 'summary::', simpleTag,
178 'related::', simpleTag,
179 // 'headerimage::', simpleTag,
180 'categories::', simpleTag,
181 // 'note::', simpleTag,
182 // 'warning::', simpleTag,
183 'private::', simpleTag,
184 'classtree::', simpleTag,
186 'code::', modalRangeTag,
187 'formula::', modalRangeTag,
188 'emphasis::', modalRangeTag,
189 'teletype::', modalRangeTag,
190 'strong::', modalRangeTag,
191 'link::', modalRangeTag,
192 'anchor::', modalRangeTag,
193 'image::', modalRangeTag,
194 'soft::', modalRangeTag,
196 'note::', { listEnter.(); proseDisplay=\inline },
197 'warning::', { listEnter.(); proseDisplay=\inline },
201 'numberedlist::', listEnter,
202 'definitionlist::', listEnter,
203 'table::', listEnter,
206 current !? { //strip trailing whitespace from previous text..
209 while({(t[x]==$\n) or: (t[x]==$\ )},{x=x-1});
210 current.text = t.copyRange(0,x);
213 this.setTopNode(this.addTag(tag,nil,true,\inline));
214 lastTagLine = lineno;
215 proseDisplay = \inline;
219 this.addTag('##::',nil,false,\inline); //make it look like an ordinary tag since we drop the :: in the output tree
223 this.addTag('||::',nil,false,\inline);
228 '::', { //ends tables and lists
237 if("^[a-zA-Z]+://.+".matchRegexp(word),{ //auto link URIs
238 this.addTag('link::',word++" ",false,\inline);
241 if(("[^\\\\]::$".matchRegexp(word)) and: (lastTagLine==lineno), { //split unhandled tag-like word
242 this.addText(word.drop(-2));
243 this.handleWord("::",lineno,wordno+1);
245 this.addText(word); //plain text, add the word.
256 word = word.stripWhiteSpace;
258 if(current.notNil, { // add to current element text
259 current.text = current.text ++ word
260 },{ // no current element, so add to new 'prose' element
261 if((isWS.not) or: (afterClosing), { //don't start a new prose element with whitespace
262 afterClosing = false;
264 this.addTag('prose::', word, false, proseDisplay);
270 if(singleline,{this.endCurrent});
271 // pass through newlines for vari-line tags.
272 current !? {current.text = current.text ++ "\n"};
276 var lines = string.replace("\r","").split($\n); //split lines
277 // var lines = string.findRegexp("[^\n]+").flop[1]; //doesn't work for empty lines
279 var w, split, split2, word;
282 split = line.findRegexp("\\S+::\\S+::|\\S+::|::|\\s+|\\S+");
286 split2 = word.findRegexp("([a-zA-Z]+::)(\\S+)(::)")[1..]; //split stuff like::this::...
288 isWS = "^\\s+$".matchRegexp(word);
289 this.handleWord(word,l,w);
290 if(isWS.not,{w=w+1});
293 isWS = "^\\s+$".matchRegexp(e2[1]);
294 this.handleWord(e2[1],l,w);
299 if(modalTag.isNil and: split.isEmpty, { this.endCurrent; proseDisplay=\block; }); //force a new prose on double blank lines
302 this.handleCopyMethod;
305 parseFile {|filename|
306 var file = File.open(filename,"r");
307 currentFile = filename;
308 this.parse(file.readAllString);
314 var sects = IdentitySet[\classmethods,\instancemethods,\section,\subsection,\examples];
315 var do_children = {|dest,children|
319 if(sects.includes(node.tag)) {
320 n = dest.detect {|x| (x.tag==node.tag) and: {x.text==node.text}};
322 dest = dest.add(node);
324 n.children = do_children.(n.children,node.children);
327 dest = dest.add(node);
333 root = do_children.(root,p2.root);
336 parseMetaData {|path|
337 var line, file, tag, text, match;
338 var tags = IdentitySet[\class,\title,\summary,\categories,\related,\redirect];
340 var methods = Array.new;
341 var keywords = Array.new;
344 var cmets = IdentitySet.new;
345 var imets = IdentitySet.new;
349 file = File.open(path,"r");
353 if(line.isNil) {break.value};
354 match = line.findRegexp("([a-zA-Z]+::)\\s*(\\S.*\\S|\\S)?").flop[1];
356 tag = match[1].toLower.drop(-2).asSymbol;
358 if(inHeader and: {tags.includes(tag)}) {
359 root.add((tag:tag, text:text));
361 class = text.asSymbol.asClass;
366 \description, {pfx="."},
368 \examples, {pfx="."},
369 \instancemethods, {pfx="-"},
370 \classmethods, {pfx="*"},
373 // - m.isExtensionOf(c) (perhaps not very important, we can see this in the class doc)
374 match = text.findRegexp("\\(.*\\)|[^ ,]+").flop[1].reject{|x|x[0]==$(};
376 if("^[a-z][a-zA-Z0-9_]*|[-<>@|&%*+/!?=]+$".matchRegexp(name).not) {
377 warn("Methodname not valid: '"++name++"' in"+path);
381 warn("Methods should be given as getters, without the _ suffix:"+name+"in"+path);
384 methods = methods.add("_"++pfx++m);
393 match = text.findRegexp("[^ ,]+").flop[1][1];
395 m = match.drop(1).asSymbol.asGetter;
396 methods = methods.add("_"++pfx++m);
404 match = text.findRegexp("[^ ,]+").flop[1];
406 m = name.asSymbol.asGetter;
414 match = text.findRegexp("[^ ,]+").flop[1];
415 keywords = keywords ++ match;
423 // Add undocumented methods
427 (mets = c.methods) !? {
428 //ignore these methods by default. Note that they can still be explicitly documented.
429 docmets = docmets | IdentitySet[\categories, \init, \checkInputs, \new1, \argNamesInputsOffset];
430 mets.collectAs({|m|m.name.asGetter},IdentitySet).do {|name|
431 if(docmets.includes(name).not) {
432 l = l.add("_"++pfx++name.asString);
438 methods = methods ++ f.(class,imets,"-") ++ f.(class.class,cmets,"*");
440 methodList = methods;
441 keywordList = keywords;
444 *getMethodDoc {|classname,methodname|
445 var a, p, src, node = nil, findparent, parent;
446 var findmet = {|children|
450 \instancemethods, {parent = n.tag},
451 \classmethods, {parent = n.tag},
453 if(parent == findparent
454 and: {n.text.findRegexp("[^ ,]+").flop[1].indexOfEqual(methodname).notNil}) {
459 if(n.children.size>0 and: {node.isNil}) { findmet.(n.children) };
464 if(copyMethodCache.isNil) {
465 copyMethodCache = Array.new(4); //create an empty cache with room for 4 class docs
467 a = copyMethodCache.detect{|a|a.key==classname}; // check if class was already in cache
473 SCDoc.helpSourceDirs.do {|dir|
474 var x = dir+/+"Classes"+/+classname++".schelp";
482 warn("SCDoc: copymethod:: could not find class doc"+classname);
487 // add to cache, possibly removing the oldest one
488 if(copyMethodCache.size >= copyMethodCache.maxSize) {
489 copyMethodCache.removeAt(0);
491 copyMethodCache.add(classname -> p);
494 findparent = switch(methodname[0],
496 $-, \instancemethods);
497 methodname = methodname.drop(1);
501 warn("SCDoc: copymethod:: could not find"+methodname+"in"+findparent+"of class doc"+classname);
508 var do_children = {|children|
511 if(n.tag==\copymethod) {
512 #name, met = n.text.findRegexp("[^ ,]+").flop[1];
513 node = this.class.getMethodDoc(name,met);
517 children[i]=(tag:\method, text:met.drop(1), children:[(tag:\prose, text:"(copymethod::"+n.text+"failed)", display:\block)]);
520 if(n.children.size>0) { do_children.(n.children) };
527 // FIXME: move to renderer?
528 generateUndocumentedMethods {|class,node,title|
529 var syms, name, mets, l = Array.new;
530 var docmets = IdentitySet.new;
533 n.text.findRegexp("\\(.*\\)|[^ ,]+").flop[1].do {|m|
535 docmets.add(m.asSymbol.asGetter);
540 var do_children = {|children|
544 \method, { addMet.(n) },
545 \private, { addMet.(n) },
546 \subsection, { do_children.(n.children) }
552 if(class.isNil, {^nil});
554 do_children.(node.children);
556 (mets = class.methods) !? {
557 //ignore these methods by default. Note that they can still be explicitly documented.
558 docmets = docmets | IdentitySet[\categories, \init, \checkInputs, \new1, \argNamesInputsOffset];
559 syms = mets.collectAs({|m|m.name.asGetter},IdentitySet);
561 if(docmets.includes(name).not) {
562 l = l.add((tag:\method, text:name.asString));
577 dumpSubTree {|t,i="",lev=1|
580 (i++"TAG:"+e.tag+"( level"+lev+e.display+")").postln;
582 (i++"TEXT: \""++e.text++"\"").postln;
585 (i++"CHILDREN:").postln;
586 this.dumpSubTree(e.children,i++" ",lev+1);
592 this.dumpSubTree(root);
596 findNode {|tag,rootNode=nil|
597 var sym = tag.asSymbol;
598 ^((rootNode ?? { root }).detect {|n| n.tag == tag} ?? {
599 (tag:nil, text:"", children:[]);
604 makeCategoryTree {|catMap,node,filter=nil,toc=false|
605 var a, p, e, n, l, m, kinds, folder, v, dumpCats, sorted;
606 var tree = Dictionary.new;
608 catMap.pairsDo {|cat,files|
611 if(filter.isNil or: {filter.matchRegexp(l.first)}, {
615 p[c][\subcats] = Dictionary.new;
616 p[c][\entries] = List.new;
622 files.do {|f| a.add(f)};
628 var ents = x[\entries];
629 var subs = x[\subcats];
633 ents.sort {|a,b| a.path.basename < b.path.basename}.do {|doc|
634 folder = doc.path.dirname;
635 folder = if(folder==".", {""}, {" ["++folder++"]"});
637 l.add((tag:'link', text:doc.path++"##"++doc.title));
638 l.add((tag:'prose', text:" - "++doc.summary));
639 l.add((tag:'soft', text:folder));
640 switch(doc.installed,
641 \extension, { l.add((tag:'soft', text:" (+)")) },
642 \missing, { l.add((tag:'strong', text:" (not installed)")) }
644 /* if(doc.path.dirname=="Classes") {
645 c = doc.path.basename.asSymbol.asClass;
647 if(c.filenameSymbol.asString.beginsWith(thisProcess.platform.classLibraryDir).not) {
648 l.add((tag:'soft', text:" (+)"));
651 l.add((tag:'strong', text:" (not installed)"));
658 subs.keys.asList.sort {|a,b| a<b}.do {|k|
659 z = SCDocRenderer.simplifyName(y++">"++k);
661 l.add((tag:\anchor, text:z));
662 l.add((tag:\strong, text:k));
663 l.add((tag:\tree, children:m=List.new));
664 dumpCats.value(subs[k],m,z);
668 sorted = tree.keys.asList.sort {|a,b| a<b};
671 node.add((tag:'prose', text:"Jump to: ", display:\block));
673 if(i!=0, {node.add((tag:'prose', text:", ", display:\inline))});
674 node.add((tag:'link', text:"#"++SCDocRenderer.simplifyName(k)++"#"++k));
675 // node.add((tag:'prose', text:" ", display:\inline));
680 node.add((tag:\section, text:k, children:m=List.new));
681 m.add((tag:\tree, children:l=List.new));
682 dumpCats.(tree[k],l,k);
686 overviewCategories {|catMap|
688 r.add((tag:'title', text:"Document Categories"));
689 r.add((tag:'summary', text:"All documents by categories"));
690 r.add((tag:'related', text:"Overviews/Documents, Browse, Search"));
692 this.makeCategoryTree(catMap,r,toc:false);
698 overviewAllClasses {|docMap|
699 var name, doc, link, n, r = List.new, cap, old_cap=nil, sortedKeys;
700 r.add((tag:'title', text:"Classes"));
701 r.add((tag:'summary', text:"Alphabetical index of all classes"));
702 r.add((tag:'related', text:"Overviews/ClassTree, Overviews/Methods"));
704 sortedKeys = Class.allClasses.reject {|c| c.name.asString.find("Meta_")==0};
707 r.add((tag:'prose', text:"Jump to: ", display:\block));
709 name = c.name.asString;
710 cap = name.first.toUpper;
712 r.add((tag:'link', text:"#"++cap.asString));
713 r.add((tag:'prose', text:" ", display:\inline));
720 name = c.name.asString;
721 link = "Classes" +/+ name;
724 cap = name.first.toUpper;
726 r.add((tag:'section', text:cap.asString, children:n=List.new));
727 n.add((tag:'list', children:n=List.new));
731 n.add((tag:'link', text: link));
732 n.add((tag:'prose', text: " - "++ if(doc.notNil, {doc.summary}, {""})));
733 switch(doc.installed,
734 \extension, { n.add((tag:'soft', text:" (+)")) },
735 \missing, { n.add((tag:'strong', text:" (not installed)")) }
743 overviewAllMethods {|docMap|
744 var name, n, r = List.new, cap, old_cap, t, m, ext, sortedKeys, pfx;
745 r.add((tag:'title', text:"Methods"));
746 r.add((tag:'summary', text:"Alphabetical index of all methods"));
747 r.add((tag:'related', text:"Overviews/ClassTree, Overviews/Classes"));
749 r.add((tag:'prose', text:"This is an alphabetical list of all implemented methods, including private and undocumented methods.", display:\block));
750 r.add((tag:'prose', text:"The classnames are prefixed by * for classmethods and postfixed by + for extensionmethods.", display:\block));
752 t = IdentityDictionary.new;
754 Class.allClasses.do {|c|
755 name = c.name.asString;
757 if(t[x.name]==nil, {t[x.name] = List.new});
759 t[x.name].add([name,x.isExtensionOf(c)]);
763 sortedKeys = t.keys.asList.sort {|a,b| a<b};
766 r.add((tag:'prose', text:"Jump to: ", display:\block));
769 cap = name.first.toLower;
771 r.add((tag:'link', text:"#"++cap.asString));
772 r.add((tag:'prose', text:" ", display:\inline));
780 cap = name.first.toLower;
782 r.add((tag:'section', text:cap.asString, children:n=List.new));
783 n.add((tag:'definitionlist', children:m=List.new));
788 m.add((tag:'anchor', text:name));
789 m.add((tag:'code', text:name));
791 if(name.last==$_, {name=name.drop(-1)});
795 if(i!=0, {m.add((tag:'prose', text:", ", display:\inline))});
796 if(n.find("Meta_")==0, {
798 m.add((tag:'prose', text:"*", display:\inline));
801 // m.add((tag:'link', text: "Classes" +/+ n ++ "#" ++ SCDocRenderer.simplifyName(name)));
802 m.add((tag:'link', text: "Classes" +/+ n ++ "#" ++ pfx ++ name));
803 if(c[1], {m.add((tag:'prose', text:"+", display:\inline))});
811 /* overviewAllDocuments {|docMap|
812 var kind, name, doc, link, n, r = List.new, cap, old_cap, sortedKeys;
813 r.add((tag:'title', text:"Documents"));
814 r.add((tag:'summary', text:"Alphabetical index of all documents"));
815 r.add((tag:'related', text:"Overviews/Categories"));
817 sortedKeys = docMap.keys.asList.sort {|a,b| a.split($/).last < b.split($/).last};
820 r.add((tag:'prose', text:"Jump to: ", display:\block));
821 sortedKeys.do {|link|
822 name = link.split($/).last;
823 cap = name.first.toUpper;
825 r.add((tag:'link', text:"#"++cap.asString));
826 r.add((tag:'prose', text:" ", display:\inline));
832 sortedKeys.do {|link|
834 name = link.split($/).last;
836 kind = if(kind==".", {""}, {" ["++kind++"]"});
837 cap = name.first.toUpper;
839 r.add((tag:'section', text:cap.asString, children:n=List.new));
840 n.add((tag:'list', children:n=List.new));
844 n.add((tag:'link', text: link++"##"++doc.title));
845 // n.add((tag:'||'));
846 n.add((tag:'prose', text: " - "++if(doc.notNil, {doc.summary}, {""})));
847 // n.add((tag:'||'));
848 n.add((tag:'soft', text: kind));
854 /* overviewServer {|catMap|
856 r.add((tag:'title', text:"Server stuff"));
857 r.add((tag:'summary', text:"Overview of server related stuff"));
859 this.makeCategoryTree(catMap,r,"^Server$");
860 this.makeCategoryTree(catMap,r,"^UGens$");