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
@ -885,91 +889,94 @@ def parse_wiki(text, context=None, begin_marker=None, end_marker=None):
[Heading(5, 'heading 5th level')] [Heading(5, 'heading 5th level')]
>>> parse_wiki('plain text') >>> parse_wiki('plain text')
[Paragraph([PlainText('plain text')])] [Paragraph([PlainText('plain text ')])]
>>> 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')
[Paragraph([PlainText('FreedomBox')])] [Paragraph([PlainText('FreedomBox ')])]
>>> parse_wiki('making a point!') >>> parse_wiki('making a point!')
[Paragraph([PlainText('making a point!')])] [Paragraph([PlainText('making a point! ')])]
>>> parse_wiki('Back to [[FreedomBox/Manual|manual]] page.') >>> parse_wiki('Back to [[FreedomBox/Manual|manual]] page.')
[Paragraph([PlainText('Back to'), Link('FreedomBox/Manual', \ [Paragraph([PlainText('Back to '), Link('FreedomBox/Manual', \
[PlainText('manual')]), PlainText('page.')])] [PlainText('manual')]), PlainText(' page. ')])]
>>> 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 ')])])])]
>>> parse_wiki(' * first item\\n * second item') >>> parse_wiki(' * first item\\n * second item')
[List('bulleted', [ListItem([Paragraph([PlainText('first item')])]), \ [List('bulleted', [ListItem([Paragraph([PlainText('first item ')])]), \
ListItem([Paragraph([PlainText('second item')])])])] ListItem([Paragraph([PlainText('second item ')])])])]
>>> parse_wiki('text to introduce\\n * a list') >>> parse_wiki('text to introduce\\n * a list')
[Paragraph([PlainText('text to introduce')]), \ [Paragraph([PlainText('text to introduce ')]), \
List('bulleted', [ListItem([Paragraph([PlainText('a list')])])])] List('bulleted', [ListItem([Paragraph([PlainText('a list ')])])])]
>>> parse_wiki(' . first item\\n . second item') >>> parse_wiki(' . first item\\n . second item')
[List('plain', [ListItem([Paragraph([PlainText('first item')])]), \ [List('plain', [ListItem([Paragraph([PlainText('first item ')])]), \
ListItem([Paragraph([PlainText('second item')])])])] ListItem([Paragraph([PlainText('second item ')])])])]
>>> parse_wiki(' * item 1\\n * item 1.1') >>> parse_wiki(' * item 1\\n * item 1.1')
[List('bulleted', [ListItem([Paragraph([PlainText('item 1')]), \ [List('bulleted', [ListItem([Paragraph([PlainText('item 1 ')]), \
List('bulleted', [ListItem([Paragraph([PlainText('item 1.1')])])])])])] List('bulleted', [ListItem([Paragraph([PlainText('item 1.1 ')])])])])])]
>>> parse_wiki(' 1. item 1\\n 1. item 1.1') >>> parse_wiki(' 1. item 1\\n 1. item 1.1')
[List('numbered', [ListItem([Paragraph([PlainText('item 1')]), \ [List('numbered', [ListItem([Paragraph([PlainText('item 1 ')]), \
List('numbered', [ListItem([Paragraph([PlainText('item 1.1')])])])])])] List('numbered', [ListItem([Paragraph([PlainText('item 1.1 ')])])])])])]
>>> parse_wiki(' * single,\\n multiline item') >>> parse_wiki(' * single,\\n multiline item')
[List('bulleted', \ [List('bulleted', \
[ListItem([Paragraph([PlainText('single,'), \ [ListItem([Paragraph([PlainText('single, '), \
PlainText('multiline item')])])])] PlainText('multiline item ')])])])]
>>> parse_wiki(' * single,\\n \\n multipara item') >>> parse_wiki(' * single,\\n \\n multipara item')
[List('bulleted', \ [List('bulleted', \
[ListItem([Paragraph([PlainText('single,')]), \ [ListItem([Paragraph([PlainText('single, ')]), \
Paragraph([PlainText('multipara item')])])])] Paragraph([PlainText('multipara item ')])])])]
>>> parse_wiki('----') >>> parse_wiki('----')
[HorizontalRule(4)] [HorizontalRule(4)]
@ -980,9 +987,9 @@ Paragraph([PlainText('multipara item')])])])]
[Table([TableRow([TableItem([Paragraph([BoldText('A')])]), \ [Table([TableRow([TableItem([Paragraph([BoldText('A')])]), \
TableItem([Paragraph([BoldText('B')])]), \ TableItem([Paragraph([BoldText('B')])]), \
TableItem([Paragraph([BoldText('C')])])]), \ TableItem([Paragraph([BoldText('C')])])]), \
TableRow([TableItem([Paragraph([PlainText('1')])]), \ TableRow([TableItem([Paragraph([PlainText('1 ')])]), \
TableItem([Paragraph([PlainText('2')])]), \ TableItem([Paragraph([PlainText('2 ')])]), \
TableItem([Paragraph([PlainText('3')])])])])] TableItem([Paragraph([PlainText('3 ')])])])])]
>>> parse_wiki("||<tablestyle='border:1px solid black;width: 80%'>A||") >>> parse_wiki("||<tablestyle='border:1px solid black;width: 80%'>A||")
[Table([TableRow([TableItem([Paragraph([PlainText('A')])])])], \ [Table([TableRow([TableItem([Paragraph([PlainText('A')])])])], \
@ -1020,52 +1027,53 @@ from="## BEGIN_INCLUDE", to="## END_INCLUDE")>>')
>>> parse_wiki('a\\n\\n## END_INCLUDE\\n\\nb', \ >>> parse_wiki('a\\n\\n## END_INCLUDE\\n\\nb', \
None, None, '## END_INCLUDE') None, None, '## END_INCLUDE')
[Paragraph([PlainText('a')])] [Paragraph([PlainText('a ')])]
>>> parse_wiki('a\\n\\n## BEGIN_INCLUDE\\n\\nb' \ >>> parse_wiki('a\\n\\n## BEGIN_INCLUDE\\n\\nb' \
'\\n\\n## END_INCLUDE\\n\\nc', \ '\\n\\n## END_INCLUDE\\n\\nc', \
None, '## BEGIN_INCLUDE', '## END_INCLUDE') None, '## BEGIN_INCLUDE', '## END_INCLUDE')
[Paragraph([PlainText('b')])] [Paragraph([PlainText('b ')])]
>>> parse_wiki('a<<BR>>\\nb') >>> parse_wiki('a<<BR>>\\nb')
[Paragraph([PlainText('a')]), Paragraph([PlainText('b')])] [Paragraph([PlainText('a')]), Paragraph([PlainText('b ')])]
>>> parse_wiki('{{{#!wiki caution\\n\\nOnce some other app is set as the \ >>> parse_wiki('{{{#!wiki caution\\n\\nOnce some other app is set as the \
home page, you can only navigate to the !FreedomBox Service (Plinth) by \ home page, you can only navigate to the !FreedomBox Service (Plinth) by \
typing https://myfreedombox.rocks/plinth/ into the browser. <<BR>>\\n\ typing https://myfreedombox.rocks/plinth/ into the browser. <<BR>>\\n\
''/freedombox'' can also be used as an alias to ''/plinth''\\n}}}') ''/freedombox'' can also be used as an alias to ''/plinth''\\n}}}')
[Admonition('caution', [Paragraph([PlainText('Once some other app is set \ [Admonition('caution', [Paragraph([PlainText('Once some other app is set \
as the home page, you can only navigate to the FreedomBox Service (Plinth) by \ as the home page, you can only navigate to the FreedomBox Service (Plinth) by \
typing '), Url('https://myfreedombox.rocks/plinth/'), PlainText('into the \ typing '), Url('https://myfreedombox.rocks/plinth/'), PlainText(' into the \
browser.')]), Paragraph([PlainText('/freedombox can also be used as an alias \ browser. ')]), Paragraph([PlainText('/freedombox can also be used as an alias \
to /plinth')])])] to /plinth')])])]
>>> parse_wiki('{{{\\nmulti-line\\n\ >>> parse_wiki('{{{\\nmulti-line\\n\
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'),\
>>> parse_wiki('text to introduce \\n{{{\\n a multiliner\\nstarting at\ PlainText(' ')])]
>>> 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 ')]), \
CodeText(' a multiliner\\nstarting at\\n different indents.')] CodeText(' a multiliner\\nstarting at\\n different indents.')]
>>> parse_wiki('Blah, blah:\\n {{{\\nmulti-line\\nformatted text\\n\ >>> parse_wiki('Blah, blah:\\n {{{\\nmulti-line\\nformatted text\\n\
starting at col #1\\n}}}') starting at col #1\\n}}}')
[Paragraph([PlainText('Blah, blah:')]), \ [Paragraph([PlainText('Blah, blah: ')]), \
CodeText('multi-line\\nformatted text\\nstarting at col #1')] CodeText('multi-line\\nformatted text\\nstarting at col #1')]
>>> parse_wiki(' * Blah, blah:\\n {{{\\nmulti-line\\nformatted text\ >>> parse_wiki(' * Blah, blah:\\n {{{\\nmulti-line\\nformatted text\
\\nstarting at col #1\\n}}}') \\nstarting at col #1\\n}}}')
[List('bulleted', \ [List('bulleted', \
[ListItem([Paragraph([PlainText('Blah, blah:')]), \ [ListItem([Paragraph([PlainText('Blah, blah: ')]), \
CodeText('multi-line\\nformatted text\\nstarting at col #1')])])] CodeText('multi-line\\nformatted text\\nstarting at col #1')])])]
>>> parse_wiki(' {{{\\n nmap -p 80 --open -sV 192.168.0.0/24 \ >>> parse_wiki(' {{{\\n nmap -p 80 --open -sV 192.168.0.0/24 \
(replace the ip/netmask with the one the router uses)\\n }}}\\n In \ (replace the ip/netmask with the one the router uses)\\n }}}\\n In \
most cases you can look at your current IP address, and change the last \ most cases you can look at your current IP address, and change the last \
digits with zero to find your home network, like so: XXX.XXX.XXX.0/24') digits with zero to find your home network, like so: XXX.XXX.XXX.0/24')
[CodeText(' nmap -p 80 --open -sV 192.168.0.0/24 (replace the \ [CodeText(' nmap -p 80 --open -sV 192.168.0.0/24 (replace the \
ip/netmask with the one the router uses)'), Paragraph([PlainText('In \ ip/netmask with the one the router uses)'), Paragraph([PlainText(' In \
most cases you can look at your current IP address, and change the last \ most cases you can look at your current IP address, and change the last \
digits with zero to find your home network, like so: XXX.XXX.XXX.0/24')])] digits with zero to find your home network, like so: XXX.XXX.XXX.0/24 ')])]
>>> parse_wiki('text to introduce \\n----\\n<<TableOfContents()>>') >>> parse_wiki('text to introduce\\n----\\n<<TableOfContents()>>')
[Paragraph([PlainText('text to introduce')]), \ [Paragraph([PlainText('text to introduce ')]), \
HorizontalRule(4), TableOfContents()] HorizontalRule(4), TableOfContents()]
>>> parse_wiki(' If this command shows an error such as ''new key but \ >>> parse_wiki(' If this command shows an error such as ''new key but \
@ -1074,69 +1082,69 @@ 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')]
>>> parse_wiki('User documentation:\\n * List of \ >>> parse_wiki('User documentation:\\n * List of \
[[FreedomBox/Features|applications]] offered by !FreedomBox.') [[FreedomBox/Features|applications]] offered by !FreedomBox.')
[Paragraph([PlainText('User documentation:')]), List('bulleted', \ [Paragraph([PlainText('User documentation: ')]), List('bulleted', \
[ListItem([Paragraph([PlainText('List of'), Link('FreedomBox/Features', \ [ListItem([Paragraph([PlainText('List of '), Link('FreedomBox/Features', \
[PlainText('applications')]), PlainText('offered by FreedomBox.')])])])] [PlainText('applications')]), PlainText(' offered by FreedomBox. ')])])])]
>>> parse_wiki('\ >>> parse_wiki('\
* Within !FreedomBox Service (Plinth)\\n\ * Within !FreedomBox Service (Plinth)\\n\
1. select ''Apps''\\n\ 1. select ''Apps''\\n\
2. go to ''Radicale (Calendar and Addressbook)'' and \\n\ 2. go to ''Radicale (Calendar and Addressbook)'' and\\n\
3. install the application. After the installation is complete, make sure \ 3. install the application. After the installation is complete, make sure \
the application is marked "enabled" in the !FreedomBox interface. Enabling \ the application is marked "enabled" in the !FreedomBox interface. Enabling \
the application launches the Radicale CalDAV/CardDAV server. \\n\ the application launches the Radicale CalDAV/CardDAV server.\\n\
4. define the access rights:\\n\ 4. define the access rights:\\n\
* Only the owner of a calendar/addressbook can view or make changes\\n\ * Only the owner of a calendar/addressbook can view or make changes\\n\
* Any user can view any calendar/addressbook, but only the owner can make \ * Any user can view any calendar/addressbook, but only the owner can make \
changes\\n\ changes\\n\
* Any user can view or make changes to any calendar/addressbook') * Any user can view or make changes to any calendar/addressbook')
[List('bulleted', [\ [List('bulleted', [\
ListItem([Paragraph([PlainText('Within FreedomBox Service (Plinth)')]), \ ListItem([Paragraph([PlainText('Within FreedomBox Service (Plinth) ')]), \
List('numbered', [ListItem([Paragraph([PlainText('select Apps')])]), \ List('numbered', [ListItem([Paragraph([PlainText('select Apps ')])]), \
ListItem([Paragraph([PlainText('go to Radicale (Calendar and Addressbook) \ ListItem([Paragraph([PlainText('go to Radicale (Calendar and Addressbook) \
and')])]), \ and ')])]), \
ListItem([Paragraph([PlainText('install the application. After the \ ListItem([Paragraph([PlainText('install the application. After the \
installation is complete, make sure the application is marked "enabled" in \ installation is complete, make sure the application is marked "enabled" in \
the FreedomBox interface. Enabling the application launches the Radicale \ the FreedomBox interface. Enabling the application launches the Radicale \
CalDAV/CardDAV server.')])]), \ CalDAV/CardDAV server. ')])]), \
ListItem([Paragraph([PlainText('define the access rights:')]), \ ListItem([Paragraph([PlainText('define the access rights: ')]), \
List('bulleted', [ListItem([Paragraph([PlainText('Only the owner of a \ List('bulleted', [ListItem([Paragraph([PlainText('Only the owner of a \
calendar/addressbook can view or make changes')])]), \ calendar/addressbook can view or make changes ')])]), \
ListItem([Paragraph([PlainText('Any user can view any calendar/addressbook, \ ListItem([Paragraph([PlainText('Any user can view any calendar/addressbook, \
but only the owner can make changes')])]), \ but only the owner can make changes ')])]), \
ListItem([Paragraph([PlainText('Any user can view or make changes to any \ ListItem([Paragraph([PlainText('Any user can view or make changes to any \
calendar/addressbook')])])])])])])])] calendar/addressbook ')])])])])])])])]
>>> parse_wiki('[[attachment:freedombox-screenshot-home.png|\ >>> parse_wiki('[[attachment:freedombox-screenshot-home.png|\
{{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 \
Attribution-ShareAlike 4.0 International]]'' (from June 13rd 2016).") Attribution-ShareAlike 4.0 International]]'' (from June 13rd 2016).")
[List('bulleted', [ListItem([Paragraph([PlainText('New wiki and manual \ [List('bulleted', [ListItem([Paragraph([PlainText('New wiki and manual \
content licence:'), Link('https://creativecommons.org/licenses/by-sa/4.0/', \ content licence: '), Link('https://creativecommons.org/licenses/by-sa/4.0/', \
[ItalicText('Creative Commons Attribution-ShareAlike 4.0 International')]), \ [ItalicText('Creative Commons Attribution-ShareAlike 4.0 International')]), \
PlainText('(from June 13rd 2016).')])])])] 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. ')])]
>>> parse_wiki("'''Synchronizing contacts'''\\n 1. Click on the hamburger \ >>> parse_wiki("'''Synchronizing contacts'''\\n 1. Click on the hamburger \
menus of CalDAV and CardDAV and select either \\"Refresh ...\\" in case of \ menus of CalDAV and CardDAV and select either \\"Refresh ...\\" in case of \
@ -1144,13 +1152,14 @@ 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). ')])]),\
ListItem([Paragraph([PlainText('Check the checkboxes for the address books \ ListItem([Paragraph([PlainText('Check the checkboxes for the address books \
and calendars you want to synchronize and click on the sync button in the \ and calendars you want to synchronize and click on the sync button in the \
header. (see the third screenshot below)')])])])] header. (see the third screenshot below) ')])])])]
>>> parse_wiki("After Roundcube is installed, it can be accessed at \ >>> parse_wiki("After Roundcube is installed, it can be accessed at \
{{{https://<your freedombox>/roundcube}}}. Enter your username and password. \ {{{https://<your freedombox>/roundcube}}}. Enter your username and password. \
@ -1164,19 +1173,19 @@ the IMAP server. Using encrypted connection to your IMAP server is strongly \
recommended. To do this, prepend 'imaps://' at the beginning of your IMAP \ recommended. To do this, prepend 'imaps://' at the beginning of your IMAP \
server address. For example, ''imaps://imap.example.org''.") server address. For example, ''imaps://imap.example.org''.")
[Paragraph([PlainText('After Roundcube is installed, it can be accessed \ [Paragraph([PlainText('After Roundcube is installed, it can be accessed \
at'), CodeText('https://<your freedombox>/roundcube'), PlainText('. Enter \ at '), CodeText('https://<your freedombox>/roundcube'), PlainText('. Enter \
your username and password. The username for many mail services will be the \ your username and password. The username for many mail services will be the \
full email address such as'), ItalicText('exampleuser@example.org'), \ full email address such as '), ItalicText('exampleuser@example.org'), \
PlainText('and not just the username like'), ItalicText('exampleuser'), \ 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 \
@ -1184,19 +1193,19 @@ https://www.torproject.org/projects/torbrowser.html and follow the \
instructions on that site to install and run it.') instructions on that site to install and run it.')
[Paragraph([PlainText('Tor Browser is the recommended way to browse the \ [Paragraph([PlainText('Tor Browser is the recommended way to browse the \
web using Tor. You can download the Tor Browser from '), Url('\ web using Tor. You can download the Tor Browser from '), Url('\
https://www.torproject.org/projects/torbrowser.html'), PlainText('and follow \ https://www.torproject.org/projects/torbrowser.html'), PlainText(' and follow \
the instructions on that site to install and run it.')])] the instructions on that site to install and run it. ')])]
>>> parse_wiki('After installation a web page becomes available on \ >>> parse_wiki('After installation a web page becomes available on \
https://<your-freedombox>/_minidlna.') https://<your-freedombox>/_minidlna.')
[Paragraph([PlainText('After installation a web page becomes available on \ [Paragraph([PlainText('After installation a web page becomes available on \
https://<your-freedombox>/_minidlna.')])] https://<your-freedombox>/_minidlna. ')])]
>>> parse_wiki('or http://10.42.0.1/.') >>> parse_wiki('or http://10.42.0.1/.')
[Paragraph([PlainText('or '), Url('http://10.42.0.1/'), PlainText('.')])] [Paragraph([PlainText('or '), Url('http://10.42.0.1/'), PlainText('. ')])]
>>> parse_wiki('or http://10.42.0.1/:') >>> parse_wiki('or http://10.42.0.1/:')
[Paragraph([PlainText('or '), Url('http://10.42.0.1/'), PlainText(':')])] [Paragraph([PlainText('or '), Url('http://10.42.0.1/'), PlainText(': ')])]
>>> parse_wiki('||<style="text-align: center;"> [[FreedomBox/Hardware/\ >>> parse_wiki('||<style="text-align: center;"> [[FreedomBox/Hardware/\
A20-OLinuXino-Lime2|{{attachment:a20-olinuxino-lime2_thumb.jpg|A20 OLinuXino \ A20-OLinuXino-Lime2|{{attachment:a20-olinuxino-lime2_thumb.jpg|A20 OLinuXino \
@ -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 \
@ -1228,14 +1242,14 @@ that this exposes the !FreedomBox's services to your entire local network.")
[List('numbered', [ListItem([Paragraph([PlainText("When created, go to \ [List('numbered', [ListItem([Paragraph([PlainText("When created, go to \
the virtual machine's Settings -> [Network] -> [Adapter 1]->[Attached to:] \ the virtual machine's Settings -> [Network] -> [Adapter 1]->[Attached to:] \
and choose the network type your want the machine to use according to the \ and choose the network type your want the machine to use according to the \
explanation in Network Configuration below. The recommended type is the"), \ explanation in Network Configuration below. The recommended type is the "), \
ItalicText('Bridged adapter'), PlainText("option, but be aware that this \ ItalicText('Bridged adapter'), PlainText(" option, but be aware that this \
exposes the FreedomBox's services to your entire local network.")])])])] exposes the FreedomBox's services to your entire local network. ")])])])]
>>> parse_wiki('After logging in, you can become root with the command \ >>> parse_wiki('After logging in, you can become root with the command \
`sudo su`.\\n \\n=== Build Image ===') `sudo su`.\\n \\n=== Build Image ===')
[Paragraph([PlainText('After logging in, you can become root with the \ [Paragraph([PlainText('After logging in, you can become root with the \
command'), MonospaceText('sudo su'), PlainText('.')]), \ command '), MonospaceText('sudo su'), PlainText('. ')]), \
Heading(3, 'Build Image')] Heading(3, 'Build Image')]
>>> parse_wiki('Quassel Core will be initialized too.\\n\\n\ >>> parse_wiki('Quassel Core will be initialized too.\\n\\n\
@ -1246,15 +1260,16 @@ width=394}}\\n\
1. Click the `Add` button to launch `Add Core Account` dialog.\\n\ 1. Click the `Add` button to launch `Add Core Account` dialog.\\n\
') ')
[Paragraph(\ [Paragraph(\
[PlainText('Quassel Core will be initialized too.')]), \ [PlainText('Quassel Core will be initialized too. ')]), \
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', \
ListItem([Paragraph([PlainText('Click the'), MonospaceText('Add'), \ [PlainText('Connect to Core')], 'width=394'), PlainText(' ')])]), \
PlainText('button to launch'), MonospaceText('Add Core Account'), \ ListItem([Paragraph([PlainText('Click the '), MonospaceText('Add'), \
PlainText('dialog.')])])])] PlainText(' button to launch '), MonospaceText('Add Core Account'), \
PlainText(' dialog. ')])])])]
""" """
elements = [] elements = []
@ -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,9 +1390,10 @@ 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:
next_list_item += '\n' + lines.pop(0) next_list_item += '\n' + lines.pop(0)
@ -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))
@ -1492,13 +1513,13 @@ def generate_inner_docbook(parsed_wiki, context=None):
>>> generate_inner_docbook([\ >>> generate_inner_docbook([\
Heading(1, 'heading 1st level'), \ Heading(1, 'heading 1st level'), \
Heading(2, 'heading 2nd level'), \ Heading(2, 'heading 2nd level'), \
Paragraph([PlainText('plain text')]), \ Paragraph([PlainText('plain text ')]), \
Heading(3, 'heading 3rd level'), \ Heading(3, 'heading 3rd level'), \
Heading(2, 'heading 2nd level'), \ Heading(2, 'heading 2nd level'), \
]) ])
'<section><title>heading 1st level</title>\ '<section><title>heading 1st level</title>\
<section><title>heading 2nd level</title>\ <section><title>heading 2nd level</title>\
<para>plain text</para>\ <para>plain text </para>\
<section><title>heading 3rd level</title>\ <section><title>heading 3rd level</title>\
</section></section>\ </section></section>\
<section><title>heading 2nd level</title>\ <section><title>heading 2nd level</title>\
@ -1507,8 +1528,8 @@ Heading(2, 'heading 2nd level'), \
>>> generate_inner_docbook([Heading(1, 'Date & Time')]) >>> generate_inner_docbook([Heading(1, 'Date & Time')])
'<section><title>Date &amp; Time</title></section>' '<section><title>Date &amp; Time</title></section>'
>>> generate_inner_docbook([Paragraph([PlainText('plain text')])]) >>> generate_inner_docbook([Paragraph([PlainText('plain text ')])])
'<para>plain text</para>' '<para>plain text </para>'
>>> generate_inner_docbook([Paragraph([Url('https://freedombox.org')])]) >>> generate_inner_docbook([Paragraph([Url('https://freedombox.org')])])
'<para><ulink url="https://freedombox.org"/></para>' '<para><ulink url="https://freedombox.org"/></para>'
@ -1520,7 +1541,7 @@ Heading(2, 'heading 2nd level'), \
'<para><emphasis role="strong">bold</emphasis></para>' '<para><emphasis role="strong">bold</emphasis></para>'
>>> generate_inner_docbook([Paragraph([\ >>> generate_inner_docbook([Paragraph([\
PlainText('normal text followed by'), BoldText('bold text')])]) PlainText('normal text followed by '), BoldText('bold text')])])
'<para>normal text followed by \ '<para>normal text followed by \
<emphasis role="strong">bold text</emphasis></para>' <emphasis role="strong">bold text</emphasis></para>'
@ -1639,9 +1660,9 @@ Url('http://example.com')])])
>>> generate_inner_docbook([Paragraph([\ >>> generate_inner_docbook([Paragraph([\
PlainText('User documentation:')]), \ PlainText('User documentation:')]), \
List('bulleted', [ListItem([Paragraph([PlainText('List of'), \ List('bulleted', [ListItem([Paragraph([PlainText('List of '), \
Link('FreedomBox/Features', [PlainText('applications')]), \ Link('FreedomBox/Features', [PlainText('applications')]), \
PlainText('offered by FreedomBox.')])])])]) PlainText(' offered by FreedomBox.')])])])])
'<para>User documentation:</para><itemizedlist><listitem><para>List of \ '<para>User documentation:</para><itemizedlist><listitem><para>List of \
<ulink url="https://wiki.debian.org/FreedomBox/Features#">applications\ <ulink url="https://wiki.debian.org/FreedomBox/Features#">applications\
</ulink> offered by FreedomBox.</para></listitem></itemizedlist>' </ulink> offered by FreedomBox.</para></listitem></itemizedlist>'
@ -1650,52 +1671,52 @@ PlainText('offered by FreedomBox.')])])])])
ListItem([Paragraph([PlainText('Within FreedomBox Service (Plinth)')]), \ ListItem([Paragraph([PlainText('Within FreedomBox Service (Plinth)')]), \
List('numbered', [ListItem([Paragraph([PlainText('select Apps')])]), \ List('numbered', [ListItem([Paragraph([PlainText('select Apps')])]), \
ListItem([Paragraph([PlainText('go to Radicale (Calendar and Addressbook) \ ListItem([Paragraph([PlainText('go to Radicale (Calendar and Addressbook) \
and')])]), \ and ')])]), \
ListItem([Paragraph([PlainText('install the application. After the \ ListItem([Paragraph([PlainText('install the application. After the \
installation is complete, make sure the application is marked "enabled" in \ installation is complete, make sure the application is marked "enabled" in \
the FreedomBox interface. Enabling the application launches the Radicale \ the FreedomBox interface. Enabling the application launches the Radicale \
CalDAV/CardDAV server.')])]), \ CalDAV/CardDAV server. ')])]), \
ListItem([Paragraph([PlainText('define the access rights:')]), \ ListItem([Paragraph([PlainText('define the access rights: ')]), \
List('bulleted', [ListItem([Paragraph([PlainText('Only the owner of a \ List('bulleted', [ListItem([Paragraph([PlainText('Only the owner of a \
calendar/addressbook can view or make changes')])]), \ calendar/addressbook can view or make changes ')])]), \
ListItem([Paragraph([PlainText('Any user can view any calendar/addressbook, \ ListItem([Paragraph([PlainText('Any user can view any calendar/addressbook, \
but only the owner can make changes')])]), \ but only the owner can make changes ')])]), \
ListItem([Paragraph([PlainText('Any user can view or make changes to any \ ListItem([Paragraph([PlainText('Any user can view or make changes to any \
calendar/addressbook')])])])])])])])]) calendar/addressbook ')])])])])])])])])
'<itemizedlist>\ '<itemizedlist>\
<listitem><para>Within FreedomBox Service (Plinth)</para> \ <listitem><para>Within FreedomBox Service (Plinth)</para> \
<orderedlist numeration="arabic">\ <orderedlist numeration="arabic">\
<listitem><para>select Apps</para></listitem>\ <listitem><para>select Apps</para></listitem>\
<listitem><para>go to Radicale (Calendar and Addressbook) and</para>\ <listitem><para>go to Radicale (Calendar and Addressbook) and </para>\
</listitem>\ </listitem>\
<listitem><para>install the application. After the installation is complete, \ <listitem><para>install the application. After the installation is complete, \
make sure the application is marked "enabled" in the FreedomBox interface. \ make sure the application is marked "enabled" in the FreedomBox interface. \
Enabling the application launches the Radicale CalDAV/CardDAV server.</para>\ Enabling the application launches the Radicale CalDAV/CardDAV server. </para>\
</listitem>\ </listitem>\
<listitem><para>define the access rights:</para> \ <listitem><para>define the access rights: </para> \
<itemizedlist>\ <itemizedlist>\
<listitem><para>Only the owner of a calendar/addressbook can view or make \ <listitem><para>Only the owner of a calendar/addressbook can view or make \
changes</para></listitem>\ changes </para></listitem>\
<listitem><para>Any user can view any calendar/addressbook, but only the \ <listitem><para>Any user can view any calendar/addressbook, but only the \
owner can make changes</para></listitem>\ owner can make changes </para></listitem>\
<listitem><para>Any user can view or make changes to any calendar/addressbook\ <listitem><para>Any user can view or make changes to any calendar/addressbook \
</para></listitem></itemizedlist>\ </para></listitem></itemizedlist>\
</listitem></orderedlist>\ </listitem></orderedlist>\
</listitem></itemizedlist>' </listitem></itemizedlist>'
>>> generate_inner_docbook([Paragraph([PlainText('An alternative to \ >>> generate_inner_docbook([Paragraph([PlainText('An alternative to \
downloading these images is to'), Link('InstallingDebianOn/TI/BeagleBone', \ downloading these images is to '), Link('InstallingDebianOn/TI/BeagleBone', \
[PlainText('install Debian')]), PlainText('on the BeagleBone and then'), \ [PlainText(' install Debian')]), PlainText(' on the BeagleBone and then '), \
Link('FreedomBox/Hardware/Debian', [PlainText('install FreedomBox')]), \ Link('FreedomBox/Hardware/Debian', [PlainText('install FreedomBox')]), \
PlainText('on it.')])]) PlainText(' on it. ')])])
'<para>An alternative to downloading these images is to \ '<para>An alternative to downloading these images is to \
<ulink url="https://wiki.debian.org/InstallingDebianOn/TI/BeagleBone#">\ <ulink url="https://wiki.debian.org/InstallingDebianOn/TI/BeagleBone#">\
install Debian</ulink> on the BeagleBone and then \ install Debian</ulink> on the BeagleBone and then \
<ulink url="https://wiki.debian.org/FreedomBox/Hardware/Debian#">install \ <ulink url="https://wiki.debian.org/FreedomBox/Hardware/Debian#">install \
FreedomBox</ulink> on it.</para>' FreedomBox</ulink> on it. </para>'
>>> generate_inner_docbook([Paragraph([PlainText('After Roundcube is \ >>> generate_inner_docbook([Paragraph([PlainText('After Roundcube is \
installed, it can be accessed at'), CodeText('https://<your freedombox>\ installed, it can be accessed at '), CodeText('https://<your freedombox>\
/roundcube'), PlainText('.')])]) /roundcube'), PlainText('.')])])
'<para>After Roundcube is installed, it can be accessed at <code>\ '<para>After Roundcube is installed, it can be accessed at <code>\
https://&lt;your freedombox&gt;/roundcube</code>.</para>' https://&lt;your freedombox&gt;/roundcube</code>.</para>'