Renamed style directory to styles

This commit is contained in:
2024-08-12 17:12:43 -04:00
parent ecfb90957a
commit 4b53a5a019
17 changed files with 2 additions and 2 deletions

View File

@@ -0,0 +1,373 @@
package tss
import "fmt"
import "image"
import "errors"
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/backend/style"
type styleBuilder struct {
sheet Sheet
}
type faceKey struct {
name string
size int
}
// BuildStyle builds a Tomo style from the specified sheet. Resources associated
// with it (such as textures) can be freed by closing the returned cookie.
func BuildStyle (sheet Sheet) (*style.Style, event.Cookie, error) {
builder := &styleBuilder {
sheet: sheet,
}
return builder.build()
}
func (this *styleBuilder) build () (*style.Style, event.Cookie, error) {
err := this.sheet.Flatten()
if err != nil { return nil, nil, err }
getColor := func (name string) color.Color {
if list, ok := this.sheet.Variables[name]; ok {
if len(list) > 0 {
if col, ok := list[0].(ValueColor); ok {
return col
}
}
}
return color.RGBA { R: 255, B: 255, A: 255 }
}
cookies := []event.Cookie { }
sty := &style.Style {
Rules: make([]style.Rule, len(this.sheet.Rules)),
Colors: map[tomo.Color] color.Color {
tomo.ColorBackground: getColor("ColorBackground"),
tomo.ColorForeground: getColor("ColorForeground"),
tomo.ColorRaised: getColor("ColorRaised"),
tomo.ColorSunken: getColor("ColorSunken"),
tomo.ColorAccent: getColor("ColorAccent"),
},
}
for index, rule := range this.sheet.Rules {
styleRule := style.Rule {
Role: tomo.Role {
Package: rule.Selector.Package,
Object: rule.Selector.Object,
},
Tags: rule.Selector.Tags,
Set: make(style.AttrSet),
}
for name, attr := range rule.Attrs {
styleAttr, cookie, err := this.buildAttr(name, attr)
if err != nil { return nil, nil, err }
styleRule.Set.Add(styleAttr)
if cookie != nil {
cookies = append(cookies, cookie)
}
}
sty.Rules[index] = styleRule
}
// TODO include all the faces in this.faces (ONCE EACH) in the
// multicookie
return sty, event.MultiCookie(cookies...), nil
}
func (this *styleBuilder) buildAttr (name string, attr []ValueList) (tomo.Attr, event.Cookie, error) {
errWrongType := func () error {
return errors.New(fmt.Sprintf("wrong type for %s attribute", name))
}
expectSingle := func () error {
if len(attr) != 1 {
return errors.New(fmt.Sprintf (
"%s attribute requires exactly one value list",
name))
}
return nil
}
expectSingleSingle := func () error {
err := expectSingle()
if err != nil { return err }
if len(attr[0]) != 1 {
return errors.New(fmt.Sprintf (
"%s attribute requires exactly one value",
name))
}
return nil
}
expectNumbers := func (list ValueList) error {
for _, value := range list {
if _, ok := value.(ValueNumber); ok { continue }
return errWrongType()
}
return nil
}
numbers := func (list ValueList) ([]int, error) {
nums := make([]int, len(list))
for index, value := range list {
if value, ok := value.(ValueNumber); ok {
nums[index] = int(value)
continue
}
return nil, errWrongType()
}
return nums, nil
}
bools := func (list ValueList) ([]bool, error) {
bools := make([]bool, len(list))
for index, value := range list {
if value, ok := value.(ValueKeyword); ok {
switch value {
case "true":
bools[index] = true
continue
case "false":
bools[index] = false
continue
}
}
return nil, errWrongType()
}
return bools, nil
}
point := func (list ValueList) (image.Point, error) {
err := expectNumbers(list)
if err != nil { return image.Point { }, err }
vector := image.Point { }
switch len(attr[0]) {
case 1:
vector.X = int(list[0].(ValueNumber))
vector.Y = int(list[0].(ValueNumber))
case 2:
vector.X = int(list[0].(ValueNumber))
vector.Y = int(list[1].(ValueNumber))
default:
return image.Point { }, errors.New(fmt.Sprintf (
"%s attribute requires exactly one or two values",
name))
}
return vector, nil
}
switch name {
case "Color":
err := expectSingleSingle()
if err != nil { return nil, nil, err }
if col, ok := attr[0][0].(ValueColor); ok {
return tomo.AColor(col), nil, nil
}
return nil, nil, errWrongType()
case "Texture":
// TODO load image from file
case "TextureMode":
err := expectSingleSingle()
if err != nil { return nil, nil, err }
if keyword, ok := attr[0][0].(ValueKeyword); ok {
switch keyword {
case "tile": return tomo.ATextureMode(tomo.TextureModeCenter), nil, nil
case "center": return tomo.ATextureMode(tomo.TextureModeCenter), nil, nil
}
return nil, nil, errors.New(fmt.Sprintf (
"unknown texture mode: %s",
keyword))
}
return nil, nil, errWrongType()
case "Border":
attrBorder, err := buildAttrBorder(attr)
if err != nil { return nil, nil, err }
return attrBorder, nil, nil
case "MinimumSize":
err := expectSingle()
if err != nil { return nil, nil, err }
vector, err := point(attr[0])
if err != nil { return nil, nil, err }
return tomo.AttrMinimumSize(vector), nil, nil
case "Padding":
err := expectSingle()
if err != nil { return nil, nil, err }
numbers, err := numbers(attr[0])
if err != nil { return nil, nil, err }
inset := tomo.Inset { }
if !copyBorderValue(inset[:], numbers) {
return nil, nil, errors.New(fmt.Sprintf (
"%s attribute requires exactly one, two, or four values",
name))
}
return tomo.AttrPadding(inset), nil, nil
case "Gap":
err := expectSingle()
if err != nil { return nil, nil, err }
vector, err := point(attr[0])
if err != nil { return nil, nil, err }
return tomo.AttrGap(vector), nil, nil
case "TextColor":
err := expectSingleSingle()
if err != nil { return nil, nil, err }
if col, ok := attr[0][0].(ValueColor); ok {
return tomo.ATextColor(col), nil, nil
}
return nil, nil, errWrongType()
case "DotColor":
err := expectSingleSingle()
if err != nil { return nil, nil, err }
if col, ok := attr[0][0].(ValueColor); ok {
return tomo.ADotColor(col), nil, nil
}
return nil, nil, errWrongType()
case "Face":
// TODO support weight, italic, slant
err := expectSingle()
if err != nil { return nil, nil, err }
list := attr[0]
if len(list) != 2 {
return nil, nil, errors.New(fmt.Sprintf (
"%s attribute requires exactly two values",
name))
}
name, ok := list[0].(ValueString)
if !ok { return nil, nil, errWrongType() }
size, ok := list[1].(ValueNumber)
if !ok { return nil, nil, errWrongType() }
return tomo.AFace(tomo.Face {
Font: string(name),
Size: float64(size),
}), nil, nil
case "Wrap":
err := expectSingleSingle()
if err != nil { return nil, nil, err }
if value, ok := attr[0][0].(ValueKeyword); ok {
switch value {
case "true": return tomo.AWrap(true), nil, nil
case "false": return tomo.AWrap(false), nil, nil
}
}
return nil, nil, errWrongType()
case "Align":
err := expectSingle()
if err != nil { return nil, nil, err }
list := attr[0]
if len(list) != 2 {
return nil, nil, errors.New(fmt.Sprintf (
"%s attribute requires exactly two values",
name))
}
aligns := [2]tomo.Align { }
for index, value := range list {
if keyword, ok := value.(ValueKeyword); ok {
switch keyword {
case "start": aligns[index] = tomo.AlignStart; continue
case "middle": aligns[index] = tomo.AlignMiddle; continue
case "end": aligns[index] = tomo.AlignEnd; continue
case "even": aligns[index] = tomo.AlignEven; continue
default: return nil, nil, errors.New(fmt.Sprintf (
"unknown texture mode: %s",
keyword))
}
}
return nil, nil, errWrongType()
}
return tomo.AAlign(aligns[0], aligns[1]), nil, nil
case "Overflow":
err := expectSingle()
if err != nil { return nil, nil, err }
bools, err := bools(attr[0])
if err != nil { return nil, nil, err }
if len(bools) != 2 {
return nil, nil, errors.New(fmt.Sprintf (
"%s attribute requires exactly two values",
name))
}
return tomo.AOverflow(bools[0], bools[1]), nil, nil
case "Layout":
// TODO allow use of some layouts in the objects package
default: return nil, nil, errors.New(fmt.Sprintf("unknown attribute name %s", name))
}
return nil, nil, errors.New(fmt.Sprintf("unimplemented attribute name %s", name))
}
func buildAttrBorder (attr []ValueList) (tomo.Attr, error) {
borders := make([]tomo.Border, len(attr))
for index, list := range attr {
colors := make([]color.Color, 0, len(list))
sizes := make([]int, 0, len(list))
capturingSize := false
for _, value := range list {
if capturingSize {
if value, ok := value.(ValueNumber); ok {
sizes = append(sizes, int(value))
continue
}
} else {
if _, ok := value.(ValueCut); ok {
capturingSize = true
continue
}
if value, ok := value.(ValueColor); ok {
colors = append(colors, value)
continue
}
}
return nil, errors.New("malformed Border attribute value list")
}
border := tomo.Border { }
if !copyBorderValue(border.Width[:], sizes) {
return nil, errors.New("malformed Border attribute width list")
}
if !copyBorderValue(border.Color[:], colors) {
return nil, errors.New("malformed Border attribute color list")
}
borders[index] = border
}
return tomo.ABorder(borders...), nil
}
func copyBorderValue[T any, U ~[]T] (destination, source U) bool {
if len(source) > len(destination) { return false }
switch len(source) {
case 1:
destination[0] = source[0]
destination[1] = source[0]
destination[2] = source[0]
destination[3] = source[0]
return true
case 2:
destination[0] = source[0]
destination[1] = source[1]
destination[2] = source[0]
destination[3] = source[1]
return true
case 4:
destination[0] = source[0]
destination[1] = source[1]
destination[2] = source[2]
destination[3] = source[3]
return true
default:
return false
}
}

