Skip to content

Commit

Permalink
Merge pull request #43 from dnnrly/feature/to-absolute
Browse files Browse the repository at this point in the history
Generate absolute config from SVGs
  • Loading branch information
dnnrly committed Mar 2, 2024
2 parents 4851d6d + 9a5c547 commit 22dfd76
Show file tree
Hide file tree
Showing 16 changed files with 595 additions and 61 deletions.
11 changes: 11 additions & 0 deletions .golangci-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Options for analysis running.
run:
concurrency: 4
timeout: 5m
build-tags:
- docs

skip-dirs:
- mocks

show-stats: true
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ test: ## run unit tests

.PHONY: fuzz
fuzz: ## run fuzz tests
go test -tags fuzz -fuzz=FuzzDrawing
go test -tags fuzz -fuzz=FuzzDrawing -fuzztime=2m
go test -tags fuzz -fuzz=FuzzConfig -fuzztime=2m
go test -tags fuzz -fuzz=FuzzToAbsolute -fuzztime=2m

.PHONY: ci-test
ci-test: ## ci target - run tests to generate coverage data
Expand Down
29 changes: 29 additions & 0 deletions cmd/layli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"io"
"os"
"strings"

Expand Down Expand Up @@ -77,5 +78,33 @@ func Execute() error {
rootCmd.PersistentFlags().StringVarP(&layout, "layout", "l", "flow-square", "the layout algorithm")
rootCmd.PersistentFlags().BoolVar(&showGrid, "show-grid", false, "show the path grid dots (great for debugging)")

rootCmd.AddCommand(
&cobra.Command{
Use: "to-absolute [flags] [layout file]",
Short: "convert a Layli generated SVG into a layli file that can regenerate it",
Long: ``,
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error {
f, err := os.Open(args[0])
if err != nil {
return fmt.Errorf("opening input: %w", err)
}

svg, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("reading input: %w", err)
}

err = layli.AbsoluteFromSVG(string(svg), func(data string) error {
return os.WriteFile(output, []byte(data), 0644)
})
if err != nil {
return fmt.Errorf("generating layli file %s: %w", output, err)
}

return nil
},
})

return rootCmd.Execute()
}
31 changes: 18 additions & 13 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
)

type ConfigPath struct {
Attempts int `yaml:"attempts"`
Strategy string `yaml:"strategy"`
Class string `yaml:"class"`
Attempts int `yaml:"attempts,omitempty"`
Strategy string `yaml:"strategy,omitempty"`
Class string `yaml:"class,omitempty"`
}

type ConfigStyles map[string]string
Expand All @@ -36,9 +36,9 @@ func (styles ConfigStyles) toCSS() string {
}

