added decktext parser and spec
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
||||
# 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 = []
|
||||
Reference in New Issue
Block a user