mill 0.1 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/mill/error.rb ADDED
@@ -0,0 +1,7 @@
1
+ module Mill
2
+
3
+ class Error < RuntimeError
4
+
5
+ end
6
+
7
+ end
@@ -1,91 +1,59 @@
1
1
  module HTMLHelpers
2
2
 
3
- class HTMLError < Exception; end
4
-
5
- IgnoreErrors = %Q{
6
- <table> lacks "summary" attribute
7
- <img> lacks "alt" attribute
8
- <form> proprietary attribute "novalidate"
9
- <input> attribute "type" has invalid value "email"
10
- <input> attribute "tabindex" has invalid value "-1"
11
- }.split(/\n/).map(&:strip)
12
-
13
- def html_document(&block)
14
- builder = Nokogiri::HTML::Builder.new(encoding: 'utf-8') do |doc|
3
+ LinkElementsXPath = '//@href | //@src'
4
+
5
+ def html_document(type=:html4_transitional, &block)
6
+ doc = Nokogiri::HTML::Document.new
7
+ doc.encoding = 'UTF-8'
8
+ doc.internal_subset.remove
9
+ case type
10
+ when :html4_transitional
11
+ doc.create_internal_subset('html', '-//W3C//DTD HTML 4.01 Transitional//EN', 'http://www.w3.org/TR/html4/loose.dtd')
12
+ when :html5
13
+ doc.create_internal_subset('html', nil, nil)
14
+ else
15
+ raise "Unknown HTML type: #{type.inspect}"
16
+ end
17
+ Nokogiri::HTML::Builder.with(doc) do |doc|
15
18
  yield(doc)
16
19
  end
17
- builder.doc
20
+ doc
18
21
  end
19
22
 
20
23
  def html_fragment(&block)
21
24
  html = Nokogiri::HTML::DocumentFragment.parse('')
22
25
  Nokogiri::HTML::Builder.with(html) do |html|
23
- yield(html)
26
+ yield(html) if block_given?
24
27
  end
25
28
  html
26
29
  end
27
30
 
28
31
  def parse_html(str)
29
- html = Nokogiri::HTML::Document.parse(str) { |config| config.strict }
30
- html.errors.each do |error|
31
- next if error.message =~ /^Tag (.*?) invalid$/
32
- raise HTMLError, "HTML error at line #{error.line}, column #{error.column}: #{error.message}"
32
+ if str.strip.empty?
33
+ html = html_fragment
34
+ else
35
+ html = Nokogiri::HTML::Document.parse(str) { |config| config.strict }
36
+ check_errors(html)
33
37
  end
34
38
  html
35
39
  end
36
40
 
37
- def tidy_html(html, &block)
38
- html_str = html.to_s
39
- tidy = TidyFFI::Tidy.new(html_str, char_encoding: 'UTF8')
40
- errors = parse_tidy_errors(tidy).reject do |error|
41
- IgnoreErrors.include?(error[:error])
42
- end
43
- unless errors.empty?
44
- full_error = StringIO.new('')
45
- full_error.puts "invalid HTML:"
46
- html_lines = html_str.split(/\n/)
47
- errors.each do |error|
48
- full_error.puts "\t#{error[:msg]}:"
49
- html_lines.each_with_index do |html_line, i|
50
- if i >= [0, error[:line] - 2].max && i <= [error[:line] + 2, html_lines.length].min
51
- if i == error[:line]
52
- output = [
53
- error[:column] > 0 ? (html_line[0 .. error[:column] - 1]) : '',
54
- Term::ANSIColor.negative,
55
- html_line[error[:column]],
56
- Term::ANSIColor.clear,
57
- html_line[error[:column] + 1 .. -1],
58
- ]
59
- else
60
- output = [html_line]
61
- end
62
- full_error.puts "\t\t%3s: %s" % [i + 1, output.join]
63
- end
64
- end
65
- if block_given?
66
- yield(full_error.string)
67
- else
68
- STDERR.print(full_error.string)
69
- end
70
- raise HTMLError, "HTML error: #{error[:msg]}" if error[:type] == :error
71
- end
72
- end
41
+ def parse_html_fragment(str)
42
+ html = Nokogiri::HTML::DocumentFragment.parse(str) { |config| config.strict }
43
+ check_errors(html)
44
+ html
73
45
  end
