style_inliner 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d7481c1d3452a48020915539d4244a6458c097e9
4
+ data.tar.gz: 3f348b7b787d8f67cb81270766046f5aa11cdf44
5
+ SHA512:
6
+ metadata.gz: a08494863a4a41ad2b028682d3c19ce49cef2fc7215045e0134e4b27f40d766036dca8d06a46ae58d6300d48e122ce083454862c92a7845b931286a3d1063503
7
+ data.tar.gz: 9fb35e3c483ca933539082f5e5f66d544f52636a68f828b0675837e6520befed1ffccc5d76f6df138f1038807f578e1223d711ece804ad060174baf92c457b8e
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ cache: bundler
2
+ before_install:
3
+ - gem install bundler
4
+ language: ruby
5
+ rvm:
6
+ - 2.2.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## 0.0.1
2
+ - 1st release :tada:
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in style_inliner.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Ryo Nakamura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # StyleInliner [![Build Status](https://travis-ci.org/r7kamura/style_inliner.svg?branch=master)](https://travis-ci.org/r7kamura/style_inliner)
2
+ Inline CSS style rules into style attributes of each HTML element.
3
+
4
+ ## Installation
5
+
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem "style_inliner"
10
+ ```
11
+
12
+ And then execute:
13
+
14
+ ```sh
15
+ bundle
16
+ ```
17
+
18
+ Or install it yourself as:
19
+
20
+ ```sh
21
+ gem install style_inliner
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```rb
27
+ html = <<-EOS
28
+ <!DOCTYPE html>
29
+ <html>
30
+ <head>
31
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
32
+ <style>
33
+ body {
34
+ background: red;
35
+ }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ </body>
40
+ </html>
41
+ EOS
42
+ puts StyleInliner::Document.new(html).inline
43
+ ```
44
+
45
+ ```html
46
+ <!DOCTYPE html>
47
+ <html>
48
+ <head>
49
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
50
+
51
+ </head>
52
+ <body style="background-color: red">
53
+ </body>
54
+ </html>
55
+ ```
56
+
57
+ ## Development
58
+
59
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
60
+
61
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
62
+
63
+ ## Contributing
64
+
65
+ Bug reports and pull requests are welcome on GitHub at https://github.com/r7kamura/style_inliner.
66
+
67
+ ## License
68
+
69
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "style_inliner"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,2 @@
1
+ require "style_inliner/document"
2
+ require "style_inliner/version"
@@ -0,0 +1,37 @@
1
+ module StyleInliner
2
+ class DeclarationBlock
3
+ # @param rule_sets [Array<StyleInliner::RuleSet>]
4
+ def initialize(rule_sets)
5
+ @rule_sets = rule_sets
6
+ end
7
+
8
+ def delete_property(property_name)
9
+ merged_rule_set.instance_variable_get(:@declarations).delete(property_name)
10
+ end
11
+
12
+ # @return [String]
13
+ def get_property(property_name)
14
+ merged_rule_set[property_name]
15
+ end
16
+
17
+ # @return [String]
18
+ def to_s
19
+ merged_rule_set.declarations_to_s.gsub('"', "'").split(/;(?![^(]*\))/).map(&:strip).sort.join("; ")
20
+ end
21
+
22
+ private
23
+
24
+ # @return [CssParser::RuleSet]
25
+ def merged_rule_set
26
+ @merged_rule_set ||= ::CssParser.merge(
27
+ @rule_sets.map do |rule_set|
28
+ ::CssParser::RuleSet.new(
29
+ nil,
30
+ rule_set.declarations,
31
+ rule_set.specificity,
32
+ )
33
+ end
34
+ ).tap(&:expand_shorthand!)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,136 @@
1
+ require "css_parser"
2
+ require "nokogiri"
3
+ require "style_inliner/node_style_folding"
4
+ require "style_inliner/rule_set"
5
+
6
+ module StyleInliner
7
+ class Document
8
+ UNMERGEABLE_PSEUDO_CLASS_NAMES = %w(
9
+ active
10
+ after
11
+ before
12
+ first-letter
13
+ first-line
14
+ focus
15
+ hover
16
+ selection
17
+ target
18
+ visited
19
+ )
20
+
21
+ # @param html [String]
22
+ def initialize(html)
23
+ @html = html
24
+ end
25
+
26
+ # @return [Nokogiri::XML::Node]
27
+ def inline
28
+ load_styles_from_html
29
+ merge_styles_into_each_element
30
+ fold_style_attributes
31
+ append_style_element_for_unmergeable_rule_sets
32
+ root
33
+ end
34
+
35
+ private
36
+
37
+ # @param selector [Stirng]
38
+ # @param declarations [String]
39
+ # @param media_types [Array<Symbol>]
40
+ def add_unmergeable_rule_set(selector, declarations, media_types)
41
+ css_parser_for_unmergeable_rules.add_rule_set!(
42
+ ::CssParser::RuleSet.new(selector, declarations),
43
+ media_types,
44
+ )
45
+ end
46
+
47
+ def append_style_element_for_unmergeable_rule_sets
48
+ rule_set_string = css_parser_for_unmergeable_rules.to_s
49
+ unless rule_set_string.empty?
50
+ if (body_node = root.at("body"))
51
+ body_node.prepend_child(
52
+ ::Nokogiri::XML.fragment("<style>\n#{rule_set_string}</style>")
53
+ )
54
+ end
55
+ end
56
+ end
57
+
58
+ # @param node [Nokogiri::XML::Node]
59
+ # @return [false, true]
60
+ def check_node_stylability(node)
61
+ node.element? && node.name != "head" && node.parent.name != "head"
62
+ end
63
+
64
+ # @param selector [String]
65
+ # @return [false, true]
66
+ def check_selector_mergeability(selector)
67
+ !selector.start_with?("@") && !UNMERGEABLE_PSEUDO_CLASS_NAMES.any? { |name| selector.include?(":#{name}") }
68
+ end
69
+
70
+ # @return [CssParser::Parser]
71
+ def css_parser_for_mergeable_rules
72
+ @css_parser_for_mergeable_rules ||= ::CssParser::Parser.new
73
+ end
74
+
75
+ # @return [CssParser::Parser]
76
+ def css_parser_for_unmergeable_rules
77
+ @css_parser_for_unmergeable_rules ||= ::CssParser::Parser.new
78
+ end
79
+
80
+ def fold_style_attributes
81
+ root.search("*[@style]").each do |node|
82
+ NodeStyleFolding.new(node).call
83
+ end
84
+ end
85
+
86
+ # Load styles from <style> and <link> elements from a given HTML document.
87
+ def load_styles_from_html
88
+ load_styles_from_link_elements
89
+ load_styles_from_style_elements
90
+ end
91
+
92
+ # @todo
93
+ def load_styles_from_link_elements
94
+ end
95
+
96
+ def load_styles_from_style_elements
97
+ root.search("style").each do |style_node|
98
+ css_parser_for_mergeable_rules.add_block!(style_node.inner_html)
99
+ style_node.remove
100
+ end
101
+ end
102
+
103
+ def merge_styles_into_each_element
104
+ css_parser_for_mergeable_rules.each_selector(:all) do |selector, declarations, specificity, media_types|
105
+ if check_selector_mergeability(selector)
106
+ root.search(strip_link_pseudo_class(selector)).each do |node|
107
+ if check_node_stylability(node)
108
+ push_encoded_rule_set_into_style_attribute(node, declarations, specificity)
109
+ end
110
+ end
111
+ else
112
+ add_unmergeable_rule_set(selector, declarations, media_types)
113
+ end
114
+ end
115
+ end
116
+
117
+ # @param node [Nokogiri::XML::Node]
118
+ # @param declarations [String]
119
+ # @param specificity [Integer]
120
+ def push_encoded_rule_set_into_style_attribute(node, declarations, specificity)
121
+ rule_set = RuleSet.new(declarations: declarations, specificity: specificity)
122
+ node["style"] = "#{node['style']} #{rule_set.encode}"
123
+ end
124
+
125
+ # @return [Nokogiri::XML::Node]
126
+ def root
127
+ @root ||= ::Nokogiri.HTML(@html)
128
+ end
129
+
130
+ # @param selector [String]
131
+ # @return [String]
132
+ def strip_link_pseudo_class(selector)
133
+ selector.gsub(":link", "")
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,105 @@
1
+ require "style_inliner/declaration_block"
2
+ require "style_inliner/rule_set"
3
+
4
+ module StyleInliner
5
+ class NodeStyleFolding
6
+ CORRESPONDENCE_TABLES = Hash.new({}).merge(
7
+ "blockquote" => {
8
+ "text-align" => "align",
9
+ },
10
+ "body" => {
11
+ "background-color" => "bgcolor",
12
+ },
13
+ "div" => {
14
+ "text-align" => "align",
15
+ },
16
+ "h1" => {
17
+ "text-align" => "align",
18
+ },
19
+ "h2" => {
20
+ "text-align" => "align",
21
+ },
22
+ "h3" => {
23
+ "text-align" => "align",
24
+ },
25
+ "h4" => {
26
+ "text-align" => "align",
27
+ },
28
+ "h5" => {
29
+ "text-align" => "align",
30
+ },
31
+ "h6" => {
32
+ "text-align" => "align",
33
+ },
34
+ "img" => {
35
+ "float" => "align",
36
+ },
37
+ "p" => {
38
+ "text-align" => "align",
39
+ },
40
+ "table" => {
41
+ "background-color" => "bgcolor",
42
+ "background-image" => "background",
43
+ },
44
+ "td" => {
45
+ "background-color" => "bgcolor",
46
+ "text-align" => "align",
47
+ "vertical-align" => "valign",
48
+ },
49
+ "th" => {
50
+ "background-color" => "bgcolor",
51
+ "text-align" => "align",
52
+ "vertical-align" => "valign",
53
+ },
54
+ "tr" => {
55
+ "background-color" => "bgcolor",
56
+ "text-align" => "align",
57
+ },
58
+ )
59
+
60
+ # @param node [Nokogiri::XML::Node]
61
+ def initialize(node)
62
+ @node = node
63
+ end
64
+
65
+ def call
66
+ update_css_compatible_attributes
67
+ update_style_attribute
68
+ end
69
+
70
+ private
71
+
72
+ # @return [Hash{String => String}]
73
+ def correspondence_table
74
+ CORRESPONDENCE_TABLES[@node.name]
75
+ end
76
+
77
+ # @return [StyleInliner::RuleSet]
78
+ def declaration_block
79
+ @declaration_block ||= DeclarationBlock.new(RuleSet.decode(@node["style"]))
80
+ end
81
+
82
+ # @param property_value [String]
83
+ # @return [String]
84
+ def preprocess_attribute_value(property_value)
85
+ property_value.gsub(/url\(['|"](.*)['|"]\)/, '\1').gsub(/;$|\s*!important/, '').strip
86
+ end
87
+
88
+ def update_css_compatible_attributes
89
+ correspondence_table.each do |property_name, attribute_name|
90
+ if @node[attribute_name].nil? && !declaration_block.get_property(property_name).empty?
91
+ @node[attribute_name] = preprocess_attribute_value(declaration_block.get_property(property_name))
92
+ declaration_block.delete_property(property_name)
93
+ end
94
+ end
95
+ end
96
+
97
+ def update_style_attribute
98
+ if (value = declaration_block.to_s).empty?
99
+ @node.remove_attribute("style")
100
+ else
101
+ @node["style"] = value
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,32 @@
1
+ module StyleInliner
2
+ class RuleSet
3
+ PATTERN = /\[SPEC\=(\d+)\[(.[^\]]*)\]\]/
4
+
5
+ attr_reader :declarations
6
+ attr_reader :specificity
7
+
8
+ class << self
9
+ # @param source [String]
10
+ def decode(source)
11
+ source.scan(PATTERN).map do |specificity, declarations|
12
+ new(
13
+ declarations: declarations,
14
+ specificity: specificity.to_i,
15
+ )
16
+ end
17
+ end
18
+ end
19
+
20
+ # @param specificity [Integer] e.g. `1`
21
+ # @param declarations [String] e.g. `"background: red; color: yellow;"`
22
+ def initialize(declarations:, specificity:)
23
+ @declarations = declarations
24
+ @specificity = specificity
25
+ end
26
+
27
+ # @return [String]
28
+ def encode
29
+ "[SPEC=#{@specificity}[#{@declarations}]]"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ module StyleInliner
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,21 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "style_inliner/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "style_inliner"
7
+ spec.version = StyleInliner::VERSION
8
+ spec.authors = ["Ryo Nakamura"]
9
+ spec.email = ["r7kamura@gmail.com"]
10
+ spec.summary = "Inline CSS style rules into style attributes of each HTML element."
11
+ spec.homepage = "https://github.com/r7kamura/style_inliner"
12
+ spec.license = "MIT"
13
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
14
+ spec.require_paths = ["lib"]
15
+ spec.add_dependency "css_parser"
16
+ spec.add_dependency "nokogiri"
17
+ spec.add_development_dependency "activesupport", "4.2.6"
18
+ spec.add_development_dependency "bundler", "~> 1.12"
19
+ spec.add_development_dependency "rake", "~> 10.0"
20
+ spec.add_development_dependency "rspec", "3.4.0"
21
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: style_inliner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Ryo Nakamura
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: css_parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 4.2.6
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 4.2.6
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 3.4.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 3.4.0
97
+ description:
98
+ email:
99
+ - r7kamura@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".travis.yml"
107
+ - CHANGELOG.md
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - bin/console
113
+ - bin/setup
114
+ - lib/style_inliner.rb
115
+ - lib/style_inliner/declaration_block.rb
116
+ - lib/style_inliner/document.rb
117
+ - lib/style_inliner/node_style_folding.rb
118
+ - lib/style_inliner/rule_set.rb
119
+ - lib/style_inliner/version.rb
120
+ - style_inliner.gemspec
121
+ homepage: https://github.com/r7kamura/style_inliner
122
+ licenses:
123
+ - MIT
124
+ metadata: {}
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.4.5
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Inline CSS style rules into style attributes of each HTML element.
145
+ test_files: []
146
+ has_rdoc: