jekyll-inline-external-svg 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +52 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +93 -0
- data/LICENSE +674 -0
- data/README.md +94 -0
- data/Rakefile +5 -0
- data/jekyll-inline-external-svg.gemspec +27 -0
- data/lib/jekyll-inline-external-svg.rb +145 -0
- data/spec/fixtures/_config.yml +9 -0
- data/spec/fixtures/_layouts/default.html +4 -0
- data/spec/fixtures/files/square.svg +6 -0
- data/spec/fixtures/index.html +38 -0
- data/spec/jekyll-inline-svg_spec.rb +129 -0
- data/spec/spec_helper.rb +21 -0
- metadata +137 -0
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# jekyll-inline-external-svg
|
2
|
+
|
3
|
+
SVG optimizer and inliner for jekyll (with support for external SVG images)
|
4
|
+
|
5
|
+
This liquid tag will let you inline SVG images in your jekyll sites. It will add `{%svg %}` to `Liquid::Tag`.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Run `gem install jekyll-inline-external-svg` or add `gem "jekyll-inline-external-svg", "~>0.0.1", github: "bih/jekyll-inline-external-svg"` to your **Gemfile**.
|
10
|
+
|
11
|
+
Then in your **_config.yml** :
|
12
|
+
|
13
|
+
```
|
14
|
+
gems:
|
15
|
+
- jekyll-inline-external-svg
|
16
|
+
```
|
17
|
+
|
18
|
+
Optimization is opt-in and can be enabled by adding this to your `_config.yml`
|
19
|
+
|
20
|
+
```
|
21
|
+
svg:
|
22
|
+
optimize: true
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Use the Liquid tag in your pages :
|
28
|
+
|
29
|
+
```
|
30
|
+
{% svg /path/to/square.svg width=24 foo="bar" %}
|
31
|
+
{% svg /path/to/square.svg width=24 foo="bar" is_external="true" %}
|
32
|
+
```
|
33
|
+
|
34
|
+
Jekyll will include the svg file in your output HTML like this :
|
35
|
+
|
36
|
+
```
|
37
|
+
<svg width=24 foo="bar" version="1.1" id="square" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 24 24" >
|
38
|
+
<rect width="20" height="20" x="2" y="2" />
|
39
|
+
</svg>
|
40
|
+
```
|
41
|
+
|
42
|
+
**Note** : You will generally want to set the width/height of your SVG or a `style` attribute, but anything can be passed through.
|
43
|
+
|
44
|
+
Paths with a space should be quoted :
|
45
|
+
|
46
|
+
```
|
47
|
+
{% svg "/path/to/foo bar.svg" %}
|
48
|
+
# or :
|
49
|
+
{% svg '/path/to/foo bar.svg' %}
|
50
|
+
```
|
51
|
+
Otherwise anything after the first space will be considered an attribute.
|
52
|
+
|
53
|
+
Liquid variables will be interpreted if enclosed in double brackets :
|
54
|
+
|
55
|
+
```
|
56
|
+
{% assign size=40 %}
|
57
|
+
{% svg "/path/to/{{site.foo-name}}.svg" width="{{size}}" %}
|
58
|
+
```
|
59
|
+
`height` is automatically set to match `width` if omitted. It can't be left unset because IE11 won't use the viewport attribute to calculate the image's aspect ratio.
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
Relative paths and absolute paths will both be interpreted from Jekyll's configured [source directory](https://jekyllrb.com/docs/configuration/). So both :
|
64
|
+
|
65
|
+
```
|
66
|
+
{% svg "/path/to/foo.svg" %}
|
67
|
+
{% svg "path/to/foo.svg" %}
|
68
|
+
```
|
69
|
+
|
70
|
+
Should resolve to `/your/site/source/path/to/foo.svg`. As jekyll prevents you from getting out of the source dir, `/../drawing.svg` will also resolve to `./drawing.svg`.
|
71
|
+
|
72
|
+
|
73
|
+
## Safety
|
74
|
+
|
75
|
+
In [safe mode](https://jekyllrb.com/docs/plugins/) (ie. on github pages), the plugin will be disabled as it's not yet trusted. However it should be "safe" as defined by [Jekyll](https://jekyllrb.com/docs/plugins/) (ie. no arbitrary code execution).
|
76
|
+
|
77
|
+
Some processing is done to remove useless data :
|
78
|
+
|
79
|
+
- metadata
|
80
|
+
- comments
|
81
|
+
- unused groups
|
82
|
+
- Other filters from [svg_optimizer](https://github.com/fnando/svg_optimizer)
|
83
|
+
- default size
|
84
|
+
|
85
|
+
If any important data gets removed, or the output SVG looks different from input, it's a bug. Please file an issue to this repository describing your problem.
|
86
|
+
|
87
|
+
It does not perform any input validation on attributes. They will be appended as-is to the root node.
|
88
|
+
|
89
|
+
## Motivations
|
90
|
+
|
91
|
+
This has been creeated specifically to display svg icons in html pages.
|
92
|
+
|
93
|
+
PNG/BMP sprites are clearly a no go in a world where "a screen" can be anything from 4" to 150", ranging from 480p to 4k. So what are our vector alternatives?
|
94
|
+
Font-icons are [bad](https://cloudfour.com/thinks/seriously-dont-use-icon-fonts/). While **xlink** looks like an ideal solution, with an elegant : `<use xlink:href="/path/to/icons.svg#play"></use>`, it's badly supported in IE (up to ie11). And embedding SVGs in an `<img>` is not going to cut it. Inlined SVG icons, in my opinion, is the best option we got right now. It's also where the industry seems to be going, with big actors like [github](https://github.com/blog/2112-delivering-octicons-with-svg) starting to transition from font-icons to inlined SVG.
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "jekyll-inline-external-svg"
|
5
|
+
spec.summary = "A SVG Inliner for Jekyll"
|
6
|
+
spec.description = <<-EOF
|
7
|
+
A Liquid tag to inline and optimize internal/external SVG images in your HTML
|
8
|
+
Supports custom DOM Attributes parameters and variables interpretation.
|
9
|
+
EOF
|
10
|
+
spec.version = "1.1.1"
|
11
|
+
spec.authors = ["Sebastien DUMETZ"]
|
12
|
+
spec.email = "s.dumetz@holusion.com"
|
13
|
+
spec.homepage = "https://github.com/bih/jekyll-inline-external-svg"
|
14
|
+
spec.licenses = ["GPL-3.0"]
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r!^bin/!) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r!^(test|spec|features)/!)
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "jekyll", "~> 3.3"
|
22
|
+
spec.add_dependency 'svg_optimizer', "0.1.0"
|
23
|
+
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.6"
|
25
|
+
spec.add_development_dependency "rake", "~>12.0"
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
27
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
require 'svg_optimizer'
|
3
|
+
require 'jekyll/liquid_extensions'
|
4
|
+
require 'open-uri'
|
5
|
+
|
6
|
+
PLUGINS_BLACKLIST = [
|
7
|
+
SvgOptimizer::Plugins::CleanupId,
|
8
|
+
]
|
9
|
+
|
10
|
+
PLUGINS = SvgOptimizer::DEFAULT_PLUGINS.delete_if {|plugin|
|
11
|
+
PLUGINS_BLACKLIST.include? plugin
|
12
|
+
}
|
13
|
+
|
14
|
+
|
15
|
+
module Jekyll
|
16
|
+
module Tags
|
17
|
+
class JekyllInlineSvg < Liquid::Tag
|
18
|
+
#import lookup_variable function
|
19
|
+
# https://github.com/jekyll/jekyll/blob/master/lib/jekyll/liquid_extensions.rb
|
20
|
+
include Jekyll::LiquidExtensions
|
21
|
+
|
22
|
+
# For interpoaltion, look for liquid variables
|
23
|
+
VARIABLE = /\{\{\s*([\w]+\.?[\w]*)\s*\}\}/i
|
24
|
+
|
25
|
+
#Separate file path from other attributes
|
26
|
+
PATH_SYNTAX = %r!
|
27
|
+
^(?<path>[^\s"']+|"[^"]+"|'[^']+')
|
28
|
+
(?<params>.*)
|
29
|
+
!x
|
30
|
+
|
31
|
+
# parse the first parameter in a string, giving :
|
32
|
+
# [full_match, param_name, double_quoted_val, single_quoted_val, unquoted_val]
|
33
|
+
# The Regex works like :
|
34
|
+
# - first group
|
35
|
+
# - match a group of characters that is alphanumeric, _ or -.
|
36
|
+
# - second group (non-capturing OR)
|
37
|
+
# - match a double-quoted string
|
38
|
+
# - match a single-quoted string
|
39
|
+
# - match an unquoted string matching the set : [\w\.\-#]
|
40
|
+
PARAM_SYNTAX= %r!
|
41
|
+
([\w-]+)\s*=\s*
|
42
|
+
(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.\-#]+))
|
43
|
+
!x
|
44
|
+
|
45
|
+
def initialize(tag_name, markup, tokens)
|
46
|
+
super
|
47
|
+
@svg, @params = JekyllInlineSvg.parse_params(markup)
|
48
|
+
end
|
49
|
+
|
50
|
+
#lookup Liquid variables from markup in context
|
51
|
+
def interpolate(markup, context)
|
52
|
+
markup.scan VARIABLE do |variable|
|
53
|
+
markup = markup.sub(VARIABLE, lookup_variable(context, variable.first))
|
54
|
+
end
|
55
|
+
markup
|
56
|
+
end
|
57
|
+
def split_params(markup, context)
|
58
|
+
params={}
|
59
|
+
while (match = PARAM_SYNTAX.match(markup))
|
60
|
+
markup = markup[match.end(0)..-1]
|
61
|
+
value = if match[2]
|
62
|
+
interpolate(match[2].gsub(%r!\\"!, '"'), context)
|
63
|
+
elsif match[3]
|
64
|
+
interpolate(match[3].gsub(%r!\\'!, "'"),context)
|
65
|
+
elsif match[4]
|
66
|
+
lookup_variable(context, match[4])
|
67
|
+
end
|
68
|
+
params[match[1]] = value
|
69
|
+
end
|
70
|
+
return params
|
71
|
+
end
|
72
|
+
#Parse parameters. Returns : [svg_path, parameters]
|
73
|
+
# Does not interpret variables as it's done at render time
|
74
|
+
def self.parse_params(markup)
|
75
|
+
matched = markup.strip.match(PATH_SYNTAX)
|
76
|
+
if !matched
|
77
|
+
raise SyntaxError, <<~END
|
78
|
+
Syntax Error in tag 'highlight' while parsing the following markup:
|
79
|
+
#{markup}
|
80
|
+
Valid syntax: svg <path> [property=value]
|
81
|
+
END
|
82
|
+
end
|
83
|
+
path = matched["path"].sub(%r!^["']!,"").sub(%r!["']$!,"").strip
|
84
|
+
params = matched["params"].strip
|
85
|
+
return path, params
|
86
|
+
end
|
87
|
+
def fmt(params)
|
88
|
+
r = params.to_a.select{|v| v[1] != ""}.map {|v| %!#{v[0]}="#{v[1]}"!}
|
89
|
+
r.join(" ")
|
90
|
+
end
|
91
|
+
def create_plugin(params)
|
92
|
+
mod = Class.new(SvgOptimizer::Plugins::Base) do
|
93
|
+
def self.set (p)
|
94
|
+
@@params = p
|
95
|
+
end
|
96
|
+
def process
|
97
|
+
@@params.each {|key,val| xml.root.set_attribute(key,val)}
|
98
|
+
return xml
|
99
|
+
end
|
100
|
+
end
|
101
|
+
mod.set(params)
|
102
|
+
return mod
|
103
|
+
end
|
104
|
+
def add_file_to_dependency(site, path, context)
|
105
|
+
if context.registers[:page] && context.registers[:page].key?("path")
|
106
|
+
site.regenerator.add_dependency(
|
107
|
+
site.in_source_dir(context.registers[:page]["path"]),
|
108
|
+
path
|
109
|
+
)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def render(context)
|
114
|
+
#global site variable
|
115
|
+
site = context.registers[:site]
|
116
|
+
#check if given name is a variable. Otherwise use it as a file name
|
117
|
+
svg_file = Jekyll.sanitized_path(site.source, interpolate(@svg,context))
|
118
|
+
return unless svg_file
|
119
|
+
add_file_to_dependency(site,svg_file, context)
|
120
|
+
#replace variables with their current value
|
121
|
+
params = split_params(@params,context)
|
122
|
+
#because ie11 require to have a height AND a width
|
123
|
+
if params.key? "width" and ! params.key? "height"
|
124
|
+
params["height"] = params["width"]
|
125
|
+
end
|
126
|
+
#params = @params
|
127
|
+
if params.key? "is_external"
|
128
|
+
file = open(svg_file).read
|
129
|
+
else
|
130
|
+
file = File.open(svg_file, "rb").read
|
131
|
+
end
|
132
|
+
conf = lookup_variable(context,"site.svg")
|
133
|
+
if conf["optimize"] == true
|
134
|
+
xml = SvgOptimizer.optimize(file, [create_plugin(params)] + PLUGINS)
|
135
|
+
else
|
136
|
+
xml = Nokogiri::XML(file)
|
137
|
+
params.each {|key,val| xml.root.set_attribute(key,val)}
|
138
|
+
xml = xml.to_xml
|
139
|
+
end
|
140
|
+
return xml
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
Liquid::Template.register_tag('svg', Jekyll::Tags::JekyllInlineSvg)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
---
|
2
|
+
svgname: square
|
3
|
+
svgclass: hello
|
4
|
+
---
|
5
|
+
Hello world
|
6
|
+
<div id="base">
|
7
|
+
{% svg files/square.svg %}
|
8
|
+
</div>
|
9
|
+
|
10
|
+
<div id="height">
|
11
|
+
{% svg files/square.svg width=24 %}
|
12
|
+
{% svg files/square.svg width=24 height=48 %}
|
13
|
+
{% svg files/square.svg width=24 height="" %}
|
14
|
+
</div>
|
15
|
+
<div id="attributes">
|
16
|
+
{% svg files/square.svg role="navigation" data-foo="bar" fill="#ffffff" stroke=#000000 %}
|
17
|
+
</div>
|
18
|
+
<div id="path">
|
19
|
+
<label>Resolve relative and absolute paths to site's source</label>
|
20
|
+
{% svg files/square.svg %}
|
21
|
+
{% svg /files/square.svg %}
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<div id="jail">
|
25
|
+
<label>Resolve relative and absolute paths to site's source</label>
|
26
|
+
{% svg /../files/square.svg %}
|
27
|
+
{% svg ../files/square.svg %}
|
28
|
+
</div>
|
29
|
+
|
30
|
+
<div id="interpret">
|
31
|
+
<label>Interpret embedded variables</label>
|
32
|
+
{% svg /files/{{page.svgname}}.svg %}
|
33
|
+
{% svg /files/square.svg id="name-{{page.svgname}}" class="class-{{page.svgclass}}" %}
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<div id="optimize">
|
37
|
+
{% svg /files/{{page.svgname}}.svg data-foo="" %}
|
38
|
+
</div>
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require "nokogiri"
|
5
|
+
|
6
|
+
describe(Jekyll::Tags::JekyllInlineSvg) do
|
7
|
+
|
8
|
+
def read(f)
|
9
|
+
File.read(dest_dir(f))
|
10
|
+
end
|
11
|
+
# return an array of the page's svgs
|
12
|
+
def parse(f)
|
13
|
+
Nokogiri::HTML(read(f))
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#parse_params" do
|
17
|
+
it "parse XML root parameters" do
|
18
|
+
svg, params = Jekyll::Tags::JekyllInlineSvg.parse_params("/path/to/foo size=40 style=\"hello\"")
|
19
|
+
expect(svg).to eq("/path/to/foo")
|
20
|
+
expect(params).to eq("size=40 style=\"hello\"")
|
21
|
+
end
|
22
|
+
it "accepts double quoted path" do
|
23
|
+
svg, params = Jekyll::Tags::JekyllInlineSvg.parse_params("\"/path/to/foo space\"")
|
24
|
+
expect(svg).to eq("/path/to/foo space")
|
25
|
+
end
|
26
|
+
it "accepts single quoted path" do
|
27
|
+
svg, params = Jekyll::Tags::JekyllInlineSvg.parse_params("'/path/to/foo space'")
|
28
|
+
expect(svg).to eq("/path/to/foo space")
|
29
|
+
end
|
30
|
+
it "strip leading and trailing spaces" do
|
31
|
+
svg, params = Jekyll::Tags::JekyllInlineSvg.parse_params(" /path/to/foo ")
|
32
|
+
expect(svg).to eql("/path/to/foo")
|
33
|
+
end
|
34
|
+
# required when a variable is defined with leading/trailing space then embedded.
|
35
|
+
it "strip in-quote leading and trailing spaces" do
|
36
|
+
svg, params = Jekyll::Tags::JekyllInlineSvg.parse_params("'/path/to/foo '")
|
37
|
+
expect(svg).to eql("/path/to/foo")
|
38
|
+
end
|
39
|
+
it "keep Liquid variables" do
|
40
|
+
svg, params = Jekyll::Tags::JekyllInlineSvg.parse_params("/path/to/{{foo}}")
|
41
|
+
expect(svg).to eql("/path/to/{{foo}}")
|
42
|
+
end
|
43
|
+
it "don't parse parameters" do
|
44
|
+
svg, params = Jekyll::Tags::JekyllInlineSvg.parse_params("'/path/to/foo space' id='bar' style=\"hello\"")
|
45
|
+
expect(params).to eq("id='bar' style=\"hello\"")
|
46
|
+
end
|
47
|
+
it "raise error on invalid syntax" do
|
48
|
+
expect {Jekyll::Tags::JekyllInlineSvg.parse_params("")}.to raise_error (SyntaxError)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
[
|
52
|
+
Jekyll.configuration({
|
53
|
+
"source" => source_dir,
|
54
|
+
"destination" => dest_dir,
|
55
|
+
"url" => "http://example.org",
|
56
|
+
}),
|
57
|
+
Jekyll.configuration({
|
58
|
+
"source" => source_dir,
|
59
|
+
"destination" => dest_dir,
|
60
|
+
"url" => "http://example.org",
|
61
|
+
"svg" => { "optimize" => true}
|
62
|
+
})
|
63
|
+
].each do |config|
|
64
|
+
is_opt = config["svg"] and config["svg"]["optimize"] == true
|
65
|
+
describe "Integration (with #{is_opt ? "" : "no"} optimisation)" do
|
66
|
+
before(:context) do
|
67
|
+
site = Jekyll::Site.new(config)
|
68
|
+
site.process
|
69
|
+
@data = parse("index.html")
|
70
|
+
@base = @data.css("#base").css("svg").first
|
71
|
+
end
|
72
|
+
it "render site" do
|
73
|
+
expect(File.exist?(dest_dir("index.html"))).to be_truthy
|
74
|
+
end
|
75
|
+
it "exports svg" do
|
76
|
+
data = @data.xpath("//svg")
|
77
|
+
expect(data).to be_truthy
|
78
|
+
expect(data.first).to be_truthy
|
79
|
+
expect(@base).to be_truthy
|
80
|
+
# Do not strip other width and height attributes
|
81
|
+
end
|
82
|
+
it "add a height if only width is given" do
|
83
|
+
data = @data.css("#height").css("svg")
|
84
|
+
expect(data).to be_truthy
|
85
|
+
expect(data[0].get_attribute("height")).to eql("24")
|
86
|
+
expect(data[0].get_attribute("width")).to eql("24")
|
87
|
+
# do not set height if given
|
88
|
+
expect(data[1].get_attribute("height")).to eql("48")
|
89
|
+
expect(data[1].get_attribute("width")).to eql("24")
|
90
|
+
#do not set height if forced to empty string
|
91
|
+
expect(data[2].get_attribute("height")).to is_opt ? be_falsy : eql("")
|
92
|
+
|
93
|
+
expect(data[2].get_attribute("width")).to eql("24")
|
94
|
+
end
|
95
|
+
it "keep attributes" do
|
96
|
+
data = @data.css("#attributes").css("svg")
|
97
|
+
expect(data).to be_truthy
|
98
|
+
expect(data[0].get_attribute("role")).to eql("navigation")
|
99
|
+
expect(data[0].get_attribute("data-foo")).to eql("bar")
|
100
|
+
expect(data[0].get_attribute("fill")).to eql("#ffffff")
|
101
|
+
expect(data[0].get_attribute("stroke")).to eql("#000000")
|
102
|
+
end
|
103
|
+
it "parse relative paths" do
|
104
|
+
data = @data.css("#path").css("svg")
|
105
|
+
expect(data.size).to eq(2)
|
106
|
+
expect(data[0].to_html).to eq(data[1].to_html) #should use to_xml?
|
107
|
+
end
|
108
|
+
it "jails to Jekyll source" do
|
109
|
+
data = @data.css("#jail").css("svg")
|
110
|
+
ref = @base.to_xml
|
111
|
+
expect(data.size).to eq(2)
|
112
|
+
data.each{ |item| expect(item.to_xml).to eql(ref) }
|
113
|
+
end
|
114
|
+
it "interpret variables" do
|
115
|
+
data = @data.css("#interpret").css("svg")
|
116
|
+
ref = @base.to_xml
|
117
|
+
expect(data.size).to eq(2)
|
118
|
+
expect(data[0].to_xml).to eql(ref)
|
119
|
+
expect(data[1].get_attribute("id")).to eql("name-square")
|
120
|
+
expect(data[1].get_attribute("class")).to eql("class-hello")
|
121
|
+
end
|
122
|
+
it "#{is_opt ? "do" : "do not"} optimize" do
|
123
|
+
data = @data.css("#optimize").css("svg")
|
124
|
+
expect(data.first.get_attribute("data-foo")).to is_opt ? be_falsy : eql("")
|
125
|
+
expect(data.xpath("//comment()").first).to is_opt ? be_falsy : be_truthy
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|