Skip to content

Commit ec5e853

Browse files
committed
Refactored FreshMark to separate the core pieces from the "default" pieces. Also moved all implementation details out of publicy-accessible API.
1 parent 149ec0f commit ec5e853

File tree

6 files changed

+211
-150
lines changed

6 files changed

+211
-150
lines changed

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

-22
This file was deleted.

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

+49-93
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@
1515
*/
1616
package com.diffplug.freshmark;
1717

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;
2318
import java.util.function.Function;
2419
import java.util.regex.Matcher;
2520
import java.util.regex.Pattern;
@@ -29,102 +24,61 @@
2924
import com.diffplug.jscriptbox.ScriptBox;
3025
import com.diffplug.jscriptbox.TypedScriptEngine;
3126

32-
public class FreshMark implements Compiler {
33-
static final Parser parser = new Parser("freshmark");
34-
35-
private final Map<String, ?> properties;
36-
private final Function<String, String> template;
37-
38-
public FreshMark(Map<String, ?> properties, Consumer<String> warningStream) {
39-
this.properties = properties;
40-
this.template = key -> {
41-
Object value = properties.get(key);
42-
if (value != null) {
43-
return Objects.toString(value);
44-
} else {
45-
warningStream.accept("Unknown key '" + key + "'");
46-
return key + "=UNKNOWN";
47-
}
48-
};
49-
}
50-
51-
/** Compiles the given document. */
52-
public String compile(String input) {
53-
return parser.compile(input, this);
54-
}
55-
56-
@Override
57-
public String compileSection(String section, String program, String input) {
58-
return Errors.rethrow().get(() -> {
59-
TypedScriptEngine engine = ScriptBox.create()
60-
.setAll(properties)
61-
.set("link").toFunc2(FreshMark::link)
62-
.set("image").toFunc2(FreshMark::image)
63-
.set("shield").toFunc4(FreshMark::shield)
64-
.set("prefixDelimiterReplace").toFunc4(FreshMark::prefixDelimiterReplace)
65-
.buildTyped(Language.nashorn());
66-
67-
// apply the templating engine to the program
68-
engine.getRaw().put("input", input);
69-
engine.eval(template(program, template));
70-
String compiled = engine.get("output", String.class);
71-
if (compiled.length() == 0) {
72-
compiled = "\n";
73-
} else {
74-
if (compiled.charAt(0) != '\n') {
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", "-->");
38+
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());
47+
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")) {
7558
compiled = "\n" + compiled;
7659
}
77-
if (compiled.charAt(compiled.length() - 1) != '\n') {
60+
if (!compiled.endsWith("\n")) {
7861
compiled = compiled + "\n";
7962
}
80-
}
81-
return parser.prefix + " " + section + "\n" +
82-
program +
83-
parser.postfix +
84-
compiled +
85-
parser.prefix + " /" + section + " " + parser.postfix;
86-
});
87-
}
88-
89-
/** Generates a markdown link. */
90-
static String link(String text, String url) {
91-
return "[" + text + "](" + url + ")";
92-
}
93-
94-
/** Generates a markdown image. */
95-
static String image(String altText, String url) {
96-
return "!" + link(altText, url);
97-
}
63+
return parser.prefix + " " + section + "\n" +
64+
program +
65+
parser.postfix +
66+
compiled +
67+
parser.prefix + " /" + section + " " + parser.postfix;
68+
});
69+
}
70+
};
9871

99-
/** Generates shields using <a href="http://shields.io/">shields.io</a>. */
100-
static String shield(String altText, String subject, String status, String color) {
101-
return image(altText, "https://img.shields.io/badge/" + shieldEscape(subject) + "-" + shieldEscape(status) + "-" + shieldEscape(color) + ".svg");
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);
10275
}
10376

104-
private static String shieldEscape(String raw) {
105-
return Errors.rethrow().get(() -> URLEncoder.encode(
106-
raw.replace("_", "__").replace("-", "--").replace(" ", "_"),
107-
StandardCharsets.UTF_8.name()));
108-
}
77+
/** For the given section, return the proper templated value for the given key. */
78+
protected abstract String keyToValue(String section, String key);
10979

