plugin syntax dc_wiki: added XHTML => dc_wiki converter, plus some bug fixes:

- escape_chars regex has been fixed (an \ was missing for backslash escaping)
     - escape_chars is now splited in consts so XHTML => dc_wiki escape less characters, for better readability
     - fixed bad length check in DCWikiParser.parser_link
    dotclear wiki syntax should now be usable
This commit is contained in:
Goffi 2016-01-18 17:04:29 +01:00
commit 76234954fe

View file

@ -23,8 +23,10 @@ from sat.core.i18n import _
from sat.core.log import getLogger
log = getLogger(__name__)
from sat.core.constants import Const as C
from sat.core import exceptions
from twisted.words.xish import domish
from sat.tools import xml_tools
import copy
import re
SYNTAX_NAME = "dc_wiki"
@ -42,8 +44,15 @@ PLUGIN_INFO = {
NOTE_TPL = u'[{}]' # Note template
NOTE_A_REV_TPL = u'rev_note_{}'
NOTE_A_TPL = u'note_{}'
ESCAPE_CHARS_BASE = r"(?P<escape_char>[][{}%|\\/*#@{{}}~$-])"
ESCAPE_CHARS_EXTRA = r"!?_+'()" # These chars are not escaped in XHTML => dc_wiki conversion,
# but are used in the other direction
ESCAPE_CHARS = ESCAPE_CHARS_BASE.format('')
FLAG_UL = 'ul' # must be the name of the element
FLAG_OL = 'ol'
ELT_WITH_STYLE = ('img', 'div') # elements where a style attribute is expected
wiki = [r"\\(?P<escape_char>[][!_+%'|\/*#@{}~?$()-])",
wiki = [r'\\' + ESCAPE_CHARS_BASE.format(ESCAPE_CHARS_EXTRA),
r"^!!!!!(?P<h1_title>.+?)$",
r"^!!!!(?P<h2_title>.+?)$",
r"^!!!(?P<h3_title>.+?)$",
@ -175,7 +184,7 @@ class DCWikiParser(object):
url_data = string.split(u'|')
a_elt = parent.addElement('a')
length = len(url_data)
if length == 0:
if length == 1:
url = url_data[0]
a_elt['href'] = url
a_elt.addContent(url)
@ -289,12 +298,345 @@ class DCWikiParser(object):
return div_elt
class XHTMLParser(object):
def __init__(self):
self.flags = None
self.toto = 0
self.footnotes = None # will hold a map from url to buffer id
for i in xrange(1,6):
setattr(self,
'parser_h{}'.format(i),
lambda elt, buf, level=i: self.parserHeading(elt, buf, level)
)
def parser_a(self, elt, buf):
try:
url = elt['href']
except KeyError:
# probably an anchor
try:
id_ = elt['id']
if not id_:
# we don't want empty values
raise KeyError
except KeyError:
self.parserGeneric(elt, buf)
else:
buf.append(u'~~{}~~'.format(id_))
return
link_data = [url]
name = unicode(elt)
if name != url:
link_data.insert(0, name)
lang = elt.getAttribute('lang')
title = elt.getAttribute('title')
if lang is not None:
link_data.append(lang)
elif title is not None:
link_data.appand(u'')
if title is not None:
link_data.append(title)
buf.append(u'[')
buf.append(u'|'.join(link_data))
buf.append(u']')
def parser_acronym(self, elt, buf):
try:
title = elt['title']
except KeyError:
log.debug(u"Acronyme without title, using generic parser")
self.parserGeneric(elt, buf)
return
buf.append(u'??{}|{}??'.format(unicode(elt), title))
def parser_blockquote(self, elt, buf):
# we remove wrapping <p> to avoid empty line with "> "
children = list([child for child in elt.children if unicode(child).strip() not in ('', '\n')])
if len(children) == 1 and children[0].name == 'p':
elt = children[0]
tmp_buf = []
self.parseChildren(elt, tmp_buf)
blockquote = u'> ' + u'\n> '.join(u''.join(tmp_buf).split('\n'))
buf.append(blockquote)
def parser_br(self, elt, buf):
buf.append(u'%%%')
def parser_code(self, elt, buf):
buf.append(u'@@')
self.parseChildren(elt, buf)
buf.append(u'@@')
def parser_del(self, elt, buf):
buf.append(u'--')
self.parseChildren(elt, buf)
buf.append(u'--')
def parser_div(self, elt, buf):
if elt.getAttribute('class') == 'footnotes':
self.parserFootnote(elt, buf)
else:
self.parseChildren(elt, buf, block=True)
def parser_em(self, elt, buf):
buf.append(u"''")
self.parseChildren(elt, buf)
buf.append(u"''")
def parser_h6(self, elt, buf):
# XXX: <h6/> heading is not managed by wiki syntax
# so we handle it with a <h5/>
elt = copy.copy(elt) # we don't want to change to original element
elt.name = 'h5'
self._parse(elt, buf)
def parser_hr(self, elt, buf):
buf.append(u'\n----\n')
def parser_img(self, elt, buf):
try:
url = elt['src']
except KeyError:
log.warning(u"Ignoring <img/> without src")
return
image_data=[url]
alt = elt.getAttribute('alt')
style = elt.getAttribute('style', '')
desc = elt.getAttribute('longdesc')
if '0 1em 1em 0' in style:
position = 'L'
elif '0 0 1em 1em' in style:
position = 'R'
elif 'auto' in style:
position = 'C'
else:
position = None
if alt:
image_data.append(alt)
elif position or desc:
image_data.append(u'')
if position:
image_data.append(position)
elif desc:
image_data.append(u'')
if desc:
image_data.append(desc)
buf.append(u'((')
buf.append(u'|'.join(image_data))
buf.append(u'))')
def parser_ins(self, elt, buf):
buf.append(u'++')
self.parseChildren(elt, buf)
buf.append(u'++')
def parser_li(self, elt, buf):
flag = None
current_flag = None
bullets = []
for flag in reversed(self.flags):
if flag in (FLAG_UL, FLAG_OL):
if current_flag is None:
current_flag = flag
if flag == current_flag:
bullets.append(u'*' if flag == FLAG_UL else u'#')
else:
break
if flag != current_flag and buf[-1] == u' ':
# this trick is to avoid a space when we switch
# from (un)ordered to the other type on the same row
# e.g. *# unorder + ordered item
del buf[-1]
buf.extend(bullets)
buf.append(u' ')
self.parseChildren(elt, buf)
buf.append(u'\n')
def parser_ol(self, elt, buf):
self.parserList(elt, buf, FLAG_OL)
def parser_p(self, elt, buf):
self.parseChildren(elt, buf)
buf.append(u'\n\n')
def parser_pre(self, elt, buf):
pre = u''.join([child.toXml() if domish.IElement.providedBy(child) else unicode(child) for child in elt.children])
pre = u' ' + u'\n '.join(pre.split('\n'))
buf.append(pre)
def parser_q(self, elt, buf):
quote_data=[unicode(elt)]
lang = elt.getAttribute('lang')
cite = elt.getAttribute('url')
if lang:
quote_data.append(lang)
elif cite:
quote_data.append(u'')
if cite:
quote_data.append(cite)
buf.append(u'{{')
buf.append(u'|'.join(quote_data))
buf.append(u'}}')
def parser_span(self, elt, buf):
self.parseChildren(elt, buf, block=True)
def parser_strong(self, elt, buf):
buf.append(u'__')
self.parseChildren(elt, buf)
buf.append(u'__')
def parser_sup(self, elt, buf):
# sup is mainly used for footnotes, so we check if we have an anchor inside
children = list([child for child in elt.children if unicode(child).strip() not in ('', '\n')])
if (len(children) == 1 and domish.IElement.providedBy(children[0])
and children[0].name == 'a' and '#' in children[0].getAttribute('href', '')):
url = children[0]['href']
note_id = url[url.find('#')+1:]
if not note_id:
log.warning("bad link found in footnote")
self.parserGeneric(elt, buf)
return
# this looks like a footnote
buf.append(u'$$')
buf.append(u' ') # placeholder
self.footnotes[note_id] = len(buf) - 1
buf.append(u'$$')
else:
self.parserGeneric(elt, buf)
def parser_ul(self, elt, buf):
self.parserList(elt, buf, FLAG_UL)
def parserList(self, elt, buf, type_):
self.flags.append(type_)
self.parseChildren(elt, buf, block=True)
idx = 0
for flag in reversed(self.flags):
idx -= 1
if flag == type_:
del self.flags[idx]
break
if idx == 0:
raise exceptions.InternalError(u"flag has been removed by an other parser")
def parserHeading(self, elt, buf, level):
buf.append((6-level) * u'!')
for child in elt.children:
# we ignore other elements for a Hx title
self.parserText(child, buf)
buf.append(u'\n')
def parserFootnote(self, elt, buf):
for elt in elt.elements():
# all children other than <p/> are ignored
if elt.name == 'p':
a_elt = elt.a
if a_elt is None:
log.warning(u"<p/> element doesn't contain <a/> in footnote, ignoring it")
continue
try:
note_idx = self.footnotes[a_elt['id']]
except KeyError:
log.warning(u"Note id doesn't match any known note, ignoring it")
# we create a dummy element to parse all children after the <a/>
dummy_elt = domish.Element((None, 'note'))
a_idx = elt.children.index(a_elt)
dummy_elt.children = elt.children[a_idx+1:]
note_buf = []
self.parseChildren(dummy_elt, note_buf)
# now we can replace the placeholder
buf[note_idx] = u''.join(note_buf)
def parserText(self, txt, buf, keep_whitespaces=False):
txt = unicode(txt)
if not keep_whitespaces:
# we get text and only let one inter word space
txt = u' '.join(txt.split())
txt = re.sub(ESCAPE_CHARS, r'\\\1', txt)
if txt:
buf.append(txt)
return txt
def parserGeneric(self, elt, buf):
# as dotclear wiki syntax handle arbitrary XHTML code
# we use this feature to add elements that we don't know
buf.append(u"\n\n///html\n{}\n///\n\n".format(elt.toXml()))
def parseChildren(self, elt, buf, block=False):
first_visible = True
for child in elt.children:
if not block and not first_visible and buf and buf[-1][-1] not in (' ','\n'):
# we add separation if it isn't already there
buf.append(u' ')
if domish.IElement.providedBy(child):
self._parse(child, buf)
first_visible = False
else:
appended = self.parserText(child, buf)
if appended:
first_visible = False
def _parse(self, elt, buf):
elt_name = elt.name.lower()
style = elt.getAttribute('style')
if style and elt_name not in ELT_WITH_STYLE:
# if we have style we use generic parser to put raw HTML
# to avoid losing it
parser = self.parserGeneric
else:
try:
parser = getattr(self, "parser_{}".format(elt_name))
except AttributeError:
log.debug("Can't find parser for {} element, using generic one".format(elt.name))
parser = self.parserGeneric
parser(elt, buf)
def parse(self, elt):
self.flags = []
self.footnotes = {}
buf = []
self._parse(elt, buf)
return u''.join(buf)
def parseString(self, string):
wrapped_html = u"<div>{}</div>".format(string)
try:
div_elt = xml_tools.ElementParser()(wrapped_html)
except domish.ParserError as e:
log.warning(u"Error while parsing HTML content: {}".format(e))
return
children = list(div_elt.elements())
if len(children) == 1 and children[0].name == 'div':
div_elt = children[0]
return self.parse(div_elt)
class DCWikiSyntax(object):
def __init__(self, host):
log.info(_(u"Dotclear wiki syntax plugin initialization"))
self.host = host
self._dc_parser = DCWikiParser()
self._xhtml_parser = XHTMLParser()
self._stx = self.host.plugins["TEXT-SYNTAXES"]
self._stx.addSyntax(SYNTAX_NAME, self.parseWiki, self.parseXHTML, [self._stx.OPT_NO_THREAD])
@ -303,4 +645,4 @@ class DCWikiSyntax(object):
return div_elt.toXml()
def parseXHTML(self, xhtml):
raise NotImplementedError
return self._xhtml_parser.parseString(xhtml)