diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0a51321..ca808404 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
- [#210: Fix issue with WCAG 2.1 success criterion 1.3.1 (Info and Relationships)](https://github.com/alphagov/tech-docs-gem/pull/210)
- [#209: Some search and keyboard navigation updates](https://github.com/alphagov/tech-docs-gem/pull/209)
+- [#214: Implement row level table headings to allow accessible tables with row headings](https://github.com/alphagov/tech-docs-gem/pull/214)
### Ruby version bump
diff --git a/lib/govuk_tech_docs/tech_docs_html_renderer.rb b/lib/govuk_tech_docs/tech_docs_html_renderer.rb
index a5805775..8b191ccc 100644
--- a/lib/govuk_tech_docs/tech_docs_html_renderer.rb
+++ b/lib/govuk_tech_docs/tech_docs_html_renderer.rb
@@ -30,5 +30,49 @@ def table(header, body)
)
end
+
+ def table_row(body)
+ # Post-processing the table_cell HTML to implement row headings.
+ #
+ # Doing this in table_row instead of table_cell is a hack.
+ #
+ # Ideally, we'd use the table_cell callback like:
+ #
+ # def table_cell(content, alignment, header)
+ # if header
+ # "
#{content} | "
+ # elsif content.start_with? "# "
+ # "#{content.sub(/^# /, "")} | "
+ # else
+ # "#{content} | "
+ # end
+ # end
+ #
+ # Sadly, Redcarpet's table_cell callback doesn't allow you to distinguish
+ # table cells and table headings until https://github.com/vmg/redcarpet/commit/27dfb2a738a23aadd286ac9e7ecd61c4545d29de
+ # (which is not yet released). This means we can't use the table_cell callback
+ # without breaking column headers, so we're having to hack it in table_row.
+
+ fragment = Nokogiri::HTML::DocumentFragment.parse(body)
+ fragment.children.each do |cell|
+ next unless cell.name == "td"
+ next if cell.children.empty?
+
+ first_child = cell.children.first
+ next unless first_child.text?
+
+ leading_text = first_child.content
+ next unless leading_text.start_with?("#")
+
+ cell.name = "th"
+ cell["scope"] = "row"
+ first_child.content = leading_text.sub(/# */, "")
+ end
+
+ tr = Nokogiri::XML::Node.new "tr", fragment
+ tr.children = fragment.children
+
+ tr.to_html
+ end
end
end
diff --git a/spec/govuk_tech_docs/tech_docs_html_renderer_spec.rb b/spec/govuk_tech_docs/tech_docs_html_renderer_spec.rb
new file mode 100644
index 00000000..3e856bac
--- /dev/null
+++ b/spec/govuk_tech_docs/tech_docs_html_renderer_spec.rb
@@ -0,0 +1,44 @@
+RSpec.describe GovukTechDocs::TechDocsHTMLRenderer do
+ let(:app) { double("app") }
+ let(:context) { double("context") }
+ let(:processor) {
+ allow(context).to receive(:app) { app }
+ allow(app).to receive(:api)
+ Redcarpet::Markdown.new(described_class.new(context: context), tables: true)
+ }
+
+ describe "#render a table" do
+ markdown_table = <<~MARKDOWN
+ | A | B |
+ |------|---|
+ |# C | D |
+ | E | F |
+ |# *G* | H |
+ MARKDOWN
+
+ it "treats cells in the heading row as headings" do
+ output = processor.render markdown_table
+
+ expect(output).to include("A | ")
+ expect(output).to include("B | ")
+ end
+
+ it "treats cells starting with # as row headings" do
+ output = processor.render markdown_table
+ expect(output).to include('C | ')
+ end
+
+ it "treats cells starting with # with more complex markup as row headings" do
+ output = processor.render markdown_table
+ expect(output).to match(/G<\/em>\s*<\/th>/)
+ end
+
+ it "treats other cells as ordinary cells" do
+ output = processor.render markdown_table
+ expect(output).to include("D | ")
+ expect(output).to include("E | ")
+ expect(output).to include("F | ")
+ expect(output).to include("H | ")
+ end
+ end
+end
|