Skip to content

Commit b2788de

Browse files
authored
Support GitHub style markdown heading anchor and link reference (#1540)
I started the work to address #1537, but I think we should just go with the GH markdown style anchor/link generation in the future. Anchors Generated: | Context | Before | After | |---------|--------|-------| | Heading `== Hello` (standalone) | `id="label-Hello"` | `id="hello"` | | Heading `== Hello` (in class Foo) | `id="class-Foo-label-Hello"` | `id="class-foo-hello"` | | Heading `== Hello` (in method #bar) | `id="method-i-bar-label-Hello"` | `id="method-i-bar-hello"` | | Class/Module anchor | `id="class-Foo::Bar"` | `id="class-foo-bar"` | **Legacy anchors preserved as hidden `<span class="legacy-anchor">` elements. So links targeting old anchors should still work.** Links Generated: | Syntax | Before | After | |--------|--------|-------| | `rdoc-ref:@foo` | `href="#label-foo"` | `href="#foo"` | | `rdoc-ref:Class@foo` | `href="#class-Class-label-foo"` | `href="#class-class-foo"` | | `rdoc-ref:Class#method@foo` | `href="#method-i-method-label-foo"` | `href="#method-i-method-foo"` | | `rdoc-ref:Class@Section Title` | `href="#Section Title"` | `href="#section-title"` | These changes now enable Markdown links to work with GitHub-style anchors. Markdown `[link](#foo)` passes through unchanged as `href="#foo"`. - Before: Anchor was `id="label-Foo"` -> link broken - After: Anchor is `id="foo"` -> link works Supportive Changes: - ToC sidebar links updated to use new anchor format Closes #1537
1 parent f58333e commit b2788de

24 files changed

+364
-81
lines changed

ExampleMarkdown.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ For the following styles, see ExampleRDoc.rdoc for style examples:
1414

1515
These items all use the same styles as RDoc format files.
1616

17+
## Anchor Links
18+
19+
RDoc supports GitHub-style anchor links. You can link to any heading using its
20+
anchor, which is the heading text converted to lowercase with spaces replaced
21+
by hyphens and special characters removed.
22+
23+
For example:
24+
25+
* [Link to Footnotes](#footnotes)
26+
* [Link to Blockquotes](#blockquotes)
27+
* [Link to Anchor Links](#anchor-links)
28+
1729
## Footnotes
1830

1931
Footnotes are rendered at the bottom of the documentation section[^1]. For
@@ -36,4 +48,3 @@ Here is how a blockquote looks.
3648
> > 75 years?
3749
3850
This text is from [Riker Ipsum](http://rikeripsum.com)
39-

ExampleRDoc.rdoc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
This document contains example output to show RDoc styling. This file was
44
created from a RDoc Markup file.
55

6+
== Anchor Links
7+
8+
RDoc generates GitHub-style anchors for headings. The anchor is the heading
9+
text converted to lowercase with spaces replaced by hyphens and special
10+
characters removed.
11+
12+
You can link to headings using Markdown-style syntax:
13+
14+
- {Link to Headings}[#headings]
15+
- {Link to Paragraphs}[#paragraphs]
16+
- {Link to Verbatim sections}[#verbatim-sections]
17+
618
== Headings
719

820
You should not use headings beyond level 3, it is a sign of poor organization

doc/rdoc/markup_reference.rb

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -957,22 +957,30 @@
957957
#
958958
# [Heading]
959959
#
960-
# - Link: <tt>RDoc::RD@LICENSE</tt> links to RDoc::RDoc::RD@LICENSE.
960+
# Headings generate GitHub-style anchors: lowercase, spaces as hyphens,
961+
# special characters removed. For example, <tt>== Hello World</tt> generates
962+
# anchor <tt>hello-world</tt>.
961963
#
962-
# Note that spaces in the actual heading are represented by <tt>+</tt> characters
963-
# in the linkable text.
964+
# Link to headings are recommended to use the GitHub-style anchor format:
964965
#
965-
# - Link: <tt>RDoc::Options@Saved+Options</tt>
966-
# links to RDoc::Options@Saved+Options.
966+
# - <tt>RDoc::Options@saved-options</tt> links to RDoc::Options@saved-options.
967+
# - <tt>RDoc::RD@license</tt> links to RDoc::RD@license.
967968
#
968-
# Punctuation and other special characters must be escaped like CGI.escape.
969+
# To link to headings on the same page, you can also use Markdown-style anchor links:
970+
#
971+
# - <tt>{link text}[#hello-world]</tt> links to the heading <tt>== Hello World</tt>.
972+
# - <tt>{link text}[#saved-options]</tt> links to the heading <tt>== Saved Options</tt>.
973+
#
974+
# The legacy format with <tt>+</tt> for spaces is also supported, but not recommended:
975+
#
976+
# - <tt>RDoc::Options@Saved+Options</tt> links to RDoc::Options@Saved+Options.
969977
#
970978
# Pro tip: The link to any heading is available in the alphabetical table of contents
971-
# at the top left of the page for the class or module.
979+
# at the right sidebar of the page.
972980
#
973981
# [Section]
974982
#
975-
# See {Directives for Organizing Documentation}[#class-RDoc::MarkupReference-label-Directives+for+Organizing+Documentation].
983+
# See {Directives for Organizing Documentation}[#class-rdoc-markupreference-directives-for-organizing-documentation].
976984
#
977985
# - Link: <tt>RDoc::Markup::ToHtml@Visitor</tt> links to RDoc::Markup::ToHtml@Visitor.
978986
#

lib/rdoc/code_object/class_module.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,26 @@ def aref_prefix # :nodoc:
188188
end
189189

190190
##
191-
# HTML fragment reference for this module or class. See
192-
# RDoc::NormalClass#aref and RDoc::NormalModule#aref
191+
# HTML fragment reference for this module or class using GitHub-style
192+
# anchor format (lowercase, :: replaced with -).
193+
#
194+
# Examples:
195+
# Foo -> class-foo
196+
# Foo::Bar -> class-foo-bar
193197

194198
def aref
199+
"#{aref_prefix}-#{full_name.downcase.gsub('::', '-')}"
200+
end
201+
202+
##
203+
# Legacy HTML fragment reference for backward compatibility.
204+
# Returns the old RDoc-style anchor format.
205+
#
206+
# Examples:
207+
# Foo -> class-Foo
208+
# Foo::Bar -> class-Foo::Bar
209+
210+
def legacy_aref
195211
"#{aref_prefix}-#{full_name}"
196212
end
197213

lib/rdoc/code_object/context/section.rb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,30 @@ def add_comment(comment)
7070
end
7171

7272
##
73-
# Anchor reference for linking to this section
73+
# Anchor reference for linking to this section using GitHub-style format.
74+
#
75+
# Examples:
76+
# "Section" -> "section"
77+
# "One Two" -> "one-two"
78+
# "[untitled]" -> "untitled"
7479

7580
def aref
7681
title = @title || '[untitled]'
7782

83+
RDoc::Text.to_anchor(title)
84+
end
85+
86+
##
87+
# Legacy anchor reference for backward compatibility.
88+
#
89+
# Examples:
90+
# "Section" -> "section"
91+
# "One Two" -> "one+two"
92+
# "[untitled]" -> "5Buntitled-5D"
93+
94+
def legacy_aref
95+
title = @title || '[untitled]'
96+
7897
CGI.escape(title).gsub('%', '-').sub(/^-/, '')
7998
end
8099

lib/rdoc/generator/template/aliki/class.rhtml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
</ol>
3030
<% end %>
3131

32+
<span id="<%= h klass.legacy_aref %>" class="legacy-anchor"></span>
3233
<h1 id="<%= h klass.aref %>" class="anchor-link <%= klass.type %>">
3334
<%= klass.type %> <%= klass.full_name %>
3435
</h1>
@@ -38,6 +39,7 @@
3839
</section>
3940

4041
<%- klass.each_section do |section, constants, attributes| %>
42+
<span id="<%= section.legacy_aref %>" class="legacy-anchor"></span>
4143
<section id="<%= section.aref %>" class="documentation-section anchor-link">
4244
<%- if section.title then %>
4345
<header class="documentation-section-title">

lib/rdoc/generator/template/aliki/css/rdoc.css

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,26 @@ main .anchor-link:target {
11901190
scroll-margin-top: calc(var(--layout-header-height) + 2rem);
11911191
}
11921192

1193+
/* Legacy anchor for backward compatibility with old label- prefix links */
1194+
.legacy-anchor {
1195+
display: block;
1196+
position: relative;
1197+
visibility: hidden;
1198+
scroll-margin-top: calc(var(--layout-header-height) + 2rem);
1199+
}
1200+
1201+
/* When a legacy anchor is targeted, highlight the next heading sibling */
1202+
.legacy-anchor:target + h1,
1203+
.legacy-anchor:target + h2,
1204+
.legacy-anchor:target + h3,
1205+
.legacy-anchor:target + h4,
1206+
.legacy-anchor:target + h5,
1207+
.legacy-anchor:target + h6 {
1208+
margin-left: calc(-1 * var(--space-5));
1209+
padding-left: calc(var(--space-5) / 2);
1210+
border-left: calc(var(--space-5) / 2) solid var(--color-border-default);
1211+
}
1212+
11931213

11941214
/* Utility Classes */
11951215
.hide { display: none !important; }

lib/rdoc/generator/template/darkfish/class.rhtml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
</ol>
3434
<% end %>
3535

36+
<span id="<%= h klass.legacy_aref %>" class="legacy-anchor"></span>
3637
<h1 id="<%= h klass.aref %>" class="anchor-link <%= klass.type %>">
3738
<%= klass.type %> <%= klass.full_name %>
3839
</h1>
@@ -42,6 +43,7 @@
4243
</section>
4344

4445
<%- klass.each_section do |section, constants, attributes| %>
46+
<span id="<%= section.legacy_aref %>" class="legacy-anchor"></span>
4547
<section id="<%= section.aref %>" class="documentation-section anchor-link">
4648
<%- if section.title then %>
4749
<header class="documentation-section-title">

lib/rdoc/generator/template/darkfish/css/rdoc.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,25 @@ main .anchor-link:target {
9292
scroll-margin-top: 1rem;
9393
}
9494

95+
/* Legacy anchor for backward compatibility with old label- prefix links */
96+
.legacy-anchor {
97+
display: block;
98+
position: relative;
99+
visibility: hidden;
100+
scroll-margin-top: 1rem;
101+
}
102+
103+
/* When a legacy anchor is targeted, highlight the next heading sibling */
104+
.legacy-anchor:target + h1,
105+
.legacy-anchor:target + h2,
106+
.legacy-anchor:target + h3,
107+
.legacy-anchor:target + h4,
108+
.legacy-anchor:target + h5,
109+
.legacy-anchor:target + h6 {
110+
margin-left: -10px;
111+
border-left: 10px solid var(--border-color);
112+
}
113+
95114
/* 4. Links */
96115
a {
97116
color: var(--link-color);

lib/rdoc/markup/heading.rb

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,82 @@ def accept(visitor)
6262
visitor.accept_heading(self)
6363
end
6464

65-
# An HTML-safe anchor reference for this header.
65+
# An HTML-safe anchor reference for this header using GitHub-style formatting:
66+
# - Lowercase
67+
# - Spaces converted to hyphens
68+
# - Special characters removed (except hyphens)
69+
#
70+
# Examples:
71+
# "Hello" -> "hello"
72+
# "Hello World" -> "hello-world"
73+
# "Foo Bar Baz" -> "foo-bar-baz"
74+
#
6675
#: () -> String
6776
def aref
68-
"label-#{self.class.to_label.convert text.dup}"
77+
self.class.to_label.convert text.dup
6978
end
7079

71-
# Creates a fully-qualified label which will include the label from +context+. This helps keep ids unique in HTML.
80+
# An HTML-safe anchor reference using legacy RDoc formatting:
81+
# - Prefixed with "label-"
82+
# - Original case preserved
83+
# - Spaces converted to + (URL encoding style)
84+
# - Special characters percent-encoded
85+
#
86+
# Returns nil if it would be the same as the GitHub-style aref (no alias needed).
87+
#
88+
# Examples:
89+
# "hello" -> "label-hello" (different due to label- prefix)
90+
# "Hello" -> "label-Hello"
91+
# "Hello World" -> "label-Hello+World"
92+
# "Foo Bar Baz" -> "label-Foo+Bar+Baz"
93+
#
94+
#: () -> String?
95+
def legacy_aref
96+
"label-#{self.class.to_label.convert_legacy text.dup}"
97+
end
98+
99+
# Creates a fully-qualified label (GitHub-style) which includes the context's aref prefix.
100+
# This helps keep IDs unique in HTML when headings appear within class/method documentation.
101+
#
102+
# Examples (without context):
103+
# "Hello World" -> "hello-world"
104+
#
105+
# Examples (with context being class Foo):
106+
# "Hello World" -> "class-foo-hello-world"
107+
#
108+
# Examples (with context being method #bar):
109+
# "Hello World" -> "method-i-bar-hello-world"
110+
#
72111
#: (RDoc::Context?) -> String
73112
def label(context = nil)
74-
label = +""
75-
label << "#{context.aref}-" if context&.respond_to?(:aref)
76-
label << aref
77-
label
113+
result = +""
114+
result << "#{context.aref}-" if context&.respond_to?(:aref)
115+
result << aref
116+
result
117+
end
118+
119+
# Creates a fully-qualified legacy label for backward compatibility.
120+
# This is used to generate a secondary ID attribute on the heading's inner anchor,
121+
# allowing old-style links (e.g., #label-Hello+World) to continue working.
122+
#
123+
# Examples (without context):
124+
# "hello" -> "label-hello"
125+
# "Hello World" -> "label-Hello+World"
126+
#
127+
# Examples (with context being class Foo):
128+
# "hello" -> "class-Foo-label-hello"
129+
# "Hello World" -> "class-Foo-label-Hello+World"
130+
#
131+
#: (RDoc::Context?) -> String
132+
def legacy_label(context = nil)
133+
result = +""
134+
if context&.respond_to?(:legacy_aref)
135+
result << "#{context.legacy_aref}-"
136+
elsif context&.respond_to?(:aref)
137+
result << "#{context.aref}-"
138+
end
139+
result << legacy_aref
140+
result
78141
end
79142

80143
# HTML markup of the text of this label without the surrounding header element.

0 commit comments

Comments
 (0)