View File

@@ -0,0 +1,17 @@
package tss
import "testing"
func TestValueColor (test *testing.T) {
testValueColorRGBA(test, 0xFB380CFF, 0xFBFB, 0x3838, 0x0C0C, 0xFFFF)
testValueColorRGBA(test, 0xFB380C00, 0x0000, 0x0000, 0x0000, 0x0000)
}
func testValueColorRGBA (test *testing.T, col ValueColor, r, g, b, a uint32) {
gr, gg, gb, ga := col.RGBA()
test.Logf("testing RGBA for color #%08X", col)
if gr != r { test.Errorf("r component inequal (%04X != %04X)", gr, r) }
if gg != g { test.Errorf("g component inequal (%04X != %04X)", gg, g) }
if gb != b { test.Errorf("b component inequal (%04X != %04X)", gb, b) }
if ga != a { test.Errorf("a component inequal (%04X != %04X)", ga, a) }
}

View File

@@ -0,0 +1,53 @@
package tss
import "fmt"
import "errors"
// Flatten evaluates all variables recursively, thereby eliminating all
// instances of ValueVariable.
func (this *Sheet) Flatten () error {
if this.flat { return nil }
this.flat = true
for name, variable := range this.Variables {
variable, err := this.eval(variable)
if err != nil { return err }
this.Variables[name] = variable
}
for index, rule := range this.Rules {
for name, attr := range rule.Attrs {
for index, list := range attr {
list, err := this.eval(list)
if err != nil { return err }
attr[index] = list
}
rule.Attrs[name] = attr
}
this.Rules[index] = rule
}
return nil
}
func (this *Sheet) eval (source ValueList) (ValueList, error) {
destination := make(ValueList, 0, len(source))
for _, value := range source {
if name, ok := value.(ValueVariable); ok {
variable, ok := this.Variables[string(name)]
if !ok {
return nil, errors.New(fmt.Sprintf(
"variable $%s does not exist",
value))
}
variable, err := this.eval(variable)
if err != nil { return nil, err }
destination = append(destination, variable...)
continue
} else {
destination = append(destination, value)
}
}
return destination, nil
}

