| title | Shuttle: An S-expression Based Markup Language |
|---|---|
| author | Carson Gross (carson.gross@montana.edu) |
| created | February 14, 2026 |
| updated | February 14, 2026 |
| status | Living Document |
This section is non-normative.
HTML uses an SGML-derived derived angle-bracket syntax that is verbose and token-heavy. For contexts where compactness matters, such as LLM token budgets, a more concise notation is desirable.
The Shuttle Hypertext Markup Language (SHML) provides such a notation by adopting S-expressions as its surface syntax. Every Shuttle expression maps unambiguously to an HTML element, preserving full fidelity: tag names, attributes, text content, and nesting are all represented.
A Shuttle document:
(div class=container
(h1 Welcome)
(p This is Shuttle.))Produces the following HTML:
<div class="container">
<h1>Welcome</h1>
<p>This is Shuttle.</p>
</div>For the gpt-4o tokenizer the above HTML creates 27 tokens, whereas the SHML produces 19 tokens, a 29.6% reduction in token count.
Shuttle is designed with the following goals:
- Minimal syntax: the fewest possible metacharacters and rules.
- Unambiguous parsing: every valid input has exactly one parse tree.
- Lossless mapping: round-trip fidelity to HTML serialization.
- Human readability: natural to read and write for anyone familiar with HTML.
This specification defines the surface syntax of the Shuttle language and its mapping to HTML serialization. It does not define a Document Object Model, rendering behavior, or processing model beyond parsing and serialization.
The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
The following terms are used throughout this specification:
Shuttle document : A Unicode string conforming to the grammar defined in this specification.
Element : A tagged S-expression consisting of a tag name, zero or more properties, and zero or more content items.
Tag name : A name identifying the type of an element.
Property : A name-value pair associated with an element, corresponding to an HTML attribute.
Property name : A name identifying a property.
Property value : A string value associated with a property name.
Boolean property
: A property with no value, indicated by a trailing = with nothing
following before the next whitespace or ).
Empty string property
: A property whose value is the empty string, indicated by ="".
Content item : A text string or child element within an element, following the properties.
Text content : A run of characters within the content of an element that is not a nested element.
Shuttle processor : A conforming implementation that parses Shuttle documents and produces serialized output.
Parse error : A condition where the input does not conform to the grammar defined in this specification.
Whitespace : Any of the following Unicode code points: U+0020 SPACE, U+0009 TAB, U+000A LINE FEED, U+000D CARRIAGE RETURN.
Void element
: An HTML element that cannot have content. The void elements are: area,
base, br, col, embed, hr, img, input, link, meta,
source, track, wbr.
A Shuttle document is a sequence of Unicode code points.
Whitespace characters serve as delimiters between tokens. The recognized whitespace characters are U+0020 SPACE, U+0009 TAB, U+000A LINE FEED, and U+000D CARRIAGE RETURN.
The following ABNF grammar (RFC 5234) defines the syntax of a Shuttle document.
document = *( WS / comment / element / text-content )
element = "(" *WS tag-name *( 1*WS property ) content ")"
tag-name = name-start-char *name-char
property = property-name "=" property-value
/ property-name "=" DQUOTE DQUOTE ; empty string
/ property-name "=" ; boolean
property-name = name-start-char *name-char
property-value = quoted-value / unquoted-value
quoted-value = DQUOTE *quoted-char DQUOTE
quoted-char = unescaped-quoted / entity-reference
unescaped-quoted = %x20-21 / %x23-25 / %x27-7E
; visible ASCII + space, excluding " and &
unquoted-value = 1*unquoted-char
unquoted-char = %x21-27 / %x2A-3B / %x3D / %x3F-7E
; visible ASCII excluding ( ) SPACE &
content = *( 1*WS ( element / text-segment ) )
text-segment = 1*( text-char / entity-reference / element )
text-char = %x21-25 / %x27 / %x2A-3B / %x3D-7E / WS
; any printable char or whitespace, excluding ( ) &
entity-reference = "&" 1*( ALPHA / DIGIT / "#" ) ";"
comment = "(!" comment-text "!)"
comment-text = *comment-char
comment-char = %x00-20 / %x22-10FFFF
; any character except "!", or
/ %x21 %x00-28 / %x21 %x2A-10FFFF
; "!" not followed by ")"
name-start-char = ALPHA / "_"
name-char = ALPHA / DIGIT / "_" / "-" / "." / ":"
WS = %x20 / %x09 / %x0A / %x0DNote: The comment-char production informally means "any character sequence
that does not contain !)". Comments do not nest, mirroring the behavior of
HTML comments.
An element is an S-expression delimited by parentheses. It begins with a tag name, followed by zero or more properties, followed by zero or more content items.
To parse an element from an input stream:
- Assert: the current input character is U+0028 LEFT PARENTHESIS.
- Consume the U+0028 LEFT PARENTHESIS.
- Skip any whitespace.
- Let tagName be the result of consuming a name.
- If tagName is empty, this is a parse error; return failure.
- Let properties be an empty ordered list.
- Let children be an empty ordered list.
- In a loop:
- Skip any whitespace.
- If the current input character is U+0029 RIGHT PARENTHESIS, consume it and return a new element with tag name tagName, properties properties, and content children.
- If the current input character is U+0028 LEFT PARENTHESIS, let child be the result of parsing an element. Append child to children.
- Otherwise, if children is empty and the next tokens match the
propertyproduction, let prop be the result of parsing a property. Append prop to properties. - Otherwise, let text be the result of consuming text content. Append text to children.
To consume a name from an input stream:
- Let name be the empty string.
- If the current input character is not a
name-start-char, return name. - Append the current input character to name and advance.
- While the current input character is a
name-char, append it to name and advance. - Return name.
A tag name identifies the type of an element. Tag names begin with a letter
(A-Z, a-z) or underscore (_), followed by zero or more letters, digits
(0-9), underscores, hyphens (-), dots (.), or colons (:).
As a non-normative convenience, the pattern can be expressed as the regular
expression [a-zA-Z_][a-zA-Z0-9_\-.:]*.
Shuttle does not restrict which tag names are valid. Any conforming name is accepted. The interpretation of tag names (e.g., mapping to known HTML elements) is the responsibility of the consuming application.
Properties appear after the tag name and before any content items within an
element. Each property consists of a property name, the = character, and
optionally a property value.
Three forms of properties are defined:
A property name followed by = and a value. The value may be quoted or
unquoted.
Unquoted values terminate at the next whitespace, (, or ).
Quoted values are delimited by double quotes (") and may contain spaces,
parentheses, and other characters that would otherwise be significant. Entity
references are expanded within quoted values.
(a href=/home Home)
(a href="https://example.com/path?q=1&r=2" Link)A property name followed by = with no value before the next whitespace or
). In HTML serialization, this produces a boolean attribute.
(input type=checkbox checked=)Produces:
<input type="checkbox" checked>A property name followed by ="". This is distinct from a boolean property:
the serialized output includes an explicit empty value.
(input value="")Produces:
<input value="">To parse a property from an input stream:
- Let name be the result of consuming a name.
- Assert: the current input character is U+003D EQUALS SIGN.
- Consume the U+003D EQUALS SIGN.
- If the current input character is U+0022 QUOTATION MARK:
- Consume the U+0022 QUOTATION MARK.
- Let value be the empty string.
- While the current input character is not U+0022 QUOTATION MARK:
- If the current input character is U+0026 AMPERSAND, let ref be the result of consuming an entity reference. Append ref to value.
- Otherwise, append the current input character to value and advance.
- Consume the closing U+0022 QUOTATION MARK.
- Return a new property with property name name and property value value.
- If the current input character is whitespace or U+0029 RIGHT PARENTHESIS, return a new boolean property with property name name.
- Otherwise, let value be the empty string. While the current input
character is not whitespace, U+0028 LEFT PARENTHESIS, or U+0029 RIGHT
PARENTHESIS:
- Append the current input character to value and advance.
- Return a new property with property name name and property value value.
Content items comprise all tokens after the last property within an element, up
to the matching ). Content may consist of interleaved text content and nested
elements.
To consume text content from an input stream:
- Let text be the empty string.
- While the current input character is not U+0028 LEFT PARENTHESIS, U+0029
RIGHT PARENTHESIS, or end of input:
- If the current input character is U+0026 AMPERSAND, let ref be the result of consuming an entity reference. Append ref to text.
- Otherwise, append the current input character to text and advance.
- Return text.
A token is parsed as a property if and only if:
- No content items have yet been encountered in the current element, AND
- The token matches the
propertyproduction (i.e., it has the formName=...).
Once any non-property content is encountered --- whether text content or a
nested element --- all subsequent tokens are treated as content, even if they
syntactically match the property production.
Authors MUST use
=to escape the equals sign when text content at the beginning of an element's content area matches the property production. Failure to do so will cause the text to be interpreted as a property.
Without escaping, x=5 would be parsed as a property:
(! WRONG: x is parsed as a property with value "5" !)
(p x=5 is the solution)
(! CORRECT: = prevents property parsing !)
(p x=5 is the solution)The correct form produces:
<p>x=5 is the solution</p>Shuttle supports HTML named character references and numeric character references within text content and quoted property values.
The syntax for entity references is:
- Named:
&name;where name is a recognized HTML character reference name. - Numeric (decimal):
&#digits;where digits is one or more ASCII digits. - Numeric (hexadecimal):
&#xhexdigits;where hexdigits is one or more hexadecimal digits.
The complete set of recognized named references is that defined by the HTML Standard.
To consume an entity reference from an input stream:
- Assert: the current input character is U+0026 AMPERSAND.
- Consume the U+0026 AMPERSAND.
- Let ref be the empty string.
- While the current input character is not U+003B SEMICOLON and is not
whitespace:
- Append the current input character to ref and advance.
- If the current input character is U+003B SEMICOLON, consume it.
- Resolve ref to the corresponding Unicode code point(s) per the HTML Standard's named character references table, or as a numeric reference.
- If ref cannot be resolved, this is a parse error. Return the literal
string
&concatenated with ref concatenated with;. - Return the resolved character(s).
The following characters MUST be escaped in certain contexts:
| Context | Character | Escape | Reason |
|---|---|---|---|
| Start of content area | = preceded by a valid name |
= |
Would be parsed as a property |
| Text content | ( |
( |
Would open a child element |
| Text content | ) |
) |
Would close the current element |
| Text content | & |
& |
Would start an entity reference |
| Quoted property value | " |
" |
Would close the quoted value |
An unescaped U+0028 LEFT PARENTHESIS in text content is interpreted as the opening of a child element. If the content following the parenthesis does not form a valid element, this is a parse error.
Authors SHOULD escape literal parentheses in content using ( and
).
This section defines how a parsed Shuttle document is serialized to HTML.
To serialize to HTML given an element el:
- Let output be the empty string.
- Append
<to output. - Append el's tag name to output.
- For each prop in el's properties:
- Append U+0020 SPACE to output.
- Append prop's property name to output.
- If prop is a boolean property, do nothing further for this property.
- Otherwise:
- Append
="to output. - Append prop's property value, with
",&,<,>replaced by their character reference forms, to output. - Append
"to output.
- Append
- Append
>to output. - For each child in el's content:
- If child is an element, append the result of serializing child to HTML to output.
- If child is text content, append child with
&,<,>replaced by their character reference forms to output.
- If el's tag name is not a void element name:
- Append
</to output. - Append el's tag name to output.
- Append
>to output.
- Append
- Return output.
When serializing to HTML, the following elements MUST NOT have a closing tag:
area, base, br, col, embed, hr, img, input, link, meta,
source, track, wbr.
If a void element has content items in the Shuttle source, a conforming Shuttle processor MUST emit a parse error.
In the serialized output, property values are ALWAYS quoted with double quotes, regardless of whether the Shuttle source used quoted or unquoted values.
A comment begins with the two-character sequence (! and ends with the
two-character sequence !). Everything between these delimiters is ignored
during parsing and produces no output.
Comments may span multiple lines. Comments do not nest: the first !)
encountered always closes the comment, mirroring the behavior of HTML comments.
Comments may appear anywhere that whitespace is allowed.
comment = "(!" comment-text "!)"
comment-text = *comment-char
comment-char = ; any character except the sequence "!)"Example:
(! This is a comment !)
(div class=main
(! Navigation section !)
(nav
(a href=/ Home) (!inline comment !)
(a href=/about About))
(p Content here))Multiline comment:
(!
This entire block is a comment.
It can span multiple lines.
!)
(p Hello)The treatment of whitespace depends on its position within the structure:
- Between
(and the tag name --- allowed and consumed. Not included in output. - Between properties --- serves as a delimiter and is consumed. Not included in output.
- Between the last property (or tag name) and the first content item --- serves as a delimiter and is consumed. Not included in output.
- Within text content --- preserved verbatim in the output. This includes spaces, tabs, and newlines.
- Before the closing
)--- if content exists, trailing whitespace is part of the text content and is preserved. - Between sibling elements within content --- preserved as text content.
Shuttle preserves whitespace within content exactly as written. It does not perform whitespace normalization. Any whitespace collapsing is the responsibility of the consuming application (e.g., an HTML renderer).
The following conditions constitute a parse error:
- Unmatched parenthesis --- a U+0028 LEFT PARENTHESIS without a matching U+0029 RIGHT PARENTHESIS, or vice versa.
- Empty element ---
()with no tag name. - Invalid tag name --- a tag name that does not conform to the name production.
- Invalid property name --- a property name that does not conform to the name production.
- Unterminated quoted value --- a U+0022 QUOTATION MARK that is not closed before the end of the element or end of input.
- Content in void element --- when targeting HTML serialization, content within a void element.
- Invalid entity reference --- a U+0026 AMPERSAND followed by a sequence that does not resolve to a known character reference.
- Unterminated comment --- the sequence
(!without a matching!)before end of input.
A conforming Shuttle processor MUST report all parse errors.
A conforming Shuttle processor MAY attempt error recovery. When error recovery is attempted, the behavior is implementation-defined.
The following recovery strategies are non-normative suggestions:
- Unmatched
(--- treat remaining input as text content of the nearest open element. - Empty element
()--- skip the expression. - Invalid entity reference --- pass through as literal text.
- Unterminated quoted value --- close the value at the next
)or end of input. - Unterminated comment --- treat everything from
(!to end of input as a comment.
Shuttle is a syntactic notation and does not define execution semantics. However, since Shuttle output is HTML, all security considerations that apply to HTML processing apply to the output of a Shuttle processor.
This section is non-normative.
(p Hello World)<p>Hello World</p>(a href=/home class=nav Home)<a href="/home" class="nav">Home</a>(input type=checkbox checked=)<input type="checkbox" checked>(input value="")<input value="">(div class=container
(h1 Title)
(p First paragraph)
(p Second paragraph))<div class="container">
<h1>Title</h1>
<p>First paragraph</p>
<p>Second paragraph</p>
</div>(p x=5 is the solution)<p>x=5 is the solution</p>(div class="my container" Multiple classes)<div class="my container">Multiple classes</div>(! Page header !)
(header
(h1 Site Title) (!main heading !)
(nav
(!primary navigation !)
(a href=/ Home)
(a href=/about About)))<header>
<h1>Site Title</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>(html lang=en
(head
(meta charset=utf-8)
(meta name=viewport content="width=device-width, initial-scale=1")
(title My Page)
(link rel=stylesheet href=style.css))
(body
(header
(h1 Welcome to My Page))
(main
(article
(h2 Introduction)
(p This is a complete HTML page written in Shuttle.)
(p It demonstrates how Shuttle maps naturally to HTML structure.)))
(footer
(p © 2026 Example Corp))))<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Page</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>Welcome to My Page</h1>
</header>
<main>
<article>
<h2>Introduction</h2>
<p>This is a complete HTML page written in Shuttle.</p>
<p>It demonstrates how Shuttle maps naturally to HTML structure.</p>
</article>
</main>
<footer>
<p>© 2026 Example Corp</p>
</footer>
</body>
</html>| Pattern | Shuttle | HTML |
|---|---|---|
| Text element | (p Hello) |
<p>Hello</p> |
| Attribute | (a href=/ Home) |
<a href="/">Home</a> |
| Boolean attr | (input disabled=) |
<input disabled> |
| Empty attr | (input value="") |
<input value=""> |
| Void element | (br) |
<br> |
| Nesting | (ul (li A) (li B)) |
<ul><li>A</li><li>B</li></ul> |
| Comment | (! note !) |
<!-- note --> |