Skip to content
This repository was archived by the owner on Nov 1, 2017. It is now read-only.

Commit ab1ceb2

Browse files
committed
Merge branch 'master' into fire-crema-dependency
Conflicts: lib/task_list/version.rb
2 parents 89a1d43 + 88f0135 commit ab1ceb2

6 files changed

Lines changed: 157 additions & 47 deletions

File tree

app/assets/javascripts/task_lists.coffee

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,29 @@
9797
incomplete = "[ ]"
9898
complete = "[x]"
9999

100+
# Escapes the String for regular expression matching.
101+
escapePattern = (str) ->
102+
str.
103+
replace(/([\[\]])/g, "\\$1"). # escape square brackets
104+
replace(/\s/, "\\s"). # match all white space
105+
replace("x", "[xX]") # match all cases
106+
107+
incompletePattern = ///
108+
#{escapePattern(incomplete)}
109+
///
110+
completePattern = ///
111+
#{escapePattern(complete)}
112+
///
113+
100114
# Pattern used to identify all task list items.
101115
# Useful when you need iterate over all items.
102116
itemPattern = ///
103117
^
104118
(?:\s*[-+*]|(?:\d+\.))? # optional list prefix
105119
\s* # optional whitespace prefix
106120
( # checkbox
107-
#{complete. replace(/([\[\]])/g, "\\$1")}|
108-
#{incomplete.replace(/([\[\]])/g, "\\$1")}
121+
#{escapePattern(complete)}|
122+
#{escapePattern(incomplete)}
109123
)
110124
(?=\s) # followed by whitespace
111125
///
@@ -127,8 +141,8 @@ codeFencesPattern = ///
127141
itemsInParasPattern = ///
128142
^
129143
(
130-
#{complete. replace(/[\[\]]/g, "\\$1")}|
131-
#{incomplete.replace(/[\[\]]/g, "\\$1")}
144+
#{escapePattern(complete)}|
145+
#{escapePattern(incomplete)}
132146
)
133147
.+
134148
$
@@ -148,9 +162,9 @@ updateTaskListItem = (source, itemIndex, checked) ->
148162
if index == itemIndex
149163
line =
150164
if checked
151-
line.replace(incomplete, complete)
165+
line.replace(incompletePattern, complete)
152166
else
153-
line.replace(complete, incomplete)
167+
line.replace(completePattern, incomplete)
154168
line
155169
result.join("\n")
156170

lib/task_list.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ def summary
2424
end
2525

2626
class Item < Struct.new(:checkbox_text, :source)
27-
Complete = "[x]".freeze # see TaskListFilter
27+
Complete = /\[[xX]\]/.freeze # see TaskList::Filter
2828
def complete?
29-
checkbox_text == Complete
29+
checkbox_text =~ Complete
3030
end
3131
end
3232
end

lib/task_list/filter.rb

Lines changed: 27 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -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

test/functional/test_task_lists_behavior.html

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,26 @@
7474
<input type="checkbox" class="task-list-item-checkbox" disabled />
7575
I'm a task list item
7676
</li>
77+
<li class="task-list-item">
78+
<input type="checkbox" class="task-list-item-checkbox" disabled />
79+
with non-breaking space
80+
</li>
81+
<li class="task-list-item">
82+
<input type="checkbox" class="task-list-item-checkbox" disabled checked />
83+
completed, lower
84+
</li>
85+
<li class="task-list-item">
86+
<input type="checkbox" class="task-list-item-checkbox" disabled checked />
87+
completed capitalized
88+
</li>
7789
</ul>
7890
</div>
7991
<form action="/update" method="POST" data-remote data-type="json">
80-
<textarea name="comment[body]" class="js-task-list-field">- [ ] I'm a task list item</textarea>
92+
<textarea name="comment[body]" class="js-task-list-field" cols="40" rows="10">
93+
- [ ] I'm a task list item
94+
- [ ] with non-breaking space
95+
- [x] completed, lower
96+
- [X] completed capitalized</textarea>
8197
</form>
8298
</div>
8399

test/task_list/filter_test.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,61 @@ def test_handles_encoding_correctly
7878
assert_equal unicode, item.text.strip
7979
end
8080

