html-pipeline-task_list 0.0.1
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.
- 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>
|