scrappy 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/History.txt +6 -0
  2. data/Manifest +21 -14
  3. data/README.rdoc +5 -9
  4. data/Rakefile +1 -2
  5. data/bin/scrappy +141 -51
  6. data/lib/scrappy.rb +6 -9
  7. data/lib/scrappy/agent/agent.rb +3 -3
  8. data/lib/scrappy/extractor/extractor.rb +108 -0
  9. data/lib/scrappy/{agent → extractor}/formats.rb +0 -0
  10. data/lib/scrappy/extractor/fragment.rb +111 -0
  11. data/lib/scrappy/extractor/selector.rb +41 -0
  12. data/lib/scrappy/{selectors → extractor/selectors}/base_uri.rb +1 -3
  13. data/lib/scrappy/extractor/selectors/css.rb +5 -0
  14. data/lib/scrappy/{selectors → extractor/selectors}/new_uri.rb +1 -3
  15. data/lib/scrappy/{selectors → extractor/selectors}/root.rb +1 -4
  16. data/lib/scrappy/{selectors → extractor/selectors}/section.rb +1 -4
  17. data/lib/scrappy/{selectors → extractor/selectors}/slice.rb +1 -3
  18. data/lib/scrappy/{selectors → extractor/selectors}/uri.rb +2 -4
  19. data/lib/scrappy/{selectors → extractor/selectors}/uri_pattern.rb +2 -4
  20. data/lib/scrappy/extractor/selectors/visual.rb +39 -0
  21. data/lib/scrappy/{selectors → extractor/selectors}/xpath.rb +1 -4
  22. data/lib/scrappy/server/admin.rb +89 -2
  23. data/lib/scrappy/server/helpers.rb +11 -2
  24. data/lib/scrappy/server/server.rb +1 -0
  25. data/lib/scrappy/trainer/trainer.rb +101 -0
  26. data/public/javascripts/annotator.js +75 -0
  27. data/public/javascripts/remote.js +132 -0
  28. data/public/stylesheets/application.css +39 -12
  29. data/scrappy.gemspec +13 -11
  30. data/views/extractors.haml +24 -0
  31. data/views/layout.haml +14 -4
  32. data/views/patterns.haml +19 -0
  33. data/views/samples.haml +28 -0
  34. metadata +58 -56
  35. data/lib/scrappy/agent/extractor.rb +0 -196
  36. data/lib/scrappy/selectors/css.rb +0 -10
  37. data/public/javascripts/scrappy.js +0 -65
  38. data/views/kb.haml +0 -15
