added decktext parser and spec
This commit is contained in:
@@ -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 `<title>` 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.
|
||||
|
||||
+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 = []
|
||||
@@ -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:
|
||||
|
||||
- `<!-- decktext_begin -->` marks the beginning of DeckText,
|
||||
- `<!-- decktext_end -->` 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 `<title></title>` 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 `<hr>` and `<hr />` 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:
|
||||
|
||||
- `<!-- decktext_begin -->` to mark the beginning of DeckText-compliant document fragments
|
||||
- `<!-- decktext_end -->` to mark the end of DeckText-compliant document fragments
|
||||
- `<title></title>` for document title display
|
||||
- `<hr>`, `<hr/>`, `<hr />` as a page separator
|
||||
- `<a href="(link)">Link name</a>` for defining hyperlinks (see "Link processing" section for details)
|
||||
- `<b></b>`,`<strong></strong>` for bold text
|
||||
- `<i></i>`,`<em></em>` for italic text
|
||||
- `<tt></tt>`,`<code></code>`, `<pre></pre>`, `<xmp></xmp>` for code text
|
||||
- `<s></s>`,`<del></del>` for strikethrough text
|
||||
- `<q></q>`, `<blockquote></blockquote>` for quoted text
|
||||
- `<li></li>` 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 `<a>`, 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: `<TITLE>` 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</title>
|
||||
<style>
|
||||
*{font-family:monospace;font-variant-ligatures:normal;white-space:pre-wrap;tab-size:4}
|
||||
hr{margin: 1em 0}
|
||||
/* ... other desired CSS styles ... */
|
||||
</style>
|
||||
</head><body>
|
||||
<!-- decktext_begin -->
|
||||
<!-- ... your valid DeckText markup here ... -->
|
||||
<!-- decktext_end -->
|
||||
</body></html>
|
||||
```
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user