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(CabalObject
):
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 def handle_signature(self
, sig
, signode
):
561 As in sphinx.directives.ObjectDescription
563 By default make an object description from name and adding
564 either deprecated or since as annotation.
566 env
= self
.state
.document
.settings
.env
569 parts
= sig
.split(' ',1)
571 signode
+= addnodes
.desc_name(name
, name
)
572 signode
+= addnodes
.desc_addname(' ', ' ')
574 rest
= parts
[1].strip()
575 signode
+= addnodes
.desc_annotation(rest
, rest
)
579 def get_env_key(self
, env
, name
):
580 store
= CabalDomain
.types
[self
.objtype
]
584 env
= self
.state
.document
.settings
.env
585 section
= self
.arguments
[0].strip().split(' ',1)[0]
586 if section
== 'None':
587 env
.ref_context
.pop('cabal:cfg-section', None)
589 env
.ref_context
['cabal:cfg-section'] = section
590 return super(CabalConfigSection
, self
).run()
592 class ConfigField(CabalField
):
593 section_key
= 'cabal:cfg-section'
594 indextemplate
= '%s ; cabal project option'
595 def handle_signature(self
, sig
, signode
):
597 if sig
.startswith('-'):
598 name
= parse_flag(self
, sig
, signode
)
600 name
= super(ConfigField
, self
).handle_signature(sig
, signode
)
604 def get_index_entry(self
, env
, name
):
605 if name
.startswith('-'):
606 section
= self
.env
.ref_context
.get(self
.section_key
)
607 if section
is not None:
608 parts
= ('cfg-flag', section
, name
)
609 indexname
= section
+ ':' + name
611 parts
= ('cfg-flag', name
)
613 indexentry
= name
+ '; cabal project option'
614 targetname
= '-'.join(parts
)
615 return indexentry
, targetname
617 return super(ConfigField
,self
).get_index_entry(env
, name
)
619 def get_env_key(self
, env
, name
):
620 section
= self
.env
.ref_context
.get(self
.section_key
)
621 if name
.startswith('-'):
622 return (section
, name
), 'cfg-flags'
623 return (section
, name
), 'cfg-fields'
625 class CabalConfigFieldXRef(CabalFieldXRef
):
626 section_key
= 'cabal:cfg-section'
633 class ConfigFieldIndex(Index
):
634 name
= 'syntax-quicklinks'
635 localname
= "Cabal Syntax Quicklinks"
636 shortname
= "Quicklinks"
639 def __init__(self
, typ
, name
, doc
, anchor
, meta
):
646 def _gather_data(self
, obj_types
):
648 Gather objects and return [(title, [Entry])]
650 def massage(typ
, datum
):
651 name
, (doc
, anchor
, meta
) = datum
652 return self
.Entry(typ
, name
, doc
, anchor
, meta
)
655 for typ
in obj_types
:
656 store
= CabalDomain
.types
[typ
]
657 fields
+= [massage(typ
, x
)
658 for x
in self
.domain
.data
[store
].items()]
660 fields
.sort(key
=lambda x
: (x
.doc
, x
.meta
.index
))
667 current_title
= fields
[0].meta
.title
669 if field
.meta
.title
!= current_title
:
670 result
.append((current_title
, current
))
672 current_title
= field
.meta
.title
673 current
.append(field
)
674 result
.append((current_title
, current
))
679 def generate(self
, docnames
=None):
681 Try to group entries such that if entry has a section then put it
684 Otherwise group it under same `title`.
686 Try to keep in same order as it was defined.
688 sort by (document, index)
689 group on (document, doc_section)
691 TODO: Check how to extract section numbers from (document,doc_section)
692 and add it as annotation to titles
695 # (title, section store, fields store)
696 entries
= [('cabal.project fields', 'cfg-section', 'cfg-field'),
697 ('cabal project flags', 'cfg-section', 'cfg-flag'),
698 ('package.cabal fields', 'pkg-section', 'pkg-field')]
701 for label
, section_key
, key
in entries
:
703 data
= self
._gather
_data
([section_key
, key
])
706 for section
, entries
in data
:
708 elem_type
= 0 # Normal entry
710 elem_type
= 2 # sub_entry
712 assert len(entries
) != 0
713 docname
= entries
[0].doc
714 if section
is not None:
715 section_title
, section_anchor
= section
717 (section_title
, 1, docname
, section_anchor
, '', '', ''))
719 for entry
in entries
:
721 if isinstance(entry
.name
, tuple):
727 extra
= render_meta(meta
)
728 descr
= meta
.synopsis
if meta
.synopsis
is not None else ''
729 field
= (name
, elem_type
, docname
,
730 entry
.anchor
, extra
, '', descr
)
731 references
.append(field
)
732 result
.append((label
, references
))
736 def make_data_keys(typ
, target
, node
):
738 Returns a list of keys to search for targets of this type
741 Used for resolving references
743 if typ
== 'pkg-field':
744 section
= node
.get('cabal:pkg-section')
745 return [(section
, target
),
747 elif typ
in ('cfg-field', 'cfg-flag'):
748 section
= node
.get('cabal:cfg-section')
749 return [(section
, target
), (None, target
)]
754 def render_deprecated(deprecated
):
755 if isinstance(deprecated
, StrictVersion
):
756 return 'deprecated since: '+str(deprecated
)
760 def render_removed(deprecated
, removed
):
761 if isinstance(deprecated
, StrictVersion
):
762 return 'removed in: ' + str(removed
) + '; deprecated since: '+str(deprecated
)
764 return 'removed in: ' + str(removed
)
766 def render_meta(meta
):
768 Render meta as short text
770 Will render either deprecated or since info
772 if meta
.removed
is not None:
773 return render_removed(meta
.deprecated
, meta
.removed
)
774 if meta
.deprecated
is not None:
775 return render_deprecated(meta
.deprecated
)
776 elif meta
.since
is not None:
777 return 'since version: ' + str(meta
.since
)
781 def render_meta_title(meta
):
783 Render meta as suitable to use in titles
785 rendered
= render_meta(meta
)
787 return '(' + rendered
+ ')'
790 def make_title(typ
, key
, meta
):
792 Render title of an object (section, field or flag)
794 if typ
== 'pkg-section':
795 return "package.cabal " + key
+ " section " + render_meta_title(meta
)
797 elif typ
== 'pkg-field':
799 if section
is not None:
800 base
= "package.cabal " + section
+ " section " + name
+ ": field"
802 base
= "package.cabal " + name
+ " field"
804 return base
+ render_meta_title(meta
)
806 elif typ
== 'cfg-section':
807 return "cabal.project " + key
+ " section " + render_meta_title(meta
)
809 elif typ
== 'cfg-field':
811 return "cabal.project " + name
+ " field " + render_meta_title(meta
)
813 elif typ
== 'cfg-flag':
815 return "cabal flag " + name
+ " " + render_meta_title(meta
)
818 raise ValueError("Unknown type: " + typ
)
820 def make_full_name(typ
, key
, meta
):
822 Return an anchor name for object type
824 if typ
== 'pkg-section':
825 return 'pkg-section-' + key
827 elif typ
== 'cfg-section':
828 return 'cfg-section-' + key
830 elif typ
== 'pkg-field':
832 if section
is not None:
833 return '-'.join(('pkg-field',section
, name
))
835 return 'pkg-field-' + name
837 elif typ
== 'cfg-field':
838 return 'cfg-field-' + key
841 raise ValueError('Unknown object type: ' + typ
)
843 class CabalDomain(Domain
):
845 Sphinx domain for cabal
847 needs Domain.merge_doc for parallel building, just union all dicts
852 'pkg-section': ObjType(_('pkg-section'), 'pkg-section'),
853 'pkg-field' : ObjType(_('pkg-field') , 'pkg-field' ),
854 'cfg-section': ObjType(_('cfg-section'), 'cfg-section'),
855 'cfg-field' : ObjType(_('cfg-field') , 'cfg-field' ),
858 'pkg-section': CabalPackageSection
,
859 'pkg-field' : CabalPackageField
,
860 'cfg-section': CabalConfigSection
,
861 'cfg-field' : ConfigField
,
864 'pkg-section': XRefRole(warn_dangling
=True),
865 'pkg-field' : CabalPackageFieldXRef(warn_dangling
=True),
866 'cfg-section': XRefRole(warn_dangling
=True),
867 'cfg-field' : CabalConfigFieldXRef(warn_dangling
=True),
868 'cfg-flag' : CabalConfigFieldXRef(warn_dangling
=True),
874 'index-num' : {}, #per document number of objects
875 # used to order references page
883 'pkg-section': 'pkg-sections',
884 'pkg-field' : 'pkg-fields',
885 'cfg-section': 'cfg-sections',
886 'cfg-field' : 'cfg-fields',
887 'cfg-flag' : 'cfg-flags',
889 def clear_doc(self
, docname
):
890 for k
in ['pkg-sections', 'pkg-fields', 'cfg-sections',
891 'cfg-fields', 'cfg-flags']:
893 for name
, (fn
, _
, _
) in self
.data
[k
].items():
897 del self
.data
[k
][name
]
899 del self
.data
['index-num'][docname
]
903 def resolve_xref(self
, env
, fromdocname
, builder
, type, target
, node
, contnode
):
904 objtypes
= self
.objtypes_for_role(type)
905 for typ
, key
in ((typ
, key
)
907 for key
in make_data_keys(typ
, target
, node
)):
909 data
= env
.domaindata
['cabal'][self
.types
[typ
]][key
]
912 doc
, ref
, meta
= data
913 title
= make_title(typ
, key
, meta
)
914 return make_refnode(builder
, fromdocname
, doc
, ref
, contnode
, title
)
916 def get_objects(self
):
918 Used for search functionality
920 for typ
in ['pkg-section', 'pkg-field',
921 'cfg-section', 'cfg-field', 'cfg-flag']:
922 key
= self
.types
[typ
]
923 for name
, (fn
, target
, meta
) in self
.data
[key
].items():
924 title
= make_title(typ
, name
, meta
)
925 yield title
, title
, typ
, fn
, target
, 0
927 class CabalLexer(lexer
.RegexLexer
):
929 Basic cabal lexer, does not try to be smart
933 filenames
= ['.cabal']
938 (r
'^(\s*)(--.*)$', lexer
.bygroups(token
.Whitespace
, token
.Comment
.Single
)),
940 (r
'^(\s*)([\w\-_]+)(:)',
941 lexer
.bygroups(token
.Whitespace
, token
.Keyword
, token
.Punctuation
)),
942 (r
'^([\w\-_]+)', token
.Keyword
), # library, executable, flag etc.
943 (r
'[^\S\n]+', token
.Text
),
944 (r
'&&|\|\||==|<=|\^>=|>=|<|>', token
.Operator
),
945 (r
',|:|{|}', token
.Punctuation
),
951 app
.add_domain(CabalDomain
)
952 app
.add_lexer('cabal', CabalLexer
)