Integrate bloat reports into docgen

This change updates the bloat script to output an RST version of its
report card table. Metadata is added to the bloat_report GN template
indicating its RST output, allowing it to be listed as a dependency of
pw_doc_group targets.

Change-Id: I3f098d352856a9dd8688bac44e3b60ddbb97a3a6
diff --git a/pw_bloat/BUILD.gn b/pw_bloat/BUILD.gn
index f5e97d3..2b460ef 100644
--- a/pw_bloat/BUILD.gn
+++ b/pw_bloat/BUILD.gn
@@ -13,6 +13,7 @@
 # the License.
 
 import("$dir_pw_build/pw_executable.gni")
+import("$dir_pw_docgen/docs.gni")
 
 group("pw_bloat") {
   deps = [
@@ -46,3 +47,10 @@
     "base_main.cc",
   ]
 }
+
+pw_doc_group("docs") {
+  sources = [
+    "bloat.rst",
+  ]
+  report_deps = [ "examples:simple_bloat" ]
+}
diff --git a/pw_bloat/bloat.gni b/pw_bloat/bloat.gni
index 07eb3da..95bbd6c 100644
--- a/pw_bloat/bloat.gni
+++ b/pw_bloat/bloat.gni
@@ -128,8 +128,13 @@
     ]
   }
 
+  _doc_rst_output = "$target_gen_dir/${target_name}.rst"
+
   # Create an action which runs the size report script on the provided targets.
   pw_python_script(target_name) {
+    metadata = {
+      pw_doc_sources = rebase_path([ _doc_rst_output ], root_build_dir)
+    }
     script = "$dir_pw_bloat/py/bloat.py"
     inputs = [
       _bloaty_config,
@@ -138,7 +143,7 @@
     ]
     outputs = [
       "$target_gen_dir/${target_name}.txt",
-      "$target_gen_dir/${target_name}.rst",
+      _doc_rst_output,
     ]
     deps = _all_target_dependencies
     args = _bloat_script_args + _binary_paths
diff --git a/pw_bloat/bloat.rst b/pw_bloat/bloat.rst
new file mode 100644
index 0000000..3e72a1f
--- /dev/null
+++ b/pw_bloat/bloat.rst
@@ -0,0 +1,31 @@
+.. _chapter-bloat:
+
+.. default-domain:: cpp
+
+.. highlight:: sh
+
+-----
+Bloat
+-----
+The bloat module provides tools to generate size report cards for output
+binaries.
+
+.. TODO(frolv): Explain how bloat works and how to set it up.
+
+Documentation integration
+=========================
+Bloat reports are easy to add to documentation files. All ``bloat_report``
+targets output a ``.rst`` file containing a tabular report card. This file
+can be imported directly into a documentation file using the ``include``
+directive.
+
+For example, the ``simple_bloat`` bloat report under ``//pw_bloat/examples``
+is imported into this file as follows:
+
+.. code:: rst
+
+  .. include:: examples/simple_bloat.rst
+
+Resulting in this output:
+
+.. include:: examples/simple_bloat.rst
diff --git a/pw_bloat/py/bloat.py b/pw_bloat/py/bloat.py
index 11e8c42..ad142de 100644
--- a/pw_bloat/py/bloat.py
+++ b/pw_bloat/py/bloat.py
@@ -146,18 +146,25 @@
             print(f'{sys.argv[0]}: failed to run diff on {binary}',
                   file=sys.stderr)
 
+    def write_file(filename: str, contents: str) -> None:
+        path = os.path.join(args.out_dir, filename)
+        with open(path, 'w') as output_file:
+            output_file.write(contents)
+        print(f'Output written to {path}')
+
     # TODO(frolv): Remove when custom output for full mode is added.
     if not args.full:
         out = bloat_output.TableOutput(
             args.title, diffs, charset=bloat_output.LineCharset)
         report.append(out.diff())
 
-    with open(os.path.join(
-            args.out_dir, f'{args.target}.txt'), 'w') as output_file:
-        output_file.write('\n'.join(report))
-        output_file.write('\n')
+        rst = bloat_output.RstOutput(diffs)
+        write_file(f'{args.target}.rst', rst.diff())
 
-    print('\n'.join(report))
+    complete_output = '\n'.join(report)
+    write_file(f'{args.target}.txt', complete_output)
+    print(complete_output)
+
     return 0
 
 
diff --git a/pw_bloat/py/bloat_output.py b/pw_bloat/py/bloat_output.py
index fdebe2e..35cd49d 100644
--- a/pw_bloat/py/bloat_output.py
+++ b/pw_bloat/py/bloat_output.py
@@ -16,7 +16,8 @@
 
 import abc
 import enum
-from typing import Collection, Dict, List, Optional, Tuple, Type, Union
+from typing import Callable, Collection, Dict, List, Optional, Tuple, Type
+from typing import TypeVar, Union
 
 from binary_diff import BinaryDiff, FormattedDiff
 
@@ -71,6 +72,11 @@
     HH = '═'
 
 
+def identity(val: str) -> str:
+    """Returns a string unmodified."""
+    return val
+
+
 class TableOutput(Output):
     """Tabular output."""
 
@@ -80,8 +86,14 @@
                  title: Optional[str],
                  diffs: Collection[BinaryDiff] = (),
                  charset: Union[Type[AsciiCharset],
-                                Type[LineCharset]] = AsciiCharset):
+                                Type[LineCharset]] = AsciiCharset,
+                 preprocess: Callable[[str], str] = identity,
+                 # TODO(frolv): Make this a Literal type.
+                 justify: str = 'rjust'):
         self._cs = charset