File without changes
@@ -0,0 +1,111 @@
1
+ module Sc
2
+ class Fragment
3
+ include RDF::NodeProxy
4
+
5
+ def extract options={}
6
+ uri = options[:doc][:uri]
7
+
8
+ # Identify the fragment's mappings
9
+ docs = sc::selector.map { |s| graph.node(s).select options[:doc] }.flatten
10
+
11
+ # Generate nodes for each page mapping
12
+ docs.map do |doc|
13
+ # Build RDF nodes from identifier selectors (if present)
14
+ nodes = self.nodes(uri, doc, options[:referenceable])
15
+
16
+ # Add info to each node
17
+ nodes.map do |node|
18
+ # Build the object -- it can be a node or a literal
19
+ object = if sc::type.include?(Node('rdf:Literal'))
20
+ value = doc[:value].to_s.strip
21
+ if options[:referenceable]
22
+ node.rdf::value = value
23
+ node.rdf::type = Node('rdf:Literal')
24
+ node
25
+ else
26
+ value
27
+ end
28
+ else
29
+ # Add statements about the node
30
+ sc::type.each { |type| node.rdf::type += [type] if type != Node('rdf:Resource') }
31
+ sc::superclass.each { |superclass| node.rdfs::subClassOf += [superclass] }
32
+ sc::sameas.each { |samenode| node.owl::sameAs += [samenode] }
33
+
34
+ node
35
+ end
36
+
37
+ # Process subfragments
38
+ consistent = true
39
+ sc::subfragment.each do |subfragment|
40
+ # Get subfragment object
41
+ subfragment = graph.node(subfragment, Node('sc:Fragment'))
42
+ # Extract data from the subfragment
43
+ subnodes = subfragment.extract(options.merge(:doc=>doc))
44
+
45
+ # Add relations
46
+ subnodes.each do |subnode|
47
+ node.graph << subnode if subnode.is_a?(RDF::Node)
48
+ subfragment.sc::relation.each { |relation| node[relation] += [subnode] }
49
+ end
50
+
51
+ # Check consistency
52
+ consistent = false if subfragment.sc::min_cardinality.first and subnodes.size < subfragment.sc::min_cardinality.first.to_i
53
+ consistent = false if subfragment.sc::max_cardinality.first and subnodes.size > subfragment.sc::max_cardinality.first.to_i
54
+ end
55
+
56
+ # Skip the node if it has inconsistent relations
57
+ # For example: extracting a sioc:Post with no dc:title would
58
+ # violate the constraint sc:min_cardinality = 1
59
+ next if !consistent
60
+
61
+ # Add referenceable data if requested
62
+ if options[:referenceable]
63
+ sources = [doc[:content]].flatten.map { |n| Node(Scrappy::Extractor.node_hash(doc[:uri], n.path)) }
64
+ sources.each do |source|
65
+ sc::type.each { |type| source.sc::type += [type] }
66
+ sc::relation.each { |relation| source.sc::relation += [relation] }
67
+ node.graph << source
68
+ node.sc::source += [source]
69
+ end
70
+ end
71
+
72
+ # Object points to either the node or the literal
73
+ object
74
+ end
75
+ end.flatten.compact
76
+ end
77
+
78
+ def nodes uri, doc, referenceable
79
+ nodes = sc::identifier.map { |s| graph.node(s).select doc }.flatten.map do |d|
80
+ node = Node(parse_uri(uri, d[:value]))
81
+
82
+ if referenceable
83
+ # Include the fragment where the URI was built from
84
+ uri_node = Node(nil, node.graph)
85
+ hash = Scrappy::Extractor.node_hash(d[:uri], d[:content].path)
86
+
87
+ node.sc::uri = uri_node
88
+ uri_node.rdf::value = node.to_s
89
+ uri_node.sc::source = Node(hash)
90
+ end
91
+
92
+ node
93
+ end
94
+ nodes << Node(nil) if nodes.empty?
95
+
96
+ nodes
97
+ end
98
+
99
+ private
100
+ # Parses a URI by resolving relative paths
101
+ def parse_uri(uri, rel_uri)
102
+ return ID('*') if rel_uri.nil?
103
+ begin
104
+ ID(URI::parse(uri.split('/')[0..3]*'/').merge(rel_uri).to_s)
105
+ rescue
106
+ ID('*')
107
+ end
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,41 @@
1
+ module Sc
2
+ class Selector
3
+ include RDF::NodeProxy
4
+ include Scrappy::Formats
5
+
6
+ def select doc
7
+ if sc::debug.first=="true" and Scrappy::Agent::Options.debug
8
+ puts '== DEBUG'
9
+ puts '== Selector:'
10
+ puts node.serialize(:yarf, false)
11
+ puts '== On fragment:'
12
+ puts "URI: #{doc[:uri]}"
13
+ puts "Content: #{doc[:content]}"
14
+ puts "Value: #{doc[:value]}"
15
+ end
16
+
17
+ # Process selector
18
+ # Filter method is defined in each subclass
19
+ results = filter doc
20
+
21
+ if sc::debug.first=="true" and Scrappy::Agent::Options.debug
22
+ puts "== No results" if results.empty?
23
+ results.each_with_index do |result, i|
24
+ puts "== Result ##{i}:"
25
+ puts "URI: #{result[:uri]}"
26
+ puts "Content: #{result[:content]}"
27
+ puts "Value: #{result[:value].inspect}"
28
+ end
29
+ puts
30
+ end
31
+
32
+ # Return results if no nested selectors
33
+ return results if sc::selector.empty?
34
+
35
+ # Process nested selectors
36
+ results.map do |result|
37
+ sc::selector.map { |s| graph.node(s).select result }
38
+ end.flatten
39
+ end
40
+ end
41
+ end
@@ -1,7 +1,5 @@
1
1
  module Sc
2
- class BaseUriSelector
3
- include RDF::NodeProxy
4
-
2
+ class BaseUriSelector < Selector
5
3
  def filter doc
6
4
  [ { :uri=>doc[:uri], :content=>doc[:content], :value=>doc[:uri] } ]
