highcarb 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|