Coverage for C:\leo.repo\leo-editor\leo\plugins\importers\coffeescript.py : 59%

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