Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.IllegalFormatConversionException;
import java.util.List;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
Expand Down Expand Up @@ -65,6 +64,7 @@ private StringFunctions() {
"getGroupStart",
"getGroupEnd",
"getGroupCount",
"getGroupNames",
"encode",
"decode",
"startsWith",
Expand Down Expand Up @@ -310,6 +310,16 @@ public Object childEvaluate(
sb.append("match.").append(parameters.get(0)).append(".groupCount");
return resolver.getVariable(sb.toString());
}
if (functionName.equalsIgnoreCase("getGroupNames")) {
if (parameters.size() < 1) {
throw new ParserException(
I18N.getText(
"macro.function.general.notEnoughParam", functionName, 1, parameters.size()));
}
StringBuilder sb = new StringBuilder();
sb.append("match.").append(parameters.get(0)).append(".groupNames");
return resolver.getVariable(sb.toString());
}
if (functionName.startsWith("getGroup")) {
if (parameters.size() < 3) {
throw new ParserException(
Expand Down Expand Up @@ -635,12 +645,19 @@ public String replace(String string, String pattern, String replacement) {
* @param pattern The pattern to match.
* @return The number of matches that were found
* @throws ParserException
* <p>Variables that are set in the variable resolver. match.groupCount = The number of
* capture groups in the pattern. {matchNo} is a sequence used to differentiate different
* calls to strfind match.{matchNo}.matchCount = The number of matches found.
* match.{matchNo}.m{M}.group{G} = The matching string for Match {M} and Group number {G}.
* match.{matchNo}.m{M}.group{G}.start = The start of Group number {G} in Match Number {M}
* match.{matchNo}.m{M}.group{G}.end = The end of Group number {G} in Match Number {M}
* <p><hr>Variables that are set in the variable resolver:
* <ul>
* <li>match.groupCount = The number of capture groups in the pattern.
* <li>match.groupNames = A Json array of named capture groups in the order they were
* defined.
* <li>{matchNo} is a sequence used to differentiate different calls to strfind
* <li>match.{matchNo}.matchCount = The number of matches found.
* <li>match.{matchNo}.m{M}.group{G} = The matching string for Match {M} and Group
* number/name {G}.
* <li>match.{matchNo}.m{M}.group{G}.start = The start of Group number/name {G} in Match
* Number {M}
* <li>match.{matchNo}.m{M}.group{G}.end = The end of Group number/name {G} in Match Number
* {M}
*/
public BigDecimal stringFind(VariableResolver resolver, String str, String pattern)
throws ParserException {
Expand All @@ -650,19 +667,35 @@ public BigDecimal stringFind(VariableResolver resolver, String str, String patte

int matchId = nextMatchNo();
resolver.setVariable("match." + matchId + ".groupCount", m.groupCount());
resolver.setVariable("match." + matchId + ".groupNames", m.namedGroups());

// create a reversed map to make subsequent lookups easier
Map<Integer, String> groupIndexToName = new HashMap<>();
p.namedGroups().forEach((name, index) -> groupIndexToName.put(index, name));

String baseKey = "";
while (m.find()) {
found++;
for (int i = 1; i < m.groupCount() + 1; i++) {
resolver.setVariable(
"match." + matchId + ".m" + found + ".group" + i, m.group(i) == null ? "" : m.group(i));
resolver.setVariable(
"match." + matchId + ".m" + found + ".group" + i + ".start", m.start(i));
resolver.setVariable("match." + matchId + ".m" + found + ".group" + i + ".end", m.end(i));
}
resolver.setVariable(
"match." + matchId + ".m" + found + ".group0", m.group() == null ? "" : m.group());
resolver.setVariable("match." + matchId + ".m" + found + ".group0.start", m.start());
resolver.setVariable("match." + matchId + ".m" + found + ".group0.end", m.end());
for (int i = 1; i <= m.groupCount(); i++) {
String value = m.group(i) == null ? "" : m.group(i);
// by group index
baseKey = String.format("match.%s.m%d.group%d", matchId, found, i);
resolver.setVariable(baseKey, value);
resolver.setVariable(baseKey + ".start", m.start(i));
resolver.setVariable(baseKey + ".end", m.end(i));
// by group name
String groupName = groupIndexToName.get(i);
if (groupName != null) {
baseKey = String.format("match.%s.m%d.group%s", matchId, found, groupName);
resolver.setVariable(baseKey, value);
resolver.setVariable(baseKey + ".start", m.start(i));
resolver.setVariable(baseKey + ".end", m.end(i));
}
}
baseKey = String.format("match.%s.m%d.group0", matchId, found);
resolver.setVariable(baseKey, m.group() == null ? "" : m.group());
resolver.setVariable(baseKey + ".start", m.start());
resolver.setVariable(baseKey + ".end", m.end());
}
resolver.setVariable("match." + matchId + ".matchCount", found);
return BigDecimal.valueOf(matchId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.rptools.dicelib.expression.ExpressionParser;
import net.rptools.dicelib.expression.Result;
import net.rptools.maptool.client.MapToolVariableResolver;
Expand Down Expand Up @@ -138,7 +139,8 @@ private JSONMacroFunctions() {
"json.removeFirst",
"json.rolls",
"json.objrolls",
"json.toHtmlTable");
"json.toHtmlTable",
"json.fromStrFind");

typeConversion = new JsonMTSTypeConversion();
jsonArrayFunctions = new JsonArrayFunctions(typeConversion);
Expand Down Expand Up @@ -551,6 +553,24 @@ public Object childEvaluate(

return jsonHtmlFunctions.jsonToHtmlTable(functionName, json, options);
}
case "json.fromStrFind":
{
// args: string, pattern [, group0Key [, returnUnnamedGroups [, returnStringValues]]]
FunctionUtil.checkNumberParam(functionName, args, 2, 5);
String group0Key =
args.size() > 2 ? FunctionUtil.paramAsString(functionName, args, 2, false) : "0";
boolean returnUnnamedGroups =
args.size() > 3 ? FunctionUtil.getBooleanValue(args.get(3)) : false;
boolean returnStringValues =
args.size() > 4 ? FunctionUtil.getBooleanValue(args.get(4)) : false;
return jsonFromStringFind(
functionName,
args.get(0).toString(),
args.get(1).toString(),
group0Key,
returnUnnamedGroups,
returnStringValues);
}
}
throw new ParserException(I18N.getText("macro.function.general.unknownFunction", functionName));
}
Expand Down Expand Up @@ -787,6 +807,114 @@ public String jsonIndent(JsonElement json, int indent) {
}
}

/**
* Matches the pattern against the input string and returns a Json array of Json objects with
* capture groups as keys and their matches as values.
*
* @param str The string to match the pattern against.
* @param pattern The pattern to match.
* @param group0Key Return group 0 matches against this key. The key cannot be the same as a named
* capture group used in the {@code pattern} nor a positive integer to avoid Json object key
* clashes. If {@code group0Key} is blank any group 0 matches will not be returned.
* Default={@code "0"}.
* @param returnUnnamedGroups Whether to return unnamed capture groups ({@code true}) or not
* ({@code false}) alongside named capture groups. If returned, unnamed capture groups use
* their group index as the key. Default={@code false} N.B. while group 0 does not and cannot
* have a capture group name, its return is controlled separately by the {@code group0Key}
* parameter.
* @param returnStringValues Whether to return all matched values as strings ({@code true}), or
* convert to primitives ({@code false}). Default={@code false} so for example a match of "10"
* would be returned as 10.
* @return A Json array containing objects for each match.
* @throws ParserException The parser exception.
*/
public String jsonFromStringFind(
String functionName,
String str,
String pattern,
String group0Key,
boolean returnUnnamedGroups,
boolean returnStringValues)
throws ParserException {
Pattern p = Pattern.compile(pattern);

// if we have been provided a key for group 0, check it is valid to use as a key
group0Key = group0Key.trim();
if (!group0Key.isEmpty()) {
// 1. check if the group0Key does not clash with a named capture group present in the regex
// pattern
if (p.namedGroups().containsKey(group0Key)) {
throw new ParserException(
I18N.getText(
"macro.function.jsonFromStrFind.group0KeyNameClash", group0Key, functionName));
}
try {
// 2. check if an integer has been provided and if so only allow <= 0
// this is to avoid potential clashes with indexed capture group keys
if (Integer.parseInt(group0Key) > 0) {
throw new ParserException(
I18N.getText(
"macro.function.jsonFromStrFind.group0KeyIndexClash", group0Key, functionName));
}
} catch (NumberFormatException e) {
// any non-integer is ok
}
}
Matcher m = p.matcher(str);

// create a reversed map of the namedGroups to group index to make subsequent lookups easier
Map<Integer, String> groupIndexToName = new HashMap<>();
p.namedGroups().forEach((name, index) -> groupIndexToName.put(index, name));

JsonArray jsonArrayMatches = new JsonArray();
while (m.find()) {
// create a new object for each match
JsonObject jsonObjectMatch = new JsonObject();

// only return group 0 when the key != ""
if (!group0Key.isEmpty()) {
if (returnStringValues) {
jsonObjectMatch.addProperty(group0Key, m.group(0));
} else {
jsonObjectMatch.add(group0Key, typeConversion.convertPrimitiveFromString(m.group(0)));
}
}

// add a key for each group match
for (int i = 1; i <= m.groupCount(); i++) {
String value = m.group(i);
if (value == null) {
// skip if there was no match for this group
continue;
}

String groupName = groupIndexToName.get(i);
if (groupName != null) {
// add the capture group by name
if (returnStringValues) {
jsonObjectMatch.addProperty(groupName, value);
} else {
jsonObjectMatch.add(groupName, typeConversion.convertPrimitiveFromString(value));
}
} else {
if (returnUnnamedGroups) {
// add the capture group by index
if (returnStringValues) {
jsonObjectMatch.addProperty(Integer.toString(i), value);
} else {
jsonObjectMatch.add(
Integer.toString(i), typeConversion.convertPrimitiveFromString(value));
}
}
}
}
if (!jsonObjectMatch.isEmpty()) {
jsonArrayMatches.add(jsonObjectMatch);
}
}
return new Gson().toJson(jsonArrayMatches);
}

/**
* This method calls the required functions to perform the json.path.* MT Script functions.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2179,6 +2179,8 @@ macro.function.jsonhtml.onlyArray = Got {0} but the option "{1}
macro.function.jsonhtml.onlyString = Got {0} but the option "{1}" in function "{2}" only accepts a string.
macro.function.jsonhtml.invalidSortMethod = Got {0} but the option "{1}" in function "{2}" only accepts strings starting with "a", "d", "r", "n" representing ascending, descending, reverse, or none, respectively.
macro.function.jsonhtml.exceededDepthLimit = The "DEPTHLIMIT" option of {0} was exceeded at a json path depth of {1}.
macro.function.jsonFromStrFind.group0KeyNameClash = Got the group 0 key "{0}" in function "{1}", but it clashes with a named capture group. Rename one or the other.
macro.function.jsonFromStrFind.group0KeyIndexClash = Got the group 0 key "{0}" in function "{1}", but it cannot be a positive integer. Rename it.
macro.function.macroLink.arguments = Arguments
# Informational messages for Macro functions.
macro.function.macroLink.autoExecToolTip = Auto Execute Macro Link
Expand Down
Loading