Coverage for C:\Repos\leo-editor\leo\plugins\importers\coffeescript.py: 65%
138 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
1#@+leo-ver=5-thin
2#@+node:ekr.20160505094722.1: * @file ../plugins/importers/coffeescript.py
3"""The @auto importer for coffeescript."""
4import re
5from typing import Any, Dict, List
6from leo.core import leoGlobals as g
7from leo.plugins.importers import linescanner
8Importer = linescanner.Importer
9Target = linescanner.Target
10#@+others
11#@+node:ekr.20160505094722.2: ** class CS_Importer(Importer)
12class CS_Importer(Importer):
14 #@+others
15 #@+node:ekr.20160505101118.1: *3* coffee_i.__init__
16 def __init__(self, importCommands, **kwargs):
17 """Ctor for CoffeeScriptScanner class."""
18 super().__init__(
19 importCommands,
20 language='coffeescript',
21 state_class=CS_ScanState, # Not used: This class overrides i.scan_line.
22 strict=True
23 )
24 self.errors = 0
25 self.root = None
26 self.tab_width = None # NOT the same as self.c.tabwidth. Set in run().
27 #@+node:ekr.20161129024357.1: *3* coffee_i.get_new_dict
28 #@@nobeautify
30 def get_new_dict(self, context):
31 """
32 Return a *general* state dictionary for the given context.
33 Subclasses may override...
34 """
35 comment, block1, block2 = self.single_comment, self.block1, self.block2
37 def add_key(d, key, data):
38 aList = d.get(key,[])
39 aList.append(data)
40 d[key] = aList
42 d: Dict[str, List[Any]]
44 if context:
45 d = {
46 # key kind pattern ends?
47 '\\': [('len+1', '\\', None),],
48 '#': [('len', '###', context == '###'),],
49 '"': [('len', '"', context == '"'),],
50 "'": [('len', "'", context == "'"),],
51 }
52 if block1 and block2:
53 add_key(d, block2[0], ('len', block1, True))
54 else:
55 # Not in any context.
56 d = {
57 # key kind pattern new-ctx deltas
58 '\\':[('len+1', '\\', context, None),],
59 '#': [('len','###','###', None),], # Docstring
60 '"': [('len', '"', '"', None),],
61 "'": [('len', "'", "'", None),],
62 '{': [('len', '{', context, (1,0,0)),],
63 '}': [('len', '}', context, (-1,0,0)),],
64 '(': [('len', '(', context, (0,1,0)),],
65 ')': [('len', ')', context, (0,-1,0)),],
66 '[': [('len', '[', context, (0,0,1)),],
67 ']': [('len', ']', context, (0,0,-1)),],
68 }
69 if comment:
70 add_key(d, comment[0], ('all', comment, '', None))
71 if block1 and block2:
72 add_key(d, block1[0], ('len', block1, block1, None))
73 return d
74 #@+node:ekr.20161119170345.1: *3* coffee_i.Overrides for i.gen_lines
75 #@+node:ekr.20161118134555.2: *4* coffee_i.end_block
76 def end_block(self, line, new_state, stack):
77 """
78 Handle an unusual case: an underindented tail line.
80 line is **not** a class/def line. It *is* underindented so it
81 *terminates* the previous block.
82 """
83 top = stack[-1]
84 assert new_state.indent < top.state.indent, (new_state, top.state)
85 self.cut_stack(new_state, stack)
86 top = stack[-1]
87 self.add_line(top.p, line)
88 tail_p = None if top.at_others_flag else top.p
89 return tail_p
90 #@+node:ekr.20161118134555.3: *4* coffee_i.cut_stack (Same as Python)
91 def cut_stack(self, new_state, stack):
92 """Cut back the stack until stack[-1] matches new_state."""
93 assert len(stack) > 1 # Fail on entry.
94 while stack:
95 top_state = stack[-1].state
96 if new_state.level() < top_state.level():
97 assert len(stack) > 1, stack # <
98 stack.pop()
99 elif top_state.level() == new_state.level():
100 assert len(stack) > 1, stack # ==
101 stack.pop()
102 break
103 else:
104 # This happens often in valid coffescript programs.
105 break
106 # Restore the guard entry if necessary.
107 if len(stack) == 1:
108 stack.append(stack[-1])
109 assert len(stack) > 1 # Fail on exit.
110 #@+node:ekr.20161118134555.6: *4* coffee_i.start_new_block
111 def start_new_block(self, i, lines, new_state, prev_state, stack):
112 """Create a child node and update the stack."""
113 assert not new_state.in_context(), new_state
114 line = lines[i]
115 top = stack[-1]
116 # Adjust the stack.
117 if new_state.indent > top.state.indent:
118 pass
119 elif new_state.indent == top.state.indent:
120 stack.pop()
121 else:
122 self.cut_stack(new_state, stack)
123 # Create the child.
124 top = stack[-1]
125 parent = top.p
126 h = self.gen_ref(line, parent, top)
127 child = self.create_child_node(parent, line, h)
128 stack.append(Target(child, new_state))
129 #@+node:ekr.20161118134555.7: *4* coffee_i.starts_block
130 pattern_table = [
131 re.compile(r'^\s*class'),
132 re.compile(r'^\s*(.+):(.*)->'),
133 re.compile(r'^\s*(.+)=(.*)->'),
134 ]
136 def starts_block(self, i, lines, new_state, prev_state):
137 """True if the line starts with the patterns above outside any context."""
138 if prev_state.in_context():
139 return False
140 line = lines[i]
141 for pattern in self.pattern_table:
142 if pattern.match(line):
143 return True
144 return False
146 #@+node:ekr.20161108181857.1: *3* coffee_i.post_pass & helpers
147 def post_pass(self, parent):
148 """Massage the created nodes."""
149 #
150 # Generic: use base Importer methods...
151 self.clean_all_headlines(parent)
152 self.clean_all_nodes(parent)
153 #
154 # Specific to coffeescript...
155 self.move_trailing_lines(parent)
156 #
157 # Generic: use base Importer methods...
158 self.unindent_all_nodes(parent)
159 #
160 # This sub-pass must follow unindent_all_nodes.
161 self.promote_trailing_underindented_lines(parent)
162 #
163 # This probably should be the last sub-pass.
164 self.delete_all_empty_nodes(parent)
165 #@+node:ekr.20160505170558.1: *4* coffee_i.move_trailing_lines & helper (not ready)
166 def move_trailing_lines(self, parent):
167 """Move trailing lines into the following node."""
168 return # Not ready yet, and maybe never.
169 # pylint: disable=unreachable
170 prev_lines = []
171 last = None
172 for p in parent.subtree():
173 trailing_lines = self.delete_trailing_lines(p)
174 if prev_lines:
175 self.prepend_lines(p, prev_lines)
176 prev_lines = trailing_lines
177 last = p.copy()
178 if prev_lines:
179 # These should go after the @others lines in the parent.
180 lines = self.get_lines(parent)
181 for i, s in enumerate(lines):
182 if s.strip().startswith('@others'):
183 lines = lines[: i + 1] + prev_lines + lines[i + 2 :]
184 self.set_lines(parent, lines)
185 break
186 else:
187 # Fall back.
188 assert last, "move_trailing_lines"
189 self.set_lines(last, prev_lines)
190 #@+node:ekr.20160505173347.1: *5* coffee_i.delete_trailing_lines
191 def delete_trailing_lines(self, p):
192 """Delete the trailing lines of p and return them."""
193 body_lines, trailing_lines = [], []
194 for s in self.get_lines(p):
195 if s.isspace():
196 trailing_lines.append(s)
197 else:
198 body_lines.extend(trailing_lines)
199 body_lines.append(s)
200 trailing_lines = []
201 # Clear trailing lines if they are all blank.
202 if all(z.isspace() for z in trailing_lines):
203 trailing_lines = []
204 self.set_lines(p, body_lines)
205 return trailing_lines
206 #@+node:ekr.20160505180032.1: *4* coffee_i.undent_coffeescript_body
207 def undent_coffeescript_body(self, s):
208 """Return the undented body of s."""
209 lines = g.splitLines(s)
210 # Undent all leading whitespace or comment lines.
211 leading_lines = []
212 for line in lines:
213 if self.is_ws_line(line):
214 # Tricky. Stipping a black line deletes it.
215 leading_lines.append(line if line.isspace() else line.lstrip())
216 else:
217 break
218 i = len(leading_lines)
219 # Don't unindent the def/class line! It prevents later undents.
220 tail = self.undent_body_lines(lines[i:], ignoreComments=True)
221 # Remove all blank lines from leading lines.
222 if 0:
223 for i, line in enumerate(leading_lines):
224 if not line.isspace():
225 leading_lines = leading_lines[i:]
226 break
227 result = ''.join(leading_lines) + tail
228 return result
229 #@-others
230 @classmethod
231 def do_import(cls):
232 def f(c, s, parent):
233 return cls(c.importCommands).run(s, parent)
234 return f
235#@+node:ekr.20161110045131.1: ** class CS_ScanState
236class CS_ScanState:
237 """A class representing the state of the coffeescript line-oriented scan."""
239 def __init__(self, d=None):
240 """CS_ScanState ctor."""
241 if d:
242 indent = d.get('indent')
243 is_ws_line = d.get('is_ws_line')
244 prev = d.get('prev')
245 assert indent is not None and is_ws_line is not None
246 self.bs_nl = False
247 self.context = prev.context
248 self.indent = prev.indent if prev.bs_nl else indent
249 else:
250 self.bs_nl = False
251 self.context = ''
252 self.indent = 0
254 #@+others
255 #@+node:ekr.20161118064325.1: *3* cs_state.__repr__
256 def __repr__(self):
257 """CS_State.__repr__"""
258 return '<CSState %r indent: %s>' % (self.context, self.indent)
260 __str__ = __repr__
261 #@+node:ekr.20161119115413.1: *3* cs_state.level
262 def level(self):
263 """CS_ScanState.level."""
264 return self.indent
265 #@+node:ekr.20161118140100.1: *3* cs_state.in_context
266 def in_context(self):
267 """True if in a special context."""
268 return self.context or self.bs_nl
269 #@+node:ekr.20161119052920.1: *3* cs_state.update
270 def update(self, data):
271 """
272 Update the state using the 6-tuple returned by i.scan_line.
273 Return i = data[1]
274 """
275 context, i, delta_c, delta_p, delta_s, bs_nl = data
276 # self.bs_nl = bs_nl
277 self.context = context
278 # self.curlies += delta_c
279 # self.parens += delta_p
280 # self.squares += delta_s
281 return i
283 #@-others
284#@-others
285importer_dict = {
286 'func': CS_Importer.do_import(),
287 'extensions': ['.coffee',],
288}
289#@@language python
290#@@tabwidth -4
291#@-leo