feat(selector): ~= regex

This commit is contained in:
lisonge 2024-03-12 17:57:50 +08:00
parent a5d2040e5c
commit a085efd013
10 changed files with 228 additions and 63 deletions

View File

@ -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(

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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")
}

View File

@ -0,0 +1,3 @@
package li.songe.selector
expect fun String.toMatches(): (input: CharSequence) -> Boolean

View 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) }
}

View File

@ -0,0 +1,6 @@
package li.songe.selector
actual fun String.toMatches(): (input: CharSequence) -> Boolean {
val regex = Regex(this)
return { input -> regex.matches(input) }
}

View File

@ -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())
}
}