Skip to content
This repository was archived by the owner on Sep 15, 2021. It is now read-only.

Commit b14ff10

Browse files
authored
Implement generating overview page. (#30)
This change adds a new feature to generate an overview page for all of the rule sets in the generated documentation. This is particularly useful for HTML documentation and allows users to directly drop the generated HTML docs onto a webserver and serve the docs as a standalone website. This change also adds a link_ext attribute, which allows for customizing the file extension used for links in the generated documentation since some webservers serve Markdown pages with the .md file extension rather than .html. Fixes #24
1 parent 71d74c1 commit b14ff10

19 files changed

+441
-96
lines changed

skydoc/build.proto

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ message RuleDefinition {
9494

9595
// The list of outputs for this rule.
9696
repeated OutputTarget output = 5;
97+
98+
enum Type {
99+
RULE = 1;
100+
MACRO = 2;
101+
REPOSITORY_RULE = 3;
102+
}
103+
optional Type type = 6;
97104
}
98105

99106
message BuildLanguage {

skydoc/macro_extractor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def _add_macro_doc(self, stmt):
7171

7272
rule = self.__language.rule.add()
7373
rule.name = stmt.name
74+
rule.type = build_pb2.RuleDefinition.MACRO
7475

7576
doc = ast.get_docstring(stmt)
7677
if doc:

skydoc/macro_extractor_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def multiline(name, foo=False, visibility=None):
8686
mandatory: false
8787
documentation: "The visibility of this rule.\\n\\nDocumentation for visibility continued here."
8888
}
89+
type: MACRO
8990
}
9091
""")
9192

@@ -115,6 +116,7 @@ def undocumented(name, visibility=None):
115116
type: UNKNOWN
116117
mandatory: false
117118
}
119+
type: MACRO
118120
}
119121
""")
120122

@@ -167,6 +169,7 @@ def public(name, visibility=None):
167169
mandatory: false
168170
documentation: "The visibility of this rule."
169171
}
172+
type: MACRO
170173
}
171174
""")
172175

@@ -230,6 +233,7 @@ def example_macro(name, foo, visibility=None):
230233
mandatory: false
231234
documentation: "The visibility of this rule."
232235
}
236+
type: MACRO
233237
}
234238
""")
235239

@@ -279,6 +283,7 @@ def macro_with_example(name, foo, visibility=None):
279283
mandatory: false
280284
documentation: "The visibility of this rule."
281285
}
286+
type: MACRO
282287
}
283288
""")
284289

@@ -338,6 +343,7 @@ def macro_with_outputs(name, foo, visibility=None):
338343
template: "%{name}.jar"
339344
documentation: "A Java archive."
340345
}
346+
type: MACRO
341347
}
342348
""")
343349

skydoc/main.py

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
'generated in subdirectories that match the package structure of the '
4949
'input .bzl files. The prefix to strip must be common to all .bzl files; '
5050
'otherwise, skydoc will raise an error.')
51+
gflags.DEFINE_bool('overview', False, 'Whether to generate an overview page')
52+
gflags.DEFINE_string('overview_filename', 'index',
53+
'The file name to use for the overview page.')
54+
gflags.DEFINE_string('link_ext', 'html',
55+
'The file extension used for links in the generated documentation')
5156

5257
FLAGS = gflags.FLAGS
5358

@@ -60,12 +65,13 @@
6065

6166
CSS_FILE = 'main.css'
6267

63-
def _create_jinja_environment():
68+
def _create_jinja_environment(link_ext):
6469
env = jinja2.Environment(
6570
loader=jinja2.FileSystemLoader(_runfile_path(TEMPLATE_PATH)),
6671
keep_trailing_newline=True,
6772
line_statement_prefix='%')
6873
env.filters['markdown'] = lambda text: jinja2.Markup(mistune.markdown(text))
74+
env.filters['link'] = lambda fname: '/' + fname + '.' + link_ext
6975
return env
7076

7177

@@ -112,14 +118,21 @@ def merge_languages(macro_language, rule_language):
112118
new_rule.CopyFrom(rule)
113119
return macro_language
114120