110-
/** Replaces everything between the */
111-
static String prefixDelimiterReplace(String input, String prefix, String delimiter, String replacement) {
112-
StringBuilder builder = new StringBuilder(input.length() * 3 / 2);
113-
114-
int lastElement = 0;
115-
Pattern pattern = Pattern.compile("(.*?" + Pattern.quote(prefix) + ")(.*?)(" + Pattern.quote(delimiter) + ")", Pattern.DOTALL);
116-
Matcher matcher = pattern.matcher(input);
117-
while (matcher.find()) {
118-
builder.append(matcher.group(1));
119-
builder.append(replacement);
120-
builder.append(matcher.group(3));
121-
lastElement = matcher.end();
122-
}
123-
builder.append(input.substring(lastElement));
124-
return builder.toString();
125-
}
126-
127-
private static final Pattern TEMPLATE = Pattern.compile("(.*?)\\{\\{(.*?)\\}\\}", Pattern.DOTALL);
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);
12882

12983
/** Replaces whatever is inside of {@code &#123;&#123;key&#125;&#125;} tags using the {@code keyToValue} function. */
13084
static String template(String input, Function<String, String> keyToValue) {
@@ -140,4 +94,6 @@ static String template(String input, Function<String, String> keyToValue) {
14094
result.append(input.substring(lastElement));
14195
return result.toString();
14296
}
97+
98+
private static final Pattern TEMPLATE = Pattern.compile("(.*?)\\{\\{(.*?)\\}\\}", Pattern.DOTALL);
14399
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.net.URLEncoder;
19+
import java.nio.charset.StandardCharsets;
20+
import java.util.Map;
21+
import java.util.Objects;
22+
import java.util.function.Consumer;
23+
import java.util.regex.Matcher;
24+
import java.util.regex.Pattern;
25+
26+
import com.diffplug.common.base.Errors;
27+
import com.diffplug.jscriptbox.ScriptBox;
28+
29+
/** The defaault implementation. */
30+
public class FreshMarkDefault extends FreshMark {
31+
private final Map<String, ?> properties;
32+
private final Consumer<String> warningStream;
33+
34+
public FreshMarkDefault(Map<String, ?> properties, Consumer<String> warningStream) {
35+
this.properties = properties;
36+
this.warningStream = warningStream;
37+
}
38+
39+
@Override
40+
protected void setupScriptEngine(String section, ScriptBox scriptBox) {
41+
scriptBox.setAll(properties)
42+
.set("link").toFunc2(FreshMarkDefault::link)
43+
.set("image").toFunc2(FreshMarkDefault::image)
44+
.set("shield").toFunc4(FreshMarkDefault::shield)
45+
.set("prefixDelimiterReplace").toFunc4(FreshMarkDefault::prefixDelimiterReplace);
46+
}
47+
48+
@Override
49+
protected String keyToValue(String section, String key) {
50+
Object value = properties.get(key);
51+
if (value != null) {
52+
return Objects.toString(value);
53+
} else {
54+
warningStream.accept("Unknown key '" + key + "'");
55+
return key + "=UNKNOWN";
56+
}
57+
}
58+
59+
////////////////////////
60+
// built-in functions //
61+
////////////////////////
62+
/** Generates a markdown link. */
63+
static String link(String text, String url) {
64+
return "[" + text + "](" + url + ")";
65+
}
66+
67+
/** Generates a markdown image. */
68+
static String image(String altText, String url) {
69+
return "!" + link(altText, url);
70+
}
71+
72+
/** Generates shields using <a href="http://shields.io/">shields.io</a>. */
73+
static String shield(String altText, String subject, String status, String color) {
74+
return image(altText, "https://img.shields.io/badge/" + shieldEscape(subject) + "-" + shieldEscape(status) + "-" + shieldEscape(color) + ".svg");
75+
}
76+
77+
private static String shieldEscape(String raw) {
78+
return Errors.rethrow().get(() -> URLEncoder.encode(
79+
raw.replace("_", "__").replace("-", "--").replace(" ", "_"),
80+
StandardCharsets.UTF_8.name()));
81+
}
82+
83+
/** Replaces after prefix and before delimiter with replacement. */
84+
static String prefixDelimiterReplace(String input, String prefix, String delimiter, String replacement) {
85+
StringBuilder builder = new StringBuilder(input.length() * 3 / 2);
86+
int lastElement = 0;
87+
Pattern pattern = Pattern.compile("(.*?" + Pattern.quote(prefix) + ")(.*?)(" + Pattern.quote(delimiter) + ")", Pattern.DOTALL);
88+
Matcher matcher = pattern.matcher(input);
89+
while (matcher.find()) {
90+
builder.append(matcher.group(1));
91+
builder.append(replacement);
92+
builder.append(matcher.group(3));
93+
lastElement = matcher.end();
94+
}
95+
builder.append(input.substring(lastElement));
96+
return builder.toString();
97+
}
98+
}

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

+12-5
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919
import java.util.regex.Matcher;
2020
import java.util.regex.Pattern;
2121

22-
public class Parser {
22+
/** A format defined by "tag start" and "tag end" chunks of text. */
23+
class Parser {
2324
final String prefix, postfix;
2425
final Pattern pattern;
2526

26-
public Parser(String name) {
27-
prefix = "<!---" + name;
28-
postfix = "-->";
27+
Parser(String prefix, String postfix) {
28+
this.prefix = prefix;
29+
this.postfix = postfix;
2930
pattern = Pattern.compile(prefix + "(.*?)" + postfix, Pattern.DOTALL);
3031
}
3132

@@ -51,14 +52,20 @@ void bodyAndTags(String input, Consumer<String> body, Consumer<String> tag) {
5152
}
5253
}
5354

55+
/** Interface which can compile a single section of a FreshMark document. */
56+
@FunctionalInterface
57+
interface Compiler {
58+
String compileSection(String section, String program, String in);
59+
}
60+
5461
/**
5562
* Compiles an input string to an output string, using the given compiler to compile each section.
5663
*
5764
* @param fullInput the raw input string
5865
* @param compiler used to compile each section
5966
* @return the compiled output string
6067
*/
61-
public String compile(String fullInput, Compiler compiler) {
68+
String compile(String fullInput, Compiler compiler) {
6269
StringBuilder result = new StringBuilder(fullInput.length() * 3 / 2);
6370
/** Associates errors with the part of the input that caused it. */
6471
class ErrorFormatter {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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.ArrayList;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import org.junit.Assert;
24+
import org.junit.Test;
25+
26+
public class FreshMarkDefaultTest {
27+
@Test
28+
public void testPrefixDelimReplacement() {
29+
String before = TestResource.getTestResource("javadoc_before.txt");
30+
String after = TestResource.getTestResource("javadoc_after.txt");
31+
String afterActual = FreshMarkDefault.prefixDelimiterReplace(before, "https://diffplug.github.io/durian/javadoc/", "/", "4.0");
32+
Assert.assertEquals(after, afterActual);
33+
}
34+
35+
@Test
36+
public void testFull() {
37+
String before = TestResource.getTestResource("full_before.txt");
38+
String after = TestResource.getTestResource("full_after.txt");
39+
40+
Map<String, String> props = new HashMap<>();
41+
props.put("stable", "3.2.0");
42+
props.put("version", "3.3.0-SNAPSHOT");
43+
props.put("group", "com.diffplug.durian");
44+
props.put("name", "durian");
45+
props.put("org", "diffplug");
46+
List<String> warnings = new ArrayList<>();
47+
FreshMark freshmark = new FreshMarkDefault(props, warnings::add);
48+
String afterActual = freshmark.compile(before);
49+
Assert.assertEquals(after, afterActual);
50+
Assert.assertTrue(warnings.isEmpty());
51+
}
52+
}

0 commit comments

Comments
 (0)