task_list 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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