121+
class WriterOptions(object):
122+
def __init__(self, output_dir, output_file, output_zip, overview,
123+
overview_filename, link_ext):
124+
self.output_dir = output_dir
125+
self.output_file = output_file
126+
self.output_zip = output_zip
127+
self.overview = overview
128+
self.overview_filename = overview_filename
129+
self.link_ext = link_ext
130+
115131
class MarkdownWriter(object):
116132
"""Writer for generating documentation in Markdown."""
117133

118-
def __init__(self, output_dir, output_file, output_zip, strip_prefix):
119-
self.__output_dir = output_dir
120-
self.__output_file = output_file
121-
self.__output_zip = output_zip
122-
self.__strip_prefix = strip_prefix
134+
def __init__(self, writer_options):
135+
self.__options = writer_options
123136

124137
def write(self, rulesets):
125138
"""Write the documentation for the rules contained in rulesets."""
@@ -129,19 +142,21 @@ def write(self, rulesets):
129142
for ruleset in rulesets:
130143
if len(ruleset.rules) > 0:
131144
output_files.append(self._write_ruleset(temp_dir, ruleset))
145+
if self.__options.overview:
146+
output_files.append(self._write_overview(temp_dir, rulesets))
132147

133-
if self.__output_zip:
148+
if self.__options.output_zip:
134149
# We are generating a zip archive containing all the documentation.
135150
# Write each documentation file generated in the temp directory to the
136151
# zip file.
137-
with zipfile.ZipFile(self.__output_file, 'w') as zf:
152+
with zipfile.ZipFile(self.__options.output_file, 'w') as zf:
138153
for output_file, output_path in output_files:
139154
zf.write(output_file, output_path)
140155
else:
141156
# We are generating documentation in the output_dir directory. Copy each
142157
# documentation file to output_dir.
143158
for output_file, output_path in output_files:
144-
dest_file = os.path.join(self.__output_dir, output_path)
159+
dest_file = os.path.join(self.__options.output_dir, output_path)
145160
dest_dir = os.path.dirname(dest_file)
146161
if not os.path.exists(dest_dir):
147162
os.makedirs(dest_dir)
@@ -153,13 +168,13 @@ def write(self, rulesets):
153168

154169
def _write_ruleset(self, output_dir, ruleset):
155170
# Load template and render Markdown.
156-
env = _create_jinja_environment()
171+
env = _create_jinja_environment(self.__options.link_ext)
157172
template = env.get_template('markdown.jinja')
158173
out = template.render(ruleset=ruleset)
159174

160175
# Write output to file. Output files are created in a directory structure
161176
# that matches that of the input file.
162-
output_path = ruleset.output_filename(self.__strip_prefix, 'md')
177+
output_path = ruleset.output_file + '.md'
163178
output_file = "%s/%s" % (output_dir, output_path)
164179
file_dirname = os.path.dirname(output_file)
165180
if not os.path.exists(file_dirname):
@@ -168,57 +183,68 @@ def _write_ruleset(self, output_dir, ruleset):
168183
f.write(out)
169184
return (output_file, output_path)
170185

186+
def _write_overview(self, output_dir, rulesets):
187+
template = self.__env.get_template('markdown_overview.jinja')
188+
out = template.render(rulesets=rulesets)
189+
190+
output_file = "%s/%s.md" % (output_dir, self.options.overview_filename)
191+
with open(output_file, "w") as f:
192+
f.write(out)
193+
return (output_file, "%s.md" % self.options.overview_filename)
194+
171195
class HtmlWriter(object):
172196
"""Writer for generating documentation in HTML."""
173197

174-
def __init__(self, output_dir, output_file, output_zip, strip_prefix):
175-
self.__output_dir = output_dir
176-
self.__output_file = output_file
177-
self.__output_zip = output_zip
178-
self.__strip_prefix = strip_prefix
179-
self.__env = _create_jinja_environment()
198+
def __init__(self, options):
199+
self.__options = options
200+
self.__env = _create_jinja_environment(self.__options.link_ext)
180201

