Skip to content

Commit 2f5bf0e

Browse files
authored
fix(asciidoc): escape AsciiDoc table cell separator (|) in generated docs (OpenAPITools#24184)
Regex alternation patterns like `^([0-9A-Z]{3})|([0-9A-Z]{5})$` render an unescaped `|` into the AsciiDoc `|===` model/parameter tables, which AsciiDoc parses as a new cell delimiter and breaks the table layout (OpenAPITools#24119). The same issue applies to any free-form value placed in a table cell (description, default, enum values), not just pattern. Add a reusable Mustache lambda (tablecellcontent) that escapes an unescaped `|` as `\|` when rendering table-cell content in model.mustache and param.mustache, replacing the earlier pattern-only fix in toRegularExpression(). The escape is idempotent via a negative lookbehind, so a pattern that already contains a backslash-escaped pipe isn't double-escaped. Fixes OpenAPITools#24119
1 parent fdbb563 commit 2f5bf0e

5 files changed

Lines changed: 98 additions & 8 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AsciidocDocumentationCodegen.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,27 @@ public void execute(final Template.Fragment frag, final Writer out) throws IOExc
150150
}
151151
}
152152

153+
/**
154+
* Lambda escaping AsciiDoc table cell separators ("|") in rendered content, so
155+
* that free-form values (descriptions, defaults, patterns, enum values, ...)
156+
* cannot break the layout of the table they are placed in. Use:
157+
*
158+
* <pre>
159+
* {{#tablecellcontent}}{{description}}{{/tablecellcontent}}
160+
* </pre>
161+
*/
162+
public static class TableCellContentLambda implements Mustache.Lambda {
163+
// Only escape a "|" that isn't already escaped, so values that legitimately
164+
// contain a backslash-escaped pipe (e.g. a regex pattern using "\|" to match
165+
// a literal pipe character) aren't mangled with a redundant backslash.
166+
private static final java.util.regex.Pattern UNESCAPED_PIPE = java.util.regex.Pattern.compile("(?<!\\\\)\\|");
167+
168+
@Override
169+
public void execute(final Template.Fragment frag, final Writer out) throws IOException {
170+
out.write(UNESCAPED_PIPE.matcher(frag.execute()).replaceAll("\\\\|"));
171+
}
172+
}
173+
153174
protected String invokerPackage = "org.openapitools.client";
154175
protected String groupId = "org.openapitools";
155176
protected String artifactId = "openapi-client";
@@ -275,6 +296,7 @@ public AsciidocDocumentationCodegen() {
275296
languageSpecificPrimitives = new HashSet<>();
276297
importMapping = new HashMap<>();
277298

299+
additionalProperties.put("tablecellcontent", new TableCellContentLambda());
278300
}
279301

280302
@Override

modules/openapi-generator/src/main/resources/asciidoc-documentation/model.mustache

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
| Field Name| Required| Nullable | Type| Description | Format
1818

