diff --git a/AGENTS.md b/AGENTS.md index c984184190..ddfdc9f664 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,37 @@ npx stylelint "lib/rdoc/generator/template/aliki/css/rdoc.css" - Style and formatting checks - Many issues auto-fixable with `--fix` +### Type annotations + +Annotate method types using [Sorbet flavored RBS](https://sorbet.org/docs/rbs-support) in inline comments. +For more information about RBS syntax, see the [documentation](https://github.com/ruby/rbs/blob/master/docs/syntax.md). + +A few examples: + +```ruby +# Method that receives an integer and doesn't return anything +#: (Integer) -> void +def foo(something); end + +# Method that receives a string and returns an integer +#: (String) -> Integer +def bar(something) + 123 +end + +# Method that doesn't accept arguments and returns a hash of symbol to string +#: () -> Hash[Symbol, String] +def bar + { key: "value" } +end + +# Method that accepts a block, which yields a single integer argument and returns whatever the block returns +#: [T] () { (Integer) -> T } -> T +def bar + yield(5) +end +``` + ### Documentation Generation ```bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cff518bde..9911267cc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,6 +86,15 @@ npm run lint:css npm run lint:css -- --fix ``` +## Type annotations + +RDoc is currently not a typed codebase. Despite not running a type checker, contributors have been +adding some comment annotations to make the codebase easier to navigate and understand. + +These annotations use [Sorbet flavored RBS](https://sorbet.org/docs/rbs-support) annotations, +so that we can tag definitions as abstract and override. For more information on RBS syntax, +see the [documentation](https://github.com/ruby/rbs/blob/master/docs/syntax.md). + ## Parser Generation RDoc uses generated parsers for Markdown and RD formats. diff --git a/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml b/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml index 2cb46ed517..94004ae7e1 100644 --- a/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml +++ b/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml @@ -35,7 +35,7 @@ <%= h f.page_name %> - <%- next %> + <%- next -%> <%- end %>
  • diff --git a/lib/rdoc/generator/template/darkfish/_sidebar_pages.rhtml b/lib/rdoc/generator/template/darkfish/_sidebar_pages.rhtml index d6e0650abc..1d93300124 100644 --- a/lib/rdoc/generator/template/darkfish/_sidebar_pages.rhtml +++ b/lib/rdoc/generator/template/darkfish/_sidebar_pages.rhtml @@ -13,7 +13,7 @@ <%- f = files.shift %> <%- if files.empty? %>
  • <%= h f.page_name %>
  • - <%- next %> + <%- next -%> <%- end %>
  • ><% if n == f.page_name diff --git a/lib/rdoc/markup.rb b/lib/rdoc/markup.rb index fc9f4a1b2a..8307874461 100644 --- a/lib/rdoc/markup.rb +++ b/lib/rdoc/markup.rb @@ -210,6 +210,7 @@ def convert(input, formatter) autoload :BlankLine, "#{__dir__}/markup/blank_line" autoload :BlockQuote, "#{__dir__}/markup/block_quote" autoload :Document, "#{__dir__}/markup/document" + autoload :Element, "#{__dir__}/markup/element" autoload :HardBreak, "#{__dir__}/markup/hard_break" autoload :Heading, "#{__dir__}/markup/heading" autoload :Include, "#{__dir__}/markup/include" diff --git a/lib/rdoc/markup/blank_line.rb b/lib/rdoc/markup/blank_line.rb index c6505ef95c..d790ebdb55 100644 --- a/lib/rdoc/markup/blank_line.rb +++ b/lib/rdoc/markup/blank_line.rb @@ -1,27 +1,29 @@ # frozen_string_literal: true -## -# An empty line. This class is a singleton. -class RDoc::Markup::BlankLine - - @instance = new - - ## - # RDoc::Markup::BlankLine is a singleton - - def self.new - @instance +module RDoc + class Markup + # An empty line + class BlankLine < Element + @instance = new + + # RDoc::Markup::BlankLine is a singleton + #: () -> BlankLine + def self.new + @instance + end + + # Calls #accept_blank_line on +visitor+ + # @override + #: (untyped) -> void + def accept(visitor) + visitor.accept_blank_line(self) + end + + # @override + #: (PP) -> void + def pretty_print(q) # :nodoc: + q.text("blankline") + end + end end - - ## - # Calls #accept_blank_line on +visitor+ - - def accept(visitor) - visitor.accept_blank_line self - end - - def pretty_print(q) # :nodoc: - q.text 'blankline' - end - end diff --git a/lib/rdoc/markup/element.rb b/lib/rdoc/markup/element.rb new file mode 100644 index 0000000000..996cc6b1a1 --- /dev/null +++ b/lib/rdoc/markup/element.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module RDoc + class Markup + # Base class defining the interface for all markup elements found in documentation + # @abstract + class Element + # @abstract + #: (untyped) -> void + def accept(visitor) + raise NotImplementedError, "#{self.class} must implement the accept method" + end + + # @abstract + #: (PP) -> void + def pretty_print(q) + raise NotImplementedError, "#{self.class} must implement the pretty_print method" + end + end + end +end diff --git a/lib/rdoc/markup/hard_break.rb b/lib/rdoc/markup/hard_break.rb index e1a4270b94..62ee63c8fd 100644 --- a/lib/rdoc/markup/hard_break.rb +++ b/lib/rdoc/markup/hard_break.rb @@ -1,31 +1,34 @@ # frozen_string_literal: true -## -# A hard-break in the middle of a paragraph. -class RDoc::Markup::HardBreak - - @instance = new - - ## - # RDoc::Markup::HardBreak is a singleton - - def self.new - @instance - end - - ## - # Calls #accept_hard_break on +visitor+ - - def accept(visitor) - visitor.accept_hard_break self +module RDoc + class Markup + # A hard-break in the middle of a paragraph. + class HardBreak < Element + @instance = new + + # RDoc::Markup::HardBreak is a singleton + #: () -> HardBreak + def self.new + @instance + end + + # Calls #accept_hard_break on +visitor+ + # @override + #: (untyped) -> void + def accept(visitor) + visitor.accept_hard_break(self) + end + + #: (top) -> bool + def ==(other) # :nodoc: + self.class === other + end + + # @override + #: (PP) -> void + def pretty_print(q) # :nodoc: + q.text("[break]") + end + end end - - def ==(other) # :nodoc: - self.class === other - end - - def pretty_print(q) # :nodoc: - q.text "[break]" - end - end diff --git a/lib/rdoc/markup/heading.rb b/lib/rdoc/markup/heading.rb index d4994845c8..fdbe8f1d1e 100644 --- a/lib/rdoc/markup/heading.rb +++ b/lib/rdoc/markup/heading.rb @@ -1,84 +1,101 @@ # frozen_string_literal: true -## -# A heading with a level (1-6) and text -RDoc::Markup::Heading = - Struct.new :level, :text do - - @to_html = nil - @to_label = nil - - ## - # A singleton RDoc::Markup::ToLabel formatter for headings. - - def self.to_label - @to_label ||= RDoc::Markup::ToLabel.new - end - - ## - # A singleton plain HTML formatter for headings. Used for creating labels - # for the Table of Contents - - def self.to_html - return @to_html if @to_html - - markup = RDoc::Markup.new - markup.add_regexp_handling RDoc::CrossReference::CROSSREF_REGEXP, :CROSSREF - - @to_html = RDoc::Markup::ToHtml.new nil - - def @to_html.handle_regexp_CROSSREF(target) - target.text.sub(/^\\/, '') +module RDoc + class Markup + # A heading with a level (1-6) and text + # + # RDoc syntax: + # = Heading 1 + # == Heading 2 + # === Heading 3 + # + # Markdown syntax: + # # Heading 1 + # ## Heading 2 + # ### Heading 3 + class Heading < Element + #: String + attr_reader :text + + #: Integer + attr_accessor :level + + # A singleton RDoc::Markup::ToLabel formatter for headings. + #: () -> RDoc::Markup::ToLabel + def self.to_label + @to_label ||= Markup::ToLabel.new + end + + # A singleton plain HTML formatter for headings. Used for creating labels for the Table of Contents + #: () -> RDoc::Markup::ToHtml + def self.to_html + @to_html ||= begin + markup = Markup.new + markup.add_regexp_handling CrossReference::CROSSREF_REGEXP, :CROSSREF + + to_html = Markup::ToHtml.new nil + + def to_html.handle_regexp_CROSSREF(target) + target.text.sub(/^\\/, '') + end + + to_html + end + end + + #: (Integer, String) -> void + def initialize(level, text) + super() + + @level = level + @text = text + end + + #: (Object) -> bool + def ==(other) + other.is_a?(Heading) && other.level == @level && other.text == @text + end + + # @override + #: (untyped) -> void + def accept(visitor) + visitor.accept_heading(self) + end + + # An HTML-safe anchor reference for this header. + #: () -> String + def aref + "label-#{self.class.to_label.convert text.dup}" + end + + # Creates a fully-qualified label which will include the label from +context+. This helps keep ids unique in HTML. + #: (RDoc::Context?) -> String + def label(context = nil) + label = +"" + label << "#{context.aref}-" if context&.respond_to?(:aref) + label << aref + label + end + + # HTML markup of the text of this label without the surrounding header element. + #: () -> String + def plain_html + no_image_text = text + + if matched = no_image_text.match(/rdoc-image:[^:]+:(.*)/) + no_image_text = matched[1] + end + + self.class.to_html.to_html(no_image_text) + end + + # @override + #: (PP) -> void + def pretty_print(q) + q.group 2, "[head: #{level} ", ']' do + q.pp text + end + end end - - @to_html - end - - ## - # Calls #accept_heading on +visitor+ - - def accept(visitor) - visitor.accept_heading self - end - - ## - # An HTML-safe anchor reference for this header. - - def aref - "label-#{self.class.to_label.convert text.dup}" end - - ## - # Creates a fully-qualified label which will include the label from - # +context+. This helps keep ids unique in HTML. - - def label(context = nil) - label = aref - - label = [context.aref, label].compact.join '-' if - context and context.respond_to? :aref - - label - end - - ## - # HTML markup of the text of this label without the surrounding header - # element. - - def plain_html - text = self.text.dup - - if matched = text.match(/rdoc-image:[^:]+:(.*)/) - text = matched[1] - end - - self.class.to_html.to_html(text) - end - - def pretty_print(q) # :nodoc: - q.group 2, "[head: #{level} ", ']' do - q.pp text - end - end - end diff --git a/lib/rdoc/markup/raw.rb b/lib/rdoc/markup/raw.rb index 36e80130ca..441a496ef5 100644 --- a/lib/rdoc/markup/raw.rb +++ b/lib/rdoc/markup/raw.rb @@ -1,69 +1,66 @@ # frozen_string_literal: true -## -# A section of text that is added to the output document as-is -class RDoc::Markup::Raw - - ## - # The component parts of the list - - attr_reader :parts - - ## - # Creates a new Raw containing +parts+ - - def initialize *parts - @parts = [] - @parts.concat parts - end - - ## - # Appends +text+ +module RDoc + class Markup + # A section of text that is added to the output document as-is + class Raw + # The component parts of the list + #: Array[String] + attr_reader :parts + + # Creates a new Raw containing +parts+ + #: (*String) -> void + def initialize(*parts) + @parts = parts + end - def <<(text) - @parts << text - end + # Appends +text+ + #: (String) -> void + def <<(text) + @parts << text + end - def ==(other) # :nodoc: - self.class == other.class and @parts == other.parts - end + #: (top) -> bool + def ==(other) # :nodoc: + self.class == other.class && @parts == other.parts + end - ## - # Calls #accept_raw+ on +visitor+ + # Calls #accept_raw+ on +visitor+ + # @override + #: (untyped) -> void + def accept(visitor) + visitor.accept_raw(self) + end - def accept(visitor) - visitor.accept_raw self - end + # Appends +other+'s parts + #: (Raw) -> void + def merge(other) + @parts.concat(other.parts) + end - ## - # Appends +other+'s parts + # @override + #: (PP) -> void + def pretty_print(q) # :nodoc: + self.class.name =~ /.*::(\w{1,4})/i - def merge(other) - @parts.concat other.parts - end + q.group(2, "[#{$1.downcase}: ", ']') do + q.seplist(@parts) do |part| + q.pp(part) + end + end + end - def pretty_print(q) # :nodoc: - self.class.name =~ /.*::(\w{1,4})/i + # Appends +texts+ onto this Paragraph + #: (*String) -> void + def push(*texts) + self.parts.concat(texts) + end - q.group 2, "[#{$1.downcase}: ", ']' do - q.seplist @parts do |part| - q.pp part + # The raw text + #: () -> String + def text + @parts.join(" ") end end end - - ## - # Appends +texts+ onto this Paragraph - - def push *texts - self.parts.concat texts - end - - ## - # The raw text - - def text - @parts.join ' ' - end - end diff --git a/lib/rdoc/markup/table.rb b/lib/rdoc/markup/table.rb index 9105fe2141..7aaa657901 100644 --- a/lib/rdoc/markup/table.rb +++ b/lib/rdoc/markup/table.rb @@ -1,52 +1,60 @@ # frozen_string_literal: true -## -# A section of table -class RDoc::Markup::Table - # headers of each column - attr_accessor :header +module RDoc + class Markup + # A section of table + class Table < Element + # Headers of each column + #: Array[String] + attr_accessor :header - # alignments of each column - attr_accessor :align + # Alignments of each column + #: Array[Symbol?] + attr_accessor :align - # body texts of each column - attr_accessor :body + # Body texts of each column + #: Array[String] + attr_accessor :body - # Creates new instance - def initialize(header, align, body) - @header, @align, @body = header, align, body - end - - # :stopdoc: - def ==(other) - self.class == other.class and - @header == other.header and - @align == other.align and - @body == other.body - end + #: (Array[String], Array[Symbol?], Array[String]) -> void + def initialize(header, align, body) + @header, @align, @body = header, align, body + end - def accept(visitor) - visitor.accept_table @header, @body, @align - end + #: (Object) -> bool + def ==(other) + self.class == other.class && @header == other.header && + @align == other.align && @body == other.body + end - def pretty_print(q) - q.group 2, '[Table: ', ']' do - q.group 2, '[Head: ', ']' do - q.seplist @header.zip(@align) do |text, align| - q.pp text - if align - q.text ":" - q.breakable - q.text align.to_s - end - end + # @override + #: (untyped) -> void + def accept(visitor) + visitor.accept_table(@header, @body, @align) end - q.breakable - q.group 2, '[Body: ', ']' do - q.seplist @body do |body| - q.group 2, '[', ']' do - q.seplist body do |text| + + # @override + #: (untyped) -> String + def pretty_print(q) + q.group 2, '[Table: ', ']' do + q.group 2, '[Head: ', ']' do + q.seplist @header.zip(@align) do |text, align| q.pp text + if align + q.text ":" + q.breakable + q.text align.to_s + end + end + end + q.breakable + q.group 2, '[Body: ', ']' do + q.seplist @body do |body| + q.group 2, '[', ']' do + q.seplist body do |text| + q.pp text + end + end end end end