181202
def write(self, rulesets):
182203
# Generate navigation used for all rules.
183204
nav_template = self.__env.get_template('nav.jinja')
184-
nav = nav_template.render(rulesets=rulesets)
205+
nav = nav_template.render(
206+
rulesets=rulesets,
207+
overview=self.__options.overview,
208+
overview_filename=self.__options.overview_filename)
185209

186210
try:
187211
temp_dir = tempfile.mkdtemp()
188212
output_files = []
189213
for ruleset in rulesets:
190214
if len(ruleset.rules) > 0:
191215
output_files.append(self._write_ruleset(temp_dir, ruleset, nav))
216+
if self.__options.overview:
217+
output_files.append(self._write_overview(temp_dir, rulesets, nav))
192218

193-
if self.__output_zip:
194-
with zipfile.ZipFile(self.__output_file, 'w') as zf:
219+
if self.__options.output_zip:
220+
with zipfile.ZipFile(self.__options.output_file, 'w') as zf:
195221
for output_file, output_path in output_files:
196222
zf.write(output_file, output_path)
197223
zf.write(os.path.join(_runfile_path(CSS_PATH), CSS_FILE),
198224
'%s' % CSS_FILE)
199225
else:
200226
for output_file, output_path in output_files:
201-
dest_file = os.path.join(self.__output_dir, output_path)
227+
dest_file = os.path.join(self.__options.output_dir, output_path)
202228
dest_dir = os.path.dirname(dest_file)
203229
if not os.path.exists(dest_dir):
204230
os.makedirs(dest_dir)
205231
shutil.copyfile(output_file, dest_file)
206232

207233
# Copy CSS file.
208234
shutil.copyfile(os.path.join(_runfile_path(CSS_PATH), CSS_FILE),
209-
os.path.join(self.__output_dir, CSS_FILE))
235+
os.path.join(self.__options.output_dir, CSS_FILE))
210236
finally:
211237
# Delete temporary directory.
212238
shutil.rmtree(temp_dir)
213239

214240
def _write_ruleset(self, output_dir, ruleset, nav):
215241
# Load template and render markdown.
216242
template = self.__env.get_template('html.jinja')
217-
out = template.render(ruleset=ruleset, nav=nav)
243+
out = template.render(title=ruleset.title, ruleset=ruleset, nav=nav)
218244

219245
# Write output to file. Output files are created in a directory structure
220246
# that matches that of the input file.
221-
output_path = ruleset.output_filename(self.__strip_prefix, 'html')
247+
output_path = ruleset.output_file + '.html'
222248
output_file = "%s/%s" % (output_dir, output_path)
223249
file_dirname = os.path.dirname(output_file)
224250
if not os.path.exists(file_dirname):
@@ -227,6 +253,15 @@ def _write_ruleset(self, output_dir, ruleset, nav):
227253
f.write(out)
228254
return (output_file, output_path)
229255

