1 # -*- coding: utf-8 -*-
3 Sphinx domain for documenting all things cabal
5 The main reason to use this instead of adding object types to std domain
6 is the ability to generate nice 'Reference' page and also provide some meta
7 data for objects described with directives described here.
9 Most directives have at least following optional arguments
12 version of Cabal in which feature was added.
16 Feature was deprecated, and optionally since which version.
21 `:synopsis: Short desc`
22 Text used as short description on reference page.
27 .. rst:directive:: .. cabal::cfg-section
29 Describes a package.cabal section, such as library or executable.
31 All following `pkg-field` directives will add section name
32 to their fields name for disambiguating duplicates.
34 You can reset the section disambiguation with `.. pkg-section:: None`.
36 .. rst::role:: pkg-section
38 References section added by `.. pkg-section`
40 .. rst:directive:: .. cabal::pkg-field
42 Describes a package.cabal field.
44 Can have a :default: field. Will group on reference page under pkg-section
45 if set and parent header otherwise.
47 .. rst::role:: pkg-field
49 References field added by `.. pkg-field`, fields can be disambiguated
50 with section name `:pkg-field:`section:field`.
53 .. rst:directive:: .. cabal:cfg-section::
55 Same as `.. cabal::pkg-section` but does not produce any visible output
58 .. rst:directive:: .. cabal:cfg-field::
60 Describes a cabal.project field.
62 Can have multiple arguments, if arguments start with '-' then it is treated
65 Can have a :default: field. Will group on reference page under pkg-section
66 if set and parent header otherwise.
68 .. rst::role:: cfg-field
70 References field added by `.. cfg-field`.
72 .. rst::role:: cfg-flag
74 References flag added by `.. cfg-field`.
77 All roles can be supplied with title as in standard sphinx references::
79 :pkg-field:`Build dependencies<build-depends>`
84 - Directives for describing executables, their subcommands and flags.
86 These should act in a way similar to `.. std::option` directive, but with
87 extra meta. And should also end up in reference.
89 At least setup and 'new-build` subcommands should get special directives
91 - Improve rendering of flags in `.. cfg-field::` directive. It should be
92 possible without copy-pasting code from sphinx.directives.ObjectDescription
93 by examining result of ObjectDescription.run and inserting flags into
96 Alternatively Or `.. flags::` sub-directive can be added which will be aware
97 of parent `.. cfg-field` directive.
99 - With same ObjectDescription.run trick as above, render since and deprecated
100 info same way as standard object fields, and use fancy rendering only on
103 - Add 'since_version` config value to sphinx env and use it to decide if
104 version meta info should be rendered on reference page and thus reduce some
106 Can also be used to generate 'Whats new' reference page
113 from docutils
import nodes
114 from docutils
.parsers
.rst
import Directive
, directives
, roles
116 import pygments
.lexer
as lexer
117 import pygments
.token
as token
119 from distutils
.version
import StrictVersion
121 from sphinx
import addnodes
122 from sphinx
.directives
import ObjectDescription
123 from sphinx
.domains
import ObjType
, Domain
, Index
124 from sphinx
.domains
.std
import StandardDomain
125 from sphinx
.locale
import _
126 from sphinx
.roles
import XRefRole
127 from sphinx
.util
.docfields
import Field
, DocFieldTransformer
128 from sphinx
.util
.nodes
import make_refnode
130 def parse_deprecated(txt
):
134 return StrictVersion(txt
)
138 def parse_flag(env
, sig
, signode
):
141 for i
, flag
in enumerate(sig
.split(',')):
144 parts
= flag
.split('=')
148 if len(parts
) == 0: continue
152 sig
= sep
+ ' '.join(parts
[1:])
153 sig
= re
.sub(r
'<([-a-zA-Z ]+)>', r
'⟨\1⟩', sig
)
155 signode
+= addnodes
.desc_name(', ', ', ')
156 signode
+= addnodes
.desc_name(name
, name
)
158 signode
+= addnodes
.desc_addname(sig
, sig
)
165 Meta data associated with object
176 self
.deprecated
= deprecated
177 self
.removed
= removed
178 self
.synopsis
= synopsis
180 self
.section
= section
184 def find_section_title(parent
):
186 Find current section id and title if possible
188 while parent
is not None:
189 if isinstance(parent
, nodes
.section
):
191 parent
= parent
.parent
196 section_id
= parent
['ids'][0]
197 section_name
= parent
['names'][0]
200 if isinstance(kid
, nodes
.title
):
201 return kid
.astext(), section_id
203 print(section_name
, section_id
)
204 return section_name
, section_id
207 class CabalSection(Directive
):
209 Marks section to which following objects belong, used to disambiguate
210 references to fields and flags which can have similar names
212 Does not generate any output besides anchor.
215 required_arguments
= 1
216 optional_arguments
= 0
217 final_argument_whitespace
= True
220 'deprecated': parse_deprecated
,
221 'removed': StrictVersion
,
222 'since' : StrictVersion
,
223 'synopsis' : lambda x
:x
,
225 section_key
= 'cabal:pkg-section'
226 target_prefix
= 'pkg-section-'
230 def get_index_entry(self
, name
):
231 return self
.indextemplate
% name
234 env
= self
.state
.document
.settings
.env
235 section
= self
.arguments
[0].strip()
238 self
.domain
, self
.objtype
= self
.name
.split(':', 1)
240 self
.domain
, self
.objtype
= '', self
.name
242 if section
== 'None':
243 env
.ref_context
.pop(self
.section_key
, None)
246 env
.ref_context
[self
.section_key
] = section
247 targetname
= self
.target_prefix
+ section
248 node
= nodes
.target('', '', ids
=[targetname
])
249 self
.state
.document
.note_explicit_target(node
)
251 indexentry
= self
.get_index_entry(section
)
253 inode
= addnodes
.index(
255 (self
.indextype
, indexentry
, targetname
, '', None)])
257 # find title of parent section node
258 title
= find_section_title(self
.state
.parent
)
260 data_key
= CabalDomain
.types
[self
.objtype
]
262 # find how many sections in this document were added
263 num
= env
.domaindata
['cabal']['index-num'].get(env
.docname
, 0)
264 env
.domaindata
['cabal']['index-num'][env
.docname
] = num
+ 1
266 meta
= Meta(since
=self
.options
.get('since'),
267 deprecated
=self
.options
.get('deprecated'),
268 removed
=self
.options
.get('removed'),
269 synopsis
=self
.options
.get('synopsis'),
273 store
= env
.domaindata
['cabal'][data_key
]
274 if not section
in store
:
275 store
[section
] = env
.docname
, targetname
, meta
280 class CabalObject(ObjectDescription
):
282 'noindex' : directives
.flag
,
283 'deprecated': parse_deprecated
,
284 'removed' : StrictVersion
,
285 'since' : StrictVersion
,
286 'synopsis' : lambda x
:x
289 # node attribute marking which section field belongs to
291 # template for index, it is passed a field name as argument
292 # used by default deg_index_entry method
297 Collect meta data for fields
299 Reads optional arguments passed to directive and also
300 tries to find current section title and adds it as section
302 env
= self
.state
.document
.settings
.env
303 # find title of current section, will group references page by it
304 num
= env
.domaindata
['cabal']['index-num'].get(env
.docname
, 0)
305 env
.domaindata
['cabal']['index-num'][env
.docname
] = num
+ 1
307 title
= find_section_title(self
.state
.parent
)
308 return Meta(since
=self
.options
.get('since'),
309 deprecated
=self
.options
.get('deprecated'),
310 removed
=self
.options
.get('removed'),
313 synopsis
=self
.options
.get('synopsis'))
315 def get_env_key(self
, env
, name
):
317 Should return a key used to reference this field and key in domain
318 data to store this object
320 section
= self
.env
.ref_context
.get(self
.section_key
)
321 store
= CabalDomain
.types
[self
.objtype
]
322 return (section
, name
), store
324 def get_index_entry(self
, env
, name
):
326 Should return index entry and anchor
328 By default uses indextemplate attribute to generate name and
329 index entry by joining directive name, section and field name
331 section
= self
.env
.ref_context
.get(self
.section_key
)
333 if section
is not None:
334 parts
= (self
.objtype
, section
, name
)
335 indexentry
= self
.indextemplate
% (section
+ ':' + name
)
337 parts
= (self
.objtype
, name
)
338 indexentry
= self
.indextemplate
% name
340 targetname
= '-'.join(parts
)
341 return indexentry
, targetname
344 def add_target_and_index(self
, name
, sig
, signode
):
346 As in sphinx.directive.ObjectDescription
348 By default adds 'pair' index as returned by get_index_entry and
349 stores object data into domain data store as returned by get_env_data
351 env
= self
.state
.document
.settings
.env
353 indexentry
, targetname
= self
.get_index_entry(self
, name
)
355 signode
['ids'].append(targetname
)
356 self
.state
.document
.note_explicit_target(signode
)
358 inode
= addnodes
.index(
359 entries
=[('pair', indexentry
, targetname
, '', None)])
360 signode
.insert(0, inode
)
362 key
, store
= self
.get_env_key(env
, name
)
363 env
.domaindata
['cabal'][store
][key
] = env
.docname
, targetname
, self
.cabal_meta
366 self
.cabal_meta
= self
.get_meta()
367 result
= super(CabalObject
, self
).run()
369 if self
.cabal_meta
.since
is not None \
370 or self
.cabal_meta
.deprecated
is not None:
372 #find content part of description
374 if isinstance(item
, addnodes
.desc
):
381 if isinstance(item
, addnodes
.desc_content
):
387 # find exsting field list and add to it
389 for item
in contents
:
390 if isinstance(item
, nodes
.field_list
):
394 field_list
= nodes
.field_list('')
395 contents
.insert(0, field_list
)
398 if self
.cabal_meta
.since
is not None:
400 field
= nodes
.field('')
401 field_name
= nodes
.field_name('Since', 'Since')
402 since
= 'Cabal ' + str(self
.cabal_meta
.since
)
403 field_body
= nodes
.field_body(since
, nodes
.paragraph(since
, since
))
406 field_list
.insert(0, field
)
408 if self
.cabal_meta
.deprecated
is not None:
409 field
= nodes
.field('')
410 field_name
= nodes
.field_name('Deprecated', 'Deprecated')
411 if isinstance(self
.cabal_meta
.deprecated
, StrictVersion
):
412 since
= 'Cabal ' + str(self
.cabal_meta
.deprecated
)
416 field_body
= nodes
.field_body(since
, nodes
.paragraph(since
, since
))
419 field_list
.insert(0, field
)
421 if self
.cabal_meta
.removed
is not None:
422 field
= nodes
.field('')
423 field_name
= nodes
.field_name('Removed', 'Removed')
424 if isinstance(self
.cabal_meta
.removed
, StrictVersion
):
425 since
= 'Cabal ' + str(self
.cabal_meta
.removed
)
429 field_body
= nodes
.field_body(since
, nodes
.paragraph(since
, since
))
432 field_list
.insert(0, field
)
435 class CabalPackageSection(CabalObject
):
437 Cabal section in package.cabal file
439 section_key
= 'cabal:pkg-section'
440 indextemplate
= '%s; package.cabal section'
442 def handle_signature(self
, sig
, signode
):
444 As in sphinx.directives.ObjectDescription
446 By default make an object description from name and adding
447 either deprecated or since as annotation.
449 env
= self
.state
.document
.settings
.env
452 parts
= sig
.split(' ',1)
454 signode
+= addnodes
.desc_name(name
, name
)
455 signode
+= addnodes
.desc_addname(' ', ' ')
457 rest
= parts
[1].strip()
458 signode
+= addnodes
.desc_annotation(rest
, rest
)
462 def get_env_key(self
, env
, name
):
463 store
= CabalDomain
.types
[self
.objtype
]
467 env
= self
.state
.document
.settings
.env
468 section
= self
.arguments
[0].strip().split(' ',1)[0]
469 if section
== 'None':
470 env
.ref_context
.pop('cabal:pkg-section', None)
472 env
.ref_context
['cabal:pkg-section'] = section
473 return super(CabalPackageSection
, self
).run()
476 class CabalField(CabalObject
):
478 Base for fields in *.cabal files
481 'noindex' : directives
.flag
,
482 'deprecated': parse_deprecated
,
483 'removed' : StrictVersion
,
484 'since' : StrictVersion
,
485 'synopsis' : lambda x
:x
489 Field('default', label
='Default value', names
=['default'], has_arg
=False)
492 def handle_signature(self
, sig
, signode
):
494 As in sphinx.directives.ObjectDescription
496 By default make an object description from name and adding
497 either deprecated or since as annotation.
499 env
= self
.state
.document
.settings
.env
502 parts
= sig
.split(':',1)
504 signode
+= addnodes
.desc_name(name
, name
)
505 signode
+= addnodes
.desc_addname(': ', ': ')
508 rest
= parts
[1].strip()
509 signode
+= addnodes
.desc_annotation(rest
, rest
)
513 class CabalPackageField(CabalField
):
515 Describes section in package.cabal file
517 section_key
= 'cabal:pkg-section'
518 indextemplate
= '%s; package.cabal field'
520 class CabalFieldXRef(XRefRole
):
522 Cross ref node for all kinds of fields
524 Gets section_key entry from context and stores it on node, so it can
525 later be used by CabalDomain.resolve_xref to find target for reference to
528 section_key
= 'cabal:pkg-section'
529 def process_link(self
, env
, refnode
, has_explicit_title
, title
, target
):
530 parts
= target
.split(':',1)
532 section
, target
= parts
533 section
= section
.strip()
534 target
= target
.strip()
535 refnode
[self
.section_key
] = section
537 refnode
[self
.section_key
] = env
.ref_context
.get(self
.section_key
)
542 # Directives for config files.
545 class CabalPackageFieldXRef(CabalFieldXRef
):
547 Role referencing cabal.project section
549 section_key
= 'cabal:pkg-section'
551 class CabalConfigSection(CabalSection
):
553 Marks section in package.cabal file
555 indextemplate
= '%s; cabal.project section'
556 section_key
= 'cabal:cfg-section'
557 target_prefix
= 'cfg-section-'
559 class ConfigField(CabalField
):
560 section_key
= 'cabal:cfg-section'
561 indextemplate
= '%s ; cabal project option'
562 def handle_signature(self
, sig
, signode
):
564 if sig
.startswith('-'):
565 name
= parse_flag(self
, sig
, signode
)
567 name
= super(ConfigField
, self
).handle_signature(sig
, signode
)
571 def get_index_entry(self
, env
, name
):
572 if name
.startswith('-'):
573 section
= self
.env
.ref_context
.get(self
.section_key
)
574 if section
is not None:
575 parts
= ('cfg-flag', section
, name
)
576 indexname
= section
+ ':' + name
578 parts
= ('cfg-flag', name
)
580 indexentry
= name
+ '; cabal project option'
581 targetname
= '-'.join(parts
)
582 return indexentry
, targetname
584 return super(ConfigField
,self
).get_index_entry(env
, name
)
586 def get_env_key(self
, env
, name
):
587 section
= self
.env
.ref_context
.get(self
.section_key
)
588 if name
.startswith('-'):
589 return (section
, name
), 'cfg-flags'
590 return (section
, name
), 'cfg-fields'
592 class CabalConfigFieldXRef(CabalFieldXRef
):
593 section_key
= 'cabal:cfg-section'
600 class ConfigFieldIndex(Index
):
601 name
= 'projectindex'
602 localname
= "Cabal reference"
603 shortname
= "Reference"
606 def __init__(self
, typ
, name
, doc
, anchor
, meta
):
613 def _gather_data(self
, obj_types
):
615 Gather objects and return [(title, [Entry])]
617 def massage(typ
, datum
):
618 name
, (doc
, anchor
, meta
) = datum
619 return self
.Entry(typ
, name
, doc
, anchor
, meta
)
622 for typ
in obj_types
:
623 store
= CabalDomain
.types
[typ
]
624 fields
+= [massage(typ
, x
)
625 for x
in self
.domain
.data
[store
].items()]
627 fields
.sort(key
=lambda x
: (x
.doc
, x
.meta
.index
))
634 current_title
= fields
[0].meta
.title
636 if field
.meta
.title
!= current_title
:
637 result
.append((current_title
, current
))
639 current_title
= field
.meta
.title
640 current
.append(field
)
641 result
.append((current_title
, current
))
646 def generate(self
, docnames
=None):
648 Try to group entries such that if entry has a section then put it
651 Otherwise group it under same `title`.
653 Try to keep in same order as it was defined.
655 sort by (document, index)
656 group on (document, doc_section)
658 TODO: Check how to extract section numbers from (document,doc_section)
659 and add it as annotation to titles
662 # (title, section store, fields store)
663 entries
= [('cabal.project fields', 'cfg-section', 'cfg-field'),
664 ('cabal project flags', 'cfg-section', 'cfg-flag'),
665 ('package.cabal fields', 'pkg-section', 'pkg-field')]
668 for label
, section_key
, key
in entries
:
670 data
= self
._gather
_data
([section_key
, key
])
673 for section
, entries
in data
:
675 elem_type
= 0 # Normal entry
677 elem_type
= 2 # sub_entry
679 assert len(entries
) != 0
680 docname
= entries
[0].doc
681 if section
is not None:
682 section_title
, section_anchor
= section
684 (section_title
, 1, docname
, section_anchor
, '', '', ''))
686 for entry
in entries
:
688 if isinstance(entry
.name
, tuple):
694 extra
= render_meta(meta
)
695 descr
= meta
.synopsis
if meta
.synopsis
is not None else ''
696 field
= (name
, elem_type
, docname
,
697 entry
.anchor
, extra
, '', descr
)
698 references
.append(field
)
699 result
.append((label
, references
))
703 def make_data_keys(typ
, target
, node
):
705 Returns a list of keys to search for targets of this type
708 Used for resolving references
710 if typ
== 'pkg-field':
711 section
= node
.get('cabal:pkg-section')
712 return [(section
, target
),
714 elif typ
in ('cfg-field', 'cfg-flag'):
715 section
= node
.get('cabal:cfg-section')
716 return [(section
, target
), (None, target
)]
721 def render_deprecated(deprecated
):
722 if isinstance(deprecated
, StrictVersion
):
723 return 'deprecated since: '+str(deprecated
)
727 def render_removed(deprecated
, removed
):
728 if isinstance(deprecated
, StrictVersion
):
729 return 'removed in: ' + str(removed
) + '; deprecated since: '+str(deprecated
)
731 return 'removed in: ' + str(removed
)
733 def render_meta(meta
):
735 Render meta as short text
737 Will render either deprecated or since info
739 if meta
.removed
is not None:
740 return render_removed(meta
.deprecated
, meta
.removed
)
741 if meta
.deprecated
is not None:
742 return render_deprecated(meta
.deprecated
)
743 elif meta
.since
is not None:
744 return 'since version: ' + str(meta
.since
)
748 def render_meta_title(meta
):
750 Render meta as suitable to use in titles
752 rendered
= render_meta(meta
)
754 return '(' + rendered
+ ')'
757 def make_title(typ
, key
, meta
):
759 Render title of an object (section, field or flag)
761 if typ
== 'pkg-section':
762 return "package.cabal " + key
+ " section " + render_meta_title(meta
)
764 elif typ
== 'pkg-field':
766 if section
is not None:
767 base
= "package.cabal " + section
+ " section " + name
+ ": field"
769 base
= "package.cabal " + name
+ " field"
771 return base
+ render_meta_title(meta
)
773 elif typ
== 'cfg-section':
774 return "cabal.project " + key
+ " section " + render_meta_title(meta
)
776 elif typ
== 'cfg-field':
778 return "cabal.project " + name
+ " field " + render_meta_title(meta
)
780 elif typ
== 'cfg-flag':
782 return "cabal flag " + name
+ " " + render_meta_title(meta
)
785 raise ValueError("Unknown type: " + typ
)
787 def make_full_name(typ
, key
, meta
):
789 Return an anchor name for object type
791 if typ
== 'pkg-section':
792 return 'pkg-section-' + key
794 elif typ
== 'pkg-field':
796 if section
is not None:
797 return '-'.join(('pkg-field',section
, name
))
799 return 'pkg-field-' + name
801 elif typ
== 'cfg-field':
802 return 'cfg-field-' + key
805 raise ValueError('Unknown object type: ' + typ
)
807 class CabalDomain(Domain
):
809 Sphinx domain for cabal
811 needs Domain.merge_doc for parallel building, just union all dicts
816 'pkg-section': ObjType(_('pkg-section'), 'pkg-section'),
817 'pkg-field' : ObjType(_('pkg-field') , 'pkg-field' ),
818 'cfg-section': ObjType(_('cfg-section'), 'cfg-section'),
819 'cfg-field' : ObjType(_('cfg-field') , 'cfg-field' ),
822 'pkg-section': CabalPackageSection
,
823 'pkg-field' : CabalPackageField
,
824 'cfg-section': CabalConfigSection
,
825 'cfg-field' : ConfigField
,
828 'pkg-section': XRefRole(warn_dangling
=True),
829 'pkg-field' : CabalPackageFieldXRef(warn_dangling
=True),
830 'cfg-section': XRefRole(warn_dangling
=True),
831 'cfg-field' : CabalConfigFieldXRef(warn_dangling
=True),
832 'cfg-flag' : CabalConfigFieldXRef(warn_dangling
=True),
838 'index-num' : {}, #per document number of objects
839 # used to order references page
847 'pkg-section': 'pkg-sections',
848 'pkg-field' : 'pkg-fields',
849 'cfg-section': 'cfg-sections',
850 'cfg-field' : 'cfg-fields',
851 'cfg-flag' : 'cfg-flags',
853 def clear_doc(self
, docname
):
854 for k
in ['pkg-sections', 'pkg-fields', 'cfg-sections',
855 'cfg-fields', 'cfg-flags']:
857 for name
, (fn
, _
, _
) in self
.data
[k
].items():
861 del self
.data
[k
][name
]
863 del self
.data
['index-num'][docname
]
867 def resolve_xref(self
, env
, fromdocname
, builder
, type, target
, node
, contnode
):
868 objtypes
= self
.objtypes_for_role(type)
869 for typ
, key
in ((typ
, key
)
871 for key
in make_data_keys(typ
, target
, node
)):
873 data
= env
.domaindata
['cabal'][self
.types
[typ
]][key
]
876 doc
, ref
, meta
= data
877 title
= make_title(typ
, key
, meta
)
878 return make_refnode(builder
, fromdocname
, doc
, ref
, contnode
, title
)
880 def get_objects(self
):
882 Used for search functionality
884 for typ
in ['pkg-section', 'pkg-field',
885 'cfg-section', 'cfg-field', 'cfg-flag']:
886 key
= self
.types
[typ
]
887 for name
, (fn
, target
, meta
) in self
.data
[key
].items():
888 title
= make_title(typ
, name
, meta
)
889 yield title
, title
, typ
, fn
, target
, 0
891 class CabalLexer(lexer
.RegexLexer
):
893 Basic cabal lexer, does not try to be smart
897 filenames
= ['.cabal']
902 (r
'^(\s*)(--.*)$', lexer
.bygroups(token
.Whitespace
, token
.Comment
.Single
)),
904 (r
'^(\s*)([\w\-_]+)(:)',
905 lexer
.bygroups(token
.Whitespace
, token
.Keyword
, token
.Punctuation
)),
906 (r
'^([\w\-_]+)', token
.Keyword
), # library, executable, flag etc.
907 (r
'[^\S\n]+', token
.Text
),
908 (r
'&&|\|\||==|<=|\^>=|>=|<|>', token
.Operator
),
909 (r
',|:|{|}', token
.Punctuation
),
915 app
.add_domain(CabalDomain
)
916 app
.add_lexer('cabal', CabalLexer
)