74
46
 
75
- def parse_tidy_errors(tidy)
76
- return [] unless tidy.errors
77
- tidy.errors.split(/\n/).map do |error_str|
78
- error_str =~ /^line (\d+) column (\d+) - (.*?): (.*)$/ or raise "Can't parse error: #{error_str}"
79
- {
80
- msg: error_str,
81
- line: $1.to_i - 1,
82
- column: $2.to_i - 1,
83
- type: $3.downcase.to_sym,
84
- error: $4.strip,
85
- }
47
+ def check_errors(html)
48
+ html.errors.each do |error|
49
+ raise Mill::Error, "HTML error #{error}" unless error.message =~ /Tag .+? invalid$/
86
50
  end
87
51
  end
88
52
 
53
+ def find_link_elements(html)
54
+ html.xpath(LinkElementsXPath)
55
+ end
56
+
89
57
  def replace_element(html, xpath, &block)
90
58
  html.xpath(xpath).each do |elem|
91
59
  elem.replace(yield(elem))
@@ -145,6 +113,15 @@ module HTMLHelpers
145
113
  end
146
114
  end
147
115
 
116
+ def link_if(state, html, &block)
117
+ elem = html_fragment { |h| yield(h) }
118
+ if state
119
+ html.a(href: uri) { html << elem.to_html }
120
+ else
121
+ html << elem.to_html
122
+ end
123
+ end
124
+
148
125
  class PreText < String
149
126
 
150
127
  def to_html
@@ -157,8 +134,21 @@ module HTMLHelpers
157
134
 
158
135
  class ::String
159
136
 
160
- def to_html
161
- Nokogiri::HTML::DocumentFragment.parse(RubyPants.new(self).to_html).to_html
137
+ Converters = {
138
+ nil => RubyPants,
139
+ smart_quotes: RubyPants,
140
+ markdown: Kramdown::Document,
141
+ textile: RedCloth,
142
+ pre: PreText,
143
+ }
144
+
145
+ def to_html(options={})
146
+ converter = Converters[options[:mode]] or raise "Unknown to_html mode: #{options[:mode].inspect}"
147
+ html = Nokogiri::HTML::DocumentFragment.parse(converter.new(self).to_html)
148
+ if !options[:multiline] && (p_elem = html.at_xpath('p'))
149
+ html = p_elem.children.to_html
150
+ end
151
+ html.to_html
162
152
  end
163
153
 
164
154
  end
@@ -1,4 +1,4 @@
1
- class Mill
1
+ module Mill
2
2
 
3
3
  class Navigator
4
4
 
@@ -7,76 +7,52 @@ class Mill
7
7
  attr_accessor :uri
8
8
  attr_accessor :title
9
9
 
10
- def initialize(params={})
11
- params.each { |k, v| send("#{k}=", v) }
12
- end
13
-
14
- def uri=(uri)
10
+ def initialize(uri:, title: nil)
15
11
  @uri = Addressable::URI.parse(uri)
12
+ @title = title
16
13
  end
17
14
 
18
15
  end
19
16
 
20
- attr_accessor :items
21
-
22
- def initialize(params={})
23
- @items = []
24
- params.each { |k, v| send("#{k}=", v) }
17
+ def initialize(items: [])
18
+ @items = Hash[
19
+ items.map do |uri, title|
20
+ item = Item.new(uri: uri, title: title)
21
+ [item.uri, item]
22
+ end
23
+ ]
25
24
  end
26
25
 
