#! /usr/bin/env python -- # -*- python -*-
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
'''Structured Text Manipulation
Parse a structured text string into a form that can be used with
structured formats, like html.
Structured text is text that uses indentation and simple
symbology to indicate the structure of a document.
A structured string consists of a sequence of paragraphs separated by
one or more blank lines. Each paragraph has a level which is defined
as the minimum indentation of the paragraph. A paragraph is a
sub-paragraph of another paragraph if the other paragraph is the last
preceding paragraph that has a lower level.
Special symbology is used to indicate special constructs:
- A single-line paragraph whose immediately succeeding paragraphs are lower
level is treated as a header.
- A paragraph that begins with a '-', '*', or 'o' is treated as an
unordered list (bullet) element.
- A paragraph that begins with a sequence of digits followed by a
white-space character is treated as an ordered list element.
- A paragraph that begins with a sequence of sequences, where each
sequence is a sequence of digits or a sequence of letters followed
by a period, is treated as an ordered list element.
- A paragraph with a first line that contains some text, followed by
some white-space and '--' is treated as
a descriptive list element. The leading text is treated as the
element title.
- Sub-paragraphs of a paragraph that ends in the word 'example' or the
word 'examples', or '::' is treated as example code and is output as is.
- Text enclosed single quotes (with white-space to the left of the
first quote and whitespace or punctuation to the right of the second quote)
is treated as example code.
- Text surrounded by '*' characters (with white-space to the left of the
first '*' and whitespace or punctuation to the right of the second '*')
is emphasized.
- Text surrounded by '**' characters (with white-space to the left of the
first '**' and whitespace or punctuation to the right of the second '**')
is made strong.
- Text surrounded by '_' underscore characters (with whitespace to the left
and whitespace or punctuation to the right) is made underlined.
- Text encloded by double quotes followed by a colon, a URL, and concluded
by punctuation plus white space, *or* just white space, is treated as a
hyper link. For example:
"Zope":http://www.zope.org/ is ...
Is interpreted as '<a href="http://www.zope.org/">Zope</a> is ....'
Note: This works for relative as well as absolute URLs.
- Text enclosed by double quotes followed by a comma, one or more spaces,
an absolute URL and concluded by punctuation plus white space, or just
white space, is treated as a hyper link. For example:
"mail me", mailto:amos@digicool.com.
Is interpreted as '<a href="mailto:amos@digicool.com">mail me</a>.'
- Text enclosed in brackets which consists only of letters, digits,
underscores and dashes is treated as hyper links within the document.
For example:
As demonstrated by Smith [12] this technique is quite effective.
Is interpreted as '... by Smith <a href="#12">[12]</a> this ...'. Together
with the next rule this allows easy coding of references or end notes.
- Text enclosed in brackets which is preceded by the start of a line, two
periods and a space is treated as a named link. For example:
.. [12] "Effective Techniques" Smith, Joe ...
Is interpreted as '<a name="12">[12]</a> "Effective Techniques" ...'.
Together with the previous rule this allows easy coding of references or
end notes.
- A paragraph that has blocks of text enclosed in '||' is treated as a
table. The text blocks correspond to table cells and table rows are
denoted by newlines. By default the cells are center aligned. A cell
can span more than one column by preceding a block of text with an
equivalent number of cell separators '||'. Newlines and '|' cannot
be a part of the cell text. For example:
|||| **Ingredients** ||
|| *Name* || *Amount* ||
||Spam||10||
||Eggs||3||
is interpreted as::
<TABLE BORDER=1 CELLPADDING=2>
<TR>
<TD ALIGN=CENTER COLSPAN=2> <strong>Ingredients</strong> </TD>
</TR>
<TR>
<TD ALIGN=CENTER COLSPAN=1> <em>Name</em> </TD>
<TD ALIGN=CENTER COLSPAN=1> <em>Amount</em> </TD>
</TR>
<TR>
<TD ALIGN=CENTER COLSPAN=1>Spam</TD>
<TD ALIGN=CENTER COLSPAN=1>10</TD>
</TR>
<TR>
<TD ALIGN=CENTER COLSPAN=1>Eggs</TD>
<TD ALIGN=CENTER COLSPAN=1>3</TD>
</TR>
</TABLE>
'''
import ts_regex
import regex
from ts_regex import gsub
import string,re
def untabify(aString,
indent_tab=ts_regex.compile('\(\n\|^\)\( *\)\t').search_group,
):
'''\
Convert indentation tabs to spaces.
'''
result=''
rest=aString
while 1:
ts_results = indent_tab(rest, (1,2))
if ts_results:
start, grps = ts_results
lnl=len(grps[0])
indent=len(grps[1])
result=result+rest[:start]
rest="\n%s%s" % (' ' * ((indent/8+1)*8),
rest[start+indent+1+lnl:])
else:
return result+rest
def indent(aString, indent=2):
"""Indent a string the given number of spaces"""
r=untabify(aString).split('\n')
if not r: return ''
if not r[-1]: del r[-1]
tab=' '*indent
return "%s%s\n" % (tab,('\n'+tab).join(r))
def reindent(aString, indent=2, already_untabified=0):
"reindent a block of text, so that the minimum indent is as given"
if not already_untabified: aString=untabify(aString)
l=indent_level(aString)[0]
if indent==l: return aString
r=[]
append=r.append
if indent > l:
tab=' ' * (indent-l)
for s in aString.split('\n'): append(tab+s)
else:
l=l-indent
for s in aString.split('\n'): append(s[l:])
return '\n'.join(r)
def indent_level(aString,
indent_space=ts_regex.compile('\n\( *\)').search_group,
):
'''\
Find the minimum indentation for a string, not counting blank lines.
'''
start=0
text='\n'+aString
indent=l=len(text)
while 1:
ts_results = indent_space(text, (1,2), start)
if ts_results:
start, grps = ts_results
i=len(grps[0])
start=start+i+1
if start < l and text[start] != '\n': # Skip blank lines
if not i: return (0,aString)
if i < indent: indent = i
else:
return (indent,aString)
def paragraphs(list,start):
l=len(list)
level=list[start][0]
i=start+1
while i < l and list[i][0] > level: i=i+1
return i-1-start
def structure(list):
if not list: return []
i=0
l=len(list)
r=[]
while i < l:
sublen=paragraphs(list,i)
i=i+1
r.append((list[i-1][1],structure(list[i:i+sublen])))
i=i+sublen
return r
class Table:
CELL=' <TD ALIGN=CENTER COLSPAN=%i>%s</TD>\n'
ROW=' <TR>\n%s </TR>\n'
TABLE='\n<TABLE BORDER=1 CELLPADDING=2>\n%s</TABLE>'
def create(self,aPar,
td_reg=re.compile(r'[ \t\n]*\|\|([^\0|]*)')
):
'''parses a table and returns nested list representing the
table'''
self.table=[]
text=filter(None,aPar.split('\n'))
for line in text:
row=[]
while 1:
mo = td_reg.match(line)
if not mo: return 0
pos = mo.end(1)
row.append(mo.group(1))
if pos==len(line):break
line=line[pos:]
self.table.append(row)
return 1
def html(self):
'''Creates an HTML representation of table'''
htmltable=[]
for row in self.table:
htmlrow=[]
colspan=1
for cell in row:
if cell=='':
colspan=colspan+1
continue
else:
htmlrow.append(self.CELL%(colspan,cell))
colspan=1
htmltable.append(self.ROW % ''.join(htmlrow))
return self.TABLE % ''.join(htmltable)
table=Table()
class StructuredText:
"""Model text as structured collection of paragraphs.
Structure is implied by the indentation level.
This class is intended as a base classes that do actual text
output formatting.
"""
def __init__(self, aStructuredString, level=0,
paragraph_divider=regex.compile('\(\r?\n *\)+\r?\n'),
):
'''Convert a structured text string into a structured text object.
Aguments:
aStructuredString -- The string to be parsed.
level -- The level of top level headings to be created.
'''
pat = ' \"([%s0-9-_,./?=@~&]*)\":' % string.letters+ \
'([-:%s0-9_,./?=@#~&]*?)' % string.letters + \
'([.:?;] )'
p_reg = re.compile(pat,re.M)
aStructuredString = p_reg.sub(r'<a href="\2">\1</a>\3 ' , aStructuredString)
pat = ' \"([%s0-9-_,./?=@~&]*)\", ' % string.letters+ \
'([-:%s0-9_,./?=@#~&]*?)' % string.letters + \
'([.:?;] )'
p_reg = re.compile(pat,re.M)
aStructuredString = p_reg.sub(r'<a href="\2">\1</a>\3 ' , aStructuredString)
protoless = aStructuredString.find('<a href=":')
if protoless != -1:
aStructuredString = re.sub('<a href=":', '<a href="',
aStructuredString)
self.level=level
paragraphs=ts_regex.split(untabify(aStructuredString),
paragraph_divider)
paragraphs=map(indent_level,paragraphs)
self.structure=structure(paragraphs)
def __str__(self):
return str(self.structure)
ctag_prefix=r'([\x00- \\(]|^)'
ctag_suffix=r'([\x00- ,.:;!?\\)]|$)'
ctag_middle=r'[%s]([^\x00- %s][^%s]*[^\x00- %s]|[^%s])[%s]'
ctag_middl2=r'[%s][%s]([^\x00- %s][^%s]*[^\x00- %s]|[^%s])[%s][%s]'
def ctag(s,
em=re.compile(
ctag_prefix+(ctag_middle % (("*",)*6) )+ctag_suffix),
strong=re.compile(
ctag_prefix+(ctag_middl2 % (("*",)*8))+ctag_suffix),
under=re.compile(
ctag_prefix+(ctag_middle % (("_",)*6) )+ctag_suffix),
code=re.compile(
ctag_prefix+(ctag_middle % (("\'",)*6))+ctag_suffix),
):
if s is None: s=''
s=strong.sub(r'\1<strong>\2</strong>\3',s)
s=under.sub( r'\1<u>\2</u>\3',s)
s=code.sub( r'\1<code>\2</code>\3',s)
s=em.sub( r'\1<em>\2</em>\3',s)
return s
class HTML(StructuredText):
'''\
An HTML structured text formatter.
'''\
def __str__(self,
extra_dl=re.compile("</dl>\n<dl>"),
extra_ul=re.compile("</ul>\n<ul>"),
extra_ol=re.compile("</ol>\n<ol>"),
):
'''\
Return an HTML string representation of the structured text data.
'''
s=self._str(self.structure,self.level)
s=extra_dl.sub('\n',s)
s=extra_ul.sub('\n',s)
s=extra_ol.sub('\n',s)
return s
def ul(self, before, p, after):
if p: p="<p>%s</p>" % ctag(p).strip()
return ('%s<ul><li>%s\n%s\n</li></ul>\n'
% (before,p,after))
def ol(self, before, p, after):
if p: p="<p>%s</p>" % ctag(p).strip()
return ('%s<ol><li>%s\n%s\n</li></ol>\n'
% (before,p,after))
def dl(self, before, t, d, after):
return ('%s<dl><dt>%s</dt><dd><p>%s</p>\n%s\n</dd></dl>\n'
% (before,ctag(t),ctag(d),after))
def head(self, before, t, level, d):
if level > 0 and level < 6:
return ('%s<h%d>%s</h%d>\n%s\n'
% (before,level,ctag(t).strip(),level,d))
t="<p><strong>%s</strong></p>" % ctag(t).strip()
return ('%s<dl><dt>%s\n</dt><dd>%s\n</dd></dl>\n'
% (before,t,d))
def normal(self,before,p,after):
return '%s<p>%s</p>\n%s\n' % (before,ctag(p),after)
def pre(self,structure,tagged=0):
if not structure: return ''
if tagged:
r=''
else:
r='<PRE>\n'
for s in structure:
r="%s%s\n\n%s" % (r,html_quote(s[0]),self.pre(s[1],1))
if not tagged: r=r+'</PRE>\n'
return r
def table(self,before,table,after):
return '%s<p>%s</p>\n%s\n' % (before,ctag(table),after)
def _str(self,structure,level,
# Static
bullet=ts_regex.compile('[ \t\n]*[o*-][ \t\n]+\([^\0]*\)'
).match_group,
example=ts_regex.compile('[\0- ]examples?:[\0- ]*$'
).search,
dl=ts_regex.compile('\([^\n]+\)[ \t]+--[ \t\n]+\([^\0]*\)'
).match_group,
nl=ts_regex.compile('\n').search,
ol=ts_regex.compile(
'[ \t]*\(\([0-9]+\|[%s]+\)[.)]\)+[ \t\n]+\([^\0]*\|$\)' % string.letters
).match_group,
olp=ts_regex.compile('[ \t]*([0-9]+)[ \t\n]+\([^\0]*\|$\)'
).match_group,
):
r=''
for s in structure:
ts_results = bullet(s[0], (1,))
if ts_results:
p = ts_results[1]
if s[0][-2:]=='::' and s[1]: ps=self.pre(s[1])
else: ps=self._str(s[1],level)
r=self.ul(r,p,ps)
continue
ts_results = ol(s[0], (3,))
if ts_results:
p = ts_results[1]
if s[0][-2:]=='::' and s[1]: ps=self.pre(s[1])
else: ps=self._str(s[1],level)
r=self.ol(r,p,ps)
continue
ts_results = olp(s[0], (1,))
if ts_results:
p = ts_results[1]
if s[0][-2:]=='::' and s[1]: ps=self.pre(s[1])
else: ps=self._str(s[1],level)
r=self.ol(r,p,ps)
continue
ts_results = dl(s[0], (1,2))
if ts_results:
t,d = ts_results[1]
r=self.dl(r,t,d,self._str(s[1],level))
continue
if example(s[0]) >= 0 and s[1]:
# Introduce an example, using pre tags:
r=self.normal(r,s[0],self.pre(s[1]))
continue
if s[0][-2:]=='::' and s[1]:
# Introduce an example, using pre tags:
r=self.normal(r,s[0][:-1],self.pre(s[1]))
continue
if table.create(s[0]):
## table support.
r=self.table(r,table.html(),self._str(s[1],level))
continue
else:
if nl(s[0]) < 0 and s[1] and s[0][-1:] != ':':
# Treat as a heading
t=s[0]
r=self.head(r,t,level,
self._str(s[1],level and level+1))
else:
r=self.normal(r,s[0],self._str(s[1],level))
return r
def html_quote(v,
character_entities=(
(re.compile('&'), '&'),
(re.compile("<"), '<' ),
(re.compile(">"), '>' ),
(re.compile('"'), '"')
)): #"
text=str(v)
for re,name in character_entities:
text=re.sub(name,text)
return text
def html_with_references(text, level=1):
text = re.sub(
r'[\0\n]\.\. \[([0-9_%s-]+)\]' % string.letters,
r'\n <a name="\1">[\1]</a>',
text)
text = re.sub(
r'([\x00- ,])\[(?P<ref>[0-9_%s-]+)\]([\x00- ,.:])' % string.letters,
r'\1<a href="#\2">[\2]</a>\3',
text)
text = re.sub(
r'([\0- ,])\[([^]]+)\.html\]([\0- ,.:])',
r'\1<a href="\2.html">[\2]</a>\3',
text)
return HTML(text,level=level)
def main():
import sys, getopt
opts,args=getopt.getopt(sys.argv[1:],'twl')
if args:
[infile]=args
s=open(infile,'r').read()
else:
s=sys.stdin.read()
if opts:
if filter(lambda o: o[0]=='-w', opts):
print 'Content-Type: text/html\n'
if filter(lambda o: o[0]=='-l', opts):
import locale
locale.setlocale(locale.LC_ALL,"")
if s[:2]=='#!':
s=re.sub('^#![^\n]+','',s)
mo = re.compile('([\0-\n]*\n)').match(s)
if mo is not None:
s = s[len(mo.group(0)) :]
s=str(html_with_references(s))
if s[:4]=='<h1>':
t=s[4: s.find('</h1>')]
s='''<html><head><title>%s</title>
</head><body>
%s
</body></html>
''' % (t,s)
print s
else:
print html_with_references(s)
if __name__=="__main__": main()
|