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()
}.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 ->
if (s == null) {

View File

@ -130,7 +130,7 @@ sealed class ResolvedRule(
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
fun query(
@ -159,7 +159,7 @@ sealed class ResolvedRule(
}
return target
} finally {
defaultCacheTransform.indexCache.clear()
defaultCacheTransform.cache.clear()
}
}

View File

@ -4,9 +4,21 @@ import android.accessibilityservice.AccessibilityService
import android.graphics.Rect
import android.view.accessibility.AccessibilityEvent
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.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?
get() = try {
@ -51,24 +63,13 @@ inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode:
}
}
/**
* 此方法小概率造成无限节点片段,底层原因未知
*
* https://github.com/gkd-kit/gkd/issues/28
*/
fun AccessibilityNodeInfo.getDepth(): Int {
var p: AccessibilityNodeInfo? = this
var depth = 0
while (true) {
val p2 = p?.parent
if (p2 != null) {
p = p2
depth++
} else {
break
}
fun AccessibilityNodeInfo.getChildOrNull(i: Int?): AccessibilityNodeInfo? {
i ?: return null
return if (i in 0 until childCount) {
getChild(i)
} else {
null
}
return depth
}
fun AccessibilityNodeInfo.getVid(): CharSequence? {
@ -135,51 +136,34 @@ val getChildren: (AccessibilityNodeInfo) -> Sequence<AccessibilityNodeInfo> = {
}
}
val allowPropertyNames by lazy {
mapOf(
"id" to PrimitiveValue.StringValue.TYPE_NAME,
"vid" to PrimitiveValue.StringValue.TYPE_NAME,
"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,
)
private val typeInfo by lazy {
initDefaultTypeInfo().apply {
nodeType.props = nodeType.props.filter { !it.name.startsWith('_') }.toTypedArray()
contextType.props = contextType.props.filter { !it.name.startsWith('_') }.toTypedArray()
}.contextType
}
fun Selector.checkSelector(): String? {
binaryExpressions.forEach { e ->
if (!allowPropertyNames.contains(e.name)) {
return "未知属性:${e.name}"
}
if (e.value.type != "null" && allowPropertyNames[e.name] != e.value.type) {
return "非法类型:${e.name}=${e.value.type}"
}
val error = checkType(typeInfo) ?: return null
if (BuildConfig.DEBUG) {
LogUtils.d(
"Selector check error",
source,
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
val tempRect = Rect()
var tempVid: CharSequence? = null
@ -198,6 +182,28 @@ private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) {
}
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 ->
when (name) {
"id" -> node.viewIdResourceName
@ -226,8 +232,13 @@ private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) {
"height" -> node.getTempRect().height()
"index" -> node.getIndex()
"depth" -> node.getDepth()
"depth" -> node.getDepthX()
"childCount" -> node.childCount
"parent" -> cache.parent[node] ?: node.parent.apply {
cache.parent[node] = this
}
else -> null
}
}
@ -235,22 +246,43 @@ private fun createGetAttr(): ((AccessibilityNodeInfo, String) -> Any?) {
data class CacheTransform(
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 {
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? {
return getChild(index)?.also { child ->
indexCache[child] = index
return cache.child[this to index] ?: getChild(index)?.also { child ->
cache.index[child] = index
cache.parent[child] = this
cache.child[this to index] = child
}
}
fun AccessibilityNodeInfo.getIndexX(): Int {
indexCache[this]?.let { return it }
parent?.forEachIndexed { index, child ->
cache.index[this]?.let { return it }
getParentX()?.forEachIndexed { index, child ->
if (child != null) {
indexCache[child] = index
cache.index[child] = index
}
if (child == this) {
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(
getAttr = { node, name ->
when (name) {
"index" -> {
node.getIndexX()
when (node) {
is AccessibilityNodeInfo -> {
when (name) {
"index" -> {
node.getIndexX()
}
else -> {
getNodeAttr(node, name)
}
}
}
is CharSequence -> getCharSequenceAttr(node, name)
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 },
getChildren = getChildrenCache,
getParent = { node -> node.parent },
getParent = getParent,
getDescendants = { node ->
sequence {
val stack = getChildrenCache(node).toMutableList()
@ -319,9 +389,9 @@ fun createCacheTransform(): CacheTransform {
},
getBeforeBrothers = { node, connectExpression ->
sequence {
val parentVal = node.parent ?: return@sequence
val index =
indexCache[node] // 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 indexCache 是空
val parentVal = getParent(node) ?: return@sequence
// 如果 node 由 quickFind 得到, 则第一次调用此方法可能得到 cache.index 是空
val index = cache.index[node]
if (index != null) {
var i = index - 1
var offset = 0
@ -349,9 +419,9 @@ fun createCacheTransform(): CacheTransform {
}
},
getAfterBrothers = { node, connectExpression ->
val parentVal = node.parent
val parentVal = getParent(node)
if (parentVal != null) {
val index = indexCache[node]
val index = cache.index[node]
if (index != null) {
sequence {
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> {
val cache = NodeCache()
val getNodeAttr = createGetNodeAttr(cache)
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 },
getChildren = getChildren,
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_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_junit = { module = "androidx.test.ext:junit", version = "1.1.5" }
androidx_espresso = { module = "androidx.test.espresso:espresso-core", version = "3.5.1" }
androidx_junit = { module = "androidx.test.ext:junit", version = "1.2.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_compiler = { module = "androidx.room:room-compiler", 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 {
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) {
abstract fun <T> traversal(
internal abstract fun <T> traversal(
node: T, transform: Transform<T>, connectExpression: ConnectExpression
): Sequence<T>

View File

@ -1,6 +1,4 @@
package li.songe.selector.data
import li.songe.selector.Transform
package li.songe.selector
data class ConnectSegment(
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) {
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)
}
}

View File

@ -1,20 +1,18 @@
package li.songe.selector.data
import li.songe.selector.Transform
package li.songe.selector
data class ConnectWrapper(
val connectSegment: ConnectSegment,
val segment: ConnectSegment,
val to: PropertyWrapper,
) {
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>,
trackNodes: MutableList<T>,
): List<T>? {
connectSegment.traversal(node, transform).forEach {
segment.traversal(node, transform).forEach {
if (it == null) return@forEach
val r = to.matchTracks(it, transform, trackNodes)
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 {
sealed class Expression : Position {
internal abstract fun <T> match(node: T, transform: Transform<T>): Boolean
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 {
// https://stackoverflow.com/questions/47648689
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,
transform: Transform<T>,
left: Expression,

View File

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

View File

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

@ -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
import li.songe.selector.Transform
package li.songe.selector
data class PropertySegment(
@ -18,7 +16,7 @@ data class PropertySegment(
override fun toString(): String {
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 {

View File

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

View File

@ -1,13 +1,10 @@
package li.songe.selector
import li.songe.selector.data.BinaryExpression
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
import li.songe.selector.parser.selectorParser
class Selector internal constructor(private val propertyWrapper: PropertyWrapper) {
class Selector internal constructor(
val source: String, private val propertyWrapper: PropertyWrapper
) {
override fun toString(): String {
return propertyWrapper.toString()
}
@ -17,7 +14,7 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
while (true) {
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 ->
@ -41,25 +38,25 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
return propertyWrapper.matchTracks(node, transform, trackNodes)
}
val qfIdValue = propertyWrapper.propertySegment.expressions.firstOrNull().let { e ->
if (e is BinaryExpression && e.name == "id" && e.operator == CompareOperator.Equal && e.value is PrimitiveValue.StringValue) {
e.value.value
val qfIdValue = propertyWrapper.segment.expressions.firstOrNull().let { e ->
if (e is BinaryExpression && e.left.value == "id" && e.operator.value == CompareOperator.Equal && e.right is ValueExpression.StringLiteral) {
e.right.value
} else {
null
}
}
val qfVidValue = propertyWrapper.propertySegment.expressions.firstOrNull().let { e ->
if (e is BinaryExpression && e.name == "vid" && e.operator == CompareOperator.Equal && e.value is PrimitiveValue.StringValue) {
e.value.value
val qfVidValue = propertyWrapper.segment.expressions.firstOrNull().let { e ->
if (e is BinaryExpression && e.left.value == "vid" && e.operator.value == CompareOperator.Equal && e.right is ValueExpression.StringLiteral) {
e.right.value
} else {
null
}
}
val qfTextValue = propertyWrapper.propertySegment.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) {
e.value.value
val qfTextValue = propertyWrapper.segment.expressions.firstOrNull().let { e ->
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.right.value
} else {
null
}
@ -68,8 +65,8 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
val canQf = qfIdValue != null || qfVidValue != null || qfTextValue != null
// 主动查询
val isMatchRoot = propertyWrapper.propertySegment.expressions.firstOrNull().let { e ->
e is BinaryExpression && e.name == "depth" && e.operator == CompareOperator.Equal && e.value.value == 0
val isMatchRoot = propertyWrapper.segment.expressions.firstOrNull().let { e ->
e is BinaryExpression && e.left.value == "depth" && e.operator.value == CompareOperator.Equal && e.right.value == 0
}
val connectKeys = run {
@ -77,7 +74,7 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
val keys = mutableListOf<String>()
while (c != null) {
c.apply {
keys.add(connectSegment.operator.key)
keys.add(segment.operator.key)
}
c = c.to.to
}
@ -88,28 +85,144 @@ class Selector internal constructor(private val propertyWrapper: PropertyWrapper
var p: PropertyWrapper? = propertyWrapper
val expressions = mutableListOf<BinaryExpression>()
while (p != null) {
val s = p.propertySegment
val s = p.segment
expressions.addAll(s.binaryExpressions)
p = p.to?.to
}
expressions.distinct().toTypedArray()
}
val propertyNames = run {
binaryExpressions.map { e -> e.name }.distinct().toTypedArray()
val useCache = run {
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 =
connectKeys.contains(ConnectOperator.BeforeBrother.key) || connectKeys.contains(
ConnectOperator.AfterBrother.key
) || propertyNames.contains("index")
fun checkType(typeInfo: TypeInfo): SelectorCheckException? {
try {
propertyWrapper.segment.binaryExpressions.forEach { exp ->
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 {
fun parse(source: String) = ParserSet.selectorParser(source)
fun parse(source: String) = selectorParser(source)
fun parseOrNull(source: String) = try {
ParserSet.selectorParser(source)
selectorParser(source)
} catch (e: Exception) {
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
@JsExport
data class GkdSyntaxError internal constructor(
data class SyntaxError internal constructor(
val expectedValue: String,
val position: Int,
val source: String,
@ -17,21 +17,21 @@ data class GkdSyntaxError internal constructor(
)
internal fun gkdAssert(
source: String,
source: CharSequence,
offset: Int,
value: String = "",
expectedValue: String? = null
) {
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(
source: String,
source: CharSequence,
offset: Int,
expectedValue: String = "",
cause: Exception? = null
): Nothing {
throw GkdSyntaxError(expectedValue, offset, source, cause)
throw SyntaxError(expectedValue, offset, source.toString(), cause)
}

View File

@ -1,10 +1,9 @@
package li.songe.selector
import li.songe.selector.data.ConnectExpression
@Suppress("UNUSED")
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 getChildren: (T) -> Sequence<T>,
val getParent: (T) -> T?,

View File

@ -1,4 +1,4 @@
package li.songe.selector.data
package li.songe.selector
data class TupleExpression(
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>(
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
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.data.BinaryExpression
import li.songe.selector.data.CompareOperator
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.TupleExpression
import li.songe.selector.ValueExpression
import li.songe.selector.gkdAssert
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
internal object ParserSet {
@ -212,10 +218,26 @@ internal object ParserSet {
i++
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, _ ->
var i = offset
if (tupleExpressionReg.matches(source.subSequence(offset, source.length))) {
if (isTupleExpression(source.subSequence(offset, source.length))) {
val tupleExpressionResult = tupleExpressionParser(source, i)
i += tupleExpressionResult.length
ParserResult(tupleExpressionResult.data, i - offset)
@ -244,13 +266,20 @@ internal object ParserSet {
)
}
val attrOperatorParser =
Parser(CompareOperator.allSubClasses.joinToString("") { it.key }) { source, offset, _ ->
val operator = CompareOperator.allSubClasses.find { compareOperator ->
source.startsWith(compareOperator.key, offset)
} ?: gkdError(source, offset, "CompareOperator")
ParserResult(operator, operator.key.length)
}
private fun attrOperatorParser(
source: CharSequence,
offset: Int
): PositionImpl<CompareOperator> {
val operator = CompareOperator.allSubClasses.find { compareOperator ->
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 ->
var i = offset
gkdAssert(source, i, prefix)
@ -312,7 +341,7 @@ internal object ParserSet {
private val varPrefix = "_" + ('a'..'z').joinToString("") + ('A'..'Z').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
gkdAssert(source, i, prefix)
var data = source[i].toString()
@ -327,120 +356,253 @@ internal object ParserSet {
ParserResult(data, i - offset)
}
val valueParser =
Parser("tfn-" + stringParser.prefix + integerParser.prefix) { source, offset, prefix ->
var i = offset
gkdAssert(source, i, prefix)
val value: PrimitiveValue = when (source[i]) {
't' -> {
private fun isVarChar(c: Char?, start: Boolean = false): Boolean {
c ?: return false
return (c == '_' || c in 'a'..'z' || c in 'A'..'Z' || (!start && c in '0'..'9'))
}
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++
"rue".forEach { c ->
gkdAssert(source, i, c.toString())
i += whiteCharParser(source, i).length
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++
}
PrimitiveValue.BooleanValue(true)
}
'f' -> {
i++
"alse".forEach { c ->
gkdAssert(source, i, c.toString())
lastToken = ValueExpression.CallExpression(
start = lastToken.start,
end = i,
lastToken,
arguments
)
} else {
val result = parseVariable(source, i)
i += result.length
i += whiteCharParser(source, i).length
gkdAssert(source, 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++
"ull".forEach { c ->
gkdAssert(source, i, c.toString())
i++
if (lastToken !is ValueExpression.Variable) {
gkdError(source, i, "Variable")
}
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 -> {
val s = stringParser(source, i)
i += s.length
PrimitiveValue.StringValue(s.data)
}
'-' -> {
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)
isVarChar(char) -> {
val variable = source.drop(i).takeWhile { c -> isVarChar(c) }.toString()
lastToken = ValueExpression.Identifier(start = i, variable)
i += variable.length
}
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
val parserResult = propertyParser(source, i)
i += parserResult.length
val leftValueResult = valueParser(source, i)
i += leftValueResult.length
i += whiteCharParser(source, i).length
val operatorResult = attrOperatorParser(source, i)
i += operatorResult.length
i += whiteCharParser(source, i).length
val valueResult = valueParser(source, i).let { result ->
val rightValueResult = valueParser(source, i).let { result ->
// 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 {
result.data.value.toMatches()
result.value.toMatches()
} catch (e: Exception) {
gkdError(source, i, "valid primitive string regex", e)
}
result.copy(data = result.data.copy(matches = matches))
result.copy(
matches = matches
)
} else {
result
}
}
if (!operatorResult.data.allowType(valueResult.data)) {
gkdError(source, i, "valid primitive value")
}
i += valueResult.length
ParserResult(
BinaryExpression(
parserResult.data, operatorResult.data, valueResult.data
), i - offset
i += rightValueResult.length
return BinaryExpression(
start = offset,
end = i,
leftValueResult,
operatorResult,
rightValueResult
)
}
val logicalOperatorParser = Parser { source, offset, _ ->
private fun logicalOperatorParser(
source: CharSequence,
offset: Int
): PositionImpl<LogicalOperator> {
var i = offset
i += whiteCharParser(source, i).length
val operator = LogicalOperator.allSubClasses.find { logicalOperator ->
source.startsWith(logicalOperator.key, offset)
} ?: 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
fun expressionParser(source: String, offset: Int): ParserResult<Expression> {
fun expressionParser(
source: CharSequence,
offset: Int,
one: Boolean = false, // 是否只解析一个表达式
): Expression {
var i = offset
i += whiteCharParser(source, i).length
// [exp, ||, exp, &&, &&]
val parserResults = mutableListOf<ParserResult<*>>()
val parserResults = mutableListOf<Position>()
while (i < source.length && source[i] != ']' && source[i] != ')') {
when (source[i]) {
'(' -> {
val start = i
if (parserResults.isNotEmpty()) {
val lastToken = parserResults.last()
if (lastToken.data !is LogicalOperator) {
if (!(lastToken is PositionImpl<*> && lastToken.value is LogicalOperator)) {
var count = 0
while (i - 1 >= count && source[i - 1 - count] in whiteCharParser.prefix) {
count++
@ -450,16 +612,44 @@ internal object ParserSet {
)
}
}
// [(a)=1]
// [(a=1)]
i++
parserResults.add(expressionParser(source, i).apply { i += length })
val exp = expressionParser(source, i).apply { i += length }
gkdAssert(source, 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 "|&" -> {
parserResults.add(logicalOperatorParser(source, i).apply { 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 -> {
@ -474,21 +664,29 @@ internal object ParserSet {
)
}
if (parserResults.size == 1) {
return ParserResult(parserResults.first().data as Expression, i - offset)
return parserResults.first() as Expression
}
// 运算符优先级 && > ||
// a && b || c -> ab || c
// 0 1 2 3 4 -> 0 1 2
val tokens = parserResults.map { it.data }.toMutableList()
val tokens = parserResults.toMutableList()
var index = 0
while (index < tokens.size) {
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(
left = tokens[index - 1] as Expression,
operator = LogicalOperator.AndOperator,
right = tokens[index + 1] as Expression
start = left.start,
end = right.end,
left = left,
operator = operator,
right = right
)
tokens.removeAt(index - 1)
tokens.removeAt(index + 1 - 1)
@ -497,15 +695,22 @@ internal object ParserSet {
}
}
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(
left = tokens[0] as Expression,
operator = tokens[1] as LogicalOperator.OrOperator,
right = tokens[2] as Expression
start = left.start,
end = right.end,
left = left,
operator = operator,
right = right
)
tokens.removeAt(0)
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, "]")
i++
ParserResult(
exp.data, i - offset
exp, i - offset
)
}
@ -578,30 +783,30 @@ internal object ParserSet {
}
ParserResult(Unit, 0)
}
val selectorParser: (String) -> Selector = { source ->
var i = 0
i += whiteCharParser(source, i).length
val combinatorSelectorResult = connectSelectorParser(source, i)
i += combinatorSelectorResult.length
i += whiteCharParser(source, i).length
i += endParser(source, i).length
val data = combinatorSelectorResult.data
val propertySelectorList = mutableListOf<PropertySegment>()
val combinatorSelectorList = mutableListOf<ConnectSegment>()
propertySelectorList.add(data.first)
data.second.forEach {
propertySelectorList.add(it.second)
combinatorSelectorList.add(it.first)
}
val wrapperList = mutableListOf(PropertyWrapper(propertySelectorList.first()))
combinatorSelectorList.forEachIndexed { index, combinatorSelector ->
val combinatorSelectorWrapper = ConnectWrapper(combinatorSelector, wrapperList.last())
val propertySelectorWrapper =
PropertyWrapper(propertySelectorList[index + 1], combinatorSelectorWrapper)
wrapperList.add(propertySelectorWrapper)
}
Selector(wrapperList.last())
}
}
internal fun selectorParser(source: String): Selector {
var i = 0
i += whiteCharParser(source, i).length
val combinatorSelectorResult = connectSelectorParser(source, i)
i += combinatorSelectorResult.length
i += whiteCharParser(source, i).length
i += endParser(source, i).length
val data = combinatorSelectorResult.data
val propertySelectorList = mutableListOf<PropertySegment>()
val combinatorSelectorList = mutableListOf<ConnectSegment>()
propertySelectorList.add(data.first)
data.second.forEach {
propertySelectorList.add(it.second)
combinatorSelectorList.add(it.first)
}
val wrapperList = mutableListOf(PropertyWrapper(propertySelectorList.first()))
combinatorSelectorList.forEachIndexed { index, combinatorSelector ->
val combinatorSelectorWrapper = ConnectWrapper(combinatorSelector, wrapperList.last())
val propertySelectorWrapper =
PropertyWrapper(propertySelectorList[index + 1], combinatorSelectorWrapper)
wrapperList.add(propertySelectorWrapper)
}
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 {
ignoreUnknownKeys = true
}
private val transform = Transform<TestNode>(getAttr = { node, name ->
if (name == "_id") return@Transform node.id
if (name == "_pid") return@Transform node.pid
val value = node.attr[name] ?: return@Transform null
if (value is JsonNull) return@Transform null
value.intOrNull ?: value.booleanOrNull ?: value.content
}, getName = { node -> node.attr["name"]?.content }, getChildren = { node ->
node.children.asSequence()
}, getParent = { node -> node.parent })
private fun getNodeAttr(node: TestNode, name: String): Any? {
if (name == "_id") return node.id
if (name == "_pid") return node.pid
if (name == "parent") return node.parent
val value = node.attr[name] ?: return null
if (value is JsonNull) return null
return value.intOrNull ?: value.booleanOrNull ?: value.content
}
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>()
@ -42,7 +74,7 @@ class ParserTest {
val file = assetsDir.resolve("$githubAssetId.json")
if (!file.exists()) {
URL("https://github.com/gkd-kit/inspect/files/${githubAssetId}/file.zip").openStream()
URL("https://f.gkd.li/${githubAssetId}").openStream()
.use { inputStream ->
val zipInputStream = ZipInputStream(inputStream)
var entry = zipInputStream.nextEntry
@ -78,7 +110,7 @@ class ParserTest {
@Test
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("[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\$='广告']"
val selector = Selector.parse(text)
println("trackIndex: " + selector.trackIndex)
println("canCacheIndex: " + Selector.parse("A + B").canCacheIndex)
println("canCacheIndex: " + Selector.parse("A > B - C").canCacheIndex)
println("canCacheIndex: " + Selector.parse("A + B").useCache)
println("canCacheIndex: " + Selector.parse("A > B - C").useCache)
}
@Test
@ -118,7 +150,7 @@ class ParserTest {
fun check_parser() {
val selector = Selector.parse("View > Text[index>-0]")
println("selector: $selector")
println("canCacheIndex: " + selector.canCacheIndex)
println("canCacheIndex: " + selector.useCache)
}
@ -188,7 +220,7 @@ class ParserTest {
@Test
fun check_regex() {
val source = "[vid~=`(?is)TV.*`]"
val source = "[1<parent.getChild][vid=`im_cover`]"
println("source:$source")
val selector = Selector.parse(source)
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 }
.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")
}
}