Class: Banzai::Filter::TaskListFilter
- Inherits:
-
HTML::Pipeline::Filter
- Object
- HTML::Pipeline::Filter
- Banzai::Filter::TaskListFilter
- Includes:
- Concerns::PipelineTimingCheck
- Defined in:
- lib/banzai/filter/task_list_filter.rb
Overview
TaskListFilter annotates task list items with aria-labels, adds strikethroughs to the text body of inapplicable items (created with ‘[~]`).
This should be run on the HTML generated by the Markdown filter, which handles the actual parsing, after the SanitizationFilter.
Constant Summary collapse
- CSS =
'input.task-list-item-checkbox'- XPATH =
Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
Constants included from Concerns::PipelineTimingCheck
Concerns::PipelineTimingCheck::MAX_PIPELINE_SECONDS
Class Method Summary collapse
-
.text_for_task_item_from_input(input) ⇒ Object
Gets the text for the task list item, given the <input> checkbox that declares it.
-
.text_html_for_task_item_from_input(input) ⇒ Object
Gets the HTML corresponding to the task list item text, given the <input> checkbox that declares it.
-
.yield_next_siblings_until(start, els) ⇒ Object
Yields the #next_sibling of start, and then the #next_sibling of that, until either there are no more next siblings or a matching element is encountered.
-
.yield_text_nodes_without_descending_into(start, els) ⇒ Object
Starting from start, iteratively yield text nodes contained within its children, and its (repeated) #next_siblings and their children, not descending into any of the elements given by els.
Instance Method Summary collapse
Methods included from Concerns::PipelineTimingCheck
Class Method Details
.text_for_task_item_from_input(input) ⇒ Object
Gets the text for the task list item, given the <input> checkbox that declares it.
51 52 53 54 55 56 57 |
# File 'lib/banzai/filter/task_list_filter.rb', line 51 def text_for_task_item_from_input(input) text_content = +'' yield_next_siblings_until(input, %w[ol ul]) do |el| text_content << el.text end text_content.strip end |
.text_html_for_task_item_from_input(input) ⇒ Object
Gets the HTML corresponding to the task list item text, given the <input> checkbox that declares it. This should be used for task list item matching only.
61 62 63 64 65 66 67 |
# File 'lib/banzai/filter/task_list_filter.rb', line 61 def text_html_for_task_item_from_input(input) html_content = +'' yield_next_siblings_until(input, %w[ol ul]) do |el| html_content << el.to_html end html_content end |
.yield_next_siblings_until(start, els) ⇒ Object
Yields the #next_sibling of start, and then the #next_sibling of that, until either there are no more next siblings or a matching element is encountered.
The following #next_sibling is evaluated before each element is yielded, so they can safely be reparented or removed without affecting iteration.
74 75 76 77 78 79 80 81 |
# File 'lib/banzai/filter/task_list_filter.rb', line 74 def yield_next_siblings_until(start, els) it = start.next_sibling while it && els.exclude?(it.name) following = it.next_sibling yield it it = following end end |
.yield_text_nodes_without_descending_into(start, els) ⇒ Object
Starting from start, iteratively yield text nodes contained within its children, and its (repeated) #next_siblings and their children, not descending into any of the elements given by els.
The following #next_sibling is evaluated before yielding, as above.
88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/banzai/filter/task_list_filter.rb', line 88 def yield_text_nodes_without_descending_into(start, els) stack = [start] while stack.any? it = stack.pop stack << it.next_sibling if it.next_sibling if it.text? yield it unless it.content.blank? elsif els.exclude?(it.name) stack.concat(it.children.reverse) end end end |
Instance Method Details
#call ⇒ Object
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/banzai/filter/task_list_filter.rb', line 16 def call doc.xpath(XPATH).each do |node| text_content = self.class.text_for_task_item_from_input(node) truncated_text_content = text_content.truncate(100, separator: ' ', omission: '…') node['aria-label'] = format(_('Check option: %{option}'), option: truncated_text_content) next unless node.has_attribute?('data-inapplicable') # We manually apply a <s> to strikethrough text in inapplicable task items, # specifically in tight lists where text within the list items isn't contained in a paragraph. # (Those are handled entirely by styles.) # # To handle tight lists, we wrap every text node after the checkbox in <s>, not descending # into <p> or <div> (as they're indicative of non-tight lists) or <ul> or <ol> (as we # explicitly want to avoid strikethrough styles on sublists, which may have applicable # task items!). # This is awkward, but we need to include a text node with a space after the input. # Otherwise, the strikethrough will start *immediately* next to the <input>, because # the first next sibling of the input is always a text node that starts with a space! space = node.add_next_sibling(node.document.create_text_node(' ')) inapplicable_s = node.document.create_element('s') inapplicable_s['class'] = 'inapplicable' self.class.yield_text_nodes_without_descending_into(space.next_sibling, %w[p div ul ol]) do |el| el.wrap(inapplicable_s) end end doc end |