77"""
88
99from __future__ import annotations
10- from typing import List , Tuple , Optional , Any , Dict
11- from dataclasses import dataclass , field
12- from abc import ABC , abstractclassmethod
10+ from typing import List , Tuple , Optional
1311import itertools
1412
1513from docutils import nodes
1614
1715
1816__title__ = 'sphinxnotes-snippet'
19- __license__ = 'BSD' ,
17+ __license__ = 'BSD'
2018__version__ = '1.0b6'
2119__author__ = 'Shengyu Zhang'
2220__url__ = 'https://sphinx-notes.github.io/snippet'
2321__description__ = 'Non-intrusive snippet manager for Sphinx documentation'
2422__keywords__ = 'documentation, sphinx, extension, utility'
2523
26- @dataclass
27- class Snippet (ABC ):
24+ class Snippet (object ):
2825 """
29- Snippet is a {abstract,data}class represents a snippet of reStructuredText
30- documentation. Note that it is not always continuous fragment at text (rst)
31- level.
32- """
33- _scope :Tuple [int ,int ] = field (init = False )
34- _refid :Optional [str ] = field (init = False )
35-
36- def __post_init__ (self ) -> None :
37- """Post-init processing routine of dataclass"""
38-
39- # Calcuate scope before deepcopy
40- scope = [float ('inf' ), - float ('inf' )]
41- for node in self .nodes ():
42- if not node .line :
43- continue # Skip node that have None line, I dont know why :'(
44- scope [0 ] = min (scope [0 ], line_of_start (node ))
45- scope [1 ] = max (scope [1 ], line_of_end (node ))
46- self ._scope = scope
47-
48- # Find exactly one id attr in nodes
49- self ._refid = None
50- for node in self .nodes ():
51- if node ['ids' ]:
52- self ._refid = node ['ids' ][0 ]
53- break
54- # If no node has id, use parent's
55- if not self ._refid :
56- for node in self .nodes ():
57- if node .parent ['ids' ]:
58- self ._refid = node .parent ['ids' ][0 ]
59- break
60-
61-
62- @abstractclassmethod
63- def nodes (self ) -> List [nodes .Node ]:
64- """Return the out of tree nodes that make up this snippet."""
65- pass
66-
67-
68- @abstractclassmethod
69- def excerpt (self ) -> str :
70- """Return excerpt of snippet (for preview)."""
71- pass
72-
73-
74- @abstractclassmethod
75- def kind (self ) -> str :
76- """Return kind of snippet (for filtering)."""
77- pass
78-
79-
80- def file (self ) -> str :
81- """Return source file path of snippet"""
82- # All nodes should have same source file
83- return self .nodes ()[0 ].source
84-
85-
86- def scope (self ) -> Tuple [int ,int ]:
87- """
88- Return the scope of snippet, which corresponding to the line
89- number in the source file.
90-
91- A scope is a left closed and right open interval of the line number
92- ``[left, right)``.
93- """
94- return self ._scope
95-
96-
97- def text (self ) -> List [str ]:
98- """Return the original reStructuredText text of snippet."""
99- return read_partial_file (self .file (), self .scope ())
26+ Snippet is base class of reStructuredText snippet.
10027
28+ :param nodes: Document nodes that make up this snippet
29+ """
10130
102- def refid (self ) -> Optional [str ]:
103- """
104- Return the possible identifier key of snippet.
105- It is picked from nodes' (or nodes' parent's) `ids attr`_.
31+ #: Source file path of snippet
32+ file :str
10633
107- .. _ids attr: https://docutils.sourceforge.io/docs/ref/doctree.html#ids
108- """
109- return self . _refid
34+ #: Line number range of snippet, in the source file which is left closed
35+ #: and right opened.
36+ lineno : Tuple [ int , int ]
11037
38+ #: The original reStructuredText of snippet
39+ rst :List [str ]
11140
112- def __getstate__ (self ) -> Dict [str ,Any ]:
113- """Implement :py:meth:`pickle.object.__getstate__`."""
114- return self .__dict__ .copy ()
41+ #: The possible identifier key of snippet, which is picked from nodes'
42+ #: (or nodes' parent's) `ids attr`_.
43+ #:
44+ #: .. _ids attr: https://docutils.sourceforge.io/docs/ref/doctree.html#ids
45+ refid :Optional [str ]
11546
47+ def __init__ (self , * nodes :nodes .Node ) -> None :
48+ assert len (nodes ) != 0
11649
117- @dataclass
118- class Headline (Snippet ):
119- """Documentation title and possible subtitle."""
120- title :nodes .title
121- subtitle :Optional [nodes .title ]
50+ self .file = nodes [0 ].source
12251
123- def nodes (self ) -> List [nodes .Node ]:
124- if not self .subtitle :
125- return [self .title ]
126- return [self .title , self .subtitle ]
52+ lineno = [float ('inf' ), - float ('inf' )]
53+ for node in nodes :
54+ if not node .line :
55+ continue # Skip node that have None line, I dont know why
56+ lineno [0 ] = min (lineno [0 ], _line_of_start (node ))
57+ lineno [1 ] = max (lineno [1 ], _line_of_end (node ))
58+ self .lineno = lineno
59+
60+ lines = []
61+ with open (self .file , "r" ) as f :
62+ start = self .lineno [0 ] - 1
63+ stop = self .lineno [1 ] - 1
64+ for line in itertools .islice (f , start , stop ):
65+ lines .append (line .strip ('\n ' ))
66+ self .rst = lines
67+
68+ # Find exactly one ID attr in nodes
69+ self .refid = None
70+ for node in nodes :
71+ if node ['ids' ]:
72+ self .refid = node ['ids' ][0 ]
73+ break
12774
75+ # If no ID found, try parent
76+ if not self .refid :
77+ for node in nodes :
78+ if node .parent ['ids' ]:
79+ self .refid = node .parent ['ids' ][0 ]
80+ break
12881
129- def excerpt (self ) -> str :
130- if not self .subtitle :
131- return '<%s>' % self .title .astext ()
132- return '<%s ~%s~>' % (self .title .astext (), self .subtitle .astext ())
13382
13483
135- @ classmethod
136- def kind ( cls ) -> str :
137- return 'd'
84+ class Text ( Snippet ):
85+ #: Text of snippet
86+ text : str
13887
88+ def __init__ (self , node :nodes .Node ) -> None :
89+ super ().__init__ (node )
90+ self .text = node .astext ()
13991
140- def text (self ) -> List [str ]:
141- """
142- Headline represents a reStructuredText document,
143- so return the whole source file.
144- """
145- with open (self .file ()) as f :
146- return f .read ().splitlines ()
14792
93+ class CodeBlock (Text ):
94+ #: Language of code block
95+ language :str
96+ #: Caption of code block
97+ caption :Optional [str ]
14898
149- def __getstate__ (self ) -> Dict [ str , Any ] :
150- self . title = self . title . deepcopy ( )
151- if self . subtitle :
152- self .subtitle = self . subtitle . deepcopy ()
153- return super (). __getstate__ ( )
99+ def __init__ (self , node : nodes . literal_block ) -> None :
100+ assert isinstance ( node , nodes . literal_block )
101+ super (). __init__ ( node )
102+ self .language = node [ 'language' ]
103+ self . caption = node . get ( 'caption' )
154104
155105
156- @dataclass
157- class Code (Snippet ):
158- """A code block with description."""
159- description :List [nodes .Body ]
160- block :nodes .literal_block
106+ class WithCodeBlock (object ):
107+ code_blocks :List [CodeBlock ]
161108
162- def nodes (self ) -> List [nodes .Node ]:
163- return self .description .copy () + [self .block ]
109+ def __init__ (self , nodes :nodes .Nodes ) -> None :
110+ self .code_blocks = []
111+ for n in nodes .traverse (nodes .literal_block ):
112+ self .code_blocks .append (self .CodeBlock (n ))
164113
165114
166- def excerpt (self ) -> str :
167- return '/%s/ ' % self .language () + \
168- self .description [0 ].astext ().replace ('\n ' , '' )
115+ class Title (Text ):
116+ def __init__ (self , node :nodes .title ) -> None :
117+ assert isinstance (node , nodes .title )
118+ super ().__init__ (node )
169119
170120
171- @classmethod
172- def kind (cls ) -> str :
173- return 'c'
121+ class WithTitle (object ):
122+ title :Optional [Title ]
174123
124+ def __init__ (self , node :nodes .Node ) -> None :
125+ title_node = node .next_node (nodes .title )
126+ self .title = Title (title_node ) if title_node else None
175127
176- def language (self ) -> str :
177- """Return the (programing) language that appears in code."""
178- return self .block ['language' ]
179128
129+ class Section (Snippet , WithTitle ):
130+ def __init__ (self , node :nodes .section ) -> None :
131+ assert isinstance (node , nodes .section )
132+ Snippet .__init__ (self , node )
133+ WithTitle .__init__ (self , node )
180134
181- def __getstate__ (self ) -> Dict [str ,Any ]:
182- self .description = [x .deepcopy () for x in self .description ]
183- self .block = self .block .deepcopy ()
184- return super ().__getstate__ ()
185135
136+ class Document (Section ):
137+ def __init__ (self , node :nodes .document ) -> None :
138+ assert isinstance (node , nodes .document )
139+ super ().__init__ (node .next_node (nodes .section ))
186140
187- def read_partial_file (filename :str , scope :Tuple [int ,Optional [int ]]) -> List [str ]:
188- lines = []
189- with open (filename , "r" ) as f :
190- start = scope [0 ] - 1
191- stop = scope [1 ] - 1 if scope [1 ] else None
192- for line in itertools .islice (f , start , stop ):
193- lines .append (line .strip ('\n ' ))
194- return lines
195141
142+ ################
143+ # Nodes helper #
144+ ################
196145
197- def line_of_start (node :nodes .Node ) -> int :
146+ def _line_of_start (node :nodes .Node ) -> int :
198147 assert node .line
199148 if isinstance (node , nodes .title ):
200149 if isinstance (node .parent .parent , nodes .document ):
@@ -213,11 +162,11 @@ def line_of_start(node:nodes.Node) -> int:
213162 return node .line
214163
215164
216- def line_of_end (node :nodes .Node ) -> Optional [int ]:
165+ def _line_of_end (node :nodes .Node ) -> Optional [int ]:
217166 next_node = node .next_node (descend = False , siblings = True , ascend = True )
218167 while next_node :
219168 if next_node .line :
220- return line_of_start (next_node )
169+ return _line_of_start (next_node )
221170 next_node = next_node .next_node (
222171 # Some nodes' line attr is always None, but their children has
223172 # valid line attr
0 commit comments