[KeepAnno] Add table of contents to user guide
Bug: b/248408342
Change-Id: I88fa8890297e6b416ef4be487b92642e52921508
diff --git a/doc/keepanno-guide.md b/doc/keepanno-guide.md
index 9970ae4..7b16d03 100644
--- a/doc/keepanno-guide.md
+++ b/doc/keepanno-guide.md
@@ -16,8 +16,20 @@
[R8 component](https://issuetracker.google.com/issues?q=status:open%20componentid:326788).
+## Table of contents
-## Introduction
+- [Introduction](#introduction)
+- [Build configuration](#build-configuration)
+- [Annotating code using reflection](#using-reflection)
+- [Annotating code used by reflection (or via JNI)](#used-by-reflection)
+- [Annotating APIs](#apis)
+- [Migrating rules to annotations](#migrating-rules)
+- [My use case is not covered!](#other-uses)
+- [Troubleshooting](#troubleshooting)
+
+
+
+## Introduction<a id="introduction"></a>
When using a Java/Kotlin shrinker such as R8 or Proguard, developers must inform
the shrinker about parts of the program that are used either externally from the
@@ -35,7 +47,7 @@
hopefully more clear and direct meaning.
-## Build configuration
+## Build configuration<a id="build-configuration"></a>
To use the keep annotations your build must include the library of
annotations. It is currently built as part of each R8 build and if used with R8,
@@ -61,7 +73,7 @@
# ... the rest of your R8 compilation command here ...
```
-### Annotating code using reflection
+## Annotating code using reflection<a id="using-reflection"></a>
The keep annotation library defines a family of annotations depending on your
use case. You should generally prefer [@UsesReflection](https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/com/android/tools/r8/keepanno/annotations/UsesReflection.html) where applicable.
@@ -96,18 +108,16 @@
-
-### Annotating code used by reflection (or via JNI)
+## Annotating code used by reflection (or via JNI)<a id="used-by-reflection"></a>
-
-### Annotating APIs
+## Annotating APIs<a id="apis"></a>
-### Migrating rules to annotations
+## Migrating rules to annotations<a id="migrating-rules"></a>
-### My use case is not covered!
+## My use case is not covered!<a id="other-uses"></a>
-### Troubleshooting
+## Troubleshooting<a id="troubleshooting"></a>
diff --git a/doc/keepanno-guide.template.md b/doc/keepanno-guide.template.md
index 6e0a8df..2002802 100644
--- a/doc/keepanno-guide.template.md
+++ b/doc/keepanno-guide.template.md
@@ -13,8 +13,10 @@
[R8 component](https://issuetracker.google.com/issues?q=status:open%20componentid:326788).
+[[[TOC]]]
-## Introduction
+
+## [Introduction](introduction)
When using a Java/Kotlin shrinker such as R8 or Proguard, developers must inform
the shrinker about parts of the program that are used either externally from the
@@ -32,7 +34,7 @@
hopefully more clear and direct meaning.
-## Build configuration
+## [Build configuration](build-configuration)
To use the keep annotations your build must include the library of
annotations. It is currently built as part of each R8 build and if used with R8,
@@ -58,7 +60,7 @@
# ... the rest of your R8 compilation command here ...
```
-### Annotating code using reflection
+## [Annotating code using reflection](using-reflection)
The keep annotation library defines a family of annotations depending on your
use case. You should generally prefer `@UsesReflection` where applicable.
@@ -68,18 +70,16 @@
[[[INCLUDE CODE:UsesReflectionOnVirtualMethod]]]
-
-### Annotating code used by reflection (or via JNI)
+## [Annotating code used by reflection (or via JNI)](used-by-reflection)
-
-### Annotating APIs
+## [Annotating APIs](apis)
-### Migrating rules to annotations
+## [Migrating rules to annotations](migrating-rules)
-### My use case is not covered!
+## [My use case is not covered!](other-uses)
-### Troubleshooting
+## [Troubleshooting](troubleshooting)
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
index f4fd1a8..518d613 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepAnnoMarkdownGenerator.java
@@ -4,6 +4,8 @@
package com.android.tools.r8.keepanno.utils;
+import static com.android.tools.r8.keepanno.utils.KeepItemAnnotationGenerator.quote;
+
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.keepanno.annotations.KeepBinding;
import com.android.tools.r8.keepanno.annotations.KeepCondition;
@@ -22,9 +24,15 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
public class KeepAnnoMarkdownGenerator {
@@ -39,6 +47,8 @@
private static String JAVADOC_URL =
"https://storage.googleapis.com/r8-releases/raw/main/docs/keepanno/javadoc/";
+ private static final String TOC_MARKER = "[[[TOC]]]";
+
private static final String INCLUDE_MD_START = "[[[INCLUDE";
private static final String INCLUDE_MD_DOC_START = "[[[INCLUDE DOC";
private static final String INCLUDE_MD_CODE_START = "[[[INCLUDE CODE";
@@ -135,10 +145,17 @@
println("[comment]: <> (Changes should be made in " + template + ")");
println();
List<String> readAllLines = FileUtils.readAllLines(template);
+ TableEntry root = new TableEntry(0, "root", "root", null);
+ readAllLines = collectTableOfContents(readAllLines, root);
+
for (int i = 0; i < readAllLines.size(); i++) {
String line = readAllLines.get(i);
try {
- processLine(line, generator);
+ if (line.trim().equals(TOC_MARKER)) {
+ printTableOfContents(root);
+ } else {
+ processLine(line, generator);
+ }
} catch (Exception e) {
System.err.println("Parse error on line " + (i + 1) + ":");
System.err.println(line);
@@ -147,6 +164,70 @@
}
}
+ private void printTableOfContents(TableEntry root) {
+ println("## Table of contents");
+ println();
+ printTableSubEntries(root.subSections.values());
+ println();
+ }
+
+ private void printTableSubEntries(Collection<TableEntry> entries) {
+ for (TableEntry entry : entries) {
+ println("- " + entry.getHrefLink());
+ generator.withIndent(() -> printTableSubEntries(entry.subSections.values()));
+ }
+ }
+
+ private List<String> collectTableOfContents(List<String> lines, TableEntry root) {
+ Set<String> seen = new HashSet<>();
+ TableEntry current = root;
+ List<String> newLines = new ArrayList<>(lines.size());
+ Iterator<String> iterator = lines.iterator();
+ // Skip forward until the TOC insertion.
+ while (iterator.hasNext()) {
+ String line = iterator.next();
+ newLines.add(line);
+ if (line.trim().equals(TOC_MARKER)) {
+ break;
+ }
+ }
+ // Find TOC entries and replace the headings with links.
+ while (iterator.hasNext()) {
+ String line = iterator.next();
+ int headingDepth = 0;
+ for (int i = 0; i < line.length(); i++) {
+ char c = line.charAt(i);
+ if (c != '#') {
+ headingDepth = i;
+ break;
+ }
+ }
+ if (headingDepth == 0) {
+ newLines.add(line);
+ continue;
+ }
+ String headingPrefix = line.substring(0, headingDepth);
+ String headingContent = line.substring(headingDepth).trim();
+ int splitIndex = headingContent.indexOf("](");
+ if (splitIndex < 0 || !headingContent.startsWith("[") || !headingContent.endsWith(")")) {
+ throw new RuntimeException("Invalid heading format. Use [Heading Text](heading-id)");
+ }
+ String headingText = headingContent.substring(1, splitIndex);
+ String headingId = headingContent.substring(splitIndex + 2, headingContent.length() - 1);
+ if (!seen.add(headingId)) {
+ throw new RuntimeException("Duplicate heading id: " + headingText);
+ }
+ while (headingDepth <= current.depth) {
+ current = current.parent;
+ }
+ TableEntry entry = new TableEntry(headingDepth, headingText, headingId, current);
+ current.subSections.put(headingText, entry);
+ current = entry;
+ newLines.add(headingPrefix + " " + entry.getIdAnchor());
+ }
+ return newLines;
+ }
+
private String replaceCodeAndDocMarkers(String line) {
String originalLine = line;
line = line.trim();
@@ -190,25 +271,46 @@
private StringBuilder unindentLines(String replacement, StringBuilder builder) {
int shortestSpacePrefix = Integer.MAX_VALUE;
List<String> lines = StringUtils.split(replacement, '\n');
+ lines = trimEmptyLines(lines);
for (String line : lines) {
if (!line.isEmpty()) {
shortestSpacePrefix = Math.min(shortestSpacePrefix, findFirstNonSpaceIndex(line));
}
}
- if (shortestSpacePrefix > 0) {
- for (String line : lines) {
- if (!line.isEmpty()) {
- builder.append(line.substring(shortestSpacePrefix));
- }
- builder.append('\n');
+ for (String line : lines) {
+ if (!line.isEmpty()) {
+ builder.append(line.substring(shortestSpacePrefix));
}
- } else {
- builder.append(replacement);
builder.append('\n');
}
return builder;
}
+ private static List<String> trimEmptyLines(List<String> lines) {
+ int startLineIndex = 0;
+ int endLineIndex = lines.size() - 1;
+ while (true) {
+ String line = lines.get(startLineIndex);
+ if (line.trim().isEmpty()) {
+ startLineIndex++;
+ } else {
+ break;
+ }
+ }
+ while (true) {
+ String line = lines.get(endLineIndex);
+ if (line.trim().isEmpty()) {
+ --endLineIndex;
+ } else {
+ break;
+ }
+ }
+ if (startLineIndex != 0 || endLineIndex != lines.size() - 1) {
+ lines = lines.subList(startLineIndex, endLineIndex + 1);
+ }
+ return lines;
+ }
+
private int findFirstNonSpaceIndex(String line) {
for (int i = 0; i < line.length(); i++) {
if (line.charAt(i) != ' ') {
@@ -244,4 +346,27 @@
}
generator.println(line);
}
+
+ private static class TableEntry {
+ final int depth;
+ final String name;
+ final String id;
+ final TableEntry parent;
+ final Map<String, TableEntry> subSections = new LinkedHashMap<>();
+
+ public TableEntry(int depth, String name, String id, TableEntry parent) {
+ this.depth = depth;
+ this.name = name;
+ this.id = id;
+ this.parent = parent;
+ }
+
+ public String getHrefLink() {
+ return "[" + name + "](#" + id + ")";
+ }
+
+ public String getIdAnchor() {
+ return name + "<a id=" + quote(id) + "></a>";
+ }
+ }
}
diff --git a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
index 005c344..d139b43 100644
--- a/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
+++ b/src/test/java/com/android/tools/r8/keepanno/utils/KeepItemAnnotationGenerator.java
@@ -47,7 +47,7 @@
Generator.run();
}
- private static String quote(String str) {
+ public static String quote(String str) {
return "\"" + str + "\"";
}
@@ -218,7 +218,7 @@
this.writer = writer;
}
- private void withIndent(Runnable fn) {
+ public void withIndent(Runnable fn) {
indent += 2;
fn.run();
indent -= 2;