Files
t-deckard/deck/text.py
T

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}': '',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&apos;': "'",
'&amp;': '&'
}
# 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 = []