doc: wikiparser: Preserve spaces during parsing and generation

- Matches the original XML more accurately and simplifies some parsing.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2020-08-25 19:59:21 -07:00 committed by James Valleroy
parent aa27037c71
commit d760ee004f
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808

View File

@ -166,10 +166,7 @@ class Paragraph(Element):
xml = '' xml = ''
for item_xml in items_xml: for item_xml in items_xml:
if item_xml[0] in '.,:;-_!?':
xml += item_xml xml += item_xml
else:
xml += ' ' + item_xml
return f'<para>{xml}</para>' return f'<para>{xml}</para>'
@ -555,7 +552,6 @@ def split_formatted(text, delimiter, end_delimiter=None):
Return (formatted_text, remaining_text) if it is found. Return (formatted_text, remaining_text) if it is found.
Return (None, text) otherwise. Return (None, text) otherwise.
""" """
text = text.strip()
end_delimiter = end_delimiter or delimiter end_delimiter = end_delimiter or delimiter
content = None content = None
if text.startswith(delimiter): if text.startswith(delimiter):
@ -698,7 +694,6 @@ def parse_text(line, context=None, parse_links=True):
content = re.split(r"''|`|{{|__|\[\[", line)[0] content = re.split(r"''|`|{{|__|\[\[", line)[0]
if content: if content:
line = line.replace(content, '', 1) line = line.replace(content, '', 1)
content = content.strip()
result += parse_plain_text(content, parse_links=parse_links) result += parse_plain_text(content, parse_links=parse_links)
continue continue
@ -711,7 +706,6 @@ def parse_plain_text(content, parse_links=True):
"""Parse a line or plain text and generate plain text and URL objects.""" """Parse a line or plain text and generate plain text and URL objects."""
result = [] result = []
while content: while content:
content = content.strip()
wiki_link_match = re.search( wiki_link_match = re.search(
r'(?: |^)([A-Z][a-z0-9]+([A-Z][a-z0-9]+)+)(?: |$)', content) r'(?: |^)([A-Z][a-z0-9]+([A-Z][a-z0-9]+)+)(?: |$)', content)
link_match = re.search(r'(https?://[^<> ]+[^<> .:\(\)])', content) link_match = re.search(r'(https?://[^<> ]+[^<> .:\(\)])', content)
@ -737,9 +731,6 @@ def parse_plain_text(content, parse_links=True):
# Replace occurrences of !WikiText with WikiText # Replace occurrences of !WikiText with WikiText
text = re.sub(r'([^A-Za-z]|^)!', r'\g<1>', text) text = re.sub(r'([^A-Za-z]|^)!', r'\g<1>', text)
# Gobble multiple spaces
text = re.sub(r' +', r' ', text)
result.append(PlainText(text)) result.append(PlainText(text))
if end: if end:
@ -755,8 +746,8 @@ def parse_table_row(line, context=None):
row_cells = re.split(r'\|\|', line)[1:-1] row_cells = re.split(r'\|\|', line)[1:-1]
row_items = [] row_items = []
for cell in row_cells: for cell in row_cells:
content = cell.strip() content = cell
if content: if content.strip():
# remove <tablestyle=...> that was already processed # remove <tablestyle=...> that was already processed
content = re.sub('<tablestyle=[^>]+>', '', content) content = re.sub('<tablestyle=[^>]+>', '', content)
align = None align = None
@ -804,8 +795,21 @@ def parse_list(list_data, context=None):
else: else:
content = list_data.pop(0)[2] content = list_data.pop(0)[2]
new_content = ''
in_code_block = False
for line in content.splitlines(True):
if line.startswith(' ' * current_level) and not in_code_block:
line = line[current_level:]
if line.strip().startswith('{{{'):
in_code_block = True
elif line.strip().startswith('}}}'):
in_code_block = False
new_content += line
parsed_list.add_item( parsed_list.add_item(
ListItem(parse_wiki(content, context), ListItem(parse_wiki(new_content, context),
override_marker=override_marker)) override_marker=override_marker))
return parsed_list, list_data return parsed_list, list_data
@ -889,19 +893,20 @@ def parse_wiki(text, context=None, begin_marker=None, end_marker=None):
>>> parse_wiki(' plain multispaced text ') >>> parse_wiki(' plain multispaced text ')
[Paragraph([PlainText(' plain multispaced text ')])] [Paragraph([PlainText(' plain multispaced text ')])]
>>> parse_wiki('https://freedombox.org') >>> parse_wiki('https://freedombox.org')
[Paragraph([Url('https://freedombox.org')])] [Paragraph([Url('https://freedombox.org'), PlainText(' ')])]
>>> parse_wiki("''italic''") >>> parse_wiki("''italic''")
[Paragraph([ItalicText('italic')])] [Paragraph([ItalicText('italic'), PlainText(' ')])]
>>> parse_wiki("'''bold'''") >>> parse_wiki("'''bold'''")
[Paragraph([BoldText('bold')])] [Paragraph([BoldText('bold'), PlainText(' ')])]
>>> parse_wiki("normal text followed by '''bold text'''") >>> parse_wiki("normal text followed by '''bold text'''")
[Paragraph([PlainText('normal text followed by'), BoldText('bold text')])] [Paragraph([PlainText('normal text followed by '), BoldText('bold text'), \
PlainText(' ')])]
>>> parse_wiki('`monospace`') >>> parse_wiki('`monospace`')
[Paragraph([MonospaceText('monospace')])] [Paragraph([MonospaceText('monospace'), PlainText(' ')])]
>>> parse_wiki('{{{code}}}') >>> parse_wiki('{{{code}}}')
[Paragraph([CodeText('code')])] [Paragraph([CodeText('code'), PlainText(' ')])]
>>> parse_wiki('__underline__') >>> parse_wiki('__underline__')
[Paragraph([UnderlineText('underline')])] [Paragraph([UnderlineText('underline'), PlainText(' ')])]
>>> parse_wiki('~-smaller text-~') >>> parse_wiki('~-smaller text-~')
[Paragraph([SmallerTextWarning(), PlainText('smaller text ')])] [Paragraph([SmallerTextWarning(), PlainText('smaller text ')])]
>>> parse_wiki('!FreedomBox') >>> parse_wiki('!FreedomBox')
@ -914,36 +919,38 @@ def parse_wiki(text, context=None, begin_marker=None, end_marker=None):
>>> parse_wiki('[[attachment:Searx.webm|Searx installation and first steps\ >>> parse_wiki('[[attachment:Searx.webm|Searx installation and first steps\
|&do=get]]') |&do=get]]')
[Paragraph([Link('attachment:Searx.webm', \ [Paragraph([Link('attachment:Searx.webm', \
[PlainText('Searx installation and first steps')], '&do=get')])] [PlainText('Searx installation and first steps')], '&do=get'), \
PlainText(' ')])]
>>> parse_wiki('[[https://onionshare.org/|Onionshare]]') >>> parse_wiki('[[https://onionshare.org/|Onionshare]]')
[Paragraph([Link('https://onionshare.org/', [PlainText('Onionshare')])])] [Paragraph([Link('https://onionshare.org/', [PlainText('Onionshare')]), \
PlainText(' ')])]
>>> parse_wiki('/!\\\\') >>> parse_wiki('/!\\\\')
[Paragraph([EmbeddedAttachment('icons/alert.png', \ [Paragraph([EmbeddedAttachment('icons/alert.png', \
[PlainText('icons/alert.png')], 'height=20')])] [PlainText('icons/alert.png')], 'height=20'), PlainText(' ')])]
>>> parse_wiki('(./)') >>> parse_wiki('(./)')
[Paragraph([EmbeddedAttachment('icons/checkmark.png', \ [Paragraph([EmbeddedAttachment('icons/checkmark.png', \
[PlainText('icons/checkmark.png')], 'height=20')])] [PlainText('icons/checkmark.png')], 'height=20'), PlainText(' ')])]
>>> parse_wiki('{X}') >>> parse_wiki('{X}')
[Paragraph([EmbeddedAttachment('icons/icon-error.png', \ [Paragraph([EmbeddedAttachment('icons/icon-error.png', \
[PlainText('icons/icon-error.png')], 'height=20')])] [PlainText('icons/icon-error.png')], 'height=20'), PlainText(' ')])]
>>> parse_wiki('{i}') >>> parse_wiki('{i}')
[Paragraph([EmbeddedAttachment('icons/icon-info.png', \ [Paragraph([EmbeddedAttachment('icons/icon-info.png', \
[PlainText('icons/icon-info.png')], 'height=20')])] [PlainText('icons/icon-info.png')], 'height=20'), PlainText(' ')])]
>>> parse_wiki('{o}') >>> parse_wiki('{o}')
[Paragraph([EmbeddedAttachment('icons/star_off.png', \ [Paragraph([EmbeddedAttachment('icons/star_off.png', \
[PlainText('icons/star_off.png')], 'height=20')])] [PlainText('icons/star_off.png')], 'height=20'), PlainText(' ')])]
>>> parse_wiki('{*}') >>> parse_wiki('{*}')
[Paragraph([EmbeddedAttachment('icons/star_on.png', \ [Paragraph([EmbeddedAttachment('icons/star_on.png', \
[PlainText('icons/star_on.png')], 'height=20')])] [PlainText('icons/star_on.png')], 'height=20'), PlainText(' ')])]
>>> parse_wiki('{{attachment:cockpit-enable.png}}') >>> parse_wiki('{{attachment:cockpit-enable.png}}')
[Paragraph([EmbeddedAttachment('cockpit-enable.png', \ [Paragraph([EmbeddedAttachment('cockpit-enable.png', \
[PlainText('cockpit-enable.png')])])] [PlainText('cockpit-enable.png')]), PlainText(' ')])]
>>> parse_wiki('{{attachment:Backups_Step1_v49.png|Backups: Step 1|\ >>> parse_wiki('{{attachment:Backups_Step1_v49.png|Backups: Step 1|\
width=800}}') width=800}}')
[Paragraph([EmbeddedAttachment('Backups_Step1_v49.png', \ [Paragraph([EmbeddedAttachment('Backups_Step1_v49.png', \
[PlainText('Backups: Step 1')], 'width=800')])] [PlainText('Backups: Step 1')], 'width=800'), PlainText(' ')])]
>>> parse_wiki(' * single item') >>> parse_wiki(' * single item')
[List('bulleted', [ListItem([Paragraph([PlainText('single item ')])])])] [List('bulleted', [ListItem([Paragraph([PlainText('single item ')])])])]
@ -1042,7 +1049,8 @@ to /plinth')])])]
preformatted text (source code)\\n}}}''') preformatted text (source code)\\n}}}''')
[CodeText('multi-line\\npreformatted text (source code)')] [CodeText('multi-line\\npreformatted text (source code)')]
>>> parse_wiki('text to introduce {{{ a singleliner}}}') >>> parse_wiki('text to introduce {{{ a singleliner}}}')
[Paragraph([PlainText('text to introduce'), CodeText(' a singleliner')])] [Paragraph([PlainText('text to introduce '), CodeText(' a singleliner'),\
PlainText(' ')])]
>>> parse_wiki('text to introduce\\n{{{\\n a multiliner\\nstarting at\ >>> parse_wiki('text to introduce\\n{{{\\n a multiliner\\nstarting at\
\\n different indents.\\n}}}') \\n different indents.\\n}}}')
[Paragraph([PlainText('text to introduce ')]), \ [Paragraph([PlainText('text to introduce ')]), \
@ -1074,9 +1082,9 @@ the keys:\\n {{{\\n$ gpg --keyserver keys.gnupg.net --recv-keys \
BCBEBD57A11F70B23782BC5736C361440C9BC971\\n$ gpg --keyserver keys.gnupg.net \ BCBEBD57A11F70B23782BC5736C361440C9BC971\\n$ gpg --keyserver keys.gnupg.net \
--recv-keys 7D6ADB750F91085589484BE677C0C75E7B650808\\n$ gpg --keyserver \ --recv-keys 7D6ADB750F91085589484BE677C0C75E7B650808\\n$ gpg --keyserver \
keys.gnupg.net --recv-keys 013D86D8BA32EAB4A6691BF85D4153D6FE188FC8\\n }}}') keys.gnupg.net --recv-keys 013D86D8BA32EAB4A6691BF85D4153D6FE188FC8\\n }}}')
[Paragraph([PlainText('If this command shows an error such as new key but \ [Paragraph([PlainText(' If this command shows an error such as new key \
contains no user ID - skipped, then use a different keyserver to download the \ but contains no user ID - skipped, then use a different keyserver to download \
keys:')]), CodeText('$ gpg --keyserver keys.gnupg.net --recv-keys \ the keys: ')]), CodeText('$ gpg --keyserver keys.gnupg.net --recv-keys \
BCBEBD57A11F70B23782BC5736C361440C9BC971\\n$ gpg --keyserver keys.gnupg.net \ BCBEBD57A11F70B23782BC5736C361440C9BC971\\n$ gpg --keyserver keys.gnupg.net \
--recv-keys 7D6ADB750F91085589484BE677C0C75E7B650808\\n$ gpg --keyserver \ --recv-keys 7D6ADB750F91085589484BE677C0C75E7B650808\\n$ gpg --keyserver \
keys.gnupg.net --recv-keys 013D86D8BA32EAB4A6691BF85D4153D6FE188FC8')] keys.gnupg.net --recv-keys 013D86D8BA32EAB4A6691BF85D4153D6FE188FC8')]
@ -1120,7 +1128,7 @@ calendar/addressbook')])])])])])])])]
{{attachment:freedombox-screenshot-home.png|Home Page|width=300}}]]') {{attachment:freedombox-screenshot-home.png|Home Page|width=300}}]]')
[Paragraph([Link('attachment:freedombox-screenshot-home.png', \ [Paragraph([Link('attachment:freedombox-screenshot-home.png', \
[EmbeddedAttachment('freedombox-screenshot-home.png', \ [EmbeddedAttachment('freedombox-screenshot-home.png', \
[PlainText('Home Page')], 'width=300')])])] [PlainText('Home Page')], 'width=300')]), PlainText(' ')])]
>>> parse_wiki(" * New wiki and manual content licence: \ >>> parse_wiki(" * New wiki and manual content licence: \
''[[https://creativecommons.org/licenses/by-sa/4.0/|Creative Commons \ ''[[https://creativecommons.org/licenses/by-sa/4.0/|Creative Commons \
@ -1133,8 +1141,8 @@ PlainText('(from June 13rd 2016).')])])])]
>>> parse_wiki('An alternative to downloading these images is to \ >>> parse_wiki('An alternative to downloading these images is to \
[[InstallingDebianOn/TI/BeagleBone|install Debian]] on the !BeagleBone and \ [[InstallingDebianOn/TI/BeagleBone|install Debian]] on the !BeagleBone and \
then [[FreedomBox/Hardware/Debian|install FreedomBox]] on it.') then [[FreedomBox/Hardware/Debian|install FreedomBox]] on it.')
[Paragraph([PlainText('An alternative to downloading these images is to'),\ [Paragraph([PlainText('An alternative to downloading these images is to ')\
Link('InstallingDebianOn/TI/BeagleBone', [PlainText('install Debian')]), \ , Link('InstallingDebianOn/TI/BeagleBone', [PlainText('install Debian')]), \
PlainText(' on the BeagleBone and then '), Link('FreedomBox/Hardware/Debian', \ PlainText(' on the BeagleBone and then '), Link('FreedomBox/Hardware/Debian', \
[PlainText('install FreedomBox')]), PlainText(' on it. ')])] [PlainText('install FreedomBox')]), PlainText(' on it. ')])]
@ -1144,7 +1152,8 @@ existing accounts or \\"Create ...\\" in case of new accounts (see the second \
screenshot below).\\n 1. Check the checkboxes for the address books and \ screenshot below).\\n 1. Check the checkboxes for the address books and \
calendars you want to synchronize and click on the sync button in the header. \ calendars you want to synchronize and click on the sync button in the header. \
(see the third screenshot below)") (see the third screenshot below)")
[Paragraph([BoldText('Synchronizing contacts')]), List('numbered', \ [Paragraph([BoldText('Synchronizing contacts'), PlainText(' ')]), \
List('numbered', \
[ListItem([Paragraph([PlainText('Click on the hamburger menus of CalDAV and \ [ListItem([Paragraph([PlainText('Click on the hamburger menus of CalDAV and \
CardDAV and select either "Refresh ..." in case of existing accounts or \ CardDAV and select either "Refresh ..." in case of existing accounts or \
"Create ..." in case of new accounts (see the second screenshot below). ')])]),\ "Create ..." in case of new accounts (see the second screenshot below). ')])]),\
@ -1171,12 +1180,12 @@ PlainText('and not just the username like'), ItalicText('exampleuser'), \
PlainText(". Enter the address of your email service's IMAP server address in \ PlainText(". Enter the address of your email service's IMAP server address in \
the "), ItalicText('Server'), PlainText(' field. You can try providing your \ the "), ItalicText('Server'), PlainText(' field. You can try providing your \
domain name here such as '), ItalicText('example.org'), PlainText(' for email \ domain name here such as '), ItalicText('example.org'), PlainText(' for email \
address'), ItalicText('exampleuser@example.org'), PlainText("and if this does \ address '), ItalicText('exampleuser@example.org'), PlainText(" and if this \
not work, consult your email provider's documentation for the address of the \ does not work, consult your email provider's documentation for the address \
IMAP server. Using encrypted connection to your IMAP server is strongly \ of the IMAP server. Using encrypted connection to your IMAP server is \
recommended. To do this, prepend 'imaps://' at the beginning of your IMAP \ strongly recommended. To do this, prepend 'imaps://' at the beginning of \
server address. For example,"), ItalicText('imaps://imap.example.org'), \ your IMAP server address. For example, "), \
PlainText('.')])] ItalicText('imaps://imap.example.org'), PlainText('. ')])]
>>> parse_wiki('Tor Browser is the recommended way to browse the web \ >>> parse_wiki('Tor Browser is the recommended way to browse the web \
using Tor. You can download the Tor Browser from \ using Tor. You can download the Tor Browser from \
@ -1207,18 +1216,23 @@ MICRO|width=235,height=132}}]]<<BR>> [[FreedomBox/Hardware/A20-OLinuXino-MICRO\
|A20 OLinuXino MICRO]] ||<style="text-align: center;"> [[FreedomBox/Hardware/\ |A20 OLinuXino MICRO]] ||<style="text-align: center;"> [[FreedomBox/Hardware/\
APU|{{attachment:apu1d_thumb.jpg|PC Engines APU|width=235,height=157}}]]<<BR>>\ APU|{{attachment:apu1d_thumb.jpg|PC Engines APU|width=235,height=157}}]]<<BR>>\
[[FreedomBox/Hardware/APU|PC Engines APU]] ||') [[FreedomBox/Hardware/APU|PC Engines APU]] ||')
[Table([TableRow([TableItem([Paragraph([Link('FreedomBox/Hardware/\ [Table([TableRow([TableItem([Paragraph([PlainText(' '), \
A20-OLinuXino-Lime2', [EmbeddedAttachment('a20-olinuxino-lime2_thumb.jpg', [\ Link('FreedomBox/Hardware/A20-OLinuXino-Lime2', \
PlainText('A20 OLinuXino Lime2')], 'width=235,height=159')])]), Paragraph([\ [EmbeddedAttachment('a20-olinuxino-lime2_thumb.jpg', \
Link('FreedomBox/Hardware/A20-OLinuXino-Lime2', [PlainText('A20 OLinuXino \ [PlainText('A20 OLinuXino Lime2')], 'width=235,height=159')])]), \
Lime2')])])], 'center'), TableItem([Paragraph([Link('FreedomBox/Hardware/\ Paragraph([PlainText(' '), Link('FreedomBox/Hardware/A20-OLinuXino-Lime2', \
A20-OLinuXino-MICRO', [EmbeddedAttachment('a20-olinuxino-micro_thumb.jpg', [\ [PlainText('A20 OLinuXino Lime2')]), PlainText(' ')])], 'center'), \
PlainText('A20 OLinuXino MICRO')], 'width=235,height=132')])]), Paragraph([\ TableItem([Paragraph([PlainText(' '), \
Link('FreedomBox/Hardware/A20-OLinuXino-MICRO', [PlainText('A20 OLinuXino \ Link('FreedomBox/Hardware/A20-OLinuXino-MICRO', \
MICRO')])])], 'center'), TableItem([Paragraph([Link('FreedomBox/Hardware/\ [EmbeddedAttachment('a20-olinuxino-micro_thumb.jpg', \
APU', [EmbeddedAttachment('apu1d_thumb.jpg', [PlainText('PC Engines APU')], \ [PlainText('A20 OLinuXino MICRO')], 'width=235,height=132')])]), \
'width=235,height=157')])]), Paragraph([Link('FreedomBox/Hardware/APU', [\ Paragraph([PlainText(' '), Link('FreedomBox/Hardware/A20-OLinuXino-MICRO', \
PlainText('PC Engines APU')])])], 'center')])])] [PlainText('A20 OLinuXino MICRO')]), PlainText(' ')])], 'center'), \
TableItem([Paragraph([PlainText(' '), Link('FreedomBox/Hardware/APU', \
[EmbeddedAttachment('apu1d_thumb.jpg', [PlainText('PC Engines APU')], \
'width=235,height=157')])]), Paragraph([PlainText(' '), \
Link('FreedomBox/Hardware/APU', [PlainText('PC Engines APU')]), \
PlainText(' ')])], 'center')])])]
>>> parse_wiki(" 1. When created, go to the virtual machine's Settings -> \ >>> parse_wiki(" 1. When created, go to the virtual machine's Settings -> \
[Network] -> [Adapter 1]->[Attached to:] and choose the network type your \ [Network] -> [Adapter 1]->[Attached to:] and choose the network type your \
@ -1250,8 +1264,9 @@ width=394}}\\n\
List('numbered', \ List('numbered', \
[ListItem([Paragraph([PlainText('Launch Quassel Client. You will be greeted \ [ListItem([Paragraph([PlainText('Launch Quassel Client. You will be greeted \
with a wizard to '), MonospaceText('Connect to Core'), PlainText('. ')]), \ with a wizard to '), MonospaceText('Connect to Core'), PlainText('. ')]), \
Paragraph([EmbeddedAttachment('quassel-client-1-connect-to-core.png', \ Paragraph([PlainText(' '), \
[PlainText('Connect to Core')], 'width=394')])]), \ EmbeddedAttachment('quassel-client-1-connect-to-core.png', \
[PlainText('Connect to Core')], 'width=394'), PlainText(' ')])]), \
ListItem([Paragraph([PlainText('Click the '), MonospaceText('Add'), \ ListItem([Paragraph([PlainText('Click the '), MonospaceText('Add'), \
PlainText(' button to launch '), MonospaceText('Add Core Account'), \ PlainText(' button to launch '), MonospaceText('Add Core Account'), \
PlainText(' dialog. ')])])])] PlainText(' dialog. ')])])])]
@ -1264,7 +1279,7 @@ PlainText('dialog.')])])])]
if begin_marker: if begin_marker:
removed_lines = [] removed_lines = []
while lines: while lines:
line = lines.pop(0).strip() line = lines.pop(0)
removed_lines.append(line) removed_lines.append(line)
if line.startswith(begin_marker): if line.startswith(begin_marker):
break break
@ -1375,7 +1390,8 @@ PlainText('dialog.')])])])]
break break
else: else:
next_list_item += '\n' + line next_list_item += '\n' + line
elif candidate.strip().startswith('{{'): elif (candidate.strip().startswith('{{')
and next_list_item[-1] != '\n'):
# Add line break before inline image # Add line break before inline image
next_list_item += ' <<BR>>\n' + lines.pop(0) next_list_item += ' <<BR>>\n' + lines.pop(0)
else: else:
@ -1396,7 +1412,7 @@ PlainText('dialog.')])])])]
list_type = ListType.BULLETED list_type = ListType.BULLETED
else: else:
list_type = ListType.NUMBERED list_type = ListType.NUMBERED
content = line.lstrip(match.group(2) + ' ') content = ' ' * indent + line.lstrip(match.group(2) + ' ')
list_data.append((list_type, indent, content)) list_data.append((list_type, indent, content))
new_list, _ = parse_list(list_data, context) new_list, _ = parse_list(list_data, context)
@ -1457,13 +1473,17 @@ PlainText('dialog.')])])])]
if line.strip(): if line.strip():
texts = [] texts = []
br = '<<BR>>' br = '<<BR>>'
texts.extend(parse_text(line.rstrip(br), context)) space_line = line.rstrip(br) if br in line else line + ' '
texts.extend(parse_text(space_line, context))
if br not in line: if br not in line:
# Collect text until next empty line is reached. # Collect text until next empty line is reached.
while lines and lines[0].strip(): while lines and lines[0].strip():
if end_marker and lines[0].strip().startswith(end_marker): if end_marker and lines[0].strip().startswith(end_marker):
break break
if br in line:
break
# If any of the syntax that ends a paragraph # If any of the syntax that ends a paragraph
paragraph_breakers = ['{{{', '##', '----', '||'] paragraph_breakers = ['{{{', '##', '----', '||']
if any([ if any([
@ -1476,7 +1496,8 @@ PlainText('dialog.')])])])]
break break
line = lines.pop(0) line = lines.pop(0)
texts.extend(parse_text(line.rstrip(br), context)) space_line = line.rstrip(br) if br in line else line + ' '
texts.extend(parse_text(space_line, context))
elements.append(Paragraph(texts)) elements.append(Paragraph(texts))