feat(selector): support not exp, method invoke, type check, object type
Some checks are pending
Build-Apk / build (push) Waiting to run

This commit is contained in:
lisonge 2024-07-01 21:33:13 +08:00
parent 695563c7b8
commit 6490018230
35 changed files with 1725 additions and 645 deletions

View File

@ -398,7 +398,14 @@ data class RawSubscription(
listOfNotNull(r.matches, r.excludeMatches, r.anyMatches).flatten() listOfNotNull(r.matches, r.excludeMatches, r.anyMatches).flatten()
}.flatten() }.flatten()
val allSelector = allSelectorStrings.map { s -> Selector.parseOrNull(s) } val allSelector = allSelectorStrings.map { s ->
try {
Selector.parse(s)
} catch (e: Exception) {
LogUtils.d("非法选择器", e.toString())
null
}
}
allSelector.forEachIndexed { i, s -> allSelector.forEachIndexed { i, s ->
if (s == null) { if (s == null) {

View File

@ -130,7 +130,7 @@ sealed class ResolvedRule(
else -> true else -> true
} }
private val canCacheIndex = (matches + excludeMatches).any { s -> s.canCacheIndex } private val canCacheIndex = (matches + excludeMatches).any { s -> s.useCache }
private val transform = if (canCacheIndex) defaultCacheTransform.transform else defaultTransform private val transform = if (canCacheIndex) defaultCacheTransform.transform else defaultTransform
fun query( fun query(
@ -159,7 +159,7 @@ sealed class ResolvedRule(
} }
return target return target
} finally { } finally {
defaultCacheTransform.indexCache.clear() defaultCacheTransform.cache.clear()
} }
} }

View File

@ -4,9 +4,21 @@ import android.accessibilityservice.AccessibilityService
import android.graphics.Rect import android.graphics.Rect
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import com.blankj.utilcode.util.LogUtils
import li.songe.gkd.BuildConfig
import li.songe.selector.MismatchExpressionTypeException
import li.songe.selector.MismatchOperatorTypeException
import li.songe.selector.MismatchParamTypeException
import li.songe.selector.Selector import li.songe.selector.Selector
import li.songe.selector.Transform import li.songe.selector.Transform
import li.songe.selector.data.PrimitiveValue import li.songe.selector.UnknownIdentifierException
import li.songe.selector.UnknownIdentifierMethodException
import li.songe.selector.UnknownMemberException
import li.songe.selector.UnknownMemberMethodException
import li.songe.selector.getCharSequenceAttr
import li.songe.selector.getCharSequenceInvoke
import li.songe.selector.getIntInvoke
import li.songe.selector.initDefaultTypeInfo
val AccessibilityService.safeActiveWindow: AccessibilityNodeInfo? val AccessibilityService.safeActiveWindow: AccessibilityNodeInfo?
get() = try { get() = try {
@ -51,24 +63,13 @@ inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode:
} }
} }
/** fun AccessibilityNodeInfo.getChildOrNull(i: Int?): AccessibilityNodeInfo? {
* 此方法小概率造成无限节点片段,底层原因未知 i ?: return null
* return if (i in 0 until childCount) {
* https://github.com/gkd-kit/gkd/issues/28 getChild(i)
*/ } else {
fun AccessibilityNodeInfo.getDepth(): Int { null
var p: AccessibilityNodeInfo? = this
var depth = 0
while (true) {
val p2 = p?.parent
if (p2 != null) {
p = p2
depth++
} else {
break
}
} }
return depth
} }
fun AccessibilityNodeInfo.getVid(): CharSequence? { fun AccessibilityNodeInfo.getVid(): CharSequence? {
@ -135,51 +136,34 @@ val getChildren: (AccessibilityNodeInfo) -> Sequence<AccessibilityNodeInfo> = {
} }
} }
val allowPropertyNames by lazy { private val typeInfo by lazy {
mapOf( initDefaultTypeInfo().apply {
"id" to PrimitiveValue.StringValue.TYPE_NAME, nodeType.props = nodeType.props.filter { !it.name.startsWith('_') }.toTypedArray()
"vid" to PrimitiveValue.StringValue.TYPE_NAME, contextType.props = contextType.props.filter { !it.name.startsWith('_') }.toTypedArray()
}.contextType
"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_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_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_NAME,
"depth" to PrimitiveValue.IntValue.TYPE_NAME,
"childCount" to PrimitiveValue.IntValue.TYPE_NAME,
)
} }
fun Selector.checkSelector(): String? { fun Selector.checkSelector(): String? {
binaryExpressions.forEach { e -> val error = checkType(typeInfo) ?: return null
if (!allowPropertyNames.contains(e.name)) { if (BuildConfig.DEBUG) {
return "未知属性:${e.name}" LogUtils.d(
} "Selector check error",
if (e.value.type != "null" && allowPropertyNames[e.name] != e.value.type) { source,
return "非法类型:${e.name}=${e.value.type}" error.message
} )
}
return when (error) {
is MismatchExpressionTypeException -> "不匹配表达式类型:${error.exception.stringify()}"
is MismatchOperatorTypeException -> "不匹配操作符类型:${error.exception.stringify()}"
is MismatchParamTypeException -> "不匹配参数类型:${error.call.stringify()}"
is UnknownIdentifierException -> "未知属性:${error.value.value}"
is UnknownIdentifierMethodException -> "未知方法:${error.value.value}"
is UnknownMemberException -> "未知属性:${error.value.property}"
is UnknownMemberMethodException -> "未知方法:${error.value.property}"
} }
return null
} }
private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) { private fun createGetNodeAttr(cache: NodeCache): ((AccessibilityNodeInfo, String) -> Any?) {
var tempNode: AccessibilityNodeInfo? = null var tempNode: AccessibilityNodeInfo? = null
val tempRect = Rect() val tempRect = Rect()
var tempVid: CharSequence? = null var tempVid: CharSequence? = null
@ -198,6 +182,28 @@ private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) {
} }
return tempVid return tempVid
} }
/**
* 在无缓存时, 此方法小概率造成无限节点片段,底层原因未知
*
* https://github.com/gkd-kit/gkd/issues/28
*/
fun AccessibilityNodeInfo.getDepthX(): Int {
var p: AccessibilityNodeInfo = this
var depth = 0
while (true) {
val p2 = cache.parent[p] ?: p.parent.apply {
cache.parent[p] = this
}
if (p2 != null) {
p = p2
depth++
} else {
break
}
}
return depth
}
return { node, name -> return { node, name ->
when (name) { when (name) {
"id" -> node.viewIdResourceName "id" -> node.viewIdResourceName
@ -226,8 +232,13 @@ private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) {
"height" -> node.getTempRect().height() "height" -> node.getTempRect().height()
"index" -> node.getIndex() "index" -> node.getIndex()
"depth" -> node.getDepth() "depth" -> node.getDepthX()
"childCount" -> node.childCount "childCount" -> node.childCount
"parent" -> cache.parent[node] ?: node.parent.apply {
cache.parent[node] = this
}
else -> null else -> null
} }
} }
@ -235,22 +246,43 @@ private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) {
data class CacheTransform( data class CacheTransform(
val transform: Transform<AccessibilityNodeInfo>, val transform: Transform<AccessibilityNodeInfo>,
val indexCache: HashMap<AccessibilityNodeInfo, Int>, val cache: NodeCache,
) )
data class NodeCache(
val child: MutableMap<Pair<AccessibilityNodeInfo, Int>, AccessibilityNodeInfo> = HashMap(),
val index: MutableMap<AccessibilityNodeInfo, Int> = HashMap(),
val parent: MutableMap<AccessibilityNodeInfo, AccessibilityNodeInfo?> = HashMap(),
) {
fun clear() {
emptyMap<String, String>()
child.clear()
parent.clear()
index.clear()
}
}
fun createCacheTransform(): CacheTransform { fun createCacheTransform(): CacheTransform {
val indexCache = HashMap<AccessibilityNodeInfo, Int>() val cache = NodeCache()
fun AccessibilityNodeInfo.getParentX(): AccessibilityNodeInfo? {
return parent?.also { parent ->
cache.parent[this] = parent
}
}
fun AccessibilityNodeInfo.getChildX(index: Int): AccessibilityNodeInfo? { fun AccessibilityNodeInfo.getChildX(index: Int): AccessibilityNodeInfo? {
return getChild(index)?.also { child -> return cache.child[this to index] ?: getChild(index)?.also { child ->
indexCache[child] = index cache.index[child] = index
cache.parent[child] = this
cache.child[this to index] = child
} }
} }
fun AccessibilityNodeInfo.getIndexX(): Int { fun AccessibilityNodeInfo.getIndexX(): Int {
indexCache[this]?.let { return it } cache.index[this]?.let { return it }
parent?.forEachIndexed { index, child -> getParentX()?.forEachIndexed { index, child ->
if (child != null) { if (child != null) {
indexCache[child] = index cache.index[child] = index
} }
if (child == this) { if (child == this) {
return index return index
@ -267,22 +299,60 @@ fun createCacheTransform(): CacheTransform {
} }
} }
} }
val getAttr = createGetAttr() val getNodeAttr = createGetNodeAttr(cache)
val getParent = { node: AccessibilityNodeInfo ->
cache.parent[node] ?: node.parent.apply {
cache.parent[node] = this
}
}
val transform = Transform( val transform = Transform(
getAttr = { node, name -> getAttr = { node, name ->
when (name) { when (node) {
"index" -> { is AccessibilityNodeInfo -> {
node.getIndexX() when (name) {
"index" -> {
node.getIndexX()
}
else -> {
getNodeAttr(node, name)
}
}
} }
is CharSequence -> getCharSequenceAttr(node, name)
else -> { else -> {
getAttr(node, name) null
} }
} }
}, },
getInvoke = { target, name, args ->
when (target) {
is AccessibilityNodeInfo -> when (name) {
"getChild" -> {
args.getIntOrNull()?.let { index ->
if (index in 0 until target.childCount) {
target.getChildX(index)
} else {
null
}
}
}
else -> null
}
is CharSequence -> getCharSequenceInvoke(target, name, args)
is Int -> getIntInvoke(target, name, args)
else -> null
}
},
getName = { node -> node.className }, getName = { node -> node.className },
getChildren = getChildrenCache, getChildren = getChildrenCache,
getParent = { node -> node.parent }, getParent = getParent,
getDescendants = { node -> getDescendants = { node ->
sequence { sequence {
val stack = getChildrenCache(node).toMutableList() val stack = getChildrenCache(node).toMutableList()
@ -319,9 +389,9 @@ fun createCacheTransform(): CacheTransform {
}, },
getBeforeBrothers = { node, connectExpression -> getBeforeBrothers = { node, connectExpression ->
sequence { sequence {
val parentVal = node.parent ?: return@sequence val parentVal = getParent(node) ?: return@sequence
val index = // 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 cache.index 是空
indexCache[node] // 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 indexCache 是空 val index = cache.index[node]
if (index != null) { if (index != null) {
var i = index - 1 var i = index - 1
var offset = 0 var offset = 0
@ -349,9 +419,9 @@ fun createCacheTransform(): CacheTransform {
} }
}, },
getAfterBrothers = { node, connectExpression -> getAfterBrothers = { node, connectExpression ->
val parentVal = node.parent val parentVal = getParent(node)
if (parentVal != null) { if (parentVal != null) {
val index = indexCache[node] val index = cache.index[node]
if (index != null) { if (index != null) {
sequence { sequence {
var i = index + 1 var i = index + 1
@ -418,12 +488,41 @@ fun createCacheTransform(): CacheTransform {
}, },
) )
return CacheTransform(transform, indexCache) return CacheTransform(transform, cache)
}
private fun List<Any?>.getIntOrNull(i: Int = 0): Int? {
return getOrNull(i) as? Int ?: return null
} }
fun createTransform(): Transform<AccessibilityNodeInfo> { fun createTransform(): Transform<AccessibilityNodeInfo> {
val cache = NodeCache()
val getNodeAttr = createGetNodeAttr(cache)
return Transform( return Transform(
getAttr = createGetAttr(), getAttr = { target, name ->
when (target) {
is AccessibilityNodeInfo -> getNodeAttr(target, name)
is CharSequence -> getCharSequenceAttr(target, name)
else -> {
null
}
}
},
getInvoke = { target, name, args ->
when (target) {
is AccessibilityNodeInfo -> when (name) {
"getChild" -> {
target.getChildOrNull(args.getIntOrNull())
}
else -> null
}
is CharSequence -> getCharSequenceInvoke(target, name, args)
is Int -> getIntInvoke(target, name, args)
else -> null
}
},
getName = { node -> node.className }, getName = { node -> node.className },
getChildren = getChildren, getChildren = getChildren,
getParent = { node -> node.parent }, getParent = { node -> node.parent },

View File

@ -40,8 +40,8 @@ others_floating_bubble_view = { module = "io.github.torrydo:floating-bubble-view
androidx_appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0" } androidx_appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0" }
androidx_core_ktx = { module = "androidx.core:core-ktx", version = "1.13.1" } androidx_core_ktx = { module = "androidx.core:core-ktx", version = "1.13.1" }
androidx_lifecycle_runtime_ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version = "2.8.2" } androidx_lifecycle_runtime_ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version = "2.8.2" }
androidx_junit = { module = "androidx.test.ext:junit", version = "1.1.5" } androidx_junit = { module = "androidx.test.ext:junit", version = "1.2.1" }
androidx_espresso = { module = "androidx.test.espresso:espresso-core", version = "3.5.1" } androidx_espresso = { module = "androidx.test.espresso:espresso-core", version = "3.6.1" }
androidx_room_runtime = { module = "androidx.room:room-runtime", version.ref = "room" } androidx_room_runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx_room_compiler = { module = "androidx.room:room-compiler", version.ref = "room" } androidx_room_compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
androidx_room_ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx_room_ktx = { module = "androidx.room:room-ktx", version.ref = "room" }

View File

@ -0,0 +1,27 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
data class BinaryExpression(
override val start: Int,
override val end: Int,
val left: ValueExpression,
val operator: PositionImpl<CompareOperator>,
val right: ValueExpression,
) : Expression() {
override fun <T> match(node: T, transform: Transform<T>): Boolean {
return operator.value.compare(node, transform, left, right)
}
override val binaryExpressions
get() = arrayOf(this)
override fun stringify() = "${left.stringify()}${operator.stringify()}${right.stringify()}"
val properties: Array<String>
get() = arrayOf(*left.properties, *right.properties)
val methods: Array<String>
get() = arrayOf(*left.methods, *right.methods)
}

View File

@ -0,0 +1,314 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
sealed class CompareOperator(val key: String) : Stringify {
override fun stringify() = key
internal abstract fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean
internal abstract fun allowType(left: ValueExpression, right: ValueExpression): Boolean
companion object {
// https://stackoverflow.com/questions/47648689
val allSubClasses by lazy {
listOf(
Equal,
NotEqual,
Start,
NotStart,
Include,
NotInclude,
End,
NotEnd,
Less,
LessEqual,
More,
MoreEqual,
Matches,
NotMatches
).sortedBy { -it.key.length }.toTypedArray()
}
// example
// id="com.lptiyu.tanke:id/ab1"
// id="com.lptiyu.tanke:id/ab2"
private fun CharSequence.contentReversedEquals(other: CharSequence): Boolean {
if (this === other) return true
if (this.length != other.length) return false
for (i in this.length - 1 downTo 0) {
if (this[i] != other[i]) return false
}
return true
}
}
data object Equal : CompareOperator("=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is CharSequence && right is CharSequence) {
left.contentReversedEquals(right)
} else {
left == right
}
}
override fun allowType(left: ValueExpression, right: ValueExpression) = true
}
data object NotEqual : CompareOperator("!=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
return !Equal.compare(node, transform, leftExp, rightExp)
}
override fun allowType(left: ValueExpression, right: ValueExpression) = true
}
data object Start : CompareOperator("^=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is CharSequence && right is CharSequence) {
left.startsWith(right)
} else {
false
}
}
override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {
return (left is ValueExpression.StringLiteral || left is ValueExpression.Variable) && (right is ValueExpression.StringLiteral || right is ValueExpression.Variable)
}
}
data object NotStart : CompareOperator("!^=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is CharSequence && right is CharSequence) {
!left.startsWith(right)
} else {
false
}
}
override fun allowType(left: ValueExpression, right: ValueExpression) =
Start.allowType(left, right)
}
data object Include : CompareOperator("*=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is CharSequence && right is CharSequence) {
left.contains(right)
} else {
false
}
}
override fun allowType(left: ValueExpression, right: ValueExpression) =
Start.allowType(left, right)
}
data object NotInclude : CompareOperator("!*=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is CharSequence && right is CharSequence) {
!left.contains(right)
} else {
false
}
}
override fun allowType(left: ValueExpression, right: ValueExpression) =
Start.allowType(left, right)
}
data object End : CompareOperator("$=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is CharSequence && right is CharSequence) {
left.endsWith(right)
} else {
false
}
}
override fun allowType(left: ValueExpression, right: ValueExpression) =
Start.allowType(left, right)
}
data object NotEnd : CompareOperator("!$=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is CharSequence && right is CharSequence) {
!left.endsWith(
right
)
} else {
false
}
}
override fun allowType(left: ValueExpression, right: ValueExpression) =
Start.allowType(left, right)
}
data object Less : CompareOperator("<") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is Int && right is Int) left < right else false
}
override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {
return (left is ValueExpression.Variable || left is ValueExpression.IntLiteral) && (right is ValueExpression.IntLiteral || right is ValueExpression.Variable)
}
}
data object LessEqual : CompareOperator("<=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is Int && right is Int) left <= right else false
}
override fun allowType(left: ValueExpression, right: ValueExpression) =
Less.allowType(left, right)
}
data object More : CompareOperator(">") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is Int && right is Int) left > right else false
}
override fun allowType(left: ValueExpression, right: ValueExpression) =
Less.allowType(left, right)
}
data object MoreEqual : CompareOperator(">=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
val right = rightExp.getAttr(node, transform)
return if (left is Int && right is Int) left >= right else false
}
override fun allowType(left: ValueExpression, right: ValueExpression) =
Less.allowType(left, right)
}
data object Matches : CompareOperator("~=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
return if (left is CharSequence && rightExp is ValueExpression.StringLiteral) {
rightExp.outMatches(left)
} else {
false
}
}
override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {
return (left is ValueExpression.Variable) && (right is ValueExpression.StringLiteral && right.matches != null)
}
}
data object NotMatches : CompareOperator("!~=") {
override fun <T> compare(
node: T,
transform: Transform<T>,
leftExp: ValueExpression,
rightExp: ValueExpression
): Boolean {
val left = leftExp.getAttr(node, transform)
return if (left is CharSequence && rightExp is ValueExpression.StringLiteral) {
!rightExp.outMatches(left)
} else {
false
}
}
override fun allowType(left: ValueExpression, right: ValueExpression): Boolean {
return Matches.allowType(left, right)
}
}
}

