##// END OF EJS Templates
more test fixed
Matthias BUSSONNIER -
Show More
@@ -1,333 +1,334 b''
1 1 from __future__ import print_function
2 2 from converters.utils import remove_fake_files_url
3 3
4 4 # Stdlib
5 5 import codecs
6 6 import io
7 7 import logging
8 8 import os
9 9 import pprint
10 10 import re
11 11 import subprocess
12 12 import sys
13 13 import json
14 14 import copy
15 15 from types import FunctionType
16 16 from shutil import rmtree
17 17
18 18 # From IPython
19 19 from IPython.external import argparse
20 20 from IPython.nbformat import current as nbformat
21 21 from IPython.utils.text import indent
22 22 from IPython.nbformat.v3.nbjson import BytesEncoder
23 23 from IPython.utils import path, py3compat
24 24
25 25 # local
26 26 from lexers import IPythonLexer
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Class declarations
30 30 #-----------------------------------------------------------------------------
31 31
32 32 class ConversionException(Exception):
33 33 pass
34 34
35 35 class DocStringInheritor(type):
36 36 """
37 37 This metaclass will walk the list of bases until the desired
38 38 superclass method is found AND if that method has a docstring and only
39 39 THEN does it attach the superdocstring to the derived class method.
40 40
41 41 Please use carefully, I just did the metaclass thing by following
42 42 Michael Foord's Metaclass tutorial
43 43 (http://www.voidspace.org.uk/python/articles/metaclasses.shtml), I may
44 44 have missed a step or two.
45 45
46 46 source:
47 47 http://groups.google.com/group/comp.lang.python/msg/26f7b4fcb4d66c95
48 48 by Paul McGuire
49 49 """
50 50 def __new__(meta, classname, bases, classDict):
51 51 newClassDict = {}
52 52 for attributeName, attribute in classDict.items():
53 53 if type(attribute) == FunctionType:
54 54 # look through bases for matching function by name
55 55 for baseclass in bases:
56 56 if hasattr(baseclass, attributeName):
57 57 basefn = getattr(baseclass,attributeName)
58 58 if basefn.__doc__:
59 59 attribute.__doc__ = basefn.__doc__
60 60 break
61 61 newClassDict[attributeName] = attribute
62 62 return type.__new__(meta, classname, bases, newClassDict)
63 63
64 64 class Converter(object):
65 65 __metaclass__ = DocStringInheritor
66 66 default_encoding = 'utf-8'
67 67 extension = str()
68 68 figures_counter = 0
69 69 infile = str()
70 70 infile_dir = str()
71 71 infile_root = str()
72 72 files_dir = str()
73 73 with_preamble = True
74 74 user_preamble = None
75 output = str()
75 output = unicode()
76 76 raw_as_verbatim = False
77 77
78 78 def __init__(self, infile):
79 79 self.infile = infile
80 80 self.infile_dir, infile_root = os.path.split(infile)
81 81 infile_root = os.path.splitext(infile_root)[0]
82 82 files_dir = os.path.join(self.infile_dir, infile_root + '_files')
83 83 if not os.path.isdir(files_dir):
84 84 os.mkdir(files_dir)
85 85 self.infile_root = infile_root
86 86 self.files_dir = files_dir
87 87 self.outbase = os.path.join(self.infile_dir, infile_root)
88 88
89 89 def __del__(self):
90 90 if not os.listdir(self.files_dir):
91 91 os.rmdir(self.files_dir)
92 92
93 93 def dispatch(self, cell_type):
94 94 """return cell_type dependent render method, for example render_code
95 95 """
96 96 return getattr(self, 'render_' + cell_type, self.render_unknown)
97 97
98 98 def dispatch_display_format(self, format):
99 99 """return output_type dependent render method, for example render_output_text
100 100 """
101 101 return getattr(self, 'render_display_format_' + format, self.render_unknown_display)
102 102
103 103 def convert(self, cell_separator='\n'):
104 104 """
105 105 Generic method to converts notebook to a string representation.
106 106
107 107 This is accomplished by dispatching on the cell_type, so subclasses of
108 108 Convereter class do not need to re-implement this method, but just
109 109 need implementation for the methods that will be dispatched.
110 110
111 111 Parameters
112 112 ----------
113 113 cell_separator : string
114 114 Character or string to join cells with. Default is "\n"
115 115
116 116 Returns
117 117 -------
118 118 out : string
119 119 """
120 120 lines = []
121 121 lines.extend(self.optional_header())
122 122 lines.extend(self.main_body(cell_separator))
123 123 lines.extend(self.optional_footer())
124 124 return u'\n'.join(lines)
125 125
126 126 def main_body(self, cell_separator='\n'):
127 127 converted_cells = []
128 128 for worksheet in self.nb.worksheets:
129 129 for cell in worksheet.cells:
130 130 #print(cell.cell_type) # dbg
131 131 conv_fn = self.dispatch(cell.cell_type)
132 132 if cell.cell_type in ('markdown', 'raw'):
133 133 remove_fake_files_url(cell)
134 134 converted_cells.append('\n'.join(conv_fn(cell)))
135 135 cell_lines = cell_separator.join(converted_cells).split('\n')
136 136 return cell_lines
137 137
138 138 def render(self):
139 139 "read, convert, and save self.infile"
140 140 if not hasattr(self, 'nb'):
141 141 self.read()
142 142 self.output = self.convert()
143 assert(type(self.output) == unicode)
143 144 return self.save()
144 145
145 146 def read(self):
146 147 "read and parse notebook into NotebookNode called self.nb"
147 148 with open(self.infile) as f:
148 149 self.nb = nbformat.read(f, 'json')
149 150
150 151 def save(self, outfile=None, encoding=None):
151 152 "read and parse notebook into self.nb"
152 153 if outfile is None:
153 154 outfile = self.outbase + '.' + self.extension
154 155 if encoding is None:
155 156 encoding = self.default_encoding
156 157 with io.open(outfile, 'w', encoding=encoding) as f:
157 158 f.write(self.output)
158 159 return os.path.abspath(outfile)
159 160
160 161 def optional_header(self):
161 162 """
162 163 Optional header to insert at the top of the converted notebook
163 164
164 165 Returns a list
165 166 """
166 167 return []
167 168
168 169 def optional_footer(self):
169 170 """
170 171 Optional footer to insert at the end of the converted notebook
171 172
172 173 Returns a list
173 174 """
174 175 return []
175 176
176 177 def _new_figure(self, data, fmt):
177 178 """Create a new figure file in the given format.
178 179
179 180 Returns a path relative to the input file.
180 181 """
181 182 figname = '%s_fig_%02i.%s' % (self.infile_root,
182 183 self.figures_counter, fmt)
183 184 self.figures_counter += 1
184 185 fullname = os.path.join(self.files_dir, figname)
185 186
186 187 # Binary files are base64-encoded, SVG is already XML
187 188 if fmt in ('png', 'jpg', 'pdf'):
188 189 data = data.decode('base64')
189 190 fopen = lambda fname: open(fname, 'wb')
190 191 else:
191 192 fopen = lambda fname: codecs.open(fname, 'wb', self.default_encoding)
192 193
193 194 with fopen(fullname) as f:
194 195 f.write(data)
195 196
196 197 return fullname
197 198
198 199 def render_heading(self, cell):
199 200 """convert a heading cell
200 201
201 202 Returns list."""
202 203 raise NotImplementedError
203 204
204 205 def render_code(self, cell):
205 206 """Convert a code cell
206 207
207 208 Returns list."""
208 209 raise NotImplementedError
209 210
210 211 def render_markdown(self, cell):
211 212 """convert a markdown cell
212 213
213 214 Returns list."""
214 215 raise NotImplementedError
215 216
216 217 def _img_lines(self, img_file):
217 218 """Return list of lines to include an image file."""
218 219 # Note: subclasses may choose to implement format-specific _FMT_lines
219 220 # methods if they so choose (FMT in {png, svg, jpg, pdf}).
220 221 raise NotImplementedError
221 222
222 223 def render_display_data(self, output):
223 224 """convert display data from the output of a code cell
224 225
225 226 Returns list.
226 227 """
227 228 lines = []
228 229
229 230 for fmt in output.keys():
230 231 if fmt in ['png', 'svg', 'jpg', 'pdf']:
231 232 img_file = self._new_figure(output[fmt], fmt)
232 233 # Subclasses can have format-specific render functions (e.g.,
233 234 # latex has to auto-convert all SVG to PDF first).
234 235 lines_fun = getattr(self, '_%s_lines' % fmt, None)
235 236 if not lines_fun:
236 237 lines_fun = self._img_lines
237 238 lines.extend(lines_fun(img_file))
238 239 elif fmt != 'output_type':
239 240 conv_fn = self.dispatch_display_format(fmt)
240 241 lines.extend(conv_fn(output))
241 242 return lines
242 243
243 244 def render_raw(self, cell):
244 245 """convert a cell with raw text
245 246
246 247 Returns list."""
247 248 raise NotImplementedError
248 249
249 250 def render_unknown(self, cell):
250 251 """Render cells of unkown type
251 252
252 253 Returns list."""
253 254 data = pprint.pformat(cell)
254 255 logging.warning('Unknown cell: %s' % cell.cell_type)
255 256 return self._unknown_lines(data)
256 257
257 258 def render_unknown_display(self, output, type):
258 259 """Render cells of unkown type
259 260
260 261 Returns list."""
261 262 data = pprint.pformat(output)
262 263 logging.warning('Unknown output: %s' % output.output_type)
263 264 return self._unknown_lines(data)
264 265
265 266 def render_stream(self, output):
266 267 """render the stream part of an output
267 268
268 269 Returns list.
269 270
270 271 Identical to render_display_format_text
271 272 """
272 273 return self.render_display_format_text(output)
273 274
274 275 def render_pyout(self, output):
275 276 """convert pyout part of a code cell
276 277
277 278 Returns list."""
278 279 raise NotImplementedError
279 280
280 281
281 282 def render_pyerr(self, output):
282 283 """convert pyerr part of a code cell
283 284
284 285 Returns list."""
285 286 raise NotImplementedError
286 287
287 288 def _unknown_lines(self, data):
288 289 """Return list of lines for an unknown cell.
289 290
290 291 Parameters
291 292 ----------
292 293 data : str
293 294 The content of the unknown data as a single string.
294 295 """
295 296 raise NotImplementedError
296 297
297 298 # These are the possible format types in an output node
298 299
299 300 def render_display_format_text(self, output):
300 301 """render the text part of an output
301 302
302 303 Returns list.
303 304 """
304 305 raise NotImplementedError
305 306
306 307 def render_display_format_html(self, output):
307 308 """render the html part of an output
308 309
309 310 Returns list.
310 311 """
311 312 raise NotImplementedError
312 313
313 314 def render_display_format_latex(self, output):
314 315 """render the latex part of an output
315 316
316 317 Returns list.
317 318 """
318 319 raise NotImplementedError
319 320
320 321 def render_display_format_json(self, output):
321 322 """render the json part of an output
322 323
323 324 Returns list.
324 325 """
325 326 raise NotImplementedError
326 327
327 328 def render_display_format_javascript(self, output):
328 329 """render the javascript part of an output
329 330
330 331 Returns list.
331 332 """
332 333 raise NotImplementedError
333 334
@@ -1,81 +1,82 b''
1 1 from converters.base import Converter
2 from converters.utils import cell_to_lines
2 3 from shutil import rmtree
3 4 import json
4 5
5 6 class ConverterNotebook(Converter):
6 7 """
7 8 A converter that is essentially a null-op.
8 9 This exists so it can be subclassed
9 10 for custom handlers of .ipynb files
10 11 that create new .ipynb files.
11 12
12 13 What distinguishes this from JSONWriter is that
13 14 subclasses can specify what to do with each type of cell.
14 15
15 16 Writes out a notebook file.
16 17
17 18 """
18 19 extension = 'ipynb'
19 20
20 21 def __init__(self, infile, outbase):
21 22 Converter.__init__(self, infile)
22 23 self.outbase = outbase
23 24 rmtree(self.files_dir)
24 25
25 26 def convert(self):
26 return json.dumps(json.loads(Converter.convert(self, ',')), indent=1, sort_keys=True)
27 return unicode(json.dumps(json.loads(Converter.convert(self, ',')), indent=1, sort_keys=True))
27 28
28 29 def optional_header(self):
29 30 s = \
30 31 """{
31 32 "metadata": {
32 33 "name": "%(name)s"
33 34 },
34 35 "nbformat": 3,
35 36 "worksheets": [
36 37 {
37 38 "cells": [""" % {'name':self.outbase}
38 39
39 40 return s.split('\n')
40 41
41 42 def optional_footer(self):
42 43 s = \
43 44 """]
44 45 }
45 46 ]
46 47 }"""
47 48 return s.split('\n')
48 49
49 50 def render_heading(self, cell):
50 51 return cell_to_lines(cell)
51 52
52 53 def render_code(self, cell):
53 54 return cell_to_lines(cell)
54 55
55 56 def render_markdown(self, cell):
56 57 return cell_to_lines(cell)
57 58
58 59 def render_raw(self, cell):
59 60 return cell_to_lines(cell)
60 61
61 62 def render_pyout(self, output):
62 63 return cell_to_lines(output)
63 64
64 65 def render_pyerr(self, output):
65 66 return cell_to_lines(output)
66 67
67 68 def render_display_format_text(self, output):
68 69 return [output.text]
69 70
70 71 def render_display_format_html(self, output):
71 72 return [output.html]
72 73
73 74 def render_display_format_latex(self, output):
74 75 return [output.latex]
75 76
76 77 def render_display_format_json(self, output):
77 78 return [output.json]
78 79
79 80
80 81 def render_display_format_javascript(self, output):
81 82 return [output.javascript]
@@ -1,327 +1,333 b''
1 1 from __future__ import print_function
2 2 from lexers import IPythonLexer
3
3 4 import subprocess
5 import copy
6 import json
4 7 import re
8
5 9 from IPython.utils.text import indent
10 from IPython.utils import path, py3compat
11 from IPython.nbformat.v3.nbjson import BytesEncoder
6 12
7 13 #-----------------------------------------------------------------------------
8 14 # Utility functions
9 15 #-----------------------------------------------------------------------------
10 16 def highlight(src, lang='ipython'):
11 17 """Return a syntax-highlighted version of the input source.
12 18 """
13 19 from pygments import highlight
14 20 from pygments.lexers import get_lexer_by_name
15 21 from pygments.formatters import HtmlFormatter
16 22
17 23 if lang == 'ipython':
18 24 lexer = IPythonLexer()
19 25 else:
20 26 lexer = get_lexer_by_name(lang, stripall=True)
21 27
22 28 return highlight(src, lexer, HtmlFormatter())
23 29
24 30 def output_container(f):
25 31 """add a prompt-area next to an output"""
26 32 def wrapped(self, output):
27 33 rendered = f(self, output)
28 34 if not rendered:
29 35 # empty output
30 36 return []
31 37 lines = []
32 38 lines.append('<div class="hbox output_area">')
33 39 lines.extend(self._out_prompt(output))
34 40 classes = "output_subarea output_%s" % output.output_type
35 41 if 'html' in output.keys():
36 42 classes += ' output_html rendered_html'
37 43 if output.output_type == 'stream':
38 44 classes += " output_%s" % output.stream
39 45 lines.append('<div class="%s">' % classes)
40 46 lines.extend(rendered)
41 47 lines.append('</div>') # subarea
42 48 lines.append('</div>') # output_area
43 49
44 50 return lines
45 51
46 52 return wrapped
47 53
48 54 def text_cell(f):
49 55 """wrap text cells in appropriate divs"""
50 56 def wrapped(self, cell):
51 57 rendered = f(self, cell)
52 58 classes = "text_cell_render border-box-sizing rendered_html"
53 59 lines = ['<div class="%s">' % classes] + rendered + ['</div>']
54 60 return lines
55 61 return wrapped
56 62
57 63
58 64 def remove_fake_files_url(cell):
59 65 """Remove from the cell source the /files/ pseudo-path we use.
60 66 """
61 67 src = cell.source
62 68 cell.source = src.replace('/files/', '')
63 69
64 70
65 71 # ANSI color functions:
66 72
67 73 def remove_ansi(src):
68 74 """Strip all ANSI color escape sequences from input string.
69 75
70 76 Parameters
71 77 ----------
72 78 src : string
73 79
74 80 Returns
75 81 -------
76 82 string
77 83 """
78 84 return re.sub(r'\033\[(0|\d;\d\d)m', '', src)
79 85
80 86
81 87 def ansi2html(txt):
82 88 """Render ANSI colors as HTML colors
83 89
84 90 This is equivalent to util.fixConsole in utils.js
85 91
86 92 Parameters
87 93 ----------
88 94 txt : string
89 95
90 96 Returns
91 97 -------
92 98 string
93 99 """
94 100
95 101 ansi_colormap = {
96 102 '30': 'ansiblack',
97 103 '31': 'ansired',
98 104 '32': 'ansigreen',
99 105 '33': 'ansiyellow',
100 106 '34': 'ansiblue',
101 107 '35': 'ansipurple',
102 108 '36': 'ansicyan',
103 109 '37': 'ansigrey',
104 110 '01': 'ansibold',
105 111 }
106 112
107 113 # do ampersand first
108 114 txt = txt.replace('&', '&amp;')
109 115 html_escapes = {
110 116 '<': '&lt;',
111 117 '>': '&gt;',
112 118 "'": '&apos;',
113 119 '"': '&quot;',
114 120 '`': '&#96;',
115 121 }
116 122 for c, escape in html_escapes.iteritems():
117 123 txt = txt.replace(c, escape)
118 124
119 125 ansi_re = re.compile('\x1b' + r'\[([\dA-Fa-f;]*?)m')
120 126 m = ansi_re.search(txt)
121 127 opened = False
122 128 cmds = []
123 129 opener = ''
124 130 closer = ''
125 131 while m:
126 132 cmds = m.groups()[0].split(';')
127 133 closer = '</span>' if opened else ''
128 134 opened = len(cmds) > 1 or cmds[0] != '0'*len(cmds[0]);
129 135 classes = []
130 136 for cmd in cmds:
131 137 if cmd in ansi_colormap:
132 138 classes.append(ansi_colormap.get(cmd))
133 139
134 140 if classes:
135 141 opener = '<span class="%s">' % (' '.join(classes))
136 142 else:
137 143 opener = ''
138 144 txt = re.sub(ansi_re, closer + opener, txt, 1)
139 145
140 146 m = ansi_re.search(txt)
141 147
142 148 if opened:
143 149 txt += '</span>'
144 150 return txt
145 151
146 152
147 153 # Pandoc-dependent code
148 154
149 155 def markdown2latex(src):
150 156 """Convert a markdown string to LaTeX via pandoc.
151 157
152 158 This function will raise an error if pandoc is not installed.
153 159
154 160 Any error messages generated by pandoc are printed to stderr.
155 161
156 162 Parameters
157 163 ----------
158 164 src : string
159 165 Input string, assumed to be valid markdown.
160 166
161 167 Returns
162 168 -------
163 169 out : string
164 170 Output as returned by pandoc.
165 171 """
166 172 p = subprocess.Popen('pandoc -f markdown -t latex'.split(),
167 173 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
168 174 out, err = p.communicate(src.encode('utf-8'))
169 175 if err:
170 176 print(err, file=sys.stderr)
171 177 #print('*'*20+'\n', out, '\n'+'*'*20) # dbg
172 178 return unicode(out,'utf-8')
173 179
174 180
175 181 def markdown2rst(src):
176 182 """Convert a markdown string to LaTeX via pandoc.
177 183
178 184 This function will raise an error if pandoc is not installed.
179 185
180 186 Any error messages generated by pandoc are printed to stderr.
181 187
182 188 Parameters
183 189 ----------
184 190 src : string
185 191 Input string, assumed to be valid markdown.
186 192
187 193 Returns
188 194 -------
189 195 out : string
190 196 Output as returned by pandoc.
191 197 """
192 198 p = subprocess.Popen('pandoc -f markdown -t rst'.split(),
193 199 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
194 200 out, err = p.communicate(src.encode('utf-8'))
195 201 if err:
196 202 print(err, file=sys.stderr)
197 203 #print('*'*20+'\n', out, '\n'+'*'*20) # dbg
198 204 return unicode(out,'utf-8')
199 205
200 206
201 207 def rst_directive(directive, text=''):
202 208 """
203 209 Makes ReST directive block and indents any text passed to it.
204 210 """
205 211 out = [directive, '']
206 212 if text:
207 213 out.extend([indent(text), ''])
208 214 return out
209 215
210 216
211 217 def coalesce_streams(outputs):
212 218 """merge consecutive sequences of stream output into single stream
213 219
214 220 to prevent extra newlines inserted at flush calls
215 221
216 222 TODO: handle \r deletion
217 223 """
218 224 new_outputs = []
219 225 last = outputs[0]
220 226 new_outputs = [last]
221 227 for output in outputs[1:]:
222 228 if (output.output_type == 'stream' and
223 229 last.output_type == 'stream' and
224 230 last.stream == output.stream
225 231 ):
226 232 last.text += output.text
227 233 else:
228 234 new_outputs.append(output)
229 235
230 236 return new_outputs
231 237
232 238
233 239 def rst2simplehtml(infile):
234 240 """Convert a rst file to simplified html suitable for blogger.
235 241
236 242 This just runs rst2html with certain parameters to produce really simple
237 243 html and strips the document header, so the resulting file can be easily
238 244 pasted into a blogger edit window.
239 245 """
240 246
241 247 # This is the template for the rst2html call that produces the cleanest,
242 248 # simplest html I could find. This should help in making it easier to
243 249 # paste into the blogspot html window, though I'm still having problems
244 250 # with linebreaks there...
245 251 cmd_template = ("rst2html --link-stylesheet --no-xml-declaration "
246 252 "--no-generator --no-datestamp --no-source-link "
247 253 "--no-toc-backlinks --no-section-numbering "
248 254 "--strip-comments ")
249 255
250 256 cmd = "%s %s" % (cmd_template, infile)
251 257 proc = subprocess.Popen(cmd,
252 258 stdout=subprocess.PIPE,
253 259 stderr=subprocess.PIPE,
254 260 shell=True)
255 261 html, stderr = proc.communicate()
256 262 if stderr:
257 263 raise IOError(stderr)
258 264
259 265 # Make an iterator so breaking out holds state. Our implementation of
260 266 # searching for the html body below is basically a trivial little state
261 267 # machine, so we need this.
262 268 walker = iter(html.splitlines())
263 269
264 270 # Find start of main text, break out to then print until we find end /div.
265 271 # This may only work if there's a real title defined so we get a 'div class'
266 272 # tag, I haven't really tried.
267 273 for line in walker:
268 274 if line.startswith('<body>'):
269 275 break
270 276
271 277 newfname = os.path.splitext(infile)[0] + '.html'
272 278 with open(newfname, 'w') as f:
273 279 for line in walker:
274 280 if line.startswith('</body>'):
275 281 break
276 282 f.write(line)
277 283 f.write('\n')
278 284
279 285 return newfname
280 286
281 287 #-----------------------------------------------------------------------------
282 288 # Cell-level functions -- similar to IPython.nbformat.v3.rwbase functions
283 289 # but at cell level instead of whole notebook level
284 290 #-----------------------------------------------------------------------------
285 291
286 292 def writes_cell(cell, **kwargs):
287 293 kwargs['cls'] = BytesEncoder
288 294 kwargs['indent'] = 3
289 295 kwargs['sort_keys'] = True
290 296 kwargs['separators'] = (',',': ')
291 297 if kwargs.pop('split_lines', True):
292 298 cell = split_lines_cell(copy.deepcopy(cell))
293 299 return py3compat.str_to_unicode(json.dumps(cell, **kwargs), 'utf-8')
294 300
295 301
296 302 _multiline_outputs = ['text', 'html', 'svg', 'latex', 'javascript', 'json']
297 303
298 304
299 305 def split_lines_cell(cell):
300 306 """
301 307 Split lines within a cell as in
302 308 IPython.nbformat.v3.rwbase.split_lines
303 309
304 310 """
305 311 if cell.cell_type == 'code':
306 312 if 'input' in cell and isinstance(cell.input, basestring):
307 313 cell.input = (cell.input + '\n').splitlines()
308 314 for output in cell.outputs:
309 315 for key in _multiline_outputs:
310 316 item = output.get(key, None)
311 317 if isinstance(item, basestring):
312 318 output[key] = (item + '\n').splitlines()
313 319 else: # text, heading cell
314 320 for key in ['source', 'rendered']:
315 321 item = cell.get(key, None)
316 322 if isinstance(item, basestring):
317 323 cell[key] = (item + '\n').splitlines()
318 324 return cell
319 325
320 326
321 327 def cell_to_lines(cell):
322 328 '''
323 329 Write a cell to json, returning the split lines.
324 330 '''
325 331 split_lines_cell(cell)
326 332 s = writes_cell(cell).strip()
327 333 return s.split('\n')
General Comments 0
You need to be logged in to leave comments. Login now