Ticket #10637: tools_sws2rst_2.patch

File tools_sws2rst_2.patch, 21.9 KB (added by pang, 10 years ago)

replaces the first patch

  • new file sagenb/misc/comments2rst.py

    # HG changeset patch
    # User Pablo Angulo <pablo.angulo@uam.es>
    # Date 1295452728 -3600
    # Node ID 009e2c8923f6913be8dbefacbda66e48736617ef
    # Parent  eaa34d73e6ff6d1fd06449e223c1f7cc0bde3cdc
    [mq]: tools_for_sws2rst.patch
    
    diff -r eaa34d73e6ff -r 009e2c8923f6 sagenb/misc/comments2rst.py
    - +  
     1# -*- coding: utf-8 -*-
     2r"""
     3Convert html from text cells in the notebook into ReStructuredText
     4
     5This is called by sws2rst
     6
     7- Pablo Angulo Ardoy (2011-02-25): initial version
     8"""
     9#**************************************************
     10# Copyright (C) 2011 Pablo Angulo
     11#
     12# Distributed under the terms of the GPL License
     13#**************************************************
     14
     15
     16import re
     17import os
     18from BeautifulSoup import (ICantBelieveItsBeautifulSoup, Tag,
     19                           CData, Comment, Declaration, ProcessingInstruction)
     20
     21def preprocess_display_latex(text):
     22    '''replace $$some display latex$$ with <display>some display latex</display>
     23    before the soup is built.
     24
     25    Deals with the situation where <p></p> tags are mixed
     26    with $$, like $$<p>display_latex$$</p>, unless the mess is huge
     27    '''
     28    ls = []
     29    start_tag = True
     30    partes = text.split('$$')
     31    for c in partes[:-1]:
     32        if start_tag:
     33            ls.append(c)
     34            ls.append('<display>')
     35        else:
     36            c0, count = prune_tags(c)
     37            ls.append(c0)
     38            ls.append('</display>')
     39            if count == 1:
     40                ls.append('<p>')
     41            elif count == -1:
     42                ls.append('</p>')
     43            elif abs(count)>1:
     44                raise Exception, 'display latex was messed up with html code'
     45        start_tag = not start_tag
     46    ls.append(partes[-1])
     47    return ''.join(ls)
     48
     49def prune_tags(text):
     50    count = text.count('<p>') - text.count('</p>')
     51    return text.replace('<br/>','').replace('<br />','').replace('<p>','').replace('</p>',''), count
     52
     53escapable_chars = { '+' :r'\+',
     54                    '*' :r'\*',
     55                    '|' :r'\|',
     56                    '-' :r'\-'}
     57def escape_chars(text):
     58    for c,r in escapable_chars.iteritems():
     59        text = text.replace(c,r)
     60    return text
     61     
     62def replace_courier(soup):
     63    '''Lacking a better option, I use courier font to mark <code>
     64    within tinyMCE. And I want to turn that into real code tags.
     65
     66    Most users won't be needing this(?)
     67    '''
     68    for t in soup.findAll(lambda s:s.has_key('style') and 'courier' in s['style']):
     69        tag = Tag(soup, 'code')
     70        while t.contents:
     71            tag.append(t.contents[0])
     72        t.replaceWith(tag)
     73
     74#inline_latex is careful not to confuse escaped dollars
     75inline_latex = re.compile(r'([^\\])\$(.*?)([^\\])\$')
     76latex_beginning = re.compile(r'\$(.*?)([^\\])\$')
     77def replace_latex(soup):
     78    '''Replaces inline latex by :math:`code` and escapes
     79    some rst special chars like +, -, * and | outside of inline latex
     80
     81    does not escape chars inside display or pre tags
     82    '''
     83    for t in soup.findAll(text=re.compile('.+')):
     84        if latex_beginning.match(t):
     85            t.replaceWith(inline_latex.sub('\\1:math:`\\2\\3`',
     86                                           latex_beginning.sub(':math:`\\1\\2`',
     87                                                               unicode(t),
     88                                                               1)))       
     89        elif inline_latex.search(t):
     90            t.replaceWith(inline_latex.sub('\\1:math:`\\2\\3`',
     91                                           unicode(t)))
     92        elif not (t.fetchParents(name = 'display')
     93                  or t.fetchParents(name = 'pre')):
     94            t.replaceWith(escape_chars(t))
     95
     96class Soup2Rst(object):
     97    """builds the rst text from the Soup Tree
     98    """
     99    tags = {'h1':'header',
     100            'h2':'header',
     101            'h3':'header',
     102            'h4':'header',
     103            'p': 'inline_no_tag',
     104            '[document]': 'document',
     105            'br': 'br',
     106            'b':'strong',
     107            'strong':'strong',
     108            'em':'em',
     109            'pre':'pre',
     110            'code':'code',
     111            'display':'display',
     112            'span':'inline_no_tag',
     113            'ul':'ul',
     114            'ol':'ol',
     115            'li':'li',
     116            'a':'a',
     117            'table':'table',
     118#            'tr':'tr',
     119            'td':'inline_no_tag',
     120            'th':'inline_no_tag',
     121            'tt':'inline_no_tag',
     122            'div':'block_no_tag',
     123            'img':'img',
     124#            '':'',
     125            }
     126
     127    headers = {'h1':u'=',
     128               'h2':u':',
     129               'h3':u'~',
     130               'h4':u'-',
     131               }
     132   
     133    def __init__(self, images_dir):
     134        self.images_dir = images_dir
     135        self._nested_list = 0
     136        self._inside_ol   = False
     137        self._inside_code_tag = False
     138
     139    def visit(self, node):
     140        if isinstance(node, (CData, Comment, Declaration, ProcessingInstruction)):
     141            return ''
     142        elif hasattr(node, 'name'):
     143            try:
     144                visitor = getattr(self, 'visit_' + self.tags[node.name])
     145                return visitor(node)
     146            except (KeyError, AttributeError):
     147                print 'Warning: node not supported (or something else?) ' + node.name
     148                return unicode(node)
     149        else:
     150            #Assume plain string
     151            return unicode(node).replace('\n','')
     152
     153    def visit_document(self, node):
     154        return '\n'.join(self.visit(tag) for tag in node.contents)   
     155
     156    def get_plain_text(self, node):
     157        '''Gets all text, removing all tags'''
     158        if hasattr(node, 'contents'):
     159            t = ' '.join(self.get_plain_text(tag) for tag in node.contents)
     160        else:
     161            t = unicode(node)
     162        return t.replace('\n','')
     163       
     164    def visit_header(self, node):
     165        s = ' '.join(self.visit(tag) for tag in node.contents)
     166        spacer = self.headers[node.name]*len(s)
     167        return s.replace( '\n', '') +  '\n' + spacer
     168
     169    def visit_pre(self, node):
     170        return '::\n\n\t'+unicode(node)[5:-6].replace('<br />','\n').replace('<br></br>','\n').replace('\n','\n\t')
     171
     172    def visit_ul(self, node):
     173        self._nested_list += 1
     174        result = '\n'.join(self.visit(tag) for tag in node.contents)
     175        self._nested_list -= 1
     176        return result
     177
     178    def visit_ol(self, node):
     179        self._nested_list += 1
     180        self._inside_ol = True
     181        result = '\n'.join(self.visit(tag) for tag in node.contents)
     182        self._nested_list -= 1
     183        self._inside_ol = False
     184        return result
     185
     186    def visit_li(self, node):
     187        return (' '*self._nested_list
     188                + ('#. ' if self._inside_ol else '- ')
     189                +' '.join(self.visit(tag) for tag in node.contents))
     190
     191    def visit_display(self, node):
     192        return ('\n.. MATH::\n\n\t' +
     193                unicode(node)[9:-10].replace('<br></br>','\n').replace('\n','\n\t') +
     194                '\n\n')
     195
     196    def visit_img(self, node):
     197        return '.. image:: ' + os.path.join(self.images_dir, node['src']) + '\n\t:align: center\n'
     198
     199    def visit_table(self,node):
     200        rows = []
     201        for elt in node.contents:
     202            if not hasattr(elt,'name'):
     203                pass
     204            elif elt.name == 'thead':
     205                rows.extend(self.prepare_tr(row)
     206                            for row in elt
     207                            if hasattr(row,'name') and
     208                            row.name=='tr')
     209                rows.append([]) #this row represents a separator
     210            elif elt.name == 'tbody':
     211                rows.extend(self.prepare_tr(row)
     212                            for row in elt
     213                            if hasattr(row,'name') and
     214                            row.name=='tr')
     215            elif elt.name == 'tr':
     216                rows.append(self.prepare_tr(elt))
     217
     218        ncols = max(len(row) for row in rows)
     219        for row in rows:
     220            if len(row) < ncols:
     221                row.extend( ['']*(ncols - len(row)))
     222        cols_sizes = [max(len(td) for td in tds_in_col)
     223                      for tds_in_col in zip(*rows)]
     224        result = [' '.join('='*c for c in cols_sizes)]
     225       
     226        for row in rows:
     227            if any(td for td in row):
     228                result.append(' '.join(td+' '*(l - len(td))
     229                                       for l,td in zip(cols_sizes,row)))
     230            else:
     231                result.append(' '.join('-'*c for c in cols_sizes))
     232        result.append(' '.join('='*c for c in cols_sizes))
     233        return '\n'.join(result)
     234
     235    def prepare_tr(self, node):
     236        return [self.visit(tag) for tag in node.contents if tag!='\n']
     237       
     238    def visit_br(self, node):
     239        return '\n'
     240
     241    def visit_strong(self, node):
     242        if node.contents:
     243            content = ' '.join(self.visit(tag) for tag in node.contents).strip()
     244            if '``' in content or self._inside_code_tag:
     245                return content
     246            else:
     247                return '**' + content + '**'
     248        else:
     249            return ''
     250
     251    def visit_em(self,node):
     252        if node.contents:
     253            return '*' + ' '.join(self.visit(tag) for tag in node.contents).strip() + '*'
     254        else:
     255            return ''
     256
     257    def visit_code(self, node):
     258        if node.contents:
     259            self._inside_code_tag = True
     260            content = self.get_plain_text(node).strip()
     261            self._inside_code_tag = False
     262            return '``' + content + '``'
     263        else:
     264            return ''
     265
     266    def visit_inline_no_tag(self, node):
     267        return (' '.join(self.visit(tag)
     268                         for tag in node.contents)).strip() + '\n'
     269
     270    def visit_block_no_tag(self, node):
     271        return '\n'.join(self.visit(tag) for tag in node.contents)
     272
     273    def visit_a(self, node):
     274        return ('`' + ' '.join(self.visit(tag) for tag in node.contents) +
     275                ' <' + node['href'] + '>`_'
     276                )
     277
     278def html2rst(text, images_dir):
     279    '''Converts html, tipically generated by tinyMCE, into rst
     280    compatible with Sage documentation.
     281
     282    The main job is done by BeautifulSoup, which is much more
     283    robust than conventional parsers like HTMLParser, but also
     284    several details specific of this context are taken into
     285    account, so this code differs from generic approaches like
     286    those found on the web.
     287
     288    INPUT:
     289
     290    - ``text`` -- string -- a chunk of HTML text
     291
     292    - ``images_dir`` -- string -- folder where images are stored
     293
     294    OUTPUT:
     295
     296    - string -- rst text
     297
     298    EXAMPLES::
     299
     300        sage: from sagenb.misc.comments2rst import html2rst
     301        sage: html2rst('<p>Some text with <em>math</em>: $e^{\pi i}=-1$</p>', '')
     302        u'Some text with  *math* : :math:`e^{\\pi i}=-1`'
     303        sage: html2rst('<p>Text with <em>incorrect</p> nesting</em>.', '')       
     304        u'Text with  *incorrect*\n nesting\n.'
     305        sage: html2rst('<pre>Preformatted: \n    a+2\n</pre><p> Not preformatted: \n    a+2\n</p>', '')
     306        sage: html2rst('&aacute;ñ&nbsp;&ntildeá','')
     307        u'\xe1\xf1 \xf1\xe1'
     308        sage: html2rst('<p>some text</p><p>$$</p><p>3.183098861 \cdot 10^{-1}</p><p>$$</p>','')
     309        u'some text\n\n.. MATH::\n\n\t3.183098861 \\cdot 10^{-1}\n'
     310    '''
     311   
     312    #replace $$some display latex$$ with
     313    #<display>some display latex</display>
     314    text = preprocess_display_latex(text)
     315
     316    #eliminate nasty &nbsp;
     317    text = text.replace('&nbsp;',' ')
     318           
     319    #ICantBelieveItsBeautifulSoup is better than BeautifulSoup
     320    #for html that wasn't generated by humans (like tinyMCE)
     321    soup = ICantBelieveItsBeautifulSoup(text,
     322                       convertEntities=ICantBelieveItsBeautifulSoup.HTML_ENTITIES)   
     323
     324    #remove all comments
     325    comments = soup.findAll(text=lambda text:isinstance(text, Comment))
     326    for comment in comments:
     327        comment.extract()
     328
     329    replace_courier(soup)
     330    replace_latex(soup)
     331    v = Soup2Rst(images_dir)
     332    return v.visit(soup)
  • new file sagenb/misc/results2rst.py

    diff -r eaa34d73e6ff -r 009e2c8923f6 sagenb/misc/results2rst.py
    - +  
     1# -*- coding: utf-8 -*-
     2import re
     3IMAGES_DIR = 'images/'
     4
     5#We parse lines one by one but keep track of current scope
     6#similarly to worksheet2rst.py
     7#Results are split into different types. Some are discarded
     8class States(object):
     9    NORMAL = 0
     10    HTML = 1
     11    MATH = 2
     12    TRACEBACK = 3
     13
     14class LineTypes(object):
     15    PLAIN = 0
     16    IMAGE = 1
     17    LATEX = 2
     18    HTML  = 3
     19    TRACE = 4
     20
     21def results2rst(text, images_dir):
     22    '''Converts the result of evaluation of notebook cells
     23    into rst compatible with Sage documentation.
     24
     25    Several common patterns are identified, and treated
     26    accordingly. Some patterns are dropped, while others
     27    are not recognized.
     28
     29    Currently, latex and images are recognized and converted.
     30
     31    INPUT:
     32
     33    - ``text`` -- string -- a chunk of HTML text
     34
     35    - ``images_dir`` -- string -- folder where images are stored
     36
     37    OUTPUT:
     38
     39    - string -- rst text
     40
     41    EXAMPLES::
     42
     43        sage: from sagenb.misc.results2rst import results2rst
     44        sage: s="<html><font color='black'><img src='cell://sage0.png'></font></html>"
     45        sage: results2rst(s,'')
     46        '\n.. image:: sage0.png\n\t:align: center\n'
     47        sage: results2rst("4",'')
     48        '\t4'
     49        sage: s=r'<html><div class="math">\newcommand{\Bold}[1]{\mathbf{#1}}\frac{3}{2}</div></html>'
     50        sage: results2rst(s,'')                                       
     51        '\n.. MATH::\n\n\t\\frac{3}{2}\n'
     52    '''
     53   
     54    ##Order matters, place more restrictive regex's before more general ones
     55    ##If no regex matches, line will be discarded
     56    ##a self transition is needes to produce any output
     57    transitions = {
     58        States.NORMAL:[
     59            #IMAGE
     60                 (re.compile(r"^\<html\>\<font color='black'\>"
     61                             r"\<img src='cell\://(.*?)'\>"
     62                             r"\</font\>\</html\>"),
     63                  "\n.. image:: " + images_dir + "\\1\n\t:align: center\n",
     64                  LineTypes.IMAGE,
     65                  States.NORMAL),
     66            #SELF-CONTAINED MATH
     67                 (re.compile(r"^\<html\>\<div class=\"math\"\>"
     68                             r"\\newcommand\{\\Bold\}\[1\]\{\\mathbf\{\#1\}\}"
     69                             r"(.*?)\</div\>\</html\>$"),
     70                  "\n.. MATH::\n\n\t\\1\n",
     71                  LineTypes.LATEX,
     72                  States.NORMAL),
     73            #SELF-CONTAINED MATH - BIS
     74                 (re.compile(r"^\<html\>\<div class=\"math\"\>"
     75                             r"(.*?)\</div\>\</html\>$"),
     76                  "\n.. MATH::\n\n\t\\1",
     77                  LineTypes.LATEX,
     78                  States.NORMAL),
     79            #START Traceback
     80                 (re.compile(r"^(Traceback.*)"),
     81                  "\tTraceback (most recent call last):",
     82                  LineTypes.TRACE,
     83                  States.TRACEBACK),
     84            #START MATH
     85                 (re.compile(r"^\<html\>\<div class=\"math\"\>"
     86                             r"\\newcommand\{\\Bold\}\[1\]\{\\mathbf\{\#1\}\}(.*?)"),
     87                  "\n.. MATH::\n\n\t\\1",
     88                  LineTypes.LATEX,
     89                  States.MATH),
     90            #SELF-CONTAINED HTML
     91                 (re.compile(r"^\<html\>.*</html\>$"),
     92                  "\t<html>...</html>",
     93                  LineTypes.HTML,
     94                  States.NORMAL),       
     95            #START HTML
     96                 (re.compile(r"^\<html\>.*"),
     97                  "\t<html>...</html>",
     98                  LineTypes.HTML,
     99                  States.HTML),       
     100            #CONTINUE NORMAL
     101                 (re.compile("(.*)"),
     102                  "\t\\1",
     103                  LineTypes.PLAIN,
     104                  States.NORMAL),               
     105            ],
     106        States.MATH:[
     107             #END MATH
     108                 (re.compile(r"(.*?)\</div\>\</html\>$"),
     109                  "\t\\1",
     110                  LineTypes.LATEX,
     111                  States.NORMAL),
     112             #CONTINUE MATH
     113                 (re.compile("(.*)"),
     114                  "\t\\1",
     115                  LineTypes.LATEX,
     116                  States.MATH),       
     117            ],
     118        States.TRACEBACK:[
     119             #END Traceback
     120                 (re.compile(r"^(\S.*)"),
     121                  "\t...\n\t\\1",
     122                  LineTypes.TRACE,
     123                  States.NORMAL),
     124            ],
     125        States.HTML:[
     126             #END HTML
     127                 (re.compile(r".*</html\>$"),
     128                  "",
     129                  LineTypes.HTML,
     130                  States.NORMAL),
     131            ],
     132        }
     133    result_plain = []
     134    result_show = []
     135    state = States.NORMAL
     136    for line in text.splitlines():
     137        for regex, replacement, line_type, new_state in transitions[state]:
     138            if regex.match(line):
     139                result = result_plain if line_type in (LineTypes.PLAIN, LineTypes.HTML)\
     140                         else result_show
     141                result.append( regex.sub(replacement, line))
     142                state = new_state
     143                break
     144    result_plain.extend(result_show)
     145    return '\n'.join(result_plain)
  • new file sagenb/misc/worksheet2rst.py

    diff -r eaa34d73e6ff -r 009e2c8923f6 sagenb/misc/worksheet2rst.py
    - +  
     1#!/usr/bin/python
     2# -*- coding: utf-8 -*-
     3r"""
     4Convert worksheet.html files into ReStructuredText documents
     5
     6This is called by sws2rst
     7
     8- Pablo Angulo Ardoy (2011-02-25): initial version
     9"""
     10#**************************************************
     11# Copyright (C) 2011 Pablo Angulo
     12#
     13# Distributed under the terms of the GPL License
     14#**************************************************
     15
     16
     17import sys
     18import os
     19import re
     20from comments2rst import html2rst
     21from results2rst import results2rst
     22import codecs
     23
     24#We parse lines one by one but keep track of current scope
     25#comments
     26#{{{id=..|
     27#code
     28#///
     29#results
     30#}}}
     31#RESULT_TO_BE_DROPPED corresponds to a results section whose
     32#code was empty, and will be discarded
     33class States(object):
     34    COMMENT = 0
     35    CODE = 1
     36    RESULT = 2
     37    RESULT_TO_BE_DROPPED = 3
     38
     39# REs for splitting comments, code and results
     40START_CELL_RE = re.compile('^\{\{\{id=(\d*)\|')
     41END_CODE_RE   = re.compile('^\/\/\/')
     42END_CELL_RE   = re.compile('^\}\}\}')
     43
     44#When to switch State, and which State to
     45transitions = {
     46    States.COMMENT:(
     47        START_CELL_RE,
     48        States.CODE
     49        ),
     50    States.CODE:(
     51        END_CODE_RE,
     52        States.RESULT),
     53    States.RESULT:(
     54        END_CELL_RE,
     55        States.COMMENT),
     56    States.RESULT_TO_BE_DROPPED:(
     57        END_CELL_RE,
     58        States.COMMENT)
     59    }
     60
     61#Comments will be translated into rst by html2rst, and so on
     62comment_parser = html2rst
     63
     64def code_parser(s):
     65    """
     66   
     67    Arguments:
     68    - `s`:sage code, may or may not start with "sage:"
     69    """
     70    lines = ['::\n']
     71    for s in s.splitlines():
     72        l = s[6:] if s[:6]=='sage: ' else s
     73        if not l: continue
     74        prefix = '\t...   ' if l[0] == ' ' else '\tsage: '
     75        lines.append(prefix + l)
     76    return '\n'.join(lines)
     77
     78result_parser = results2rst
     79parsers = [comment_parser, code_parser, result_parser]
     80
     81def worksheet2rst(s, images_dir=''):
     82    '''Parses a string, tipically the content of the file
     83    worksheet.html inside a sws file, and converts it into
     84    rst compatible with Sage documentation.
     85
     86    INPUT:
     87
     88    - ``s`` -- string -- text, tipically the content of
     89                               worksheet.html
     90
     91    - ``images_dir`` -- string -- folder where images are stored
     92
     93    OUTPUT:
     94
     95    - string -- rst text
     96
     97    EXAMPLES::
     98
     99        sage: from sagenb.misc.worksheet2rst import worksheet2rst
     100        sage: worksheet2rst('<p>some text</p>\n{{{id=1|\nprint 2+2\n///\n4\n}}}')     
     101        u'.. -*- coding: utf-8 -*-\n\nsome text\n\n::\n\n\tsage: print 2+2\n\t4\n'
     102        sage: s = '{{{id=2|\nshow(f)\n///\n<html><div class="math">\\sqrt{x}</div></html>\n}}}\n'
     103        sage: worksheet2rst(s)
     104        u'.. -*- coding: utf-8 -*-\n\n\n::\n\n\tsage: show(f)\n\n.. MATH::\n\n\t\\sqrt{x}\n'       
     105    '''
     106
     107    state = States.COMMENT
     108    result = ['.. -*- coding: utf-8 -*-\n']
     109    ls = []
     110    last = 0
     111    for line in s.splitlines():
     112        regex, next_state= transitions[state]
     113        m = regex.match(line)
     114        if m:
     115            if state == States.COMMENT:
     116                last_cell_id = m.group(1)
     117                img_path = images_dir + os.path.sep
     118                result.append(parsers[state](u'\n'.join(ls), img_path))
     119            elif state == States.RESULT:
     120                img_path = os.path.join(images_dir, 'cell_%s_'%last_cell_id)
     121                result.append(parsers[state](u'\n'.join(ls),
     122                                             img_path))
     123            elif state == States.CODE:
     124                if ls and any(ls):
     125                    result.append(parsers[state](u'\n'.join(ls)))
     126                else:
     127                    next_state = States.RESULT_TO_BE_DROPPED
     128            ls = []
     129            state = next_state
     130        else:
     131            ls.append(line)
     132    if state == States.COMMENT:
     133        img_path = images_dir + os.path.sep
     134        result.append(parsers[state](u'\n'.join(ls), img_path))
     135    elif state == States.RESULT:
     136        img_path = os.path.join(images_dir, 'cell_%s_'%last_cell_id)
     137        result.append(parsers[state](u'\n'.join(ls),
     138                                     img_path))
     139    elif state == States.CODE:
     140        result.append(parsers[state](u'\n'.join(ls)))
     141
     142    return u'\n'.join(result)
     143
     144if __name__=='__main__':
     145    if len(sys.argv)>1:       
     146        fichero = codecs.open(sys.argv[1], mode='r', encoding='utf-8')
     147        text = fichero.read()
     148        fichero.close()
     149    else:
     150        text = sys.stdin.read()
     151
     152    print worksheet2rst(text).encode('utf-8')
     153