task_list 1.0.2

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.
@@ -0,0 +1,32 @@
1
+ require 'task_list/summary'
2
+ require 'task_list/version'
3
+
4
+ # encoding: utf-8
5
+ class TaskList
6
+ attr_reader :record
7
+
8
+ # `record` is the resource with the Markdown source text with task list items
9
+ # following this syntax:
10
+ #
11
+ # - [ ] a task list item
12
+ # - [ ] another item
13
+ # - [x] a completed item
14
+ #
15
+ def initialize(record)
16
+ @record = record
17
+ end
18
+
19
+ # Public: return the TaskList::Summary for this task list.
20
+ #
21
+ # Returns a TaskList::Summary.
22
+ def summary
23
+ @summary ||= TaskList::Summary.new(record.task_list_items)
24
+ end
25
+
26
+ class Item < Struct.new(:checkbox_text, :source)
27
+ Complete = /\[[xX]\]/.freeze # see TaskList::Filter
28
+ def complete?
29
+ checkbox_text =~ Complete
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,149 @@
1
+ # encoding: utf-8
2
+ require 'html/pipeline'
3
+ require 'task_list'
4
+
5
+ class TaskList
6
+ # Returns a `Nokogiri::DocumentFragment` object.
7
+ def self.filter(*args)
8
+ Filter.call(*args)
9
+ end
10
+
11
+ # TaskList filter replaces task list item markers (`[ ]` and `[x]`) with
12
+ # checkboxes, marked up with metadata and behavior.
13
+ #
14
+ # This should be run on the HTML generated by the Markdown filter, after the
15
+ # SanitizationFilter.
16
+ #
17
+ # Syntax
18
+ # ------
19
+ #
20
+ # Task list items must be in a list format:
21
+ #
22
+ # ```
23
+ # - [ ] incomplete
24
+ # - [x] complete
25
+ # ```
26
+ #
27
+ # Results
28
+ # -------
29
+ #
30
+ # The following keys are written to the result hash:
31
+ # :task_list_items - An array of TaskList::Item objects.
32
+ class Filter < HTML::Pipeline::Filter
33
+
34
+ Incomplete = "[ ]".freeze
35
+ Complete = "[x]".freeze
36
+
37
+ IncompletePattern = /\[[[:space:]]\]/.freeze # matches all whitespace
38
+ CompletePattern = /\[[xX]\]/.freeze # matches any capitalization
39
+
40
+ # Pattern used to identify all task list items.
41
+ # Useful when you need iterate over all items.
42
+ ItemPattern = /
43
+ ^
44
+ (?:\s*[-+*]|(?:\d+\.))? # optional list prefix
45
+ \s* # optional whitespace prefix
46
+ ( # checkbox
47
+ #{CompletePattern}|
48
+ #{IncompletePattern}
49
+ )
50
+ (?=\s) # followed by whitespace
51
+ /x
52
+
53
+ ListItemSelector = ".//li[task_list_item(.)]".freeze
54
+
55
+ class XPathSelectorFunction
56
+ def self.task_list_item(nodes)
57
+ nodes if nodes.text =~ ItemPattern
58
+ end
59
+ end
60
+
61
+ # Selects first P tag of an LI, if present
62
+ ItemParaSelector = "./p[1]".freeze
63
+
64
+ # List of `TaskList::Item` objects that were recognized in the document.
65
+ # This is available in the result hash as `:task_list_items`.
66
+ #
67
+ # Returns an Array of TaskList::Item objects.
68
+ def task_list_items
69
+ result[:task_list_items] ||= []
70
+ end
71
+
72
+ # Renders the item checkbox in a span including the item state.
73
+ #
74
+ # Returns an HTML-safe String.
75
+ def render_item_checkbox(item)
76
+ %(<input type="checkbox"
77
+ class="task-list-item-checkbox"
78
+ #{'checked="checked"' if item.complete?}
79
+ disabled="disabled"
80
+ />)
81
+ end
82
+
83
+ # Public: Marks up the task list item checkbox with metadata and behavior.
84
+ #
85
+ # NOTE: produces a string that, when assigned to a Node's `inner_html`,
86
+ # will corrupt the string contents' encodings. Instead, we parse the
87
+ # rendered HTML and explicitly set its encoding so that assignment will
88
+ # not change the encodings.
89
+ #
90
+ # See [this pull](https://github.com/github/github/pull/8505) for details.
91
+ #
92
+ # Returns the marked up task list item Nokogiri::XML::NodeSet object.
93
+ def render_task_list_item(item)
94
+ Nokogiri::HTML.fragment \
95
+ item.source.sub(ItemPattern, render_item_checkbox(item)), 'utf-8'
96
+ end
97
+
98
+ # Public: Select all task lists from the `doc`.
99
+ #
100
+ # Returns an Array of Nokogiri::XML::Element objects for ordered and
101
+ # unordered lists.
102
+ def list_items
103
+ doc.xpath(ListItemSelector, XPathSelectorFunction)
104
+ end
105
+
106
+ # Filters the source for task list items.
107
+ #
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.
112
+ #
113
+ # Returns nothing.
114
+ def filter!
115
+ list_items.reverse.each do |li|
116
+ add_css_class(li.parent, 'task-list')
117
+
118
+ outer, inner =
119
+ if p = li.xpath(ItemParaSelector)[0]
120
+ [p, p.inner_html]
121
+ else
122
+ [li, li.inner_html]
123
+ end
124
+ if match = (inner.chomp =~ ItemPattern && $1)
125
+ item = TaskList::Item.new(match, inner)
126
+ # prepend because we're iterating in reverse
127
+ task_list_items.unshift item
128
+
129
+ add_css_class(li, 'task-list-item')
130
+ outer.inner_html = render_task_list_item(item)
131
+ end
132
+ end
133
+ end
134
+
135
+ def call
136
+ filter!
137
+ doc
138
+ end
139
+
140
+ # Private: adds a CSS class name to a node, respecting existing class
141
+ # names.
142
+ def add_css_class(node, *new_class_names)
143
+ class_names = (node['class'] || '').split(' ')
144
+ return if new_class_names.all? { |klass| class_names.include?(klass) }
145
+ class_names.concat(new_class_names)
146
+ node['class'] = class_names.uniq.join(' ')
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,20 @@
1
+ class TaskList
2
+
3
+ def self.root_path
4
+ @root_path ||= Pathname.new(File.expand_path("../../../", __FILE__))
5
+ end
6
+
7
+ def self.asset_paths
8
+ @paths ||= Dir[root_path.join("app/assets/*")]
9
+ end
10
+
11
+ if defined? ::Rails::Railtie
12
+ class Railtie < ::Rails::Railtie
13
+ initializer "task_list" do |app|
14
+ TaskList.asset_paths.each do |path|
15
+ app.config.assets.paths << path
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+ require 'html/pipeline'
3
+ require 'task_list'
4
+
5
+ class TaskList
6
+ # Provides a summary of provided TaskList `items`.
7
+ #
8
+ # `items` is an Array of TaskList::Item objects.
9
+ class Summary < Struct.new(:items)
10
+ # Public: returns true if there are any TaskList::Item objects.
11
+ def items?
12
+ item_count > 0
13
+ end
14
+
15
+ # Public: returns the number of TaskList::Item objects.
16
+ def item_count
17
+ items.size
18
+ end
19
+
20
+ # Public: returns the number of complete TaskList::Item objects.
21
+ def complete_count
22
+ items.select{ |i| i.complete? }.size
23
+ end
24
+
25
+ # Public: returns the number of incomplete TaskList::Item objects.
26
+ def incomplete_count
27
+ items.select{ |i| !i.complete? }.size
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ class TaskList
2
+ VERSION = [1, 0, 2].join('.')
3
+ end
@@ -0,0 +1,13 @@
1
+ #!/bin/sh
2
+ set -e 0
3
+
4
+ if ! bundle check 1>/dev/null 2>&1; then
5
+ bundle install --no-color --binstubs --path vendor/gems
6
+ fi
7
+
8
+ if ! npm list bower 2>&1 | grep 0.8.5 >/dev/null; then
9
+ # npm install bower
10
+ npm install git://github.com/twitter/bower.git
11
+ fi
12
+
13
+ bower install --no-color
@@ -0,0 +1,6 @@
1
+ #!/bin/sh -e
2
+ # Usage: script/cibuild
3
+ # CI build script.
4
+
5
+ ./script/testsuite 4018
6
+ bundle exec rake test
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ root = File.expand_path("../..", __FILE__)
4
+ Dir.chdir root
5
+
6
+ port = ARGV[0] || 4000
7
+
8
+ pid = fork do
9
+ $stderr.reopen "/dev/null" # silence WEBrick output
10
+ exec 'bundle', 'exec', 'rackup', '-p', port.to_s
11
+ end
12
+ sleep 1
13
+
14
+ status = system('phantomjs', "#{root}/test/run-qunit.coffee", "http://localhost:#{port}/test/index.html")
15
+
16
+ Process.kill 'SIGINT', pid
17
+ Process.wait pid
18
+
19
+ exit status
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'task_list/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "task_list"
8
+ gem.version = TaskList::VERSION
9
+ gem.authors = ["Matt Todd"]
10
+ gem.email = ["matt@github.com"]
11
+ gem.description = %q{GitHub-flavored-Markdown TaskList components}
12
+ gem.summary = %q{GitHub-flavored-Markdown TaskList components}
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_dependency "html-pipeline"
20
+
21
+ gem.add_development_dependency "github-markdown"
22
+ gem.add_development_dependency "rake"
23
+ gem.add_development_dependency "coffee-script"
24
+ gem.add_development_dependency "json"
25
+ gem.add_development_dependency "rack"
26
+ gem.add_development_dependency "sprockets"
27
+ gem.add_development_dependency "minitest", "~> 5.3.2"
28
+ end
@@ -0,0 +1,3 @@
1
+ #= require jquery
2
+ #= require rails-behaviors/remote
3
+ #= require task_list
@@ -0,0 +1,103 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <script type="text/javascript" src="/assets/functional/helpers/remote.js"></script>
6
+ <script type="text/javascript">
7
+ function logEvent(event) {
8
+ console.log(event)
9
+ $('.log').prepend("<li>" + event.type + "</li>")
10
+ }
11
+
12
+ setInterval(function() {
13
+ if (!$('.log li:first').first().hasClass("timestamp"))
14
+ $('.log').prepend("<li class='timestamp'><time>" + new Date + "</time></li>")
15
+ }, 3000)
16
+
17
+ $(document).on('tasklist:enabled', logEvent)
18
+ $(document).on('tasklist:disabled', logEvent)
19
+ $(document).on('tasklist:change', '.js-task-list-field', function(event){
20
+ logEvent(event)
21
+ $(this).closest('.js-task-list-container').taskList('disable')
22
+ })
23
+ $(document).on('tasklist:changed', '.js-task-list-field', function(event){
24
+ logEvent(event)
25
+ $(this).closest('form').submit()
26
+ })
27
+
28
+ $(document).on('ajaxStart', logEvent)
29
+ $(document).on('ajaxSuccess', '.js-task-list-container', function(event){
30
+ logEvent(event)
31
+ $(this).taskList()
32
+ })
33
+ </script>
34
+
35
+ <style>
36
+ body {
37
+ font: 13px Helvetica;
38
+ padding: 20px;
39
+ }
40
+ input {
41
+ font-size: 20px;
42
+ }
43
+ time {
44
+ font-size: 10px;
45
+ font-style: oblique;
46
+ }
47
+
48
+ .task-list {
49
+ list-style-type: none;
50
+ }
51
+ .task-list .task-list-item {
52
+ opacity: 0.75;
53
+ }
54
+ .task-list .task-list-item.enabled {
55
+ opacity: 1.0;
56
+ }
57
+
58
+ .log {
59
+ position: absolute;
60
+ top: 20px;
61
+ right: 50px;
62
+ color: #222;
63
+ list-style: none;
64
+ margin: 0;
65
+ padding: 0;
66
+ }
67
+ </style>
68
+ </head>
69
+ <body>
70
+ <div class="js-task-list-container js-task-list-enable">
71
+ <div class="markdown">
72
+ <ul class="task-list">
73
+ <li class="task-list-item">
74
+ <input type="checkbox" class="task-list-item-checkbox" disabled />
75
+ I'm a task list item
76
+ </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>
89
+ </ul>
90
+ </div>
91
+ <form action="/update" method="POST" data-remote data-type="json">
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>
97
+ </form>
98
+ </div>
99
+
100
+ <ul class="log">
101
+ </ul>
102
+ </body>
103
+ </html>
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <link rel="stylesheet" href="/assets/units.css">
6
+ <script type="text/javascript" src="/assets/units.js"></script>
7
+ </head>
8
+ <body>
9
+ <div id="qunit"></div>
10
+ <div id="qunit-fixture"></div>
11
+ </body>
12
+ </html>
@@ -0,0 +1,50 @@
1
+ fs = require 'fs'
2
+ print = (s) -> fs.write "/dev/stderr", s, 'w'
3
+
4
+ page = new WebPage()
5
+ page.onConsoleMessage = (msg) -> console.error msg
6
+
7
+ timeoutId = null
8
+ deferTimeout = ->
9
+ clearTimeout timeoutId if timeoutId
10
+ timeoutId = setTimeout ->
11
+ console.error "Timeout"
12
+ phantom.exit 1
13
+ , 3000
14
+
15
+ page.open phantom.args[0], ->
16
+ deferTimeout()
17
+
18
+ setInterval ->
19
+ tests = page.evaluate ->
20
+ tests = document.getElementById('qunit-tests')?.children
21
+ return unless tests
22
+ for test in tests when test.className isnt 'running' and not test.recorded
23
+ test.recorded = true
24
+ if test.className is 'pass'
25
+ '.'
26
+ else if test.className is 'fail'
27
+ 'F'
28
+
29
+ return unless tests
30
+ for test in tests when test
31
+ deferTimeout()
32
+ print test
33
+
34
+ result = page.evaluate ->
35
+ result = document.getElementById('qunit-testresult')
36
+ tests = document.getElementById('qunit-tests').children
37
+
38
+ if result.innerText.match /completed/
39
+ console.error ""
40
+
41
+ for test in tests when test.className is 'fail'
42
+ console.error test.innerText
43
+
44
+ console.error result.innerText
45
+ return parseInt result.getElementsByClassName('failed')[0].innerText
46
+
47
+ return
48
+
49
+ phantom.exit result if result?
50
+ , 100