From 7d4a0514632c3b3ac38da3be8f1f6949eb1b1f38 Mon Sep 17 00:00:00 2001 From: as-op Date: Wed, 7 Feb 2024 16:51:26 +0100 Subject: [PATCH] fix(html table cell): better support for html lists --- lib/md_to_pdf/elements/html.rb | 71 +++++++++++++++++++++---- lib/md_to_pdf/elements/list.rb | 34 ++++++++---- spec/fixtures/list/tasklist_html.md | 1 + spec/fixtures/table/linefeed_in_cell.md | 4 ++ spec/fixtures/table/list_in_cell.md | 1 + spec/markdown_to_pdf/list_spec.rb | 11 ++++ spec/markdown_to_pdf/table_spec.rb | 39 ++++++++++++-- 7 files changed, 136 insertions(+), 25 deletions(-) create mode 100644 spec/fixtures/list/tasklist_html.md create mode 100644 spec/fixtures/table/linefeed_in_cell.md create mode 100644 spec/fixtures/table/list_in_cell.md diff --git a/lib/md_to_pdf/elements/html.rb b/lib/md_to_pdf/elements/html.rb index 4419602..7633f7a 100644 --- a/lib/md_to_pdf/elements/html.rb +++ b/lib/md_to_pdf/elements/html.rb @@ -107,14 +107,11 @@ def data_inlinehtml_tag(tag, node, opts) when 'img' result.push({ image: sub.attr('src') }) when 'input' - if sub.attr('type') == 'checkbox' - checkbox = opt_task_list_point_sign(@styles.task_list_point, sub.attributes.key?('checked')) - result.push(text_hash(checkbox, current_opts)) - else - data_array, current_opts = handle_unknown_inline_html_tag(sub, node, current_opts) - result.concat(data_array) - end - when 'ul', 'ol', 'li', 'label', 'p' + data_array, current_opts = handle_unknown_inline_html_tag(sub, node, current_opts) + result.concat(data_array) + when 'ul', 'ol' + result.concat(data_inlinehtml_list_tag(sub, node, opts)) + when 'label', 'p', 'li' result.concat(data_inlinehtml_tag(sub, node, opts)) when 'br' result.push(text_hash_raw("\n", current_opts)) @@ -126,6 +123,25 @@ def data_inlinehtml_tag(tag, node, opts) result end + def data_inlinehtml_list_tag(tag, node, opts) + result = [] + points, level, _list_style, content_opts = data_html_list(tag, node, opts) + result.push(text_hash_raw("\n", content_opts).merge({ list_level: level, list_indent: 0 })) if level > 1 + points.each do |point| + data = data_inlinehtml_tag(point[:tag], node, content_opts) + data.push(text_hash_raw("\n", content_opts).merge({ list_entry_type: 'end' })) + data[0][:list_entry_type] = 'first' unless data.empty? + data.unshift(text_hash(point[:bullet], point[:opts]).merge({ list_entry_type: 'bullet' })) + data.each do |item| + item[:list_level] = level if item[:list_level].nil? + item[:list_indent] = point[:width] if item[:list_indent].nil? + item[:list_indent_space] = point[:space_width] if item[:list_indent_space].nil? + end + result.concat(data) + end + result + end + def collect_html_table_tag_rows(tag, table_font_opts, opts) rows = [] tag.children.each do |sub| @@ -257,16 +273,51 @@ def collect_html_table_tag_cell(tag, opts) cell_data end + def space_stuffing(width, space_width) + amount = (width / space_width).truncate + return '' if amount < 1 + + Prawn::Text::NBSP * amount + end + + def indent_html_table_list_items(cell_data) + cell_data.each do |item| + next if item[:list_level].nil? + + # Note: There is no settings for paddings of text fragments in Prawn::Table + # so as a workaround the lists are stuffed with spaces, which is of course not pixel perfect + + # first indenting with spaces of multiline list items + # * item + # multiline item + # multiline item + if item[:list_entry_type].nil? && item[:text] != "\n" + item[:text] = "#{space_stuffing(item[:list_indent], item[:list_indent_space])}#{item[:text]}" + end + + # second indenting of nested lists + # * item + # multiline item + # * sub list item + # * sub list item + # sub list multiline item + if item[:list_level] > 1 && (item[:list_entry_type].nil? || item[:list_entry_type] == 'bullet') && item[:text] != "\n" + item[:text] = "#{space_stuffing(item[:list_indent], item[:list_indent_space])}#{item[:text]}" + end + end + cell_data + end + def collect_html_table_tag_row(tag, table_font_opts, opts) cells = [] tag.children.each do |sub| case sub.name when 'th' cell_data = collect_html_table_tag_cell(sub, opts.merge(table_font_opts[:header])) - cells.push(cell_data) + cells.push(indent_html_table_list_items(cell_data)) when 'td' cell_data = collect_html_table_tag_cell(sub, opts.merge(table_font_opts[:cell])) - cells.push(cell_data) + cells.push(indent_html_table_list_items(cell_data)) end end cells diff --git a/lib/md_to_pdf/elements/list.rb b/lib/md_to_pdf/elements/list.rb index ac68ddd..1aa97d9 100644 --- a/lib/md_to_pdf/elements/list.rb +++ b/lib/md_to_pdf/elements/list.rb @@ -25,12 +25,11 @@ def draw_list(node, opts) end end - def draw_html_list_tag(tag, node, opts) + def data_html_list(tag, node, opts) level = count_list_level_html(tag) is_ordered = tag.name.downcase == 'ol' - list_style = list_style(level, is_ordered, false) - padding = level == 1 ? opts_padding(list_style) : {} - # point_inline = opt_list_point_inline?(list_style) + is_task_list = !tag.search("input[type=checkbox]").first.nil? + list_style = list_style(level, is_ordered, is_task_list) content_opts = opts_font(list_style, opts).merge( { force_paragraph: { @@ -38,7 +37,13 @@ def draw_html_list_tag(tag, node, opts) } } ) - points = collect_points_html(tag, level, is_ordered, content_opts) + points = collect_points_html(tag, level, is_ordered, is_task_list, content_opts) + [points, level, list_style, content_opts] + end + + def draw_html_list_tag(tag, node, opts) + points, level, list_style, content_opts = data_html_list(tag, node, opts) + padding = level == 1 ? opts_padding(list_style) : {} with_block_padding_all(padding) do draw_points_html(points, node, content_opts) end @@ -82,8 +87,8 @@ def draw_inline_points(points, opts) end end - def collect_points_html(tag, level, is_ordered, content_opts) - point_style = list_point_style(level, is_ordered, false) + def collect_points_html(tag, level, is_ordered, is_task_list, content_opts) + point_style = list_point_style(level, is_ordered, is_task_list) auto_span = opt_list_point_spanning?(point_style) bullet_opts = opts_font(point_style, content_opts) spacing = opt_list_point_spacing(point_style) @@ -91,13 +96,22 @@ def collect_points_html(tag, level, is_ordered, content_opts) index = 1 tag.children.each do |sub| if sub.name.downcase == 'li' - bullet = list_bullet(point_style, is_ordered, false, index, false) + checked = false + if is_task_list + checked_box_tag = tag.search("input[type=checkbox]").first + unless checked_box_tag.nil? + checked = checked_box_tag.attributes.key?('checked') + checked_box_tag.remove + end + end + bullet = list_bullet(point_style, is_ordered, is_task_list, index, checked) bullet_width = measure_text_width(bullet, bullet_opts) + spacing - points.push({ tag: sub, bullet: bullet, width: bullet_width, opts: bullet_opts }) + space_width = measure_text_width(Prawn::Text::NBSP, bullet_opts) + points.push({ tag: sub, bullet: bullet, width: bullet_width, space_width: space_width, opts: bullet_opts }) index += 1 end end - if auto_span + if auto_span || is_task_list max_span = max_point_width(points) points.each { |point| point[:width] = max_span } end diff --git a/spec/fixtures/list/tasklist_html.md b/spec/fixtures/list/tasklist_html.md new file mode 100644 index 0000000..1f0ce93 --- /dev/null +++ b/spec/fixtures/list/tasklist_html.md @@ -0,0 +1 @@ + diff --git a/spec/fixtures/table/linefeed_in_cell.md b/spec/fixtures/table/linefeed_in_cell.md new file mode 100644 index 0000000..1feeb2e --- /dev/null +++ b/spec/fixtures/table/linefeed_in_cell.md @@ -0,0 +1,4 @@ +| Description Column 1 | +|-------------------------------| +| Lorem ipsum dolor
sit amet | +| Consectetur
adipiscing elit | diff --git a/spec/fixtures/table/list_in_cell.md b/spec/fixtures/table/list_in_cell.md new file mode 100644 index 0000000..15d633b --- /dev/null +++ b/spec/fixtures/table/list_in_cell.md @@ -0,0 +1 @@ +
  • test1
  • test2
    test2cont
  • test3
    • test3.1
    • test3.2
      test3.2cont
    • test3.3
  1. wooo
  2. waaa
  3. wiiiii
