├── test ├── test2a.scss ├── test3.css ├── test2.scss ├── test1.css ├── test1.scss ├── test2.css └── test3.scss ├── README.md ├── .gitignore ├── gosass.go └── all_test.go /test/test2a.scss: -------------------------------------------------------------------------------- 1 | blah { 2 | bloo: blee; 3 | } -------------------------------------------------------------------------------- /test/test3.css: -------------------------------------------------------------------------------- 1 | div { 2 | blah: blah; 3 | foo-prop: 42 -42 foofoo; } 4 | -------------------------------------------------------------------------------- /test/test2.scss: -------------------------------------------------------------------------------- 1 | foo { 2 | a: b; 3 | @import "test2a.scss"; 4 | c: d; 5 | } -------------------------------------------------------------------------------- /test/test1.css: -------------------------------------------------------------------------------- 1 | div { 2 | color: black; } 3 | div span { 4 | color: blue; } 5 | -------------------------------------------------------------------------------- /test/test1.scss: -------------------------------------------------------------------------------- 1 | div { 2 | color: black; 3 | span { 4 | color: blue; 5 | } 6 | } -------------------------------------------------------------------------------- /test/test2.css: -------------------------------------------------------------------------------- 1 | foo { 2 | a: b; 3 | c: d; } 4 | foo blah { 5 | bloo: blee; } 6 | -------------------------------------------------------------------------------- /test/test3.scss: -------------------------------------------------------------------------------- 1 | @mixin foo($x, $y) { 2 | foo-prop: $x $y foofoo; 3 | } 4 | 5 | @function pessimize($num) { 6 | @if $num > 0 { 7 | @return -$num; 8 | } 9 | @else { 10 | @return $num; 11 | } 12 | } 13 | 14 | div { 15 | blah: blah; 16 | @include foo(42, pessimize(42)); 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GoSass 2 | ====== 3 | 4 | Go language wrapper for LibSass (the C/C++ implementation of Sass). 5 | 6 | For sample usage, see the file `gosass.go`. But make sure you move it to a different folder or comment it out before building, because the Go build tools don't like having two separate modules in the same folder! 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /gosass.go: -------------------------------------------------------------------------------- 1 | package gosass 2 | 3 | /* 4 | #cgo LDFLAGS: -L${SRCDIR}/../../../../../clibs/lib -lsass -lstdc++ 5 | #cgo CFLAGS: -I${SRCDIR}/../../../../../clibs/include 6 | 7 | #include 8 | #include 9 | */ 10 | import "C" 11 | import ( 12 | "strings" 13 | "unsafe" 14 | ) 15 | 16 | type Options struct { 17 | OutputStyle int 18 | SourceComments bool 19 | IncludePaths []string 20 | ImagePath string 21 | // eventually gonna' have things like callbacks and whatnot 22 | } 23 | 24 | type Context struct { 25 | Options 26 | SourceString string 27 | OutputString string 28 | ErrorStatus int 29 | ErrorMessage string 30 | } 31 | 32 | type FileContext struct { 33 | Options 34 | InputPath string 35 | OutputString string 36 | ErrorStatus int 37 | ErrorMessage string 38 | } 39 | 40 | // Constants/enums for the output style. 41 | const ( 42 | NESTED_STYLE = iota 43 | EXPANDED_STYLE 44 | COMPACT_STYLE 45 | COMPRESSED_STYLE 46 | ) 47 | 48 | func Compile(goCtx *Context) { 49 | // set up the underlying C context struct 50 | cCtx := C.sass_new_context() 51 | cCtx.source_string = C.CString(goCtx.SourceString) 52 | defer C.free(unsafe.Pointer(cCtx.source_string)) 53 | cCtx.options.output_style = C.int(goCtx.Options.OutputStyle) 54 | if goCtx.Options.SourceComments { 55 | cCtx.options.source_comments = C.int(1) 56 | } else { 57 | cCtx.options.source_comments = C.int(0) 58 | } 59 | cCtx.options.include_paths = C.CString(strings.Join(goCtx.Options.IncludePaths, ":")) 60 | defer C.free(unsafe.Pointer(cCtx.options.include_paths)) 61 | cCtx.options.image_path = C.CString(goCtx.Options.ImagePath) 62 | defer C.free(unsafe.Pointer(cCtx.options.image_path)) 63 | // call the underlying C compile function to populate the C context 64 | C.sass_compile(cCtx) 65 | // extract values from the C context to populate the Go context object 66 | goCtx.OutputString = C.GoString(cCtx.output_string) 67 | goCtx.ErrorStatus = int(cCtx.error_status) 68 | goCtx.ErrorMessage = C.GoString(cCtx.error_message) 69 | // don't forget to free the C context! 70 | C.sass_free_context(cCtx) 71 | } 72 | 73 | func CompileFile(goCtx *FileContext) { 74 | // set up the underlying C context struct 75 | cCtx := C.sass_new_file_context() 76 | cCtx.input_path = C.CString(goCtx.InputPath) 77 | defer C.free(unsafe.Pointer(cCtx.input_path)) 78 | cCtx.options.output_style = C.int(goCtx.Options.OutputStyle) 79 | if goCtx.Options.SourceComments { 80 | cCtx.options.source_comments = C.int(1) 81 | } else { 82 | cCtx.options.source_comments = C.int(0) 83 | } 84 | cCtx.options.include_paths = C.CString(strings.Join(goCtx.Options.IncludePaths, ":")) 85 | defer C.free(unsafe.Pointer(cCtx.options.include_paths)) 86 | cCtx.options.image_path = C.CString(goCtx.Options.ImagePath) 87 | defer C.free(unsafe.Pointer(cCtx.options.image_path)) 88 | // call the underlying C compile function to populate the C context 89 | C.sass_compile_file(cCtx) 90 | // extract values from the C context to populate the Go context object 91 | goCtx.OutputString = C.GoString(cCtx.output_string) 92 | goCtx.ErrorStatus = int(cCtx.error_status) 93 | goCtx.ErrorMessage = C.GoString(cCtx.error_message) 94 | // don't forget to free the C context! 95 | C.sass_free_file_context(cCtx) 96 | } 97 | -------------------------------------------------------------------------------- /all_test.go: -------------------------------------------------------------------------------- 1 | package gosass 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "runtime" 7 | "testing" 8 | ) 9 | 10 | func runParallel(testFunc func(chan bool), concurrency int) { 11 | runtime.GOMAXPROCS(4) 12 | done := make(chan bool, concurrency) 13 | for i := 0; i < concurrency; i++ { 14 | go testFunc(done) 15 | } 16 | for i := 0; i < concurrency; i++ { 17 | <-done 18 | <-done 19 | } 20 | runtime.GOMAXPROCS(1) 21 | } 22 | 23 | const numConcurrentRuns = 200 24 | 25 | // const testFileName1 = "test1.scss" 26 | // const testFileName2 = "test2.scss" 27 | // const desiredOutput = "div {\n color: black; }\n div span {\n color: blue; }\n" 28 | 29 | func compileTest(t *testing.T, fileName string) (result string) { 30 | ctx := FileContext{ 31 | Options: Options{ 32 | OutputStyle: NESTED_STYLE, 33 | IncludePaths: make([]string, 0), 34 | }, 35 | InputPath: fileName, 36 | OutputString: "", 37 | ErrorStatus: 0, 38 | ErrorMessage: "", 39 | } 40 | 41 | CompileFile(&ctx) 42 | 43 | if ctx.ErrorStatus != 0 { 44 | if ctx.ErrorMessage != "" { 45 | t.Fatal("ERROR: ", ctx.ErrorMessage) 46 | } else { 47 | t.Fatalf("Unknown error, status %d", ctx.ErrorStatus) 48 | } 49 | } 50 | 51 | return ctx.OutputString 52 | } 53 | 54 | const numTests = 3 // TO DO: read the test dir and set this dynamically 55 | 56 | // NOTE: This test sometimes fails or deadlocks due to concurrency issues. It is unclear if 57 | // the test was *meant* to test concurrent behavior of gosass or that it was simply 58 | // a way to concurrently (and thus faster) test gosass. 59 | func TestConcurrent(t *testing.T) { 60 | testFunc := func(done chan bool) { 61 | done <- false 62 | for i := 1; i <= numTests; i++ { 63 | inputFile := fmt.Sprintf("test/test%d.scss", i) 64 | desiredOutput, err := ioutil.ReadFile(fmt.Sprintf("test/test%d.css", i)) 65 | if err != nil { 66 | t.Fatalf("ERROR: couldn't read test/test%d.css: %v", i, err) 67 | } 68 | result := compileTest(t, inputFile) 69 | if result != string(desiredOutput) { 70 | t.Fatalf("ERROR: incorrect output [%q] != expected [%q]", result, string(desiredOutput)) 71 | } 72 | } 73 | done <- true 74 | } 75 | runParallel(testFunc, numConcurrentRuns) 76 | } 77 | 78 | var testSassFuncs = []struct { 79 | name string 80 | context Context 81 | expected string 82 | }{ 83 | {name: "image-url test with scheme-less uri path", 84 | context: Context{ 85 | Options: Options{ 86 | OutputStyle: COMPRESSED_STYLE, 87 | SourceComments: false, 88 | IncludePaths: []string{"//include-root"}, 89 | ImagePath: "//image-root"}, 90 | SourceString: "bg { src: image-url('fonts/fancy.otf?whynot');}"}, 91 | expected: "bg{src:url(\"//image-root/fonts/fancy.otf?whynot\");}"}, 92 | {name: "image-url test with relative uri path", 93 | context: Context{ 94 | Options: Options{ 95 | OutputStyle: COMPRESSED_STYLE, 96 | SourceComments: false, 97 | IncludePaths: []string{"//include-root"}, 98 | ImagePath: "/image-root"}, 99 | SourceString: "bg { src: image-url('fonts/fancy.otf?whynot');}"}, 100 | expected: "bg{src:url(\"/image-root/fonts/fancy.otf?whynot\");}"}, 101 | } 102 | 103 | func TestSassFunctions(t *testing.T) { 104 | for _, tobj := range testSassFuncs { 105 | Compile(&tobj.context) 106 | 107 | if tobj.context.ErrorStatus != 0 { 108 | if tobj.context.ErrorMessage != "" { 109 | t.Fatal("ERROR: ", tobj.context.ErrorMessage) 110 | } else { 111 | t.Fatalf("Unknown error, status %d", tobj.context.ErrorStatus) 112 | } 113 | } else if tobj.context.OutputString != tobj.expected { 114 | t.Fatalf("Test case %s failed. Expected \"%s\" but received \"%s\".", tobj.name, tobj.expected, tobj.context.OutputString) 115 | } 116 | } 117 | } 118 | --------------------------------------------------------------------------------