├── .gitignore ├── spec ├── fixtures │ ├── profile-card │ │ ├── assets │ │ │ ├── cover.jpg │ │ │ └── avatar.jpg │ │ ├── original.html │ │ ├── structure.gss │ │ ├── texture.css │ │ ├── compiled.html │ │ └── compiled-range.html │ └── base │ │ ├── removed.html │ │ ├── injected.html │ │ ├── original.html │ │ └── compiled.html ├── Grunt.coffee ├── Options.coffee ├── Precompile.coffee └── Base.coffee ├── bower.json ├── .travis.yml ├── package.json ├── tasks └── gss2css.js ├── Gruntfile.coffee ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /bower_components/ 3 | npm-debug.log 4 | /spec/fixtures/*/grunt.html 5 | -------------------------------------------------------------------------------- /spec/fixtures/profile-card/assets/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gss/gss2css/HEAD/spec/fixtures/profile-card/assets/cover.jpg -------------------------------------------------------------------------------- /spec/fixtures/profile-card/assets/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gss/gss2css/HEAD/spec/fixtures/profile-card/assets/avatar.jpg -------------------------------------------------------------------------------- /spec/fixtures/base/removed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic comms test 5 | 10 | 11 | 12 |

Hello world

13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/base/injected.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic comms test 5 | 10 | 15 | 16 | 17 |

Hello world

18 | 19 | 20 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gss2css", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/the-gss/gss2css", 5 | "authors": [ 6 | "Henri Bergius " 7 | ], 8 | "description": "GSS to CSS precompiler", 9 | "license": "MIT", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "gss": "git://github.com/the-gss/engine.git#master" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_install: 5 | - sudo rm -rf /usr/local/phantomjs 6 | before_script: 7 | - npm install -g grunt-cli 8 | script: npm test 9 | deploy: 10 | provider: npm 11 | email: henri.bergius@iki.fi 12 | api_key: 13 | secure: GMPapgETG7u8m3IMdy5iQBqncDp7PMUA3aseJgW3Wn9Y5CAKvsuWMwdj1YOWQO9HrPAhRNEOiGVBY1HQ6e/cLPjCRYh9ur8JXU4Envje2tvzT29kIs9SK9e67zL50NLtvgYVAnfY7vzoF0KspCswQQNdOvxQaBUmnvhdMX/5AAQ= 14 | on: 15 | tags: true 16 | repo: the-gss/gss2css 17 | -------------------------------------------------------------------------------- /spec/fixtures/base/original.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic comms test 5 | 10 | 15 | 16 | 17 | 18 |

Hello world

19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/Grunt.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | chai = require 'chai' 4 | 5 | describe 'Grunt task for GSS to CSS precompilation', -> 6 | fs.readdirSync(path.resolve(__dirname, 'fixtures')).forEach (item) -> 7 | return if item is 'base' 8 | describe "Compiling #{item}", -> 9 | it 'should produce expected result', -> 10 | replacer = /[\n\s"']*/g 11 | itemPath = path.resolve __dirname, "fixtures/#{item}" 12 | try 13 | expected = fs.readFileSync "#{itemPath}/compiled.html", 'utf-8' 14 | result = fs.readFileSync "#{itemPath}/grunt.html", 'utf-8' 15 | catch e 16 | expected = '' 17 | expected = expected.replace replacer, '' 18 | result = result.replace replacer, '' if result 19 | chai.expect(result).to.equal expected 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gss-to-css", 3 | "version": "0.0.1", 4 | "description": "GSS to CSS precompiler", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "grunt test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/the-gss/gss2css.git" 12 | }, 13 | "author": "", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/the-gss/gss2css/issues" 17 | }, 18 | "keywords": [ 19 | "gruntplugin", 20 | "gss", 21 | "gridstylesheets", 22 | "css", 23 | "precompiler" 24 | ], 25 | "homepage": "https://github.com/the-gss/gss2css", 26 | "devDependencies": { 27 | "grunt": "^0.4.5", 28 | "grunt-bower-task": "^0.3.4", 29 | "grunt-cafe-mocha": "^0.1.12", 30 | "grunt-contrib-connect": "^0.7.1", 31 | "chai": "^1.9.1", 32 | "grunt-contrib-jshint": "^0.10.0" 33 | }, 34 | "dependencies": { 35 | "node-phantom-ws": "^1.0.10", 36 | "phantomjs": "^1.9.7-8", 37 | "jsdom": "^0.10.6" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/fixtures/base/compiled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic comms test 5 | 6 | 11 | 12 | 28 | 29 |

Hello world

30 | 31 | 32 | -------------------------------------------------------------------------------- /spec/fixtures/profile-card/original.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | GSS - Responsive Profile Card 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 |

Dan Daniels

33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tasks/gss2css.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | var lib = require('../index'); 3 | 4 | grunt.registerMultiTask('gss_to_css', 'Precompile GSS to CSS in HTML files', function () { 5 | var done = this.async(); 6 | var options = this.options({ 7 | baseUrl: 'http://localhost:8002/', 8 | sizes: [ 9 | { 10 | width: 1024, 11 | height: 768 12 | } 13 | ] 14 | }); 15 | 16 | var todo = this.files.length; 17 | this.files.forEach(function (f) { 18 | var sources = f.src.filter(function (source) { 19 | if (!grunt.file.exists(source)) { 20 | grunt.log.warn('Source file "' + source + '" not found.'); 21 | return false; 22 | } 23 | return true; 24 | }); 25 | sources.forEach(function (source) { 26 | lib.open(options.baseUrl + source, function (err, page, phantom) { 27 | if (err) { 28 | grunt.fail.warn(err); 29 | return; 30 | } 31 | 32 | var opts = JSON.parse(JSON.stringify(options)); 33 | lib.gss2css(page, opts, function (err, html) { 34 | if (err) { 35 | grunt.fail.warn(err); 36 | return; 37 | } 38 | 39 | todo--; 40 | grunt.file.write(f.dest, html); 41 | grunt.log.writeln('File "' + source + '" precompiled to "' + f.dest + '"'); 42 | if (phantom) { 43 | phantom.exit(); 44 | } 45 | if (todo <= 0) { 46 | done(); 47 | } 48 | }); 49 | }); 50 | }); 51 | }); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /spec/Options.coffee: -------------------------------------------------------------------------------- 1 | chai = require 'chai' 2 | lib = require '../index' 3 | 4 | describe 'Options normalization', -> 5 | it 'should keep a sizes array as-is', -> 6 | options = 7 | sizes: [ 8 | width: 1024 9 | height: 768 10 | , 11 | width: 800 12 | height: 600 13 | ] 14 | expected = options 15 | chai.expect(lib.normalizeOptions(options)).to.eql expected 16 | 17 | it 'should produce a single size with fixed ranges', -> 18 | options = 19 | ranges: 20 | width: 1024 21 | height: 768 22 | expected = 23 | sizes: [ 24 | width: 1024 25 | height: 768 26 | ] 27 | chai.expect(lib.normalizeOptions(options)).to.eql expected 28 | 29 | it 'should produce a two sizes with short width range', -> 30 | options = 31 | ranges: 32 | width: 33 | from: 1024 34 | to: 1034 35 | step: 10 36 | height: 768 37 | expected = 38 | sizes: [ 39 | width: 1024 40 | height: 768 41 | , 42 | width: 1034 43 | height: 768 44 | ] 45 | chai.expect(lib.normalizeOptions(options)).to.eql expected 46 | 47 | it 'should produce a four sizes with short ranges', -> 48 | options = 49 | ranges: 50 | width: 51 | from: 700 52 | to: 800 53 | step: 100 54 | height: 55 | from: 500 56 | to: 600 57 | step: 100 58 | expected = 59 | sizes: [ 60 | width: 700 61 | height: 500 62 | , 63 | width: 800 64 | height: 500 65 | , 66 | width: 700 67 | height: 600 68 | , 69 | width: 800 70 | height: 600 71 | ] 72 | chai.expect(lib.normalizeOptions(options)).to.eql expected 73 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = -> 2 | # Project configuration 3 | @initConfig 4 | pkg: @file.readJSON 'package.json' 5 | 6 | # GSS installation 7 | bower: 8 | install: 9 | options: 10 | copy: false 11 | 12 | # Local web server for testing purposes 13 | connect: 14 | server: 15 | options: 16 | port: 8002 17 | 18 | # GSS to CSS precompilation 19 | gss_to_css: 20 | fixtures: 21 | options: 22 | baseUrl: 'http://localhost:8002/' 23 | sizes:[ 24 | # Good old displays 25 | width: 800 26 | height: 600 27 | , 28 | # iPad landscape 29 | width: 1010 30 | height: 660 31 | , 32 | # Larger 33 | width: 1405 34 | height: 680 35 | ] 36 | files: [ 37 | expand: true 38 | cwd: '' 39 | src: ['spec/fixtures/*/original.html'] 40 | rename: (dest, src) -> src.replace 'original', 'grunt' 41 | ] 42 | 43 | # Coding standards checking 44 | jshint: 45 | lib: 46 | files: 47 | src: ['index.js'] 48 | 49 | # BDD tests on Node.js 50 | cafemocha: 51 | nodejs: 52 | src: ['spec/*.coffee'] 53 | options: 54 | reporter: 'spec' 55 | 56 | @loadTasks 'tasks' 57 | @loadNpmTasks 'grunt-bower-task' 58 | @loadNpmTasks 'grunt-contrib-connect' 59 | @loadNpmTasks 'grunt-contrib-jshint' 60 | @loadNpmTasks 'grunt-cafe-mocha' 61 | 62 | # Local tasks 63 | @registerTask 'build', => 64 | @task.run 'bower:install' 65 | 66 | @registerTask 'test', => 67 | @task.run 'jshint' 68 | @task.run 'build' 69 | @task.run 'connect' 70 | @task.run 'gss_to_css' 71 | @task.run 'cafemocha' 72 | -------------------------------------------------------------------------------- /spec/fixtures/profile-card/structure.gss: -------------------------------------------------------------------------------- 1 | /* vars */ 2 | [gap] == 20 !require; 3 | [flex-gap] >= [gap] * 2 !require; 4 | [radius] == 10 !require; 5 | [outer-radius] == [radius] * 2 !require; 6 | 7 | /* elements */ 8 | #profile-card { 9 | width: == ::window[width] - 480; 10 | height: == ::window[height] - 480; 11 | center-x: == ::window[center-x]; 12 | center-y: == ::window[center-y]; 13 | border-radius: == [outer-radius]; 14 | } 15 | 16 | #avatar { 17 | height: == 160 !require; 18 | width: == ::[height]; 19 | border-radius: == ::[height] / 2; 20 | } 21 | 22 | #name { 23 | height: == ::[intrinsic-height] !require; 24 | width: == ::[intrinsic-width] !require; 25 | } 26 | 27 | #cover { 28 | border-radius: == [radius]; 29 | } 30 | 31 | button { 32 | width: == ::[intrinsic-width] !require; 33 | height: == ::[intrinsic-height] !require; 34 | padding: == [gap]; 35 | padding-top: == [gap] / 2; 36 | padding-bottom: == [gap] / 2; 37 | border-radius: == [radius]; 38 | } 39 | 40 | @h |~-~[#name]~-~| in(#cover) gap([gap]*2) !strong; 41 | 42 | /* landscape profile-card */ 43 | @if #profile-card[width] >= #profile-card[height] { 44 | @v |-[#avatar]-[#name]-| in(#cover) 45 | gap([gap]) outer-gap([flex-gap]) 46 | chain-center-x(#cover[center-x]); 47 | 48 | @h |-10-[#cover]-10-| in(#profile-card); 49 | 50 | @v |-10-[#cover]-[#follow]-| 51 | in(#profile-card) 52 | gap([gap]); 53 | 54 | #follow[center-x] == #profile-card[center-x]; 55 | 56 | @h |-[#message]~-~[#follow]~-~[#following]-[#followers]-| 57 | in(#profile-card) 58 | gap([gap]) 59 | chain-top 60 | !strong; 61 | } 62 | 63 | /* portrait profile-card */ 64 | @else { 65 | @v |-[#avatar]-[#name]-[#follow]-[#message]-[#following]-[#followers]-| 66 | in(#cover) 67 | gap([gap]) 68 | outer-gap([flex-gap]) 69 | chain-center-x(#profile-card[center-x]); 70 | 71 | @h |-10-[#cover]-10-| in(#profile-card); 72 | @v |-10-[#cover]-10-| in(#profile-card); 73 | } 74 | -------------------------------------------------------------------------------- /spec/Precompile.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | lib = require '../index' 4 | chai = require 'chai' 5 | baseUrl = 'http://localhost:8002' 6 | 7 | fs.readdir path.resolve(__dirname, 'fixtures'), (err, items) -> 8 | return if err 9 | items.forEach (item) -> 10 | return if item is 'base' 11 | itemPath = path.resolve __dirname, "fixtures/#{item}" 12 | itemUrl = "#{baseUrl}/spec/fixtures/#{item}/original.html" 13 | describe "Precompiling #{item}", -> 14 | phantom = null 15 | after -> phantom.exit() if phantom 16 | it 'should produce expected with three fixed sizes', (done) -> 17 | config = 18 | sizes:[ 19 | # Good old displays 20 | width: 800 21 | height: 600 22 | , 23 | # iPad landscape 24 | width: 1010 25 | height: 660 26 | , 27 | # Larger 28 | width: 1405 29 | height: 680 30 | ] 31 | @timeout 0 32 | replacer = /[\n\s"']*/g 33 | try 34 | expected = fs.readFileSync "#{itemPath}/compiled.html", 'utf-8' 35 | catch e 36 | expected = '' 37 | expected = expected.replace replacer, '' 38 | lib.open itemUrl, (err, page, ph) -> 39 | phantom = ph 40 | lib.gss2css page, config, (err, html) -> 41 | #fs.writeFileSync "#{itemPath}/compiled.html", html 42 | chai.expect(html.replace(replacer, '')).to.equal expected 43 | done() 44 | it 'should produce expected with ranged width', (done) -> 45 | @timeout 0 46 | config = 47 | ranges: 48 | width: 49 | from: 400 50 | to: 1300 51 | step: 10 52 | height: 600 53 | replacer = /[\n\s"']*/g 54 | try 55 | expected = fs.readFileSync "#{itemPath}/compiled-range.html", 'utf-8' 56 | catch e 57 | expected = '' 58 | expected = expected.replace replacer, '' 59 | lib.open itemUrl, (err, page, ph) -> 60 | phantom = ph 61 | lib.gss2css page, config, (err, html) -> 62 | #fs.writeFileSync "#{itemPath}/compiled-range.html", html 63 | chai.expect(html.replace(replacer, '')).to.equal expected 64 | done() 65 | -------------------------------------------------------------------------------- /spec/fixtures/profile-card/texture.css: -------------------------------------------------------------------------------- 1 | html:not(.gss-ready) { 2 | opacity:0; 3 | } 4 | 5 | html { 6 | background-color: hsl(3, 18%, 43%); 7 | } 8 | 9 | * { 10 | -webkit-backface-visibility: hidden; 11 | margin: 0px; 12 | padding: 0px; 13 | outline: none; 14 | } 15 | 16 | #background { 17 | background-color: hsl(3, 18%, 43%); 18 | position: absolute; 19 | top: 0px; 20 | bottom: 0px; 21 | right: 0px; 22 | left: 0px; 23 | background-image: url('assets/cover.jpg'); 24 | background-size: cover; 25 | background-position: 50% 50%; 26 | opacity: .7; 27 | -webkit-filter: blur(5px) contrast(.7); 28 | } 29 | 30 | #cover { 31 | background-image: url('assets/cover.jpg'); 32 | background-size: cover; 33 | background-position: 50% 50%; 34 | } 35 | 36 | #avatar { 37 | background-image: url('assets/avatar.jpg'); 38 | background-size: cover; 39 | background-position: 50% 50%; 40 | border: 10px solid hsl(39, 40%, 90%); 41 | box-shadow: 0 1px 1px hsla(0,0%,0%,.5); 42 | } 43 | 44 | h1 { 45 | color: white; 46 | text-shadow: 0 1px 1px hsla(0,0%,0%,.5); 47 | font-size: 40px; 48 | line-height: 1.5em; 49 | font-family: "adelle",georgia,serif; 50 | font-style: normal; 51 | font-weight: 400; 52 | } 53 | 54 | button { 55 | color: hsl(3, 18%, 43%); 56 | background-color: hsl(39, 40%, 90%); 57 | text-shadow: 0 1px hsla(3, 18%, 100%, .5); 58 | font-family: "proxima-nova-soft",helvetica,sans-serif; 59 | font-style: normal; 60 | font-weight: 700; 61 | font-size: 14px; 62 | text-transform:uppercase; 63 | letter-spacing:.1em; 64 | border: none; 65 | } 66 | 67 | button.primary { 68 | background-color: #e38f71; 69 | color: white; 70 | text-shadow: 0 -1px hsla(3, 18%, 43%, .5); 71 | } 72 | 73 | #profile-card, .card { 74 | background-color: hsl(39, 40%, 90%); 75 | border: 1px solid hsla(0,0%,100%,.6); 76 | box-shadow: 0 5px 8px hsla(0,0%,0%,.3); 77 | } 78 | 79 | /* easeInOutCubic */ 80 | /* 81 | .gss-ready button, .gss-ready #name, .gss-ready #avatar { 82 | 83 | transition-property: transform; 84 | transition-duration: .5s; 85 | 86 | -webkit-transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1.000); 87 | -moz-transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1.000); 88 | -o-transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1.000); 89 | transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1.000); 90 | } 91 | */ -------------------------------------------------------------------------------- /spec/Base.coffee: -------------------------------------------------------------------------------- 1 | chai = require 'chai' 2 | lib = require '../index' 3 | fs = require 'fs' 4 | path = require 'path' 5 | baseUrl = 'http://localhost:8002' 6 | describe 'communicating with a web page', -> 7 | phantom = null 8 | page = null 9 | after -> phantom.exit() if phantom 10 | it 'should be able to open a page', (done) -> 11 | lib.open "#{baseUrl}/spec/fixtures/base/original.html", 12 | width: 600 13 | , (err, p, ph) -> 14 | phantom = ph 15 | page = p 16 | chai.expect(err).to.be.a 'null' 17 | chai.expect(page).to.be.an 'object' 18 | done() 19 | it 'should be able to talk to GSS on the page', (done) -> 20 | page.evaluate -> 21 | GSS.engines.root.vars 22 | , (err, result) -> 23 | chai.expect(result).to.be.an 'object' 24 | chai.expect(result['::window[width]']).to.be.a 'number' 25 | chai.expect(result['$hello[width]']).to.equal 200 26 | chai.expect(result['$hello[x]']).to.equal 192 27 | done() 28 | it 'after resizing the values should have changed', (done) -> 29 | lib.resize page, 30 | width: 800 31 | height: 600 32 | , (err, result) -> 33 | chai.expect(result).to.be.an 'object' 34 | chai.expect(result['::window[width]']).to.be.a 'number' 35 | chai.expect(result['$hello[width]']).to.equal 200 36 | chai.expect(result['$hello[x]']).to.equal 292 37 | done() 38 | it 'should be able to remove GSS from page', (done) -> 39 | replacer = /[\n\s"']*/g 40 | expected = fs.readFileSync path.resolve(__dirname, 'fixtures/base/removed.html'), 'utf-8' 41 | expected = expected.replace replacer, '' 42 | lib.removeGss page, (err, cleaned) -> 43 | cleaned = cleaned.replace replacer, '' 44 | chai.expect(cleaned).to.equal expected 45 | done() 46 | it 'should be able to inject CSS into the page', (done) -> 47 | replacer = /[\n\s"']*/g 48 | original = fs.readFileSync path.resolve(__dirname, 'fixtures/base/removed.html'), 'utf-8' 49 | expected = fs.readFileSync path.resolve(__dirname, 'fixtures/base/injected.html'), 'utf-8' 50 | expected = expected.replace replacer, '' 51 | css = """ 52 | #hello { 53 | color: red; 54 | } 55 | """ 56 | lib.injectCss original, css, (err, injected) -> 57 | injected = injected.replace replacer, '' 58 | chai.expect(injected).to.equal expected 59 | done() 60 | it 'should be able to precompile to CSS', (done) -> 61 | @timeout 0 62 | replacer = /[\n\s"']*/g 63 | expected = fs.readFileSync path.resolve(__dirname, 'fixtures/base/compiled.html'), 'utf-8' 64 | expected = expected.replace replacer, '' 65 | config = 66 | sizes:[ 67 | # iPhone portrait 68 | width: 310 69 | height: 352 70 | , 71 | # iPad landscape 72 | width: 1010 73 | height: 660 74 | , 75 | # Larger 76 | width: 1405 77 | height: 680 78 | ] 79 | lib.gss2css page, config, (err, html) -> 80 | #fs.writeFileSync path.resolve(__dirname, 'fixtures/base/compiled.html'), html 81 | chai.expect(html.replace(replacer, '')).to.equal expected 82 | done() 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GSS to CSS precompiler [![Build Status](https://travis-ci.org/the-gss/gss2css.png?branch=master)](https://travis-ci.org/the-gss/gss2css) 2 | ====================== 3 | 4 | This project provides both a [Node.js](http://nodejs.org/) library and a [Grunt](http://gruntjs.com/) plugin for precompiling constraint-driven [GSS](http://gridstylesheets.org/) layouts to plain CSS. 5 | 6 | gss2css utilizes [PhantomJS](http://phantomjs.org/) for rendering the existing GSS layout in various screen sizes and producing the appropriate CSS rules and media queries for those. 7 | 8 | ## Node.js module 9 | 10 | It is possible to run GSS-to-CSS precompilation as a Node.js library in your custom tooling. Example: 11 | 12 | ```js 13 | // Load the NPM module 14 | var precompiler = require('gss-to-css'); 15 | 16 | // Sizes configuration 17 | var options = { 18 | ranges: { 19 | width: { 20 | from: 400, 21 | to: 1000, 22 | step: 100 23 | }, 24 | height: 600 25 | } 26 | }; 27 | 28 | // Prepare a headless browser for the URL you're interested in 29 | precompiler.open('http://example.net', function (err, page, phantom) { 30 | 31 | // Create a version of the page with GSS converted to CSS media queries 32 | precompiler.gss2css(page, options, function (err, html) { 33 | // Serve or save the HTML string 34 | 35 | // Then close down the headless browser 36 | phantom.exit(); 37 | }); 38 | 39 | }); 40 | ``` 41 | 42 | See the [grunt sizes and ranges documentation](#optionssizes) on the sizing options to provide to the `gss2css` function. 43 | 44 | ## Grunt plugin 45 | 46 | ### Getting Started 47 | This plugin requires Grunt `~0.4.1` 48 | 49 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: 50 | 51 | ```shell 52 | npm install gss-to-css --save-dev 53 | ``` 54 | 55 | Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript: 56 | 57 | ```js 58 | grunt.loadNpmTasks('gss-to-css'); 59 | ``` 60 | 61 | ### The `gss_to_css` task 62 | 63 | #### Overview 64 | In your project's Gruntfile, add a section named `gss_to_css` to the data object passed into `grunt.initConfig()`. 65 | 66 | ```js 67 | grunt.initConfig({ 68 | gss_to_css: { 69 | options: { 70 | // Task-specific options go here 71 | }, 72 | precompile: { 73 | // Target-specific file lists and/or options go here. 74 | } 75 | }, 76 | }); 77 | ``` 78 | 79 | #### Options 80 | 81 | ##### options.baseUrl 82 | Type: `String` 83 | Default value: `http://localhost:8002/` 84 | 85 | Base URL to use for rendering the GSS-enabled pages. Must be a URL where both the HTML files and their assets, GSS included, are available. 86 | 87 | When working with local files the easiest option is to run the `gss_to_css` task together with a local web server provided by [grunt-contrib-connect](https://github.com/gruntjs/grunt-contrib-connect). 88 | 89 | ##### options.sizes 90 | Type: `Array` 91 | Default value: 92 | ```js 93 | [ 94 | { 95 | width: 1024, 96 | height: 768 97 | } 98 | ] 99 | ``` 100 | 101 | A list of sizes to render the page in and generate media queries. Useful when the page is targeting a known set of display resolutions, as is often the case when building mobile web apps. 102 | 103 | ##### options.ranges 104 | Type: `Object` 105 | Default value: `none` 106 | 107 | Ranges for width and height to utilize for producing the media queries. Allows compiling GSS into a set of responsive media queries. Overrides `options.sizes` when set. 108 | 109 | For example, to generate media queries for each screen size between 400x600 and 1400x600 in 20 pixel intervals, one could configure ranges with: 110 | 111 | ```js 112 | ranges: { 113 | width: { 114 | from: 400, 115 | to: 1400, 116 | step: 20 117 | }, 118 | height: 600 119 | } 120 | ``` 121 | 122 | Note that it is possible to configure ranges for both width and height, in which case all the size combinations will appear in the media queries. 123 | 124 | #### Usage examples 125 | In this example we'll build some local GSS-enabled HTML files into the equivalent CSS-powered ones. GSS and other dependencies are available in the local directory structure and the HTTP server is provided via grunt-contrib-connect. The files are stored in the `_site` folder: 126 | 127 | ```js 128 | grunt.initConfig({ 129 | connect: { 130 | server: { 131 | options: { 132 | port: 8002 133 | } 134 | } 135 | }, 136 | 137 | gss_to_css: { 138 | pages: { 139 | options: { 140 | baseUrl: 'http://localhost:8002/', 141 | sizes: [ 142 | { 143 | width: 800, 144 | height: 600 145 | }, 146 | { 147 | width: 1024, 148 | height: 768 149 | }, 150 | { 151 | width: 1900, 152 | height: 1080 153 | } 154 | ] 155 | }, 156 | files: [ 157 | { 158 | expand: true, 159 | cwd: '', 160 | src: ['src/*.html'] 161 | dest: '_site' 162 | } 163 | ] 164 | } 165 | } 166 | }); 167 | ``` 168 | 169 | ## Contributing 170 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/). 171 | -------------------------------------------------------------------------------- /spec/fixtures/profile-card/compiled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | GSS - Responsive Profile Card 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 25 | 26 | 63 | 64 |
65 |
66 |
67 |
68 |

Dan Daniels

69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var phantom = require('node-phantom-ws'); 2 | var jsdom = require('jsdom'); 3 | exports.open = function (url, options, callback) { 4 | if (!callback) { 5 | callback = options; 6 | options = {}; 7 | } 8 | if (!options.width) { 9 | options.width = 800; 10 | } 11 | if (!options.height) { 12 | options.height = 600; 13 | } 14 | phantom.create(function (err, ph) { 15 | if (err) { 16 | return callback(err); 17 | } 18 | ph.createPage(function (err,page) { 19 | if (err) { 20 | return callback(err); 21 | } 22 | page.set('viewportSize', { 23 | width: options.width, 24 | height: options.height 25 | }, function (err) { 26 | page.onError = function (msg, trace) { 27 | console.log(msg); 28 | }; 29 | 30 | var checkReady = function () { 31 | page.evaluate(function () { 32 | return GSS.isDisplayed; 33 | }, function (err, res) { 34 | if (res) { 35 | return callback(null, page); 36 | } 37 | setTimeout(checkReady, 10); 38 | }); 39 | }; 40 | 41 | page.open(url, function (err, status) { 42 | if (err) { 43 | return callback(err); 44 | } 45 | checkReady(); 46 | }); 47 | }); 48 | }); 49 | }, 50 | { 51 | phantomPath: require('phantomjs').path 52 | }); 53 | }; 54 | 55 | exports.resize = function (page, values, callback) { 56 | page.set('viewportSize', values, function (err) { 57 | if (err) { 58 | return callback(err); 59 | } 60 | setTimeout(function () { 61 | page.evaluate(function () { 62 | return GSS.engines.root.vars; 63 | }, function (err, vars) { 64 | if (err) { 65 | return callback(err); 66 | } 67 | callback(null, vars, page); 68 | }); 69 | }, 100); 70 | }); 71 | }; 72 | 73 | exports.removeGss = function (page, callback) { 74 | if (typeof page == 'object') { 75 | page.get('content', function (err, html) { 76 | if (err) { 77 | return callback(err); 78 | } 79 | exports.removeGss(html, callback); 80 | }); 81 | return; 82 | } 83 | var html = page; 84 | var window = jsdom.jsdom(html).createWindow(); 85 | 86 | // Remove inline and linked GSS 87 | var styles = window.document.querySelectorAll('[type="text/gss"]'); 88 | Array.prototype.slice.call(styles).forEach(function (style) { 89 | style.parentNode.removeChild(style); 90 | }); 91 | 92 | // Remove GSS engine 93 | var scripts = window.document.querySelectorAll('script[src]'); 94 | Array.prototype.slice.call(scripts).forEach(function (script) { 95 | if (script.src.indexOf('gss.js') === -1) { 96 | return; 97 | } 98 | script.parentNode.removeChild(script); 99 | }); 100 | 101 | // Remove GSS IDs 102 | var targets = window.document.querySelectorAll('[data-gss-id]'); 103 | Array.prototype.slice.call(targets).forEach(function (target) { 104 | target.removeAttribute('style'); 105 | target.removeAttribute('data-gss-id'); 106 | }); 107 | 108 | callback(null, window.document.doctype + "\n" + window.document.innerHTML); 109 | }; 110 | 111 | exports.injectCss = function (page, css, callback) { 112 | if (typeof page == 'object') { 113 | page.get('content', function (err, html) { 114 | if (err) { 115 | return callback(err); 116 | } 117 | exports.injectCss(html, css, callback); 118 | }); 119 | return; 120 | } 121 | var html = page; 122 | var window = jsdom.jsdom(html).createWindow(); 123 | 124 | var style = window.document.createElement('style'); 125 | style.textContent = css; 126 | window.document.head.appendChild(style); 127 | 128 | callback(null, window.document.doctype + "\n" + window.document.innerHTML); 129 | }; 130 | 131 | function rangeSteps (sizes, dimension, range) { 132 | var newSizes = []; 133 | var size; 134 | if (typeof range === 'number') { 135 | if (sizes.length) { 136 | sizes.forEach(function (size) { 137 | size[dimension] = range; 138 | newSizes.push(size); 139 | }); 140 | return newSizes; 141 | } 142 | size = {}; 143 | size[dimension] = range; 144 | newSizes.push(size); 145 | return newSizes; 146 | } 147 | var now = range.from; 148 | if (!range.step) { 149 | range.step = 10; 150 | } 151 | while (now <= range.to) { 152 | if (sizes.length) { 153 | for (var i = 0; i < sizes.length; i++) { 154 | size = JSON.parse(JSON.stringify(sizes[i])); 155 | size[dimension] = now; 156 | newSizes.push(size); 157 | } 158 | } else { 159 | size = {}; 160 | size[dimension] = now; 161 | newSizes.push(size); 162 | } 163 | now += range.step; 164 | } 165 | return newSizes; 166 | } 167 | 168 | exports.normalizeOptions = function (options) { 169 | if (options.ranges) { 170 | if (!options.ranges.width) { 171 | options.ranges.width = 800; 172 | } 173 | if (!options.ranges.height) { 174 | options.ranges.width = 600; 175 | } 176 | var sizes = []; 177 | sizes = rangeSteps(sizes, 'width', options.ranges.width); 178 | sizes = rangeSteps(sizes, 'height', options.ranges.height); 179 | delete options.ranges; 180 | options.sizes = sizes; 181 | return options; 182 | } 183 | if (!options.sizes) { 184 | options.sizes = [{ 185 | width: 800, 186 | height: 600 187 | }]; 188 | } 189 | return options; 190 | }; 191 | 192 | exports.gss2css = function (page, options, callback) { 193 | if (!callback) { 194 | callback = options; 195 | options = {}; 196 | } 197 | var css = "\n"; 198 | 199 | // Once we're done we can send the CSS 200 | var send = function (css) { 201 | exports.removeGss(page, function (err, cleaned) { 202 | if (err) { 203 | return callback(err); 204 | } 205 | exports.injectCss(cleaned, css, callback); 206 | }); 207 | }; 208 | 209 | var previous = null; 210 | var sizeToCss = function () { 211 | var size = options.sizes.shift(); 212 | exports.resize(page, size, function (err, vars) { 213 | page.evaluate(function () { 214 | return GSS.printCss(); 215 | }, function (err, vals) { 216 | vals = vals.replace(/}/g, '}\n '); 217 | if (options.sizes.length) { 218 | var next = options.sizes[0]; 219 | if (previous) { 220 | css += "\n@media (min-width: " + size.width + "px) and (max-width: " + (next.width-1) + "px) {\n " + vals + "\n}\n"; 221 | } else { 222 | css += "@media (max-width: " + (next.width-1) + "px) {\n " + vals + "\n}\n"; 223 | } 224 | previous = size; 225 | sizeToCss(); 226 | } else { 227 | css += "\n@media (min-width: " + size.width + "px) {\n " + vals + "\n}\n"; 228 | return send(css); 229 | } 230 | }); 231 | }); 232 | }; 233 | options = exports.normalizeOptions(options); 234 | sizeToCss(); 235 | }; 236 | -------------------------------------------------------------------------------- /spec/fixtures/profile-card/compiled-range.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | GSS - Responsive Profile Card 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 25 | 26 | 1119 | 1120 |
1121 |
1122 |
1123 |
1124 |

Dan Daniels

1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | --------------------------------------------------------------------------------