Adds CSS support and classes; breaking change.
M Makefile +3 -3
@@ 4,6 4,6 @@ all:
 	-t 3,4,5  \
 	-i "${DATA}" \
 	-s \
-	-c "2,Contractor,fill:yellow" \
-	-c "2,Convert,fill:orange" \
-	-c "2,[^ ]Manager,fill:lightgreen" > "${WHO}.svg"
+	-c "3,TRUE,contractor" \
+	-c "4,TRUE,convert" \
+	-c "2,TRUE,manager" > "${WHO}.svg"

          
M cmd/orgchart/highlightrules.go +7 -3
@@ 18,20 18,24 @@ type Rule struct {
 
 // Errors in processing (too few columns in input data, e.g.) are skipped
 func (rs Ruleset) Class(cols []string) string {
+	rv := ""
 	for _, r := range rs {
 		if r.Col >= len(cols) {
 			// ERROR -- specified column does not exist.  Skip
 			continue
 		}
 		if r.Match.MatchString(cols[r.Col]) {
-			return r.Class
+			if len(rv) > 0 {
+				rv += " "
+			}
+			rv += r.Class
 		}
 	}
-	return ""
+	return rv
 }
 
 // WARN: the strings are expected to be in format:
-// <col#>,<regexp>,<color>
+// <col#>,<regexp>,<class>
 // but this is not CSV -- commas are separators, and can't be escaped.
 func makeRuleSet(set []string) (Ruleset, error) {
 	rv := make(Ruleset, len(set))

          
M cmd/orgchart/main.go +19 -5
@@ 16,6 16,7 @@ import (
 	"encoding/csv"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"os"
 	"regexp"
 	"strconv"

          
@@ 32,15 33,28 @@ func main() {
 	fltr := goopt.String([]string{"-f"}, "", "Filter for these people's organization (regex)")
 	coln := goopt.String([]string{"-t"}, "", "Display column numbers, indexed by 0, separated by commas; empty cells are omitted; badly formatted numbers are ignored")
 	sup := goopt.Flag([]string{"-s"}, []string{}, "Display superior; only used if results are filtered", "")
-	hil := goopt.Strings([]string{"-c"}, "", "Coloring. Format: <col#>,<regexp>,<css>. Multiple allowed; first match for each line wins.")
+	hil := goopt.Strings([]string{"-c"}, "", "Column,ValueMatch,ClassName -- sets a CSS class based on a regexp match in a column")
+	cssfile := goopt.String([]string{"-y"}, "", "CSS file to embed")
 	//lin := goopt.Strings([]string{"-l"}, "", "Box stroke. Format: <col#>,<regexp>.")
 	goopt.Version = ""
 	goopt.Summary = "Organization chart generator"
 	goopt.Parse(nil)
 	if *csv == "" {
-		fmt.Println("CSV is required")
-		goopt.Usage()
-		os.Exit(1)
+		if len(goopt.Args) == 0 {
+			fmt.Println("CSV is required")
+			goopt.Usage()
+			os.Exit(1)
+		}
+		*csv = goopt.Args[0]
+	}
+	var css string
+	if *cssfile != "" {
+		cssb, err := ioutil.ReadFile(*cssfile)
+		css = string(cssb)
+		if err != nil {
+			fmt.Println(err.Error())
+			os.Exit(1)
+		}
 	}
 	fin, err := os.Open(*csv)
 	defer fin.Close()

          
@@ 85,7 99,7 @@ func main() {
 		}
 		*sup = *sup && found
 	}
-	renderSVG(os.Stdout, rs, *sup)
+	renderSVG(os.Stdout, rs, *sup, css)
 }
 
 /**************************************************************************

          
M cmd/orgchart/main_test.go +24 -0
@@ 146,3 146,27 @@ D2,C2B3`
 	setBoxH(rs)
 	assert.Equal(t, 450, height(rs))
 }
+
+func TestClass(t *testing.T) {
+	s := `Person,Manager,Is Manager,Is Contractor
+X,,true,false
+Y,,false,true
+Z,,true,true
+W,,false,false`
+	b := bytes.NewBufferString(s)
+	rx, er := makeRuleSet([]string{"2,true,A", "3,true,B"})
+	if er != nil {
+		assert.Fail(t, er.Error())
+	}
+	ps, er := makePeople(b, []int{}, rx)
+	if er != nil {
+		assert.Fail(t, er.Error())
+	}
+	//for k, v := range ps {
+	//	fmt.Printf("%s   %#v\n", k, v)
+	//}
+	assert.Equal(t, ps["X"].Class, "A")
+	assert.Equal(t, ps["Y"].Class, "B")
+	assert.Equal(t, ps["Z"].Class, "A B")
+	assert.Equal(t, ps["W"].Class, "")
+}

          
M cmd/orgchart/svg.go +43 -12
@@ 2,6 2,7 @@ package main
 
 import (
 	"io"
+	"strconv"
 
 	"github.com/ajstarks/svgo"
 )

          
@@ 57,12 58,12 @@ func setBoxH(rs Ranks) {
 	}
 }
 
-func renderSVG(out io.Writer, rs Ranks, sup bool) {
+func renderSVG(out io.Writer, rs Ranks, sup bool, css string) {
 	setBoxH(rs)
 	c := svg.New(out)
 	bgW = boxW + gapW
 	pageWidth := rs.width() * bgW
-	h := height(rs)
+	h := height(rs, sup)
 	c.Start(pageWidth, h)
 	c.Filter("blur")
 	c.FeGaussianBlur(svg.Filterspec{In: "SourceAlpha", Result: "blur"}, 5, 5)

          
@@ 71,8 72,27 @@ func renderSVG(out io.Writer, rs Ranks, 
 	c.FeGaussianBlur(svg.Filterspec{In: "SourceGraphic", Result: "blur"}, 1, 1)
 	c.Fend()
 	c.Def()
+	c.Style(`
+.person {
+	fill: inherit;
+	stroke: black;
+	stroke-width: 1;
+}
+.toptext {
+	text-anchor: middle; 
+	font-size: smaller;
+}
+.nametext {
+	text-anchor: middle; 
+	font-weight: bold;
+}
+.bottomtext {
+	text-anchor: middle; 
+	font-size: smaller;
+}
+` + css)
 	c.Gid(_id)
-	c.Rect(0, 0, boxW, boxH, "fill: inherit; stroke: black; stroke-width: 1;")
+	c.Rect(0, 0, boxW, boxH, "class='person'")
 	c.Gend()
 	c.Gid(_root_id)
 	c.Rect(0, 0, boxW, boxH, "fill: inherit; stroke: black; stroke-width: 5;")

          
@@ 154,26 174,30 @@ func drawBox(c *svg.SVG, x, y int, p *Pe
 	if root {
 		ref = _root_ref
 	}
-	c.Use(x+2, y+3, ref, `filter="url(#blur)"`)
-	fill := "fill: white"
 	if p.Class != "" {
-		fill = p.Class
+		c.Use(x+2, y+3, ref, class(p), `filter="url(#blur)"`)
+		c.Use(x, y, ref, class(p))
+	} else {
+		c.Use(x+2, y+3, ref, `filter="url(#blur)"`)
+		c.Use(x, y, ref, "fill: white")
 	}
-	c.Use(x, y, ref, fill)
 	toff := x + (boxW / 2)
 	toffY := y + textSize + textPY
 	if len(p.LineMask) > 0 && p.LineMask[0] && len(p.Lines) > 0 {
-		c.Text(toff, y+textSize+textPY, p.Lines[0], "text-anchor: middle; font-size: smaller;")
+		c.Text(toff, y+textSize+textPY, p.Lines[0], "class='toptext'")
 		toffY += textSize + textPY
 	}
-	c.Text(toff, toffY, p.Name, "text-anchor: middle; font-weight: bold;")
+	c.Text(toff, toffY, p.Name, "class='nametext'")
 	toffY += textSize + textPY
 	for i, k := range p.LineMask {
 		if i == 0 || i >= len(p.Lines) {
 			continue
 		}
-		if k {
-			c.Text(toff, toffY, p.Lines[i], "text-anchor: middle; font-size: smaller;")
+		if k && len(p.Lines[i]) > 0 {
+			txt := "class='bottomtext textrow"
+			txt += strconv.Itoa(i)
+			txt += "'"
+			c.Text(toff, toffY, p.Lines[i], txt)
 			toffY += textSize + textPY
 		}
 	}

          
@@ 214,7 238,14 @@ func width(p *Person) int {
 	return p.Width() * (boxW + gapW)
 }
 
-func height(rs Ranks) int {
+func height(rs Ranks, s bool) int {
 	high := rs.height()
+	if s {
+		high += 1
+	}
 	return (high * boxH) + ((high - 1) * (gapH / 2)) + marginH
 }
+
+func class(p *Person) string {
+	return "class='" + p.Class + "'"
+}

          
A => org.css +10 -0
@@ 0,0 1,10 @@ 
+.contractor {
+	fill: yellow;
+}
+.convert {
+	stroke-dasharray: 5,5;
+	fill: orange;
+}
+.manager {
+	fill: lightgreen;
+}

          
A => vendor/manifest +19 -0
@@ 0,0 1,19 @@ 
+{
+	"version": 0,
+	"dependencies": [
+		{
+			"importpath": "github.com/ajstarks/svgo",
+			"repository": "https://github.com/ajstarks/svgo",
+			"revision": "672fe547df4e49efc6db67a74391368bcb149b37",
+			"branch": "master",
+			"notests": true
+		},
+		{
+			"importpath": "honnef.co/go/structlayout",
+			"repository": "https://github.com/dominikh/go-structlayout",
+			"revision": "504fdf06ef5c0d83a29c594b3a169af68b9c2e14",
+			"branch": "master",
+			"notests": true
+		}
+	]
+}
  No newline at end of file