zard-doc 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +17 -10
- data/lib/zard/doc/build.rb +105 -0
- data/lib/zard/doc/build_configuration.rb +127 -0
- data/lib/zard/doc/build_export.rb +100 -0
- data/lib/zard/doc/build_publisher.rb +206 -0
- data/lib/zard/doc/build_renderer.rb +306 -0
- data/lib/zard/doc/cli.rb +100 -2
- data/lib/zard/doc/reference_markup.rb +271 -0
- data/lib/zard/doc/renderer.rb +160 -21
- data/lib/zard/doc/version.rb +1 -1
- data/lib/zard/doc.rb +2 -1
- metadata +10 -4
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zard
|
|
4
|
+
module Doc
|
|
5
|
+
class ReferenceMarkup
|
|
6
|
+
HTML_OR_AUTOLINK = /\A<(?:[A-Za-z][A-Za-z0-9+.-]{1,31}:[^<>\s]*|[^<>\s]+@[^<>\s]+|\/[A-Za-z][A-Za-z0-9-]*\s*|[A-Za-z][A-Za-z0-9-]*(?:\s+[A-Za-z_:][A-Za-z0-9_.:-]*(?:\s*=\s*(?:[^\s"'=<>`]+|'[^']*'|"[^"]*"))?)*\s*\/?|!--.*?--|![A-Z]+(?:\s+[^<>]*)?|\?.*?\?)>/m
|
|
7
|
+
|
|
8
|
+
def self.rewrite(text, &reference)
|
|
9
|
+
new(reference).rewrite(text)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.code_label?(text)
|
|
13
|
+
text.start_with?("`") && code_end(text, 0) == text.length
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.code_end(text, position)
|
|
17
|
+
delimiter = text[position..].match(/\A`+/)[0]
|
|
18
|
+
closing = text.match(/(?<!`)#{Regexp.escape(delimiter)}(?!`)/, position + delimiter.length)
|
|
19
|
+
closing&.end(0)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(reference)
|
|
23
|
+
@reference = reference
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def rewrite(text)
|
|
27
|
+
segments = []
|
|
28
|
+
prose = +""
|
|
29
|
+
fence = nil
|
|
30
|
+
definition = false
|
|
31
|
+
definition_destination = false
|
|
32
|
+
definition_title = nil
|
|
33
|
+
list_indentations = []
|
|
34
|
+
previous_quotes = 0
|
|
35
|
+
@reference_labels = {}
|
|
36
|
+
text.each_line do |line|
|
|
37
|
+
if fence
|
|
38
|
+
content = fence_content(line, fence)
|
|
39
|
+
if content
|
|
40
|
+
segments << [false, line]
|
|
41
|
+
delimiter = fence[:delimiter]
|
|
42
|
+
closing = content.match(/\A {0,3}(`{3,}|~{3,})([^\n]*)/)
|
|
43
|
+
fence = nil if closing && closing[1][0] == delimiter[0] && closing[1].length >= delimiter.length && closing[2].strip.empty?
|
|
44
|
+
next
|
|
45
|
+
end
|
|
46
|
+
fence = nil
|
|
47
|
+
end
|
|
48
|
+
if definition_title
|
|
49
|
+
segments << [false, line]
|
|
50
|
+
definition_title = nil if title_end(line, definition_title) || line.strip.empty?
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
prefix = line[/\A(?: {0,3}(?:> ?|(?:[-+*]|\d+[.)])(?: {1,4}(?! )| (?= {4})|\t)))+/].to_s
|
|
54
|
+
content = line.delete_prefix(prefix)
|
|
55
|
+
quotes = prefix.count(">")
|
|
56
|
+
quote_content = line.sub(/\A(?: {0,3}> ?)+/, "")
|
|
57
|
+
indentation = columns(quote_content[/\A[ \t]*/])
|
|
58
|
+
list_start = prefix.match?(/[-+*]|\d+[.)]/)
|
|
59
|
+
list_indentations.clear if quotes != previous_quotes
|
|
60
|
+
if list_start
|
|
61
|
+
list_indentations.pop while list_indentations.last && list_indentations.last > indentation
|
|
62
|
+
list_indentations << columns(prefix.gsub(/ {0,3}> ?/, ""))
|
|
63
|
+
elsif !line.strip.empty?
|
|
64
|
+
list_indentations.pop while list_indentations.last && list_indentations.last > indentation
|
|
65
|
+
end
|
|
66
|
+
list_indentation = list_indentations.last
|
|
67
|
+
content = dedent(quote_content, list_indentation) if !list_start && list_indentation
|
|
68
|
+
setext = content.match?(/\A {0,3}(?:=+|-+)[ \t]*\r?\n?\z/)
|
|
69
|
+
block_start = setext || list_start || quotes != previous_quotes || content.match?(/\A {0,3}(?:\#{1,6}(?:[ \t]|$)|(?:[-*_][ \t]*){3,}$)/)
|
|
70
|
+
if block_start
|
|
71
|
+
segments << [true, prose.dup]
|
|
72
|
+
prose.clear
|
|
73
|
+
end
|
|
74
|
+
previous_quotes = quotes
|
|
75
|
+
marker = content.match(/\A {0,3}(`{3,}|~{3,})([^\n]*)/)
|
|
76
|
+
code_indentation = list_start ? columns(content[/\A[ \t]*/]) : indentation - (list_indentation || 0)
|
|
77
|
+
indented = code_indentation >= 4 && prose.empty?
|
|
78
|
+
reference_definition = content.match(/\A {0,3}\[([^\n]+)\]:/)
|
|
79
|
+
if (definition && (definition_destination || content.match?(/\A[ \t]+\S/) || content.match?(/\A {0,3}["'(]/))) || reference_definition || line.strip.empty? || indented || (marker && (marker[1][0] != "`" || !marker[2].include?("`")))
|
|
80
|
+
segments << [true, prose.dup]
|
|
81
|
+
prose.clear
|
|
82
|
+
segments << [false, line]
|
|
83
|
+
definition = !!reference_definition || (definition && !line.strip.empty?)
|
|
84
|
+
definition_destination = !!(reference_definition && content[reference_definition.end(0)..].strip.empty?)
|
|
85
|
+
definition_title = open_title(reference_definition ? content[reference_definition.end(0)..] : content) if definition
|
|
86
|
+
@reference_labels[normalize_label(reference_definition[1])] = true if reference_definition
|
|
87
|
+
if marker
|
|
88
|
+
fence = {delimiter: marker[1], quotes: quotes, indentation: list_indentation}
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
definition = false
|
|
92
|
+
definition_destination = false
|
|
93
|
+
prose << line
|
|
94
|
+
end
|
|
95
|
+
if setext || content.match?(/\A {0,3}\#{1,6}(?:[ \t]|$)/)
|
|
96
|
+
segments << [true, prose.dup]
|
|
97
|
+
prose.clear
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
segments << [true, prose]
|
|
101
|
+
segments.map { |rewrite, source| rewrite ? rewrite_inline(source) : source }.join
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def rewrite_inline(text)
|
|
107
|
+
output = +""
|
|
108
|
+
position = 0
|
|
109
|
+
while position < text.length
|
|
110
|
+
if text[position] == "\\"
|
|
111
|
+
output << text[position, 2]
|
|
112
|
+
position += 2
|
|
113
|
+
elsif text[position] == "`"
|
|
114
|
+
ending = self.class.code_end(text, position) || (position + text[position..].match(/\A`+/)[0].length)
|
|
115
|
+
output << text[position...ending]
|
|
116
|
+
position = ending
|
|
117
|
+
elsif text[position] == "!" && text[position + 1] == "[" && (ending = bracket_end(text, position + 1))
|
|
118
|
+
ending = link_end(text, ending) || ending
|
|
119
|
+
output << text[position...ending]
|
|
120
|
+
position = ending
|
|
121
|
+
elsif text[position] == "[" && (ending = bracket_end(text, position))
|
|
122
|
+
body = text[(position + 1)...(ending - 1)]
|
|
123
|
+
if (link_ending = link_end(text, ending))
|
|
124
|
+
destination = text[(ending + 1)...(link_ending - 1)]
|
|
125
|
+
value = (text[ending] == "(") ? destination.match(/\A([mc]):([^\s]+)\z/) : nil
|
|
126
|
+
replacement = value && @reference.call(value[1], decode(value[2]), body)
|
|
127
|
+
output << (replacement || text[position...link_ending])
|
|
128
|
+
position = link_ending
|
|
129
|
+
else
|
|
130
|
+
if @reference_labels.key?(normalize_label(body))
|
|
131
|
+
output << text[position...ending]
|
|
132
|
+
position = ending
|
|
133
|
+
next
|
|
134
|
+
end
|
|
135
|
+
rd = body.start_with?("[") && body.end_with?("]")
|
|
136
|
+
body = body[1...-1].strip if rd
|
|
137
|
+
value = body.match(/\A([mc]):([^\s]+)\z/)
|
|
138
|
+
if value
|
|
139
|
+
target = decode(value[2])
|
|
140
|
+
output << (@reference.call(value[1], target, nil) || text[position...ending])
|
|
141
|
+
position = ending
|
|
142
|
+
elsif rd
|
|
143
|
+
output << text[position...ending]
|
|
144
|
+
position = ending
|
|
145
|
+
else
|
|
146
|
+
output << text[position]
|
|
147
|
+
position += 1
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
elsif text[position, 2] == "[["
|
|
151
|
+
output << "[["
|
|
152
|
+
position += 2
|
|
153
|
+
elsif text[position] == "<" && (html = text[position..].match(HTML_OR_AUTOLINK))
|
|
154
|
+
output << html[0]
|
|
155
|
+
position += html[0].length
|
|
156
|
+
else
|
|
157
|
+
output << text[position]
|
|
158
|
+
position += 1
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
output
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def decode(target)
|
|
165
|
+
target.gsub(/\\([\\\[\]])/, '\1')
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def columns(text)
|
|
169
|
+
text.each_char.reduce(0) { |column, character| (character == "\t") ? column + 4 - column % 4 : column + 1 }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def dedent(text, indentation)
|
|
173
|
+
whitespace = text[/\A[ \t]*/]
|
|
174
|
+
" " * [columns(whitespace) - indentation, 0].max + text.delete_prefix(whitespace)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def open_title(text)
|
|
178
|
+
opening = text.match(/(?:\A|[ \t])(["'(])/)
|
|
179
|
+
return unless opening
|
|
180
|
+
|
|
181
|
+
delimiter = (opening[1] == "(") ? ")" : opening[1]
|
|
182
|
+
delimiter unless title_end(text[opening.end(0)..], delimiter)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def title_end(text, delimiter)
|
|
186
|
+
position = 0
|
|
187
|
+
while position < text.length
|
|
188
|
+
return position + 1 if text[position] == delimiter
|
|
189
|
+
|
|
190
|
+
position += (text[position] == "\\") ? 2 : 1
|
|
191
|
+
end
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def fence_content(line, fence)
|
|
196
|
+
content = line
|
|
197
|
+
fence[:quotes].times do
|
|
198
|
+
quote = content[/\A {0,3}> ?/]
|
|
199
|
+
return unless quote
|
|
200
|
+
|
|
201
|
+
content = content.delete_prefix(quote)
|
|
202
|
+
end
|
|
203
|
+
indentation = fence[:indentation]
|
|
204
|
+
return content unless indentation
|
|
205
|
+
return if !content.strip.empty? && columns(content[/\A[ \t]*/]) < indentation
|
|
206
|
+
|
|
207
|
+
dedent(content, indentation)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def normalize_label(label)
|
|
211
|
+
label.strip.gsub(/\s+/, " ").downcase
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def bracket_end(text, position)
|
|
215
|
+
depth = 0
|
|
216
|
+
while position < text.length
|
|
217
|
+
case text[position]
|
|
218
|
+
when "\\"
|
|
219
|
+
position += 2
|
|
220
|
+
next
|
|
221
|
+
when "`"
|
|
222
|
+
ending = self.class.code_end(text, position) || (position + text[position..].match(/\A`+/)[0].length)
|
|
223
|
+
position = ending
|
|
224
|
+
next
|
|
225
|
+
when "["
|
|
226
|
+
depth += 1
|
|
227
|
+
when "]"
|
|
228
|
+
depth -= 1
|
|
229
|
+
return position + 1 if depth.zero?
|
|
230
|
+
end
|
|
231
|
+
position += 1
|
|
232
|
+
end
|
|
233
|
+
nil
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def link_end(text, position)
|
|
237
|
+
return bracket_end(text, position) if text[position] == "["
|
|
238
|
+
return unless text[position] == "("
|
|
239
|
+
|
|
240
|
+
depth = 0
|
|
241
|
+
quote = nil
|
|
242
|
+
angle = false
|
|
243
|
+
while position < text.length
|
|
244
|
+
if text[position] == "\\"
|
|
245
|
+
position += 2
|
|
246
|
+
next
|
|
247
|
+
end
|
|
248
|
+
if quote
|
|
249
|
+
quote = nil if text[position] == quote
|
|
250
|
+
elsif angle
|
|
251
|
+
angle = false if text[position] == ">"
|
|
252
|
+
elsif ["\"", "'"].include?(text[position]) && text[position - 1].match?(/\s/)
|
|
253
|
+
quote = text[position]
|
|
254
|
+
elsif text[position] == "<"
|
|
255
|
+
angle = true
|
|
256
|
+
else
|
|
257
|
+
case text[position]
|
|
258
|
+
when "("
|
|
259
|
+
depth += 1
|
|
260
|
+
when ")"
|
|
261
|
+
depth -= 1
|
|
262
|
+
return position + 1 if depth.zero?
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
position += 1
|
|
266
|
+
end
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
data/lib/zard/doc/renderer.rb
CHANGED
|
@@ -4,77 +4,107 @@ module Zard
|
|
|
4
4
|
module Doc
|
|
5
5
|
class Renderer
|
|
6
6
|
def self.render(document)
|
|
7
|
-
new(document).render
|
|
7
|
+
new([document]).render
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def
|
|
11
|
-
|
|
10
|
+
def self.render_documents(documents)
|
|
11
|
+
new(documents).render
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
def initialize(documents)
|
|
15
|
+
@documents = documents
|
|
16
|
+
@container_names = documents.flat_map(&:declarations).select { |declaration| %i[class module constant].include?(declaration.kind) }.map { |declaration| qualified_name(declaration) }
|
|
17
|
+
@targets = Hash.new { |hash, key| hash[key] = [] }
|
|
18
|
+
documents.flat_map(&:declarations).each do |declaration|
|
|
19
|
+
reference_names(declaration).each { |name| @targets[name] << declaration }
|
|
20
|
+
end
|
|
21
|
+
@anchors = {}
|
|
22
|
+
@descriptions = {}
|
|
23
|
+
@text = {}
|
|
24
|
+
documents.flat_map(&:declarations).each do |declaration|
|
|
25
|
+
next unless renderable?(declaration)
|
|
26
|
+
|
|
27
|
+
text = declaration.documentation.select { |tag| tag.name == :text }.map(&:description).join("\n")
|
|
28
|
+
@text[declaration] = render_prose(text, declaration)
|
|
29
|
+
@descriptions[declaration] = declaration.documentation.to_h do |tag|
|
|
30
|
+
description = case tag.name
|
|
31
|
+
when :example, :raw, :text
|
|
32
|
+
tag.description
|
|
33
|
+
when :see
|
|
34
|
+
render_reference(tag.description, declaration)
|
|
35
|
+
else
|
|
36
|
+
render_prose(tag.description, declaration)
|
|
37
|
+
end
|
|
38
|
+
[tag, description]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
17
42
|
|
|
18
|
-
|
|
43
|
+
def render
|
|
44
|
+
@documents.filter_map do |document|
|
|
45
|
+
sections = document.declarations.filter_map { |declaration| render_declaration(declaration) }
|
|
46
|
+
"#{sections.join("\n\n")}\n" unless sections.empty?
|
|
47
|
+
end.join("\n")
|
|
19
48
|
end
|
|
20
49
|
|
|
21
50
|
private
|
|
22
51
|
|
|
23
52
|
def render_declaration(declaration)
|
|
24
|
-
return unless declaration
|
|
53
|
+
return unless renderable?(declaration)
|
|
25
54
|
|
|
26
55
|
documentation = declaration.documentation.reject { |tag| tag.name == :raw }
|
|
27
|
-
|
|
56
|
+
description = ->(tag) { @descriptions.fetch(declaration).fetch(tag) }
|
|
28
57
|
|
|
29
58
|
parts = [heading(declaration)]
|
|
59
|
+
parts.unshift("<a id=\"#{@anchors[declaration]}\"></a>") if @anchors.key?(declaration)
|
|
30
60
|
parts << "Superclass: `#{declaration.superclass}`." if declaration.superclass
|
|
31
61
|
parts << render_container_builder(declaration) if declaration.container_builder
|
|
32
62
|
append_mixins(parts, declaration)
|
|
33
63
|
parts << "Alias of `#{display_alias_target(declaration)}`." if declaration.alias_target
|
|
34
|
-
text =
|
|
35
|
-
parts << text
|
|
64
|
+
text = @text.fetch(declaration)
|
|
65
|
+
parts << text unless text.empty?
|
|
36
66
|
|
|
37
67
|
deprecated = documentation.select { |tag| tag.name == :deprecated }
|
|
38
|
-
parts << "### Deprecated\n\n#{deprecated.map(
|
|
68
|
+
parts << "### Deprecated\n\n#{deprecated.map(&description).join("\n\n")}" unless deprecated.empty?
|
|
39
69
|
|
|
40
70
|
notes = documentation.select { |tag| tag.name == :note }
|
|
41
|
-
parts << "### Notes\n\n#{notes.map(
|
|
71
|
+
parts << "### Notes\n\n#{notes.map(&description).join("\n\n")}" unless notes.empty?
|
|
42
72
|
|
|
43
73
|
examples = documentation.select { |tag| tag.name == :example }
|
|
44
|
-
parts << "### Examples\n\n#{examples.map(
|
|
74
|
+
parts << "### Examples\n\n#{examples.map(&description).join("\n\n")}" unless examples.empty?
|
|
45
75
|
|
|
46
76
|
parameters = documentation.select { |tag| tag.name == :param }
|
|
47
77
|
unless parameters.empty?
|
|
48
|
-
items = parameters.map { |tag| list_item("`#{tag.subject}` — ", tag
|
|
78
|
+
items = parameters.map { |tag| list_item("`#{tag.subject}` — ", description.call(tag)) }
|
|
49
79
|
parts << "### Parameters\n\n#{items.join("\n")}"
|
|
50
80
|
end
|
|
51
81
|
|
|
52
82
|
returns = documentation.select { |tag| tag.name == :return }
|
|
53
|
-
parts << "### Returns\n\n#{returns.map(
|
|
83
|
+
parts << "### Returns\n\n#{returns.map(&description).join("\n\n")}" unless returns.empty?
|
|
54
84
|
|
|
55
85
|
documentation.select { |tag| tag.name == :option }.group_by(&:owner).each do |owner, options|
|
|
56
|
-
items = options.map { |tag| list_item("`#{tag.subject}` — ", tag
|
|
86
|
+
items = options.map { |tag| list_item("`#{tag.subject}` — ", description.call(tag)) }
|
|
57
87
|
parts << "### Options for `#{owner}`\n\n#{items.join("\n")}"
|
|
58
88
|
end
|
|
59
89
|
|
|
60
90
|
raises = documentation.select { |tag| tag.name == :raise }
|
|
61
91
|
unless raises.empty?
|
|
62
|
-
items = raises.map { |tag| list_item("`#{tag.subject}` — ", tag
|
|
92
|
+
items = raises.map { |tag| list_item("`#{tag.subject}` — ", description.call(tag)) }
|
|
63
93
|
parts << "### Raises\n\n#{items.join("\n")}"
|
|
64
94
|
end
|
|
65
95
|
|
|
66
96
|
yield_parameters = documentation.select { |tag| tag.name == :yieldparam }
|
|
67
97
|
unless yield_parameters.empty?
|
|
68
|
-
items = yield_parameters.map { |tag| list_item("`#{tag.subject}` — ", tag
|
|
98
|
+
items = yield_parameters.map { |tag| list_item("`#{tag.subject}` — ", description.call(tag)) }
|
|
69
99
|
parts << "### Yield parameters\n\n#{items.join("\n")}"
|
|
70
100
|
end
|
|
71
101
|
|
|
72
102
|
yield_returns = documentation.select { |tag| tag.name == :yieldreturn }
|
|
73
|
-
parts << "### Yields\n\n#{yield_returns.map(
|
|
103
|
+
parts << "### Yields\n\n#{yield_returns.map(&description).join("\n\n")}" unless yield_returns.empty?
|
|
74
104
|
|
|
75
105
|
see_also = documentation.select { |tag| tag.name == :see }
|
|
76
106
|
unless see_also.empty?
|
|
77
|
-
items = see_also.map { |tag| list_item("", tag
|
|
107
|
+
items = see_also.map { |tag| list_item("", description.call(tag)) }
|
|
78
108
|
parts << "### See also\n\n#{items.join("\n")}"
|
|
79
109
|
end
|
|
80
110
|
parts.join("\n\n")
|
|
@@ -133,6 +163,115 @@ module Zard
|
|
|
133
163
|
[declaration.namespace, declaration.name].compact.join("::")
|
|
134
164
|
end
|
|
135
165
|
|
|
166
|
+
def renderable?(declaration)
|
|
167
|
+
declaration.visibility == :public && declaration.documentation.any? { |tag| tag.name != :raw }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def reference_names(declaration)
|
|
171
|
+
return [] if declaration.refinement || declaration.kind == :refinement
|
|
172
|
+
return [qualified_name(declaration)] if %i[class module constant].include?(declaration.kind)
|
|
173
|
+
|
|
174
|
+
singleton = declaration.kind.to_s.start_with?("singleton_")
|
|
175
|
+
owner = singleton ? reference_owner(declaration) : declaration.namespace
|
|
176
|
+
return [] if singleton && !owner && declaration.namespace
|
|
177
|
+
return [] if owner && !owner.match?(/\A(?:::)?[[:upper:]\p{Lt}][[:word:]]*(?:::[[:upper:]\p{Lt}][[:word:]]*)*\z/)
|
|
178
|
+
|
|
179
|
+
name = "#{owner&.delete_prefix("::")}#{singleton ? "." : "#"}#{declaration.name}"
|
|
180
|
+
name += "=" if declaration.kind.to_s.end_with?("attribute_writer")
|
|
181
|
+
declaration.kind.to_s.end_with?("attribute_accessor") ? [name, "#{name}="] : [name]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def resolve_reference(reference, declaration)
|
|
185
|
+
return unless reference && !declaration.refinement
|
|
186
|
+
|
|
187
|
+
path = reference.sub(/\A((?:::)?[[:upper:]\p{Lt}][[:word:]]*(?:::[[:upper:]\p{Lt}][[:word:]]*)*)?(?:\?\.|\.#)/) { "#{$1}." }
|
|
188
|
+
if path.start_with?("::")
|
|
189
|
+
candidates = [path.delete_prefix("::")]
|
|
190
|
+
elsif path.start_with?("#", ".")
|
|
191
|
+
owner = declaration.kind.to_s.start_with?("singleton_") ? reference_owner(declaration) : declaration.namespace
|
|
192
|
+
owner = qualified_name(declaration) if %i[class module].include?(declaration.kind)
|
|
193
|
+
candidates = ["#{owner&.delete_prefix("::")}#{path}"]
|
|
194
|
+
else
|
|
195
|
+
namespace = declaration.namespace
|
|
196
|
+
namespace = qualified_name(declaration) if %i[class module].include?(declaration.kind)
|
|
197
|
+
candidates = lexical_candidates(path, namespace)
|
|
198
|
+
end
|
|
199
|
+
candidates.each do |candidate|
|
|
200
|
+
targets = @targets[candidate].uniq
|
|
201
|
+
if targets.empty?
|
|
202
|
+
first_constant = path.split(/::|[.#]/).first.to_s
|
|
203
|
+
local_constant = "#{candidate.delete_suffix(path)}#{first_constant}"
|
|
204
|
+
return nil if !first_constant.empty? && @container_names.include?(local_constant)
|
|
205
|
+
|
|
206
|
+
next
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
return targets.first if targets.length == 1 && renderable?(targets.first)
|
|
210
|
+
|
|
211
|
+
return nil
|
|
212
|
+
end
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def lexical_candidates(path, namespace)
|
|
217
|
+
parts = namespace.to_s.split("::")
|
|
218
|
+
parts.length.downto(0).map { |length| [*parts.first(length), path].join("::") }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def reference_owner(declaration)
|
|
222
|
+
owner = singleton_owner(declaration)
|
|
223
|
+
return owner unless owner && declaration.receiver && declaration.receiver != "self"
|
|
224
|
+
return owner.delete_prefix("::") if owner.start_with?("::")
|
|
225
|
+
return owner unless declaration.namespace
|
|
226
|
+
|
|
227
|
+
lexical_candidates(owner, declaration.namespace).find { |candidate| @container_names.include?(candidate) }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def anchor(declaration)
|
|
231
|
+
"zard-#{reference_names(declaration).first.bytes.map { |byte| byte.to_s(16).rjust(2, "0") }.join}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def render_reference(description, declaration)
|
|
235
|
+
explicit = false
|
|
236
|
+
prose = render_prose(description, declaration) { explicit = true }
|
|
237
|
+
return prose if explicit
|
|
238
|
+
|
|
239
|
+
reference, label = description.strip.split(/\s+/, 2)
|
|
240
|
+
return description unless reference
|
|
241
|
+
|
|
242
|
+
destination = if reference.match?(/\A(?:https?:\/\/|mailto:)/)
|
|
243
|
+
reference.gsub(/[\\()< >]/) { |character| "%%%02X" % character.ord }.gsub("&", "&")
|
|
244
|
+
elsif (target = resolve_reference(reference, declaration))
|
|
245
|
+
"##{@anchors[target] ||= anchor(target)}"
|
|
246
|
+
end
|
|
247
|
+
return description unless destination
|
|
248
|
+
|
|
249
|
+
title = escape_label(label || reference)
|
|
250
|
+
"[#{title}](#{destination})"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def render_prose(description, declaration)
|
|
254
|
+
ReferenceMarkup.rewrite(description) do |scheme, reference, label|
|
|
255
|
+
yield if block_given?
|
|
256
|
+
target = resolve_reference(reference, declaration)
|
|
257
|
+
next unless target
|
|
258
|
+
container = %i[class module].include?(target.kind)
|
|
259
|
+
next if (scheme == "c") != container
|
|
260
|
+
|
|
261
|
+
destination = @anchors[target] ||= anchor(target)
|
|
262
|
+
title = label || reference
|
|
263
|
+
unless label && ReferenceMarkup.code_label?(label)
|
|
264
|
+
title = title.gsub(/\\([\\\[\]])/, '\1') if label
|
|
265
|
+
title = escape_label(title)
|
|
266
|
+
end
|
|
267
|
+
"[#{title}](##{destination})"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def escape_label(label)
|
|
272
|
+
label.gsub(/[&<>]/, "&" => "&", "<" => "<", ">" => ">").gsub(/[\\\[\]`*_~]/) { |character| "\\#{character}" }
|
|
273
|
+
end
|
|
274
|
+
|
|
136
275
|
def display_name(declaration)
|
|
137
276
|
if declaration.refinement
|
|
138
277
|
separator = declaration.kind.to_s.start_with?("singleton_") ? "." : "#"
|
data/lib/zard/doc/version.rb
CHANGED
data/lib/zard/doc.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "zard"
|
|
4
4
|
require_relative "doc/version"
|
|
5
|
+
require_relative "doc/reference_markup"
|
|
5
6
|
require_relative "doc/renderer"
|
|
6
7
|
|
|
7
8
|
module Zard
|
|
@@ -10,6 +11,6 @@ module Zard
|
|
|
10
11
|
Renderer.render(document)
|
|
11
12
|
end
|
|
12
13
|
|
|
13
|
-
private_constant :Renderer
|
|
14
|
+
private_constant :Renderer, :ReferenceMarkup
|
|
14
15
|
end
|
|
15
16
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zard-doc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- USAMI Kenta
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.2
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.2
|
|
26
26
|
description: zard-doc renders API documentation from the versioned ZARD document model.
|
|
27
27
|
email:
|
|
28
28
|
- tadsan@zonu.me
|
|
@@ -35,7 +35,13 @@ files:
|
|
|
35
35
|
- README.md
|
|
36
36
|
- exe/zard-doc
|
|
37
37
|
- lib/zard/doc.rb
|
|
38
|
+
- lib/zard/doc/build.rb
|
|
39
|
+
- lib/zard/doc/build_configuration.rb
|
|
40
|
+
- lib/zard/doc/build_export.rb
|
|
41
|
+
- lib/zard/doc/build_publisher.rb
|
|
42
|
+
- lib/zard/doc/build_renderer.rb
|
|
38
43
|
- lib/zard/doc/cli.rb
|
|
44
|
+
- lib/zard/doc/reference_markup.rb
|
|
39
45
|
- lib/zard/doc/renderer.rb
|
|
40
46
|
- lib/zard/doc/version.rb
|
|
41
47
|
homepage: https://github.com/rigortype/zard
|
|
@@ -60,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
60
66
|
- !ruby/object:Gem::Version
|
|
61
67
|
version: '0'
|
|
62
68
|
requirements: []
|
|
63
|
-
rubygems_version: 4.0.
|
|
69
|
+
rubygems_version: 4.0.16
|
|
64
70
|
specification_version: 4
|
|
65
71
|
summary: Markdown API documentation for ZARD
|
|
66
72
|
test_files: []
|