Skip to content

Commit feff14a

Browse files
committed
Refactored CommentScript so that it's possible to build other kinds of comment-based scripts.
1 parent 16f8d19 commit feff14a

File tree

11 files changed

+264
-195
lines changed

11 files changed

+264
-195
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2015 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.freshmark;
17+
18+
import java.util.function.Function;
19+
import java.util.regex.Matcher;
20+
import java.util.regex.Pattern;
21+
22+
import javax.script.ScriptEngine;
23+
import javax.script.ScriptException;
24+
25+
import com.diffplug.common.base.Errors;
26+
import com.diffplug.jscriptbox.Check;
27+
28+
/**
29+
* A CommentScript is a way of automatically generating
30+
* or modifying parts of a document by embedding scripts
31+
* in the comments of that document.
32+
* <p>
33+
* A CommentScript has the following form:
34+
* <pre>
35+
* {@code
36+
* [INTRON] sectionName
37+
* script
38+
* script
39+
* [EXON]
40+
* lastProgramExecutionResult
41+
* lastProgramExecutionResult
42+
* lastProgramExecutionResult
43+
* [INTRON] /sectionName [EXON]
44+
* }
45+
* </pre>
46+
* This class is a minimal implementation of a CommentScript. To create a CommentScript,
47+
* you must provide:
48+
* <ul>
49+
* <li>The intron and exon strings in the constructor.</li>
50+
* <li>{@link #keyToValue} - defines how template keys in the script string are transformed into values</li>
51+
* <li>{@link #setupScriptEngine} - initializes any functions or variables which should be available to the script</li>
52+
* </ul>
53+
* See {@link FreshMark} for a sample implementation.
54+
*/
55+
public abstract class CommentScript {
56+
/**
57+
* Creates a CommentScript with the given comment intron/exon pair.
58+
* <p>
59+
* Comment blocks will be parsed using the following regex:
60+
* <pre>
61+
* Pattern.quote(intron) + "(.*?)" + Pattern.quote(exon)
62+
* </pre>
63+
* */
64+
protected CommentScript(String intron, String exon) {
65+
this(intron, exon, Pattern.quote(intron) + "(.*?)" + Pattern.quote(exon));
66+
}
67+
68+
/**
69+
* Creates a CommentScript with the given comment intron/exon pair, as well
70+
* as a custom regex.
71+
* <p>
72+
* Usually, you should use the {@link #CommentScript(String, String)} constructor,
73+
* unless there are some special rules for how comment blocks are parsed.
74+
*/
75+
protected CommentScript(String intron, String exon, String regex) {
76+
parser = new Parser(intron, exon, regex);
77+
}
78+
79+
/** Parser which splits up the raw document into structured tags which get passed to the compiler. */
80+
final Parser parser;
81+
82+
/** Compiles a single section/program/input combo into the appropriate output. */
83+
final Parser.Compiler compiler = new Parser.Compiler() {
84+
@Override
85+
public String compileSection(String section, String program, String input) {
86+
return Errors.rethrow().get(() -> {
87+
ScriptEngine engine = setupScriptEngine(section);
88+
89+
// apply the templating engine to the program
90+
String templatedProgram = mustacheTemplate(program, key -> keyToValue(section, key));
91+
// populate the input data
92+
engine.put("input", input);
93+
// evaluate the program and get the result
94+
engine.eval(templatedProgram);
95+
String compiled = Check.cast(engine.get("output"), String.class);
96+
// make sure that the compiled output starts and ends with a newline,
97+
// so that the tags stay separated separated nicely
98+
if (!compiled.startsWith("\n")) {
99+
compiled = "\n" + compiled;
100+
}
101+
if (!compiled.endsWith("\n")) {
102+
compiled = compiled + "\n";
103+
}
104+
return parser.prefix + " " + section + "\n" +
105+
program +
106+
parser.postfix +
107+
compiled +
108+
parser.prefix + " /" + section + " " + parser.postfix;
109+
});
110+
}
111+
};
112+
113+
/** Compiles the given input string. Input must contain only unix newlines, output is guaranteed to be the same. */
114+
public String compile(String input) {
115+
return parser.compile(input, compiler);
116+
}
117+
118+
/** Performs templating on the script. Delegates to mustache-style templating through keyToValue by default. */
119+
protected String template(String section, String script) {
120+
return mustacheTemplate(script, key -> keyToValue(section, key));
121+
}
122+
123+
/** For the given section, return the templated value for the given key. */
124+
protected abstract String keyToValue(String section, String key);
125+
126+
/**
127+
* For the given section, setup a ScriptEngine appropriately.
128+
* <p>
129+
* The {@code input} value will be set for you, and the {@code output} value will
130+
* be extracted for you, but you must do everything else.
131+
*/
132+
protected abstract ScriptEngine setupScriptEngine(String section) throws ScriptException;
133+
134+
/** Replaces whatever is inside of {@code &#123;&#123;key&#125;&#125;} tags using the {@code keyToValue} function. */
135+
static String mustacheTemplate(String input, Function<String, String> keyToValue) {
136+
Matcher matcher = MUSTACHE_PATTERN.matcher(input);
137+
StringBuilder result = new StringBuilder(input.length() * 3 / 2);
138+
139+
int lastElement = 0;
140+
while (matcher.find()) {
141+
result.append(matcher.group(1));
142+
result.append(keyToValue.apply(matcher.group(2)));
143+
lastElement = matcher.end();
144+
}
145+
result.append(input.substring(lastElement));
146+
return result.toString();
147+
}
148+
149+
/** Regex which matches for {@code {{key}}}. */
150+
private static final Pattern MUSTACHE_PATTERN = Pattern.compile("(.*?)\\{\\{(.*?)\\}\\}", Pattern.DOTALL);
151+
}