+        self._preprocess = preprocess
+        self._justify = justify
+
         super().__init__(title, diffs)
 
     def diff(self) -> str:
@@ -95,6 +107,7 @@
             max_label = max(max_label, len(diff.label))
             for segment in diff.formatted_segments():
                 for i, val in enumerate(segment):
+                    val = self._preprocess(val)
                     column_widths[i] = max(column_widths[i], len(val))
 
         separators = self._row_separators([max_label] + column_widths)
@@ -128,8 +141,9 @@
             for segment in diff.formatted_segments():
                 subrow: List[str] = []
                 label = diff.label if not subrows else ''
-                subrow.append(label.rjust(max_label, ' '))
-                subrow.extend([val.rjust(column_widths[i], ' ')
+                subrow.append(getattr(label, self._justify)(max_label, ' '))
+                subrow.extend([getattr(self._preprocess(val),
+                                       self._justify)(column_widths[i], ' ')
                                for i, val in enumerate(segment)])
                 subrows.append(self._table_row(subrow))
 
@@ -184,4 +198,10 @@
     """Tabular output in ASCII format, which is also valid RST."""
 
     def __init__(self, diffs: Collection[BinaryDiff] = ()):
-        super().__init__(None, diffs, AsciiCharset)
+        # Use RST line blocks within table cells to force each value to appear
+        # on a new line in the HTML output.
+        def add_rst_block(val: str) -> str:
+            return f'| {val}'
+
+        super().__init__(None, diffs, AsciiCharset,
+                         preprocess=add_rst_block, justify='ljust')
diff --git a/pw_docgen/py/docgen.py b/pw_docgen/py/docgen.py
index 63f844f..f1ff045 100644
--- a/pw_docgen/py/docgen.py
+++ b/pw_docgen/py/docgen.py
@@ -44,9 +44,9 @@
     parser.add_argument('--sphinx-build-dir', type=str, required=True,
                         help='Directory in which to build docs')
     parser.add_argument('--conf', type=str, required=True,
-                        'Path to conf.py file for Sphinx')
+                        help='Path to conf.py file for Sphinx')
     parser.add_argument('--gn-root', type=str, required=True,
-                        'Root of the GN build tree')
+                        help='Root of the GN build tree')
     parser.add_argument('--index', type=str, required=True,
                         help='Path to root index.rst file')
     parser.add_argument('--out-dir', type=str, required=True,