Wiki

root/cobra/trunk/Source/HelpGenerator.cobra

Revision 2617, 17.1 KB (checked in by Charles.Esterbrook, 9 months ago)

For @help, added docs.go-mono.com as one of the help sites in the Search section.

  • Property svn:eol-style set to native
Line 
1use System.Diagnostics
2use System.Reflection
3use System.Text.RegularExpressions
4
5
6class HelpGenerator
7    """
8    to-do: show doc strings
9
10    to-do: show contracts
11   
12    to-do: show unit tests?
13   
14    to-do: pick up .NET XML doc file for a given DLL
15
16    to-do: sorting should be case insensitive
17    """
18   
19    var settings = [
20        ['member separator',
21            'The HTML to put between type members. Defaults to <br />.'],
22        ['directory',
23            'The directory to put the HTML files in. Defaults to the current directory.'],
24        ['file name',
25            'The template for the file name for the HTML files. Defaults to "Help_for_TOPIC.html".'],
26        ['max file name length',
27            'The maximum file name length in case TOPIC gets too long. Defaults to 64.'],
28        ['open',
29            '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.'],
30        ['show nodes',
31            'If true, show the compiler nodes and their properties.'],
32        ['max node depth',
33            'When showing nodes, the maximum node depth to traverse. Default is 2.'],
34    ]
35
36    var searchSites = [
37        {
38            'name': 'Google',
39            'url': 'http://www.google.com/search?q=',
40            'helpers': ['Cobra', '.NET', 'C#'],
41        },
42        {
43            'name': 'Cobra Language',
44            'host': 'cobra-language.com',
45        },
46        {
47            'name': 'MSDN',
48            'host': 'msdn.microsoft.com',
49        },
50        {
51            'name': 'Mono',
52            'host': 'docs.go-mono.com',
53        },
54        {
55            'name': 'Stack Overflow',
56            'host': 'stackoverflow.com',
57            'helpers': ['.NET', 'C#'],
58        },
59    ]
60
61    var searchEngine = 'Google'  # should match a searchSites above that has a 'url'
62
63    const defaultMaxFileNameLength = 64
64   
65    var _testMethodRE = Regex(r'^test_[\w]+_\d+$', RegexOptions.Compiled)
66
67    var _settingNames as Cobra.Lang.ISet<of String>
68
69    var _nextSerialNum = 1
70
71    cue init(topic as String, node as ISyntaxNode, parentMember as IBoxMember?, parentType as IType?)
72        base.init
73        _topic, _node = topic, node
74        _parentMember, _parentType = parentMember, parentType
75        _settingNames = Set<of String>(for spec in .settings get spec[0])
76
77    pro node from var as ISyntaxNode
78
79    pro parentMember from var as IBoxMember?
80
81    pro parentType from var as IType?
82   
83    pro types from var = List<of IType>()
84
85    pro searchTerms from var = List<of String>()
86   
87   
88    pro topic from var = ''
89   
90    pro path from var = ''
91
92    def generate
93        .reduceSearchTerms
94        .reduceTypes
95        directory = .setting('directory', '.')
96        fileName = .setting('file name', 'Help_for_TOPIC.html')
97        fileName = .cleanFileName(fileName.replace('TOPIC', .topic))
98        .path = .reducePath(Path.combine(directory, fileName))
99        using tw = File.createText(.path)
100            print to tw
101                print '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
102                print '<html xmlns="http://www.w3.org/1999/xhtml">'
103                print '<head>'
104                print '    <title>[.topic]</title>'
105                print '    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />'
106                .printStylesheet
107                .embedFile('WebAssets[Path.directorySeparatorChar]help.js')
108                print '</head>'
109                print '<body>'
110                .printContent
111                print '</body>'
112                print '</html>'
113        .open
114
115    def reduceSearchTerms
116        """ Get rid of duplicate search terms, trailing whitespace, etc. """
117        result = List<of String>()
118        visited = Set<of String>()
119        for term in .searchTerms
120            term = term.trim
121            while term.contains('  '), term = term.replace('  ', ' ')
122            if term.toLower not in visited
123                visited.add(term.toLower)
124                result.add(term)
125        .searchTerms = result
126
127    def reduceTypes
128        result = List<of IType>()
129        for type in .types, if type not in result, result.add(type)
130        .types = result
131
132    def cleanFileName(fileName as String) as String
133        s = fileName
134        for bad in ['\n', '\r', '\t', '\v']  # CC: '\a' '\f'
135            s = s.replace(bad, ' ')
136        while s.contains('  '), s = s.replace('  ', ' ')
137        s = s.replace(' ', '_')
138        for c in Path.getInvalidFileNameChars
139            s = s.replace(c.toString, '')
140        maxLen = .setting('max file name length', .defaultMaxFileNameLength)
141        s = s[:maxLen]
142        return s
143
144    def reducePath(path as String) as String
145        prefix = '.' + Path.directorySeparatorChar.toString
146        if path.startsWith(prefix), path = path[prefix.length:]
147        return path
148
149    def printStylesheet
150        print '<style type="text/css">'
151        .embedFile('styles-cobra-help.css')
152        print '</style>'
153   
154    def embedFile(fileName as String)
155        dirs = [
156            Path.getDirectoryName(Assembly.getEntryAssembly.location),
157        ]
158        for dir in dirs
159            if Directory.exists(dir)
160                path = Path.combine(dir, fileName)
161                if File.exists(path)
162                    print File.readAllText(path)
163                    continue
164   
165    def printContent
166        .printHeader
167        print '<div class="content">'
168        .printLocation
169        .printInfo
170        print '<div class="clear"></div>'
171        .printSearchLinks
172        .printTypes
173        .printNode
174        .printSettings
175        print '</div>'
176        .printFooter
177
178    def printHeader
179        sep = ' &nbsp;|&nbsp; '
180        print '<div class="header">'
181        print '<div class="title">'
182        print 'Help for [.topic.htmlEncode]'
183        print '</div>'
184        print '<div class="cobra">'
185        print '<a href="http://cobra-language.com/">The Cobra Programming Language</a>'
186        print '[sep]<a href="http://cobra-language.com/support">Support</a>'
187        print '[sep]<a href="http://cobra-language.com/discuss">Discuss</a>'
188        print '[sep]<a href="http://cobra-language.com/wiki">Wiki</a>'
189        print '</div>'
190        print '</div>'
191
192    def printLocation
193        print '<div class="left">'
194        .heading('Location')
195        print '<table>'
196
197        if .parentMember
198            what = if(.parentMember inherits AbstractMethod, 'method', 'member')
199            print '<tr> <td class="k"> in [what] </td> <td class="v"> [.parentMember.name.htmlEncode] </td> </tr>'
200       
201        if .parentType
202            print '<tr> <td class="k"> in [.parentType.englishName] </td> <td class="v"> [.parentType.name] </td> </tr>'
203       
204        print '<tr> <td class="k"> in file </td> <td class="v"> [.node.token.fileName.htmlEncode] </td> </tr>'
205       
206        print '<tr> <td class="k"> at line </td> <td class="v"> [.node.token.lineNum] </td> </tr>'
207       
208        print '</table>'
209        print '</div>'
210
211    def printInfo
212        print '<div class="left">'
213        .heading('Info')
214
215        print '<table>'
216
217        print '<tr> <td class="k"> help for </td> <td class="v"> [.topic.htmlEncode] </td> </tr>'
218        .parentNameSpaceRow(.node)
219
220        # to-do: maybe the following should be refactored so that nodes augment the info
221        #        like what is done with .searchTerms and .types
222
223        type as INamedNode?
224        try
225            type = (.node to dynamic).type
226        catch
227            pass
228        if type
229            print '<tr> <td class="k"> expression type </td> <td class="v"> [type.name.htmlEncode] </td> </tr>'
230            .parentNameSpaceRow(type)
231
232        if .node inherits DotExpr
233            type = nil
234            try
235                type = (.node to dynamic).left.receiverType
236            catch
237                pass
238            if type
239                print '<tr> <td class="k"> receiver type </td> <td class="v"> [type.name.htmlEncode] </td> </tr>'
240                .parentNameSpaceRow(type)
241           
242        defi = nil
243        try
244            defi = (.node to dynamic).definition
245        catch
246            pass
247        if defi
248            try
249                value = defi.cobraSourceSignature
250            catch
251                value = defi.idString
252            print '<tr> <td class="k"> definition </td> <td class="v"> [value.toString.htmlEncode] </td> </tr>'
253            .parentNameSpaceRow(defi)
254
255        print '</table>'
256        print '</div>'
257
258    def parentNameSpaceRow(node)
259        ns as NameSpace?
260        try
261            ns = node.parentNameSpace
262        catch
263            pass
264        if ns and not ns.isRoot
265            print '<tr> <td class="k"> in namespace </td> <td class="v"> [ns.fullName.htmlEncode] </td> </tr>'
266
267    def printSearchLinks
268        if .searchTerms.count == 0, return
269        .heading('Search')
270
271        # extract the basic searchUrl
272        searchUrls = for site in .searchSites where site['name'] == .searchEngine get site['url']
273        assert searchUrls.count == 1
274        searchUrl = searchUrls[0]
275       
276        print '<div class="search-container">'
277        print '    <div id="sources" class="search-sources">'
278        first = true
279        for site in .searchSites
280            current = if(first, 'class="current" ', '')
281            name = site['name'] to String
282            rel = _siteId(name)
283            print '        <a [current]rel="[rel]" href="#">[name.htmlEncode]</a>'
284            first = false
285        print '    </div>'
286
287        print '<div id="queries" class="search-queries">'
288        first = true
289        for site in .searchSites
290            id = _siteId(site['name'])
291            style = if(first, '', ' style="display: none"')
292            print '<table id="[id]" class="query-category"[style]>'
293            if site.containsKey('url'), url = site['url']
294            else, url = searchUrl + 'site:[site["host"]]+'
295            for keywords in .searchTerms
296                print '<tr>'
297                keywordsForUrl = keywords.replace(' ', '+').urlEncode
298                print '<td><a href="[url][keywordsForUrl]">[keywords.htmlEncode]</a></td>'
299                if site.containsKey('helpers')
300                    for helper as String in site['helpers']
301                        helperForUrl = helper.replace(' ', '+').urlEncode
302                        print '<td><a href="[url][keywords]+[helperForUrl]">+ [helper.htmlEncode]</a></td>'
303                print '</tr>'
304            print '</table>'
305            first = false
306        print '</div>'
307        print '</div>'
308
309    def _siteId(siteName as String) as String
310        return siteName.toLower.replace(' ', '-') + '-queries'
311
312    def printTypes
313        visited = Set<of IType>()
314        i = 0
315        while i < .types.count  # .types can grow while looping
316            type = .types[i].nonNil
317            if type not in visited
318                visited.add(type)
319                _printType(type)
320            i += 1
321
322    def _printType(type as IType)
323        type = type.nonNil
324        .heading('[type.englishName] [type.name]')
325        print '<table>'
326        # types: primitive, box [ class, struct, interface ], enum, mixin, extension
327        if type inherits Box
328            _printBoxPairs(type)
329        else if type inherits PrimitiveType
330            if type.box, _printBoxPairs(type.box to !)
331        else if type inherits EnumDecl
332            _printEnumDetails(type)
333        # to-do: show interface as given by DocGenerator. ... is that still needed?
334        print '</table>'
335   
336    def _printBoxPairs(box as Box)
337        if box.baseClass
338            .pair('base class', box.baseClass.name)
339        if box.baseInterfaces.count
340            .pair('base interfaces', (for i in box.baseInterfaces get i.name).sorted.join(', '))
341        if box.parentNameSpace
342            .pair('parent name space', box.parentNameSpace.fullName)
343        if box.innerType
344            .pair('for loop type', box.innerType.name)
345            .types.add(box.innerType)
346        if box.isGeneric
347            for gp in box.genericParams
348                if gp inherits GenericParam
349                    # to-do: show generic constraints
350                    pass
351                else if gp not in .types
352                    .types.add(gp)
353
354        # to-do: invariants
355        # to-do: attributes     
356       
357        # members: public vs. non, static vs. instance, member type
358        members = box.allMembers.sorted(do(a as IBoxMember, b as IBoxMember)=a.name.compareTo(b.name))
359        members = for member in members where not member inherits TestMethod
360        members = for member in members where not _testMethodRE.isMatch(member.name)
361
362        # to-do: constructors
363        memberKindNames = ['constants', 'variables', 'events', 'properties', 'indexers', 'methods']
364        for visibility in 0 : 2    # public, non-public
365            for level in 0 : 2     # shared, instance
366                for kind in 0 : 6  # member type
367                    matches = List<of IBoxMember>()
368                    for member in members
369                        if member inherits Box
370                            # to-do: nested types
371                            continue
372#                           if visibility == 0 and not 'public' in member.isNames, continue
373#                           if member.isFromBinaryLibrary and 'private' in member.isNames, continue
374                        else if member inherits BoxMember
375                            branch kind
376                                on 0, if not member inherits BoxConst, continue
377                                on 1, if not member inherits BoxVar, continue
378                                on 2, if not member inherits BoxEvent, continue
379                                on 3, if not member inherits Property, continue
380                                on 4, if not member inherits Indexer, continue
381                                on 5, if not member inherits Method, continue
382                            if visibility == 0 and not member.isPublic, continue
383                            if visibility == 1 and member.isPublic, continue
384                            if member.isPrivate and member.parentBox.isFromBinaryLibrary, continue
385                            if level == 0 and not member.isShared, continue
386                            if level == 1 and member.isShared, continue
387                        else
388                            throw FallThroughException(member)
389                        matches.add(member)
390                    if matches.count
391                        category = ['public', 'non-public'][visibility]
392                        category += ' ' + ['shared', 'instance'][level]
393                        category += ' ' + memberKindNames[kind]
394                        print '<tr>'
395                        print '<td class="k"> [category] </td>'
396                        print '<td class="v">'
397                        for match in matches, _printBoxMember(match)
398                        print '</td>'
399                        print '</tr>'
400
401    def _printBoxMember(m as IBoxMember)
402        print '<span class="box-pair-member">'
403        matchId = _boxMemberId(m)
404        print '<a id="link-[matchId]" href="#" rel="[matchId]" class="box-pair-link">'
405        print _boxMemberName(m)
406        print '</a>'
407        print '</span>'
408        print .setting('member separator', '<br />')
409        print '<div id="[matchId]" class="popup text">'
410        try
411            signature = (m to dynamic).cobraSourceSignature to String
412        catch
413            signature = m.name
414        print '<div class="signature">[signature.htmlEncode]</div>'
415        print '<div class="docs">'
416        # to-do: doc string
417        print '</div>'
418        print '</div>'
419
420    def _boxMemberId(member as IBoxMember) as String
421        _nextSerialNum += 1
422        return 'member-[_nextSerialNum-1]'
423
424    def _boxMemberName(member as IBoxMember) as String
425        if member inherits BoxMember
426            name = StringBuilder()
427            if member inherits Indexer
428                name.append('\[')
429                print '<span class="box-pair-paramlist">' stop
430                sep = ''
431                for param in member.params
432                    name.append(sep)
433                    name.append('<span class="box-pair-param">[param.name.htmlEncode]</span>')
434                    sep = ', '
435                print '</span>' stop
436                name.append(']')
437            else
438                name.append('.' + member.name.htmlEncode)
439                if member inherits Method
440                    if member.params.count > 0
441                        name.append('(')
442                        print '<span class="box-pair-paramlist">' stop                     
443                        sep = ''
444                        for param in member.params
445                            name.append(sep)
446                            name.append('<span class="box-pair-param">[param.name.htmlEncode]</span>')
447                            sep = ', '
448                        print '</span>' stop
449                        name.append(')')
450            return name.toString
451        else
452            return member.name.htmlEncode
453
454    def _printEnumDetails(type as EnumDecl)
455        for decl in type.declsInOrder
456            .pair(decl.name, '')
457
458    def pair(key, value)
459        if not key inherits Html, key = .htmlEncode(key)
460        if not value inherits Html, value = .htmlEncode(value)
461        print '<tr>'
462        print '<td class="k">[key]</td>'
463        print '<td class="v">[value]</td>'
464        print '</tr>'
465
466    def printNode
467        if .setting('show nodes', '0').toLower in ['1', 'true']
468            .heading('Nodes')
469            _printNode('@help', .node, Set<of INode>(), 1, .setting('max node depth', 2))
470
471    def _printNode(source as String, node as INode, visited as Set<of INode>, depth as int, maxDepth as int)
472        if node in visited or depth > maxDepth, return
473        nodes = .printObject(source, node)
474        visited.add(node)
475        depth += 1
476        for name as String, subNode as INode in nodes
477            if subNode not in visited
478                source = '[name.htmlEncode] of [.descriptionFor(node)]'
479                _printNode(source, subNode, visited, depth, maxDepth)
480
481    def printObject(source as String, obj) as List<of dynamic>
482        nodes = List<of dynamic>()
483        props = obj.typeOf.getProperties.toList
484        props.sort(do(a as PropertyInfo, b as PropertyInfo)=a.name.compareTo(b.name))
485        print '<table class="object">'
486        print '<tr> <td class="heading" colspan="2">[.descriptionFor(obj)]</td> </tr>'
487        print '<tr> <td class="source" colspan="2">from [source]</td> </tr>'
488        i = 0
489        for prop in props
490            if prop.canRead
491                name = Utils.cobraNameForNativeMemberName(prop.name)
492                try
493                    value = prop.getValue(obj, nil)
494                catch exc as Exception
495                    value = '(exception: [exc.typeOf.name]: [exc.message])'
496                if value implements INode, nodes.add([name, value])
497                value = CobraCore.toTechString(value).htmlEncode
498                print '<tr class="r[i]"> <td class="k"> [name] </td> <td class="v"> [value] </td> </tr>'
499                i = (i + 1) % 2
500        print '</table>'
501        return nodes
502
503    def descriptionFor(obj as INode) as String
504        if obj inherits IExpr
505            return '[obj.toCobraSource.htmlEncode] / [obj.idString.htmlEncode]'
506        else
507            return obj.idString.htmlEncode
508
509    def printSettings
510        .heading('Environment Variables')
511        .p('You can use the following environment variables to control @help\'s behavior.')
512        print '<table>'
513        i = 0
514        for name, description in .settings
515            name, description = .envVarName(name).htmlEncode, description.htmlEncode
516            print '<tr class="r[i]"> <td class="k"> [name] </td> <td class="v"> [description] </td> </tr>'
517            i = (i + 1) % 2
518        print '</table>'
519
520    def printFooter
521        print '<div class="footer"></div>'
522
523    def setting(name as String, defaultValue as String) as String
524        """
525        Return a setting from the environment variables,
526        returning `defaultValue` if no env var is set.
527        """
528        assert name in _settingNames
529        envVarName = .envVarName(name)
530        value = Environment.getEnvironmentVariable(envVarName) ? ''
531        value = value.trim
532        if value == '', value = defaultValue
533        return value
534
535    def setting(name as String, defaultValue as int) as int
536        return int.parse(.setting(name, defaultValue.toString))
537
538    def envVarName(settingName as String) as String
539        return 'COBRA_HELP_' + settingName.toUpper.replace(' ', '_')
540
541    def htmlEncode(obj as Object) as String
542        return CobraCore.htmlEncode(obj)
543
544    def htmlEncode(s as String) as String
545        return CobraCore.htmlEncode(s)
546
547    def heading(name as String)
548        print '<div class="heading">[name.htmlEncode]</div>'
549
550    def p(html as String)
551        print '<p>' + html + '</p>'
552
553    def open
554        open = .setting('open', '1').trim
555        if open.toLower in ['false', '0']
556            pass
557        else, if open.toLower in ['true', '1']
558            Process.start(.path)
559        else
560            Process.start(open, .path)
561
562
563extend String
564
565    def htmlEncode as String
566        return CobraCore.htmlEncode(this)
567
568    def urlEncode as String
569        return CobraCore.urlEncode(this)
Note: See TracBrowser for help on using the browser.