Download native/spec/02-syntax.md from EHS-Studio/dssl: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/EHS-Studio/dssl/resolve/main/native/spec/02-syntax.md
- Command line
-
hf download hf://EHS-Studio/dssl/native/spec/02-syntax.md
-
curl -L -o 02-syntax.md https://huggingface.co/EHS-Studio/dssl/resolve/main/native/spec/02-syntax.md
2. Syntax
This chapter turns source text into a syntax tree. It needs no tokens, no document and no vocabulary except the lists in the appendices.
2.1 Source text
- SYN-1. Source is a sequence of Unicode characters. Line numbers start at 1 and count
\ncharacters. A parser that reads bytes MUST decode UTF-8 first.\ris not special: a\r\nfile leaves a\rat the end of each line, and values are trimmed, so it does no harm. - SYN-2. Every diagnostic carries the file name and a 1-based line number. For a construct that spans several lines, the line is the one where the construct started.
2.2 Comments
There are two comment forms: // to the end of the line and /* a block */.
- SYN-3. Comments are removed before anything else is parsed, in one pass over the whole source, left to right.
- SYN-4. A quote character (
"or') that is not inside a comment starts a string. A string ends at the next occurrence of the same quote character that is not directly preceded by a backslash. Comment markers inside a string are plain text. - SYN-5. A line comment runs to, but not including, the next
\n. A block comment runs to the first*/, or to the end of the source if there is none. Inside a block comment, every character except\nis replaced by a space and every\nis kept. Removing a comment therefore never changes the line number of anything after it. - SYN-6. An unquoted
url(...)is one token for the comment pass. When the lettersurl((any case) are not directly preceded by a letter, digit,_or-, and the first character after the(and any white space is not a quote, the text through the closing)is kept exactly as written, newlines included, so a//inside it (url(https://x.com/a.png)) is not a comment. Aurl(with no closing)is not special. A quotedurl("...")is covered by SYN-4. (Before 0.16.3 the engine treated that//as a comment, see D-1.)
Comments are removed from everything else, including the body of raw { }.
// a line comment
.card {
padding: 16 /* a block comment, even in the middle of a line */
}
2.3 Logical lines
The comment-free source is split on \n into physical lines. Physical lines are joined into
logical chunks, and each chunk is processed by 2.4.
- SYN-7. A blank physical line is skipped. (Inside a
raw { }capture it is kept. See 2.7.) - SYN-8. A chunk is incomplete when any of these holds:
- its parentheses or square brackets are unbalanced (more opening than closing, counting outside quoted strings), or
- after trimming trailing white space, it ends in
,, or - after trimming trailing white space, it ends in
:.
- SYN-9. While the buffer is incomplete, the next non-blank physical line is trimmed and appended to the buffer with one space between them. When the buffer becomes complete, it is processed as one chunk, with the line number of the first physical line. A chunk that is incomplete at the end of the file is processed as it stands.
Why: this is how multi-line values, multi-line selector lists and prop: followed by the
value on the next line all work with no semicolons.
.card,
.panel {
background: linear-gradient(
to bottom, red, blue
)
color:
color.ink
}
2.4 Processing a chunk
A chunk is a string. The parser keeps a stack of open blocks. The root is always at the bottom of the stack. It repeats the following on the chunk until nothing is left:
- Trim the chunk. If it is empty, stop.
- Find the first top-level
{and the first top-level}. "Top-level" means not inside a quoted string and not inside parentheses or square brackets. - If there is a
}and either there is no{or the}comes first:- treat the text before it as declarations (2.6) if it is not empty,
- if the stack holds only the root, report
unexpected "}"(SYN-10); otherwise pop one block, - continue with the text after the
}.
- Else if there is a
{: the text before it is the header text.- SYN-11. If the header text contains a top-level
;, everything before the last such;is treated as declarations (2.6) and the header is the text after it. This is what lets.card { background: red; hover { background: blue } }work on one line. If the remaining header is empty, reportblock is missing a header before "{"and skip the{. - If the header text is empty, report the same error and skip the
{. - Otherwise classify the header (2.5), append a node to the block on top of the stack, and run the block-open rule (2.7). If the block is not closed on this line, push it.
- SYN-11. If the header text contains a top-level
- Else the whole chunk is declarations (2.6). Stop.
A node always carries the line number of its chunk.
2.5 Classifying a header
The header is trimmed. The first row that matches wins.
| Header | Node kind | Notes |
|---|---|---|
exactly tokens |
tokens |
4.1 |
exactly pin |
pin |
the must-win layer, 5.2 |
exactly raw or @css |
raw |
verbatim CSS, 2.7 |
exactly fonts |
fonts |
8.7 |
starts with behavior |
behavior, name = rest, trimmed |
6.9 |
starts with motion |
motion, name = rest, trimmed |
chapter 7 |
starts with @layer |
layer, name = rest, trimmed |
5.2 |
starts with @media |
media, query = rest, trimmed |
5.5 |
starts with @container |
container, query = rest after the 10 characters, trimmed |
5.6 |
starts with @signal |
signal, expr = rest, trimmed |
5.9 |
starts with any other @ |
bogus |
error unknown at-rule "<first word>", the whole block is skipped (2.7) |
starts with :: |
pseudo, name = text after :: up to the first space or ( |
5.8. If the name is not a pseudo-element name and not a vendor pseudo, error unknown pseudo-element "::name" and the block is skipped |
| exactly one of the state words | state, name = the word |
5.7 |
| anything else | selector, sel = the header |
a CSS selector list |
The state words, the pseudo-element names and the vendor-pseudo pattern are in appendix A.
- SYN-12. Matching of
tokens,pin,raw,@cssandfontsis on the whole trimmed header.tokens foo {is a selector.@media(width>1)(no space) is an unknown at-rule.@containeris the one at-rule that does not require a space.
Unknown at-rules are skipped whole, so nothing inside them leaks out. For verbatim CSS the
author writes raw { }.
raw {
@supports (display: grid) {
.box { display: grid; }
}
}
2.6 Declarations
A declaration text is split on top-level ;. Inside a tokens block (the block on top of
the stack, or any block below it is a tokens block), each piece is further split by the
token-pair rule (2.8). Each non-empty trimmed piece is one declaration:
- SYN-13. Find the first top-level
:in the piece. - SYN-14. If there is none:
use <name>where<name>matches[A-Za-z][\w-]*is a declaration with propertyuseand value<name>.- A bare word matching
[a-z][a-z0-9-]*(case-insensitive) is a declaration with that word lowercased as the property and an empty value. (This is howtruncateandvisually-hiddenare written.) - Anything else is the error
expected ":" in "<first 40 characters>"with the hintdeclarations look like prop: value.
- SYN-15. The property is the text before the colon, trimmed and lowercased. The value is
the text after it, trimmed, with its case kept. An empty property is the error
declaration is missing a property name. - SYN-16. A property that is all digits with an optional fraction (
9,2.5) is legal only when some block on the stack is atokensblock. Elsewhere it is the error"<prop>" is not a valid property name, with a hint about token keys. - SYN-17. Any other property MUST match
^(-{1,2}[a-z][a-z0-9.-]*|[a-z][a-z0-9-]*)$(case-insensitive), otherwise the error"<prop>" is not a valid property nameis reported and the declaration is dropped. Dotted names are legal only after a leading--(--color.accent).
A valid declaration becomes a decl node { prop, value, line } appended to the block on
top of the stack.
2.7 Block-open rule
After a header is classified and its node is appended:
rawandfonts.- SYN-18. They are legal only at the top level (the stack holds only the root).
Otherwise report
raw { } must be top-level(orfonts { } ...) with the hintmove it out of the enclosing block, and skip the balanced block. - SYN-19. The body is captured verbatim into
node.css. If the matching}is on the same line as the header, the body is the text between the braces. Otherwise capture continues on following physical lines: each one is appended followed by\n, blank lines included, until the braces balance. The first line contributes only the text after the{. Brace matching ignores quoted strings. Text after the closing}is processed as a new chunk. If the file ends first, reportthe raw { } block was never closed with "}"and the CSS captured so far is discarded.
- SYN-18. They are legal only at the top level (the stack holds only the root).
Otherwise report
bogus.- SYN-20. The block is skipped: the parser finds the matching
}(quote-aware) and continues after it. If there is none, reportthe "<first 30 characters>" block was never closed with "}"and discard the rest of the chunk.
- SYN-20. The block is skipped: the parser finds the matching
- Everything else. The node is pushed on the stack.
At the end of the file, if more than the root is on the stack, report
<n> block(s) were never closed with "}".
2.8 Token pairs
- SYN-21. Inside a
tokensblock,pill: 999 card: 22on one line is two declarations. A piece is split at a space when all of these hold: a top-level:(not part of::) has already been seen in the piece, the space is outside strings, parentheses and brackets, and the text after the space looks likename:wherenamematches[A-Za-z0-9][\w.-]*followed by optional spaces and a:that is not::.
2.9 The syntax tree
root { kind: 'root', children: [...] }
node { kind, line, children: [...], ...extra }
decl { kind: 'decl', prop, value, line }
Extra fields by kind: behavior and motion have name, layer has name, media and
container have query, signal has expr, selector has sel, state and pseudo
have name, raw and fonts have css. Child order is source order. The tree is data: a
Level 1 parser is tested by comparing its tree and its errors with the reference.