jekyll-inline-external-svg 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.
@@ -0,0 +1,5 @@
1
+ require "rspec/core/rake_task"
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
@@ -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,9 @@
1
+ timezone: UTC
2
+
3
+ defaults:
4
+ -
5
+ scope:
6
+ path: ""
7
+ type: page
8
+ values:
9
+ layout: default
@@ -0,0 +1,4 @@
1
+ ---
2
+ ---
3
+ THIS IS MY LAYOUT
4
+ {{ content }}
@@ -0,0 +1,6 @@
1
+
2
+ <?xml version="1.0" encoding="utf-8"?>
3
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 24 24" width="150" height="150">
4
+ <rect width="20" height="20" x="2" y="2" />
5
+ <!-- comment -->
6
+ </svg>
@@ -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