diff --git a/README.md b/README.md index ba30599..689a769 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,21 @@ Exports the following functions: - `totp(key, time_step, digits=6)`: generate a `digits`-long TOTP code from (bytes-only) key, using `time_step` as the basis (30 seconds is default and the most common) - `get_epoch()`: a helper platform-agnostic function to get the current Unix timestamp in seconds +### `deck.text` + +A library that implements a [DeckText](decktext-spec.md) format parser. Exports a single class, `DeckText`, with the following methods: + +- `doc = DeckText(src=None)` constructor: initialize a DeckText document object from the passed source +- `doc.load(src)`: load DeckText source into the existing document object. + +After the source is loaded, the document object exposes two properties: `title` and `pages`. + +The `title` property is a normal string that represents the document's title (read from the `` HTML tag). + +The `pages` property contains a list of DeckText pages in the form of `{content, links}` dictionary, where `content` is the rendered (ready-to-display) page content and `links` is the list of links present on the page. + +Each link is in the form of `(url, label)` tuple. The client using this class for DeckText page rendering is supposed to provide a UI to visit the links based on this list. + ### `deck.llm` A helper library for interaction with remote LLM (large language model) endpoints that expose OpenAI-compatible API. As of now, it only exports a single class, `LLMChat`, to encapsulate all the interaction. diff --git a/deck/text.py b/deck/text.py new file mode 100644 index 0000000..5264548 --- /dev/null +++ b/deck/text.py @@ -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: [''], + _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 = [] diff --git a/decktext-spec.md b/decktext-spec.md new file mode 100644 index 0000000..f06d4c6 --- /dev/null +++ b/decktext-spec.md @@ -0,0 +1,121 @@ +# DeckText specification and manual + +DeckText is a strictly defined subset of HTML (Hypertext Markup Language) aimed at simple, lightweight, text-only document authoring with basic formatting and page-oriented structure for consumption on low-powered portable devices with line-oriented UI, while being backwards-compatible with any existing Web browser implementation. + +## Rationale + +Rendering full HTML is a very compute-intensive task, especially for the devices that do not need all the features. A subset that would allow conveying all required text-only information, keeping the parser as simple as possible (while still being backwards-compatible with the original HTML specification), is a necessity for such devices without having to switch to alternative "small Web" protocols such as Gopher, Gemini, Nex or Spartan. + +## General definitions + +A **DeckText site** is a simple HTML document that conforms to this specification. Although it is allowed to have multiple HTML documents, it's recommended for the entire site to be served as a single-file HTTP(S) download. + +A DeckText site consists of: + +- document title, +- DeckText beginning and end markers, +- page separators, +- page contents consisting of a strict subset of HTML tags. + +A **DeckText-only client** (henceforth DTOC) is a software application that allows its users to browse DeckText sites without needing to implement a full HTML rendering engine. + +DTOCs MUST support UTF-8 encoding inside the documents and render them accordingly. Particular Unicode character set availability outside ASCII range is not required as it depends on the fonts installed on the target device. + +## Beginning and end markers + +To mark the beginning and the end of a DeckText-compliant document fragment within an HTML page, the following case-insensitive markers are used: + +- `` marks the beginning of DeckText, +- `` marks the end of DeckText. + +A single HTML page MAY contain several begin-end marker pairs, and a DTOC MUST read through all these sections and compile the document to display from all of them. + +Between the begin-end markers, only text content is allowed. + +## Title + +Just like any HTML document, a valid DeckText document MUST contain a single `` tag. A DTOC MUST read and display its contents to the user as a document/site title. The tag MUST NOT have any attributes and MAY be placed outside any other HTML element. + +This tag is the only one that is not required to be placed between the DeckText start and end markers in order to be read by a DTOC. + +## Pages + +Every DeckText document is split into logical pages, which may or may not match physical pages on the target device. + +The horizontal rule element (both `
    ` and `
    ` forms are allowed) is solely defined as the **page separator**. DTOCs MUST split the document by this separator and offer the user to switch between pages based on it. + +Any content between the first DeckText start marker and the first page separator forms the first page of the document. + +It is recommended for the first page of the document to contain its overall description and a table of contents with the corresponding page numbers. + +## Page content + +When displaying page content, the following rules apply: + +1. All opening and closing tags not supported by the client are ignored. +2. Whitespace IS NOT collapsed. +3. All newlines ARE preserved. +4. Lines longer than the viewport width MUST wrap. +5. Display font SHOULD be monospace, unless the target platform does not support it. + +## Supported HTML tags + +DTOCs MUST implement these tags: + +- `` to mark the beginning of DeckText-compliant document fragments +- `` to mark the end of DeckText-compliant document fragments +- `` for document title display +- `
    `, `
    `, `
    ` as a page separator +- `Link name` for defining hyperlinks (see "Link processing" section for details) +- ``,`` for bold text +- ``,`` for italic text +- ``,``, `
    `, `` for code text
    +- ``,`` for strikethrough text
    +- ``, `
    ` for quoted text +- `
  • ` for list items (all lists are considered unordered) + +Nesting any of these elements is NOT supported in DeckText. In case such nesting occurs, a DTOC MAY display the inner element contents verbatim, process it correctly or throw a parsing error, depending on the implementation. + +DTOCs MUST NOT implement attribute support for any tag except ``, and only the `href="..."` attribute is required to be supported. + +Tag support is case-insensitive but mixed case IS NOT supported. I.e. within a single opening/closing tag, all letters MUST be either uppercase or lowercase: `` and `<title>` are supported but `<Title>` is not. + +## Link processing + +Whenever an `<a>` tag is encountered within the DeckText block, a DTOC MUST do the following: + +1. Look for the matching `</a>` tag and record the link label. +2. Look for the `href=""` attribute within the opening `<a>` tag and extract the link URL. The URL inside the `href` attribute MUST be wrapped in double quotes (no other quotes allowed). +3. Provide a clear UI for the user to visit the URL when selecting the link label. + +A DTOC also SHOULD implement anchored page URLs in the form of `#{page_number}` links, e.g. `<a href="#5">Page 5</a>`, for easy navigation within the DeckText site. This is not mandatory because a DeckText client itself should provide a convenient UI for navigating by page numbers. + +## DeckText compatibility with "traditional" Web browsers + +In order for your DeckText documents to display normally in both DTOCs and full-featured Web browsers according to this specification, the following HTML template is recommended: + +```html +<!DOCTYPE html> +<html><head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,height=device-height,user-scalable=no,initial-scale=1.0"/> + <title>Your Document Title + + + + + + +``` + +Keep in mind that any valid DeckText markup is also valid HTML markup, but not vice versa. Make sure you adhere to the rules provided in this specification. + +## Credits + +The original version of this specification was created by [Luxferre](https://luxferre.top) and published in December 2025. + +The specification and reference DTOC implementations are released into public domain with no warranties.