html-pipeline-task_list 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +91 -0
- data/.circleci/setup-rubygems.sh +3 -0
- data/.gitignore +15 -0
- data/.rubocop.yml +45 -0
- data/CONTRIBUTING.md +11 -0
- data/Gemfile +4 -0
- data/Guardfile +27 -0
- data/LICENSE +21 -0
- data/README.md +104 -0
- data/Rakefile +16 -0
- data/app/assets/javascripts/task_list.js +107 -0
- data/app/assets/stylesheets/task_list.scss +20 -0
- data/html-pipeline-task_list.gemspec +37 -0
- data/lib/html/pipeline/task_list.rb +51 -0
- data/lib/html/pipeline/task_list/filter.rb +179 -0
- data/lib/html/pipeline/task_list/railtie.rb +30 -0
- data/lib/html/pipeline/task_list/summary.rb +34 -0
- data/lib/html/pipeline/task_list/version.rb +9 -0
- data/package-lock.json +455 -0
- data/package.json +36 -0
- data/test/behavior.html +109 -0
- data/test/dist/jquery-ujs.js +1 -0
- data/test/dist/jquery.js +1 -0
- data/test/dist/qunit.css +1 -0
- data/test/dist/qunit.js +1 -0
- data/test/dist/task_list.js +1 -0
- data/test/html/pipeline/task_list/filter_test.rb +197 -0
- data/test/html/pipeline/task_list/summary_test.rb +40 -0
- data/test/html/pipeline/task_list_test.rb +32 -0
- data/test/qunit.html +17 -0
- data/test/run-qunit.js +168 -0
- data/test/server.js +51 -0
- data/test/test_helper.rb +5 -0
- data/test/unit/events_test.js +106 -0
- data/test/unit/updates_test.js +476 -0
- metadata +253 -0
data/package.json
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
{
|
2
|
+
"name": "task_list",
|
3
|
+
"version": "1.0.2",
|
4
|
+
"description": "GitHub-flavored-Markdown TaskList components",
|
5
|
+
"homepage": "https://github.com/codetre/html-pipeline-task_list",
|
6
|
+
"repository": "https://github.com/codetre/html-pipeline-task_list",
|
7
|
+
"license": "MIT",
|
8
|
+
"dependencies": {
|
9
|
+
"jquery": "^3.4.1",
|
10
|
+
"jquery-ujs": "^1.2.2"
|
11
|
+
},
|
12
|
+
"devDependencies": {
|
13
|
+
"node-qunit-puppeteer": "^2.0.1",
|
14
|
+
"qunit": "^2.9.3"
|
15
|
+
},
|
16
|
+
"scripts": {
|
17
|
+
"server": "node ./test/server.js .",
|
18
|
+
"test": "node-qunit-puppeteer ./test/qunit.html 10000 --no-sandbox"
|
19
|
+
},
|
20
|
+
"main": [
|
21
|
+
"app/assets/javascripts/task_list.js",
|
22
|
+
"app/assets/stylesheets/task_list.scss"
|
23
|
+
],
|
24
|
+
"ignore": [
|
25
|
+
".gitignore",
|
26
|
+
".travis.yml",
|
27
|
+
"*.gemspec",
|
28
|
+
"*.md",
|
29
|
+
"config.ru",
|
30
|
+
"Gemfile",
|
31
|
+
"lib/",
|
32
|
+
"Rakefile",
|
33
|
+
"script/",
|
34
|
+
"test/"
|
35
|
+
]
|
36
|
+
}
|
data/test/behavior.html
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<script type="text/javascript" src="dist/jquery.js"></script>
|
6
|
+
<script type="text/javascript" src="https://code.jquery.com/jquery-migrate-3.1.0.js"></script>
|
7
|
+
<script type="text/javascript" src="dist/jquery-ujs.js"></script>
|
8
|
+
<script type="text/javascript" src="dist/task_list.js"></script>
|
9
|
+
<script type="text/javascript">
|
10
|
+
function logEvent(event) {
|
11
|
+
console.log(event)
|
12
|
+
$('.log').prepend("<li>" + event.type + "</li>")
|
13
|
+
}
|
14
|
+
|
15
|
+
setInterval(function() {
|
16
|
+
if (!$('.log li:first').first().hasClass("timestamp"))
|
17
|
+
$('.log').prepend("<li class='timestamp'><time>" + new Date + "</time></li>")
|
18
|
+
}, 3000)
|
19
|
+
|
20
|
+
$(document).on('tasklist:enabled', logEvent)
|
21
|
+
$(document).on('tasklist:disabled', logEvent)
|
22
|
+
$(document).on('tasklist:change', '.js-task-list-field', function(event){
|
23
|
+
logEvent(event)
|
24
|
+
$(this).closest('.js-task-list-container').taskList('enable')
|
25
|
+
})
|
26
|
+
$(document).on('tasklist:changed', '.js-task-list-field', function(event){
|
27
|
+
logEvent(event)
|
28
|
+
$(this).closest('form').submit()
|
29
|
+
})
|
30
|
+
|
31
|
+
$(document).on('ajaxStart', logEvent)
|
32
|
+
$(document).on('ajaxError', logEvent)
|
33
|
+
$(document).on('ajaxComplete', logEvent)
|
34
|
+
$(document).on('ajaxSuccess', function(event){
|
35
|
+
logEvent(event)
|
36
|
+
$(this).taskList()
|
37
|
+
})
|
38
|
+
</script>
|
39
|
+
|
40
|
+
<style>
|
41
|
+
body {
|
42
|
+
font: 13px Helvetica;
|
43
|
+
padding: 20px;
|
44
|
+
}
|
45
|
+
input {
|
46
|
+
font-size: 20px;
|
47
|
+
}
|
48
|
+
time {
|
49
|
+
font-size: 10px;
|
50
|
+
font-style: oblique;
|
51
|
+
}
|
52
|
+
|
53
|
+
.task-list {
|
54
|
+
list-style-type: none;
|
55
|
+
}
|
56
|
+
.task-list .task-list-item {
|
57
|
+
opacity: 0.75;
|
58
|
+
}
|
59
|
+
.task-list .task-list-item.enabled {
|
60
|
+
opacity: 1.0;
|
61
|
+
}
|
62
|
+
|
63
|
+
.log {
|
64
|
+
position: absolute;
|
65
|
+
top: 20px;
|
66
|
+
right: 50px;
|
67
|
+
color: #222;
|
68
|
+
list-style: none;
|
69
|
+
margin: 0;
|
70
|
+
padding: 0;
|
71
|
+
}
|
72
|
+
</style>
|
73
|
+
</head>
|
74
|
+
<body>
|
75
|
+
<div class="js-task-list-container is-task-list-enabled">
|
76
|
+
<div class="markdown">
|
77
|
+
<ul class="task-list">
|
78
|
+
<li class="task-list-item enabled">
|
79
|
+
<input type="checkbox" class="task-list-item-checkbox" enabled />
|
80
|
+
I'm a task list item
|
81
|
+
</li>
|
82
|
+
<li class="task-list-item enabled">
|
83
|
+
<input type="checkbox" class="task-list-item-checkbox" enabled />
|
84
|
+
with non-breaking space
|
85
|
+
</li>
|
86
|
+
<li class="task-list-item enabled">
|
87
|
+
<input type="checkbox" class="task-list-item-checkbox" enabled checked />
|
88
|
+
completed, lower
|
89
|
+
</li>
|
90
|
+
<li class="task-list-item enabled">
|
91
|
+
<input type="checkbox" class="task-list-item-checkbox" enabled checked />
|
92
|
+
completed capitalized
|
93
|
+
</li>
|
94
|
+
</ul>
|
95
|
+
</div>
|
96
|
+
<form action="/update" method="POST" data-remote="true" data-type="json">
|
97
|
+
<textarea name="comment[body]" class="js-task-list-field" cols="40" rows="10">
|
98
|
+
- [ ] I'm a task list item
|
99
|
+
- [ ] with non-breaking space
|
100
|
+
- [x] completed, lower
|
101
|
+
- [X] completed capitalized</textarea>
|
102
|
+
</form>
|
103
|
+
</div>
|
104
|
+
|
105
|
+
<ul class="log">
|
106
|
+
</ul>
|
107
|
+
</body>
|
108
|
+
</html>
|
109
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
../../node_modules/jquery-ujs/src/rails.js
|
data/test/dist/jquery.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
../../node_modules/jquery/dist/jquery.js
|
data/test/dist/qunit.css
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
../../node_modules/qunit/qunit/qunit.css
|
data/test/dist/qunit.js
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
../../node_modules/qunit/qunit/qunit.js
|
@@ -0,0 +1 @@
|
|
1
|
+
../../app/assets/javascripts/task_list.js
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('../../../test_helper', __dir__)
|
4
|
+
require 'html/pipeline/task_list/filter'
|
5
|
+
|
6
|
+
class HTML::Pipeline::TaskList::FilterTest < Minitest::Test
|
7
|
+
def setup
|
8
|
+
@pipeline = HTML::Pipeline.new [
|
9
|
+
HTML::Pipeline::MarkdownFilter,
|
10
|
+
HTML::Pipeline::TaskList::Filter
|
11
|
+
], {}, {}
|
12
|
+
|
13
|
+
@context = {}
|
14
|
+
@item_selector = 'input.task-list-item-checkbox[type=checkbox]'
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_filters_items_in_a_list
|
18
|
+
text = <<~MARKDOWN
|
19
|
+
- [ ] incomplete
|
20
|
+
- [x] complete
|
21
|
+
MARKDOWN
|
22
|
+
|
23
|
+
assert_equal 2, filter(text)[:output].css(@item_selector).size
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_filters_items_with_html_contents
|
27
|
+
text = <<~MARKDOWN
|
28
|
+
- [ ] incomplete **with bold** text
|
29
|
+
- [x] complete __with italic__ text
|
30
|
+
MARKDOWN
|
31
|
+
|
32
|
+
assert_equal 2, filter(text)[:output].css(@item_selector).size
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_filters_items_in_a_list_wrapped_in_paras
|
36
|
+
# See issue #7951 for details.
|
37
|
+
text = <<~MARKDOWN
|
38
|
+
- [ ] one
|
39
|
+
- [ ] this one will be wrapped in a para
|
40
|
+
|
41
|
+
- [ ] this one too, wtf
|
42
|
+
MARKDOWN
|
43
|
+
|
44
|
+
assert_equal 3, filter(text)[:output].css(@item_selector).size
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_populates_result_with_task_list_items
|
48
|
+
text = <<~MARKDOWN
|
49
|
+
- [ ] incomplete
|
50
|
+
- [x] complete
|
51
|
+
MARKDOWN
|
52
|
+
|
53
|
+
result = filter(text)
|
54
|
+
assert !result[:task_list_items].empty?
|
55
|
+
incomplete, complete = result[:task_list_items]
|
56
|
+
|
57
|
+
assert incomplete
|
58
|
+
assert !incomplete.complete?
|
59
|
+
|
60
|
+
assert complete
|
61
|
+
assert complete.complete?
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_skips_lists_in_code_blocks
|
65
|
+
code = <<~MARKDOWN
|
66
|
+
```
|
67
|
+
- [ ] incomplete
|
68
|
+
- [x] complete
|
69
|
+
```
|
70
|
+
MARKDOWN
|
71
|
+
|
72
|
+
assert filter(code)[:output].css(@item_selector).empty?,
|
73
|
+
'should not have any task list items'
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_handles_encoding_correctly
|
77
|
+
unicode = '中文'
|
78
|
+
text = <<~MARKDOWN
|
79
|
+
- [ ] #{unicode}
|
80
|
+
MARKDOWN
|
81
|
+
|
82
|
+
assert item = filter(text)[:output].css('.task-list-item').pop
|
83
|
+
assert_equal unicode, item.text.strip
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_handles_nested_items
|
87
|
+
text = <<~MARKDOWN
|
88
|
+
- [ ] one
|
89
|
+
- [ ] one.one
|
90
|
+
MARKDOWN
|
91
|
+
|
92
|
+
assert filter(text)[:output].css('.task-list-item .task-list-item').pop
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_handles_complicated_nested_items
|
96
|
+
text = <<~MARKDOWN
|
97
|
+
- [ ] one
|
98
|
+
- [ ] one.one
|
99
|
+
- [x] one.two
|
100
|
+
- [ ] one.two.one
|
101
|
+
- [ ] one.two.two
|
102
|
+
- [ ] one.three
|
103
|
+
- [ ] one.four
|
104
|
+
- [ ] two
|
105
|
+
- [x] two.one
|
106
|
+
- [ ] two.two
|
107
|
+
- [ ] three
|
108
|
+
MARKDOWN
|
109
|
+
|
110
|
+
output = filter(text)[:output]
|
111
|
+
|
112
|
+
assert_equal 6 + 2, output.css('.task-list-item .task-list-item').size
|
113
|
+
assert_equal 2, output.css('.task-list-item .task-list-item .task-list-item').size
|
114
|
+
end
|
115
|
+
|
116
|
+
# NOTE: This is an edge case experienced regularly by users using a Swiss
|
117
|
+
# German keyboard.
|
118
|
+
# See: https://github.com/github/github/pull/18362
|
119
|
+
def test_non_breaking_space_between_brackets
|
120
|
+
text = "- [\xC2\xA0] ok"
|
121
|
+
|
122
|
+
assert item = filter(text)[:output].css('.task-list-item').pop, 'item expected'
|
123
|
+
assert_equal 'ok', item.text.strip
|
124
|
+
end
|
125
|
+
|
126
|
+
# See: https://github.com/github/github/pull/18362
|
127
|
+
def test_non_breaking_space_between_brackets_in_paras
|
128
|
+
text = <<~MARKDOWN
|
129
|
+
- [\xC2\xA0] one
|
130
|
+
- [\xC2\xA0] this one will be wrapped in a para
|
131
|
+
|
132
|
+
- [\xC2\xA0] this one too, wtf
|
133
|
+
MARKDOWN
|
134
|
+
|
135
|
+
assert_equal 3, filter(text)[:output].css(@item_selector).size
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_capital_x
|
139
|
+
text = <<~MARKDOWN
|
140
|
+
- [x] lower case
|
141
|
+
- [X] capital
|
142
|
+
MARKDOWN
|
143
|
+
|
144
|
+
assert_equal 2, filter(text)[:output].css('[checked]').size
|
145
|
+
end
|
146
|
+
|
147
|
+
def test_skips_task_list_when_no_content
|
148
|
+
text = <<~MARKDOWN
|
149
|
+
- [x]
|
150
|
+
- [X]
|
151
|
+
- [ ]
|
152
|
+
MARKDOWN
|
153
|
+
|
154
|
+
result = filter(text)
|
155
|
+
assert_nil result[:task_list_items]
|
156
|
+
assert result[:output].css(@item_selector).empty?,
|
157
|
+
'should not have any task list items'
|
158
|
+
end
|
159
|
+
|
160
|
+
def test_handles_input_with_a_mix_of_lists_and_task_lists
|
161
|
+
text = <<~MARKDOWN
|
162
|
+
- Item 1
|
163
|
+
- Item 2
|
164
|
+
|
165
|
+
- [x] Task 1
|
166
|
+
- [ ] Task 2
|
167
|
+
MARKDOWN
|
168
|
+
|
169
|
+
result = filter(text)
|
170
|
+
|
171
|
+
output = result[:output].to_s
|
172
|
+
complete, incomplete = result[:task_list_items]
|
173
|
+
assert complete.complete?
|
174
|
+
refute incomplete.complete?
|
175
|
+
|
176
|
+
# regular list items
|
177
|
+
assert_match %r{<li>\n<p>Item 1</p>\n</li>}, output
|
178
|
+
assert_match %r{<li>\n<p>Item 2</p>\n</li>}, output
|
179
|
+
|
180
|
+
# task list items
|
181
|
+
assert_match(
|
182
|
+
%r{<p><input type="checkbox" class="task-list-item-checkbox" checked disabled> Task 1</p>},
|
183
|
+
output
|
184
|
+
)
|
185
|
+
assert_match(
|
186
|
+
%r{<p><input type="checkbox" class="task-list-item-checkbox" disabled> Task 2</p>},
|
187
|
+
output
|
188
|
+
)
|
189
|
+
end
|
190
|
+
|
191
|
+
protected
|
192
|
+
|
193
|
+
def filter(input, context = @context, result = nil)
|
194
|
+
result ||= {}
|
195
|
+
@pipeline.call(input, context, result)
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('../../../test_helper', __dir__)
|
4
|
+
|
5
|
+
class HTML::Pipeline::TaskList::SummaryTest < Minitest::Test
|
6
|
+
def setup
|
7
|
+
@complete = make_item '[x]', 'complete'
|
8
|
+
@incomplete = make_item '[ ]', 'incomplete'
|
9
|
+
@items = [@complete, @incomplete]
|
10
|
+
@summary = make_summary @items
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_no_items
|
14
|
+
summary = make_summary []
|
15
|
+
assert !summary.items?, 'no task list items are expected'
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_items
|
19
|
+
assert @summary.items?, 'task list items are expected'
|
20
|
+
assert_equal 2, @summary.item_count
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_complete_count
|
24
|
+
assert_equal 1, @summary.complete_count
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_incomplete_count
|
28
|
+
assert_equal 1, @summary.incomplete_count
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def make_item(checkbox_text = '[ ]', source = 'an item!')
|
34
|
+
HTML::Pipeline::TaskList::Item.new(checkbox_text, source)
|
35
|
+
end
|
36
|
+
|
37
|
+
def make_summary(items)
|
38
|
+
HTML::Pipeline::TaskList::Summary.new(items)
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path('../../test_helper', __dir__)
|
4
|
+
|
5
|
+
class TaskListTest < Minitest::Test
|
6
|
+
Record = Struct.new(:body) do
|
7
|
+
def task_list_items
|
8
|
+
[]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_has_summary
|
13
|
+
assert summary = task_list('- [ ] one').summary, 'summary expected'
|
14
|
+
assert_kind_of HTML::Pipeline::TaskList::Summary, summary
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_complete_item
|
18
|
+
item = HTML::Pipeline::TaskList::Item.new('[x]', 'complete')
|
19
|
+
assert item.complete?, 'expected to be complete'
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_incomplete_item
|
23
|
+
item = HTML::Pipeline::TaskList::Item.new('[ ]', 'incomplete')
|
24
|
+
assert !item.complete?, 'expected to be incomplete'
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def task_list(text)
|
30
|
+
HTML::Pipeline::TaskList.new(Record.new(text))
|
31
|
+
end
|
32
|
+
end
|
data/test/qunit.html
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<link rel="stylesheet" href="dist/qunit.css">
|
6
|
+
<script type="text/javascript" src="dist/jquery.js"></script>
|
7
|
+
<script src="https://code.jquery.com/jquery-migrate-3.1.0.js"></script>
|
8
|
+
<script type="text/javascript" src="dist/qunit.js"></script>
|
9
|
+
<script type="text/javascript" src="dist/task_list.js"></script>
|
10
|
+
<script type="text/javascript" src="unit/events_test.js"></script>
|
11
|
+
<script type="text/javascript" src="unit/updates_test.js"></script>
|
12
|
+
</head>
|
13
|
+
<body>
|
14
|
+
<div id="qunit"></div>
|
15
|
+
<div id="qunit-fixture"></div>
|
16
|
+
</body>
|
17
|
+
</html>
|