mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-15 19:22:26 +08:00
feat(selector): ~= regex
This commit is contained in:
parent
a5d2040e5c
commit
a085efd013
|
@ -131,33 +131,33 @@ val getChildren: (AccessibilityNodeInfo) -> Sequence<AccessibilityNodeInfo> = {
|
|||
|
||||
val allowPropertyNames by lazy {
|
||||
mapOf(
|
||||
"id" to PrimitiveValue.StringValue.type,
|
||||
"vid" to PrimitiveValue.StringValue.type,
|
||||
"id" to PrimitiveValue.StringValue.TYPE_NAME,
|
||||
"vid" to PrimitiveValue.StringValue.TYPE_NAME,
|
||||
|
||||
"name" to PrimitiveValue.StringValue.type,
|
||||
"text" to PrimitiveValue.StringValue.type,
|
||||
"text.length" to PrimitiveValue.IntValue.type,
|
||||
"desc" to PrimitiveValue.StringValue.type,
|
||||
"desc.length" to PrimitiveValue.IntValue.type,
|
||||
"name" to PrimitiveValue.StringValue.TYPE_NAME,
|
||||
"text" to PrimitiveValue.StringValue.TYPE_NAME,
|
||||
"text.length" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
"desc" to PrimitiveValue.StringValue.TYPE_NAME,
|
||||
"desc.length" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
|
||||
"clickable" to PrimitiveValue.BooleanValue.type,
|
||||
"focusable" to PrimitiveValue.BooleanValue.type,
|
||||
"checkable" to PrimitiveValue.BooleanValue.type,
|
||||
"checked" to PrimitiveValue.BooleanValue.type,
|
||||
"editable" to PrimitiveValue.BooleanValue.type,
|
||||
"longClickable" to PrimitiveValue.BooleanValue.type,
|
||||
"visibleToUser" to PrimitiveValue.BooleanValue.type,
|
||||
"clickable" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
||||
"focusable" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
||||
"checkable" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
||||
"checked" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
||||
"editable" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
||||
"longClickable" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
||||
"visibleToUser" to PrimitiveValue.BooleanValue.TYPE_NAME,
|
||||
|
||||
"left" to PrimitiveValue.IntValue.type,
|
||||
"top" to PrimitiveValue.IntValue.type,
|
||||
"right" to PrimitiveValue.IntValue.type,
|
||||
"bottom" to PrimitiveValue.IntValue.type,
|
||||
"width" to PrimitiveValue.IntValue.type,
|
||||
"height" to PrimitiveValue.IntValue.type,
|
||||
"left" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
"top" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
"right" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
"bottom" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
"width" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
"height" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
|
||||
"index" to PrimitiveValue.IntValue.type,
|
||||
"depth" to PrimitiveValue.IntValue.type,
|
||||
"childCount" to PrimitiveValue.IntValue.type,
|
||||
"index" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
"depth" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
"childCount" to PrimitiveValue.IntValue.TYPE_NAME,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -289,7 +289,8 @@ fun createCacheTransform(): CacheTransform {
|
|||
getBeforeBrothers = { node, connectExpression ->
|
||||
sequence {
|
||||
val parentVal = node.parent ?: return@sequence
|
||||
val index = indexCache[node] // 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 indexCache 是空
|
||||
val index =
|
||||
indexCache[node] // 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 indexCache 是空
|
||||
if (index != null) {
|
||||
var i = index - 1
|
||||
var offset = 0
|
||||
|
@ -305,7 +306,8 @@ fun createCacheTransform(): CacheTransform {
|
|||
offset++
|
||||
}
|
||||
} else {
|
||||
val list = getChildrenCache(parentVal).takeWhile { it != node }.toMutableList()
|
||||
val list =
|
||||
getChildrenCache(parentVal).takeWhile { it != node }.toMutableList()
|
||||
list.reverse()
|
||||
yieldAll(list.filterIndexed { i, _ ->
|
||||
connectExpression.checkOffset(
|
||||
|
|
|
@ -7,6 +7,7 @@ data class GkdSyntaxError internal constructor(
|
|||
val expectedValue: String,
|
||||
val position: Int,
|
||||
val source: String,
|
||||
override val cause: Exception? = null
|
||||
) : Exception(
|
||||
"expected $expectedValue in selector at position $position, but got ${
|
||||
source.getOrNull(
|
||||
|
@ -26,6 +27,11 @@ internal fun gkdAssert(
|
|||
}
|
||||
}
|
||||
|
||||
internal fun gkdError(source: String, offset: Int, expectedValue: String = ""): Nothing {
|
||||
throw GkdSyntaxError(expectedValue, offset, source)
|
||||
internal fun gkdError(
|
||||
source: String,
|
||||
offset: Int,
|
||||
expectedValue: String = "",
|
||||
cause: Exception? = null
|
||||
): Nothing {
|
||||
throw GkdSyntaxError(expectedValue, offset, source, cause)
|
||||
}
|
|
@ -8,7 +8,7 @@ data class BinaryExpression(
|
|||
val value: PrimitiveValue
|
||||
) : Expression() {
|
||||
override fun <T> match(node: T, transform: Transform<T>) =
|
||||
operator.compare(transform.getAttr(node, name), value.value)
|
||||
operator.compare(transform.getAttr(node, name), value)
|
||||
|
||||
override val binaryExpressions
|
||||
get() = arrayOf(this)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package li.songe.selector.data
|
||||
|
||||
sealed class CompareOperator(val key: String) {
|
||||
abstract fun compare(left: Any?, right: Any?): Boolean
|
||||
abstract fun compare(left: Any?, right: PrimitiveValue): Boolean
|
||||
abstract fun allowType(type: PrimitiveValue): Boolean
|
||||
|
||||
companion object {
|
||||
|
@ -10,7 +10,18 @@ sealed class CompareOperator(val key: String) {
|
|||
listOf(
|
||||
Equal,
|
||||
NotEqual,
|
||||
Start, NotStart, Include, NotInclude, End, NotEnd, Less, LessEqual, More, MoreEqual
|
||||
Start,
|
||||
NotStart,
|
||||
Include,
|
||||
NotInclude,
|
||||
End,
|
||||
NotEnd,
|
||||
Less,
|
||||
LessEqual,
|
||||
More,
|
||||
MoreEqual,
|
||||
Matches,
|
||||
NotMatches
|
||||
).sortedBy { -it.key.length }.toTypedArray()
|
||||
}
|
||||
|
||||
|
@ -28,11 +39,11 @@ sealed class CompareOperator(val key: String) {
|
|||
}
|
||||
|
||||
data object Equal : CompareOperator("=") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is CharSequence && right is CharSequence) {
|
||||
left.contentReversedEquals(right)
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
||||
left.contentReversedEquals(right.value)
|
||||
} else {
|
||||
left == right
|
||||
left == right.value
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,61 +51,87 @@ sealed class CompareOperator(val key: String) {
|
|||
}
|
||||
|
||||
data object NotEqual : CompareOperator("!=") {
|
||||
override fun compare(left: Any?, right: Any?) = !Equal.compare(left, right)
|
||||
override fun compare(left: Any?, right: PrimitiveValue) = !Equal.compare(left, right)
|
||||
override fun allowType(type: PrimitiveValue) = true
|
||||
}
|
||||
|
||||
data object Start : CompareOperator("^=") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is CharSequence && right is CharSequence) left.startsWith(right) else false
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
||||
left.startsWith(right.value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
||||
}
|
||||
|
||||
data object NotStart : CompareOperator("!^=") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is CharSequence && right is CharSequence) !left.startsWith(right) else false
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
||||
!left.startsWith(right.value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
||||
}
|
||||
|
||||
data object Include : CompareOperator("*=") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is CharSequence && right is CharSequence) left.contains(right) else false
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
||||
left.contains(right.value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
||||
}
|
||||
|
||||
data object NotInclude : CompareOperator("!*=") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is CharSequence && right is CharSequence) !left.contains(right) else false
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
||||
!left.contains(right.value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
||||
}
|
||||
|
||||
data object End : CompareOperator("$=") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is CharSequence && right is CharSequence) left.endsWith(right) else false
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
||||
left.endsWith(right.value)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
||||
}
|
||||
|
||||
data object NotEnd : CompareOperator("!$=") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is CharSequence && right is CharSequence) !left.endsWith(right) else false
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
||||
!left.endsWith(
|
||||
right.value
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.StringValue
|
||||
}
|
||||
|
||||
data object Less : CompareOperator("<") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is Int && right is Int) left < right else false
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is Int && right is PrimitiveValue.IntValue) left < right.value else false
|
||||
}
|
||||
|
||||
|
||||
|
@ -102,29 +139,56 @@ sealed class CompareOperator(val key: String) {
|
|||
}
|
||||
|
||||
data object LessEqual : CompareOperator("<=") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is Int && right is Int) left <= right else false
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is Int && right is PrimitiveValue.IntValue) left <= right.value else false
|
||||
}
|
||||
|
||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
|
||||
}
|
||||
|
||||
data object More : CompareOperator(">") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is Int && right is Int) left > right else false
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is Int && right is PrimitiveValue.IntValue) left > right.value else false
|
||||
}
|
||||
|
||||
|
||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
|
||||
}
|
||||
|
||||
data object MoreEqual : CompareOperator(">=") {
|
||||
override fun compare(left: Any?, right: Any?): Boolean {
|
||||
return if (left is Int && right is Int) left >= right else false
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is Int && right is PrimitiveValue.IntValue) left >= right.value else false
|
||||
}
|
||||
|
||||
|
||||
override fun allowType(type: PrimitiveValue) = type is PrimitiveValue.IntValue
|
||||
}
|
||||
|
||||
data object Matches : CompareOperator("~=") {
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
||||
right.outMatches(left)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun allowType(type: PrimitiveValue): Boolean {
|
||||
return type is PrimitiveValue.StringValue && type.matches != null
|
||||
}
|
||||
}
|
||||
|
||||
data object NotMatches : CompareOperator("!~=") {
|
||||
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
|
||||
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
|
||||
!right.outMatches(left)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun allowType(type: PrimitiveValue): Boolean {
|
||||
return Matches.allowType(type)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -5,25 +5,55 @@ sealed class PrimitiveValue(open val value: Any?, open val type: String) {
|
|||
override fun toString() = "null"
|
||||
}
|
||||
|
||||
data class BooleanValue(override val value: Boolean) : PrimitiveValue(value, type) {
|
||||
data class BooleanValue(override val value: Boolean) : PrimitiveValue(value, TYPE_NAME) {
|
||||
override fun toString() = value.toString()
|
||||
|
||||
companion object {
|
||||
const val type = "boolean"
|
||||
const val TYPE_NAME = "boolean"
|
||||
}
|
||||
}
|
||||
|
||||
data class IntValue(override val value: Int) : PrimitiveValue(value, type) {
|
||||
data class IntValue(override val value: Int) : PrimitiveValue(value, TYPE_NAME) {
|
||||
override fun toString() = value.toString()
|
||||
|
||||
companion object {
|
||||
const val type = "int"
|
||||
const val TYPE_NAME = "int"
|
||||
}
|
||||
}
|
||||
|
||||
data class StringValue(override val value: String) : PrimitiveValue(value, type) {
|
||||
data class StringValue(
|
||||
override val value: String,
|
||||
val matches: ((CharSequence) -> Boolean)? = null
|
||||
) : PrimitiveValue(value, TYPE_NAME) {
|
||||
|
||||
val outMatches: (value: CharSequence) -> Boolean = run {
|
||||
matches ?: return@run { false }
|
||||
getMatchValue(value, "(?is)", ".*")?.let { startsWithValue ->
|
||||
return@run { value -> value.startsWith(startsWithValue, ignoreCase = true) }
|
||||
}
|
||||
getMatchValue(value, "(?is).*", ".*")?.let { containsValue ->
|
||||
return@run { value -> value.contains(containsValue, ignoreCase = true) }
|
||||
}
|
||||
getMatchValue(value, "(?is).*", "")?.let { endsWithValue ->
|
||||
return@run { value -> value.endsWith(endsWithValue, ignoreCase = true) }
|
||||
}
|
||||
return@run matches
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val type = "string"
|
||||
const val TYPE_NAME = "string"
|
||||
private const val REG_SPECIAL_STRING = "\\^$.?*|+()[]{}"
|
||||
private fun getMatchValue(value: String, prefix: String, suffix: String): String? {
|
||||
if (value.startsWith(prefix) && value.endsWith(suffix) && value.length >= (prefix.length + suffix.length)) {
|
||||
for (i in prefix.length until value.length - suffix.length) {
|
||||
if (value[i] in REG_SPECIAL_STRING) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return value.subSequence(prefix.length, value.length - suffix.length).toString()
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
|
|
|
@ -17,6 +17,7 @@ import li.songe.selector.data.PropertyWrapper
|
|||
import li.songe.selector.data.TupleExpression
|
||||
import li.songe.selector.gkdAssert
|
||||
import li.songe.selector.gkdError
|
||||
import li.songe.selector.toMatches
|
||||
|
||||
internal object ParserSet {
|
||||
val whiteCharParser = Parser("\u0020\t\r\n") { source, offset, prefix ->
|
||||
|
@ -393,7 +394,19 @@ internal object ParserSet {
|
|||
val operatorResult = attrOperatorParser(source, i)
|
||||
i += operatorResult.length
|
||||
i += whiteCharParser(source, i).length
|
||||
val valueResult = valueParser(source, i)
|
||||
val valueResult = valueParser(source, i).let { result ->
|
||||
// check regex
|
||||
if ((operatorResult.data == CompareOperator.Matches || operatorResult.data == CompareOperator.NotMatches) && result.data is PrimitiveValue.StringValue) {
|
||||
val matches = try {
|
||||
result.data.value.toMatches()
|
||||
} catch (e: Exception) {
|
||||
gkdError(source, i, "valid primitive string regex", e)
|
||||
}
|
||||
result.copy(data = result.data.copy(matches = matches))
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
if (!operatorResult.data.allowType(valueResult.data)) {
|
||||
gkdError(source, i, "valid primitive value")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package li.songe.selector
|
||||
|
||||
expect fun String.toMatches(): (input: CharSequence) -> Boolean
|
30
selector/src/jsMain/kotlin/li/songe/selector/toMatches.js.kt
Normal file
30
selector/src/jsMain/kotlin/li/songe/selector/toMatches.js.kt
Normal file
|
@ -0,0 +1,30 @@
|
|||
package li.songe.selector
|
||||
|
||||
import kotlin.js.RegExp
|
||||
|
||||
// https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/js/src/kotlin/text/regex.kt
|
||||
actual fun String.toMatches(): (input: CharSequence) -> Boolean {
|
||||
if (length >= 4 && startsWith("(?")) {
|
||||
for (i in 2 until length) {
|
||||
when (get(i)) {
|
||||
in 'a'..'z' -> {}
|
||||
in 'A'..'Z' -> {}
|
||||
')' -> {
|
||||
val flags = subSequence(2, i).toMutableList()
|
||||
.apply { add('g'); add('u') }
|
||||
.joinToString("")
|
||||
val nativePattern = RegExp(substring(i + 1), flags)
|
||||
return { input ->
|
||||
nativePattern.reset()
|
||||
val match = nativePattern.exec(input.toString())
|
||||
match != null && match.index == 0 && nativePattern.lastIndex == input.length
|
||||
}
|
||||
}
|
||||
|
||||
else -> break
|
||||
}
|
||||
}
|
||||
}
|
||||
val regex = Regex(this)
|
||||
return { input -> regex.matches(input) }
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package li.songe.selector
|
||||
|
||||
actual fun String.toMatches(): (input: CharSequence) -> Boolean {
|
||||
val regex = Regex(this)
|
||||
return { input -> regex.matches(input) }
|
||||
}
|
|
@ -185,4 +185,15 @@ class ParserTest {
|
|||
println("result:" + transform.querySelectorAll(snapshotNode, selector).map { n -> n.id }
|
||||
.toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun check_regex() {
|
||||
val source = "[vid~=`(?is)TV.*`]"
|
||||
println("source:$source")
|
||||
val selector = Selector.parse(source)
|
||||
val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/14445410")
|
||||
println("selector:$selector")
|
||||
println("result:" + transform.querySelectorAll(snapshotNode, selector).map { n -> n.id }
|
||||
.toList())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user