Implement index-of expression (#1113)
This commit is contained in:
committed by
GitHub
parent
d716636d83
commit
379b1fc606
@@ -230,6 +230,7 @@ list(APPEND INCLUDE_FILES
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/image.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/image_expression.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/in.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/index_of.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/interpolate.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/interpolator.hpp
|
||||
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/is_constant.hpp
|
||||
@@ -630,6 +631,7 @@ list(APPEND SRC_FILES
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/image.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/image_expression.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/in.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/index_of.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/interpolate.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/is_constant.cpp
|
||||
${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/is_expression.cpp
|
||||
|
||||
@@ -178,7 +178,8 @@ enum class Kind : int32_t {
|
||||
ImageExpression,
|
||||
In,
|
||||
Within,
|
||||
Distance
|
||||
Distance,
|
||||
IndexOf
|
||||
};
|
||||
|
||||
class Expression {
|
||||
|
||||
41
include/mbgl/style/expression/index_of.hpp
Normal file
41
include/mbgl/style/expression/index_of.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <mbgl/style/expression/expression.hpp>
|
||||
#include <mbgl/style/conversion.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace mbgl {
|
||||
namespace style {
|
||||
namespace expression {
|
||||
|
||||
class IndexOf : public Expression {
|
||||
public:
|
||||
IndexOf(std::unique_ptr<Expression> keyword_,
|
||||
std::unique_ptr<Expression> input_,
|
||||
std::unique_ptr<Expression> fromIndex_);
|
||||
|
||||
static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx);
|
||||
|
||||
EvaluationResult evaluate(const EvaluationContext& params) const override;
|
||||
void eachChild(const std::function<void(const Expression&)>&) const override;
|
||||
|
||||
bool operator==(const Expression& e) const override;
|
||||
|
||||
std::vector<std::optional<Value>> possibleOutputs() const override;
|
||||
std::string getOperator() const override;
|
||||
|
||||
private:
|
||||
EvaluationResult evaluateForArrayInput(const std::vector<Value>& array,
|
||||
const Value& keyword,
|
||||
size_t fromIndex) const;
|
||||
EvaluationResult evaluateForStringInput(const std::string& string, const Value& keyword, size_t fromIndex) const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Expression> keyword;
|
||||
std::unique_ptr<Expression> input;
|
||||
std::unique_ptr<Expression> fromIndex;
|
||||
};
|
||||
|
||||
} // namespace expression
|
||||
} // namespace style
|
||||
} // namespace mbgl
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"expression": ["index-of", ["get", "i"], ["array", ["get", "arr"]]],
|
||||
"inputs": [
|
||||
[{}, {"properties": {"i": null, "arr": [9, 8, 7]}}],
|
||||
[{}, {"properties": {"i": null, "arr": [9, 8, 7, null]}}],
|
||||
[{}, {"properties": {"i": 1, "arr": [9, 8, 7]}}],
|
||||
[{}, {"properties": {"i": 9, "arr": [9, 8, 7, 9]}}],
|
||||
[{}, {"properties": {"i": 1, "arr": null}}]
|
||||
],
|
||||
"expected": {
|
||||
"compiled": {
|
||||
"result": "success",
|
||||
"isFeatureConstant": false,
|
||||
"isZoomConstant": true,
|
||||
"type": "number"
|
||||
},
|
||||
"serialized": ["index-of", ["get", "i"], ["array", ["get", "arr"]]],
|
||||
"outputs": [
|
||||
-1,
|
||||
3,
|
||||
-1,
|
||||
0,
|
||||
{"error": "Expected value to be of type array, but found null instead."}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"expression": ["index-of", ["get", "substr"], ["string", ["get", "str"]]],
|
||||
"inputs": [
|
||||
[{}, {"properties": {"substr": null, "str": "helloworld"}}],
|
||||
[{}, {"properties": {"substr": "foo", "str": "helloworld"}}],
|
||||
[{}, {"properties": {"substr": "low", "str": "helloworld"}}],
|
||||
[{}, {"properties": {"substr": "low", "str": null}}]
|
||||
],
|
||||
"expected": {
|
||||
"compiled": {
|
||||
"result": "success",
|
||||
"isFeatureConstant": false,
|
||||
"isZoomConstant": true,
|
||||
"type": "number"
|
||||
},
|
||||
"serialized": ["index-of", ["get", "substr"], ["string", ["get", "str"]]],
|
||||
"outputs": [
|
||||
-1,
|
||||
-1,
|
||||
3,
|
||||
{"error": "Expected value to be of type string, but found null instead."}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"expression": ["index-of", ["get", "i"], ["get", "arr"]],
|
||||
"inputs": [
|
||||
[{}, {"properties": {"i": null, "arr": [9, 8, 7]}}],
|
||||
[{}, {"properties": {"i": 1, "arr": [9, 8, 7]}}],
|
||||
[{}, {"properties": {"i": 9, "arr": [9, 8, 7]}}],
|
||||
[
|
||||
{},
|
||||
{
|
||||
"properties": {
|
||||
"i": "foo",
|
||||
"arr": ["baz", "bar", "hello", "foo", "world"]
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{},
|
||||
{
|
||||
"properties": {
|
||||
"i": true,
|
||||
"arr": ["foo", 123, null, 456, false, {}, true]
|
||||
}
|
||||
}
|
||||
],
|
||||
[{}, {"properties": {"i": 1, "arr": null}}]
|
||||
],
|
||||
"expected": {
|
||||
"compiled": {
|
||||
"result": "success",
|
||||
"isFeatureConstant": false,
|
||||
"isZoomConstant": true,
|
||||
"type": "number"
|
||||
},
|
||||
"serialized": ["index-of", ["get", "i"], ["get", "arr"]],
|
||||
"outputs": [
|
||||
-1,
|
||||
-1,
|
||||
0,
|
||||
3,
|
||||
6,
|
||||
{
|
||||
"error": "Expected second argument to be of type array or string, but found null instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"expression": ["index-of", ["get", "substr"], ["get", "str"]],
|
||||
"inputs": [
|
||||
[{}, {"properties": {"substr": null, "str": "helloworld"}}],
|
||||
[{}, {"properties": {"substr": "foo", "str": "helloworld"}}],
|
||||
[{}, {"properties": {"substr": "low", "str": "helloworld"}}],
|
||||
[{}, {"properties": {"substr": true, "str": "falsetrue"}}],
|
||||
[{}, {"properties": {"substr": false, "str": "falsetrue"}}],
|
||||
[{}, {"properties": {"substr": 123, "str": "hello123world"}}],
|
||||
[{}, {"properties": {"substr": "low", "str": null}}]
|
||||
],
|
||||
"expected": {
|
||||
"compiled": {
|
||||
"result": "success",
|
||||
"isFeatureConstant": false,
|
||||
"isZoomConstant": true,
|
||||
"type": "number"
|
||||
},
|
||||
"serialized": ["index-of", ["get", "substr"], ["get", "str"]],
|
||||
"outputs": [
|
||||
-1,
|
||||
-1,
|
||||
3,
|
||||
5,
|
||||
0,
|
||||
5,
|
||||
{
|
||||
"error": "Expected second argument to be of type array or string, but found null instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"expression": ["index-of", ["get", "needle"], ["get", "haystack"]],
|
||||
"inputs": [
|
||||
[{}, {"properties": {"needle": 1, "haystack": 123}}],
|
||||
[{}, {"properties": {"needle": "foo", "haystack": {}}}],
|
||||
[{}, {"properties": {"needle": "foo", "haystack": null}}]
|
||||
],
|
||||
"expected": {
|
||||
"compiled": {
|
||||
"result": "success",
|
||||
"isFeatureConstant": false,
|
||||
"isZoomConstant": true,
|
||||
"type": "number"
|
||||
},
|
||||
"serialized": ["index-of", ["get", "needle"], ["get", "haystack"]],
|
||||
"outputs": [
|
||||
{
|
||||
"error": "Expected second argument to be of type array or string, but found number instead."
|
||||
},
|
||||
{
|
||||
"error": "Expected second argument to be of type array or string, but found object instead."
|
||||
},
|
||||
{
|
||||
"error": "Expected second argument to be of type array or string, but found null instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"expression": ["index-of", ["get", "needle"], ["get", "haystack"]],
|
||||
"inputs": [
|
||||
[{}, {"properties": {"needle": {}, "haystack": [9, 8, 7]}}],
|
||||
[{}, {"properties": {"needle": {}, "haystack": "helloworld"}}]
|
||||
],
|
||||
"expected": {
|
||||
"compiled": {
|
||||
"result": "success",
|
||||
"isFeatureConstant": false,
|
||||
"isZoomConstant": true,
|
||||
"type": "number"
|
||||
},
|
||||
"serialized": ["index-of", ["get", "needle"], ["get", "haystack"]],
|
||||
"outputs": [
|
||||
{
|
||||
"error": "Expected first argument to be of type boolean, string, number or null, but found object instead."
|
||||
},
|
||||
{
|
||||
"error": "Expected first argument to be of type boolean, string, number or null, but found object instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"expression": ["index-of", ["get", "needle"], ["get", "hay"], ["get", "i"]],
|
||||
"inputs": [
|
||||
[{}, {"properties": {"needle": null, "hay": "helloworld", "i": 0}}],
|
||||
[{}, {"properties": {"needle": "foo", "hay": "helloworld", "i": 0}}],
|
||||
[{}, {"properties": {"needle": "low", "hay": "helloworldlow", "i": 4}}],
|
||||
[{}, {"properties": {"needle": true, "hay": "falsetruetrue", "i": 6}}],
|
||||
[{}, {"properties": {"needle": false, "hay": "falsetrue", "i": 0}}],
|
||||
[{}, {"properties": {"needle": 123, "hay": "hello123world", "i": 6}}],
|
||||
[{}, {"properties": {"needle": "low", "hay": null, "i": 0}}],
|
||||
[{}, {"properties": {"needle": 7, "hay": [9, 8, 7, 8, 7, 7], "i": 3}}],
|
||||
[{}, {"properties": {"needle": 9, "hay": [9, 8, 7, 8, 7, 7], "i": 1}}],
|
||||
[{}, {"properties": {"needle": 8, "hay": [9, 8, 7, 8, 7, 7], "i": 1}}],
|
||||
[{}, {"properties": {"needle": 8, "hay": [9, 8, 7, 8, 7, 7], "i": 10000}}],
|
||||
[{}, {"properties": {"needle": 8, "hay": [9, 8, 7, 8, 7, 7], "i": "wrong"}}]
|
||||
],
|
||||
"expected": {
|
||||
"compiled": {
|
||||
"result": "success",
|
||||
"isFeatureConstant": false,
|
||||
"isZoomConstant": true,
|
||||
"type": "number"
|
||||
},
|
||||
"serialized": ["index-of", ["get", "needle"], ["get", "hay"], ["get", "i"]],
|
||||
"outputs": [
|
||||
-1,
|
||||
-1,
|
||||
10,
|
||||
9,
|
||||
0,
|
||||
-1,
|
||||
{
|
||||
"error": "Expected second argument to be of type array or string, but found null instead."
|
||||
},
|
||||
4,
|
||||
-1,
|
||||
1,
|
||||
-1,
|
||||
{
|
||||
"error": "Expected third argument to be of type number, but found string instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ MapLibre welcomes participation and contributions from everyone. Please read [`C
|
||||
### ✨ Features and improvements
|
||||
|
||||
* Change to a more natural fling animation and allow setting `flingThreshold` and `flingAnimationBaseTime` in `UiSettings` ([#963](https://github.com/maplibre/maplibre-native/pull/963))
|
||||
* Add support for [index-of expression](https://maplibre.org/maplibre-style-spec/expressions/#index-of) ([#1113](https://github.com/maplibre/maplibre-native/pull/1113))
|
||||
|
||||
### 🐞 Bug fixes
|
||||
|
||||
|
||||
@@ -1552,6 +1552,31 @@ public class Expression {
|
||||
return new Expression("in", needle, haystack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first position at which a `needle` can be found in a `haystack`.
|
||||
*
|
||||
* @param needle the item expression
|
||||
* @param haystack the array or string expression
|
||||
* @return position in the array or string or -1 if not found.
|
||||
* @see <a href="https://maplibre.org/maplibre-gl-js-docs/style-spec/#index-of">Style specification</a>
|
||||
*/
|
||||
public static Expression indexOf(@NonNull Expression keyword, @NonNull Expression input) {
|
||||
return new Expression("index-of", keyword, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first position at which a `needle` can be found in a `haystack`.
|
||||
*
|
||||
* @param needle the item expression
|
||||
* @param haystack the array or string expression
|
||||
* @param fromIndex the index to start searching from
|
||||
* @return position in the array or string or -1 if not found.
|
||||
* @see <a href="https://maplibre.org/maplibre-gl-js-docs/style-spec/#index-of">Style specification</a>
|
||||
*/
|
||||
public static Expression indexOf(@NonNull Expression keyword, @NonNull Expression input, @NonNull Expression fromIndex) {
|
||||
return new Expression("index-of", keyword, input, fromIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves whether an item exists in an array or a substring exists in a string.
|
||||
*
|
||||
|
||||
@@ -455,6 +455,27 @@ class ExpressionTest {
|
||||
Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testIndexOf() {
|
||||
val expected = arrayOf<Any>("index-of", 2f, arrayOf<Any>("literal", arrayOf<Any>(1f, 2f, 3f)))
|
||||
val actual =
|
||||
Expression.indexOf(Expression.literal(2f), Expression.literal(arrayOf<Any>(1f, 2f, 3f)))
|
||||
.toArray()
|
||||
Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testIndexOfWithFromIndex() {
|
||||
val expected = arrayOf<Any>("index-of", 2f, arrayOf<Any>("literal", arrayOf<Any>(1f, 2f, 3f)), 1f)
|
||||
val actual =
|
||||
Expression.indexOf(Expression.literal(2f), Expression.literal(arrayOf<Any>(1f, 2f, 3f)), Expression.literal(1f))
|
||||
.toArray()
|
||||
Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual))
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testInNumber() {
|
||||
|
||||
138
src/mbgl/style/expression/index_of.cpp
Normal file
138
src/mbgl/style/expression/index_of.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include "mbgl/style/expression/expression.hpp"
|
||||
#include <mbgl/style/conversion_impl.hpp>
|
||||
#include <mbgl/style/expression/index_of.hpp>
|
||||
#include <mbgl/util/string.hpp>
|
||||
#include <iostream>
|
||||
namespace mbgl {
|
||||
namespace style {
|
||||
namespace expression {
|
||||
|
||||
IndexOf::IndexOf(std::unique_ptr<Expression> keyword_,
|
||||
std::unique_ptr<Expression> input_,
|
||||
std::unique_ptr<Expression> fromIndex_)
|
||||
: Expression(Kind::IndexOf, type::Number),
|
||||
keyword(std::move(keyword_)),
|
||||
input(std::move(input_)),
|
||||
fromIndex(std::move(fromIndex_)) {}
|
||||
|
||||
EvaluationResult IndexOf::evaluate(const EvaluationContext ¶ms) const {
|
||||
const EvaluationResult evaluatedKeyword = keyword->evaluate(params);
|
||||
const EvaluationResult evaluatedInput = input->evaluate(params);
|
||||
if (!evaluatedKeyword) {
|
||||
return evaluatedKeyword.error();
|
||||
}
|
||||
if (!evaluatedInput) {
|
||||
return evaluatedInput.error();
|
||||
}
|
||||
|
||||
if (!(evaluatedKeyword->is<double>() || evaluatedKeyword->is<std::string>() || evaluatedKeyword->is<bool>() ||
|
||||
evaluatedKeyword->is<NullValue>())) {
|
||||
return EvaluationError{"Expected first argument to be of type boolean, string, number or null, but found " +
|
||||
toString(typeOf(*evaluatedKeyword)) + " instead."};
|
||||
}
|
||||
|
||||
size_t fromIndexValue = 0;
|
||||
if (fromIndex) {
|
||||
const EvaluationResult evaluatedFromIndex = fromIndex->evaluate(params);
|
||||
if (!evaluatedFromIndex) {
|
||||
return evaluatedFromIndex.error();
|
||||
}
|
||||
if (!evaluatedFromIndex->is<double>()) {
|
||||
return EvaluationError{"Expected third argument to be of type number, but found " +
|
||||
toString(typeOf(*evaluatedFromIndex)) + " instead."};
|
||||
}
|
||||
fromIndexValue = static_cast<size_t>(evaluatedFromIndex->get<double>());
|
||||
}
|
||||
|
||||
return evaluatedInput->match(
|
||||
[&](const std::string &s) { return evaluateForStringInput(s, *evaluatedKeyword, fromIndexValue); },
|
||||
[&](const std::vector<Value> &v) { return evaluateForArrayInput(v, *evaluatedKeyword, fromIndexValue); },
|
||||
[&](const auto &) -> EvaluationResult {
|
||||
return EvaluationError{"Expected second argument to be of type array or string, but found " +
|
||||
toString(typeOf(*evaluatedInput)) + " instead."};
|
||||
});
|
||||
}
|
||||
|
||||
EvaluationResult IndexOf::evaluateForArrayInput(const std::vector<Value> &array,
|
||||
const Value &keywordValue,
|
||||
size_t fromIndexValue) const {
|
||||
for (size_t index = fromIndexValue; index < array.size(); ++index) {
|
||||
if (array[index] == keywordValue) {
|
||||
return static_cast<double>(index);
|
||||
}
|
||||
}
|
||||
return static_cast<double>(-1);
|
||||
}
|
||||
|
||||
EvaluationResult IndexOf::evaluateForStringInput(const std::string &string,
|
||||
const Value &keywordValue,
|
||||
size_t fromIndexValue) const {
|
||||
std::string keywordString = keywordValue.match(
|
||||
[](const std::string &s) { return s; },
|
||||
[](bool b) { return b ? std::string{"true"} : std::string{"false"}; },
|
||||
[](NullValue) { return std::string{"null"}; },
|
||||
[](double n) { return util::toString(n); },
|
||||
[](const auto &) -> std::string {
|
||||
// it should be impossible to get here - we do validation in evaluate()
|
||||
assert(false);
|
||||
return "";
|
||||
});
|
||||
size_t index = string.find(keywordString, fromIndexValue);
|
||||
if (index == std::string::npos) {
|
||||
return static_cast<double>(-1);
|
||||
}
|
||||
return static_cast<double>(index);
|
||||
}
|
||||
|
||||
void IndexOf::eachChild(const std::function<void(const Expression &)> &visit) const {
|
||||
visit(*keyword);
|
||||
visit(*input);
|
||||
if (fromIndex) {
|
||||
visit(*fromIndex);
|
||||
}
|
||||
}
|
||||
|
||||
using namespace mbgl::style::conversion;
|
||||
ParseResult IndexOf::parse(const Convertible &value, ParsingContext &ctx) {
|
||||
assert(isArray(value));
|
||||
|
||||
std::size_t length = arrayLength(value);
|
||||
if (length != 3 && length != 4) {
|
||||
ctx.error("Expected 2 or 3 arguments, but found " + util::toString(length - 1) + " instead.");
|
||||
return ParseResult();
|
||||
}
|
||||
|
||||
ParseResult keyword = ctx.parse(arrayMember(value, 1), 1);
|
||||
ParseResult input = ctx.parse(arrayMember(value, 2), 2);
|
||||
|
||||
ParseResult fromIndex = length == 4 ? ctx.parse(arrayMember(value, 3), 3) : ParseResult();
|
||||
|
||||
if (!keyword || !input) {
|
||||
return ParseResult();
|
||||
}
|
||||
|
||||
return ParseResult(
|
||||
std::make_unique<IndexOf>(std::move(*keyword), std::move(*input), fromIndex ? std::move(*fromIndex) : nullptr));
|
||||
}
|
||||
|
||||
bool IndexOf::operator==(const Expression &e) const {
|
||||
if (e.getKind() == Kind::IndexOf) {
|
||||
auto rhs = static_cast<const IndexOf *>(&e);
|
||||
const auto fromIndexEqual = (fromIndex && rhs->fromIndex && *fromIndex == *(rhs->fromIndex)) ||
|
||||
(!fromIndex && !rhs->fromIndex);
|
||||
return *keyword == *(rhs->keyword) && *input == *(rhs->input) && fromIndexEqual;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::optional<Value>> IndexOf::possibleOutputs() const {
|
||||
return {std::nullopt};
|
||||
}
|
||||
|
||||
std::string IndexOf::getOperator() const {
|
||||
return "index-of";
|
||||
}
|
||||
|
||||
} // namespace expression
|
||||
} // namespace style
|
||||
} // namespace mbgl
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <mbgl/style/expression/number_format.hpp>
|
||||
#include <mbgl/style/expression/step.hpp>
|
||||
#include <mbgl/style/expression/within.hpp>
|
||||
#include <mbgl/style/expression/index_of.hpp>
|
||||
|
||||
#include <mbgl/style/expression/find_zoom_curve.hpp>
|
||||
#include <mbgl/style/expression/dsl.hpp>
|
||||
@@ -139,6 +140,7 @@ MAPBOX_ETERNAL_CONSTEXPR const auto expressionRegistry =
|
||||
{"to-string", Coercion::parse},
|
||||
{"var", Var::parse},
|
||||
{"within", Within::parse},
|
||||
{"index-of", IndexOf::parse},
|
||||
});
|
||||
|
||||
bool isExpression(const std::string& name) {
|
||||
|
||||
Reference in New Issue
Block a user