@@ -31,8 +31,11 @@ def self.filter(*args)
3131 # :task_list_items - An array of TaskList::Item objects.
3232 class Filter < HTML ::Pipeline ::Filter
3333
34- Incomplete = "[ ]" . freeze
35- Complete = "[x]" . freeze
34+ Incomplete = "[ ]" . freeze
35+ Complete = "[x]" . freeze
36+
37+ IncompletePattern = /\[ [[:space:]]\] / . freeze # matches all whitespace
38+ CompletePattern = /\[ [xX]\] / . freeze # matches any capitalization
3639
3740 # Pattern used to identify all task list items.
3841 # Useful when you need iterate over all items.
@@ -41,26 +44,22 @@ class Filter < HTML::Pipeline::Filter
4144 (?:\s *[-+*]|(?:\d +\. ))? # optional list prefix
4245 \s * # optional whitespace prefix
4346 ( # checkbox
44- #{ Regexp . escape ( Complete ) } |
45- #{ Regexp . escape ( Incomplete ) }
47+ #{ CompletePattern } |
48+ #{ IncompletePattern }
4649 )
4750 (?=\s ) # followed by whitespace
4851 /x
4952
50- ListSelector = [
51- # select UL/OL
52- ".//li[starts-with(text(),'[ ]')]/.." ,
53- ".//li[starts-with(text(),'[x]')]/.." ,
54- # and those wrapped in Ps
55- ".//li/p[1][starts-with(text(),'[ ]')]/../.." ,
56- ".//li/p[1][starts-with(text(),'[x]')]/../.."
57- ] . join ( ' | ' ) . freeze
53+ ListItemSelector = ".//li[task_list_item(.)]" . freeze
5854
59- # Selects all LIs from a TaskList UL/OL
60- ItemSelector = ".//li" . freeze
55+ class XPathSelectorFunction
56+ def self . task_list_item ( nodes )
57+ nodes if nodes . text =~ ItemPattern
58+ end
59+ end
6160
6261 # Selects first P tag of an LI, if present
63- ItemParaSelector = ".// p[1]" . freeze
62+ ItemParaSelector = "./p[1]" . freeze
6463
6564 # List of `TaskList::Item` objects that were recognized in the document.
6665 # This is available in the result hash as `:task_list_items`.
@@ -100,20 +99,22 @@ def render_task_list_item(item)
10099 #
101100 # Returns an Array of Nokogiri::XML::Element objects for ordered and
102101 # unordered lists.
103- def task_lists
104- doc . xpath ( ListSelector )
102+ def list_items
103+ doc . xpath ( ListItemSelector , XPathSelectorFunction )
105104 end
106105
107- # Public: filters a Nokogiri::XML::Element ordered/unordered list, marking
108- # up the list items in order to add behavior and include metadata.
106+ # Filters the source for task list items.
109107 #
110- # Modifies the provided node.
108+ # Each item is wrapped in HTML to identify, style, and layer
109+ # useful behavior on top of.
110+ #
111+ # Modifications apply to the parsed document directly.
111112 #
112113 # Returns nothing.
113- def filter_list ( node )
114- add_css_class ( node , 'task-list' )
114+ def filter!
115+ list_items . reverse . each do |li |
116+ add_css_class ( li . parent , 'task-list' )
115117
116- node . xpath ( ItemSelector ) . each do |li |
117118 outer , inner =
118119 if p = li . xpath ( ItemParaSelector ) [ 0 ]
119120 [ p , p . inner_html ]
@@ -122,28 +123,15 @@ def filter_list(node)
122123 end
123124 if match = ( inner . chomp =~ ItemPattern && $1)
124125 item = TaskList ::Item . new ( match , inner )
125- task_list_items << item
126+ # prepend because we're iterating in reverse
127+ task_list_items . unshift item
126128
127129 add_css_class ( li , 'task-list-item' )
128130 outer . inner_html = render_task_list_item ( item )
129131 end
130132 end
131133 end
132134
133- # Filters the source for task list items.
134- #
135- # Each item is wrapped in HTML to identify, style, and layer
136- # useful behavior on top of.
137- #
138- # Modifications apply to the parsed document directly.
139- #
140- # Returns nothing.
141- def filter!
142- task_lists . each do |node |
143- filter_list node
144- end
145- end
146-
147135 def call
148136 filter!
149137 doc
@@ -153,6 +141,7 @@ def call
153141 # names.
154142 def add_css_class ( node , *new_class_names )
155143 class_names = ( node [ 'class' ] || '' ) . split ( ' ' )
144+ return if new_class_names . all? { |klass | class_names . include? ( klass ) }
156145 class_names . concat ( new_class_names )
157146 node [ 'class' ] = class_names . uniq . join ( ' ' )
158147 end
0 commit comments