├── .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 [](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 |
--------------------------------------------------------------------------------