81+
def test_handles_nested_items
82+
text = <<-md
83+
- [ ] one
84+
- [ ] one.one
85+
md
86+
assert item = filter(text)[:output].css('.task-list-item .task-list-item').pop
87+
end
88+
89+
def test_handles_complicated_nested_items
90+
text = <<-md
91+
- [ ] one
92+
- [ ] one.one
93+
- [x] one.two
94+
- [ ] one.two.one
95+
- [ ] one.two.two
96+
- [ ] one.three
97+
- [ ] one.four
98+
- [ ] two
99+
- [x] two.one
100+
- [ ] two.two
101+
- [ ] three
102+
md
103+
104+
assert_equal 6 + 2, filter(text)[:output].css('.task-list-item .task-list-item').size
105+
assert_equal 2, filter(text)[:output].css('.task-list-item .task-list-item .task-list-item').size
106+
end
107+
108+
# NOTE: This is an edge case experienced regularly by users using a Swiss
109+
# German keyboard.
110+
# See: https://github.com/github/github/pull/18362
111+
def test_non_breaking_space_between_brackets
112+
text = "- [\xC2\xA0] ok"
113+
assert item = filter(text)[:output].css('.task-list-item').pop, "item expected"
114+
assert_equal 'ok', item.text.strip
115+
end
116+
117+
# See: https://github.com/github/github/pull/18362
118+
def test_non_breaking_space_between_brackets_in_paras
119+
text = <<-md
120+
- [\xC2\xA0] one
121+
- [\xC2\xA0] this one will be wrapped in a para
122+
123+
- [\xC2\xA0] this one too, wtf
124+
md
125+
assert_equal 3, filter(text)[:output].css(@item_selector).size
126+
end
127+
128+
def test_capital_X
129+
text = <<-md
130+
- [x] lower case
131+
- [X] capital
132+
md
133+
assert_equal 2, filter(text)[:output].css("[checked]").size
134+
end
135+
81136
protected
82137

83138
def filter(input, context = @context, result = nil)

test/unit/test_updates.coffee

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,36 @@ module "TaskList updates",
2020
disabled: true
2121
checked: false
2222

23+
# non-breaking space. See: https://github.com/github/task-lists/pull/14
24+
@nbsp = String.fromCharCode(160)
25+
@incompleteNBSPItem = $ '<li>', class: 'task-list-item'
26+
@incompleteNBSPCheckbox = $ '<input>',
27+
type: 'checkbox'
28+
class: 'task-list-item-checkbox'
29+
disabled: true
30+
checked: false
31+
2332
@field = $ '<textarea>', class: 'js-task-list-field', text: """
2433
- [x] complete
2534
- [ ] incomplete
35+
- [#{@nbsp}] incompleteNBSP
2636
"""
2737

2838
@changes =
2939
toComplete: """
3040
- [ ] complete
3141
- [ ] incomplete
42+
- [#{@nbsp}] incompleteNBSP
3243
"""
3344
toIncomplete: """
3445
- [x] complete
3546
- [x] incomplete
47+
- [#{@nbsp}] incompleteNBSP
48+
"""
49+
toIncompleteNBSP: """
50+
- [x] complete
51+
- [ ] incomplete
52+
- [x] incompleteNBSP
3653
"""
3754

3855
@completeItem.append @completeCheckbox
@@ -43,6 +60,10 @@ module "TaskList updates",
4360
@list.append @incompleteItem
4461
@incompleteItem.expectedIndex = 2
4562

63+
@incompleteNBSPItem.append @incompleteNBSPCheckbox
64+
@list.append @incompleteNBSPItem
65+
@incompleteNBSPItem.expectedIndex = 3
66+
4667
@container.append @list
4768
@container.append @field
4869

@@ -79,3 +100,18 @@ asyncTest "updates the source, marking the complete item as incomplete", ->
79100
, 20
80101

81102
@completeCheckbox.click()
103+
104+
# See: https://github.com/github/task-lists/pull/14
105+
asyncTest "updates the source for items with non-breaking spaces", ->
106+
expect 3
107+
108+
@field.on 'tasklist:changed', (event, index, checked) =>
109+
ok checked
110+
equal index, @incompleteNBSPItem.expectedIndex
111+
equal @field.val(), @changes.toIncompleteNBSP
112+
113+
setTimeout ->
114+
start()
115+
, 20
116+
117+
@incompleteNBSPCheckbox.click()

0 commit comments

Comments
 (0)