View File

@ -1,4 +1,4 @@
package li.songe.selector.data package li.songe.selector
sealed class ConnectExpression { sealed class ConnectExpression {
abstract val minOffset: Int abstract val minOffset: Int

View File

@ -1,9 +1,9 @@
package li.songe.selector.data package li.songe.selector
import li.songe.selector.Transform sealed class ConnectOperator(val key: String) : Stringify {
override fun stringify() = key
sealed class ConnectOperator(val key: String) { internal abstract fun <T> traversal(
abstract fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression node: T, transform: Transform<T>, connectExpression: ConnectExpression
): Sequence<T> ): Sequence<T>

View File

@ -1,6 +1,4 @@
package li.songe.selector.data package li.songe.selector
import li.songe.selector.Transform
data class ConnectSegment( data class ConnectSegment(
val operator: ConnectOperator = ConnectOperator.Ancestor, val operator: ConnectOperator = ConnectOperator.Ancestor,
@ -10,10 +8,10 @@ data class ConnectSegment(
if (operator == ConnectOperator.Ancestor && connectExpression is PolynomialExpression && connectExpression.a == 1 && connectExpression.b == 0) { if (operator == ConnectOperator.Ancestor && connectExpression is PolynomialExpression && connectExpression.a == 1 && connectExpression.b == 0) {
return "" return ""
} }
return operator.key + connectExpression.toString() return operator.stringify() + connectExpression.toString()
} }
fun <T> traversal(node: T, transform: Transform<T>): Sequence<T?> { internal fun <T> traversal(node: T, transform: Transform<T>): Sequence<T?> {
return operator.traversal(node, transform, connectExpression) return operator.traversal(node, transform, connectExpression)
} }
} }

View File

@ -1,20 +1,18 @@
package li.songe.selector.data package li.songe.selector
import li.songe.selector.Transform
data class ConnectWrapper( data class ConnectWrapper(
val connectSegment: ConnectSegment, val segment: ConnectSegment,
val to: PropertyWrapper, val to: PropertyWrapper,
) { ) {
override fun toString(): String { override fun toString(): String {
return (to.toString() + "\u0020" + connectSegment.toString()).trim() return (to.toString() + "\u0020" + segment.toString()).trim()
} }
fun <T> matchTracks( internal fun <T> matchTracks(
node: T, transform: Transform<T>, node: T, transform: Transform<T>,
trackNodes: MutableList<T>, trackNodes: MutableList<T>,
): List<T>? { ): List<T>? {
connectSegment.traversal(node, transform).forEach { segment.traversal(node, transform).forEach {
if (it == null) return@forEach if (it == null) return@forEach
val r = to.matchTracks(it, transform, trackNodes) val r = to.matchTracks(it, transform, trackNodes)
if (r != null) return r if (r != null) return r

View File

@ -0,0 +1,45 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
sealed class SelectorCheckException(override val message: String) : Exception(message)
@JsExport
data class UnknownIdentifierException(
val value: ValueExpression.Identifier,
) : SelectorCheckException("Unknown Identifier: ${value.value}")
@JsExport
data class UnknownMemberException(
val value: ValueExpression.MemberExpression,
) : SelectorCheckException("Unknown Member: ${value.property}")
@JsExport
data class UnknownIdentifierMethodException(
val value: ValueExpression.Identifier,
) : SelectorCheckException("Unknown Identifier Method: ${value.value}")
@JsExport
data class UnknownMemberMethodException(
val value: ValueExpression.MemberExpression,
) : SelectorCheckException("Unknown Member Method: ${value.property}")
@JsExport
data class MismatchParamTypeException(
val call: ValueExpression.CallExpression,
val argument: ValueExpression.LiteralExpression,
val type: PrimitiveType
) : SelectorCheckException("Mismatch Param Type: ${argument.value} should be ${type.key}")
@JsExport
data class MismatchExpressionTypeException(
val exception: BinaryExpression,
val leftType: PrimitiveType,
val rightType: PrimitiveType,
) : SelectorCheckException("Mismatch Expression Type: ${exception.stringify()}")
@JsExport
data class MismatchOperatorTypeException(
val exception: BinaryExpression,
) : SelectorCheckException("Mismatch Operator Type: ${exception.stringify()}")

View File

@ -1,8 +1,6 @@
package li.songe.selector.data package li.songe.selector
import li.songe.selector.Transform sealed class Expression : Position {
sealed class Expression {
internal abstract fun <T> match(node: T, transform: Transform<T>): Boolean internal abstract fun <T> match(node: T, transform: Transform<T>): Boolean
abstract val binaryExpressions: Array<BinaryExpression> abstract val binaryExpressions: Array<BinaryExpression>

View File

@ -0,0 +1,31 @@
package li.songe.selector
data class LogicalExpression(
override val start: Int,
override val end: Int,
val left: Expression,
val operator: PositionImpl<LogicalOperator>,
val right: Expression,
) : Expression() {
override fun <T> match(node: T, transform: Transform<T>): Boolean {
return operator.value.compare(node, transform, left, right)
}
override val binaryExpressions
get() = left.binaryExpressions + right.binaryExpressions
override fun stringify(): String {
val leftStr = if (left is LogicalExpression && left.operator.value != operator.value) {
"(${left.stringify()})"
} else {
left.stringify()
}
val rightStr = if (right is LogicalExpression && right.operator.value != operator.value) {
"(${right.stringify()})"
} else {
right.stringify()
}
return "$leftStr\u0020${operator.stringify()}\u0020$rightStr"
}
}

View File

@ -1,8 +1,8 @@
package li.songe.selector.data package li.songe.selector
import li.songe.selector.Transform sealed class LogicalOperator(val key: String) : Stringify {
override fun stringify() = key
sealed class LogicalOperator(val key: String) {
companion object { companion object {
// https://stackoverflow.com/questions/47648689 // https://stackoverflow.com/questions/47648689
val allSubClasses by lazy { val allSubClasses by lazy {
@ -12,7 +12,7 @@ sealed class LogicalOperator(val key: String) {
} }
} }
abstract fun <T> compare( internal abstract fun <T> compare(
node: T, node: T,
transform: Transform<T>, transform: Transform<T>,
left: Expression, left: Expression,

View File

@ -10,7 +10,6 @@ class MultiplatformSelector private constructor(
val tracks = selector.tracks val tracks = selector.tracks
val trackIndex = selector.trackIndex val trackIndex = selector.trackIndex
val connectKeys = selector.connectKeys val connectKeys = selector.connectKeys
val propertyNames = selector.propertyNames
val qfIdValue = selector.qfIdValue val qfIdValue = selector.qfIdValue
val qfVidValue = selector.qfVidValue val qfVidValue = selector.qfVidValue
@ -18,15 +17,8 @@ class MultiplatformSelector private constructor(
val canQf = selector.canQf val canQf = selector.canQf
val isMatchRoot = selector.isMatchRoot val isMatchRoot = selector.isMatchRoot
// [name,operator,value][] val binaryExpressions = selector.binaryExpressions
val binaryExpressions = selector.binaryExpressions.map { e -> fun checkType(typeInfo: TypeInfo) = selector.checkType(typeInfo)
arrayOf(
e.name,
e.operator.key,
e.value.type,
e.value.toString()
)
}.toTypedArray()
fun <T : Any> match(node: T, transform: MultiplatformTransform<T>): T? { fun <T : Any> match(node: T, transform: MultiplatformTransform<T>): T? {
return selector.match(node, transform.transform) return selector.match(node, transform.transform)

View File

@ -5,13 +5,15 @@ import kotlin.js.JsExport
@JsExport @JsExport
@Suppress("UNCHECKED_CAST", "UNUSED") @Suppress("UNCHECKED_CAST", "UNUSED")
class MultiplatformTransform<T : Any>( class MultiplatformTransform<T : Any>(
getAttr: (T, String) -> Any?, getAttr: (Any?, String) -> Any?,
getInvoke: (Any?, String, List<Any?>) -> Any?,
getName: (T) -> String?, getName: (T) -> String?,
getChildren: (T) -> Array<T>, getChildren: (T) -> Array<T>,
getParent: (T) -> T?, getParent: (T) -> T?,
) { ) {
internal val transform = Transform( internal val transform = Transform(
getAttr = getAttr, getAttr = getAttr,
getInvoke = getInvoke,
getName = getName, getName = getName,
getChildren = { node -> getChildren(node).asSequence() }, getChildren = { node -> getChildren(node).asSequence() },
getParent = getParent, getParent = getParent,

View File

@ -0,0 +1,20 @@
package li.songe.selector
data class NotExpression(
override val start: Int,
val expression: Expression
) : Expression() {
override val end: Int
get() = expression.end
override fun <T> match(node: T, transform: Transform<T>): Boolean {
return !expression.match(node, transform)
}
override val binaryExpressions: Array<BinaryExpression>
get() = expression.binaryExpressions
override fun stringify(): String {
return "!(${expression.stringify()})"
}
}

View File

@ -1,4 +1,4 @@
package li.songe.selector.data package li.songe.selector
/** /**
* an+b * an+b

View File

@ -0,0 +1,26 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
sealed interface Stringify {
fun stringify(): String
}
sealed interface Position : Stringify {
val start: Int
val end: Int
val length: Int
get() = end - start
}
@JsExport
data class PositionImpl<T : Stringify>(
override val start: Int,
override val end: Int,
val value: T
) : Position {
override fun stringify(): String {
return value.stringify()
}
}

View File

@ -1,6 +1,4 @@
package li.songe.selector.data package li.songe.selector
import li.songe.selector.Transform
data class PropertySegment( data class PropertySegment(
@ -18,7 +16,7 @@ data class PropertySegment(
override fun toString(): String { override fun toString(): String {
val matchTag = if (tracked) "@" else "" val matchTag = if (tracked) "@" else ""
return matchTag + name + expressions.joinToString("") { "[$it]" } return matchTag + name + expressions.joinToString("") { "[${it.stringify()}]" }
} }
private fun <T> matchName(node: T, transform: Transform<T>): Boolean { private fun <T> matchName(node: T, transform: Transform<T>): Boolean {

View File

@ -1,9 +1,7 @@
package li.songe.selector.data package li.songe.selector
import li.songe.selector.Transform
data class PropertyWrapper( data class PropertyWrapper(
val propertySegment: PropertySegment, val segment: PropertySegment,
val to: ConnectWrapper? = null, val to: ConnectWrapper? = null,
) { ) {
override fun toString(): String { override fun toString(): String {
@ -11,7 +9,7 @@ data class PropertyWrapper(
to.toString() + "\u0020" to.toString() + "\u0020"
} else { } else {
"" ""
}) + propertySegment.toString() }) + segment.toString()
} }
fun <T> matchTracks( fun <T> matchTracks(
@ -19,7 +17,7 @@ data class PropertyWrapper(
transform: Transform<T>, transform: Transform<T>,
trackNodes: MutableList<T>, trackNodes: MutableList<T>,
): List<T>? { ): List<T>? {
if (!propertySegment.match(node, transform)) { if (!segment.match(node, transform)) {
return null return null
} }
trackNodes.add(node) trackNodes.add(node)

View File

@ -1,13 +1,10 @@
package li.songe.selector package li.songe.selector
import li.songe.selector.data.BinaryExpression import li.songe.selector.parser.selectorParser
import li.songe.selector.data.CompareOperator
import li.songe.selector.data.ConnectOperator
import li.songe.selector.data.PrimitiveValue
import li.songe.selector.data.PropertyWrapper
import li.songe.selector.parser.ParserSet
class Selector internal constructor(private val propertyWrapper: PropertyWrapper) { class Selector internal constructor(
val source: String, private val propertyWrapper: PropertyWrapper
) {
override fun toString(): String { override fun toString(): String {
return propertyWrapper.toString() return propertyWrapper.toString()
} }
@ -17,7 +14,7 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
while (true) { while (true) {
list.add(list.last().to?.to ?: break) list.add(list.last().to?.to ?: break)
} }
list.map { p -> p.propertySegment.tracked }.toTypedArray<Boolean>() list.map { p -> p.segment.tracked }.toTypedArray<Boolean>()
} }
val trackIndex = tracks.indexOfFirst { it }.let { i -> val trackIndex = tracks.indexOfFirst { it }.let { i ->
@ -41,25 +38,25 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
return propertyWrapper.matchTracks(node, transform, trackNodes) return propertyWrapper.matchTracks(node, transform, trackNodes)
} }
val qfIdValue = propertyWrapper.propertySegment.expressions.firstOrNull().let { e -> val qfIdValue = propertyWrapper.segment.expressions.firstOrNull().let { e ->
if (e is BinaryExpression && e.name == "id" && e.operator == CompareOperator.Equal && e.value is PrimitiveValue.StringValue) { if (e is BinaryExpression && e.left.value == "id" && e.operator.value == CompareOperator.Equal && e.right is ValueExpression.StringLiteral) {
e.value.value e.right.value
} else { } else {
null null
} }
} }
val qfVidValue = propertyWrapper.propertySegment.expressions.firstOrNull().let { e -> val qfVidValue = propertyWrapper.segment.expressions.firstOrNull().let { e ->
if (e is BinaryExpression && e.name == "vid" && e.operator == CompareOperator.Equal && e.value is PrimitiveValue.StringValue) { if (e is BinaryExpression && e.left.value == "vid" && e.operator.value == CompareOperator.Equal && e.right is ValueExpression.StringLiteral) {
e.value.value e.right.value
} else { } else {
null null
} }
} }
val qfTextValue = propertyWrapper.propertySegment.expressions.firstOrNull().let { e -> val qfTextValue = propertyWrapper.segment.expressions.firstOrNull().let { e ->
if (e is BinaryExpression && e.name == "text" && (e.operator == CompareOperator.Equal || e.operator == CompareOperator.Start || e.operator == CompareOperator.Include || e.operator == CompareOperator.End) && e.value is PrimitiveValue.StringValue) { if (e is BinaryExpression && e.left.value == "text" && (e.operator.value == CompareOperator.Equal || e.operator.value == CompareOperator.Start || e.operator.value == CompareOperator.Include || e.operator.value == CompareOperator.End) && e.right is ValueExpression.StringLiteral) {
e.value.value e.right.value
} else { } else {
null null
} }
@ -68,8 +65,8 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
val canQf = qfIdValue != null || qfVidValue != null || qfTextValue != null val canQf = qfIdValue != null || qfVidValue != null || qfTextValue != null
// 主动查询 // 主动查询
val isMatchRoot = propertyWrapper.propertySegment.expressions.firstOrNull().let { e -> val isMatchRoot = propertyWrapper.segment.expressions.firstOrNull().let { e ->
e is BinaryExpression && e.name == "depth" && e.operator == CompareOperator.Equal && e.value.value == 0 e is BinaryExpression && e.left.value == "depth" && e.operator.value == CompareOperator.Equal && e.right.value == 0
} }
val connectKeys = run { val connectKeys = run {
@ -77,7 +74,7 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
val keys = mutableListOf<String>() val keys = mutableListOf<String>()
while (c != null) { while (c != null) {
c.apply { c.apply {
keys.add(connectSegment.operator.key) keys.add(segment.operator.key)
} }
c = c.to.to c = c.to.to
} }
@ -88,28 +85,144 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
var p: PropertyWrapper? = propertyWrapper var p: PropertyWrapper? = propertyWrapper
val expressions = mutableListOf<BinaryExpression>() val expressions = mutableListOf<BinaryExpression>()
while (p != null) { while (p != null) {
val s = p.propertySegment val s = p.segment
expressions.addAll(s.binaryExpressions) expressions.addAll(s.binaryExpressions)
p = p.to?.to p = p.to?.to
} }
expressions.distinct().toTypedArray() expressions.distinct().toTypedArray()
} }
val propertyNames = run { val useCache = run {
binaryExpressions.map { e -> e.name }.distinct().toTypedArray() if (connectKeys.contains(ConnectOperator.BeforeBrother.key)) {
return@run true
}
if (connectKeys.contains(ConnectOperator.AfterBrother.key)) {
return@run true
}
binaryExpressions.forEach { b ->
if (b.properties.any { useCacheProperties.contains(it) }) {
return@run true
}
if (b.methods.any { useCacheMethods.contains(it) }) {
return@run true
}
}
return@run false
} }
val canCacheIndex = fun checkType(typeInfo: TypeInfo): SelectorCheckException? {
connectKeys.contains(ConnectOperator.BeforeBrother.key) || connectKeys.contains( try {
ConnectOperator.AfterBrother.key propertyWrapper.segment.binaryExpressions.forEach { exp ->
) || propertyNames.contains("index") if (!exp.operator.value.allowType(exp.left, exp.right)) {
throw MismatchOperatorTypeException(exp)
}
val leftType = getExpType(exp.left, typeInfo)
val rightType = getExpType(exp.right, typeInfo)
if (leftType != null && rightType != null && leftType != rightType) {
throw MismatchExpressionTypeException(exp, leftType, rightType)
}
}
} catch (e: SelectorCheckException) {
return e
}
return null
}
companion object { companion object {
fun parse(source: String) = ParserSet.selectorParser(source) fun parse(source: String) = selectorParser(source)
fun parseOrNull(source: String) = try { fun parseOrNull(source: String) = try {
ParserSet.selectorParser(source) selectorParser(source)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
} }
} }
private val useCacheProperties by lazy {
arrayOf("index", "parent")
}
private val useCacheMethods by lazy {
arrayOf("getChild")
}
private fun getExpType(exp: ValueExpression, typeInfo: TypeInfo): PrimitiveType? {
return when (exp) {
is ValueExpression.NullLiteral -> null
is ValueExpression.BooleanLiteral -> PrimitiveType.BooleanType
is ValueExpression.IntLiteral -> PrimitiveType.IntType
is ValueExpression.StringLiteral -> PrimitiveType.StringType
is ValueExpression.Variable -> checkVariable(exp, typeInfo).type
}
}
private fun checkVariable(value: ValueExpression.Variable, typeInfo: TypeInfo): TypeInfo {
return when (value) {
is ValueExpression.CallExpression -> {
val method = when (value.callee) {
is ValueExpression.CallExpression -> {
throw IllegalArgumentException("Unsupported nested call")
}
is ValueExpression.Identifier -> {
// getChild(0)
typeInfo.methods.find { it.name == value.callee.value && it.params.size == value.arguments.size }
?: throw UnknownIdentifierMethodException(value.callee)
}
is ValueExpression.MemberExpression -> {
// parent.getChild(0)
checkVariable(
value.callee.object0, typeInfo
).methods.find { it.name == value.callee.property && it.params.size == value.arguments.size }
?: throw UnknownMemberMethodException(value.callee)
}
}
method.params.forEachIndexed { index, argTypeInfo ->
when (val argExp = value.arguments[index]) {
is ValueExpression.NullLiteral -> {}
is ValueExpression.BooleanLiteral -> {
if (argTypeInfo.type != PrimitiveType.BooleanType) {
throw MismatchParamTypeException(
value,
argExp,
PrimitiveType.BooleanType
)
}
}
is ValueExpression.IntLiteral -> {
if (argTypeInfo.type != PrimitiveType.IntType) {
throw MismatchParamTypeException(value, argExp, PrimitiveType.IntType)
}
}
is ValueExpression.StringLiteral -> {
if (argTypeInfo.type != PrimitiveType.StringType) {
throw MismatchParamTypeException(
value,
argExp,
PrimitiveType.StringType
)
}
}
is ValueExpression.Variable -> {
checkVariable(argExp, argTypeInfo)
}
}
}
return method.returnType
}
is ValueExpression.Identifier -> {
typeInfo.props.find { it.name == value.value }?.type
?: throw UnknownIdentifierException(value)
}
is ValueExpression.MemberExpression -> {
checkVariable(value.object0, typeInfo).props.find { it.name == value.property }?.type
?: throw UnknownMemberException(value)
}
}
}

View File

@ -3,7 +3,7 @@ package li.songe.selector
import kotlin.js.JsExport import kotlin.js.JsExport
@JsExport @JsExport
data class GkdSyntaxError internal constructor( data class SyntaxError internal constructor(
val expectedValue: String, val expectedValue: String,
val position: Int, val position: Int,
val source: String, val source: String,
@ -17,21 +17,21 @@ data class GkdSyntaxError internal constructor(
) )
internal fun gkdAssert( internal fun gkdAssert(
source: String, source: CharSequence,
offset: Int, offset: Int,
value: String = "", value: String = "",
expectedValue: String? = null expectedValue: String? = null
) { ) {
if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) { if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) {
throw GkdSyntaxError(expectedValue ?: value, offset, source) throw SyntaxError(expectedValue ?: value, offset, source.toString())
} }
} }
internal fun gkdError( internal fun gkdError(
source: String, source: CharSequence,
offset: Int, offset: Int,
expectedValue: String = "", expectedValue: String = "",
cause: Exception? = null cause: Exception? = null
): Nothing { ): Nothing {
throw GkdSyntaxError(expectedValue, offset, source, cause) throw SyntaxError(expectedValue, offset, source.toString(), cause)
} }

View File

@ -1,10 +1,9 @@
package li.songe.selector package li.songe.selector
import li.songe.selector.data.ConnectExpression
@Suppress("UNUSED") @Suppress("UNUSED")
class Transform<T>( class Transform<T>(
val getAttr: (T, String) -> Any?, val getAttr: (Any?, String) -> Any?,
val getInvoke: (Any?, String, List<Any?>) -> Any? = { _, _, _ -> null },
val getName: (T) -> CharSequence?, val getName: (T) -> CharSequence?,
val getChildren: (T) -> Sequence<T>, val getChildren: (T) -> Sequence<T>,
val getParent: (T) -> T?, val getParent: (T) -> T?,

View File

@ -1,4 +1,4 @@
package li.songe.selector.data package li.songe.selector
data class TupleExpression( data class TupleExpression(
val numbers: List<Int>, val numbers: List<Int>,

View File

@ -0,0 +1,71 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
sealed class PrimitiveType(val key: String) {
data object BooleanType : PrimitiveType("boolean")
data object IntType : PrimitiveType("int")
data object StringType : PrimitiveType("string")
data class ObjectType(val name: String) : PrimitiveType("object")
}
@JsExport
data class MethodInfo(
val name: String,
val returnType: TypeInfo,
val params: Array<TypeInfo> = emptyArray(),
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as MethodInfo
if (name != other.name) return false
if (returnType != other.returnType) return false
if (!params.contentEquals(other.params)) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + returnType.hashCode()
result = 31 * result + params.contentHashCode()
return result
}
}
@JsExport
data class PropInfo(
val name: String,
val type: TypeInfo,
)
@JsExport
data class TypeInfo(
val type: PrimitiveType,
var props: Array<PropInfo> = arrayOf(),
var methods: Array<MethodInfo> = arrayOf(),
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as TypeInfo
if (type != other.type) return false
if (!props.contentEquals(other.props)) return false
if (!methods.contentEquals(other.methods)) return false
return true
}
override fun hashCode(): Int {
var result = type.hashCode()
result = 31 * result + props.contentHashCode()
result = 31 * result + methods.contentHashCode()
return result
}
}

View File

@ -0,0 +1,135 @@
package li.songe.selector
import kotlin.js.JsExport
@JsExport
sealed class ValueExpression(open val value: Any?, open val type: String) : Position {
override fun stringify() = value.toString()
internal abstract fun <T> getAttr(node: T, transform: Transform<T>): Any?
abstract val properties: Array<String>
abstract val methods: Array<String>
sealed class Variable(
override val value: String,
) : ValueExpression(value, "var")
data class Identifier internal constructor(
override val start: Int,
override val value: String,
) : Variable(value) {
override val end = start + value.length
override fun <T> getAttr(node: T, transform: Transform<T>): Any? {
return transform.getAttr(node, value)
}
override val properties: Array<String>
get() = arrayOf(value)
override val methods: Array<String>
get() = emptyArray()
}
data class MemberExpression internal constructor(
override val start: Int,
override val end: Int,
val object0: Variable,
val property: String,
) : Variable(value = "${object0.stringify()}.$property") {
override fun <T> getAttr(node: T, transform: Transform<T>): Any? {
return transform.getAttr(object0.getAttr(node, transform), property)
}
override val properties: Array<String>
get() = arrayOf(*object0.properties, property)
override val methods: Array<String>
get() = object0.methods
}
data class CallExpression internal constructor(
override val start: Int,
override val end: Int,
val callee: Variable,
val arguments: List<ValueExpression>,
) : Variable(
value = "${callee.stringify()}(${arguments.joinToString(",") { it.stringify() }})",
) {
override fun <T> getAttr(node: T, transform: Transform<T>): Any? {
return when (callee) {
is CallExpression -> {
null
}
is Identifier -> {
transform.getInvoke(
node,
callee.value,
arguments.map { it.getAttr(node, transform) }
)
}
is MemberExpression -> {
transform.getInvoke(
callee.object0.getAttr(node, transform),
callee.property,
arguments.map { it.getAttr(node, transform) }
)
}
}
}
override val properties: Array<String>
get() = callee.properties.toMutableList()
.plus(arguments.flatMap { it.properties.toList() })
.toTypedArray()
override val methods: Array<String>
get() = when (callee) {
is CallExpression -> callee.methods
is Identifier -> arrayOf(callee.value)
is MemberExpression -> arrayOf(*callee.object0.methods, callee.property)
}.toMutableList().plus(arguments.flatMap { it.methods.toList() })
.toTypedArray()
}
sealed class LiteralExpression(
override val value: Any?,
override val type: String,
) : ValueExpression(value, type) {
override fun <T> getAttr(node: T, transform: Transform<T>) = value
override val properties: Array<String>
get() = emptyArray()
override val methods: Array<String>
get() = emptyArray()
}
data class NullLiteral internal constructor(
override val start: Int,
) : LiteralExpression(null, "null") {
override val end = start + 4
}
data class BooleanLiteral internal constructor(
override val start: Int,
override val value: Boolean
) : LiteralExpression(value, "boolean") {
override val end = start + if (value) 4 else 5
}
data class IntLiteral internal constructor(
override val start: Int,
override val end: Int,
override val value: Int
) : LiteralExpression(value, "int")
data class StringLiteral internal constructor(
override val start: Int,
override val end: Int,
override val value: String,
internal val matches: ((CharSequence) -> Boolean)? = null
) : LiteralExpression(value, "string") {
override fun stringify() = escapeString(value)
internal val outMatches = matches?.let { optimizeMatchString(value) ?: it } ?: { false }
}
}

View File

@ -1,17 +0,0 @@
package li.songe.selector.data
import li.songe.selector.Transform
data class BinaryExpression(
val name: String,
val operator: CompareOperator,
val value: PrimitiveValue
) : Expression() {
override fun <T> match(node: T, transform: Transform<T>) =
operator.compare(transform.getAttr(node, name), value)
override val binaryExpressions
get() = arrayOf(this)
override fun toString() = "${name}${operator.key}${value}"
}

View File

@ -1,194 +0,0 @@
package li.songe.selector.data
sealed class CompareOperator(val key: String) {
abstract fun compare(left: Any?, right: PrimitiveValue): Boolean
abstract fun allowType(type: PrimitiveValue): Boolean
companion object {
// https://stackoverflow.com/questions/47648689
val allSubClasses by lazy {
listOf(
Equal,
NotEqual,
Start,
NotStart,
Include,
NotInclude,
End,
NotEnd,
Less,
LessEqual,
More,
MoreEqual,
Matches,
NotMatches
).sortedBy { -it.key.length }.toTypedArray()
}
// example
// id="com.lptiyu.tanke:id/ab1"
// id="com.lptiyu.tanke:id/ab2"
private fun CharSequence.contentReversedEquals(other: CharSequence): Boolean {
if (this === other) return true
if (this.length != other.length) return false
for (i in this.length - 1 downTo 0) {
if (this[i] != other[i]) return false
}
return true
}
}
data object Equal : CompareOperator("=") {
override fun compare(left: Any?, right: PrimitiveValue): Boolean {
return if (left is CharSequence && right is PrimitiveValue.StringValue) {
left.contentReversedEquals(right.value)
} else {
left == right.value
}
}
override fun allowType(type: PrimitiveValue) = true
}
data object NotEqual : CompareOperator("!=") {
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: 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: 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: 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: 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: 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: 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: 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 LessEqual : CompareOperator("<=") {
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: 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: 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

@ -1,30 +0,0 @@
package li.songe.selector.data
import li.songe.selector.Transform
data class LogicalExpression(
val left: Expression,
val operator: LogicalOperator,
val right: Expression,
) : Expression() {
override fun <T> match(node: T, transform: Transform<T>): Boolean {
return operator.compare(node, transform, left, right)
}
override val binaryExpressions
get() = left.binaryExpressions + right.binaryExpressions
override fun toString(): String {
val leftStr = if (left is LogicalExpression && left.operator != operator) {
"($left)"
} else {
left.toString()
}
val rightStr = if (right is LogicalExpression && right.operator != operator) {
"($right)"
} else {
right.toString()
}
return "$leftStr\u0020${operator.key}\u0020$rightStr"
}
}

View File

@ -1,95 +0,0 @@
package li.songe.selector.data
sealed class PrimitiveValue(open val value: Any?, open val type: String) {
data object NullValue : PrimitiveValue(null, "null") {
override fun toString() = "null"
}
data class BooleanValue(override val value: Boolean) : PrimitiveValue(value, TYPE_NAME) {
override fun toString() = value.toString()
companion object {
const val TYPE_NAME = "boolean"
}
}
data class IntValue(override val value: Int) : PrimitiveValue(value, TYPE_NAME) {
override fun toString() = value.toString()
companion object {
const val TYPE_NAME = "int"
}
}
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_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 {
val wrapChar = '"'
val sb = StringBuilder(value.length + 2)
sb.append(wrapChar)
value.forEach { c ->
val escapeChar = when (c) {
wrapChar -> wrapChar
'\n' -> 'n'
'\r' -> 'r'
'\t' -> 't'
'\b' -> 'b'
'\\' -> '\\'
else -> null
}
if (escapeChar != null) {
sb.append("\\" + escapeChar)
} else {
when (c.code) {
in 0..0xf -> {
sb.append("\\x0" + c.code.toString(16))
}
in 0x10..0x1f -> {
sb.append("\\x" + c.code.toString(16))
}
else -> {
sb.append(c)
}
}
}
}
sb.append(wrapChar)
return sb.toString()
}
}
}

View File

@ -2,7 +2,7 @@ package li.songe.selector.parser
internal data class Parser<T>( internal data class Parser<T>(
val prefix: String = "", val prefix: String = "",
private val temp: (source: String, offset: Int, prefix: String) -> ParserResult<T> private val temp: (source: CharSequence, offset: Int, prefix: String) -> ParserResult<T>
) { ) {
operator fun invoke(source: String, offset: Int) = temp(source, offset, prefix) operator fun invoke(source: CharSequence, offset: Int) = temp(source, offset, prefix)
} }

View File

@ -1,22 +1,28 @@
package li.songe.selector.parser package li.songe.selector.parser
import li.songe.selector.BinaryExpression
import li.songe.selector.CompareOperator
import li.songe.selector.ConnectExpression
import li.songe.selector.ConnectOperator
import li.songe.selector.ConnectSegment
import li.songe.selector.ConnectWrapper
import li.songe.selector.Expression
import li.songe.selector.LogicalExpression
import li.songe.selector.LogicalOperator
import li.songe.selector.NotExpression
import li.songe.selector.PolynomialExpression
import li.songe.selector.Position
import li.songe.selector.PositionImpl
import li.songe.selector.PropertySegment
import li.songe.selector.PropertyWrapper
import li.songe.selector.Selector import li.songe.selector.Selector
import li.songe.selector.data.BinaryExpression import li.songe.selector.TupleExpression
import li.songe.selector.data.CompareOperator import li.songe.selector.ValueExpression
import li.songe.selector.data.ConnectExpression
import li.songe.selector.data.ConnectOperator
import li.songe.selector.data.ConnectSegment
import li.songe.selector.data.ConnectWrapper
import li.songe.selector.data.Expression
import li.songe.selector.data.LogicalExpression
import li.songe.selector.data.LogicalOperator
import li.songe.selector.data.PolynomialExpression
import li.songe.selector.data.PrimitiveValue
import li.songe.selector.data.PropertySegment
import li.songe.selector.data.PropertyWrapper
import li.songe.selector.data.TupleExpression
import li.songe.selector.gkdAssert import li.songe.selector.gkdAssert
import li.songe.selector.gkdError import li.songe.selector.gkdError
import li.songe.selector.parser.ParserSet.connectSelectorParser
import li.songe.selector.parser.ParserSet.endParser
import li.songe.selector.parser.ParserSet.whiteCharParser
import li.songe.selector.toMatches import li.songe.selector.toMatches
internal object ParserSet { internal object ParserSet {
@ -212,10 +218,26 @@ internal object ParserSet {
i++ i++
ParserResult(TupleExpression(numbers), i - offset) ParserResult(TupleExpression(numbers), i - offset)
} }
private val tupleExpressionReg = Regex("^\\(\\s*\\d+,.*$")
private fun isTupleExpression(source: CharSequence): Boolean {
// ^\(\s*\d+\s*,
var i = 0
if (source.getOrNull(i) != '(') {
return false
}
i++
i += whiteCharParser(source, i).length
if (source.getOrNull(i) !in '0'..'9') {
return false
}
i += integerParser(source, i).length
i += whiteCharParser(source, i).length
return source.getOrNull(i) == ','
}
val connectExpressionParser = Parser(polynomialExpressionParser.prefix) { source, offset, _ -> val connectExpressionParser = Parser(polynomialExpressionParser.prefix) { source, offset, _ ->
var i = offset var i = offset
if (tupleExpressionReg.matches(source.subSequence(offset, source.length))) { if (isTupleExpression(source.subSequence(offset, source.length))) {
val tupleExpressionResult = tupleExpressionParser(source, i) val tupleExpressionResult = tupleExpressionParser(source, i)
i += tupleExpressionResult.length i += tupleExpressionResult.length
ParserResult(tupleExpressionResult.data, i - offset) ParserResult(tupleExpressionResult.data, i - offset)
@ -244,13 +266,20 @@ internal object ParserSet {
) )
} }
val attrOperatorParser = private fun attrOperatorParser(
Parser(CompareOperator.allSubClasses.joinToString("") { it.key }) { source, offset, _ -> source: CharSequence,
val operator = CompareOperator.allSubClasses.find { compareOperator -> offset: Int
source.startsWith(compareOperator.key, offset) ): PositionImpl<CompareOperator> {
} ?: gkdError(source, offset, "CompareOperator") val operator = CompareOperator.allSubClasses.find { compareOperator ->
ParserResult(operator, operator.key.length) source.startsWith(compareOperator.key, offset)
} } ?: gkdError(source, offset, "CompareOperator")
return PositionImpl(
start = offset,
end = offset + operator.key.length,
value = operator
)
}
val stringParser = Parser("`'\"") { source, offset, prefix -> val stringParser = Parser("`'\"") { source, offset, prefix ->
var i = offset var i = offset
gkdAssert(source, i, prefix) gkdAssert(source, i, prefix)
@ -312,7 +341,7 @@ internal object ParserSet {
private val varPrefix = "_" + ('a'..'z').joinToString("") + ('A'..'Z').joinToString("") private val varPrefix = "_" + ('a'..'z').joinToString("") + ('A'..'Z').joinToString("")
private val varStr = varPrefix + '.' + ('0'..'9').joinToString("") private val varStr = varPrefix + '.' + ('0'..'9').joinToString("")
val propertyParser = Parser(varPrefix) { source, offset, prefix -> private val propertyParser = Parser(varPrefix) { source, offset, prefix ->
var i = offset var i = offset
gkdAssert(source, i, prefix) gkdAssert(source, i, prefix)
var data = source[i].toString() var data = source[i].toString()
@ -327,120 +356,253 @@ internal object ParserSet {
ParserResult(data, i - offset) ParserResult(data, i - offset)
} }
val valueParser = private fun isVarChar(c: Char?, start: Boolean = false): Boolean {
Parser("tfn-" + stringParser.prefix + integerParser.prefix) { source, offset, prefix -> c ?: return false
var i = offset return (c == '_' || c in 'a'..'z' || c in 'A'..'Z' || (!start && c in '0'..'9'))
gkdAssert(source, i, prefix) }
val value: PrimitiveValue = when (source[i]) {
't' -> { private fun matchLiteral(source: CharSequence, offset: Int, raw: String): Boolean {
if (source.startsWith(raw, offset)) {
val c = source.getOrNull(offset + raw.length) ?: return true
return !(c == '_' || c in 'a'..'z' || c in 'A'..'Z' || c in '0'..'9')
}
return false
}
fun parseVariable(source: CharSequence, offset: Int): ValueExpression {
var i = offset
i += whiteCharParser(source, i).length
if (i >= source.length) {
gkdError(source, i, "Variable")
}
if (matchLiteral(source, i, "true")) {
return ValueExpression.BooleanLiteral(start = i, value = true)
} else if (matchLiteral(source, i, "false")) {
return ValueExpression.BooleanLiteral(start = i, value = false)
} else if (matchLiteral(source, i, "null")) {
return ValueExpression.NullLiteral(start = i)
}
if (source[i] in stringParser.prefix) {
val result = stringParser(source, i)
i += result.length
return ValueExpression.StringLiteral(
start = i - result.length,
end = i,
value = result.data
)
}
if (source[i] == '-') {
i++
val result = integerParser(source, i)
i += result.length
return ValueExpression.IntLiteral(
start = i - result.length - 1,
end = i,
value = -result.data
)
}
if (source[i] in integerParser.prefix) {
val result = integerParser(source, i)
i += result.length
return ValueExpression.IntLiteral(
start = i - result.length,
end = i, value = result.data
)
}
var lastToken: ValueExpression.Variable? = null
while (i < source.length) {
i += whiteCharParser(source, i).length
val char = source.getOrNull(i)
when {
char == '(' -> {
val start = i
i++ i++
"rue".forEach { c -> i += whiteCharParser(source, i).length
gkdAssert(source, i, c.toString()) if (lastToken != null) {
// 暂不支持 object()()
if (lastToken is ValueExpression.CallExpression) {
gkdError(source, i, "Variable")
}
val arguments = mutableListOf<ValueExpression>()
while (i < source.length && source[i] != ')') {
val result = parseVariable(source, i)
arguments.add(result)
i += result.length
if (source.getOrNull(i) == ',') {
i++
i += whiteCharParser(source, i).length
}
}
i += whiteCharParser(source, i).length
gkdAssert(source, i, ")")
i++ i++
} lastToken = ValueExpression.CallExpression(
PrimitiveValue.BooleanValue(true) start = lastToken.start,
} end = i,
lastToken,
'f' -> { arguments
i++ )
"alse".forEach { c -> } else {
gkdAssert(source, i, c.toString()) val result = parseVariable(source, i)
i += result.length
i += whiteCharParser(source, i).length
gkdAssert(source, i, ")")
i++ i++
val end = i
return when (result) {
is ValueExpression.BooleanLiteral -> result.copy(
start = start
)
is ValueExpression.IntLiteral -> result.copy(start = start, end = end)
is ValueExpression.NullLiteral -> result.copy(start = start)
is ValueExpression.StringLiteral -> result.copy(
start = start,
end = end
)
is ValueExpression.CallExpression -> result.copy(
start = start,
end = end
)
is ValueExpression.Identifier -> result.copy(start = start)
is ValueExpression.MemberExpression -> result.copy(
start = start,
end = end
)
}
} }
PrimitiveValue.BooleanValue(false)
} }
'n' -> { char == '.' -> {
i++ i++
"ull".forEach { c -> if (lastToken !is ValueExpression.Variable) {
gkdAssert(source, i, c.toString()) gkdError(source, i, "Variable")
i++
} }
PrimitiveValue.NullValue if (!isVarChar(source.getOrNull(i), true)) {
gkdError(source, i, "Variable")
}
val property = source.drop(i).takeWhile { c -> isVarChar(c, false) }.toString()
lastToken = ValueExpression.MemberExpression(
start = lastToken.start,
end = i + property.length,
lastToken,
property
)
i += property.length
} }
in stringParser.prefix -> { isVarChar(char) -> {
val s = stringParser(source, i) val variable = source.drop(i).takeWhile { c -> isVarChar(c) }.toString()
i += s.length lastToken = ValueExpression.Identifier(start = i, variable)
PrimitiveValue.StringValue(s.data) i += variable.length
}
'-' -> {
i++
gkdAssert(source, i, integerParser.prefix)
val n = integerParser(source, i)
i += n.length
PrimitiveValue.IntValue(-n.data)
}
in integerParser.prefix -> {
val n = integerParser(source, i)
i += n.length
PrimitiveValue.IntValue(n.data)
} }
else -> { else -> {
gkdError(source, i, prefix) break
} }
} }
ParserResult(value, i - offset)
} }
if (lastToken == null) {
gkdError(source, i, "Variable")
}
return lastToken
}
val binaryExpressionParser = Parser { source, offset, _ -> private fun valueParser(source: CharSequence, offset: Int): ValueExpression {
val prefix = "tfn-" + stringParser.prefix + integerParser.prefix + varPrefix
gkdAssert(source, offset, prefix)
val result = parseVariable(source, offset)
return result
}
private fun binaryExpressionParser(source: CharSequence, offset: Int): BinaryExpression {
var i = offset var i = offset
val parserResult = propertyParser(source, i) val leftValueResult = valueParser(source, i)
i += parserResult.length i += leftValueResult.length
i += whiteCharParser(source, i).length i += whiteCharParser(source, i).length
val operatorResult = attrOperatorParser(source, i) val operatorResult = attrOperatorParser(source, i)
i += operatorResult.length i += operatorResult.length
i += whiteCharParser(source, i).length i += whiteCharParser(source, i).length
val valueResult = valueParser(source, i).let { result -> val rightValueResult = valueParser(source, i).let { result ->
// check regex // check regex
if ((operatorResult.data == CompareOperator.Matches || operatorResult.data == CompareOperator.NotMatches) && result.data is PrimitiveValue.StringValue) { if ((operatorResult.value == CompareOperator.Matches || operatorResult.value == CompareOperator.NotMatches) && result is ValueExpression.StringLiteral) {
val matches = try { val matches = try {
result.data.value.toMatches() result.value.toMatches()
} catch (e: Exception) { } catch (e: Exception) {
gkdError(source, i, "valid primitive string regex", e) gkdError(source, i, "valid primitive string regex", e)
} }
result.copy(data = result.data.copy(matches = matches)) result.copy(
matches = matches
)
} else { } else {
result result
} }
} }
if (!operatorResult.data.allowType(valueResult.data)) { i += rightValueResult.length
gkdError(source, i, "valid primitive value") return BinaryExpression(
} start = offset,
i += valueResult.length end = i,
ParserResult( leftValueResult,
BinaryExpression( operatorResult,
parserResult.data, operatorResult.data, valueResult.data rightValueResult
), i - offset
) )
} }
val logicalOperatorParser = Parser { source, offset, _ -> private fun logicalOperatorParser(
source: CharSequence,
offset: Int
): PositionImpl<LogicalOperator> {
var i = offset var i = offset
i += whiteCharParser(source, i).length i += whiteCharParser(source, i).length
val operator = LogicalOperator.allSubClasses.find { logicalOperator -> val operator = LogicalOperator.allSubClasses.find { logicalOperator ->
source.startsWith(logicalOperator.key, offset) source.startsWith(logicalOperator.key, offset)
} ?: gkdError(source, offset, "LogicalOperator") } ?: gkdError(source, offset, "LogicalOperator")
ParserResult(operator, operator.key.length) return PositionImpl(
start = i,
end = i + operator.key.length,
value = operator
)
} }
private fun unaryExpressionParser(
source: CharSequence,
offset: Int
): NotExpression {
var i = offset
i += whiteCharParser(source, i).length
gkdAssert(source, i, "!")
val start = i
i += 1
gkdAssert(source, i, "(")
val expression = expressionParser(source, i, true)
i += expression.length
return NotExpression(
start = start,
expression
)
}
// a>1 && a>1 || a>1 // a>1 && a>1 || a>1
// (a>1 || a>1) && a>1 // (a>1 || a>1) && a>1
fun expressionParser(source: String, offset: Int): ParserResult<Expression> { fun expressionParser(
source: CharSequence,
offset: Int,
one: Boolean = false, // 是否只解析一个表达式
): Expression {
var i = offset var i = offset
i += whiteCharParser(source, i).length i += whiteCharParser(source, i).length
// [exp, ||, exp, &&, &&] // [exp, ||, exp, &&, &&]
val parserResults = mutableListOf<ParserResult<*>>() val parserResults = mutableListOf<Position>()
while (i < source.length && source[i] != ']' && source[i] != ')') { while (i < source.length && source[i] != ']' && source[i] != ')') {
when (source[i]) { when (source[i]) {
'(' -> { '(' -> {
val start = i
if (parserResults.isNotEmpty()) { if (parserResults.isNotEmpty()) {
val lastToken = parserResults.last() val lastToken = parserResults.last()
if (lastToken.data !is LogicalOperator) { if (!(lastToken is PositionImpl<*> && lastToken.value is LogicalOperator)) {
var count = 0 var count = 0
while (i - 1 >= count && source[i - 1 - count] in whiteCharParser.prefix) { while (i - 1 >= count && source[i - 1 - count] in whiteCharParser.prefix) {
count++ count++
@ -450,16 +612,44 @@ internal object ParserSet {
) )
} }
} }
// [(a)=1]
// [(a=1)]
i++ i++
parserResults.add(expressionParser(source, i).apply { i += length }) val exp = expressionParser(source, i).apply { i += length }
gkdAssert(source, i, ")") gkdAssert(source, i, ")")
i++ i++
val end = i
parserResults.add(
when (exp) {
is BinaryExpression -> exp.copy(
start = start,
end = end
)
is LogicalExpression -> exp.copy(
start = start,
end = end
)
is NotExpression -> exp.copy(
start = start
)
}
)
if (one) {
break
}
} }
in "|&" -> { in "|&" -> {
parserResults.add(logicalOperatorParser(source, i).apply { i += length }) parserResults.add(logicalOperatorParser(source, i).apply { i += length })
i += whiteCharParser(source, i).length i += whiteCharParser(source, i).length
gkdAssert(source, i, "(" + propertyParser.prefix) gkdAssert(source, i, "(!" + propertyParser.prefix)
}
'!' -> {
parserResults.add(unaryExpressionParser(source, i).apply { i += length })
i += whiteCharParser(source, i).length
} }
else -> { else -> {
@ -474,21 +664,29 @@ internal object ParserSet {
) )
} }
if (parserResults.size == 1) { if (parserResults.size == 1) {
return ParserResult(parserResults.first().data as Expression, i - offset) return parserResults.first() as Expression
} }
// 运算符优先级 && > || // 运算符优先级 && > ||
// a && b || c -> ab || c // a && b || c -> ab || c
// 0 1 2 3 4 -> 0 1 2 // 0 1 2 3 4 -> 0 1 2
val tokens = parserResults.map { it.data }.toMutableList() val tokens = parserResults.toMutableList()
var index = 0 var index = 0
while (index < tokens.size) { while (index < tokens.size) {
val token = tokens[index] val token = tokens[index]
if (token == LogicalOperator.AndOperator) { if (token is PositionImpl<*> && token.value == LogicalOperator.AndOperator) {
val left = tokens[index - 1] as Expression
val right = tokens[index + 1] as Expression
@Suppress("UNCHECKED_CAST")
val operator = token as PositionImpl<LogicalOperator>
tokens[index] = LogicalExpression( tokens[index] = LogicalExpression(
left = tokens[index - 1] as Expression, start = left.start,
operator = LogicalOperator.AndOperator, end = right.end,
right = tokens[index + 1] as Expression left = left,
operator = operator,
right = right
) )
tokens.removeAt(index - 1) tokens.removeAt(index - 1)
tokens.removeAt(index + 1 - 1) tokens.removeAt(index + 1 - 1)
@ -497,15 +695,22 @@ internal object ParserSet {
} }
} }
while (tokens.size > 1) { while (tokens.size > 1) {
val left = tokens[0] as Expression
@Suppress("UNCHECKED_CAST")
val operator = tokens[1] as PositionImpl<LogicalOperator>
val right = tokens[2] as Expression
tokens[1] = LogicalExpression( tokens[1] = LogicalExpression(
left = tokens[0] as Expression, start = left.start,
operator = tokens[1] as LogicalOperator.OrOperator, end = right.end,
right = tokens[2] as Expression left = left,
operator = operator,
right = right
) )
tokens.removeAt(0) tokens.removeAt(0)
tokens.removeAt(2 - 1) tokens.removeAt(2 - 1)
} }
return ParserResult(tokens.first() as Expression, i - offset) return tokens.first() as Expression
} }
@ -519,7 +724,7 @@ internal object ParserSet {
gkdAssert(source, i, "]") gkdAssert(source, i, "]")
i++ i++
ParserResult( ParserResult(
exp.data, i - offset exp, i - offset
) )
} }
@ -578,30 +783,30 @@ internal object ParserSet {
} }
ParserResult(Unit, 0) ParserResult(Unit, 0)
} }
}
val selectorParser: (String) -> Selector = { source ->
var i = 0 internal fun selectorParser(source: String): Selector {
i += whiteCharParser(source, i).length var i = 0
val combinatorSelectorResult = connectSelectorParser(source, i) i += whiteCharParser(source, i).length
i += combinatorSelectorResult.length val combinatorSelectorResult = connectSelectorParser(source, i)
i += combinatorSelectorResult.length
i += whiteCharParser(source, i).length
i += endParser(source, i).length i += whiteCharParser(source, i).length
val data = combinatorSelectorResult.data i += endParser(source, i).length
val propertySelectorList = mutableListOf<PropertySegment>() val data = combinatorSelectorResult.data
val combinatorSelectorList = mutableListOf<ConnectSegment>() val propertySelectorList = mutableListOf<PropertySegment>()
propertySelectorList.add(data.first) val combinatorSelectorList = mutableListOf<ConnectSegment>()
data.second.forEach { propertySelectorList.add(data.first)
propertySelectorList.add(it.second) data.second.forEach {
combinatorSelectorList.add(it.first) propertySelectorList.add(it.second)
} combinatorSelectorList.add(it.first)
val wrapperList = mutableListOf(PropertyWrapper(propertySelectorList.first())) }
combinatorSelectorList.forEachIndexed { index, combinatorSelector -> val wrapperList = mutableListOf(PropertyWrapper(propertySelectorList.first()))
val combinatorSelectorWrapper = ConnectWrapper(combinatorSelector, wrapperList.last()) combinatorSelectorList.forEachIndexed { index, combinatorSelector ->
val propertySelectorWrapper = val combinatorSelectorWrapper = ConnectWrapper(combinatorSelector, wrapperList.last())
PropertyWrapper(propertySelectorList[index + 1], combinatorSelectorWrapper) val propertySelectorWrapper =
wrapperList.add(propertySelectorWrapper) PropertyWrapper(propertySelectorList[index + 1], combinatorSelectorWrapper)
} wrapperList.add(propertySelectorWrapper)
Selector(wrapperList.last()) }
} return Selector(source, wrapperList.last())
} }

View File

@ -0,0 +1,280 @@
package li.songe.selector
import kotlin.js.JsExport
fun escapeString(value: String, wrapChar: Char = '"'): String {
val sb = StringBuilder(value.length + 2)
sb.append(wrapChar)
value.forEach { c ->
val escapeChar = when (c) {
wrapChar -> wrapChar
'\n' -> 'n'
'\r' -> 'r'
'\t' -> 't'
'\b' -> 'b'
'\\' -> '\\'
else -> null
}
if (escapeChar != null) {
sb.append("\\" + escapeChar)
} else {
when (c.code) {
in 0..0xf -> {
sb.append("\\x0" + c.code.toString(16))
}
in 0x10..0x1f -> {
sb.append("\\x" + c.code.toString(16))
}
else -> {
sb.append(c)
}
}
}
}
sb.append(wrapChar)
return sb.toString()
}
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
}
internal fun optimizeMatchString(value: String): ((CharSequence) -> Boolean)? {
getMatchValue(value, "(?is)", ".*")?.let { startsWithValue ->
return { value -> value.startsWith(startsWithValue, ignoreCase = true) }
}
getMatchValue(value, "(?is).*", ".*")?.let { containsValue ->
return { value -> value.contains(containsValue, ignoreCase = true) }
}
getMatchValue(value, "(?is).*", "")?.let { endsWithValue ->
return { value -> value.endsWith(endsWithValue, ignoreCase = true) }
}
return null
}
@JsExport
class DefaultTypeInfo(
val booleanType: TypeInfo,
val intType: TypeInfo,
val stringType: TypeInfo,
val contextType: TypeInfo,
val nodeType: TypeInfo,
)
@JsExport
fun initDefaultTypeInfo(): DefaultTypeInfo {
val booleanType = TypeInfo(PrimitiveType.BooleanType)
val intType = TypeInfo(PrimitiveType.IntType)
val stringType = TypeInfo(PrimitiveType.StringType)
intType.methods = arrayOf(
MethodInfo("toString", stringType),
MethodInfo("toString", stringType, arrayOf(intType)),
MethodInfo("plus", intType, arrayOf(intType)),
MethodInfo("minus", intType, arrayOf(intType)),
MethodInfo("times", intType, arrayOf(intType)),
MethodInfo("div", intType, arrayOf(intType)),
MethodInfo("rem", intType, arrayOf(intType)),
)
stringType.props = arrayOf(
PropInfo("length", intType),
)
stringType.methods = arrayOf(
MethodInfo("get", stringType, arrayOf(intType)),
MethodInfo("at", stringType, arrayOf(intType)),
MethodInfo("substring", stringType, arrayOf(intType, intType)),
MethodInfo("toInt", intType),
MethodInfo("toInt", intType, arrayOf(intType)),
MethodInfo("indexOf", intType, arrayOf(stringType)),
MethodInfo("indexOf", intType, arrayOf(stringType, intType)),
)
val contextType = TypeInfo(PrimitiveType.ObjectType("context"))
val nodeType = TypeInfo(PrimitiveType.ObjectType("node"))
nodeType.props = arrayOf(
PropInfo("_id", intType),
PropInfo("_pid", intType),
PropInfo("id", stringType),
PropInfo("vid", stringType),
PropInfo("name", stringType),
PropInfo("text", stringType),
PropInfo("desc", stringType),
PropInfo("clickable", booleanType),
PropInfo("focusable", booleanType),
PropInfo("checkable", booleanType),
PropInfo("checked", booleanType),
PropInfo("editable", booleanType),
PropInfo("longClickable", booleanType),
PropInfo("visibleToUser", booleanType),
PropInfo("left", intType),
PropInfo("top", intType),
PropInfo("right", intType),
PropInfo("bottom", intType),
PropInfo("width", intType),
PropInfo("height", intType),
PropInfo("childCount", intType),
PropInfo("index", intType),
PropInfo("depth", intType),
PropInfo("parent", nodeType),
)
nodeType.methods = arrayOf(
MethodInfo("getChild", nodeType, arrayOf(intType)),
)
contextType.methods = arrayOf(*nodeType.methods)
contextType.props = arrayOf(*nodeType.props)
return DefaultTypeInfo(
booleanType = booleanType,
intType = intType,
stringType = stringType,
contextType = contextType,
nodeType = nodeType
)
}
@JsExport
fun getIntInvoke(target: Int, name: String, args: List<Any?>): Any? {
return when (name) {
"plus" -> {
target + (args.getIntOrNull() ?: return null)
}
"minus" -> {
target - (args.getIntOrNull() ?: return null)
}
"times" -> {
target * (args.getIntOrNull() ?: return null)
}
"div" -> {
target / (args.getIntOrNull()?.also { if (it == 0) return null } ?: return null)
}
"rem" -> {
target % (args.getIntOrNull()?.also { if (it == 0) return null } ?: return null)
}
else -> null
}
}
internal fun List<Any?>.getIntOrNull(i: Int = 0): Int? {
val v = getOrNull(i)
if (v is Int) return v
return null
}
@JsExport
fun getStringInvoke(target: String, name: String, args: List<Any?>): Any? {
return getCharSequenceInvoke(target, name, args)
}
fun getCharSequenceInvoke(target: CharSequence, name: String, args: List<Any?>): Any? {
return when (name) {
"get" -> {
target.getOrNull(args.getIntOrNull() ?: return null).toString()
}
"at" -> {
val i = args.getIntOrNull() ?: return null
if (i < 0) {
target.getOrNull(target.length + i).toString()
} else {
target.getOrNull(i).toString()
}
}
"substring" -> {
when (args.size) {
1 -> {
val start = args.getIntOrNull() ?: return null
if (start < 0) return null
if (start >= target.length) return ""
target.substring(
start,
target.length
)
}
2 -> {
val start = args.getIntOrNull() ?: return null
if (start < 0) return null
if (start >= target.length) return ""
val end = args.getIntOrNull(1) ?: return null
if (end < start) return null
target.substring(
start,
end.coerceAtMost(target.length)
)
}
else -> {
null
}
}
}
"toInt" -> when (args.size) {
0 -> target.toString().toIntOrNull()
1 -> {
val radix = args.getIntOrNull() ?: return null
if (radix !in 2..36) {
return null
}
target.toString().toIntOrNull(radix)
}
else -> null
}
"indexOf" -> {
when (args.size) {
1 -> {
val str = args[0] as? CharSequence ?: return null
target.indexOf(str.toString())
}
2 -> {
val str = args[0] as? CharSequence ?: return null
val startIndex = args.getIntOrNull(1) ?: return null
target.indexOf(str.toString(), startIndex)
}
else -> null
}
}
else -> null
}
}
@JsExport
fun getStringAttr(target: String, name: String): Any? {
return getCharSequenceAttr(target, name)
}
fun getCharSequenceAttr(target: CharSequence, name: String): Any? {
return when (name) {
"length" -> target.length
else -> null
}
}

View File

@ -24,15 +24,47 @@ class ParserTest {
private val json = Json { private val json = Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }
private val transform = Transform<TestNode>(getAttr = { node, name ->
if (name == "_id") return@Transform node.id private fun getNodeAttr(node: TestNode, name: String): Any? {
if (name == "_pid") return@Transform node.pid if (name == "_id") return node.id
val value = node.attr[name] ?: return@Transform null if (name == "_pid") return node.pid
if (value is JsonNull) return@Transform null if (name == "parent") return node.parent
value.intOrNull ?: value.booleanOrNull ?: value.content val value = node.attr[name] ?: return null
}, getName = { node -> node.attr["name"]?.content }, getChildren = { node -> if (value is JsonNull) return null
node.children.asSequence() return value.intOrNull ?: value.booleanOrNull ?: value.content
}, getParent = { node -> node.parent }) }
private fun getNodeInvoke(target: TestNode, name: String, args: List<Any?>): Any? {
when (name) {
"getChild" -> {
val arg = (args.getIntOrNull() ?: return null)
return target.children.getOrNull(arg)
}
}
return null
}
private val transform = Transform<TestNode>(
getAttr = { target, name ->
when (target) {
is TestNode -> getNodeAttr(target, name)
is String -> getCharSequenceAttr(target, name)
else -> null
}
},
getInvoke = { target, name, args ->
when (target) {
is Int -> getIntInvoke(target, name, args)
is CharSequence -> getCharSequenceInvoke(target, name, args)
is TestNode -> getNodeInvoke(target, name, args)
else -> null
}
},
getName = { node -> node.attr["name"]?.content },
getChildren = { node -> node.children.asSequence() },
getParent = { node -> node.parent }
)
private val idToSnapshot = HashMap<String, TestNode>() private val idToSnapshot = HashMap<String, TestNode>()
@ -42,7 +74,7 @@ class ParserTest {
val file = assetsDir.resolve("$githubAssetId.json") val file = assetsDir.resolve("$githubAssetId.json")
if (!file.exists()) { if (!file.exists()) {
URL("https://github.com/gkd-kit/inspect/files/${githubAssetId}/file.zip").openStream() URL("https://f.gkd.li/${githubAssetId}").openStream()
.use { inputStream -> .use { inputStream ->
val zipInputStream = ZipInputStream(inputStream) val zipInputStream = ZipInputStream(inputStream)
var entry = zipInputStream.nextEntry var entry = zipInputStream.nextEntry
@ -78,7 +110,7 @@ class ParserTest {
@Test @Test
fun test_expression() { fun test_expression() {
println(ParserSet.expressionParser("a>1&&b>1&&c>1||d>1", 0).data) println(ParserSet.expressionParser("a>1&&b>1&&c>1||d>1", 0))
println(Selector.parse("View[a>1&&b>1&&c>1||d>1&&x^=1] > Button[a>1||b*='zz'||c^=1]")) println(Selector.parse("View[a>1&&b>1&&c>1||d>1&&x^=1] > Button[a>1||b*='zz'||c^=1]"))
println(Selector.parse("[id=`com.byted.pangle:id/tt_splash_skip_btn`||(id=`com.hupu.games:id/tv_time`&&text*=`跳过`)]")) println(Selector.parse("[id=`com.byted.pangle:id/tt_splash_skip_btn`||(id=`com.hupu.games:id/tv_time`&&text*=`跳过`)]"))
} }
@ -89,8 +121,8 @@ class ParserTest {
"ImageView < @FrameLayout < LinearLayout < RelativeLayout <n LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text\$='广告']" "ImageView < @FrameLayout < LinearLayout < RelativeLayout <n LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text\$='广告']"
val selector = Selector.parse(text) val selector = Selector.parse(text)
println("trackIndex: " + selector.trackIndex) println("trackIndex: " + selector.trackIndex)
println("canCacheIndex: " + Selector.parse("A + B").canCacheIndex) println("canCacheIndex: " + Selector.parse("A + B").useCache)
println("canCacheIndex: " + Selector.parse("A > B - C").canCacheIndex) println("canCacheIndex: " + Selector.parse("A > B - C").useCache)
} }
@Test @Test
@ -118,7 +150,7 @@ class ParserTest {
fun check_parser() { fun check_parser() {
val selector = Selector.parse("View > Text[index>-0]") val selector = Selector.parse("View > Text[index>-0]")
println("selector: $selector") println("selector: $selector")
println("canCacheIndex: " + selector.canCacheIndex) println("canCacheIndex: " + selector.useCache)
} }
@ -188,7 +220,7 @@ class ParserTest {
@Test @Test
fun check_regex() { fun check_regex() {
val source = "[vid~=`(?is)TV.*`]" val source = "[1<parent.getChild][vid=`im_cover`]"
println("source:$source") println("source:$source")
val selector = Selector.parse(source) val selector = Selector.parse(source)
val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/14445410") val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/14445410")
@ -196,4 +228,32 @@ class ParserTest {
println("result:" + transform.querySelectorAll(snapshotNode, selector).map { n -> n.id } println("result:" + transform.querySelectorAll(snapshotNode, selector).map { n -> n.id }
.toList()) .toList())
} }
@Test
fun check_var() {
val result = ParserSet.parseVariable("rem(3)", 0)
println("result: $result")
println("check_var: " + result.stringify())
val selector = Selector.parse("[vid.get(-2)=`l`]")
println("selector: $selector")
val snapshotNode = getOrDownloadNode("https://i.gkd.li/i/14445410")
println(
"result: [" + transform.querySelectorAll(snapshotNode, selector)
.joinToString("||") { "_id=${it.id}" } + "]"
)
}
@Test
fun check_type() {
val source =
"[visibleToUser=true][((parent.getChild(0,).getChild( (0), )=null) && (((`` >= 1)))) || (name=null && desc=null)]"
val selector = Selector.parse(source)
val typeInfo = initDefaultTypeInfo().contextType
val error = selector.checkType(typeInfo)
println("useCache: ${selector.useCache}")
println("error: $error")
println("check_type: $selector")
}
} }