7
5
  end
@@ -0,0 +1,5 @@
1
+ module Sc
2
+ class CssSelector < XPathSelector
3
+ # By using Nokogiri, CSS and XPath use the same search method
4
+ end
5
+ end
@@ -1,7 +1,5 @@
1
1
  module Sc
2
- class NewUriSelector
3
- include RDF::NodeProxy
4
-
2
+ class NewUriSelector < Selector
5
3
  def filter doc
6
4
  contents = if sc::attribute.first
7
5
  # Select node's attribute if given
@@ -1,8 +1,5 @@
1
1
  module Sc
2
- class RootSelector
3
- include RDF::NodeProxy
4
- include Scrappy::Formats
5
-
2
+ class RootSelector < Selector
6
3
  def filter doc
7
4
  if sc::attribute.first
8
5
  # Select node's attribute if given
@@ -1,8 +1,5 @@
1
1
  module Sc
2
- class SectionSelector
3
- include RDF::NodeProxy
4
- include Scrappy::Formats
5
-
2
+ class SectionSelector < Selector
6
3
  def filter doc
7
4
  rdf::value.map do |pattern|
8
5
  doc[:content].search('h1, h2, h3, h4, h5, h6, h7, h8, h9, h10').select { |n| n.parent.name!='script' and n.text.downcase.strip == pattern }.map do |node|
@@ -1,7 +1,5 @@
1
1
  module Sc
2
- class SliceSelector
3
- include RDF::NodeProxy
4
-
2
+ class SliceSelector < Selector
5
3
  def filter doc
6
4
  rdf::value.map do |separator|
7
5
  slices = doc[:value].split(separator)
@@ -1,11 +1,9 @@
1
1
  module Sc
2
- class UriSelector
3
- include RDF::NodeProxy
4
-
2
+ class UriSelector < Selector
5
3
  def filter doc
6
4
  # Check if the UriSelector has this URI as value (without params: ?param1=value1&param2=value2)
7
5
  if rdf::value.include?(doc[:uri].match(/\A([^\?]*)(\?.*\Z)?/).captures.first)
8
- [ { :uri=>doc[:uri], :content=>doc[:content], :value=>doc[:content].text } ]
6
+ [ { :uri=>doc[:uri], :content=>doc[:content], :value=>format(doc[:value], sc::format, doc[:uri]) } ]
9
7
  else
10
8
  []
11
9
  end
@@ -1,11 +1,9 @@
1
1
  module Sc
2
- class UriPatternSelector
3
- include RDF::NodeProxy
4
-
2
+ class UriPatternSelector < Selector
5
3
  def filter doc
6
4
  # Check if the uri fits the pattern