363
internal/styles/tss/lex.go Normal file
View File

@@ -0,0 +1,363 @@
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
Slash
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",
Slash: "Slash",
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 }
}
} else {
token.Kind = Slash
}
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'
}

View File

@@ -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,
}
}

View File

@@ -0,0 +1,253 @@
package tss
import "io"
import "strconv"
import "git.tebibyte.media/sashakoshka/goparse"
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 }
name := this.Value()
err = this.ExpectNext(Equals)
if err != nil { return "", nil, err }
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(LBrace)
if err != nil { return Rule { }, err }
for {
this.Next()
if this.Is(RBrace) { break }
pos := this.Pos()
name, attr, err := this.parseAttr()
if err != nil { return Rule { }, err }
err = this.Expect(Semicolon)
if err != nil { return Rule { }, err }
if _, exists := rule.Attrs[name]; exists {
return Rule { }, parse.Errorf (
pos,
"attribute %s already declared in this rule",
name)
}
rule.Attrs[name] = attr
}
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(LBracket)
if err == nil {
this.Next()
for {
err := this.Expect(Ident, String, RBracket)
if err != nil { return Selector { }, err }
if this.Is(RBracket) { break }
if this.Is(Comma) { this.Next() }
selector.Tags = append(selector.Tags, this.Value())
err = this.ExpectNext(Comma, RBracket)
if err != nil { return Selector { }, err }
}
this.Next()
}
return selector, nil
}
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, Slash,
Comma, Semicolon)
if err != nil { return "", nil, err }
if this.Is(Semicolon) { break }
if this.Is(Comma) { this.Next() }
valueList, err := this.parseValueList()
if err != nil { return "", nil, err }
attr = append(attr, valueList)
err = this.Expect(Comma, Semicolon)
if err != nil { return "", nil, err }
}
return name, attr, nil
}
func (this *parser) parseValueList () (ValueList, error) {
list := ValueList { }
for {
err := this.ExpectDesc (
"value",
Number, Color, String, Ident, Dollar, Slash)
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()))
case Slash:
list = append(list, ValueCut { })
}
this.Next()
}
return list, nil
}
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
}
}

View File

@@ -0,0 +1,79 @@
package tss
import "os"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/backend/style"
type Sheet struct {
Variables map[string] ValueList
Rules []Rule
flat bool
}
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 ()
}
type ValueNumber int
func (ValueNumber) value () { }
type ValueColor uint32
func (ValueColor) value () { }
func (value ValueColor) RGBA () (r, g, b, a uint32) {
// extract components
bits := uint32(value)
r = (bits & 0xFF000000) >> 24
g = (bits & 0x00FF0000) >> 16
b = (bits & 0x0000FF00) >> 8
a = (bits & 0x000000FF)
// extend to 16 bits per channel
r = r << 8 | r
g = g << 8 | g
b = b << 8 | b
a = a << 8 | a
// alpha premultiply
r = (r * a) / 0xFFFF
g = (g * a) / 0xFFFF
b = (b * a) / 0xFFFF
return
}
type ValueString string
func (ValueString) value () { }
type ValueKeyword string
func (ValueKeyword) value () { }
type ValueVariable string
func (ValueVariable) value () { }
type ValueCut struct { }
func (ValueCut) value () { }
// LoadFile loads the stylesheet from the specified file. This may return a
// parse.Error, so use parse.Format to print it.
func LoadFile (name string) (*style.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()
sheet, err := Parse(Lex(name, file))
if err != nil { return nil, nil, err }
return BuildStyle(sheet)
}

View File

@@ -0,0 +1,32 @@
# Happen to use Micro to edit text?
# Drop this in ~/.config/micro/syntax and get syntax highlighting for TSS files!
filetype: tss
detect:
filename: "\\.tss$"
rules:
- type: "\\b([A-Z][a-zA-Z0-9]*).*:"
- identifier.var: "\\$[a-zA-Z0-9]*\\b"
- identifier.class: "(\\*|[a-z][a-zA-Z0-9]*)\\.(\\*|[A-Z][a-zA-Z0-9]*)"
- special: "(\\/|,|\\;|:|\\.)"
- symbol.operator: "(=|\\*)"
- symbol.brackets: "(\\{\\[|\\}\\])"
- comment:
start: "//"
end: "$"
rules:
- todo: "(TODO|XXX|FIXME|BUG):?"
- constant.string:
start: "\""
end: "\""
skip: "\\\\."
rules:
- constant.specialChar: "\\\\[abfnrtv'\\\"\\\\]"
- constant.specialChar: "\\\\([0-7]{3})"
- constant.number: "\\b[0-9][0-9.]*\\b"
- constant.string: "\\B#[0-9a-fA-F]*"