type Config struct {
Layout string `yaml:"layout"`
LayoutAttempts int `yaml:"layout-attempts"`
Path ConfigPath `yaml:"path"`
Layout string `yaml:"layout,omitempty"`
LayoutAttempts int `yaml:"layout-attempts,omitempty"`
Path ConfigPath `yaml:"path,omitempty"`
Nodes ConfigNodes `yaml:"nodes"`
Edges ConfigEdges `yaml:"edges"`
Spacing int `yaml:"-"`
Expand All @@ -48,7 +48,12 @@ type Config struct {
Border int `yaml:"border"`
Margin int `yaml:"margin"`

Styles ConfigStyles `yaml:"styles"`
Styles ConfigStyles `yaml:"styles,omitempty"`
}

func (config Config) String() string {
str, _ := yaml.Marshal(config)
return string(str)
}

type Position struct {
Expand All @@ -59,9 +64,9 @@ type Position struct {
type ConfigNode struct {
Id string `yaml:"id"`
Contents string `yaml:"contents"`
Position Position `yaml:"position"`
Class string `yaml:"class"`
Style string `yaml:"style"`
Position Position `yaml:"position,omitempty"`
Class string `yaml:"class,omitempty"`
Style string `yaml:"style,omitempty"`
}

type ConfigNodes []ConfigNode
Expand All @@ -76,11 +81,11 @@ func (nodes ConfigNodes) ByID(id string) *ConfigNode {
}

type ConfigEdge struct {
ID string `yaml:"id"`
ID string `yaml:"id,omitempty"`
From string `yaml:"from"`
To string `yaml:"to"`
Class string `yaml:"class"`
Style string `yaml:"style"`
Class string `yaml:"class,omitempty"`
Style string `yaml:"style,omitempty"`
}

type ConfigEdges []ConfigEdge
Expand Down
42 changes: 23 additions & 19 deletions demo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 37 additions & 15 deletions fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,26 @@ import (
"strings"
"testing"

"github.com/antchfx/xmlquery"
"github.com/dnnrly/layli/pathfinder/dijkstra"
)

// func FuzzConfig(f *testing.F) {
// dir, _ := os.ReadDir("./examples")
// for _, d := range dir {
// if !d.IsDir() && strings.HasSuffix(d.Name(), ".layli") {
// config, err := os.ReadFile("./examples/" + d.Name())
// if err != nil {
// panic(err)
// }
// f.Add(string(config)) // Use f.Add to provide a seed corpus
// }
// }
func FuzzConfig(f *testing.F) {
dir, _ := os.ReadDir("./examples")
for _, d := range dir {
if !d.IsDir() && strings.HasSuffix(d.Name(), ".layli") {
config, err := os.ReadFile("./examples/" + d.Name())
if err != nil {
panic(err)
}
f.Add(string(config)) // Use f.Add to provide a seed corpus
}
}

// f.Fuzz(func(t *testing.T, orig string) {
// _, _ = NewConfigFromFile(strings.NewReader(orig))
// })
// }
f.Fuzz(func(t *testing.T, orig string) {
_, _ = NewConfigFromFile(strings.NewReader(orig))
})
}

func FuzzDrawing(f *testing.F) {
dir, _ := os.ReadDir("./examples")
Expand Down Expand Up @@ -63,3 +64,24 @@ func FuzzDrawing(f *testing.F) {
_ = d.Draw()
})
}

func FuzzToAbsolute(f *testing.F) {
dir, _ := os.ReadDir("./examples")
for _, d := range dir {
if !d.IsDir() && strings.HasSuffix(d.Name(), ".svg") {
config, err := os.ReadFile("./examples/" + d.Name())
if err != nil {
panic(err)
}
f.Add(string(config)) // Use f.Add to provide a seed corpus
}
}

f.Fuzz(func(t *testing.T, orig string) {
_, err := xmlquery.Parse(strings.NewReader(orig))
if err != nil {
t.Skip()
}
_ = AbsoluteFromSVG(orig, func(string) error { return nil })
})
}
107 changes: 107 additions & 0 deletions layli.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package layli

import (
"fmt"
"strconv"
"strings"

svg "github.com/ajstarks/svgo"
"github.com/antchfx/xmlquery"
)

type OutputFunc func(output string) error
Expand All @@ -24,6 +27,10 @@ func (d *Diagram) Draw() error {
(d.Layout.LayoutWidth()-1)*d.Config.Spacing,
(d.Layout.LayoutHeight()-1)*d.Config.Spacing,
"style=\"background-color: white;\"",
fmt.Sprintf(`data-margin="%d"`, d.Config.Margin),
fmt.Sprintf(`data-border="%d"`, d.Config.Border),
fmt.Sprintf(`data-node-width="%d"`, d.Config.NodeWidth),
fmt.Sprintf(`data-node-height="%d"`, d.Config.NodeHeight),
)
if len(d.Config.Styles) != 0 {
canvas.Style("text/css", d.Config.Styles.toCSS())
Expand All @@ -49,3 +56,103 @@ func (d *Diagram) Draw() error {
canvas.End()
return d.Output(w.String())
}

// AbsuluteFromSVG parses a string of an SVG and turns it in to a Layli configuration
// with with absulute layout that can represent the same SVG
func AbsoluteFromSVG(svg string, output OutputFunc) error {
if svg == "" {
return fmt.Errorf("svg cannot be empty")
}

dom, err := xmlquery.Parse(strings.NewReader(svg))
if err != nil {
return fmt.Errorf("parsing svg: %w", err)
}

if svg[0] != '<' || dom == nil || xmlquery.FindOne(dom, "/svg") == nil {
return fmt.Errorf("error parsing svg: %s", dom.Data)
}

config := &Config{
Layout: "absolute",
Nodes: ConfigNodes{},
}

blankParse := func(s string) (int, error) {
if s == "" {
return 0, nil
}

return strconv.Atoi(s)
}

root := xmlquery.FindOne(dom, "//svg")

config.NodeWidth, err = blankParse(root.SelectAttr("data-node-width"))
if err != nil {
return fmt.Errorf("parsing node width: %w", err)
}
config.NodeHeight, err = blankParse(root.SelectAttr("data-node-height"))
if err != nil {
return fmt.Errorf("parsing node height: %w", err)
}
config.Border, err = blankParse(root.SelectAttr("data-border"))
if err != nil {
return fmt.Errorf("parsing border: %w", err)
}
config.Margin, err = blankParse(root.SelectAttr("data-margin"))
if err != nil {
return fmt.Errorf("parsing margin: %w", err)
}

for _, n := range xmlquery.Find(dom, "//rect") {
id := n.SelectAttr("id")
x, err := strconv.Atoi(n.SelectAttr("data-pos-x"))
if err != nil {
return fmt.Errorf("parsing X: %w", err)
}
y, err := strconv.Atoi(n.SelectAttr("data-pos-y"))
if err != nil {
return fmt.Errorf("parsing Y: %w", err)
}

text := xmlquery.FindOne(dom, "//*[@id='"+id+"-text']")
if text == nil {
return fmt.Errorf("no text found for node %s", id)
}

config.Nodes = append(config.Nodes, ConfigNode{
Id: id,
Contents: text.InnerText(),
Position: Position{X: x, Y: y},
Class: n.SelectAttr("class"),
Style: n.SelectAttr("style"),
})
}

for _, e := range xmlquery.Find(dom, "//g/path") {
config.Edges = append(config.Edges, ConfigEdge{
From: e.SelectAttr("data-from"),
To: e.SelectAttr("data-to"),
Class: strings.Trim(strings.ReplaceAll(e.SelectAttr("class"), "path-line", ""), " "),
})
}

config.Styles = ConfigStyles{}
styleData := xmlquery.FindOne(dom, "//style")
if styleData != nil {
for _, line := range strings.Split(styleData.InnerText(), "\n") {
if line == "" {
continue
}

key, style, found := strings.Cut(strings.Trim(line, ""), " {")
if !found {
return fmt.Errorf("cannot parse style line: %s", line)
}
config.Styles[key] = "{" + style
}
}

return output(config.String())
}
Loading

0 comments on commit 22dfd76

Please sign in to comment.