7
5
  if rdf::value.any? { |v| doc[:uri] =~ /\A#{v.gsub('.','\.').gsub('*', '.+')}\Z/ }
8
- [ { :uri=>doc[:uri], :content=>doc[:content], :value=>doc[:content].text } ]
6
+ [ { :uri=>doc[:uri], :content=>doc[:content], :value=>format(doc[:value], sc::format, doc[:uri]) } ]
9
7
  else
10
8
  []
11
9
  end
@@ -0,0 +1,39 @@
1
+ module Sc
2
+ class VisualSelector < Selector
3
+ def filter doc
4
+ doc[:content].search(sc::tag.first || "*").select do |node|
5
+ relative_x = node['vx'].to_i - doc[:content]['vx'].to_i
6
+ relative_y = node['vy'].to_i - doc[:content]['vy'].to_i
7
+
8
+ !node.text? and
9
+ ( !sc::min_relative_x.first or relative_x >= sc::min_relative_x.first.to_i) and
10
+ ( !sc::max_relative_x.first or relative_x <= sc::max_relative_x.first.to_i) and
11
+ ( !sc::min_relative_y.first or relative_y >= sc::min_relative_y.first.to_i) and
12
+ ( !sc::max_relative_y.first or relative_y <= sc::max_relative_y.first.to_i) and
13
+
14
+ ( !sc::min_x.first or node['vx'].to_i >= sc::min_x.first.to_i) and
15
+ ( !sc::max_x.first or node['vx'].to_i <= sc::max_x.first.to_i) and
16
+ ( !sc::min_y.first or node['vy'].to_i >= sc::min_y.first.to_i) and
17
+ ( !sc::max_y.first or node['vy'].to_i <= sc::max_y.first.to_i) and
18
+
19
+ ( !sc::min_width.first or node['vw'].to_i >= sc::min_width.first.to_i) and
20
+ ( !sc::max_width.first or node['vw'].to_i <= sc::max_width.first.to_i) and
21
+ ( !sc::min_height.first or node['vh'].to_i >= sc::min_height.first.to_i) and
22
+ ( !sc::max_height.first or node['vh'].to_i <= sc::max_height.first.to_i) and
23
+
24
+ ( !sc::min_font_size.first or node['vsize'].to_i >= sc::min_font_size.first.to_i) and
25
+ ( !sc::max_font_size.first or node['vsize'].to_i <= sc::max_font_size.first.to_i) and
26
+ ( !sc::min_font_weight.first or node['vweight'].to_i >= sc::min_font_weight.first.to_i) and
27
+ ( !sc::max_font_weight.first or node['vweight'].to_i <= sc::max_font_weight.first.to_i) and
28
+ ( !sc::font_family.first or node['vfont'] == sc::font_family.first)
29
+ end.map do |content|
30
+ if sc::attribute.first
31
+ # Select node's attribute if given
32
+ sc::attribute.map { |attribute| { :uri=>doc[:uri], :content=>content, :value=>content[attribute] } }
33
+ else
34
+ [ { :uri=>doc[:uri], :content=>content, :value=>format(content, sc::format, doc[:uri]) } ]
35
+ end
36
+ end.flatten
37
+ end
38
+ end
39
+ end
@@ -1,8 +1,5 @@
1
1
  module Sc
2
- class XPathSelector
3
- include RDF::NodeProxy
4
- include Scrappy::Formats
5
-
2
+ class XPathSelector < Selector
6
3
  def filter doc
7
4
  rdf::value.map do |pattern|
8
5
  interval = if sc::index.first
@@ -1,6 +1,12 @@
1
+ require 'iconv'
2
+ require 'rack-flash'
3
+
1
4
  module Scrappy
2
5
  module Admin
3
6
  def self.registered app
7
+ app.set :method_override, true
8
+ app.use Rack::Flash
9
+
4
10
  app.get '/' do
5
11
  if params[:format] and params[:uri]
6
12
  redirect "#{settings.base_uri}/#{params[:format]}/#{simplify_uri(params[:uri])}"
@@ -9,15 +15,96 @@ module Scrappy
9
15
  end
10
16
  end
11
17
 
18
+ app.get '/javascript' do
19
+ fragments = agent.fragments_for(Scrappy::Kb.extractors, params[:uri])
20
+ content_type 'application/javascript'
21
+ "window.scrappy_extractor=#{fragments.any?};" + open("#{settings.public}/javascripts/annotator.js").read
22
+ end
23
+
12
24
  app.get '/help' do
13
25
  haml :help
14
26
  end
15
27
 
16
- app.get '/kb' do
28
+ # Extractors
29
+
30
+ app.get '/extractors' do
17
31
  @uris = ( Agent::Options.kb.find(nil, Node('rdf:type'), Node('sc:UriSelector')) +
18
32
  Agent::Options.kb.find(nil, Node('rdf:type'), Node('sc:UriPatternSelector')) ).
19
33
  map { |node| node.rdf::value }.flatten.sort.map(&:to_s)
20
- haml :kb
34
+ haml :extractors
35
+ end
36
+
37
+ app.post '/extractors' do
38
+ if params[:html]
39
+ # Generate extractor automatically
40
+ iconv = Iconv.new(params[:encoding], 'UTF-8')
41
+ html = iconv.iconv(params[:html])
42
+ puts params[:html]
43
+ puts params[:uri]
44
+ raise Exception, "Automatic generation of extractors is not supported yet"
45
+ else
46
+ # Store the given extractor
47
+ Scrappy::App.add_extractor RDF::Parser.parse(:yarf,params[:rdf])
48
+ end
49
+ flash[:notice] = "Extractor stored"
50
+ redirect "#{settings.base_uri}/extractors"
51
+ end
52
+ app.delete '/extractors/*' do |uri|
53
+ Scrappy::App.delete_extractor uri
54
+ flash[:notice] = "Extractor deleted"
55
+ redirect "#{settings.base_uri}/extractors"
56
+ end
57
+
58
+ # Patterns
59
+
60
+ app.get '/patterns' do
61
+ @uris = Scrappy::Kb.patterns.find(nil, Node('rdf:type'), Node('sc:Fragment')).
62
+ map { |node| node.sc::type }.flatten.map(&:to_s).sort
63
+ haml :patterns
64
+ end
65
+
66
+ app.delete '/patterns/*' do |uri|
67
+ Scrappy::App.delete_pattern uri
68
+ flash[:notice] = "Pattern deleted"
69
+ redirect "#{settings.base_uri}/patterns"
70
+ end
71
+
72
+ # Samples
73
+
74
+ app.get '/samples' do
75
+ @samples = Scrappy::App.samples
76
+ haml :samples
77
+ end
78
+
79
+ app.get '/samples/:id' do |id|
80
+ Scrappy::App.samples[id.to_i][:html]
81
+ end
82
+
83
+ app.get '/samples/:id/:kb_type' do |id,kb_type|
84
+ kb = (kb_type == "patterns" ? Scrappy::Kb.patterns : Scrappy::Kb.extractors)
85
+ sample = Scrappy::App.samples[id.to_i]
86
+ headers 'Content-Type' => 'text/plain'
87
+ RDF::Graph.new(agent.extract(sample[:uri], sample[:html], kb, Agent::Options.referenceable)).serialize(:yarf)
88
+ end
89
+
90
+ app.post '/samples/:id/train' do |id|
91
+ new_extractor = agent.train Scrappy::App.samples[id.to_i]
92
+ Scrappy::App.add_pattern new_extractor
93
+ flash[:notice] = "Training completed"
94
+ redirect "#{settings.base_uri}/samples"
95
+ end
96
+
97
+ app.post '/samples' do
98
+ html = Iconv.iconv('UTF-8', params[:encoding], params[:html]).first
99
+ sample = Scrappy::App.add_sample(:html=>html, :uri=>params[:uri], :date=>Time.now)
100
+ flash[:notice] = "Sample stored"
101
+ redirect "#{settings.base_uri}/samples"
102
+ end
103
+
104
+ app.delete '/samples/:id' do |id|
105
+ Scrappy::App.delete_sample id.to_i
106
+ flash[:notice] = "Sample deleted"
107
+ redirect "#{settings.base_uri}/samples"
21
108
  end
22
109
  end
23
110
  end
@@ -6,10 +6,19 @@ module Scrappy
6
6
  "var e=document.createElement('script');" +
7
7
  "e.src='https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js';" +
8
8
  "e.id='scrappy';" +
9
- "document.getElementsByTagName('head')[0].appendChild(e);};" +
9
+ "document.getElementsByTagName('head')[0].appendChild(e);" +
10
+ "e=document.createElement('script');" +
11
+ "e.src='https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.10/jquery-ui.min.js';" +
12
+ "document.getElementsByTagName('head')[0].appendChild(e);" +
13
+ "e=document.createElement('link');" +
14
+ "e.href='http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.10/themes/ui-lightness/jquery-ui.css';" +
15
+ "e.rel='stylesheet';" +
16
+ "e.type='text/css';" +
17
+ "document.getElementsByTagName('head')[0].appendChild(e);" +
18
+ "};" +
10
19
  "if(!window.scrappy_loaded){" +
11
20
  "e=document.createElement('script');" +
12
- "e.src='http://localhost:3434/javascripts/scrappy.js?_=#{Time.now.to_i}';" +
21
+ "e.src='http://localhost:3434/javascript?#{Time.now.to_i}&uri='+escape(window.location);" +
13
22
  "e.onerror=function(){alert('Error: Please start Scrappy Server at http://localhost:3434');};" +
14
23
  "document.getElementsByTagName('head')[0].appendChild(e);" +
15
24
  "}"+
@@ -9,6 +9,7 @@ module Scrappy
9
9
  class Server < Sinatra::Base
10
10
  helpers JavaScriptHelpers
11
11
  register Errors
12
+ register Admin if Scrappy::Options.admin
12
13
 
13
14
  enable :sessions
14
15
  set :root, File.join(File.dirname(__FILE__), '..', '..', '..')
@@ -0,0 +1,101 @@
1
+ module Scrappy
2
+ module Trainer
3
+ # Generates visual patterns
4
+ def train *samples
5
+ RDF::Graph.new( samples.inject([]) do |triples, sample|
6
+ triples + train_sample(sample).triples
7
+ end )
8
+ end
9
+
10
+ # Optimizes the knowledge base by generalizing patterns
11
+ def optimize
12
+ end
13
+
14
+ private
15
+ def train_sample sample
16
+ results = RDF::Graph.new extract(sample[:uri], sample[:html], Scrappy::Kb.extractors, :minimum)
17
+
18
+ typed_nodes = results.find(nil, Node("rdf:type"), [])
19
+ non_root_nodes = results.find([], [], nil)
20
+
21
+ nodes = typed_nodes - non_root_nodes
22
+
23
+ RDF::Graph.new( nodes.inject([]) do |triples, node|
24
+ triples + fragment_for(node).graph.triples
25
+ end )
26
+ end
27
+
28
+ def fragment_for node, parent=nil
29
+ fragment = Node(nil)
30
+ node.keys.each do |predicate|
31
+ case predicate
32
+ when ID("sc:source") then
33
+ selector = selector_for(node.sc::source.first, parent)
34
+ fragment.graph << selector
35
+ fragment.sc::selector = selector
36
+ when ID("sc:uri") then
37
+ # Assumption: URIs are extracted from a link
38
+ selector = selector_for(node.sc::uri.first.sc::source.first, node)
39
+ selector.sc::tag = "a"
40
+ selector.sc::attribute = "href"
41
+
42
+ fragment.graph << selector
43
+ fragment.sc::identifier = selector
44
+ when ID("rdf:type") then
45
+ fragment.sc::type = node.rdf::type
46
+ else
47
+ if node[predicate].map(&:class).uniq.first != String
48
+ subfragments = node[predicate].map { |subnode| fragment_for(subnode, node) }
49
+ # Mix the subfragments
50
+ id = subfragments.first
51
+ graph = RDF::Graph.new( subfragments.inject([]) do |triples, subfragment|
52
+ triples + subfragment.graph.triples.map { |s,p,o| [s==subfragment.id ? id : s,p,o] }
53
+ end )
54
+ subfragment = graph[id]
55
+ subfragment.sc::relation = Node(predicate)
56
+ subfragment.sc::min_cardinality = "1"
57
+
58
+ fragment.graph << subfragment
59
+ fragment.sc::subfragment += [subfragment]
60
+ end
61
+ end
62
+ end
63
+ fragment.rdf::type = Node("sc:Fragment") if parent.nil?
64
+ fragment
65
+ end
66
+
67
+ def selector_for fragment, parent=nil
68
+ presentation = fragment.sc::presentation.first
69
+
70
+ selector = Node(nil)
71
+ selector.rdf::type = Node("sc:VisualSelector")
72
+
73
+ origin_x = parent ? parent.sc::source.first.sc::presentation.first.sc::x.first.to_i : 0
74
+ origin_y = parent ? parent.sc::source.first.sc::presentation.first.sc::y.first.to_i : 0
75
+
76
+ relative_x = presentation.sc::x.first.to_i - origin_x
77
+ relative_y = presentation.sc::y.first.to_i - origin_y
78
+
79
+ selector.sc::min_relative_x = relative_x.to_s
80
+ selector.sc::max_relative_x = relative_x.to_s
81
+ selector.sc::min_relative_y = relative_y.to_s
82
+ selector.sc::max_relative_y = relative_y.to_s
83
+ selector.sc::min_x = presentation.sc::x
84
+ selector.sc::max_x = presentation.sc::x
85
+ selector.sc::min_y = presentation.sc::y
86
+ selector.sc::max_y = presentation.sc::y
87
+
88
+ selector.sc::min_width = presentation.sc::width
89
+ selector.sc::max_width = presentation.sc::width
90
+ selector.sc::min_height = presentation.sc::height
91
+ selector.sc::max_height = presentation.sc::height
92
+ selector.sc::min_font_size = presentation.sc::font_size
93
+ selector.sc::max_font_size = presentation.sc::font_size
94
+ selector.sc::min_font_weight = presentation.sc::font_weight
95
+ selector.sc::max_font_weight = presentation.sc::font_weight
96
+ selector.sc::font_family = presentation.sc::font_family
97
+
98
+ selector
99
+ end
100
+ end
101
+ end