# 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: [''], _end_tag: [''], _title_start_tag: [''], _title_end_tag: [''], _page_sep_tag: ['
', '
', ''], f'{_link_begin_tag}:\\1': [''], _link_end_tag: [''], 'BOLD_BEGIN': ['', ''], 'BOLD_END': ['', ''], 'ITALIC_BEGIN': ['', ''], 'ITALIC_END': ['', ''], 'CODE_BEGIN': ['', '', '
', ''],
  'CODE_END': ['</tt>', '</code>', '</pre>', ''],
  'STRIKE_BEGIN': ['', ''],
  'STRIKE_END': ['', ''],
  'LI_BEGIN': ['
  • '], 'LI_END': ['
  • '], 'QUOTE_BEGIN': ['', '
    '], 'QUOTE_END': ['', '
    '], '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 = []