diff --git a/spec/markdown_to_pdf/list_spec.rb b/spec/markdown_to_pdf/list_spec.rb index c7213ce..b57a21d 100644 --- a/spec/markdown_to_pdf/list_spec.rb +++ b/spec/markdown_to_pdf/list_spec.rb @@ -152,6 +152,17 @@ { x: 76.032, y: 553.176, text: "three" }]) end + it 'creates task lists by html' do + generator.parse_file('list/tasklist_html.md', {}) + expect_pdf([ + { x: 36.0, y: 747.384, text: "[ ]" }, + { x: 52.008, y: 747.384, text: "aha" }, + { x: 36.0, y: 733.512, text: "[x]" }, + { x: 52.008, y: 733.512, text: "oho" }, + { x: 36.0, y: 719.64, text: "[ ]" }, + { x: 52.008, y: 719.64, text: "ehe" }]) + end + it 'creates an ordered list with correct numbers inline' do generator.parse_file('list/inline.md', { ordered_list: { point_inline: true } }) expect_pdf([ diff --git a/spec/markdown_to_pdf/table_spec.rb b/spec/markdown_to_pdf/table_spec.rb index accccbf..ab97608 100644 --- a/spec/markdown_to_pdf/table_spec.rb +++ b/spec/markdown_to_pdf/table_spec.rb @@ -89,14 +89,14 @@ { x: 421.71429, y: 744.756, text: "Header 5" }, { x: 498.85714, y: 744.756, text: "Header 6" }, { x: 36.0, y: 730.884, text: "Entry 1" }, - { x: 344.57143, y: 730.884, text: "[x]" }, - { x: 498.85714, y: 730.884, text: "[x]" }, + { x: 344.57143, y: 730.884, text: "[x] " }, + { x: 498.85714, y: 730.884, text: "[x] " }, { x: 36.0, y: 717.012, text: "Entry 2" }, - { x: 344.57143, y: 717.012, text: "[x]" }, - { x: 421.71429, y: 717.012, text: "[x]" }, + { x: 344.57143, y: 717.012, text: "[x] " }, + { x: 421.71429, y: 717.012, text: "[x] " }, { x: 36.0, y: 703.14, text: "Entry 3" }, { x: 36.0, y: 689.268, text: "Entry 4" }, - { x: 113.14286, y: 689.268, text: "[x]" }]) + { x: 113.14286, y: 689.268, text: "[x] " }]) end it 'creates a table without bad wrapping with doc font style' do @@ -163,4 +163,33 @@ { x: 36.0, y: 689.268, text: "Color empty cells to the" }, { x: 36.0, y: 675.396, text: "right" }]) end + + it 'creates a html table with linefeeds inside' do + generator.parse_file('table/linefeed_in_cell.md') + expect_pdf([ + { x: 36.0, y: 744.756, text: "Description Column 1" }, + { x: 36.0, y: 730.884, text: "Lorem ipsum dolor" }, + { x: 36.0, y: 717.012, text: "sit amet" }, + { x: 36.0, y: 703.14, text: "Consectetur" }, + { x: 36.0, y: 689.268, text: "adipiscing elit" }]) + end + + it 'creates a html table with lists inside' do + generator.parse_file('table/list_in_cell.md') + expect_pdf([ + {x:36.0, y:744.756, text:"• test1"}, + {x:36.0, y:730.884, text:"• test2"}, + {x:36.0, y:717.012, text:"  test2cont"}, + {x:36.0, y:703.14, text:"• test3"}, + {x:36.0, y:689.268, text:"  • test3.1"}, + {x:36.0, y:675.396, text:"  • test3.2"}, + {x:36.0, y:661.524, text:"    test3.2cont"}, + {x:36.0, y:647.652, text:"  • test3.3"}, + {x:216.0, y:744.756, text:"[ ] aha"}, + {x:216.0, y:730.884, text:"[x] oho"}, + {x:216.0, y:717.012, text:"[ ] ehe"}, + {x:396.0, y:744.756, text:"1. wooo"}, + {x:396.0, y:730.884, text:"2. waaa"}, + {x:396.0, y:717.012, text:"3. wiiiii"}]) + end end