Skip to content

Commit a0caf33

Browse files
authored
Support mdx file format for docs (#107428)
Adding support for MDX files in our :docs project. We parse those *.mdx files like we do for asciidoc files for code snippets and generate yaml specs from them that we test as part of our integration tests. By default: When searching for doc sources in the docs folder we fail the build if we detect multiple files of the same name but different extension. E.g. having painless-field-context.mdx and painless-field-context.asciidoc in the same source folder will fail the build. Migration Mode: To allow easier migration from asciidoc to mdx the build supports a kind of migration mode. When running the build with -Dgradle.docs.migration=true (e.g. ./gradlew buildRestTests -Dgradle.docs.migration=true) Duplicate doc source files (asciidoc and mdx) are allowed The Generated yaml rest specs for duplicates will have the extension *.mdx.yml or *asciidoc.yml. The generated yaml rest specs for duplicates are compared to each other to ensure they produce the same yml output.
1 parent b412ae6 commit a0caf33

File tree

20 files changed

+2398
-1884
lines changed

20 files changed

+2398
-1884
lines changed

build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/doc/DocsTestPluginFuncTest.groovy

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ mapper-annotated-text.asciidoc[51:69](console)// TEST[setup:seats]
4545
""")
4646
}
4747

48-
def "can console candidates"() {
48+
def "can list console candidates"() {
4949
when:
5050
def result = gradleRunner("listConsoleCandidates").build()
5151
then:

build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/doc/AsciidocSnippetParser.java

+49-261
Original file line numberDiff line numberDiff line change
@@ -8,296 +8,84 @@
88

99
package org.elasticsearch.gradle.internal.doc;
1010

11-
import org.gradle.api.InvalidUserDataException;
12-
13-
import java.io.File;
14-
import java.io.IOException;
15-
import java.nio.charset.StandardCharsets;
16-
import java.nio.file.Files;
17-
import java.nio.file.Path;
18-
import java.util.ArrayList;
19-
import java.util.Collection;
2011
import java.util.List;
2112
import java.util.Map;
22-
import java.util.function.BiConsumer;
2313
import java.util.regex.Matcher;
2414
import java.util.regex.Pattern;
25-
import java.util.stream.Collectors;
26-
import java.util.stream.Stream;
2715

28-
public class AsciidocSnippetParser implements SnippetParser {
16+
public class AsciidocSnippetParser extends SnippetParser {
2917
public static final Pattern SNIPPET_PATTERN = Pattern.compile("-{4,}\\s*");
18+
public static final Pattern TEST_RESPONSE_PATTERN = Pattern.compile("\\/\\/\s*TESTRESPONSE(\\[(.+)\\])?\s*");
19+
public static final Pattern SOURCE_PATTERN = Pattern.compile(
20+
"\\[\"?source\"?(?:\\.[^,]+)?,\\s*\"?([-\\w]+)\"?(,((?!id=).)*(id=\"?([-\\w]+)\"?)?(.*))?].*"
21+
);
3022

31-
private static final String CATCH = "catch:\\s*((?:\\/[^\\/]+\\/)|[^ \\]]+)";
32-
private static final String SKIP_REGEX = "skip:([^\\]]+)";
33-
private static final String SETUP = "setup:([^ \\]]+)";
34-
private static final String TEARDOWN = "teardown:([^ \\]]+)";
35-
private static final String WARNING = "warning:(.+)";
36-
private static final String NON_JSON = "(non_json)";
37-
private static final String SCHAR = "(?:\\\\\\/|[^\\/])";
38-
private static final String SUBSTITUTION = "s\\/(" + SCHAR + "+)\\/(" + SCHAR + "*)\\/";
39-
private static final String TEST_SYNTAX = "(?:"
40-
+ CATCH
41-
+ "|"
42-
+ SUBSTITUTION
43-
+ "|"
44-
+ SKIP_REGEX
45-
+ "|(continued)|"
46-
+ SETUP
47-
+ "|"
48-
+ TEARDOWN
49-
+ "|"
50-
+ WARNING
51-
+ "|(skip_shard_failures)) ?";
52-
53-
private final Map<String, String> defaultSubstitutions;
23+
public static final String CONSOLE_REGEX = "\\/\\/\s*CONSOLE\s*";
24+
public static final String NOTCONSOLE_REGEX = "\\/\\/\s*NOTCONSOLE\s*";
25+
public static final String TESTSETUP_REGEX = "\\/\\/\s*TESTSETUP\s*";
26+
public static final String TEARDOWN_REGEX = "\\/\\/\s*TEARDOWN\s*";
5427

5528
public AsciidocSnippetParser(Map<String, String> defaultSubstitutions) {
56-
this.defaultSubstitutions = defaultSubstitutions;
29+
super(defaultSubstitutions);
5730
}
5831

5932
@Override
60-
public List<Snippet> parseDoc(File rootDir, File docFile, List<Map.Entry<String, String>> substitutions) {
61-
String lastLanguage = null;
62-
Snippet snippet = null;
63-
String name = null;
64-
int lastLanguageLine = 0;
65-
StringBuilder contents = null;
66-
List<Snippet> snippets = new ArrayList<>();
33+
protected Pattern testResponsePattern() {
34+
return TEST_RESPONSE_PATTERN;
35+
}
6736

68-
try (Stream<String> lines = Files.lines(docFile.toPath(), StandardCharsets.UTF_8)) {
69-
List<String> linesList = lines.collect(Collectors.toList());
70-
for (int lineNumber = 0; lineNumber < linesList.size(); lineNumber++) {
71-
String line = linesList.get(lineNumber);
72-
if (SNIPPET_PATTERN.matcher(line).matches()) {
73-
if (snippet == null) {
74-
Path path = rootDir.toPath().relativize(docFile.toPath());
75-
snippet = new Snippet(path, lineNumber + 1, name);
76-
snippets.add(snippet);
77-
if (lastLanguageLine == lineNumber - 1) {
78-
snippet.language = lastLanguage;
79-
}
80-
name = null;
81-
} else {
82-
snippet.end = lineNumber + 1;
83-
}
84-
continue;
85-
}
37+
protected Pattern testPattern() {
38+
return Pattern.compile("\\/\\/\s*TEST(\\[(.+)\\])?\s*");
39+
}
8640

87-
Source source = matchSource(line);
88-
if (source.matches) {
89-
lastLanguage = source.language;
90-
lastLanguageLine = lineNumber;
91-
name = source.name;
92-
continue;
93-
}
94-
if (consoleHandled(docFile.getName(), lineNumber, line, snippet)) {
95-
continue;
96-
}
97-
if (testHandled(docFile.getName(), lineNumber, line, snippet, substitutions)) {
98-
continue;
99-
}
100-
if (testResponseHandled(docFile.getName(), lineNumber, line, snippet, substitutions)) {
101-
continue;
41+
private int lastLanguageLine = 0;
42+
private String currentName = null;
43+
private String lastLanguage = null;
44+
45+
protected void parseLine(List<Snippet> snippets, int lineNumber, String line) {
46+
if (SNIPPET_PATTERN.matcher(line).matches()) {
47+
if (snippetBuilder == null) {
48+
snippetBuilder = newSnippetBuilder().withLineNumber(lineNumber + 1)
49+
.withName(currentName)
50+
.withSubstitutions(defaultSubstitutions);
51+
if (lastLanguageLine == lineNumber - 1) {
52+
snippetBuilder.withLanguage(lastLanguage);
10253
}
103-
if (line.matches("\\/\\/\s*TESTSETUP\s*")) {
104-
snippet.testSetup = true;
105-
continue;
106-
}
107-
if (line.matches("\\/\\/\s*TEARDOWN\s*")) {
108-
snippet.testTearDown = true;
109-
continue;
110-
}
111-
if (snippet == null) {
112-
// Outside
113-
continue;
114-
}
115-
if (snippet.end == Snippet.NOT_FINISHED) {
116-
// Inside
117-
if (contents == null) {
118-
contents = new StringBuilder();
119-
}
120-
// We don't need the annotations
121-
line = line.replaceAll("<\\d+>", "");
122-
// Nor any trailing spaces
123-
line = line.replaceAll("\s+$", "");
124-
contents.append(line).append("\n");
125-
continue;
126-
}
127-
// Allow line continuations for console snippets within lists
128-
if (snippet != null && line.trim().equals("+")) {
129-
continue;
130-
}
131-
finalizeSnippet(snippet, contents.toString(), defaultSubstitutions, substitutions);
132-
substitutions = new ArrayList<>();
133-
;
134-
snippet = null;
135-
contents = null;
136-
}
137-
if (snippet != null) {
138-
finalizeSnippet(snippet, contents.toString(), defaultSubstitutions, substitutions);
139-
contents = null;
140-
snippet = null;
141-
substitutions = new ArrayList<>();
54+
currentName = null;
55+
} else {
56+
snippetBuilder.withEnd(lineNumber + 1);
14257
}
143-
} catch (IOException e) {
144-
e.printStackTrace();
58+
return;
14559
}
146-
return snippets;
147-
}
14860

149-
static Snippet finalizeSnippet(
150-
final Snippet snippet,
151-
String contents,
152-
Map<String, String> defaultSubstitutions,
153-
Collection<Map.Entry<String, String>> substitutions
154-
) {
155-
snippet.contents = contents.toString();
156-
snippet.validate();
157-
escapeSubstitutions(snippet, defaultSubstitutions, substitutions);
158-
return snippet;
61+
Source source = matchSource(line);
62+
if (source.matches) {
63+
lastLanguage = source.language;
64+
lastLanguageLine = lineNumber;
65+
currentName = source.name;
66+
return;
67+
}
68+
handleCommons(snippets, line);
15969
}
16070

161-
private static void escapeSubstitutions(
162-
Snippet snippet,
163-
Map<String, String> defaultSubstitutions,
164-
Collection<Map.Entry<String, String>> substitutions
165-
) {
166-
BiConsumer<String, String> doSubstitution = (pattern, subst) -> {
167-
/*
168-
* $body is really common but it looks like a
169-
* backreference so we just escape it here to make the
170-
* tests cleaner.
171-
*/
172-
subst = subst.replace("$body", "\\$body");
173-
subst = subst.replace("$_path", "\\$_path");
174-
subst = subst.replace("\\n", "\n");
175-
snippet.contents = snippet.contents.replaceAll(pattern, subst);
176-
};
177-
defaultSubstitutions.forEach(doSubstitution);
178-
179-
if (substitutions != null) {
180-
substitutions.forEach(e -> doSubstitution.accept(e.getKey(), e.getValue()));
181-
}
71+
protected String getTestSetupRegex() {
72+
return TESTSETUP_REGEX;
18273
}
18374

184-
private boolean testResponseHandled(
185-
String name,
186-
int lineNumber,
187-
String line,
188-
Snippet snippet,
189-
final List<Map.Entry<String, String>> substitutions
190-
) {
191-
Matcher matcher = Pattern.compile("\\/\\/\s*TESTRESPONSE(\\[(.+)\\])?\s*").matcher(line);
192-
if (matcher.matches()) {
193-
if (snippet == null) {
194-
throw new InvalidUserDataException(name + ":" + lineNumber + ": TESTRESPONSE not paired with a snippet at ");
195-
}
196-
snippet.testResponse = true;
197-
if (matcher.group(2) != null) {
198-
String loc = name + ":" + lineNumber;
199-
ParsingUtils.parse(
200-
loc,
201-
matcher.group(2),
202-
"(?:" + SUBSTITUTION + "|" + NON_JSON + "|" + SKIP_REGEX + ") ?",
203-
(Matcher m, Boolean last) -> {
204-
if (m.group(1) != null) {
205-
// TESTRESPONSE[s/adsf/jkl/]
206-
substitutions.add(Map.entry(m.group(1), m.group(2)));
207-
} else if (m.group(3) != null) {
208-
// TESTRESPONSE[non_json]
209-
substitutions.add(Map.entry("^", "/"));
210-
substitutions.add(Map.entry("\n$", "\\\\s*/"));
211-
substitutions.add(Map.entry("( +)", "$1\\\\s+"));
212-
substitutions.add(Map.entry("\n", "\\\\s*\n "));
213-
} else if (m.group(4) != null) {
214-
// TESTRESPONSE[skip:reason]
215-
snippet.skip = m.group(4);
216-
}
217-
}
218-
);
219-
}
220-
return true;
221-
}
222-
return false;
75+
protected String getTeardownRegex() {
76+
return TEARDOWN_REGEX;
22377
}
22478

225-
private boolean testHandled(String name, int lineNumber, String line, Snippet snippet, List<Map.Entry<String, String>> substitutions) {
226-
Matcher matcher = Pattern.compile("\\/\\/\s*TEST(\\[(.+)\\])?\s*").matcher(line);
227-
if (matcher.matches()) {
228-
if (snippet == null) {
229-
throw new InvalidUserDataException(name + ":" + lineNumber + ": TEST not paired with a snippet at ");
230-
}
231-
snippet.test = true;
232-
if (matcher.group(2) != null) {
233-
String loc = name + ":" + lineNumber;
234-
ParsingUtils.parse(loc, matcher.group(2), TEST_SYNTAX, (Matcher m, Boolean last) -> {
235-
if (m.group(1) != null) {
236-
snippet.catchPart = m.group(1);
237-
return;
238-
}
239-
if (m.group(2) != null) {
240-
substitutions.add(Map.entry(m.group(2), m.group(3)));
241-
return;
242-
}
243-
if (m.group(4) != null) {
244-
snippet.skip = m.group(4);
245-
return;
246-
}
247-
if (m.group(5) != null) {
248-
snippet.continued = true;
249-
return;
250-
}
251-
if (m.group(6) != null) {
252-
snippet.setup = m.group(6);
253-
return;
254-
}
255-
if (m.group(7) != null) {
256-
snippet.teardown = m.group(7);
257-
return;
258-
}
259-
if (m.group(8) != null) {
260-
snippet.warnings.add(m.group(8));
261-
return;
262-
}
263-
if (m.group(9) != null) {
264-
snippet.skipShardsFailures = true;
265-
return;
266-
}
267-
throw new InvalidUserDataException("Invalid test marker: " + line);
268-
});
269-
}
270-
return true;
271-
}
272-
return false;
79+
protected String getNotconsoleRegex() {
80+
return NOTCONSOLE_REGEX;
27381
}
27482

275-
private boolean consoleHandled(String fileName, int lineNumber, String line, Snippet snippet) {
276-
if (line.matches("\\/\\/\s*CONSOLE\s*")) {
277-
if (snippet == null) {
278-
throw new InvalidUserDataException(fileName + ":" + lineNumber + ": CONSOLE not paired with a snippet");
279-
}
280-
if (snippet.console != null) {
281-
throw new InvalidUserDataException(fileName + ":" + lineNumber + ": Can't be both CONSOLE and NOTCONSOLE");
282-
}
283-
snippet.console = true;
284-
return true;
285-
} else if (line.matches("\\/\\/\s*NOTCONSOLE\s*")) {
286-
if (snippet == null) {
287-
throw new InvalidUserDataException(fileName + ":" + lineNumber + ": NOTCONSOLE not paired with a snippet");
288-
}
289-
if (snippet.console != null) {
290-
throw new InvalidUserDataException(fileName + ":" + lineNumber + ": Can't be both CONSOLE and NOTCONSOLE");
291-
}
292-
snippet.console = false;
293-
return true;
294-
}
295-
return false;
83+
protected String getConsoleRegex() {
84+
return CONSOLE_REGEX;
29685
}
29786

29887
static Source matchSource(String line) {
299-
Pattern pattern = Pattern.compile("\\[\"?source\"?(?:\\.[^,]+)?,\\s*\"?([-\\w]+)\"?(,((?!id=).)*(id=\"?([-\\w]+)\"?)?(.*))?].*");
300-
Matcher matcher = pattern.matcher(line);
88+
Matcher matcher = SOURCE_PATTERN.matcher(line);
30189
if (matcher.matches()) {
30290
return new Source(true, matcher.group(1), matcher.group(5));
30391
}

0 commit comments

Comments
 (0)