highcarb 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +47 -0
- data/README.md +143 -0
- data/bin/highcarb +8 -0
- data/lib/highcarb/assets_controller.rb +47 -0
- data/lib/highcarb/command.rb +61 -0
- data/lib/highcarb/generator.rb +71 -0
- data/lib/highcarb/rack_app.rb +59 -0
- data/lib/highcarb/services.rb +27 -0
- data/lib/highcarb/slides_controller.rb +136 -0
- data/lib/highcarb/sockets.rb +47 -0
- data/lib/highcarb/views_controller.rb +38 -0
- data/lib/highcarb.rb +5 -0
- data/resources/views/index.coffee +42 -0
- data/resources/views/index.haml +21 -0
- data/resources/views/remote.coffee +56 -0
- data/resources/views/remote.haml +61 -0
- metadata +165 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@highcarb
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
highcarb (0.1)
|
5
|
+
coffee-script
|
6
|
+
em-websocket
|
7
|
+
haml
|
8
|
+
kramdown
|
9
|
+
mime-types
|
10
|
+
nokogiri
|
11
|
+
sass
|
12
|
+
thin
|
13
|
+
trollop
|
14
|
+
|
15
|
+
GEM
|
16
|
+
remote: http://rubygems.org/
|
17
|
+
specs:
|
18
|
+
addressable (2.2.7)
|
19
|
+
coffee-script (2.2.0)
|
20
|
+
coffee-script-source
|
21
|
+
execjs
|
22
|
+
coffee-script-source (1.2.0)
|
23
|
+
daemons (1.1.8)
|
24
|
+
em-websocket (0.3.6)
|
25
|
+
addressable (>= 2.1.1)
|
26
|
+
eventmachine (>= 0.12.9)
|
27
|
+
eventmachine (0.12.10)
|
28
|
+
execjs (1.3.0)
|
29
|
+
multi_json (~> 1.0)
|
30
|
+
haml (3.1.4)
|
31
|
+
kramdown (0.13.5)
|
32
|
+
mime-types (1.17.2)
|
33
|
+
multi_json (1.1.0)
|
34
|
+
nokogiri (1.5.0)
|
35
|
+
rack (1.4.1)
|
36
|
+
sass (3.1.15)
|
37
|
+
thin (1.3.1)
|
38
|
+
daemons (>= 1.0.9)
|
39
|
+
eventmachine (>= 0.12.6)
|
40
|
+
rack (>= 1.0.0)
|
41
|
+
trollop (1.16.2)
|
42
|
+
|
43
|
+
PLATFORMS
|
44
|
+
ruby
|
45
|
+
|
46
|
+
DEPENDENCIES
|
47
|
+
highcarb!
|
data/README.md
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# HighCarb
|
2
|
+
|
3
|
+
HighCarb is a framework to create presentations, and to control them remotely.
|
4
|
+
|
5
|
+
The presentation is based on Deck.js
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
I have to create a gem (or event a .deb package). Right now, the easiest way to install it
|
10
|
+
is clone the repository and add an alias.
|
11
|
+
|
12
|
+
```
|
13
|
+
$ cd /somewhere/
|
14
|
+
$ git clone git://github.com/ayosec/highcarb.git
|
15
|
+
$ cd highcarb
|
16
|
+
$ bundle install
|
17
|
+
$ alias highcarb="ruby /somewhere/highcarb/bin/highcarb"
|
18
|
+
```
|
19
|
+
|
20
|
+
### Dependencies
|
21
|
+
|
22
|
+
* You have to install Pygmentize if you want to highlight the code snippets.
|
23
|
+
* A JavaScript interpreter is needed to compile the CoffeeScript source. Rhino or JavaScript can be used with no problems.
|
24
|
+
|
25
|
+
In Debian (and derived) everything can be installed with
|
26
|
+
|
27
|
+
```
|
28
|
+
$ sudo apt-get install nodejs python-pygments
|
29
|
+
```
|
30
|
+
|
31
|
+
## Generate a presentation project
|
32
|
+
|
33
|
+
The `-g` flag generate a new tree with the base for the presentation
|
34
|
+
|
35
|
+
```
|
36
|
+
$ highcarb -g /my/slides/foobar
|
37
|
+
```
|
38
|
+
|
39
|
+
## Adding content
|
40
|
+
|
41
|
+
The generated tree is something like
|
42
|
+
|
43
|
+
```
|
44
|
+
/slide
|
45
|
+
├── assets
|
46
|
+
│ ├── README
|
47
|
+
│ ├── base.scss
|
48
|
+
│ ├── remote.scss
|
49
|
+
│ ├── custom.coffee
|
50
|
+
│ ├── custom-remote.coffee
|
51
|
+
│ └── vendor
|
52
|
+
│ └── deck.js
|
53
|
+
│ ├── ...
|
54
|
+
│ └── ...
|
55
|
+
├── slides
|
56
|
+
│ └── 0001.haml
|
57
|
+
└── snippets
|
58
|
+
└── README
|
59
|
+
```
|
60
|
+
|
61
|
+
### Slides
|
62
|
+
|
63
|
+
The content can be wrote in HAML, MarkDown or in raw HTML.
|
64
|
+
|
65
|
+
The generator will concatenate all the files when the presentation is shown.
|
66
|
+
|
67
|
+
#### Special tags
|
68
|
+
|
69
|
+
`%snippet` is used to load a file from the `snippets` directory. If Pygmentize is found, the code will be highlighted. If not, the content will be shown in a monospace font.
|
70
|
+
|
71
|
+
`%asset` load a file from the `assets` directory. If the file is an image, an `img` will be created. If it is a CSS file (or SCSS), a `link` tag will be used. And, for JavaScript (or CoffeeScript) files, a `script` tag is used.
|
72
|
+
|
73
|
+
If type asset type can not be determined by the MIME type, a CSS class can be added to the `asset` tag to force the type. The class can be `image`, `style` or `javascript`
|
74
|
+
|
75
|
+
If the asset is something else, a link will be added with an anchor.
|
76
|
+
|
77
|
+
`%external` can be used to create link to external pages. The shown text is shorted to be less noisy.
|
78
|
+
|
79
|
+
#### Notes
|
80
|
+
|
81
|
+
Everything with the `note` CSS class will be removed from the slide. This content is accessible in the `remote` view.
|
82
|
+
|
83
|
+
## Assets
|
84
|
+
|
85
|
+
Every file from the `asset` directory is accessible from the `http://domain/asset/` URL.
|
86
|
+
|
87
|
+
If the file is a CoffeeScript source, it will be compiled as JavaScript before be sent. Same for SCSS.
|
88
|
+
|
89
|
+
## Example
|
90
|
+
|
91
|
+
With this files
|
92
|
+
|
93
|
+
```
|
94
|
+
/slide
|
95
|
+
├── assets
|
96
|
+
│ ├── hacks.coffee
|
97
|
+
└── first.png
|
98
|
+
└── snippets
|
99
|
+
└── README
|
100
|
+
```
|
101
|
+
|
102
|
+
We could write
|
103
|
+
|
104
|
+
```haml
|
105
|
+
|
106
|
+
%asset hacks.coffee
|
107
|
+
|
108
|
+
.slide
|
109
|
+
%h1 First slide
|
110
|
+
|
111
|
+
%asset first.png
|
112
|
+
|
113
|
+
.slide
|
114
|
+
%h1 Second one
|
115
|
+
|
116
|
+
%ul
|
117
|
+
%li.slide this
|
118
|
+
%li.slide and
|
119
|
+
%li.slide that
|
120
|
+
%li.slide
|
121
|
+
See this:
|
122
|
+
%external http://somewhere.tld/sometime
|
123
|
+
```
|
124
|
+
|
125
|
+
## View the presentation
|
126
|
+
|
127
|
+
|
128
|
+
```
|
129
|
+
$ highcarb /my/slides/foobar
|
130
|
+
```
|
131
|
+
|
132
|
+
Some options are available with the `--help` flag.
|
133
|
+
|
134
|
+
With the defaults options the web server will listen on 9090, so the presentation can
|
135
|
+
be see at http://localhost:9090/
|
136
|
+
|
137
|
+
To control it from another browser go to http://localhost:9090/remote. The remote view
|
138
|
+
show the full slides, so you can see everything. Left and right keys can be used to move
|
139
|
+
the slide of the remote browser.
|
140
|
+
|
141
|
+
There is no need to restart the server if the content is changed. Everything will be regenerated
|
142
|
+
when reload the page in the browser. The HTML generated for the snippets is cached. The cached key
|
143
|
+
is the MD5 sum of the content.
|
data/bin/highcarb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
require "sass"
|
3
|
+
|
4
|
+
module HighCarb
|
5
|
+
module AssetsController
|
6
|
+
def assets(asset)
|
7
|
+
if asset.include?("/../")
|
8
|
+
plain_response! 403, "URL can not contain /../"
|
9
|
+
end
|
10
|
+
|
11
|
+
asset_path = assets_root.join("./" + asset)
|
12
|
+
if not asset_path.exist?
|
13
|
+
not_found! asset
|
14
|
+
end
|
15
|
+
|
16
|
+
if not asset_path.file?
|
17
|
+
plain_response! 403, "#{asset} is not a file"
|
18
|
+
end
|
19
|
+
|
20
|
+
output = nil
|
21
|
+
mime_type = nil
|
22
|
+
|
23
|
+
# Process SASS
|
24
|
+
if asset_path.extname == ".scss"
|
25
|
+
output = Sass::Engine.for_file(asset_path.to_s, {}).render
|
26
|
+
mime_type = "text/css"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Process CoffeeScript
|
30
|
+
if asset_path.extname == ".coffee"
|
31
|
+
output = CoffeeScript.compile(asset_path.read)
|
32
|
+
mime_type = "application/javascript"
|
33
|
+
end
|
34
|
+
|
35
|
+
if output == nil
|
36
|
+
mime_type = MIME::Types.type_for(asset_path.to_s).first || "application/octet-stream"
|
37
|
+
output = asset_path.read
|
38
|
+
end
|
39
|
+
|
40
|
+
[
|
41
|
+
200,
|
42
|
+
{ "Content-Type" => mime_type.to_s },
|
43
|
+
output
|
44
|
+
]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
require "trollop"
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
require "highcarb"
|
6
|
+
require "highcarb/generator"
|
7
|
+
require "highcarb/services"
|
8
|
+
|
9
|
+
module HighCarb
|
10
|
+
class Command
|
11
|
+
attr_reader :options
|
12
|
+
attr_reader :command_line
|
13
|
+
attr_reader :args
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@command_line = @args = []
|
17
|
+
@options = {}
|
18
|
+
@logger = Logger.new(STDERR).tap {|logger| logger.level = Logger::WARN }
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse!(args)
|
22
|
+
@command_line = args.dup
|
23
|
+
@args = args
|
24
|
+
@options = Trollop.options(@args) do
|
25
|
+
opt "generate", "Generate a new highcarb project"
|
26
|
+
opt "server", "Start the servers (default action). See --http-port and --ws-port"
|
27
|
+
|
28
|
+
opt "http-port", "HTTP server port", default: 9090
|
29
|
+
opt "ws-port", "WebSockets port", default: 9091
|
30
|
+
|
31
|
+
opt "skip-libs", "Don't download vendor libraries, like Deck.js and jQuery"
|
32
|
+
|
33
|
+
opt "verbose", "Be verbose"
|
34
|
+
end
|
35
|
+
|
36
|
+
if @options["verbose"]
|
37
|
+
@logger.level = Logger::DEBUG
|
38
|
+
end
|
39
|
+
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def run!
|
44
|
+
if args.size != 1
|
45
|
+
STDERR.puts "Please indicate the project path as an extra argument of the command. For example:"
|
46
|
+
STDERR.puts "$ \033[1m#$0 #{command_line * " "} project-path/\033[m"
|
47
|
+
exit 1
|
48
|
+
end
|
49
|
+
|
50
|
+
if options["generate"]
|
51
|
+
# Generate a new project
|
52
|
+
HighCarb::Generator.new(self, args.first).run!
|
53
|
+
else
|
54
|
+
HighCarb::Services.start!(self, @logger)
|
55
|
+
end
|
56
|
+
|
57
|
+
rescue HighCarb::Error => error
|
58
|
+
STDERR.puts "ERROR: " + error.message
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
|
2
|
+
require "pathname"
|
3
|
+
|
4
|
+
module HighCarb
|
5
|
+
class Generator
|
6
|
+
|
7
|
+
class PathAlreadyExist < HighCarb::Error
|
8
|
+
def message
|
9
|
+
"The path exist and can not overridden"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :path
|
14
|
+
attr_reader :command
|
15
|
+
|
16
|
+
def initialize(command, path)
|
17
|
+
@command = command
|
18
|
+
@path = path
|
19
|
+
end
|
20
|
+
|
21
|
+
def run!
|
22
|
+
path = Pathname.new(self.path)
|
23
|
+
raise PathAlreadyExist if path.exist?
|
24
|
+
|
25
|
+
create_file path.join("slides/0001.haml"),
|
26
|
+
".slide\n" +
|
27
|
+
" %h1 Title\n" +
|
28
|
+
" Content\n"
|
29
|
+
|
30
|
+
create_file path.join("assets/README"),
|
31
|
+
"Put in this directory any file that you want to use in your presentation (images, et al)\n\n" +
|
32
|
+
"Files ending with .coffee will be compiled with CoffeeScript.\n" +
|
33
|
+
"Files ending with .scss will be compiled with SASS. Compass is available."
|
34
|
+
|
35
|
+
create_file path.join("assets/base.scss"),
|
36
|
+
"/*\n * Write here your own styles.\n" +
|
37
|
+
" * Compass modules are available\n */\n\n\n" +
|
38
|
+
"@import url('/assets/vendor/deck.js/themes/style/swiss.css');\n" +
|
39
|
+
"@import url('/assets/vendor/deck.js/themes/transition/horizontal-slide.css');\n"
|
40
|
+
|
41
|
+
create_file path.join("assets/remote.scss"), "/* Add here your styles for the /remote view */"
|
42
|
+
create_file path.join("assets/custom-remote.coffee"), "# Add here your own code for the /remote view"
|
43
|
+
create_file path.join("assets/custom.coffee"), "# Add here your own code for the views"
|
44
|
+
|
45
|
+
create_file path.join("snippets/README"),
|
46
|
+
"Put in this directory any snippet of code that you want to include in your presentation.\n" +
|
47
|
+
"You need to install Pygmentize if you want to format the code.\n" +
|
48
|
+
"The snippets are loaded with a <snippet>name.rb</snippet> tag.\n" +
|
49
|
+
"With Haml, you can use %snippet name.rb\n"
|
50
|
+
|
51
|
+
# Download deck.js, which will include jQuery
|
52
|
+
if not command.options["skip-libs"]
|
53
|
+
vendor_path = path.join("assets/vendor").expand_path
|
54
|
+
vendor_path.mkpath
|
55
|
+
Dir.chdir vendor_path do
|
56
|
+
puts "Downloading Deck.js into \033[1m#{vendor_path}\033[m..."
|
57
|
+
system "curl -L https://github.com/imakewebthings/deck.js/tarball/master | tar xzf -"
|
58
|
+
vendor_path.children.first.rename vendor_path.join("deck.js")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Helpers
|
64
|
+
|
65
|
+
def create_file(path, content)
|
66
|
+
puts "Create \033[1m#{path}\033[m"
|
67
|
+
path.dirname.mkpath
|
68
|
+
path.open "w" do |f| f.write content end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
|
2
|
+
require "mime/types"
|
3
|
+
require "pathname"
|
4
|
+
require "haml"
|
5
|
+
require "kramdown"
|
6
|
+
|
7
|
+
require "highcarb/assets_controller"
|
8
|
+
require "highcarb/slides_controller"
|
9
|
+
require "highcarb/views_controller"
|
10
|
+
|
11
|
+
module HighCarb
|
12
|
+
|
13
|
+
class RackApp
|
14
|
+
|
15
|
+
include SlidesController
|
16
|
+
include AssetsController
|
17
|
+
include ViewsController
|
18
|
+
|
19
|
+
attr_reader :command
|
20
|
+
attr_reader :root
|
21
|
+
attr_reader :assets_root
|
22
|
+
|
23
|
+
def initialize(command)
|
24
|
+
@command = command
|
25
|
+
@root = Pathname.new(command.args.first)
|
26
|
+
@assets_root = @root.join("./assets")
|
27
|
+
end
|
28
|
+
|
29
|
+
def plain_response!(status, content)
|
30
|
+
throw :response, [status, {'Content-Type' => 'text/plain'}, content]
|
31
|
+
end
|
32
|
+
|
33
|
+
def not_found!(path)
|
34
|
+
plain_response! 404, "Object #{path} not found"
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(env)
|
38
|
+
catch(:response) do
|
39
|
+
case env["PATH_INFO"]
|
40
|
+
when "/slides"
|
41
|
+
slides
|
42
|
+
|
43
|
+
when /\A\/assets\/(.*)\Z/
|
44
|
+
assets $1
|
45
|
+
|
46
|
+
when "/remote"
|
47
|
+
render_view "remote"
|
48
|
+
|
49
|
+
when "/"
|
50
|
+
render_view "index"
|
51
|
+
|
52
|
+
else
|
53
|
+
not_found! env["PATH_INFO"]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
require "thin"
|
3
|
+
require "em-websocket"
|
4
|
+
|
5
|
+
require "highcarb/rack_app"
|
6
|
+
require "highcarb/sockets"
|
7
|
+
|
8
|
+
module HighCarb
|
9
|
+
module Services
|
10
|
+
extend self
|
11
|
+
|
12
|
+
def start!(command, logger)
|
13
|
+
EM.run do
|
14
|
+
EM::WebSocket.start(host: '0.0.0.0', port: command.options["ws-port"] ) do |websocket|
|
15
|
+
WSConnection.new websocket, logger
|
16
|
+
end
|
17
|
+
|
18
|
+
Thin::Server.start(
|
19
|
+
'0.0.0.0',
|
20
|
+
command.options["http-port"],
|
21
|
+
Rack::Builder.new { run RackApp.new(command) }
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
|
2
|
+
require "digest/md5"
|
3
|
+
require "nokogiri"
|
4
|
+
|
5
|
+
module HighCarb
|
6
|
+
module SlidesController
|
7
|
+
def slides
|
8
|
+
output = []
|
9
|
+
|
10
|
+
# Load the content from the sources
|
11
|
+
root.join("slides").children.sort.each do |slide_file|
|
12
|
+
# Only use non-hidden files
|
13
|
+
if slide_file.file? and slide_file.basename.to_s !~ /^\./
|
14
|
+
case slide_file.extname.downcase
|
15
|
+
when ".haml"
|
16
|
+
output << Haml::Engine.new(slide_file.read).render
|
17
|
+
|
18
|
+
when ".html"
|
19
|
+
output << slide_file.read
|
20
|
+
|
21
|
+
when ".md"
|
22
|
+
output << Kramdown::Document.new(slide_file.read).to_html
|
23
|
+
|
24
|
+
else
|
25
|
+
STDERR.puts "\033[31mCan not parse #{slide_file}\033[m"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
page = Nokogiri::HTML.parse(output.join)
|
31
|
+
|
32
|
+
# Find the <snippet> tags and replace with the content from a
|
33
|
+
# file located under the snippet/ directory
|
34
|
+
page.search("snippet").each do |snippet_tag|
|
35
|
+
snippet_tag.replace load_snippet(snippet_tag.inner_text.strip)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Find the <asset> tags and replace with link, script or img,
|
39
|
+
# depending on the MIME type
|
40
|
+
page.search("asset").each do |asset_tag|
|
41
|
+
asset_tag.replace load_asset(asset_tag.inner_text.strip, asset_tag.attributes["class"].to_s.split)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Find the <external> tags and replace with <a>
|
45
|
+
# The text shown will be reduced
|
46
|
+
page.search("external").each do |external_tag|
|
47
|
+
href = ERB::Util.h(external_tag.inner_text.strip)
|
48
|
+
|
49
|
+
text = href.gsub(/\w+:\/+/, "")
|
50
|
+
text = text[0,45] + "…" if text.length > 45
|
51
|
+
|
52
|
+
external_tag.replace %[<a class="external" href="#{href}" target="_blank" title="Open #{href} in a new window">#{text}</a>]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Append a server side generated identifier. This helps to identify them
|
56
|
+
# both in presenter- and remote-control-mode
|
57
|
+
last_slide_id = 0
|
58
|
+
page.search(".slide").each do |slide_node|
|
59
|
+
last_slide_id += 1
|
60
|
+
slide_node["data-slide-id"] = last_slide_id.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
# Response with everything
|
64
|
+
output = page.at("body").inner_html
|
65
|
+
throw :response, [200, {'Content-Type' => 'text/html'}, output]
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_snippet(snippet_name)
|
69
|
+
snippet_path = root.join("snippets", snippet_name)
|
70
|
+
snippet_html_cached = root.join("tmp", "snippets",
|
71
|
+
Digest::MD5.new.tap {|digest| digest << snippet_path.read }.hexdigest + ".html")
|
72
|
+
|
73
|
+
if snippet_html_cached.exist? and snippet_html_cached.mtime > snippet_path.mtime
|
74
|
+
|
75
|
+
snippet_html_cached.read
|
76
|
+
|
77
|
+
else
|
78
|
+
|
79
|
+
content = begin
|
80
|
+
IO.popen(["pygmentize", "-f", "html", "-O", "noclasses=true", snippet_path.to_s]).read
|
81
|
+
rescue Errno::ENOENT
|
82
|
+
if not @pygmentize_error_shown
|
83
|
+
STDERR.puts "\033[31mpygmentize could not be used. You have to install it if you want to highlight the snippets."
|
84
|
+
STDERR.puts "The snippets will be included with no format\033[m"
|
85
|
+
@pygmentize_error_shown = true
|
86
|
+
end
|
87
|
+
|
88
|
+
%[<pre class="raw-snippet">#{ERB::Util.h snippet_path.read}</pre>]
|
89
|
+
end
|
90
|
+
|
91
|
+
snippet_html_cached.dirname.mkpath
|
92
|
+
snippet_html_cached.open("w") {|f| f.write content }
|
93
|
+
content
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def load_asset(asset_name, css_class = [])
|
98
|
+
asset_path = assets_root.join(asset_name)
|
99
|
+
asset_url = "/assets/#{ERB::Util.h asset_name}"
|
100
|
+
asset_type = nil
|
101
|
+
|
102
|
+
if not css_class.empty?
|
103
|
+
# Check if the css_class list contains any of the valid classes
|
104
|
+
asset_type = (%w(image style javascript) & css_class).first
|
105
|
+
end
|
106
|
+
|
107
|
+
if asset_type.nil?
|
108
|
+
# If the class attribute has no known class, infer it with the MIME type
|
109
|
+
mime_type = MIME::Types.type_for(asset_name).first
|
110
|
+
asset_type =
|
111
|
+
if (mime_type and mime_type.media_type == "image")
|
112
|
+
"image"
|
113
|
+
elsif mime_type.to_s == "text/css"
|
114
|
+
"style"
|
115
|
+
elsif mime_type.to_s == "application/javascript" or asset_path.extname == "coffee"
|
116
|
+
"javascript"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
case asset_type
|
121
|
+
when "image"
|
122
|
+
%[<img class="asset" src="#{asset_url}">]
|
123
|
+
|
124
|
+
when "style"
|
125
|
+
%[<link href="#{asset_url}" rel="stylesheet">]
|
126
|
+
|
127
|
+
when "javascript"
|
128
|
+
%[<script src="#{asset_url}"></script>]
|
129
|
+
|
130
|
+
else
|
131
|
+
%[<a href="#{asset_url}" target="_blank">#{ERB::Util.h asset_name}</script>]
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
module HighCarb
|
3
|
+
class WSConnection
|
4
|
+
|
5
|
+
class <<self
|
6
|
+
attr_accessor :connected_clients
|
7
|
+
attr_accessor :last_client_id
|
8
|
+
end
|
9
|
+
|
10
|
+
self.connected_clients = []
|
11
|
+
self.last_client_id = 0
|
12
|
+
|
13
|
+
attr_reader :client_id
|
14
|
+
attr_reader :websocket
|
15
|
+
attr_reader :logger
|
16
|
+
|
17
|
+
def initialize(websocket, logger)
|
18
|
+
@logger = logger
|
19
|
+
@client_id = (self.class.last_client_id += 1)
|
20
|
+
|
21
|
+
@websocket = websocket
|
22
|
+
websocket.onopen &method(:on_open)
|
23
|
+
websocket.onclose &method(:on_close)
|
24
|
+
websocket.onmessage &method(:on_msg)
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_open
|
28
|
+
logger.info { "[WS] Open client: #{client_id}" }
|
29
|
+
self.class.connected_clients << self
|
30
|
+
end
|
31
|
+
|
32
|
+
def on_close
|
33
|
+
logger.info { "[WS] Closed client: #{client_id}" }
|
34
|
+
self.class.connected_clients.delete self
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_msg(msg)
|
38
|
+
logger.info { "[WS] Message from #{client_id}: #{msg}" }
|
39
|
+
|
40
|
+
self.class.connected_clients.each do |client|
|
41
|
+
if client != self
|
42
|
+
client.websocket.send msg
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
require "json"
|
3
|
+
require "coffee-script"
|
4
|
+
|
5
|
+
module HighCarb
|
6
|
+
module ViewsController
|
7
|
+
|
8
|
+
DefaultViewsPath = Pathname.new(File.expand_path("../../../resources/views/", __FILE__))
|
9
|
+
|
10
|
+
class ViewContext
|
11
|
+
attr_reader :options, :root
|
12
|
+
def initialize(options, root)
|
13
|
+
@options = options
|
14
|
+
@root = root
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_coffe(source)
|
18
|
+
"<script>//<![CDATA[\n" + CoffeeScript.compile(root.join(source + ".coffee").read) + "\n//]]></script>"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def render_view(view_name)
|
23
|
+
view_path = root.join("views", view_name + ".haml")
|
24
|
+
if not view_path.exist?
|
25
|
+
view_path = DefaultViewsPath.join(view_name + ".haml")
|
26
|
+
end
|
27
|
+
|
28
|
+
if not view_path.exist?
|
29
|
+
not_found! view_name + " view"
|
30
|
+
end
|
31
|
+
|
32
|
+
output = Haml::Engine.new(view_path.read).render(ViewContext.new(command.options, view_path.dirname))
|
33
|
+
|
34
|
+
throw :response, [200, {'Content-Type' => 'text/html'}, output]
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/highcarb.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
window.WebSocket = MozWebSocket if MozWebSocket?
|
3
|
+
|
4
|
+
$ ->
|
5
|
+
|
6
|
+
# Load slides and initialize Deck.js
|
7
|
+
$(".deck-container").load "/slides",
|
8
|
+
->
|
9
|
+
$(".deck-container").find(".note").remove()
|
10
|
+
$.deck ".slide"
|
11
|
+
|
12
|
+
# Open a permanent connection to the server. With this channel
|
13
|
+
# we can receive commands to change the current slide
|
14
|
+
toJson = JSON.stringify
|
15
|
+
|
16
|
+
channel = new WebSocket WebSocketsURL
|
17
|
+
channel.onmessage = (msgEvent) ->
|
18
|
+
msg = JSON.parse(msgEvent.data)
|
19
|
+
|
20
|
+
switch msg.action
|
21
|
+
when "next-slide" then $.deck('next')
|
22
|
+
when "prev-slide" then $.deck('prev')
|
23
|
+
when "go-to"
|
24
|
+
for slide, index in $.deck('getSlides')
|
25
|
+
if slide.data("slide-id") == msg.slideId
|
26
|
+
$.deck("go", index)
|
27
|
+
break
|
28
|
+
|
29
|
+
channel.onopen = ->
|
30
|
+
channel.send toJson(ack: true)
|
31
|
+
|
32
|
+
# Send a notification to every other client when the current slide has changed
|
33
|
+
lastSlideSelectedEvent = -1
|
34
|
+
$(document).bind 'deck.change', (event, from, to) ->
|
35
|
+
if lastSlideSelectedEvent != -1
|
36
|
+
clearTimeout lastSlideSelectedEvent
|
37
|
+
|
38
|
+
lastSlideSelectedEvent = \
|
39
|
+
setTimeout ->
|
40
|
+
lastSlideSelectedEvent = -1
|
41
|
+
channel.send toJson(action: "slide-selected", slideId: $.deck('getSlide').data("slide-id"))
|
42
|
+
100
|
@@ -0,0 +1,21 @@
|
|
1
|
+
!!! 5
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%link{rel: "stylesheet", href: "/assets/base.scss"}
|
5
|
+
|
6
|
+
%body.presenter
|
7
|
+
.deck-container Loading...
|
8
|
+
|
9
|
+
%script{ src: "/assets/vendor/deck.js/jquery-1.7.min.js" }
|
10
|
+
%script{ src: "/assets/vendor/deck.js/modernizr.custom.js" }
|
11
|
+
%script{ src: "/assets/vendor/deck.js/core/deck.core.js" }
|
12
|
+
%script{ src: "/assets/vendor/deck.js/extensions/menu/deck.menu.js" }
|
13
|
+
%script{ src: "/assets/vendor/deck.js/extensions/goto/deck.goto.js" }
|
14
|
+
%script{ src: "/assets/vendor/deck.js/extensions/status/deck.status.js" }
|
15
|
+
%script{ src: "/assets/vendor/deck.js/extensions/hash/deck.hash.js" }
|
16
|
+
|
17
|
+
:javascript
|
18
|
+
window.WebSocketsURL = "ws://" + location.hostname + ":#{options["ws-port"].to_json}/";
|
19
|
+
|
20
|
+
= load_coffe "index"
|
21
|
+
%script{ src: "/assets/custom.coffee" }
|
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
window.WebSocket = MozWebSocket if MozWebSocket?
|
3
|
+
|
4
|
+
$ ->
|
5
|
+
$(".slides").
|
6
|
+
load("/slides").
|
7
|
+
delegate(".slide", "click", (event) ->
|
8
|
+
node = event.target
|
9
|
+
while node
|
10
|
+
slideId = $(node).data("slide-id")
|
11
|
+
if slideId
|
12
|
+
event.preventDefault()
|
13
|
+
channel.send toJson(action: "go-to", slideId: slideId)
|
14
|
+
return false
|
15
|
+
|
16
|
+
node = node.parentNode
|
17
|
+
|
18
|
+
true
|
19
|
+
)
|
20
|
+
|
21
|
+
toJson = JSON.stringify
|
22
|
+
|
23
|
+
channel = new WebSocket WebSocketsURL
|
24
|
+
channel.onmessage = (msgEvent) ->
|
25
|
+
msg = JSON.parse(msgEvent.data)
|
26
|
+
|
27
|
+
# Set the remote-selected class to the new slide
|
28
|
+
$(".remote-selected").removeClass "remote-selected"
|
29
|
+
item = $(".slide[data-slide-id=#{msg.slideId}]").
|
30
|
+
addClass("remote-selected")
|
31
|
+
|
32
|
+
# Keep the selected item always in the center
|
33
|
+
offset = item.offset()
|
34
|
+
if offset
|
35
|
+
newTop = offset.top - ($(window).height() - item.height()) / 2
|
36
|
+
#$("html").scrollTop newTop
|
37
|
+
$("html").animate(scrollTop: newTop, 300)
|
38
|
+
|
39
|
+
channel.onopen = ->
|
40
|
+
channel.send toJson(ack: true)
|
41
|
+
|
42
|
+
# Send commands to the presenter
|
43
|
+
nextSlide = -> channel.send toJson(action: "next-slide")
|
44
|
+
prevSlide = -> channel.send toJson(action: "prev-slide")
|
45
|
+
$(".actions .next").click nextSlide
|
46
|
+
$(".actions .prev").click prevSlide
|
47
|
+
|
48
|
+
$(document).keypress (event) ->
|
49
|
+
switch(event.keyCode)
|
50
|
+
when 37 then prevSlide()
|
51
|
+
when 39 then nextSlide()
|
52
|
+
else return
|
53
|
+
|
54
|
+
event.preventDefault()
|
55
|
+
|
56
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
!!! 5
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%title Remote control
|
5
|
+
%script{ src: "/assets/vendor/deck.js/jquery-1.7.min.js" }
|
6
|
+
%script{ src: "/assets/custom.coffee" }
|
7
|
+
%script{ src: "/assets/custom-remote.coffee" }
|
8
|
+
|
9
|
+
%style
|
10
|
+
:erb
|
11
|
+
body {
|
12
|
+
background: black;
|
13
|
+
color: white;
|
14
|
+
}
|
15
|
+
|
16
|
+
.slides > .slide {
|
17
|
+
background: white;
|
18
|
+
color: black;
|
19
|
+
border: 1px solid black;
|
20
|
+
padding: 1em;
|
21
|
+
margin: 1em;
|
22
|
+
}
|
23
|
+
|
24
|
+
.slide:hover {
|
25
|
+
cursor: pointer;
|
26
|
+
border: 1px solid black;
|
27
|
+
}
|
28
|
+
|
29
|
+
.remote-selected { background: #ffc !important; }
|
30
|
+
|
31
|
+
.actions {
|
32
|
+
position: fixed;
|
33
|
+
right: 1em;
|
34
|
+
top: 1em;
|
35
|
+
}
|
36
|
+
|
37
|
+
.actions span {
|
38
|
+
color: black;
|
39
|
+
background: gray;
|
40
|
+
display: inline-block;
|
41
|
+
font-size: 120%;
|
42
|
+
cursor: pointer;
|
43
|
+
margin: 0.1ex;
|
44
|
+
padding: 0.5ex;
|
45
|
+
}
|
46
|
+
|
47
|
+
%link{rel: "stylesheet", href: "/assets/base.scss"}
|
48
|
+
%link{rel: "stylesheet", href: "/assets/remote.scss"}
|
49
|
+
|
50
|
+
%body.remote
|
51
|
+
.actions
|
52
|
+
%span.prev Prev
|
53
|
+
%span.next Next
|
54
|
+
|
55
|
+
.slides Loading ...
|
56
|
+
|
57
|
+
:javascript
|
58
|
+
window.WebSocketsURL = "ws://" + location.hostname + ":#{options["ws-port"].to_json}/";
|
59
|
+
|
60
|
+
= load_coffe "remote"
|
61
|
+
|
metadata
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: highcarb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ayose Cazorla
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thin
|
16
|
+
requirement: &4114200 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *4114200
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mime-types
|
27
|
+
requirement: &4109560 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *4109560
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: em-websocket
|
38
|
+
requirement: &4104920 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *4104920
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: trollop
|
49
|
+
requirement: &4103380 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *4103380
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: nokogiri
|
60
|
+
requirement: &4102700 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *4102700
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: haml
|
71
|
+
requirement: &4100660 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *4100660
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: sass
|
82
|
+
requirement: &4100060 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :runtime
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *4100060
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: kramdown
|
93
|
+
requirement: &4099460 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :runtime
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *4099460
|
102
|
+
- !ruby/object:Gem::Dependency
|
103
|
+
name: coffee-script
|
104
|
+
requirement: &4098560 !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :runtime
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: *4098560
|
113
|
+
description: HighCarb can build a presentation based on HAML, Markdown or raw HTML,
|
114
|
+
and let to control them from another browser, via WebSockets.
|
115
|
+
email:
|
116
|
+
- ayosec@gmail.com
|
117
|
+
executables:
|
118
|
+
- highcarb
|
119
|
+
extensions: []
|
120
|
+
extra_rdoc_files: []
|
121
|
+
files:
|
122
|
+
- .gitignore
|
123
|
+
- .rvmrc
|
124
|
+
- Gemfile
|
125
|
+
- Gemfile.lock
|
126
|
+
- README.md
|
127
|
+
- bin/highcarb
|
128
|
+
- lib/highcarb.rb
|
129
|
+
- lib/highcarb/assets_controller.rb
|
130
|
+
- lib/highcarb/command.rb
|
131
|
+
- lib/highcarb/generator.rb
|
132
|
+
- lib/highcarb/rack_app.rb
|
133
|
+
- lib/highcarb/services.rb
|
134
|
+
- lib/highcarb/slides_controller.rb
|
135
|
+
- lib/highcarb/sockets.rb
|
136
|
+
- lib/highcarb/views_controller.rb
|
137
|
+
- resources/views/index.coffee
|
138
|
+
- resources/views/index.haml
|
139
|
+
- resources/views/remote.coffee
|
140
|
+
- resources/views/remote.haml
|
141
|
+
homepage: https://github.com/ayosec/highcarb
|
142
|
+
licenses: []
|
143
|
+
post_install_message:
|
144
|
+
rdoc_options: []
|
145
|
+
require_paths:
|
146
|
+
- lib
|
147
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
148
|
+
none: false
|
149
|
+
requirements:
|
150
|
+
- - ! '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
154
|
+
none: false
|
155
|
+
requirements:
|
156
|
+
- - ! '>='
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
requirements: []
|
160
|
+
rubyforge_project:
|
161
|
+
rubygems_version: 1.8.10
|
162
|
+
signing_key:
|
163
|
+
specification_version: 3
|
164
|
+
summary: Slides manager based on Deck.js
|
165
|
+
test_files: []
|