mill 0.1 → 0.3

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.
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