27
- def item_states_for_uri(uri, &block)
28
- current_item = within_item = nil
29
- if (item = @items.find { |item| item.uri.relative? && item.uri == uri })
30
- current_item = item
31
- else
32
- within_item = @items.select do |item|
33
- item.uri.relative? && uri.path.start_with?(item.uri.path)
34
- end.sort_by do |item|
35
- item.uri.path.count('/')
36
- end.last
37
- end
38
- @items.each do |item|
39
- if item == current_item
40
- state = :current
41
- elsif item == within_item
42
- state = :within
43
- else
44
- state = :other
45
- end
46
- yield(item, state)
47
- end
26
+ def items
27
+ @items.values
48
28
  end
49
29
 
50
30
  def first_item
51
- @items.first
31
+ @items.values.first
52
32
  end
53
33
 
54
34
  def last_item
55
- @items.last
35
+ @items.values.last
56
36
  end
57
37
 
58
38
  def previous_item(uri)
59
- index = find_item_index_by_uri(uri)
60
- if index && index > 0
61
- @items[index - 1]
62
- else
63
- nil
39
+ if (item = @items[uri])
40
+ i = @items.values.index(item)
41
+ if i > 0
42
+ return @items.values[i - 1]
43
+ end
64
44
  end
45
+ nil
65
46
  end
66
47
 
67
48
  def next_item(uri)
68
- index = find_item_index_by_uri(uri)
69
- if index && index < @items.length - 1
70
- @items[index + 1]
71
- else
72
- nil
73
- end
74
- end
75
-
76
- def find_item_index_by_uri(uri)
77
- if (item = @items.find { |item| item.uri == uri })
78
- @items.index(item)
49
+ if (item = @items[uri])
50
+ i = @items.values.index(item)
51
+ if i < @items.length - 1
52
+ return @items.values[i + 1]
53
+ end
79
54
  end
55
+ nil
80
56
  end
81
57
 
82
58
  end
data/lib/mill/resource.rb CHANGED
@@ -1,68 +1,103 @@
1
- class Mill
1
+ module Mill
2
2
 
3
3
  class Resource
4
4
 
5
+ FileTypes = []
6
+
5
7
  attr_accessor :input_file
6
8
  attr_accessor :output_file
9
+ attr_accessor :type
7
10
  attr_accessor :date
8
11
  attr_accessor :public
9
12
  attr_accessor :content
10
- attr_accessor :mill
11
-
12
- def self.type
13
- # implemented by subclass
14
- end
15
-
16
- def initialize(params={})
17
- params.each { |k, v| send("#{k}=", v) }
18
- end
19
-
20
- def input_file=(p)
21
- @input_file = Path.new(p)
22
- end
23
-
24
- def output_file=(p)
25
- @output_file = Path.new(p)
13
+ attr_accessor :site
14
+
15
+ def initialize(input_file: nil,
16
+ output_file: nil,
17
+ type: nil,
18
+ date: nil,
19
+ public: false,
20
+ content: nil,
21
+ site: nil)
22
+ if input_file
23
+ @input_file = Path.new(input_file)
24
+ @date = input_file.mtime.to_datetime
25
+ else
26
+ @date = DateTime.now
27
+ end
28
+ @output_file = Path.new(output_file) if output_file
29
+ @type = type
30
+ self.date = date if date
31
+ self.public = public
32
+ @content = content
33
+ @site = site
26
34
  end
27
35
 
28
- def date=(x)
29
- @date = case x
30
- when String
31
- DateTime.parse(x)
32
- when Time
33
- DateTime.parse(x.to_s)
34
- when Date, DateTime
35
- x
36
+ def date=(date)
37
+ @date = case date
38
+ when String, Time
39
+ DateTime.parse(date.to_s)
40
+ when Date, DateTime, nil
41
+ date
36
42
  else
37
- raise "Can't assign date: #{x.inspect}"
43
+ raise Error, "Can't assign 'date' attribute: #{date.inspect}"
38
44
  end
39
45
  end
40
46
 
41
- def public=(x)
42
- @public = case x
47
+ def public=(public)
48
+ @public = case public
43
49
  when 'false', FalseClass
44
50
  false
45
51
  when 'true', TrueClass
46
52
  true
