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 lastTagLine = lineno;
120 singleline = false; //this doesn't actually matter here since we don't have a text field?
122 this.setTopNode(this.addTag(tag,nil,true));
123 lastTagLine = lineno;
126 // modal tags ignore all other tags until their closing tag occurs.
127 // here we check if we are in a modal tag (code, emphasis, link) and then
128 // if we got the closing tag.
129 if(modalTag.notNil, {
130 //only allow modal block tags to be closed with the closing tag as the first word on a line
131 if((tag==modalTag) and: ((wordno==0) or: (lastTagLine==lineno)),{
133 current.display = if(lastTagLine==lineno,\inline,\block);
139 if(("[^ ]+[^ \\]::".matchRegexp(word)) and: (lastTagLine==lineno), { //split unhandled tag-like word
140 this.addText(word.drop(-2));
141 this.handleWord("::",lineno,wordno+1);
143 this.addText(word.replace("\\::","::"));
148 'description::', noNameSection, //level 1
149 'classmethods::', noNameSection,
150 'instancemethods::', noNameSection,
151 'examples::', noNameSection,
152 'section::', namedSection.(1),
153 'subsection::', namedSection.(2),
154 'method::', namedSection.(3),
159 'keyword::', simpleTag,
160 'argument::', namedSection.(4),
162 singleline = true; //this doesn't actually matter here since we don't have a text field?
164 this.setTopNode(this.addTag(tag,nil,true));
167 singleline = true; //this doesn't actually matter here since we don't have a text field?
169 this.setTopNode(this.addTag(tag,nil,true));
171 'class::', simpleTag,
172 'redirect::', simpleTag,
173 'title::', simpleTag,
174 'summary::', simpleTag,
175 'related::', simpleTag,
176 // 'headerimage::', simpleTag,
177 'categories::', simpleTag,
178 // 'note::', simpleTag,
179 // 'warning::', simpleTag,
180 'private::', simpleTag,
181 'classtree::', simpleTag,
183 'code::', modalRangeTag,
184 'formula::', modalRangeTag,
185 'emphasis::', modalRangeTag,
186 'strong::', modalRangeTag,
187 'link::', modalRangeTag,
188 'anchor::', modalRangeTag,
189 'image::', modalRangeTag,
190 'soft::', modalRangeTag,
192 'note::', { listEnter.(); proseDisplay=\inline },
193 'warning::', { listEnter.(); proseDisplay=\inline },
197 'numberedlist::', listEnter,
198 'definitionlist::', listEnter,
199 'table::', listEnter,
202 current !? { //strip trailing whitespace from previous text..
205 while({(t[x]==$\n) or: (t[x]==$\ )},{x=x-1});
206 current.text = t.copyRange(0,x);
209 this.setTopNode(this.addTag(tag,nil,true,\inline));
210 lastTagLine = lineno;
211 proseDisplay = \inline;
215 this.addTag('##::',nil,false,\inline); //make it look like an ordinary tag since we drop the :: in the output tree
219 this.addTag('||::',nil,false,\inline);
224 '::', { //ends tables and lists
233 if("^[a-zA-Z]+://.+".matchRegexp(word),{ //auto link URIs
234 this.addTag('link::',word++" ",false,\inline);
237 if(("[^ ]+[^ \\]::".matchRegexp(word)) and: (lastTagLine==lineno), { //split unhandled tag-like word
238 this.addText(word.drop(-2));
239 this.handleWord("::",lineno,wordno+1);
241 if(word.endsWith("::")) {
242 warn("SCDocParser: Unknown tag:"+word+"in"+currentFile);
244 this.addText(word); //plain text, add the word.
255 word = word.stripWhiteSpace;
257 if(current.notNil, { // add to current element text
258 current.text = current.text ++ word
259 },{ // no current element, so add to new 'prose' element
260 if((isWS.not) or: (afterClosing), { //don't start a new prose element with whitespace
261 afterClosing = false;
263 this.addTag('prose::', word, false, proseDisplay);
269 if(singleline,{this.endCurrent});
270 // pass through newlines for vari-line tags.
271 current !? {current.text = current.text ++ "\n"};
275 var lines = string.replace("\r","").split($\n); //split lines
276 // var lines = string.findRegexp("[^\n]+").flop[1]; //doesn't work for empty lines
278 var w, split, split2, word;
281 split = line.findRegexp("[a-zA-Z]+::[^ \n\t]+::|[a-zA-Z]*::|[ \n\t]+|[^ \n\t]+"); //split words and tags and ws
285 split2 = word.findRegexp("([a-zA-Z]+::)([^ \n\t]+)(::)")[1..]; //split stuff like::this::...
287 isWS = "^[ \n\t]+$".matchRegexp(word);
288 this.handleWord(word,l,w);
289 if(isWS.not,{w=w+1});
292 isWS = "^[ \n\t]+$".matchRegexp(e2[1]);
293 this.handleWord(e2[1],l,w);
298 if(modalTag.isNil and: split.isEmpty, { this.endCurrent; proseDisplay=\block; }); //force a new prose on double blank lines
301 this.handleCopyMethod;
304 parseFile {|filename|
305 var file = File.open(filename,"r");
306 currentFile = filename;
307 this.parse(file.readAllString);
313 var sects = IdentitySet[\classmethods,\instancemethods,\section,\subsection,\examples];
314 var do_children = {|dest,children|
318 if(sects.includes(node.tag)) {
319 n = dest.detect {|x| (x.tag==node.tag) and: {x.text==node.text}};
321 dest = dest.add(node);
323 n.children = do_children.(n.children,node.children);
326 dest = dest.add(node);
332 root = do_children.(root,p2.root);
335 parseMetaData {|path|
336 var line, file, tag, text, match;
337 var tags = IdentitySet[\class,\title,\summary,\categories,\related,\redirect];
339 var methods = Array.new;
340 var keywords = Array.new;
343 var cmets = IdentitySet.new;
344 var imets = IdentitySet.new;
348 file = File.open(path,"r");
352 if(line.isNil) {break.value};
353 match = line.findRegexp("([a-zA-Z]+::)\\s*(\\S*.*\\S+)?").flop[1];
355 tag = match[1].toLower.drop(-2).asSymbol;
357 if(inHeader and: {tags.includes(tag)}) {
358 root.add((tag:tag, text:text));
360 class = text.asSymbol.asClass;
365 \description, {pfx="."},
367 \examples, {pfx="."},
368 \instancemethods, {pfx="-"},
369 \classmethods, {pfx="*"},
372 // - m.isExtensionOf(c) (perhaps not very important, we can see this in the class doc)
373 match = text.findRegexp("\\(.*\\)|[^ ,]+").flop[1].reject{|x|x[0]==$(};
375 if("[a-z][a-zA-Z0-9_]*|[-<>@|&%*+/!?=]+".matchRegexp(name).not) {
376 warn("Methodname not valid: '"++name++"' in"+path);
380 warn("Methods should be given as getters, without the _ suffix:"+name+"in"+path);
383 methods = methods.add("_"++pfx++m);
392 match = text.findRegexp("[^ ,]+").flop[1][1];
394 m = match.drop(1).asSymbol.asGetter;
395 methods = methods.add("_"++pfx++m);
403 match = text.findRegexp("[^ ,]+").flop[1];
405 m = name.asSymbol.asGetter;
413 match = text.findRegexp("[^ ,]+").flop[1];
414 keywords = keywords ++ match;
422 // Add undocumented methods
426 (mets = c.methods) !? {
427 //ignore these methods by default. Note that they can still be explicitly documented.
428 docmets = docmets | IdentitySet[\categories, \init, \checkInputs, \new1, \argNamesInputsOffset];
429 mets.collectAs({|m|m.name.asGetter},IdentitySet).do {|name|
430 if(docmets.includes(name).not) {
431 l = l.add("_"++pfx++name.asString);
437 methods = methods ++ f.(class,imets,"-") ++ f.(class.class,cmets,"*");
439 methodList = methods;
440 keywordList = keywords;
443 *getMethodDoc {|classname,methodname|
444 var a, p, src, node = nil, findparent, parent;
445 var findmet = {|children|
449 \instancemethods, {parent = n.tag},
450 \classmethods, {parent = n.tag},
452 if(parent == findparent
453 and: {n.text.findRegexp("[^ ,]+").flop[1].indexOfEqual(methodname).notNil}) {
458 if(n.children.size>0 and: {node.isNil}) { findmet.(n.children) };
463 if(copyMethodCache.isNil) {
464 copyMethodCache = Array.new(4); //create an empty cache with room for 4 class docs
466 a = copyMethodCache.detect{|a|a.key==classname}; // check if class was already in cache
472 SCDoc.helpSourceDirs.do {|dir|
473 var x = dir+/+"Classes"+/+classname++".schelp";
481 warn("SCDoc: copymethod:: could not find class doc"+classname);
486 // add to cache, possibly removing the oldest one
487 if(copyMethodCache.size >= copyMethodCache.maxSize) {
488 copyMethodCache.removeAt(0);
490 copyMethodCache.add(classname -> p);
493 findparent = switch(methodname[0],
495 $-, \instancemethods);
496 methodname = methodname.drop(1);
500 warn("SCDoc: copymethod:: could not find"+methodname+"in"+findparent+"of class doc"+classname);
507 var do_children = {|children|
510 if(n.tag==\copymethod) {
511 #name, met = n.text.findRegexp("[^ ,]+").flop[1];
512 node = this.class.getMethodDoc(name,met);
516 children[i]=(tag:\method, text:met.drop(1), children:[(tag:\prose, text:"(copymethod::"+n.text+"failed)", display:\block)]);
519 if(n.children.size>0) { do_children.(n.children) };
526 // FIXME: move to renderer?
527 generateUndocumentedMethods {|class,node,title|
528 var syms, name, mets, l = Array.new;
529 var docmets = IdentitySet.new;
532 n.text.findRegexp("\\(.*\\)|[^ ,]+").flop[1].do {|m|
534 docmets.add(m.asSymbol.asGetter);
539 var do_children = {|children|
543 \method, { addMet.(n) },
544 \private, { addMet.(n) },
545 \subsection, { do_children.(n.children) }
551 if(class.isNil, {^nil});
553 do_children.(node.children);
555 (mets = class.methods) !? {
556 //ignore these methods by default. Note that they can still be explicitly documented.
557 docmets = docmets | IdentitySet[\categories, \init, \checkInputs, \new1, \argNamesInputsOffset];
558 syms = mets.collectAs({|m|m.name.asGetter},IdentitySet);
560 if(docmets.includes(name).not) {
561 l = l.add((tag:\method, text:name.asString));
576 dumpSubTree {|t,i="",lev=1|
579 (i++"TAG:"+e.tag+"( level"+lev+e.display+")").postln;
581 (i++"TEXT: \""++e.text++"\"").postln;
584 (i++"CHILDREN:").postln;
585 this.dumpSubTree(e.children,i++" ",lev+1);
591 this.dumpSubTree(root);
595 findNode {|tag,rootNode=nil|
596 var sym = tag.asSymbol;
597 ^((rootNode ?? { root }).detect {|n| n.tag == tag} ?? {
598 (tag:nil, text:"", children:[]);
603 makeCategoryTree {|catMap,node,filter=nil,toc=false|
604 var a, p, e, n, l, m, kinds, folder, v, dumpCats, sorted;
605 var tree = Dictionary.new;
607 catMap.pairsDo {|cat,files|
610 if(filter.isNil or: {filter.matchRegexp(l.first)}, {
614 p[c][\subcats] = Dictionary.new;
615 p[c][\entries] = List.new;
621 files.do {|f| a.add(f)};
627 var ents = x[\entries];
628 var subs = x[\subcats];
632 ents.sort {|a,b| a.path.basename < b.path.basename}.do {|doc|
633 folder = doc.path.dirname;
634 folder = if(folder==".", {""}, {" ["++folder++"]"});
636 l.add((tag:'link', text:doc.path++"##"++doc.title));
637 l.add((tag:'prose', text:" - "++doc.summary));
638 l.add((tag:'soft', text:folder));
639 switch(doc.installed,
640 \extension, { l.add((tag:'soft', text:" (+)")) },
641 \missing, { l.add((tag:'strong', text:" (not installed)")) }
643 /* if(doc.path.dirname=="Classes") {
644 c = doc.path.basename.asSymbol.asClass;
646 if(c.filenameSymbol.asString.beginsWith(thisProcess.platform.classLibraryDir).not) {
647 l.add((tag:'soft', text:" (+)"));
650 l.add((tag:'strong', text:" (not installed)"));
657 subs.keys.asList.sort {|a,b| a<b}.do {|k|
658 z = SCDocRenderer.simplifyName(y++">"++k);
660 l.add((tag:\anchor, text:z));
661 l.add((tag:\strong, text:k));
662 l.add((tag:\tree, children:m=List.new));
663 dumpCats.value(subs[k],m,z);
667 sorted = tree.keys.asList.sort {|a,b| a<b};
670 node.add((tag:'prose', text:"Jump to: ", display:\block));
672 if(i!=0, {node.add((tag:'prose', text:", ", display:\inline))});
673 node.add((tag:'link', text:"#"++SCDocRenderer.simplifyName(k)++"#"++k));
674 // node.add((tag:'prose', text:" ", display:\inline));
679 node.add((tag:\section, text:k, children:m=List.new));
680 m.add((tag:\tree, children:l=List.new));
681 dumpCats.(tree[k],l,k);
685 overviewCategories {|catMap|
687 r.add((tag:'title', text:"Document Categories"));
688 r.add((tag:'summary', text:"All documents by categories"));
689 r.add((tag:'related', text:"Overviews/Documents, Browse, Search"));
691 this.makeCategoryTree(catMap,r,toc:false);
697 overviewAllClasses {|docMap|
698 var name, doc, link, n, r = List.new, cap, old_cap=nil, sortedKeys;
699 r.add((tag:'title', text:"Classes"));
700 r.add((tag:'summary', text:"Alphabetical index of all classes"));
701 r.add((tag:'related', text:"Overviews/ClassTree, Overviews/Methods"));
703 sortedKeys = Class.allClasses.reject {|c| c.name.asString.find("Meta_")==0};
706 r.add((tag:'prose', text:"Jump to: ", display:\block));
708 name = c.name.asString;
709 cap = name.first.toUpper;
711 r.add((tag:'link', text:"#"++cap.asString));
712 r.add((tag:'prose', text:" ", display:\inline));
719 name = c.name.asString;
720 link = "Classes" +/+ name;
723 cap = name.first.toUpper;
725 r.add((tag:'section', text:cap.asString, children:n=List.new));
726 n.add((tag:'list', children:n=List.new));
730 n.add((tag:'link', text: link));
731 n.add((tag:'prose', text: " - "++ if(doc.notNil, {doc.summary}, {""})));
732 switch(doc.installed,
733 \extension, { n.add((tag:'soft', text:" (+)")) },
734 \missing, { n.add((tag:'strong', text:" (not installed)")) }
742 overviewAllMethods {|docMap|
743 var name, n, r = List.new, cap, old_cap, t, m, ext, sortedKeys, pfx;
744 r.add((tag:'title', text:"Methods"));
745 r.add((tag:'summary', text:"Alphabetical index of all methods"));
746 r.add((tag:'related', text:"Overviews/ClassTree, Overviews/Classes"));
748 r.add((tag:'prose', text:"This is an alphabetical list of all implemented methods, including private and undocumented methods.", display:\block));
749 r.add((tag:'prose', text:"The classnames are prefixed by * for classmethods and postfixed by + for extensionmethods.", display:\block));
751 t = IdentityDictionary.new;
753 Class.allClasses.do {|c|
754 name = c.name.asString;
756 if(t[x.name]==nil, {t[x.name] = List.new});
758 t[x.name].add([name,x.isExtensionOf(c)]);
762 sortedKeys = t.keys.asList.sort {|a,b| a<b};
765 r.add((tag:'prose', text:"Jump to: ", display:\block));
768 cap = name.first.toLower;
770 r.add((tag:'link', text:"#"++cap.asString));
771 r.add((tag:'prose', text:" ", display:\inline));
779 cap = name.first.toLower;
781 r.add((tag:'section', text:cap.asString, children:n=List.new));
782 n.add((tag:'definitionlist', children:m=List.new));
787 m.add((tag:'anchor', text:name));
788 m.add((tag:'code', text:name));
790 if(name.last==$_, {name=name.drop(-1)});
794 if(i!=0, {m.add((tag:'prose', text:", ", display:\inline))});
795 if(n.find("Meta_")==0, {
797 m.add((tag:'prose', text:"*", display:\inline));
800 // m.add((tag:'link', text: "Classes" +/+ n ++ "#" ++ SCDocRenderer.simplifyName(name)));
801 m.add((tag:'link', text: "Classes" +/+ n ++ "#" ++ pfx ++ name));
802 if(c[1], {m.add((tag:'prose', text:"+", display:\inline))});
810 /* overviewAllDocuments {|docMap|
811 var kind, name, doc, link, n, r = List.new, cap, old_cap, sortedKeys;
812 r.add((tag:'title', text:"Documents"));
813 r.add((tag:'summary', text:"Alphabetical index of all documents"));
814 r.add((tag:'related', text:"Overviews/Categories"));
816 sortedKeys = docMap.keys.asList.sort {|a,b| a.split($/).last < b.split($/).last};
819 r.add((tag:'prose', text:"Jump to: ", display:\block));
820 sortedKeys.do {|link|
821 name = link.split($/).last;
822 cap = name.first.toUpper;
824 r.add((tag:'link', text:"#"++cap.asString));
825 r.add((tag:'prose', text:" ", display:\inline));
831 sortedKeys.do {|link|
833 name = link.split($/).last;
835 kind = if(kind==".", {""}, {" ["++kind++"]"});
836 cap = name.first.toUpper;
838 r.add((tag:'section', text:cap.asString, children:n=List.new));
839 n.add((tag:'list', children:n=List.new));
843 n.add((tag:'link', text: link++"##"++doc.title));
844 // n.add((tag:'||'));
845 n.add((tag:'prose', text: " - "++if(doc.notNil, {doc.summary}, {""})));
846 // n.add((tag:'||'));
847 n.add((tag:'soft', text: kind));
853 /* overviewServer {|catMap|
855 r.add((tag:'title', text:"Server stuff"));
856 r.add((tag:'summary', text:"Overview of server related stuff"));
858 this.makeCategoryTree(catMap,r,"^Server$");
859 this.makeCategoryTree(catMap,r,"^UGens$");