Skip to content

Commit 61c1e8a

Browse files
authored
Standardize table and heading markup elements (#1389)
I noticed that Markup elements are currently not standardized in any way - even though the code does make some assumptions. For example, there are visitors that depend on all elements implementing `accept`, but no parent class or interface that will help guarantee every element implements it. Some elements are implemented as structs, while other are classes. Some have parent classes for shared functionality and APIs and others don't. I propose that we standardize the markup elements a bit more, so that we can have a common interface for them. It will make it easier for the RDoc to Markdown translator if we can enforce that every markup element has to be able to print itself in Markdown - and I believe the code will be easier to understand too. This PR creates a base `Markup::Element` class and I standardized `Heading` and `Table` as an example. I'm also finding it challenging to understand the logic with no clue about what types are and what is expected from each method, so I also propose we annotate some of these as documentation - even if we're not adopting type checking. If folks agree, I will continue to standardize more markup elements. I'm trying to document what each element represents along the way.
1 parent 513623b commit 61c1e8a

File tree

4 files changed

+166
-119
lines changed

4 files changed

+166
-119
lines changed

lib/rdoc/markup.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ def convert(input, formatter)
210210
autoload :BlankLine, "#{__dir__}/markup/blank_line"
211211
autoload :BlockQuote, "#{__dir__}/markup/block_quote"
212212
autoload :Document, "#{__dir__}/markup/document"
213+
autoload :Element, "#{__dir__}/markup/element"
213214
autoload :HardBreak, "#{__dir__}/markup/hard_break"
214215
autoload :Heading, "#{__dir__}/markup/heading"
215216
autoload :Include, "#{__dir__}/markup/include"

lib/rdoc/markup/element.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
module RDoc
4+
class Markup
5+
# Base class defining the interface for all markup elements found in documentation
6+
# @abstract
7+
class Element
8+
# @abstract
9+
#: (untyped) -> void
10+
def accept(visitor)
11+
raise NotImplementedError, "#{self.class} must implement the accept method"
12+
end
13+
14+
# @abstract
15+
#: (PP) -> void
16+
def pretty_print(q)
17+
raise NotImplementedError, "#{self.class} must implement the pretty_print method"
18+
end
19+
end
20+
end
21+
end

lib/rdoc/markup/heading.rb

Lines changed: 96 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,101 @@
11
# frozen_string_literal: true
2-
##
3-
# A heading with a level (1-6) and text
42

5-
RDoc::Markup::Heading =
6-
Struct.new :level, :text do
7-
8-
@to_html = nil
9-
@to_label = nil
10-
11-
##
12-
# A singleton RDoc::Markup::ToLabel formatter for headings.
13-
14-
def self.to_label
15-
@to_label ||= RDoc::Markup::ToLabel.new
16-
end
17-
18-
##
19-
# A singleton plain HTML formatter for headings. Used for creating labels
20-
# for the Table of Contents
21-
22-
def self.to_html
23-
return @to_html if @to_html
24-
25-
markup = RDoc::Markup.new
26-
markup.add_regexp_handling RDoc::CrossReference::CROSSREF_REGEXP, :CROSSREF
27-
28-
@to_html = RDoc::Markup::ToHtml.new nil
29-
30-
def @to_html.handle_regexp_CROSSREF(target)
31-
target.text.sub(/^\\/, '')
3+
module RDoc
4+
class Markup
5+
# A heading with a level (1-6) and text
6+
#
7+
# RDoc syntax:
8+
# = Heading 1
9+
# == Heading 2
10+
# === Heading 3
11+
#
12+
# Markdown syntax:
13+
# # Heading 1
14+
# ## Heading 2
15+
# ### Heading 3
16+
class Heading < Element
17+
#: String
18+
attr_reader :text
19+
20+
#: Integer
21+
attr_accessor :level
22+
23+
# A singleton RDoc::Markup::ToLabel formatter for headings.
24+
#: () -> RDoc::Markup::ToLabel
25+
def self.to_label
26+
@to_label ||= Markup::ToLabel.new
27+
end
28+
29+
# A singleton plain HTML formatter for headings. Used for creating labels for the Table of Contents
30+
#: () -> RDoc::Markup::ToHtml
31+
def self.to_html
32+
@to_html ||= begin
33+
markup = Markup.new
34+
markup.add_regexp_handling CrossReference::CROSSREF_REGEXP, :CROSSREF
35+
36+
to_html = Markup::ToHtml.new nil
37+
38+
def to_html.handle_regexp_CROSSREF(target)
39+
target.text.sub(/^\\/, '')
40+
end
41+
42+
to_html
43+
end
44+
end
45+
46+
#: (Integer, String) -> void
47+
def initialize(level, text)
48+
super()
49+
50+
@level = level
51+
@text = text
52+
end
53+
54+
#: (Object) -> bool
55+
def ==(other)
56+
other.is_a?(Heading) && other.level == @level && other.text == @text
57+
end
58+
59+
# @override
60+
#: (untyped) -> void
61+
def accept(visitor)
62+
visitor.accept_heading(self)
63+
end
64+
65+
# An HTML-safe anchor reference for this header.
66+
#: () -> String
67+
def aref
68+
"label-#{self.class.to_label.convert text.dup}"
69+
end
70+
71+
# Creates a fully-qualified label which will include the label from +context+. This helps keep ids unique in HTML.
72+
#: (RDoc::Context?) -> String
73+
def label(context = nil)
74+
label = +""
75+
label << "#{context.aref}-" if context&.respond_to?(:aref)
76+
label << aref
77+
label
78+
end
79+
80+
# HTML markup of the text of this label without the surrounding header element.
81+
#: () -> String
82+
def plain_html
83+
no_image_text = text
84+
85+
if matched = no_image_text.match(/rdoc-image:[^:]+:(.*)/)
86+
no_image_text = matched[1]
87+
end
88+
89+
self.class.to_html.to_html(no_image_text)
90+
end
91+
92+
# @override
93+
#: (PP) -> void
94+
def pretty_print(q)
95+
q.group 2, "[head: #{level} ", ']' do
96+
q.pp text
97+
end
98+
end
3299
end
33-
34-
@to_html
35-
end
36-
37-
##
38-
# Calls #accept_heading on +visitor+
39-
40-
def accept(visitor)
41-
visitor.accept_heading self
42-
end
43-
44-
##
45-
# An HTML-safe anchor reference for this header.
46-
47-
def aref
48-
"label-#{self.class.to_label.convert text.dup}"
49100
end
50-
51-
##
52-
# Creates a fully-qualified label which will include the label from
53-
# +context+. This helps keep ids unique in HTML.
54-
55-
def label(context = nil)
56-
label = aref
57-
58-
label = [context.aref, label].compact.join '-' if
59-
context and context.respond_to? :aref
60-
61-
label
62-
end
63-
64-
##
65-
# HTML markup of the text of this label without the surrounding header
66-
# element.
67-
68-
def plain_html
69-
text = self.text.dup
70-
71-
if matched = text.match(/rdoc-image:[^:]+:(.*)/)
72-
text = matched[1]
73-
end
74-
75-
self.class.to_html.to_html(text)
76-
end
77-
78-
def pretty_print(q) # :nodoc:
79-
q.group 2, "[head: #{level} ", ']' do
80-
q.pp text
81-
end
82-
end
83-
84101
end

lib/rdoc/markup/table.rb

Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,60 @@
11
# frozen_string_literal: true
2-
##
3-
# A section of table
42

5-
class RDoc::Markup::Table
6-
# headers of each column
7-
attr_accessor :header
3+
module RDoc
4+
class Markup
5+
# A section of table
6+
class Table < Element
7+
# Headers of each column
8+
#: Array[String]
9+
attr_accessor :header
810

9-
# alignments of each column
10-
attr_accessor :align
11+
# Alignments of each column
12+
#: Array[Symbol?]
13+
attr_accessor :align
1114

12-
# body texts of each column
13-
attr_accessor :body
15+
# Body texts of each column
16+
#: Array[String]
17+
attr_accessor :body
1418

15-
# Creates new instance
16-
def initialize(header, align, body)
17-
@header, @align, @body = header, align, body
18-
end
19-
20-
# :stopdoc:
21-
def ==(other)
22-
self.class == other.class and
23-
@header == other.header and
24-
@align == other.align and
25-
@body == other.body
26-
end
19+
#: (Array[String], Array[Symbol?], Array[String]) -> void
20+
def initialize(header, align, body)
21+
@header, @align, @body = header, align, body
22+
end
2723

28-
def accept(visitor)
29-
visitor.accept_table @header, @body, @align
30-
end
24+
#: (Object) -> bool
25+
def ==(other)
26+
self.class == other.class && @header == other.header &&
27+
@align == other.align && @body == other.body
28+
end
3129

32-
def pretty_print(q)
33-
q.group 2, '[Table: ', ']' do
34-
q.group 2, '[Head: ', ']' do
35-
q.seplist @header.zip(@align) do |text, align|
36-
q.pp text
37-
if align
38-
q.text ":"
39-
q.breakable
40-
q.text align.to_s
41-
end
42-
end
30+
# @override
31+
#: (untyped) -> void
32+
def accept(visitor)
33+
visitor.accept_table(@header, @body, @align)
4334
end
44-
q.breakable
45-
q.group 2, '[Body: ', ']' do
46-
q.seplist @body do |body|
47-
q.group 2, '[', ']' do
48-
q.seplist body do |text|
35+
36+
# @override
37+
#: (untyped) -> String
38+
def pretty_print(q)
39+
q.group 2, '[Table: ', ']' do
40+
q.group 2, '[Head: ', ']' do
41+
q.seplist @header.zip(@align) do |text, align|
4942
q.pp text
43+
if align
44+
q.text ":"
45+
q.breakable
46+
q.text align.to_s
47+
end
48+
end
49+
end
50+
q.breakable
51+
q.group 2, '[Body: ', ']' do
52+
q.seplist @body do |body|
53+
q.group 2, '[', ']' do
54+
q.seplist body do |text|
55+
q.pp text
56+
end
57+
end
5058
end
5159
end
5260
end

0 commit comments

Comments
 (0)