1# frozen_string_literal: true
2
3require 'task_list'
4require 'task_list/filter'
5
6# Contains functionality for objects that can have task lists in their
7# descriptions.  Task list items can be added with Markdown like "* [x] Fix
8# bugs".
9#
10# Used by MergeRequest and Issue
11module Taskable
12  COMPLETED          = 'completed'
13  INCOMPLETE         = 'incomplete'
14  COMPLETE_PATTERN   = /(\[[xX]\])/.freeze
15  INCOMPLETE_PATTERN = /(\[\s\])/.freeze
16  ITEM_PATTERN       = %r{
17    ^
18    (?:(?:>\s{0,4})*)          # optional blockquote characters
19    (?:\s*(?:[-+*]|(?:\d+\.)))+  # list prefix (one or more) required - task item has to be always in a list
20    \s+                        # whitespace prefix has to be always presented for a list item
21    (\[\s\]|\[[xX]\])          # checkbox
22    (\s.+)                     # followed by whitespace and some text.
23  }x.freeze
24
25  def self.get_tasks(content)
26    content.to_s.scan(ITEM_PATTERN).map do |checkbox, label|
27      # ITEM_PATTERN strips out the hyphen, but Item requires it. Rabble rabble.
28      TaskList::Item.new("- #{checkbox}", label.strip)
29    end
30  end
31
32  def self.get_updated_tasks(old_content:, new_content:)
33    old_tasks = get_tasks(old_content)
34    new_tasks = get_tasks(new_content)
35
36    new_tasks.select.with_index do |new_task, i|
37      old_task = old_tasks[i]
38      next unless old_task
39
40      new_task.source == old_task.source && new_task.complete? != old_task.complete?
41    end
42  end
43
44  # Called by `TaskList::Summary`
45  def task_list_items
46    return [] if description.blank?
47
48    @task_list_items ||= Taskable.get_tasks(description) # rubocop:disable Gitlab/ModuleWithInstanceVariables
49  end
50
51  def tasks
52    @tasks ||= TaskList.new(self)
53  end
54
55  # Return true if this object's description has any task list items.
56  def tasks?
57    tasks.summary.items?
58  end
59
60  # Return a string that describes the current state of this Taskable's task
61  # list items, e.g. "12 of 20 tasks completed"
62  def task_status(short: false)
63    return '' if description.blank?
64
65    prep, completed = if short
66                        ['/', '']
67                      else
68                        [' of ', ' completed']
69                      end
70
71    sum = tasks.summary
72    "#{sum.complete_count}#{prep}#{sum.item_count} #{'task'.pluralize(sum.item_count)}#{completed}"
73  end
74
75  # Return a short string that describes the current state of this Taskable's
76  # task list items -- for small screens
77  def task_status_short
78    task_status(short: true)
79  end
80
81  def task_completion_status
82    @task_completion_status ||= {
83        count: tasks.summary.item_count,
84        completed_count: tasks.summary.complete_count
85    }
86  end
87end
88