fuse 0.1.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.
- checksums.yaml +15 -0
- data/LICENSE +13 -0
- data/README.md +53 -0
- data/bin/fuse +3 -0
- data/lib/fuse.rb +24 -0
- data/lib/fuse/document.rb +140 -0
- data/lib/fuse/document/asset.rb +72 -0
- data/lib/fuse/document/asset_collection.rb +33 -0
- data/lib/fuse/document/asset_types.rb +145 -0
- data/lib/fuse/exceptions.rb +25 -0
- data/lib/fuse/main.rb +129 -0
- data/lib/fuse/server.rb +71 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZDNiOTM4NDIyZTA2ZjNkYWQ5MmViMGEwNzZhM2JkZmE1OWYxMGI4MA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YWY3MmY3YzEyOTY1ZDNhMDIwZTAzMzExN2FlYTMyZDY0NGJjYmYyNA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZmMwNzExMGExNjVmZWEyYjY3ZmM2OTNiYzgwYTcxZGFjY2YzZDdkZTM5MGRj
|
10
|
+
ODI4ZTdmMzBhNDM0OWZkNTQ1M2ZkMDFiZTIzZjkyYTQwNTQwOTdjZmIwYmEz
|
11
|
+
NzlkNGZmMDUwYmM1NDQ3MGY3YmMzNTIyZjExYWQzNWY1Yzc5MDI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MzJmNDEwOGIzYTBiNGVkMDNjYTk2Y2RlNDIxNWVjNjBhYzZkZjcyNjVkN2M1
|
14
|
+
ODg2YTM1Njc2YjNlNWI4NzkyMTcxNjMzM2E2Y2EzZWYyMmU1NGYxOTQyYjc5
|
15
|
+
YjU4NDE3YjY1NjVkNGI4NTdkMDVkOGYxY2IyMTA0OTk2ZGFkMDQ=
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2013 Neil E. Pearson
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Fuse
|
2
|
+
|
3
|
+
Portable document authoring. Fuse HTML, JavaScript, CSS, images and fonts into standalone HTML files.
|
4
|
+
|
5
|
+
* Drop all your assets in a directory and hit the magic button.
|
6
|
+
* Built-in web server based on Thin for authoring your docs.
|
7
|
+
* Support for SASS and CoffeeScript.
|
8
|
+
* Sprockets-like 'require' syntax for JS and CSS dependency
|
9
|
+
* Uses uglify-js to compress JavaScript
|
10
|
+
* Simple file naming conventions for font names and CSS media types
|
11
|
+
* Transform XML documents on the fly using XSLT
|
12
|
+
|
13
|
+
## How to use Fuse
|
14
|
+
|
15
|
+
### Authoring
|
16
|
+
|
17
|
+
Put some HTML, CSS, JavaScript etc in a directory. `cd` to that directory and run:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
fuse server
|
21
|
+
```
|
22
|
+
|
23
|
+
Go to `http://localhost:9460` to view your doc.
|
24
|
+
|
25
|
+
### Compiling
|
26
|
+
|
27
|
+
When you're happy, from the same directory, run:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
fuse compile > my_doc.html
|
31
|
+
```
|
32
|
+
|
33
|
+
Presto.
|
34
|
+
|
35
|
+
## Command Line Options
|
36
|
+
|
37
|
+
Run `fuse` for a full list of command line options.
|
38
|
+
|
39
|
+
Some things you can do from the command line:
|
40
|
+
|
41
|
+
* Specify a port for the server
|
42
|
+
* Enable/disable asset embedding and/or compression
|
43
|
+
* Specify a source document and/or XSL stylesheet
|
44
|
+
* Specify the output HTML document's character set
|
45
|
+
* Specify an HTML title
|
46
|
+
|
47
|
+
## Early days
|
48
|
+
|
49
|
+
This gem is truly in its infancy. I'll put in what time I have available. It's also my first gem, so I welcome suggestions, pull requests etc.
|
50
|
+
|
51
|
+
## License
|
52
|
+
|
53
|
+
Released under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). See [LICENSE](LICENSE) for details.
|
data/bin/fuse
ADDED
data/lib/fuse.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'thin'
|
3
|
+
|
4
|
+
module Fuse
|
5
|
+
|
6
|
+
VERSION = '0.1.1'
|
7
|
+
|
8
|
+
def self.root
|
9
|
+
@root ||= File.expand_path File.dirname(__FILE__)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
$:.unshift Fuse.root unless $:.include?(Fuse.root)
|
15
|
+
|
16
|
+
require 'fuse/main'
|
17
|
+
require 'fuse/exceptions'
|
18
|
+
require 'fuse/server'
|
19
|
+
require 'fuse/document'
|
20
|
+
require 'fuse/document/asset'
|
21
|
+
require 'fuse/document/asset_types'
|
22
|
+
require 'fuse/document/asset_collection'
|
23
|
+
|
24
|
+
Fuse.main if $0 == __FILE__
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
class Fuse::Document
|
4
|
+
|
5
|
+
attr_reader :source_path, :xsl_path
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
source = (@options = options)[:source]
|
9
|
+
raise Fuse::Exception::SourceUnknown if source.nil?
|
10
|
+
raise Fuse::Exception::SourceUnknown::NotFound.new(source) unless File.exists?(source)
|
11
|
+
@source_path = expect_one(potential_sources, :source, Fuse::Exception::SourceUnknown)
|
12
|
+
@xsl_path = expect_one(potential_xsl, :xsl, Fuse::Exception::XslMissing) if source_xml?
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
result.to_html encoding: @options[:encoding]
|
17
|
+
end
|
18
|
+
|
19
|
+
def result
|
20
|
+
|
21
|
+
#todo find a way to transform Nokogiri XML to HTML without serializing
|
22
|
+
|
23
|
+
document = if xsl_path
|
24
|
+
Nokogiri::HTML(Nokogiri::XSLT(File.read xsl_path).transform(Nokogiri::XML(File.read source_path)).to_html encoding: @options[:encoding])
|
25
|
+
else
|
26
|
+
Nokogiri::HTML(File.read source_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
html = document.css('> html').first
|
30
|
+
body = html.css('> body').first
|
31
|
+
head = html.css('> head').first || body.add_previous_sibling(Nokogiri::XML::Node.new 'head', document)
|
32
|
+
|
33
|
+
document.title = @options[:title] unless @options[:title].nil?
|
34
|
+
|
35
|
+
#attach stylesheets and scripts
|
36
|
+
[Fuse::Document::Asset::StyleSheet, Fuse::Document::Asset::JavaScript].each do |klass|
|
37
|
+
collection = assets.of_type(klass).sort!
|
38
|
+
next unless collection.length > 0
|
39
|
+
if @options[:embed_assets]
|
40
|
+
tag = Nokogiri::XML::Node.new(klass::EMBED_WITH, document)
|
41
|
+
raw = collection.map do |asset|
|
42
|
+
tag['type'] = asset.type
|
43
|
+
(@options[:compress_assets] ? asset.compress : asset.filtered).strip
|
44
|
+
end.reject{ |x| x.length == 0 }.join(klass::JOIN_WITH)
|
45
|
+
next unless raw.length > 0
|
46
|
+
tag.content = raw
|
47
|
+
head << tag
|
48
|
+
else
|
49
|
+
collection.each do |asset|
|
50
|
+
data = asset.reference_with
|
51
|
+
tag = Nokogiri::XML::Node.new(data[:tag_name], document)
|
52
|
+
data[:attributes].each { |k, v| tag[k] = v unless v.nil? }
|
53
|
+
head << tag
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
#create font stylesheet
|
59
|
+
font_css = ''
|
60
|
+
fonts = {}
|
61
|
+
assets.of_type(Fuse::Document::Asset::Font).each do |asset|
|
62
|
+
(fonts[asset.face] ||= {})[asset.extension.to_sym] = asset
|
63
|
+
end
|
64
|
+
if fonts.length > 0
|
65
|
+
fonts.values.each do |formats|
|
66
|
+
first = formats.values.first
|
67
|
+
font_css << '@font-face{'
|
68
|
+
font_css << 'font-family: "%s";' % first.family
|
69
|
+
font_css << 'font-weight: %s;' % first.weight
|
70
|
+
font_css << 'font-style: %s;' % first.style
|
71
|
+
font_css << 'src: url("%s");' % formats[:eot].relative_path if formats[:eot]
|
72
|
+
css_formats = []
|
73
|
+
Fuse::Document::Asset::Font::CSS_FORMATS.each do |css_format|
|
74
|
+
css_formats << 'url("%s") format("%s")' % [
|
75
|
+
formats[css_format[:extension]].relative_path,
|
76
|
+
css_format[:format]
|
77
|
+
] if formats[css_format[:extension]]
|
78
|
+
end
|
79
|
+
font_css << 'src: %s;' % css_formats.join(', ') if css_formats.any?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
unless font_css.empty?
|
83
|
+
style_node = head.css('style:not([media]), style[media=all]').first || head.add_child(Nokogiri::XML::Node.new 'style', document)
|
84
|
+
style_node.content = font_css + style_node.content
|
85
|
+
end
|
86
|
+
|
87
|
+
#embed images and fonts
|
88
|
+
if @options[:embed_assets]
|
89
|
+
%w|@src @href @style style|.each do |xpath|
|
90
|
+
document.xpath('//' + xpath).each do |node|
|
91
|
+
assets.of_type(Fuse::Document::Asset::Image, Fuse::Document::Asset::Font).each do |asset|
|
92
|
+
node.content = node.content.gsub asset.relative_path, asset.to_datauri
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
document
|
99
|
+
end
|
100
|
+
|
101
|
+
def root
|
102
|
+
@root ||= File.directory?((opt = @options[:source])) ? opt : File.dirname(opt)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def source_xml?
|
108
|
+
source_path.match(/\.xml$/i)
|
109
|
+
end
|
110
|
+
|
111
|
+
def potential_xsl
|
112
|
+
@potential_xsl ||= potentials(:xsl, %w(xsl))
|
113
|
+
end
|
114
|
+
|
115
|
+
def potential_sources
|
116
|
+
@potential_docs ||= begin
|
117
|
+
extensions = %w|html htm|
|
118
|
+
extensions << 'xml' if potential_xsl.any?
|
119
|
+
potentials(:source, extensions)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def potentials(option, extensions)
|
124
|
+
if (option_value = @options[option])
|
125
|
+
raise Fuse::Exception::SourceUnknown::NotFound.new(option_value) unless File.exists?(option_value)
|
126
|
+
return [option_value] unless File.directory? option_value
|
127
|
+
end
|
128
|
+
Dir[File.join(option_value || root, "**/*.{#{extensions.join(',')}}")]
|
129
|
+
end
|
130
|
+
|
131
|
+
def expect_one(list, option, missing_exception)
|
132
|
+
raise missing_exception if list.empty?
|
133
|
+
raise Fuse::Exception::SourceUnknown::TooManySources.new(option, list) if list.length > 1
|
134
|
+
list.first
|
135
|
+
end
|
136
|
+
|
137
|
+
def assets
|
138
|
+
@assets ||= Asset[root]
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
class Fuse::Document::Asset
|
4
|
+
|
5
|
+
def self.[](dir)
|
6
|
+
assets = Fuse::Document::AssetCollection.new
|
7
|
+
Dir[File.join dir, '**/*.*'].each do |full_path|
|
8
|
+
asset = self.for(full_path[dir.length..-1], dir)
|
9
|
+
assets << asset if asset
|
10
|
+
end
|
11
|
+
assets
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.for(path, root = Dir.pwd)
|
15
|
+
full_path = File.join root, path
|
16
|
+
return unless File.exists? full_path
|
17
|
+
type = TYPES[(File.extname(path)[1..-1] || '').to_sym]
|
18
|
+
(@cache ||= {})[File.expand_path full_path] ||= type.new(path, root) if type
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :path
|
22
|
+
|
23
|
+
def initialize(path, root)
|
24
|
+
@root = root
|
25
|
+
@path = path
|
26
|
+
end
|
27
|
+
|
28
|
+
def full_path
|
29
|
+
@full_path ||= File.join @root, @path
|
30
|
+
end
|
31
|
+
|
32
|
+
def relative_path
|
33
|
+
@relative_path ||= path.sub(%r`^/`, '')
|
34
|
+
end
|
35
|
+
|
36
|
+
def raw
|
37
|
+
@raw ||= File.open(full_path, 'rb') { |f| f.read }
|
38
|
+
end
|
39
|
+
|
40
|
+
def filtered
|
41
|
+
@filtered ||= filter? ? filter : raw
|
42
|
+
end
|
43
|
+
|
44
|
+
def filter?
|
45
|
+
respond_to? :filter
|
46
|
+
end
|
47
|
+
|
48
|
+
def call(env)
|
49
|
+
if filter?
|
50
|
+
body = filter
|
51
|
+
[200, {'Content-Type' => type, 'Content-Length' => body.length.to_s}, [body]]
|
52
|
+
else
|
53
|
+
Rack::File.new(@root).call(env)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def type
|
58
|
+
@type ||= Rack::Mime.mime_type('.' + extension)
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_datauri(compress = false)
|
62
|
+
'data:%s;base64,%s' % [
|
63
|
+
type,
|
64
|
+
Base64.strict_encode64(compress && respond_to?(:compress) ? self.compress : raw)
|
65
|
+
]
|
66
|
+
end
|
67
|
+
|
68
|
+
def extension
|
69
|
+
@extension ||= File.extname(path).downcase[1..-1]
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Fuse::Document::AssetCollection < Array
|
2
|
+
|
3
|
+
def of_type(*types)
|
4
|
+
self.class.new select { |a|
|
5
|
+
ret = types.include? a.class
|
6
|
+
types.each { |type| ret ||= a.is_a? type } unless ret
|
7
|
+
ret
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
def sort!
|
12
|
+
unsorted = Array.new(self)
|
13
|
+
clear
|
14
|
+
unsorted.each { |a| push_with_dependents a }
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def sort
|
19
|
+
self.class.new(self).sort!
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(*args)
|
23
|
+
args.each { |arg| super(arg) unless include? arg }
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def push_with_dependents(asset)
|
28
|
+
#todo check for circular references
|
29
|
+
asset.dependents.each { |d| push_with_dependents d }
|
30
|
+
self << asset
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'sass'
|
2
|
+
require 'coffee-script'
|
3
|
+
require 'uglifier'
|
4
|
+
|
5
|
+
class Fuse::Document::Asset
|
6
|
+
|
7
|
+
module HasDependents
|
8
|
+
COMMENT_PATTERN = %r`^\s*(/\*[\s\S]*?\*/|(\s*//.*\s+)+)`
|
9
|
+
REQUIRE_PATTERN = %r`^\s*(?:\*|//)=\s+(require|require_glob)\s+(.+?)\s*$`
|
10
|
+
|
11
|
+
def dependents
|
12
|
+
ret = Fuse::Document::AssetCollection.new
|
13
|
+
local_root = File.dirname(full_path)
|
14
|
+
if (comments = raw[COMMENT_PATTERN])
|
15
|
+
comments.lines.each do |line|
|
16
|
+
if (match = REQUIRE_PATTERN.match(line))
|
17
|
+
case match[1]
|
18
|
+
when 'require'
|
19
|
+
[File.join(local_root, match[2])]
|
20
|
+
when 'require_glob'
|
21
|
+
Dir[File.join(local_root, match[2])]
|
22
|
+
else
|
23
|
+
[]
|
24
|
+
end.map do |p|
|
25
|
+
Fuse::Document::Asset.for(p[@root.length..-1], @root)
|
26
|
+
end.reject do |p|
|
27
|
+
p.nil?
|
28
|
+
end.each do |p|
|
29
|
+
ret << p
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
ret
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class StyleSheet < self
|
39
|
+
EMBED_WITH = 'style'
|
40
|
+
JOIN_WITH = ''
|
41
|
+
MEDIA_PATTERN = /\(([a-z]+(?:,\s*[a-z]+)*)\)\.[a-z]+$/i
|
42
|
+
include HasDependents
|
43
|
+
def reference_with
|
44
|
+
{
|
45
|
+
tag_name: 'link',
|
46
|
+
attributes: {
|
47
|
+
rel: 'stylesheet',
|
48
|
+
href: relative_path,
|
49
|
+
media: media
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
def media
|
54
|
+
@media ||= (match = MEDIA_PATTERN.match(path)) && match[1].split(/,\s*/).sort.join(', ')
|
55
|
+
end
|
56
|
+
def compress; ::Sass.compile raw, style: :compressed end
|
57
|
+
def type; 'text/css' end
|
58
|
+
class Sass < self
|
59
|
+
def filter; ::Sass.compile raw, style: :expanded end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class JavaScript < self
|
64
|
+
EMBED_WITH = 'script'
|
65
|
+
JOIN_WITH = ';'
|
66
|
+
include HasDependents
|
67
|
+
def reference_with
|
68
|
+
{
|
69
|
+
tag_name: 'script',
|
70
|
+
attributes: {
|
71
|
+
type: type,
|
72
|
+
src: relative_path
|
73
|
+
}
|
74
|
+
}
|
75
|
+
end
|
76
|
+
def compress; Uglifier.compile filtered end
|
77
|
+
def type; 'text/javascript' end
|
78
|
+
class Coffee < self
|
79
|
+
def filter; CoffeeScript.compile raw end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class Image < self
|
84
|
+
end
|
85
|
+
|
86
|
+
class Font < self
|
87
|
+
CSS_FORMATS = [
|
88
|
+
{ extension: :woff, format: 'woff' },
|
89
|
+
{ extension: :ttf, format: 'truetype' },
|
90
|
+
{ extension: :otf, format: 'opentype' }
|
91
|
+
]
|
92
|
+
MIME_TYPES = {
|
93
|
+
otf: 'application/x-font-opentype',
|
94
|
+
ttf: 'application/x-font-truetype',
|
95
|
+
}
|
96
|
+
VARIANT_PATTERN = %r`([^/]+?)(?:[-_ ](normal|bold|bolder|lighter|[1-9]00))?(?:[-_ ](normal|italic|oblique))?\.[a-z]+$`
|
97
|
+
def family; @family ||= variant[:family] end
|
98
|
+
def weight; @weight ||= variant[:weight] end
|
99
|
+
def style; @style ||= variant[:style] end
|
100
|
+
def variant
|
101
|
+
@variant ||= begin
|
102
|
+
match = VARIANT_PATTERN.match(path)
|
103
|
+
{
|
104
|
+
family: match[1],
|
105
|
+
weight: match[2] || 'normal',
|
106
|
+
style: match[3] || 'normal'
|
107
|
+
}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
def face; @face ||= [family, weight, style].join('-') end
|
111
|
+
def type
|
112
|
+
MIME_TYPES[extension.to_sym] || super
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class Xml < self
|
117
|
+
end
|
118
|
+
|
119
|
+
class Xsl < self
|
120
|
+
end
|
121
|
+
|
122
|
+
class Html < self
|
123
|
+
end
|
124
|
+
|
125
|
+
TYPES = {
|
126
|
+
css: StyleSheet,
|
127
|
+
scss: StyleSheet::Sass,
|
128
|
+
sass: StyleSheet::Sass,
|
129
|
+
js: JavaScript,
|
130
|
+
coffee: JavaScript::Coffee,
|
131
|
+
jpg: Image,
|
132
|
+
jpeg: Image,
|
133
|
+
png: Image,
|
134
|
+
gif: Image,
|
135
|
+
svg: Image,
|
136
|
+
ttf: Font,
|
137
|
+
woff: Font,
|
138
|
+
eot: Font,
|
139
|
+
otf: Font,
|
140
|
+
xml: Xml,
|
141
|
+
xsl: Xsl,
|
142
|
+
htm: Html,
|
143
|
+
html: Html
|
144
|
+
}
|
145
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Fuse::Exception < ::RuntimeError
|
2
|
+
class SourceUnknown < self
|
3
|
+
class TooManySources < self
|
4
|
+
attr_reader :option_name, :options
|
5
|
+
def initialize(option_name, options)
|
6
|
+
@option_name = option_name
|
7
|
+
@options = options
|
8
|
+
end
|
9
|
+
end
|
10
|
+
class NotFound < self
|
11
|
+
attr_reader :path
|
12
|
+
def initialize(path)
|
13
|
+
@path = path
|
14
|
+
end
|
15
|
+
def message; "Couldn't find '#{path}'." end
|
16
|
+
end
|
17
|
+
def message; 'Couldn\'t determine source document. Please specify one with --source.' end
|
18
|
+
end
|
19
|
+
class XslMissing
|
20
|
+
def message
|
21
|
+
'Couldn\'t locate an XSL stylesheet to transform the source document. Please specify one with --xsl.'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
def message; end
|
25
|
+
end
|
data/lib/fuse/main.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'thin'
|
3
|
+
|
4
|
+
module Fuse
|
5
|
+
|
6
|
+
DEFAULTS = {
|
7
|
+
common: {
|
8
|
+
source: '.',
|
9
|
+
encoding: 'UTF-8'
|
10
|
+
},
|
11
|
+
server: {
|
12
|
+
port: 9460,
|
13
|
+
embed_assets: false
|
14
|
+
},
|
15
|
+
compile: {
|
16
|
+
embed_assets: true,
|
17
|
+
compress_assets: true
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
def self.main
|
22
|
+
|
23
|
+
options = {}
|
24
|
+
|
25
|
+
options_parser = OptionParser.new do |opts|
|
26
|
+
|
27
|
+
opts.banner = 'Usage: fuse [command] [options]'
|
28
|
+
opts.separator ''
|
29
|
+
opts.separator 'Commands:'
|
30
|
+
opts.separator ' server : Run a local testing server'
|
31
|
+
opts.separator ' compile : Compile the document and send to STDOUT'
|
32
|
+
opts.separator ''
|
33
|
+
opts.separator 'Options:'
|
34
|
+
|
35
|
+
opts.on('-c',
|
36
|
+
'--[no-]compress-assets',
|
37
|
+
'Compress assets.'
|
38
|
+
) do |compress|
|
39
|
+
options[:compress_assets] = compress
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on('-e',
|
43
|
+
'--encoding CHARSET',
|
44
|
+
"Output encoding. Default is #{DEFAULTS[:common][:encoding]}."
|
45
|
+
) do |e|
|
46
|
+
options[:encoding] = e
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on('-m',
|
50
|
+
'--[no-]embed-assets',
|
51
|
+
'Embed assets.'
|
52
|
+
) do |embed|
|
53
|
+
options[:embed_assets] = embed
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('-p',
|
57
|
+
'--port PORT',
|
58
|
+
Integer,
|
59
|
+
"Port on which to listen (only with 'server' command). Default is #{DEFAULTS[:server][:port]}."
|
60
|
+
) do |port|
|
61
|
+
options[:port] = port
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on('-s',
|
65
|
+
'--source [FILE|DIR]',
|
66
|
+
'The source directory, or HTML or XML document. Default is current directory.'
|
67
|
+
) do |doc|
|
68
|
+
options[:source] = doc
|
69
|
+
end
|
70
|
+
|
71
|
+
opts.on('-t',
|
72
|
+
'--title TITLE',
|
73
|
+
'HTML document title.'
|
74
|
+
) do |t|
|
75
|
+
options[:title] = t
|
76
|
+
end
|
77
|
+
|
78
|
+
opts.on('-x',
|
79
|
+
'--xsl FILE',
|
80
|
+
'XSL transformation stylesheet. Default is current directory.'
|
81
|
+
) do |xsl|
|
82
|
+
abort "#{xsl} isn't a valid XSL stylesheet" unless xsl.match(/\.xsl$/i)
|
83
|
+
options[:xsl] = xsl
|
84
|
+
end
|
85
|
+
|
86
|
+
opts.on_tail('-h', '--help', 'Show this message.') { puts opts.to_s }
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
options_parser.parse!
|
91
|
+
|
92
|
+
options = merge_defaults(options)
|
93
|
+
|
94
|
+
case ARGV[0]
|
95
|
+
|
96
|
+
when 'server'
|
97
|
+
Thin::Server.start('0.0.0.0', options[:port]) do
|
98
|
+
use Rack::ShowExceptions
|
99
|
+
run Server.new(options)
|
100
|
+
end
|
101
|
+
|
102
|
+
when 'compile'
|
103
|
+
begin
|
104
|
+
print Document.new(options).to_s
|
105
|
+
rescue Exception::SourceUnknown::TooManySources
|
106
|
+
$stderr.puts "Found more than one potential #{$!.option_name} document. Please specify one with --#{$!.option_name}."
|
107
|
+
$stderr.puts $!.options.join "\n"
|
108
|
+
rescue Exception
|
109
|
+
$!.message ? $stderr.puts($!.message) : raise
|
110
|
+
end
|
111
|
+
|
112
|
+
else
|
113
|
+
puts options_parser
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def self.merge_defaults(options)
|
122
|
+
if (defaults = DEFAULTS[(ARGV[0] || '').to_sym])
|
123
|
+
defaults.merge(DEFAULTS[:common]).merge(options)
|
124
|
+
else
|
125
|
+
options
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
data/lib/fuse/server.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
class Fuse::Server
|
4
|
+
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
|
11
|
+
@env = env
|
12
|
+
|
13
|
+
call_options = @options.merge Hash[request.GET.map{ |k, v| [k.to_sym, v] }]
|
14
|
+
|
15
|
+
if (asset = Fuse::Document::Asset.for(request.path, File.directory?(@options[:source]) ? @options[:source] : File.dirname(@options[:source])))
|
16
|
+
return asset.call(env)
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
doc = Fuse::Document.new(call_options)
|
21
|
+
rescue Fuse::Exception::SourceUnknown::TooManySources
|
22
|
+
doc = render_list($!.options, $!.option_name)
|
23
|
+
rescue Fuse::Exception
|
24
|
+
if $!.message
|
25
|
+
doc = render_error($!.message)
|
26
|
+
else
|
27
|
+
raise
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
[200, {'Content-Type' => 'text/html'}, [doc.to_s]]
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def request
|
38
|
+
Rack::Request.new(@env)
|
39
|
+
end
|
40
|
+
|
41
|
+
def render_error(text)
|
42
|
+
render_body do |h|
|
43
|
+
h.p { h.text text }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_list(assets, key)
|
48
|
+
render_body do |h|
|
49
|
+
h.h3 { h.text "Choose #{key}:" }
|
50
|
+
h.ul {
|
51
|
+
assets.each do |asset|
|
52
|
+
h.li {
|
53
|
+
h.a(href: '?' + Rack::Utils.build_query(request.GET.merge(key.to_s => asset))) {
|
54
|
+
h.text asset
|
55
|
+
}
|
56
|
+
}
|
57
|
+
end
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def render_body
|
63
|
+
Nokogiri::HTML::Builder.new do |h|
|
64
|
+
h.html {
|
65
|
+
h.head { h.title { h.text 'Fuse' } }
|
66
|
+
h.body { yield h }
|
67
|
+
}
|
68
|
+
end.to_html
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fuse
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Neil E. Pearson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-03-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thin
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: uglifier
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: nokogiri
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.5.6
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.5.6
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: coffee-script
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '2.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sass
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 3.2.5
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.2.5
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 2.12.2
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 2.12.2
|
97
|
+
description: ! 'Portable document authoring. Fuse HTML, JavaScript, CSS, images and
|
98
|
+
fonts into standalone HTML files.
|
99
|
+
|
100
|
+
'
|
101
|
+
email: neil@helium.net.au
|
102
|
+
executables:
|
103
|
+
- fuse
|
104
|
+
extensions: []
|
105
|
+
extra_rdoc_files: []
|
106
|
+
files:
|
107
|
+
- bin/fuse
|
108
|
+
- lib/fuse/document/asset.rb
|
109
|
+
- lib/fuse/document/asset_collection.rb
|
110
|
+
- lib/fuse/document/asset_types.rb
|
111
|
+
- lib/fuse/document.rb
|
112
|
+
- lib/fuse/exceptions.rb
|
113
|
+
- lib/fuse/main.rb
|
114
|
+
- lib/fuse/server.rb
|
115
|
+
- lib/fuse.rb
|
116
|
+
- README.md
|
117
|
+
- LICENSE
|
118
|
+
homepage: https://github.com/hx/fuse
|
119
|
+
licenses:
|
120
|
+
- Apache 2.0
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ! '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 1.9.0
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - ! '>='
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 2.0.0
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: Fuse HTML, CSS, JavaScript, Fonts and Images into a single file.
|
142
|
+
test_files: []
|