47
53
  else
48
- raise "Can't assign public: #{x.inspect}"
54
+ raise Error, "Can't assign 'public' attribute: #{public.inspect}"
55
+ end
56
+ end
57
+
58
+ def public?
59
+ @public
60
+ end
61
+
62
+ def inspect
63
+ "<%p> input_file: %p, output_file: %p, type: %p, date: %s, public: %p, content: <%p>" % [
64
+ self.class,
65
+ @input_file ? @input_file.relative_to(@site.input_dir).to_s : nil,
66
+ @output_file ? @output_file.relative_to(@site.output_dir).to_s : nil,
67
+ @type.to_s,
68
+ @date.to_s,
69
+ @public,
70
+ @content && @content.class,
71
+ ]
72
+ end
73
+
74
+ def find_sibling_resources(klass=nil)
75
+ # parent_uri = parent_uri
76
+ @site.resources.select do |resource|
77
+ resource != self &&
78
+ (klass.nil? || resource.kind_of?(klass)) &&
79
+ resource.parent_uri == parent_uri
49
80
  end
50
81
  end
51
82
 
52
83
  def uri
53
- raise "#{@input_file}: No output file defined for #{self.class}" unless @output_file
54
- path = '/' + @output_file.relative_to(@mill.output_dir).to_s
84
+ raise Error, "#{@input_file}: No output file defined for #{self.class}" unless @output_file
85
+ path = '/' + @output_file.relative_to(@site.output_dir).to_s
55
86
  path.sub!(%r{/index\.html$}, '/')
56
- path.sub!(%r{\.html$}, '') if @mill.shorten_uris
57
- Addressable::URI.parse(path)
87
+ path.sub!(%r{\.html$}, '') if @site.shorten_uris
88
+ Addressable::URI.encode(path, Addressable::URI)
89
+ end
90
+
91
+ def parent_uri
92
+ uri + '.'
58
93
  end
59
94
 
60
95
  def absolute_uri
61
- @mill.site_uri + uri
96
+ @site.site_uri + uri
62
97
  end
63
98
 
64
99
  def tag_uri
65
- @mill.tag_uri + uri
100
+ @site.tag_uri + uri
66
101
  end
67
102
 
68
103
  def change_frequency
@@ -74,40 +109,24 @@ class Mill
74
109
  end
75
110
 
76
111
  def load
77
- raise "#{uri} (#{self.class}): no content" unless @input_file || @content
78
- self.date ||= @input_file ? @input_file.mtime : DateTime.now
79
- @mill.update_resource(self)
112
+ # implemented in subclass
80
113
  end
81
114
 
82
115
  def build
116
+ # implemented in subclass
117
+ end
118
+
119
+ def save
83
120
  @output_file.dirname.mkpath
84
- if (c = final_content)
121
+ if (content = final_content)
85
122
  # ;;warn "#{uri}: writing #{@input_file} to #{@output_file}"
86
- @output_file.write(c.to_s)
123
+ @output_file.write(content.to_s)
87
124
  @output_file.utime(@date.to_time, @date.to_time)
88
125
  elsif @input_file
89
126
  # ;;warn "#{uri}: copying #{@input_file} to #{@output_file}"
90
127
  @input_file.copy(@output_file)
91
128
  else
92
- raise "Can't build resource without content or input file: #{uri}"
93
- end
94
- validate
95
- end
96
-
97
- def validate
98
- if (schema = @mill.schema_for_type(self.class.type))
99
- validate_xml(schema)
100
- end
101
- end
102
-
103
- def validate_xml(schema)
104
- doc = Nokogiri::XML::Document.parse(@output_file.open)
105
- errors = doc.errors + schema.validate(doc)
106
- unless errors.empty?
107
- errors.each do |error|
108
- warn "[#{error.file}:#{error.line}:#{error.column}] #{error}"
109
- end
110
- raise "#{uri}: Validation failed"
129
+ raise Error, "Can't build resource without content or input file: #{uri}"
111
130
  end
112
131
  end
113
132