src/main/java/com/diffplug/freshmark/FreshMark.java

Lines changed: 69 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -15,85 +15,94 @@
1515
*/
1616
package com.diffplug.freshmark;
1717

18-
import java.util.function.Function;
18+
import java.net.URLEncoder;
19+
import java.nio.charset.StandardCharsets;
20+
import java.util.Map;
21+
import java.util.Objects;
22+
import java.util.function.Consumer;
1923
import java.util.regex.Matcher;
2024
import java.util.regex.Pattern;
2125

26+
import javax.script.ScriptEngine;
27+
import javax.script.ScriptException;
28+
2229
import com.diffplug.common.base.Errors;
2330
import com.diffplug.jscriptbox.Language;
2431
import com.diffplug.jscriptbox.ScriptBox;
25-
import com.diffplug.jscriptbox.TypedScriptEngine;
2632

27-
/**
28-
* The core implementation a FreshMark compiler. Provides two methods:
29-
* <ul>
30-
* <li>{@link #keyToValue} - defines how template keys are transformed into values</li>
31-
* <li>{@link #setupScriptEngine} - initializes any functions or variables which should be available to the program</li>
32-
* </ul>
33-
* See {@link FreshMarkDefault} for the default implementation.
34-
*/
35-
public abstract class FreshMark {
36-
/** Parser which splits up the raw document into structured tags which get passed to the compiler. */
37-
static final Parser parser = new Parser("<!---freshmark", "-->");
33+
/** The defaault implementation. */
34+
public class FreshMark extends CommentScript {
35+
private static final String INTRON = "<!---freshmark";
36+
private static final String EXON = "-->";
37+
38+
private final Map<String, ?> properties;
39+
private final Consumer<String> warningStream;
3840

39-
/** Compiles a single section/program/input combo into the appropriate output. */
40-
final Parser.Compiler compiler = new Parser.Compiler() {
41-
@Override
42-
public String compileSection(String section, String program, String input) {
43-
return Errors.rethrow().get(() -> {
44-
ScriptBox box = ScriptBox.create();
45-
setupScriptEngine(section, box);
46-
TypedScriptEngine engine = box.buildTyped(Language.nashorn());
41+
public FreshMark(Map<String, ?> properties, Consumer<String> warningStream) {
42+
super(INTRON, EXON, Pattern.quote(INTRON) + "(.*?)" + Pattern.quote(EXON));
43+
this.properties = properties;
44+
this.warningStream = warningStream;
45+
}
46+
47+
@Override
48+
protected ScriptEngine setupScriptEngine(String section) throws ScriptException {
49+
return ScriptBox.create()
50+
.setAll(properties)
51+
.set("link").toFunc2(FreshMark::link)
52+
.set("image").toFunc2(FreshMark::image)
53+
.set("shield").toFunc4(FreshMark::shield)
54+
.set("prefixDelimiterReplace").toFunc4(FreshMark::prefixDelimiterReplace)
55+
.build(Language.javascript());
56+
}
4757

48-
// apply the templating engine to the program
49-
String templatedProgram = template(program, key -> keyToValue(section, key));
50-
// populate the input data
51-
engine.getRaw().put("input", input);
52-
// evaluate the program and get the result
53-
engine.eval(templatedProgram);
54-
String compiled = engine.get("output", String.class);
55-
// make sure that the compiled output starts and ends with a newline,
56-
// so that the tags stay separated separated nicely
57-
if (!compiled.startsWith("\n")) {
58-
compiled = "\n" + compiled;
59-
}
60-
if (!compiled.endsWith("\n")) {
61-
compiled = compiled + "\n";
62-
}
63-
return parser.prefix + " " + section + "\n" +
64-
program +
65-
parser.postfix +
66-
compiled +
67-
parser.prefix + " /" + section + " " + parser.postfix;
68-
});
58+
@Override
59+
protected String keyToValue(String section, String key) {
60+
Object value = properties.get(key);
61+
if (value != null) {
62+
return Objects.toString(value);
63+
} else {
64+
warningStream.accept("Unknown key '" + key + "'");
65+
return key + "=UNKNOWN";
6966
}
70-
};
67+
}
7168

72-
/** Compiles the given input string. Input must contain only unix newlines, output is guaranteed to be the same. */
73-
public String compile(String input) {
74-
return parser.compile(input, compiler);
69+
////////////////////////
70+
// built-in functions //
71+
////////////////////////
72+
/** Generates a markdown link. */
73+
static String link(String text, String url) {
74+
return "[" + text + "](" + url + ")";
7575
}
7676

77-
/** For the given section, return the proper templated value for the given key. */
78-
protected abstract String keyToValue(String section, String key);
77+
/** Generates a markdown image. */
78+
static String image(String altText, String url) {
79+
return "!" + link(altText, url);
80+
}
7981

80-
/** For the given section, setup the JScriptBox appropriately. The `input` value will be set for you, but you need to do everything else. */
81-
protected abstract void setupScriptEngine(String section, ScriptBox scriptBox);
82+
/** Generates shields using <a href="http://shields.io/">shields.io</a>. */
83+
static String shield(String altText, String subject, String status, String color) {
84+
return image(altText, "https://img.shields.io/badge/" + shieldEscape(subject) + "-" + shieldEscape(status) + "-" + shieldEscape(color) + ".svg");
85+
}
8286

83-
/** Replaces whatever is inside of {@code &#123;&#123;key&#125;&#125;} tags using the {@code keyToValue} function. */
84-
static String template(String input, Function<String, String> keyToValue) {
85-
Matcher matcher = TEMPLATE.matcher(input);
86-
StringBuilder result = new StringBuilder(input.length() * 3 / 2);
87+
private static String shieldEscape(String raw) {
88+
return Errors.rethrow().get(() -> URLEncoder.encode(
89+
raw.replace("_", "__").replace("-", "--").replace(" ", "_"),
90+
StandardCharsets.UTF_8.name()));
91+
}
8792

93+
/** Replaces after prefix and before delimiter with replacement. */
94+
static String prefixDelimiterReplace(String input, String prefix, String delimiter, String replacement) {
95+
StringBuilder builder = new StringBuilder(input.length() * 3 / 2);
8896
int lastElement = 0;
97+
Pattern pattern = Pattern.compile("(.*?" + Pattern.quote(prefix) + ")(.*?)(" + Pattern.quote(delimiter) + ")", Pattern.DOTALL);
98+
Matcher matcher = pattern.matcher(input);
8999
while (matcher.find()) {
90-
result.append(matcher.group(1));
91-
result.append(keyToValue.apply(matcher.group(2)));
100+
builder.append(matcher.group(1));
101+
builder.append(replacement);
102+
builder.append(matcher.group(3));
92103
lastElement = matcher.end();
93104
}
94-
result.append(input.substring(lastElement));
95-
return result.toString();
105+
builder.append(input.substring(lastElement));
106+
return builder.toString();
96107
}
97-
98-
private static final Pattern TEMPLATE = Pattern.compile("(.*?)\\{\\{(.*?)\\}\\}", Pattern.DOTALL);
99108
}

0 commit comments

Comments
 (0)