137 lines
4.7 KiB
Python
137 lines
4.7 KiB
Python
# deck.text: Basic DeckText parsing support
|
|||
|
|
# (see decktext-spec.md for the specification)
|
||
|
|
# Created by Luxferre in 2025, released into public domain
|
||
|
|
|
||
|
|
import re
|
||
|
|
|
||
|
|
# internal tag delimiters and boundaries
|
||
|
|
_tag_delim = '%%%'
|
||
|
|
_start_tag = 'DECKTEXT_BEGIN'
|
||
|
|
_end_tag = 'DECKTEXT_END'
|
||
|
|
_start_boundary = f'{_tag_delim}{_start_tag}{_tag_delim}'
|
||
|
|
_end_boundary = f'{_tag_delim}{_end_tag}{_tag_delim}'
|
||
|
|
_title_start_tag = 'TITLE_BEGIN'
|
||
|
|
_title_end_tag = 'TITLE_END'
|
||
|
|
_title_start_boundary = f'{_tag_delim}{_title_start_tag}{_tag_delim}'
|
||
|
|
_title_end_boundary = f'{_tag_delim}{_title_end_tag}{_tag_delim}'
|
||
|
|
_page_sep_tag = 'PAGE_SEP'
|
||
|
|
_page_boundary = f'{_tag_delim}{_page_sep_tag}{_tag_delim}'
|
||
|
|
_link_begin_tag = 'LINK_BEGIN'
|
||
|
|
_link_end_tag = 'LINK_END'
|
||
|
|
_link_regex_src = f'{_tag_delim}{_link_begin_tag}:(.+?){_tag_delim}(.+?){_tag_delim}{_link_end_tag}{_tag_delim}'
|
||
|
|
_link_regex = re.compile(_link_regex_src)
|
||
|
|
|
||
|
|
# internal tag mapping
|
||
|
|
_tagmap = {
|
||
|
|
_start_tag: ['<!\\-\\-\\ decktext_begin\\ \\-\\->'],
|
||
|
|
_end_tag: ['<!\\-\\-\\ decktext_end\\ \\-\\->'],
|
||
|
|
_title_start_tag: ['<title>'],
|
||
|
|
_title_end_tag: ['</title>'],
|
||
|
|
_page_sep_tag: ['<hr>', '<hr/>', '<hr\\ />'],
|
||
|
|
f'{_link_begin_tag}:\\1': ['<a\\ href="(.+?)">'],
|
||
|
|
_link_end_tag: ['</a>'],
|
||
|
|
'BOLD_BEGIN': ['<b>', '<strong>'],
|
||
|
|
'BOLD_END': ['</b>', '</strong>'],
|
||
|
|
'ITALIC_BEGIN': ['<i>', '<em>'],
|
||
|
|
'ITALIC_END': ['</i>', '</em>'],
|
||
|
|
'CODE_BEGIN': ['<tt>', '<code>', '<pre>', '<xmp>'],
|
||
|
|
'CODE_END': ['</tt>', '</code>', '</pre>', '</xmp>'],
|
||
|
|
'STRIKE_BEGIN': ['<s>', '<del>'],
|
||
|
|
'STRIKE_END': ['</s>', '</del>'],
|
||
|
|
'LI_BEGIN': ['<li>'],
|
||
|
|
'LI_END': ['</li>'],
|
||
|
|
'QUOTE_BEGIN': ['<q>', '<blockquote>'],
|
||
|
|
'QUOTE_END': ['</q>', '</blockquote>'],
|
||
|
|
'IGNORE': ['<.+?>']
|
||
|
|
}
|
||
|
|
|
||
|
|
# compile regexes for this mapping
|
||
|
|
_ctagmap = []
|
||
|
|
for tagkey in _tagmap:
|
||
|
|
finaltag = f'{_tag_delim}{tagkey}{_tag_delim}'
|
||
|
|
for tagitem in _tagmap[tagkey]:
|
||
|
|
if tagkey != 'IGNORE':
|
||
|
|
_ctagmap.append((re.compile(tagitem.lower()), finaltag))
|
||
|
|
_ctagmap.append((re.compile(tagitem.upper()), finaltag))
|
||
|
|
# IGNORE tag needs to be added last
|
||
|
|
if 'IGNORE' in _tagmap:
|
||
|
|
for tagitem in _tagmap['IGNORE']:
|
||
|
|
_ctagmap.append((re.compile(tagitem), ''))
|
||
|
|
|
||
|
|
# output format replacements (tags and main HTML entities)
|
||
|
|
|
||
|
|
_render_tags = {
|
||
|
|
f'{_tag_delim}BOLD_BEGIN{_tag_delim}': '**',
|
||
|
|
f'{_tag_delim}BOLD_END{_tag_delim}': '**',
|
||
|
|
f'{_tag_delim}ITALIC_BEGIN{_tag_delim}': '_',
|
||
|
|
f'{_tag_delim}ITALIC_END{_tag_delim}': '_',
|
||
|
|
f'{_tag_delim}CODE_BEGIN{_tag_delim}': '`',
|
||
|
|
f'{_tag_delim}CODE_END{_tag_delim}': '`',
|
||
|
|
f'{_tag_delim}STRIKE_BEGIN{_tag_delim}': '~~',
|
||
|
|
f'{_tag_delim}STRIKE_END{_tag_delim}': '~~',
|
||
|
|
f'{_tag_delim}QUOTE_BEGIN{_tag_delim}': '>> ',
|
||
|
|
f'{_tag_delim}QUOTE_END{_tag_delim}': '',
|
||
|
|
f'{_tag_delim}LI_BEGIN{_tag_delim}': '* ',
|
||
|
|
f'{_tag_delim}LI_END{_tag_delim}': '',
|
||
|
|
'<': '<',
|
||
|
|
'>': '>',
|
||
|
|
'"': '"',
|
||
|
|
''': "'",
|
||
|
|
'&': '&'
|
||
|
|
}
|
||
|
|
|
||
|
|
# main class start
|
||
|
|
class DeckText:
|
||
|
|
def __init__(self, src=None):
|
||
|
|
self.title = ''
|
||
|
|
self.pages = []
|
||
|
|
if src:
|
||
|
|
self.load(src)
|
||
|
|
|
||
|
|
def _render_page(self, pagetext):
|
||
|
|
page_links = [] # list of tuples in (url, title) format
|
||
|
|
linkpool = pagetext = pagetext.strip()
|
||
|
|
linknum = 1
|
||
|
|
while True: # parse links first
|
||
|
|
linkmatch = _link_regex.search(linkpool)
|
||
|
|
if linkmatch is None:
|
||
|
|
break
|
||
|
|
fullref = linkmatch.group(0)
|
||
|
|
url = linkmatch.group(1).strip()
|
||
|
|
title = linkmatch.group(2).strip()
|
||
|
|
page_links.append((url, title))
|
||
|
|
linkpool = linkpool.replace(fullref, '')
|
||
|
|
pagetext = pagetext.replace(fullref, f'{title} ([{linknum}])')
|
||
|
|
linknum += 1
|
||
|
|
for rtag in _render_tags: # render the rest of the tags
|
||
|
|
pagetext = pagetext.replace(rtag, _render_tags[rtag])
|
||
|
|
return {
|
||
|
|
'content': pagetext,
|
||
|
|
'links': page_links
|
||
|
|
}
|
||
|
|
|
||
|
|
def load(self, src:str):
|
||
|
|
if len(src) > 0:
|
||
|
|
# pre-parse stage 1: replace ambiguous tags with own tagmap
|
||
|
|
for tagkey in _ctagmap:
|
||
|
|
src = tagkey[0].sub(tagkey[1], src)
|
||
|
|
# pre-parse stage 2: extract document title
|
||
|
|
try:
|
||
|
|
self.title = src.split(_title_start_boundary)[1].split(_title_end_boundary)[0].strip()
|
||
|
|
except:
|
||
|
|
self.title = ''
|
||
|
|
# pre-parse stage 3: only leave the contents between the DeckText begin/end markers
|
||
|
|
fragments = []
|
||
|
|
for part in src.split(_start_boundary)[1:]: # skip the part before the first marker
|
||
|
|
frag = part # by default, we may not use an end marker although that's bad
|
||
|
|
try:
|
||
|
|
frag = part.split(_end_boundary)[0]
|
||
|
|
except:
|
||
|
|
pass
|
||
|
|
fragments.append(frag.strip())
|
||
|
|
# parse stage: shape individual pages and pass them to the renderer
|
||
|
|
self.pages = list(map(self._render_page, '\n'.join(fragments).split(_page_boundary)))
|
||
|
|
else: # reset on empty string
|
||
|
|
self.title = ''
|
||
|
|
self.pages = []
|