Skip to content

Commit 5864974

Browse files
Support linking and write type links
1 parent 04aab6f commit 5864974

File tree

8 files changed

+220
-27
lines changed

8 files changed

+220
-27
lines changed

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocSymbolProvider.java

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import java.util.Locale;
1111
import java.util.Objects;
12+
import java.util.Optional;
1213
import java.util.logging.Logger;
1314
import software.amazon.smithy.codegen.core.Symbol;
1415
import software.amazon.smithy.codegen.core.SymbolProvider;
@@ -17,9 +18,11 @@
1718
import software.amazon.smithy.model.shapes.ServiceShape;
1819
import software.amazon.smithy.model.shapes.Shape;
1920
import software.amazon.smithy.model.shapes.ShapeVisitor;
21+
import software.amazon.smithy.model.shapes.StructureShape;
2022
import software.amazon.smithy.model.traits.StringTrait;
2123
import software.amazon.smithy.model.traits.TitleTrait;
2224
import software.amazon.smithy.utils.SmithyUnstableApi;
25+
import software.amazon.smithy.utils.StringUtils;
2326

2427
/**
2528
* Creates documentation Symbols for each shape in the model.
@@ -34,7 +37,8 @@
3437
* any renames from the attached service.
3538
* <li>{@code definitionFile}: The file in which the documentation for this shape
3639
* should be written. By default these are all written to a single flat directory.
37-
* If this is empty, the shape does not have its own definition section.
40+
* If this is empty, the shape does not have its own definition section and cannot
41+
* be linked to.
3842
* <li>{@link #SHAPE_PROPERTY}: A named Shape property containing the shape that
3943
* the symbol represents. Decorators provided by
4044
* {@link DocIntegration#decorateSymbolProvider} MUST set or preserve this
@@ -127,34 +131,39 @@ public Symbol serviceShape(ServiceShape shape) {
127131
.build();
128132
}
129133

134+
@Override
135+
public Symbol structureShape(StructureShape shape) {
136+
return getSymbolBuilder(shape)
137+
.definitionFile(getDefinitionFile(serviceShape, shape))
138+
.build();
139+
}
140+
130141
@Override
131142
public Symbol memberShape(MemberShape shape) {
132-
var builder = getSymbolBuilder(shape);
133-
var containerLinkId = model.expectShape(shape.getContainer())
143+
var builder = getSymbolBuilder(shape)
144+
.definitionFile(getDefinitionFile(serviceShape, model.expectShape(shape.getId().withoutMember())));
145+
146+
Optional<String> containerLinkId = model.expectShape(shape.getContainer())
134147
.accept(this)
135-
.expectProperty(LINK_ID_PROPERTY, String.class);
136-
var linkId = containerLinkId + "-" + getLinkId(getShapeName(serviceShape, shape));
137-
builder.putProperty(LINK_ID_PROPERTY, linkId);
148+
.getProperty(LINK_ID_PROPERTY, String.class);
149+
if (containerLinkId.isPresent()) {
150+
var linkId = containerLinkId.get() + "-" + getLinkId(getShapeName(serviceShape, shape));
151+
builder.putProperty(LINK_ID_PROPERTY, linkId);
152+
}
138153
return builder.build();
139154
}
140155

141156
private Symbol.Builder getSymbolBuilder(Shape shape) {
142157
var name = getShapeName(serviceShape, shape);
143158
return Symbol.builder()
144-
.name(name)
145-
.putProperty(SHAPE_PROPERTY, shape)
146-
.definitionFile(getDefinitionFile(serviceShape, shape))
147-
.putProperty(LINK_ID_PROPERTY, getLinkId(name))
148-
.putProperty(ENABLE_DEFAULT_FILE_EXTENSION, true);
159+
.name(name)
160+
.putProperty(SHAPE_PROPERTY, shape)
161+
.putProperty(LINK_ID_PROPERTY, getLinkId(name))
162+
.putProperty(ENABLE_DEFAULT_FILE_EXTENSION, true);
149163
}
150164

151165
private String getDefinitionFile(ServiceShape serviceShape, Shape shape) {
152-
if (shape.isMemberShape()) {
153-
return getDefinitionFile(serviceShape, model.expectShape(shape.getId().withoutMember()));
154-
}
155-
return getDefinitionFile(
156-
getShapeName(serviceShape, shape).replaceAll("\\s+", "")
157-
);
166+
return getDefinitionFile(getShapeName(serviceShape, shape).replaceAll("\\s+", ""));
158167
}
159168

160169
private String getDefinitionFile(String filename) {
@@ -219,7 +228,7 @@ public Symbol toSymbol(Shape shape) {
219228
}
220229

221230
private String addExtension(String path) {
222-
if (!path.endsWith(extension)) {
231+
if (!StringUtils.isBlank(path) && !path.endsWith(extension)) {
223232
path += extension;
224233
}
225234
return path;

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocgenUtils.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212
import java.io.InputStreamReader;
1313
import java.nio.charset.Charset;
1414
import java.nio.file.Path;
15+
import java.nio.file.Paths;
1516
import java.util.ArrayList;
1617
import java.util.List;
18+
import java.util.Optional;
1719
import java.util.logging.Logger;
1820
import software.amazon.smithy.codegen.core.CodegenException;
21+
import software.amazon.smithy.codegen.core.Symbol;
1922
import software.amazon.smithy.utils.SmithyUnstableApi;
23+
import software.amazon.smithy.utils.StringUtils;
2024

2125
/**
2226
* Provides various utility methods.
@@ -83,4 +87,29 @@ public static String runCommand(String command, Path directory) {
8387
public static String normalizeNewlines(String input) {
8488
return input.replaceAll("\r?\n", System.lineSeparator());
8589
}
90+
91+
/**
92+
* Gets a relative link pointing to a given symbol.
93+
*
94+
* <p>If the given symbol has no definition file or no
95+
* {@link DocSymbolProvider#LINK_ID_PROPERTY}, the response will be empty.
96+
*
97+
* @param symbol The symbol to link to.
98+
* @param relativeTo A path that the symbol should be relative to. This must be the
99+
* path to the file containing the link.
100+
* @return Optionally returns a relative link to the given symbol.
101+
*/
102+
public static Optional<String> getSymbolLink(Symbol symbol, Path relativeTo) {
103+
Optional<String> linkId = symbol.getProperty(DocSymbolProvider.LINK_ID_PROPERTY, String.class);
104+
var relativeToParent = relativeTo.getParent();
105+
if (StringUtils.isBlank(symbol.getDefinitionFile())
106+
|| linkId.isEmpty()
107+
|| StringUtils.isBlank(linkId.get())
108+
|| relativeToParent == null) {
109+
return Optional.empty();
110+
}
111+
return Optional.of(format(
112+
"./%s#%s", relativeToParent.relativize(Paths.get(symbol.getDefinitionFile())), linkId.get()
113+
));
114+
}
86115
}

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/MemberGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public Void unionShape(UnionShape shape) {
262262

263263
private void writeShapeName(Shape shape) {
264264
var symbol = context.symbolProvider().toSymbol(shape);
265-
writer.writeInline(symbol.getName());
265+
writer.writeInline("$R", symbol);
266266
}
267267
}
268268
}

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/StructureGenerator.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import software.amazon.smithy.codegen.core.directed.GenerateStructureDirective;
1010
import software.amazon.smithy.docgen.core.DocGenerationContext;
1111
import software.amazon.smithy.docgen.core.DocSettings;
12+
import software.amazon.smithy.docgen.core.DocSymbolProvider;
1213
import software.amazon.smithy.docgen.core.generators.MemberGenerator.MemberListingType;
1314
import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection;
1415
import software.amazon.smithy.docgen.core.sections.ShapeSection;
@@ -63,6 +64,8 @@ public void accept(GenerateStructureDirective<DocGenerationContext, DocSettings>
6364
var symbol = directive.symbolProvider().toSymbol(shape);
6465
directive.context().writerDelegator().useShapeWriter(shape, writer -> {
6566
writer.pushState(new ShapeSection(directive.context(), shape));
67+
68+
symbol.getProperty(DocSymbolProvider.LINK_ID_PROPERTY, String.class).ifPresent(writer::writeAnchor);
6669
writer.openHeading(symbol.getName());
6770

6871
writer.writeShapeDocs(shape, directive.model());

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/DocWriter.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,49 @@
2222
public abstract class DocWriter extends SymbolWriter<DocWriter, DocImportContainer> {
2323
private static final int MAX_HEADING_DEPTH = 6;
2424

25+
/**
26+
* The full path to the file being written to by the writer.
27+
*/
28+
protected final String filename;
29+
2530
private int headingDepth = 0;
2631

2732
/**
2833
* Constructor.
2934
*
3035
* @param importContainer The container to store any imports in.
36+
* @param filename The name of the file being written.
3137
*/
32-
public DocWriter(DocImportContainer importContainer) {
38+
public DocWriter(DocImportContainer importContainer, String filename) {
3339
super(importContainer);
40+
this.filename = filename;
41+
putFormatter('R', (s, i) -> referenceFormatter(s));
3442
}
3543

44+
/**
45+
* Formats the given reference object as a link if possible.
46+
*
47+
* <p>This given value can be expected to be one of the following types:
48+
*
49+
* <ul>
50+
* <li>{@code Symbol}: The symbol's name is the link text and a combination of
51+
* the definition file and {@link software.amazon.smithy.docgen.core.DocSymbolProvider#LINK_ID_PROPERTY}
52+
* forms the actual link. If either the link id or definition file are not set,
53+
* the formatter must return the symbol's name.
54+
* <li>{@code SymbolReference}: The reference's alias is the link text and a
55+
* combination of the referenced symbol's definition file and
56+
* {@link software.amazon.smithy.docgen.core.DocSymbolProvider#LINK_ID_PROPERTY}
57+
* forms the actual link. If either the link id or definition file are not set,
58+
* the formatter should return the reference's alias.
59+
* <li>{@code Pair<String, String>}: The key is the link text and the value is
60+
* the link. Both key and value MUST be present.
61+
* </ul>
62+
*
63+
* @param value The value to format.
64+
* @return returns a string formatted to reference the given value.
65+
*/
66+
abstract String referenceFormatter(Object value);
67+
3668
/**
3769
* Writes out the content of the shape's
3870
* <a href="https://smithy.io/2.0/spec/documentation-traits.html#smithy-api-documentation-trait">
@@ -72,6 +104,20 @@ public DocWriter openHeading(String content) {
72104
return openHeading(content, headingDepth);
73105
}
74106

107+
/**
108+
* Writes a heading with the given content and linkId.
109+
*
110+
* <p>{@link #closeHeading} will be called to enable cleaning up any resources or
111+
* context this method creates.
112+
*
113+
* @param content A string to use as the heading content.
114+
* @param linkId The identifier used to link to the heading.
115+
* @return returns the writer.
116+
*/
117+
public DocWriter openHeading(String content, String linkId) {
118+
return writeAnchor(linkId).openHeading(content);
119+
}
120+
75121
/**
76122
* Writes a heading of a given level with the given content.
77123
*
@@ -147,4 +193,18 @@ public DocWriter closeHeading() {
147193
* @return returns the writer.
148194
*/
149195
public abstract DocWriter closeMemberEntry();
196+
197+
/**
198+
* Writes a linkable element to the documentation with the given identifier.
199+
*
200+
* <p>The resulting HTML should be able to link to this anchor with {@code #linkId}.
201+
*
202+
* <p>For example, a direct HTML writer might create a {@code span} tag with
203+
* the given string as the tag's {@code id}, or modify the next emitted tag
204+
* to have the given id.
205+
*
206+
* @param linkId The anchor's link identifier.
207+
* @return returns the writer.
208+
*/
209+
public abstract DocWriter writeAnchor(String linkId);
150210
}

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/MarkdownWriter.java

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@
55

66
package software.amazon.smithy.docgen.core.writers;
77

8+
import static software.amazon.smithy.docgen.core.DocgenUtils.getSymbolLink;
9+
10+
import java.nio.file.Paths;
811
import java.util.Optional;
912
import java.util.function.Consumer;
13+
import software.amazon.smithy.codegen.core.CodegenException;
1014
import software.amazon.smithy.codegen.core.Symbol;
15+
import software.amazon.smithy.codegen.core.SymbolReference;
1116
import software.amazon.smithy.codegen.core.SymbolWriter;
1217
import software.amazon.smithy.model.Model;
1318
import software.amazon.smithy.model.shapes.Shape;
1419
import software.amazon.smithy.model.traits.DocumentationTrait;
20+
import software.amazon.smithy.utils.Pair;
1521
import software.amazon.smithy.utils.SmithyUnstableApi;
1622
import software.amazon.smithy.utils.StringUtils;
1723

@@ -22,22 +28,74 @@
2228
public class MarkdownWriter extends DocWriter {
2329

2430
/**
25-
* Constructor.
31+
* Constructs a MarkdownWriter.
32+
*
33+
* @param importContainer this file's import container.
34+
* @param filename The full path to the file being written to.
35+
*/
36+
public MarkdownWriter(DocImportContainer importContainer, String filename) {
37+
super(importContainer, filename);
38+
}
39+
40+
41+
/**
42+
* Constructs a MarkdownWriter.
43+
*
44+
* @param filename The full path to the file being written to.
2645
*/
27-
public MarkdownWriter() {
28-
super(new DocImportContainer());
46+
public MarkdownWriter(String filename) {
47+
this(new DocImportContainer(), filename);
2948
}
3049

3150
/**
3251
* Factory to construct {@code MarkdownWriter}s.
3352
*/
3453
public static final class Factory implements SymbolWriter.Factory<DocWriter> {
3554
@Override
36-
public DocWriter apply(String s, String s1) {
37-
return new MarkdownWriter();
55+
public DocWriter apply(String filename, String namespace) {
56+
return new MarkdownWriter(filename);
3857
}
3958
}
4059

60+
@Override
61+
String referenceFormatter(Object value) {
62+
var reference = getReferencePair(value);
63+
if (reference.getRight().isPresent()) {
64+
return String.format("[%s](%s)", reference.getLeft(), reference.getRight().get());
65+
} else {
66+
return reference.getLeft();
67+
}
68+
}
69+
70+
private Pair<String, Optional<String>> getReferencePair(Object value) {
71+
String text;
72+
Optional<String> ref;
73+
var relativeTo = Paths.get(filename);
74+
if (value instanceof Symbol symbolValue) {
75+
text = symbolValue.getName();
76+
ref = getSymbolLink(symbolValue, relativeTo);
77+
} else if (value instanceof SymbolReference referenceValue) {
78+
text = referenceValue.getAlias();
79+
ref = getSymbolLink(referenceValue.getSymbol(), relativeTo);
80+
} else if (value instanceof Pair pairValue) {
81+
if (pairValue.getLeft() instanceof String left && pairValue.getRight() instanceof String right) {
82+
text = left;
83+
ref = Optional.of(right);
84+
} else {
85+
throw new CodegenException(
86+
"Invalid type provided to $R. Expected both key and vale of the Pair to be Strings, but "
87+
+ "found " + value.getClass()
88+
);
89+
}
90+
} else {
91+
throw new CodegenException(
92+
"Invalid type provided to $R. Expected a Symbol, SymbolReference, or Pair<String, String>, but "
93+
+ "found " + value.getClass()
94+
);
95+
}
96+
return Pair.of(text, ref);
97+
}
98+
4199
@Override
42100
public DocWriter writeShapeDocs(Shape shape, Model model) {
43101
Optional<DocumentationTrait> docTrait;
@@ -84,6 +142,12 @@ public DocWriter closeMemberEntry() {
84142
return this;
85143
}
86144

145+
@Override
146+
public DocWriter writeAnchor(String linkId) {
147+
// Anchors have no meaning in base markdown
148+
return this;
149+
}
150+
87151
@Override
88152
public String toString() {
89153
// Ensure there's exactly one trailing newline

0 commit comments

Comments
 (0)