From 905953b7f9d68c1f70532d664d3f1e2dfbc3b4de Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 29 Jul 2024 01:50:51 -0400 Subject: [PATCH] Progress on stylesheets --- go.mod | 3 +- go.sum | 2 + internal/style/styleconcept.tss | 18 +- internal/style/tss/lex.go | 359 ++++++++++++++++++++++++++++++++ internal/style/tss/lex_test.go | 66 ++++++ internal/style/tss/parse.go | 282 +++++++++++++++++++++++++ internal/style/tss/tss.go | 21 ++ 7 files changed, 741 insertions(+), 10 deletions(-) create mode 100644 internal/style/tss/lex.go create mode 100644 internal/style/tss/lex_test.go create mode 100644 internal/style/tss/parse.go create mode 100644 internal/style/tss/tss.go diff --git a/go.mod b/go.mod index 4f68c01..d27f30d 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module git.tebibyte.media/tomo/nasin -go 1.20 +go 1.22.2 require ( + git.tebibyte.media/sashakoshka/goparse v0.2.0 git.tebibyte.media/tomo/backend v0.5.1 git.tebibyte.media/tomo/objects v0.20.1 git.tebibyte.media/tomo/tomo v0.41.1 diff --git a/go.sum b/go.sum index 647f09f..e021805 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +git.tebibyte.media/sashakoshka/goparse v0.2.0 h1:uQmKvOCV2AOlCHEDjg9uclZCXQZzq2PxaXfZ1aIMiQI= +git.tebibyte.media/sashakoshka/goparse v0.2.0/go.mod h1:tSQwfuD+EujRoKr6Y1oaRy74ZynatzkRLxjE3sbpCmk= git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q= git.tebibyte.media/tomo/backend v0.5.1 h1:u3DLVcNWNdQsIxAEGcZ+kGG7zuew8tpPbyEFBx8ehjM= git.tebibyte.media/tomo/backend v0.5.1/go.mod h1:urnfu+D56Q9AOCZ/qp5YqkmlRRTIB5p9RbzBN7yIibQ= diff --git a/internal/style/styleconcept.tss b/internal/style/styleconcept.tss index a039b48..c235b2d 100644 --- a/internal/style/styleconcept.tss +++ b/internal/style/styleconcept.tss @@ -1,21 +1,21 @@ -$colorBlack = #000000FF -$borderOutline = $black / 1 +$colorBlack = #000000FF; +$borderOutline = $black 1; *.Slider { - Border: $borderOutline, $borderColorFocused / 1 - Color: $colorGutter - Padding: 0 1 1 0 + Border: $borderOutline, $borderColorFocused 1; + Color: $colorGutter; + Padding: 0 1 1 0; } *.Slider[focused] { - Border: $borderOutline - Padding: 0 + Border: $borderOutline; + Padding: 0; } *.Slider[horizontal] { - MinimumSize: 48 0 + MinimumSize: 48 0; } *.Slider[vertical] { - MinimumSize: 0 48 + MinimumSize: 0 48; } diff --git a/internal/style/tss/lex.go b/internal/style/tss/lex.go new file mode 100644 index 0000000..bb2ca64 --- /dev/null +++ b/internal/style/tss/lex.go @@ -0,0 +1,359 @@ +package tss + +import "io" +import "bufio" +import "unicode" +import "unicode/utf8" +import "git.tebibyte.media/sashakoshka/goparse" + +const ( + Comment parse.TokenKind = iota + LBrace + RBrace + LBracket + RBracket + Equals + Colon + Comma + Semicolon + Star + Dot + Dollar + Color + Ident + Number + String +) + +var tokenNames = map[parse.TokenKind] string { + parse.EOF: "EOF", + Comment: "Comment", + LBrace: "LBrace", + RBrace: "RBrace", + LBracket: "LBracket", + RBracket: "RBracket", + Equals: "Equals", + Colon: "Colon", + Comma: "Comma", + Semicolon: "Semicolon", + Star: "Star", + Dot: "Dot", + Dollar: "Dollar", + Color: "Color", + Ident: "Ident", + Number: "Number", + String: "String", +} + +type lexer struct { + filename string + lineScanner *bufio.Scanner + rune rune + line string + lineFood string + + offset int + row int + column int + + eof bool +} + +func Lex (filename string, reader io.Reader) parse.Lexer { + lex := &lexer { + filename: filename, + lineScanner: bufio.NewScanner(reader), + } + lex.nextRune() + return lex +} + +func (this *lexer) Next () (parse.Token, error) { + for { + token, err := this.next() + if err == io.EOF { return token, this.errUnexpectedEOF() } + if err != nil { return token, err } + + if !token.Is(Comment) { + return token, err + } + } +} + +func (this *lexer) next () (token parse.Token, err error) { + err = this.skipWhitespace() + token.Position = this.pos() + if this.eof { + token.Kind = parse.EOF + err = nil + return + } + if err != nil { return } + + appendRune := func () { + token.Value += string(this.rune) + err = this.nextRune() + } + + skipRune := func () { + err = this.nextRune() + } + + defer func () { + newPos := this.pos() + newPos.End -- + token.Position = token.Position.Union(newPos) + } () + + switch { + case this.rune == '/': + token.Kind = Comment + skipRune() + if err != nil { return } + if this.rune == '/' { + for this.rune != '\n' { + skipRune() + if err != nil { return } + } + } + if this.eof { err = nil; return } + + case this.rune == '{': + token.Kind = LBrace + appendRune() + if this.eof { err = nil; return } + + case this.rune == '}': + token.Kind = RBrace + appendRune() + if this.eof { err = nil; return } + + case this.rune == '[': + token.Kind = LBracket + appendRune() + if this.eof { err = nil; return } + + case this.rune == ']': + token.Kind = RBracket + appendRune() + if this.eof { err = nil; return } + + case this.rune == '=': + token.Kind = Equals + appendRune() + if this.eof { err = nil; return } + + case this.rune == ':': + token.Kind = Colon + appendRune() + if this.eof { err = nil; return } + + case this.rune == ',': + token.Kind = Comma + appendRune() + if this.eof { err = nil; return } + + case this.rune == ';': + token.Kind = Semicolon + appendRune() + if this.eof { err = nil; return } + + case this.rune == '*': + token.Kind = Star + appendRune() + if this.eof { err = nil; return } + + case this.rune == '.': + token.Kind = Dot + appendRune() + if this.eof { err = nil; return } + + case this.rune == '$': + token.Kind = Dollar + appendRune() + if this.eof { err = nil; return } + + case this.rune == '#': + token.Kind = Color + skipRune() + if err != nil { return } + for isHexDigit(this.rune) { + appendRune() + if this.eof { err = nil; return } + } + if this.eof { err = nil; return } + + case unicode.IsLetter(this.rune): + token.Kind = Ident + for unicode.IsLetter(this.rune) || unicode.IsNumber(this.rune) { + appendRune() + if this.eof { err = nil; return } + } + if this.eof { err = nil; return } + + case this.rune == '-': + token.Kind = Number + appendRune() + for isDigit(this.rune) { + appendRune() + if this.eof { err = nil; return } + } + if this.eof { err = nil; return } + + case isDigit(this.rune): + token.Kind = Number + for isDigit(this.rune) { + appendRune() + if this.eof { err = nil; return } + } + if this.eof { err = nil; return } + + case this.rune == '\'', this.rune == '"': + stringDelimiter := this.rune + token.Kind = String + err = this.nextRune() + if err != nil { return } + + for this.rune != stringDelimiter { + if this.rune == '\\' { + var result rune + result, err = this.escapeSequence(stringDelimiter) + if err != nil { return } + token.Value += string(result) + } else { + appendRune() + if this.eof { err = nil; return } + if err != nil { return } + } + } + err = this.nextRune() + if this.eof { err = nil; return } + if err != nil { return } + + default: + err = parse.Errorf ( + this.pos(), "unexpected rune %U", + this.rune) + } + + return +} + +func (this *lexer) nextRune () error { + if this.lineFood == "" { + ok := this.lineScanner.Scan() + if ok { + this.line = this.lineScanner.Text() + this.lineFood = this.line + this.rune = '\n' + this.column = 0 + this.row ++ + } else { + err := this.lineScanner.Err() + if err == nil { + this.eof = true + return io.EOF + } else { + return err + } + } + } else { + var ch rune + var size int + for ch == 0 && this.lineFood != "" { + ch, size = utf8.DecodeRuneInString(this.lineFood) + this.lineFood = this.lineFood[size:] + } + this.rune = ch + this.column ++ + } + + return nil +} + +func (this *lexer) escapeSequence (stringDelimiter rune) (rune, error) { + err := this.nextRune() + if err != nil { return 0, err } + + if isDigit(this.rune) { + var number rune + for index := 0; index < 3; index ++ { + if !isDigit(this.rune) { break } + + number *= 8 + number += this.rune - '0' + + err = this.nextRune() + if err != nil { return 0, err } + } + return number, nil + } + + defer this.nextRune() + switch this.rune { + case '\\', '\n', stringDelimiter: + return this.rune, nil + case 'a': return '\a', nil + case 'b': return '\b', nil + case 't': return '\t', nil + case 'n': return '\n', nil + case 'v': return '\v', nil + case 'f': return '\f', nil + case 'r': return '\r', nil + default: return 0, this.errBadEscapeSequence() + } +} + +func (this *lexer) skipWhitespace () error { + for isWhitespace(this.rune) { + err := this.nextRune() + if err != nil { return err } + } + return nil +} + +func (this *lexer) pos () parse.Position { + return parse.Position { + File: this.filename, + Line: this.lineScanner.Text(), + Row: this.row - 1, + Start: this.column - 1, + End: this.column, + } +} + +func (this *lexer) errUnexpectedEOF () error { + return parse.Errorf(this.pos(), "unexpected EOF") +} + +func (this *lexer) errBadEscapeSequence () error { + return parse.Errorf(this.pos(), "bad escape sequence") +} + +func isWhitespace (char rune) bool { + switch char { + case ' ', '\t', '\r', '\n': return true + default: return false + } +} + +func isSymbol (char rune) bool { + switch char { + case + '~', '!', '@', '#', '$', '%', '^', '&', '-', '_', '=', '+', + '\\', '|', ';', ',', '<', '>', '/', '?': + return true + default: + return false + } +} + +func isDigit (char rune) bool { + return char >= '0' && char <= '9' +} + +func isHexDigit (char rune) bool { + return isDigit(char) || + char >= 'a' && char <= 'f' || + char >= 'A' && char <= 'F' +} diff --git a/internal/style/tss/lex_test.go b/internal/style/tss/lex_test.go new file mode 100644 index 0000000..d9387c0 --- /dev/null +++ b/internal/style/tss/lex_test.go @@ -0,0 +1,66 @@ +package tss + +import "fmt" +import "strings" +import "testing" +import "git.tebibyte.media/sashakoshka/goparse" + +func TestLexSimple (test *testing.T) { +testString(test, +`hello #BABE {#Beef}, 384920 #0ab3fc840`, +tok(Ident, "hello"), +tok(Color, "BABE"), +tok(LBrace, "{"), +tok(Color, "Beef"), +tok(RBrace, "}"), +tok(Comma, ","), +tok(Number, "384920"), +tok(Color, "0ab3fc840"), +tok(parse.EOF, ""), +)} + +func testString (test *testing.T, input string, correct ...parse.Token) { + lexer := Lex("test.tss", strings.NewReader(input)) + index := 0 + for { + token, err := lexer.Next() + if err != nil { test.Fatalf("lexer returned error:\n%v", parse.Format(err)) } + if index >= len(correct) { + test.Logf("%d:\t%-16s | !", index, tokStr(token)) + test.Fatalf("index %d greater than %d", index, len(correct)) + } + correctToken := correct[index] + test.Logf ( + "%d:\t%-16s | %s", + index, + tokStr(token), + tokStr(correctToken)) + if correctToken.Kind != token.Kind || correctToken.Value != token.Value { + test.Fatalf("tokens at %d do not match up", index) + } + if token.Is(parse.EOF) { break } + index ++ + } + if index < len(correct) - 1 { + test.Fatalf("index %d less than %d", index, len(correct) - 1) + } +} + +func tokStr (token parse.Token) string { + name, ok := tokenNames[token.Kind] + if !ok { + name = fmt.Sprintf("Token(%d)", token.Kind) + } + if token.Value == "" { + return name + } else { + return fmt.Sprintf("%s:\"%s\"", name, token.Value) + } +} + +func tok (kind parse.TokenKind, value string) parse.Token { + return parse.Token { + Kind: kind, + Value: value, + } +} diff --git a/internal/style/tss/parse.go b/internal/style/tss/parse.go new file mode 100644 index 0000000..988cb8f --- /dev/null +++ b/internal/style/tss/parse.go @@ -0,0 +1,282 @@ +package tss + +import "io" +import "strconv" +import "git.tebibyte.media/sashakoshka/goparse" + +type Sheet struct { + Variables map[string] ValueList + Rules []Rule +} + +type Rule struct { + Selector Selector + Attrs map[string] []ValueList +} + +type Selector struct { + Package string + Object string + Tags []string +} + +type ValueList []Value + +type Value interface { + value () +} + +func (ValueNumber) value () { } +type ValueNumber int + +func (ValueColor) value () { } +type ValueColor uint32 + +func (ValueString) value () { } +type ValueString string + +func (ValueKeyword) value () { } +type ValueKeyword string + +func (ValueVariable) value () { } +type ValueVariable string + +type parser struct { + parse.Parser + sheet Sheet + lexer parse.Lexer +} + +func newParser (lexer parse.Lexer) *parser { + return &parser { + sheet: Sheet { + Variables: make(map[string] ValueList), + }, + Parser: parse.Parser { + Lexer: lexer, + TokenNames: tokenNames, + }, + } +} + +func Parse (lexer parse.Lexer) (Sheet, error) { + parser := newParser(lexer) + err := parser.parse() + if err == io.EOF { err = nil } + if err != nil { return Sheet { }, err } + return parser.sheet, nil +} + +func (this *parser) parse () error { + err := this.Next() + if err != nil { return err } + for this.Token.Kind != parse.EOF { + err = this.parseTopLevel() + if err != nil { return err } + } + return nil +} + +func (this *parser) parseTopLevel () error { + err := this.ExpectDesc("variable or rule", Dollar, Ident, Star) + if err != nil { return err } + if this.EOF() { return nil } + pos := this.Pos() + + switch this.Kind() { + case Dollar: + name, variable, err := this.parseVariable() + if err != nil { return err } + if _, exists := this.sheet.Variables[name]; exists { + return parse.Errorf(pos, "variable %s already declared", name) + } + this.sheet.Variables[name] = variable + + case Ident, Star: + rule, err := this.parseRule() + if err != nil { return err } + this.sheet.Rules = append(this.sheet.Rules, rule) + } + return nil +} + +func (this *parser) parseVariable () (string, ValueList, error) { + err := this.Expect(Dollar) + if err != nil { return "", nil, err } + err = this.ExpectNext(Ident) + if err != nil { return "", nil, err } + err = this.ExpectNext(Equals) + if err != nil { return "", nil, err } + name := this.Value() + this.Next() + values, err := this.parseValueList() + if err != nil { return "", nil, err } + err = this.Expect(Semicolon) + if err != nil { return "", nil, err } + return name, values, this.Next() +} + +func (this *parser) parseRule () (Rule, error) { + rule := Rule { + Attrs: make(map[string] []ValueList), + } + + selector, err := this.parseSelector() + if err != nil { return Rule { }, err } + rule.Selector = selector + err = this.Expect(LBracket) + if err != nil { return Rule { }, err } + + for { + pos := this.Pos() + name, attr, err := this.parseAttr() + if err != nil { break } + _, exists := rule.Attrs[name] + if !exists { + return Rule { }, parse.Errorf ( + pos, + "attribute %s already declared in this rule", + name) + } + rule.Attrs[name] = attr + } + + err = this.Expect(LBracket) + if err != nil { return Rule { }, err } + return rule, this.Next() +} + +func (this *parser) parseSelector () (Selector, error) { + selector := Selector { } + + // package + err := this.ExpectDesc("selector", Ident, Star) + if err != nil { return Selector { }, err } + if this.Is(Ident) { + selector.Package = this.Value() + } + + err = this.ExpectNext(Dot) + if err != nil { return Selector { }, err } + + // object + err = this.ExpectNext(Ident, Star) + if err != nil { return Selector { }, err } + if this.Is(Ident) { + selector.Object = this.Value() + } + + // tags + err = this.ExpectNext(LBrace) + if err == nil { + this.Next() + for { + err := this.Expect(Ident, String, RBrace) + if err != nil { return Selector { }, err } + if this.Is(RBrace) { break } + selector.Tags = append(selector.Tags, this.Value()) + err = this.ExpectNext(Comma, RBrace) + if err != nil { return Selector { }, err } + } + } + + return selector, this.Next() +} + +func (this *parser) parseAttr () (string, []ValueList, error) { + err := this.ExpectDesc("attr", Ident) + if err != nil { return "", nil, err } + name := this.Value() + + err = this.ExpectNext(Colon) + if err != nil { return "", nil, err } + + attr := []ValueList { } + this.Next() + for { + err := this.ExpectDesc ( + "value, comma, or semicolon", + Number, Color, String, Ident, Dollar, Comma, Semicolon) + if err != nil { return "", nil, err } + if this.Is(Semicolon) { break } + valueList, err := this.parseValueList() + if err != nil { return "", nil, err } + attr = append(attr, valueList) + err = this.ExpectNext(Comma, Semicolon) + if err != nil { return "", nil, err } + } + + return name, attr, this.Next() +} + +func (this *parser) parseValueList () (ValueList, error) { + list := ValueList { } + for { + err := this.ExpectDesc ( + "value", + Number, Color, String, Ident, Dollar) + if err != nil { break } + switch this.Kind() { + case Number: + number, err := strconv.Atoi(this.Value()) + if err != nil { return nil, err } + list = append(list, ValueNumber(number)) + case Color: + color, ok := parseColor([]rune(this.Value())) + if !ok { + return nil, parse.Errorf ( + this.Pos(), + "malformed color literal") + } + list = append(list, ValueColor(color)) + case String: + list = append(list, ValueString(this.Value())) + case Ident: + list = append(list, ValueKeyword(this.Value())) + case Dollar: + err := this.ExpectNext(Ident) + if err != nil { return nil, err } + list = append(list, ValueVariable(this.Value())) + } + } + return list, this.Next() +} + +func parseColor (runes []rune) (uint32, bool) { + digits := make([]uint32, len(runes)) + for index, run := range runes { + digit := hexDigit(run) + if digit < 0 { return 0, false } + digits[index] = uint32(digit) + } + switch len(runes) { + case 3: + return digits[0] << 28 | digits[0] << 24 | + digits[1] << 20 | digits[1] << 16 | + digits[2] << 12 | digits[2] << 8 | 0xFF, true + case 6: + return digits[0] << 28 | digits[1] << 24 | + digits[2] << 20 | digits[3] << 16 | + digits[4] << 12 | digits[5] << 8 | 0xFF, true + case 4: + return digits[0] << 28 | digits[0] << 24 | + digits[1] << 20 | digits[1] << 16 | + digits[2] << 12 | digits[2] << 8 | + digits[3] << 4 | digits[3] << 0, true + case 8: + return digits[0] << 28 | digits[1] << 24 | + digits[2] << 20 | digits[3] << 16 | + digits[4] << 12 | digits[5] << 8 | + digits[6] << 4 | digits[7] << 0, true + default: return 0, false + } +} + +func hexDigit (digit rune) int { + switch { + case digit >= '0' && digit <= '9': return int(digit - '0') + case digit >= 'a' && digit <= 'f': return int(digit - 'a') + 10 + case digit >= 'A' && digit <= 'F': return int(digit - 'A') + 10 + default: return -1 + } +} diff --git a/internal/style/tss/tss.go b/internal/style/tss/tss.go new file mode 100644 index 0000000..f60e84c --- /dev/null +++ b/internal/style/tss/tss.go @@ -0,0 +1,21 @@ +package tss + +import "io" +import "os" +import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/tomo/event" + +func BuildStyle (sheet Sheet) (*tomo.Style, event.Cookie, error) { + // TODO + return nil, nil, nil +} + +func LoadFile (name string) (*tomo.Style, event.Cookie, error) { + // TODO check cache for gobbed sheet. if the cache is nonexistent or + // invalid, then open/load/cache. + + file, err := os.Open(name) + if err != nil { return nil, nil, err } + defer file.Close() + return Load(file) +}