scrappy 0.3.0 → 0.3.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.
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