use System.Diagnostics use System.Reflection use System.Text.RegularExpressions class HelpGenerator """ to-do: show doc strings to-do: show contracts to-do: show unit tests? to-do: pick up .NET XML doc file for a given DLL to-do: sorting should be case insensitive """ var settings = [ ['member separator', 'The HTML to put between type members. Defaults to
.'], ['directory', 'The directory to put the HTML files in. Defaults to the current directory.'], ['file name', 'The template for the file name for the HTML files. Defaults to "Help_for_TOPIC.html".'], ['max file name length', 'The maximum file name length in case TOPIC gets too long. Defaults to 64.'], ['open', 'Can be set to a boolean value (true, 1, false or 0) or set to the path of a program to execute on the help file.'], ['show nodes', 'If true, show the compiler nodes and their properties.'], ['max node depth', 'When showing nodes, the maximum node depth to traverse. Default is 2.'], ] var searchSites = [ { 'name': 'Google', 'url': 'http://www.google.com/search?q=', 'helpers': ['Cobra', '.NET', 'C#'], }, { 'name': 'Cobra Language', 'host': 'cobra-language.com', }, { 'name': 'MSDN', 'host': 'msdn.microsoft.com', }, { 'name': 'Mono', 'host': 'docs.go-mono.com', }, { 'name': 'Stack Overflow', 'host': 'stackoverflow.com', 'helpers': ['.NET', 'C#'], }, ] var searchEngine = 'Google' # should match a searchSites above that has a 'url' const defaultMaxFileNameLength = 64 var _testMethodRE = Regex(r'^test_[\w]+_\d+$', RegexOptions.Compiled) var _settingNames as Cobra.Lang.ISet var _nextSerialNum = 1 cue init(topic as String, node as ISyntaxNode, parentMember as IBoxMember?, parentType as IType?) base.init _topic, _node = topic, node _parentMember, _parentType = parentMember, parentType _settingNames = Set(for spec in .settings get spec[0]) pro node from var as ISyntaxNode pro parentMember from var as IBoxMember? pro parentType from var as IType? pro types from var = List() pro searchTerms from var = List() pro topic from var = '' pro path from var = '' def generate .reduceSearchTerms .reduceTypes directory = .setting('directory', '.') fileName = .setting('file name', 'Help_for_TOPIC.html') fileName = .cleanFileName(fileName.replace('TOPIC', .topic)) .path = .reducePath(Path.combine(directory, fileName)) using tw = File.createText(.path) print to tw print '' print '' print '' print ' [.topic]' print ' ' .printStylesheet .embedFile('WebAssets[Path.directorySeparatorChar]help.js') print '' print '' .printContent print '' print '' .open def reduceSearchTerms """ Get rid of duplicate search terms, trailing whitespace, etc. """ result = List() visited = Set() for term in .searchTerms term = term.trim while term.contains(' '), term = term.replace(' ', ' ') if term.toLower not in visited visited.add(term.toLower) result.add(term) .searchTerms = result def reduceTypes result = List() for type in .types, if type not in result, result.add(type) .types = result def cleanFileName(fileName as String) as String s = fileName for bad in ['\n', '\r', '\t', '\v'] # CC: '\a' '\f' s = s.replace(bad, ' ') while s.contains(' '), s = s.replace(' ', ' ') s = s.replace(' ', '_') for c in Path.getInvalidFileNameChars s = s.replace(c.toString, '') maxLen = .setting('max file name length', .defaultMaxFileNameLength) s = s[:maxLen] return s def reducePath(path as String) as String prefix = '.' + Path.directorySeparatorChar.toString if path.startsWith(prefix), path = path[prefix.length:] return path def printStylesheet print '' def embedFile(fileName as String) dirs = [ Path.getDirectoryName(Assembly.getEntryAssembly.location), ] for dir in dirs if Directory.exists(dir) path = Path.combine(dir, fileName) if File.exists(path) print File.readAllText(path) continue def printContent .printHeader print '
' .printLocation .printInfo print '
' .printSearchLinks .printTypes .printNode .printSettings print '
' .printFooter def printHeader sep = '  |  ' print '
' print '
' print 'Help for [.topic.htmlEncode]' print '
' print '
' print 'The Cobra Programming Language' print '[sep]Support' print '[sep]Discuss' print '[sep]Wiki' print '
' print '
' def printLocation print '
' .heading('Location') print '' if .parentMember what = if(.parentMember inherits AbstractMethod, 'method', 'member') print '' if .parentType print '' print '' print '' print '
in [what] [.parentMember.name.htmlEncode]
in [.parentType.englishName] [.parentType.name]
in file [.node.token.fileName.htmlEncode]
at line [.node.token.lineNum]
' print '
' def printInfo print '
' .heading('Info') print '' print '' .parentNameSpaceRow(.node) # to-do: maybe the following should be refactored so that nodes augment the info # like what is done with .searchTerms and .types type as INamedNode? try type = (.node to dynamic).type catch pass if type print '' .parentNameSpaceRow(type) if .node inherits DotExpr type = nil try type = (.node to dynamic).left.receiverType catch pass if type print '' .parentNameSpaceRow(type) defi = nil try defi = (.node to dynamic).definition catch pass if defi try value = defi.cobraSourceSignature catch value = defi.idString print '' .parentNameSpaceRow(defi) print '
help for [.topic.htmlEncode]
expression type [type.name.htmlEncode]
receiver type [type.name.htmlEncode]
definition [value.toString.htmlEncode]
' print '
' def parentNameSpaceRow(node) ns as NameSpace? try ns = node.parentNameSpace catch pass if ns and not ns.isRoot print ' in namespace [ns.fullName.htmlEncode] ' def printSearchLinks if .searchTerms.count == 0, return .heading('Search') # extract the basic searchUrl searchUrls = for site in .searchSites where site['name'] == .searchEngine get site['url'] assert searchUrls.count == 1 searchUrl = searchUrls[0] print '
' print '
' first = true for site in .searchSites current = if(first, 'class="current" ', '') name = site['name'] to String rel = _siteId(name) print ' [name.htmlEncode]' first = false print '
' print '
' first = true for site in .searchSites id = _siteId(site['name']) style = if(first, '', ' style="display: none"') print '' if site.containsKey('url'), url = site['url'] else, url = searchUrl + 'site:[site["host"]]+' for keywords in .searchTerms print '' keywordsForUrl = keywords.replace(' ', '+').urlEncode print '' if site.containsKey('helpers') for helper as String in site['helpers'] helperForUrl = helper.replace(' ', '+').urlEncode print '' print '' print '
[keywords.htmlEncode]+ [helper.htmlEncode]
' first = false print '
' print '
' def _siteId(siteName as String) as String return siteName.toLower.replace(' ', '-') + '-queries' def printTypes visited = Set() i = 0 while i < .types.count # .types can grow while looping type = .types[i].nonNil if type not in visited visited.add(type) _printType(type) i += 1 def _printType(type as IType) type = type.nonNil .heading('[type.englishName] [type.name]') print '' # types: primitive, box [ class, struct, interface ], enum, mixin, extension if type inherits Box _printBoxPairs(type) else if type inherits PrimitiveType if type.box, _printBoxPairs(type.box to !) else if type inherits EnumDecl _printEnumDetails(type) # to-do: show interface as given by DocGenerator. ... is that still needed? print '
' def _printBoxPairs(box as Box) if box.baseClass .pair('base class', box.baseClass.name) if box.baseInterfaces.count .pair('base interfaces', (for i in box.baseInterfaces get i.name).sorted.join(', ')) if box.parentNameSpace .pair('parent name space', box.parentNameSpace.fullName) if box.innerType .pair('for loop type', box.innerType.name) .types.add(box.innerType) if box.isGeneric for gp in box.genericParams if gp inherits GenericParam # to-do: show generic constraints pass else if gp not in .types .types.add(gp) # to-do: invariants # to-do: attributes # members: public vs. non, static vs. instance, member type members = box.allMembers.sorted(do(a as IBoxMember, b as IBoxMember)=a.name.compareTo(b.name)) members = for member in members where not member inherits TestMethod members = for member in members where not _testMethodRE.isMatch(member.name) # to-do: constructors memberKindNames = ['constants', 'variables', 'events', 'properties', 'indexers', 'methods'] for visibility in 0 : 2 # public, non-public for level in 0 : 2 # shared, instance for kind in 0 : 6 # member type matches = List() for member in members if member inherits Box # to-do: nested types continue # if visibility == 0 and not 'public' in member.isNames, continue # if member.isFromBinaryLibrary and 'private' in member.isNames, continue else if member inherits BoxMember branch kind on 0, if not member inherits BoxConst, continue on 1, if not member inherits BoxVar, continue on 2, if not member inherits BoxEvent, continue on 3, if not member inherits Property, continue on 4, if not member inherits Indexer, continue on 5, if not member inherits Method, continue if visibility == 0 and not member.isPublic, continue if visibility == 1 and member.isPublic, continue if member.isPrivate and member.parentBox.isFromBinaryLibrary, continue if level == 0 and not member.isShared, continue if level == 1 and member.isShared, continue else throw FallThroughException(member) matches.add(member) if matches.count category = ['public', 'non-public'][visibility] category += ' ' + ['shared', 'instance'][level] category += ' ' + memberKindNames[kind] print '' print ' [category] ' print '' for match in matches, _printBoxMember(match) print '' print '' def _printBoxMember(m as IBoxMember) print '' matchId = _boxMemberId(m) print '' print _boxMemberName(m) print '' print '' print .setting('member separator', '
') print '' def _boxMemberId(member as IBoxMember) as String _nextSerialNum += 1 return 'member-[_nextSerialNum-1]' def _boxMemberName(member as IBoxMember) as String if member inherits BoxMember name = StringBuilder() if member inherits Indexer name.append('\[') print '' stop sep = '' for param in member.params name.append(sep) name.append('[param.name.htmlEncode]') sep = ', ' print '' stop name.append(']') else name.append('.' + member.name.htmlEncode) if member inherits Method if member.params.count > 0 name.append('(') print '' stop sep = '' for param in member.params name.append(sep) name.append('[param.name.htmlEncode]') sep = ', ' print '' stop name.append(')') return name.toString else return member.name.htmlEncode def _printEnumDetails(type as EnumDecl) for decl in type.declsInOrder .pair(decl.name, '') def pair(key, value) if not key inherits Html, key = .htmlEncode(key) if not value inherits Html, value = .htmlEncode(value) print '' print '[key]' print '[value]' print '' def printNode if .setting('show nodes', '0').toLower in ['1', 'true'] .heading('Nodes') _printNode('@help', .node, Set(), 1, .setting('max node depth', 2)) def _printNode(source as String, node as INode, visited as Set, depth as int, maxDepth as int) if node in visited or depth > maxDepth, return nodes = .printObject(source, node) visited.add(node) depth += 1 for name as String, subNode as INode in nodes if subNode not in visited source = '[name.htmlEncode] of [.descriptionFor(node)]' _printNode(source, subNode, visited, depth, maxDepth) def printObject(source as String, obj) as List nodes = List() props = obj.typeOf.getProperties.toList props.sort(do(a as PropertyInfo, b as PropertyInfo)=a.name.compareTo(b.name)) print '' print '' print '' i = 0 for prop in props if prop.canRead name = Utils.cobraNameForNativeMemberName(prop.name) try value = prop.getValue(obj, nil) catch exc as Exception value = '(exception: [exc.typeOf.name]: [exc.message])' if value implements INode, nodes.add([name, value]) value = CobraCore.toTechString(value).htmlEncode print '' i = (i + 1) % 2 print '
[.descriptionFor(obj)]
from [source]
[name] [value]
' return nodes def descriptionFor(obj as INode) as String if obj inherits IExpr return '[obj.toCobraSource.htmlEncode] / [obj.idString.htmlEncode]' else return obj.idString.htmlEncode def printSettings .heading('Environment Variables') .p('You can use the following environment variables to control @help\'s behavior.') print '' i = 0 for name, description in .settings name, description = .envVarName(name).htmlEncode, description.htmlEncode print '' i = (i + 1) % 2 print '
[name] [description]
' def printFooter print '' def setting(name as String, defaultValue as String) as String """ Return a setting from the environment variables, returning `defaultValue` if no env var is set. """ assert name in _settingNames envVarName = .envVarName(name) value = Environment.getEnvironmentVariable(envVarName) ? '' value = value.trim if value == '', value = defaultValue return value def setting(name as String, defaultValue as int) as int return int.parse(.setting(name, defaultValue.toString)) def envVarName(settingName as String) as String return 'COBRA_HELP_' + settingName.toUpper.replace(' ', '_') def htmlEncode(obj as Object) as String return CobraCore.htmlEncode(obj) def htmlEncode(s as String) as String return CobraCore.htmlEncode(s) def heading(name as String) print '
[name.htmlEncode]
' def p(html as String) print '

' + html + '

' def open open = .setting('open', '1').trim if open.toLower in ['false', '0'] pass else, if open.toLower in ['true', '1'] Process.start(.path) else Process.start(open, .path) extend String def htmlEncode as String return CobraCore.htmlEncode(this) def urlEncode as String return CobraCore.urlEncode(this)