diff options
Diffstat (limited to 'website/defs.bzl')
-rw-r--r-- | website/defs.bzl | 178 |
1 files changed, 178 insertions, 0 deletions
diff --git a/website/defs.bzl b/website/defs.bzl new file mode 100644 index 000000000..f52946c15 --- /dev/null +++ b/website/defs.bzl @@ -0,0 +1,178 @@ +"""Wrappers for website documentation.""" + +load("//tools:defs.bzl", "short_path") + +# DocInfo is a provider which simple adds sufficient metadata to the source +# files (and additional data files) so that a jeyll header can be constructed +# dynamically. This is done the via BUILD system so that the plain +# documentation files can be viewable without non-compliant markdown headers. +DocInfo = provider( + fields = [ + "layout", + "description", + "permalink", + "category", + "subcategory", + "weight", + "editpath", + "authors", + ], +) + +def _doc_impl(ctx): + return [ + DefaultInfo( + files = depset(ctx.files.src + ctx.files.data), + ), + DocInfo( + layout = ctx.attr.layout, + description = ctx.attr.description, + permalink = ctx.attr.permalink, + category = ctx.attr.category, + subcategory = ctx.attr.subcategory, + weight = ctx.attr.weight, + editpath = short_path(ctx.files.src[0].short_path), + authors = ctx.attr.authors, + ), + ] + +doc = rule( + implementation = _doc_impl, + doc = "Annotate a document for jekyll headers.", + attrs = { + "src": attr.label( + doc = "The markdown source file.", + mandatory = True, + allow_single_file = True, + ), + "data": attr.label_list( + doc = "Additional data files (e.g. images).", + allow_files = True, + ), + "layout": attr.string( + doc = "The document layout.", + default = "docs", + ), + "description": attr.string( + doc = "The document description.", + default = "", + ), + "permalink": attr.string( + doc = "The document permalink.", + mandatory = True, + ), + "category": attr.string( + doc = "The document category.", + default = "", + ), + "subcategory": attr.string( + doc = "The document subcategory.", + default = "", + ), + "weight": attr.string( + doc = "The document weight.", + default = "50", + ), + "authors": attr.string_list(), + }, +) + +def _docs_impl(ctx): + # Tarball is the actual output. + tarball = ctx.actions.declare_file(ctx.label.name + ".tgz") + + # But we need an intermediate builder to translate the files. + builder = ctx.actions.declare_file("%s-builder" % ctx.label.name) + builder_content = [ + "#!/bin/bash", + "set -euo pipefail", + "declare -r T=$(mktemp -d)", + "function cleanup {", + " rm -rf $T", + "}", + "trap cleanup EXIT", + ] + for dep in ctx.attr.deps: + doc = dep[DocInfo] + + # Sanity check the permalink. + if not doc.permalink.endswith("/"): + fail("permalink %s for target %s should end with /" % ( + doc.permalink, + ctx.label.name, + )) + + # Construct the header. + header = """\ +description: {description} +permalink: {permalink} +category: {category} +subcategory: {subcategory} +weight: {weight} +editpath: {editpath} +authors: {authors} +layout: {layout}""" + + for f in dep.files.to_list(): + # Is this a markdown file? If not, then we ensure that it ends up + # in the same path as the permalink for relative addressing. + if not f.basename.endswith(".md"): + builder_content.append("mkdir -p $T/%s" % doc.permalink) + builder_content.append("cp %s $T/%s" % (f.path, doc.permalink)) + continue + + # Is this a post? If yes, then we must put this in the _posts + # directory. This directory is treated specially with respect to + # pagination and page generation. + dest = f.short_path + if doc.layout == "post": + dest = "_posts/" + f.basename + builder_content.append("echo Processing %s... >&2" % f.short_path) + builder_content.append("mkdir -p $T/$(dirname %s)" % dest) + + # Construct the header dynamically. We include the title field from + # the markdown itself, as this is the g3doc format required. The + # title will be injected by the web layout however, so we don't + # want this to appear in the document. + args = dict([(k, getattr(doc, k)) for k in dir(doc)]) + builder_content.append("title=\"$(grep -E '^# ' %s | head -n 1 | cut -d'#' -f2- || true)\"" % f.path) + builder_content.append("cat >$T/%s <<EOF" % dest) + builder_content.append("---") + builder_content.append("title: $title") + builder_content.append(header.format(**args)) + builder_content.append("---") + builder_content.append("EOF") + + # To generate the final page, we need to strip out the title (which + # was pulled above to generate the annotation in the frontmatter, + # and substitute the [TOC] tag with the {% toc %} plugin tag. Note + # that the pipeline here is almost important, as the grep will + # return non-zero if the file is empty, but we ignore that within + # the pipeline. + builder_content.append("grep -v -E '^# ' %s | sed -e 's|^\\[TOC\\]$|- TOC\\n{:toc}|' >>$T/%s" % + (f.path, dest)) + + builder_content.append("declare -r filename=$(readlink -m %s)" % tarball.path) + builder_content.append("(cd $T && tar -zcf \"${filename}\" .)\n") + ctx.actions.write(builder, "\n".join(builder_content), is_executable = True) + + # Generate the tarball. + ctx.actions.run( + inputs = depset(ctx.files.deps), + outputs = [tarball], + progress_message = "Generating %s" % ctx.label, + executable = builder, + ) + return [DefaultInfo( + files = depset([tarball]), + )] + +docs = rule( + implementation = _docs_impl, + doc = "Construct a site tarball from doc dependencies.", + attrs = { + "deps": attr.label_list( + doc = "All document dependencies.", + ), + }, +) |