1919
{{#vars}}
20-
| {{baseName}}
20+
| {{#tablecellcontent}}{{baseName}}{{/tablecellcontent}}
2121
| {{#required}}X{{/required}}
2222
| {{#isNullable}}X{{/isNullable}}
2323
| {{#isModel}}<<{{ dataType }}>>{{/isModel}} {{^container}}{{^allowableValues}} {{^isModel}}{{ dataType }}{{/isModel}}{{/allowableValues}}{{#allowableValues}}{{^allowableValues.empty}}<<{{ dataType }}>>{{/allowableValues.empty}}{{/allowableValues}} {{/container}} {{#isContainer}} of <<{{complexType}}>>{{/isContainer}}
24-
| {{description}}
25-
| {{{dataFormat}}} {{#isEnum}}_Enum:_ {{#_enum}}{{this}}, {{/_enum}}{{/isEnum}} {{^isEnum}}{{^container}}{{^allowableValues.empty}} {{#allowableValues.values}}{{this}}, {{/allowableValues.values}} {{/allowableValues.empty}}{{/container}}{{/isEnum}}
24+
| {{#tablecellcontent}}{{description}}{{/tablecellcontent}}
25+
| {{#tablecellcontent}}{{{dataFormat}}} {{#isEnum}}_Enum:_ {{#_enum}}{{this}}, {{/_enum}}{{/isEnum}} {{^isEnum}}{{^container}}{{^allowableValues.empty}} {{#allowableValues.values}}{{this}}, {{/allowableValues.values}} {{/allowableValues.empty}}{{/container}}{{/isEnum}}{{/tablecellcontent}}
2626

2727
{{/vars}}
2828
|===
@@ -36,7 +36,7 @@
3636
| Enum Values
3737

3838
{{#allowableValues.values}}
39-
| {{this}}
39+
| {{#tablecellcontent}}{{this}}{{/tablecellcontent}}
4040
{{/allowableValues.values}}
4141

4242
|===
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
| {{baseName}}
2-
| {{description}} {{#baseType}}<<{{.}}>>{{/baseType}}
1+
| {{#tablecellcontent}}{{baseName}}{{/tablecellcontent}}
2+
| {{#tablecellcontent}}{{description}}{{/tablecellcontent}} {{#baseType}}<<{{.}}>>{{/baseType}}
33
| {{^required}}-{{/required}}{{#required}}X{{/required}}
4-
| {{defaultValue}}
5-
| {{{pattern}}}
4+
| {{#tablecellcontent}}{{defaultValue}}{{/tablecellcontent}}
5+
| {{#tablecellcontent}}{{{pattern}}}{{/tablecellcontent}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/asciidoc/AsciidocGeneratorTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,40 @@ public class AsciidocGeneratorTest {
2727

2828
private final Logger LOGGER = LoggerFactory.getLogger(AsciidocGeneratorTest.class);
2929

30+
@Test
31+
public void testPipeInPatternIsEscapedInGeneratedTables() throws Exception {
32+
File output = Files.createTempDirectory("test").toFile();
33+
34+
final CodegenConfigurator configurator = new CodegenConfigurator().setGeneratorName("asciidoc")
35+
.setInputSpec("src/test/resources/3_0/issue24119-asciidoc-table-cell-pipe.yaml")
36+
.setOutputDir(output.getAbsolutePath());
37+
38+
DefaultGenerator generator = new DefaultGenerator();
39+
generator.setGenerateMetadata(false);
40+
List<File> generatedFiles = generator.opts(configurator.toClientOptInput()).generate();
41+
TestUtils.ensureContainsFile(generatedFiles, output, "index.adoc");
42+
43+
String markupContent = FileUtils.readFileToString(
44+
new File(output, "index.adoc"), StandardCharsets.UTF_8);
45+
46+
Assert.assertFalse(markupContent.contains("/^([0-9A-Z]{3})|([0-9A-Z]{5})$/"),
47+
"unescaped pipe should not appear in generated table cell: " + markupContent);
48+
Assert.assertTrue(markupContent.contains("/^([0-9A-Z]{3})\\|([0-9A-Z]{5})$/"),
49+
"expected escaped pipe in pattern column: " + markupContent);
50+
51+
Assert.assertFalse(markupContent.contains("Accepts AAA|BBBBB"),
52+
"unescaped pipe should not appear in generated description cell: " + markupContent);
53+
Assert.assertTrue(markupContent.contains("Accepts AAA\\|BBBBB"),
54+
"expected escaped pipe in description column: " + markupContent);
55+
56+
// a pipe that is already backslash-escaped in the source pattern must not be
57+
// escaped a second time.
58+
Assert.assertTrue(markupContent.contains("/^A\\|B$/"),
59+
"expected already-escaped pipe to remain single-escaped: " + markupContent);
60+
Assert.assertFalse(markupContent.contains("/^A\\\\|B$/"),
61+
"already-escaped pipe should not be double-escaped: " + markupContent);
62+
}
63+
3064
@Test
3165
public void testPingSpecTitle() throws Exception {
3266
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/ping.yaml");
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
openapi: 3.0.3
2+
info:
3+
title: asciidoc pipe escaping test
4+
version: '1.0'
5+
paths:
6+
/things:
7+
get:
8+
operationId: getThing
9+
parameters:
10+
- name: code
11+
in: query
12+
required: false
13+
description: "Accepts AAA|BBBBB"
14+
schema:
15+
type: string
16+
pattern: "^([0-9A-Z]{3})|([0-9A-Z]{5})$"
17+
- name: preEscapedCode
18+
in: query
19+
required: false
20+
schema:
21+
type: string
22+
pattern: "^A\\|B$"
23+
responses:
24+
'200':
25+
description: OK
26+
components:
27+
schemas:
28+
Thing:
29+
type: object
30+
properties:
31+
code:
32+
type: string
33+
description: "Accepts AAA|BBBBB"
34+
pattern: "^([0-9A-Z]{3})|([0-9A-Z]{5})$"

0 commit comments

Comments
 (0)