256+
def _write_overview(self, output_dir, rulesets, nav):
257+
template = self.__env.get_template('html_overview.jinja')
258+
out = template.render(title='Overview', rulesets=rulesets, nav=nav)
259+
260+
output_file = "%s/%s.html" % (output_dir, self.__options.overview_filename)
261+
with open(output_file, "w") as f:
262+
f.write(out)
263+
return (output_file, "%s.html" % self.__options.overview_filename)
264+
230265
def main(argv):
231266
if FLAGS.output_dir and FLAGS.output_file:
232267
sys.stderr.write('Only one of --output_dir or --output_file can be set.')
@@ -252,17 +287,19 @@ def main(argv):
252287
rule_doc_extractor.parse_bzl(bzl_file)
253288
merged_language = merge_languages(macro_doc_extractor.proto(),
254289
rule_doc_extractor.proto())
255-
rulesets.append(rule.RuleSet(bzl_file, merged_language,
256-
macro_doc_extractor.title,
257-
macro_doc_extractor.description))
258-
290+
rulesets.append(
291+
rule.RuleSet(bzl_file, merged_language, macro_doc_extractor.title,
292+
macro_doc_extractor.description, strip_prefix,
293+
FLAGS.format))
294+
295+
writer_options = WriterOptions(
296+
FLAGS.output_dir, FLAGS.output_file, FLAGS.zip, FLAGS.overview,
297+
FLAGS.overview_filename, FLAGS.link_ext)
259298
if FLAGS.format == "markdown":
260-
markdown_writer = MarkdownWriter(FLAGS.output_dir, FLAGS.output_file,
261-
FLAGS.zip, strip_prefix)
299+
markdown_writer = MarkdownWriter(writer_options)
262300
markdown_writer.write(rulesets)
263301
elif FLAGS.format == "html":
264-
html_writer = HtmlWriter(FLAGS.output_dir, FLAGS.output_file, FLAGS.zip,
265-
strip_prefix)
302+
html_writer = HtmlWriter(writer_options)
266303
html_writer.write(rulesets)
267304
else:
268305
sys.stderr.write(

skydoc/rule.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class Rule(object):
105105
def __init__(self, proto):
106106
self.__proto = proto
107107
self.name = proto.name
108+
self.type = proto.type
108109
self.documentation = proto.documentation
109110
self.example_documentation = proto.example_documentation
110111
self.signature = self._get_signature(proto)
@@ -115,6 +116,9 @@ def __init__(self, proto):
115116
for output in proto.output:
116117
self.outputs.append(Output(output))
117118

119+
parts = proto.documentation.split("\n\n")
120+
self.short_documentation = parts[0]
121+
118122
def _get_signature(self, proto):
119123
"""Returns the rule signature for this rule."""
120124
signature = proto.name + '('
@@ -131,18 +135,33 @@ def _get_signature(self, proto):
131135
class RuleSet(object):
132136
"""Representation of a rule set used to render documentation templates."""
133137

134-
def __init__(self, bzl_file, language, title, description):
138+
def __init__(self, bzl_file, language, title, description, strip_prefix,
139+
format):
135140
self.bzl_file = bzl_file
136141
file_basename = os.path.basename(bzl_file)
137142
self.name = file_basename.replace('.bzl', '')
138143
self.language = language
139144
self.title = title if title else "%s Rules" % self.name
140145
self.description = description
141-
self.rules = []
142-
for rule_proto in language.rule:
143-
self.rules.append(Rule(rule_proto))
144146

145-
def output_filename(self, strip_prefix, file_ext):
147+
# Generate output file name.
148+
file_extension = 'html' if format == 'html' else 'md'
146149
assert self.bzl_file.startswith(strip_prefix)
147-
output_path = self.bzl_file.replace('.bzl', '.%s' % file_ext)
148-
return output_path[len(strip_prefix):]
150+
output_path = self.bzl_file.replace('.bzl', '')
151+
self.output_file = output_path[len(strip_prefix):]
152+
153+
# Populate all rules in this ruleset.
154+
self.definitions = []
155+
self.rules = []
156+
self.repository_rules = []
157+
self.macros = []
158+
for rule_proto in language.rule:
159+
definition = Rule(rule_proto)
160+
self.definitions.append(definition)
161+
if rule_proto.type == build_pb2.RuleDefinition.RULE:
162+
self.rules.append(definition)
163+
elif rule_proto.type == build_pb2.RuleDefinition.MACRO:
164+
self.macros.append(definition)
165+
else:
166+
assert rule_proto.type == build_pb2.RuleDefinition.REPOSITORY_RULE
167+
self.repository_rules.append(definition)

skydoc/rule_extractor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ def _assemble_protos(self):
146146
for rule_desc in rules:
147147
rule = self.__language.rule.add()
148148
rule.name = rule_desc.name
149+
if rule_desc.type == 'rule':
150+
rule.type = build_pb2.RuleDefinition.RULE
151+
else:
152+
rule.type = build_pb2.RuleDefinition.REPOSITORY_RULE
149153
if rule_desc.doc:
150154
rule.documentation = rule_desc.doc
151155
if rule_desc.example_doc:

0 commit comments

Comments
 (0)